diff --git a/qrbill.go b/qrbill.go index 478dc4b..957bc8f 100644 --- a/qrbill.go +++ b/qrbill.go @@ -28,8 +28,11 @@ package qrbill import ( "bytes" + "fmt" "image" + "log" "regexp" + "strconv" "strings" "github.com/makiuchi-d/gozxing/qrcode/decoder" @@ -128,11 +131,28 @@ func (a QRCHCcyAmt) Validate() QRCHCcyAmt { c := a if c.Amt != "" { + parsed, err := strconv.ParseFloat(c.Amt, 64) + if err != nil { + log.Printf("ParseFloat(%q): %v", c.Amt, err) + } + + // The Swiss Payment Standards 2019 Swiss Implementation Guidelines + // QR-bill Version 2.3 explains: + // + // The amount element is to be entered without leading + // zeroes, including decimal separators and two decimal + // places. + // Decimal, maximum 12-digits permitted, including decimal + // separators. Only decimal points (".") are permitted as + // decimal separators. The amount must be between CHF/ + // EUR 0.01 and 999,999,999.99 + // + // (Notably, the validator is less strict and also permits values + // without decimal separators or with only one decimal place.) + // // Some banking apps are picky regarding integer numbers (e.g. 50) and // require a separator plus two digits (e.g. 50.00). - if !strings.Contains(c.Amt, ".") { - c.Amt += ".00" - } + c.Amt = fmt.Sprintf("%.2f", parsed) } return c diff --git a/qrbill_test.go b/qrbill_test.go new file mode 100644 index 0000000..b00693a --- /dev/null +++ b/qrbill_test.go @@ -0,0 +1,110 @@ +package qrbill_test + +import ( + "testing" + + "github.com/stapelberg/qrbill" +) + +func TestAmountValidation(t *testing.T) { + for _, tt := range []struct { + amount string + wantAmount string + }{ + { + // ensure empty amount values are not modified + amount: "", + wantAmount: "", + }, + + { + amount: "50", + wantAmount: "50.00", + }, + + { + amount: "50.3", + wantAmount: "50.30", + }, + + { + amount: "50.32", + wantAmount: "50.32", + }, + + { + amount: "50.32", + wantAmount: "50.32", + }, + + { + amount: "50.000", + wantAmount: "50.00", + }, + + { + amount: "50.-", + wantAmount: "0.00", // result of invalid input + }, + + { + amount: ".30", + wantAmount: "0.30", + }, + + { + amount: ".3", + wantAmount: "0.30", + }, + + { + // minimum amount mentioned in the Implementation Guidelines + amount: "0.01", + wantAmount: "0.01", + }, + + { + // maximum amount mentioned in the Implementation Guidelines + amount: "999999999.99", + wantAmount: "999999999.99", + }, + } { + t.Run(tt.amount, func(t *testing.T) { + qrch := &qrbill.QRCH{ + CdtrInf: qrbill.QRCHCdtrInf{ + IBAN: "CH0209000000870913543", + Cdtr: qrbill.Address{ + AdrTp: qrbill.AddressTypeCombined, + Name: "Legalize it", + StrtNmOrAdrLine1: "Quellenstrasse 25", + BldgNbOrAdrLine2: "8005 Zürich", + Ctry: "CH", + }, + }, + CcyAmt: qrbill.QRCHCcyAmt{ + Amt: tt.amount, + Ccy: "CHF", + }, + UltmtDbtr: qrbill.Address{ + AdrTp: qrbill.AddressTypeCombined, + Name: "Michael Stapelberg", + StrtNmOrAdrLine1: "Stauffacherstr 42", + BldgNbOrAdrLine2: "8004 Zürich", + Ctry: "CH", + }, + RmtInf: qrbill.QRCHRmtInf{ + Tp: "NON", // Reference type + Ref: "", // Reference + AddInf: qrbill.QRCHRmtInfAddInf{ + Ustrd: "test", + }, + }, + } + + validated := qrch.Validate() + if got, want := validated.CcyAmt.Amt, tt.wantAmount; got != want { + t.Errorf("CcyAmt.Amt = %q, want %q", got, want) + } + }) + } +}