diff --git a/cmd/qrbill-api/api.go b/cmd/qrbill-api/api.go
index 8092987..b1ed56f 100644
--- a/cmd/qrbill-api/api.go
+++ b/cmd/qrbill-api/api.go
@@ -27,6 +27,7 @@ func qrchFromRequest(r *http.Request) *qrbill.QRCH {
CdtrInf: qrbill.QRCHCdtrInf{
IBAN: ifEmpty(r.FormValue("criban"), "CH0209000000870913543"),
Cdtr: qrbill.Address{
+ // Must be structured address e.g. for ZKB mobile banking app
AdrTp: qrbill.AddressTypeStructured,
Name: ifEmpty(r.FormValue("crname"), "Legalize it!"),
StrtNmOrAdrLine1: ifEmpty(r.FormValue("craddr1"), "Quellenstrasse 25"),
@@ -41,6 +42,7 @@ func qrchFromRequest(r *http.Request) *qrbill.QRCH {
Ccy: "CHF",
},
UltmtDbtr: qrbill.Address{
+ // Must be structured address e.g. for ZKB mobile banking app
AdrTp: qrbill.AddressTypeStructured,
Name: ifEmpty(r.FormValue("udname"), "Michael Stapelberg"),
StrtNmOrAdrLine1: ifEmpty(r.FormValue("udaddr1"), "Brahmsstrasse 21"),
@@ -128,7 +130,7 @@ func logic() error {
case "txt":
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
- spew.Fdump(w, qrch.Fill())
+ spew.Fdump(w, qrch.Validate())
case "html":
debugHTML(w, r, prefix, qrch)
diff --git a/cmd/qrbill-api/debughtml.go b/cmd/qrbill-api/debughtml.go
index 3107bc7..8eaccb4 100644
--- a/cmd/qrbill-api/debughtml.go
+++ b/cmd/qrbill-api/debughtml.go
@@ -185,7 +185,7 @@ func debugHTML(w http.ResponseWriter, r *http.Request, prefix string, qrch *qrbi
}
fmt.Fprintf(w, `
input
%s`, spew(qrch))
- fmt.Fprintf(w, `validated
%s`, spew(qrch.Fill()))
+ fmt.Fprintf(w, `validated
%s`, spew(qrch.Validate()))
r.URL.Path = "/qr"
v := r.URL.Query()
diff --git a/qrbill.go b/qrbill.go
index 76d57e2..ea2a73a 100644
--- a/qrbill.go
+++ b/qrbill.go
@@ -6,11 +6,16 @@
//
// https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf (English)
// https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf (German)
+//
+// Note
+//
+// QRR and SCOR references are not yet implemented.
package qrbill
import (
"bytes"
"image"
+ "regexp"
"strings"
"github.com/aaronarduino/goqrsvg"
@@ -25,9 +30,6 @@ import (
// Oriented upon the Swiss Implementation Guidelines for Credit Transfers for
// the ISO 20022 Customer Credit Transfer Initiation message (pain.001).
-// Section 4.2.1: Character set:
-// UTF-8 should be used for encoding
-
// see also:
// https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#317-swissqrcode-iso-20022
@@ -43,6 +45,10 @@ const (
// CodingType is the character set code. Fixed value.
CodingType = "1" // UTF-8 restricted to the Latin character set
+
+ // Trailer is an unambiguous indicator for the end of payment data. Fixed
+ // value.
+ Trailer = "EPD" // End Payment Data
)
// AddressType corresponds to AdrTp in ISO20022.
@@ -53,9 +59,6 @@ const (
AddressTypeCombined = "K"
)
-// - fixed length: 21 alphanumeric characters
-// - only IBANs with CH or LI country code permitted
-
type Address struct {
AdrTp AddressType
Name string // Name, max 70. chars, first name + last name, or company name
@@ -66,6 +69,32 @@ type Address struct {
Ctry string // Country, two-digit country code according to ISO 3166-1
}
+func (a Address) Validate() Address {
+ c := a
+
+ if v := c.Name; len(v) > 70 {
+ c.Name = v[:70]
+ }
+
+ if v := c.StrtNmOrAdrLine1; len(v) > 70 {
+ c.StrtNmOrAdrLine1 = v[:70]
+ }
+
+ if v := c.BldgNbOrAdrLine2; len(v) > 16 {
+ c.BldgNbOrAdrLine2 = v[:16]
+ }
+
+ if v := c.PstCd; len(v) > 16 {
+ c.PstCd = v[:16]
+ }
+
+ if v := c.TwnNm; len(v) > 35 {
+ c.TwnNm = v[:35]
+ }
+
+ return c
+}
+
type QRCHHeader struct {
QRType string
Version string
@@ -102,21 +131,68 @@ type QRCH struct {
RmtInf QRCHRmtInf // Payment reference
}
-func (q *QRCH) Fill() *QRCH {
+var (
+ nonNumericRe = regexp.MustCompile(`[^0-9]`)
+ nonAlphanumericRe = regexp.MustCompile(`[^A-Za-z0-9]`)
+ nonDecimalRe = regexp.MustCompile(`[^0-9.]`)
+)
+
+func (q *QRCH) Validate() *QRCH {
clone := &QRCH{}
*clone = *q
+
+ // Fill in all fixed values:
clone.Header.QRType = QRType
clone.Header.Version = Version
clone.Header.Coding = CodingType
- clone.RmtInf.AddInf.Trailer = "EPD" // TODO: constant
+ clone.RmtInf.AddInf.Trailer = Trailer
+
+ // TODO(spec): strictly speaking, we need to restrict ourselves only to
+ // permitted characters (see below). But, even the example from SIX does not
+ // do that (Monatspr_รค_mie):
+ // https://www.moneytoday.ch/lexikon/qr-rechnung/
+
+ // 4.3.2 Permitted characters
+ // general: only the latin character set is permitted. UTF-8 should be used for encoding
+ // numeric: 0-9
+ // alphanumeric: A-Z a-z 0-9
+ // decimal: 0-9 plus decimal separator .
+
+ // Character Set as per PAIN.001.001.03:
+ // https://businessbanking.bankofireland.com/app/uploads/2018/11/OMI015017-Credit-Transfer-PAIN.001.001.03-DIGITALFINAL-VERSION.pdf
+
+ // a b c d e f g h i j k l m n o p q r s t u v w x y z
+ // A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
+ // / - ? : ( ) . , ' +
+ //
+
+ // Enforce all field constraints:
+
+ clone.CdtrInf.IBAN = nonAlphanumericRe.ReplaceAllString(clone.CdtrInf.IBAN, "")
+
+ clone.CdtrInf.Cdtr = clone.CdtrInf.Cdtr.Validate()
+
+ clone.UltmtCdtr = clone.UltmtCdtr.Validate()
+
+ clone.RmtInf.Tp = nonAlphanumericRe.ReplaceAllString(clone.RmtInf.Tp, "")
+ if v := clone.RmtInf.Tp; len(v) > 4 {
+ clone.RmtInf.Tp = v[:4]
+ }
+
+ clone.RmtInf.Ref = nonAlphanumericRe.ReplaceAllString(clone.RmtInf.Ref, "")
+ if v := clone.RmtInf.Ref; len(v) > 27 {
+ clone.RmtInf.Ref = v[:27]
+ }
+
+ if v := clone.RmtInf.AddInf.Ustrd; len(v) > 140 {
+ clone.RmtInf.AddInf.Ustrd = v[:140]
+ }
+
return clone
}
func (q *QRCH) Encode() (*Bill, error) {
- f := q.Fill()
- //f := q.Fill()
- // TODO: data content must be no more than 997 characters
- // TODO: truncate fields where necessary
+ f := q.Validate()
return &Bill{
qrcontents: strings.Join([]string{
f.Header.QRType,
@@ -155,7 +231,7 @@ func (q *QRCH) Encode() (*Bill, error) {
f.RmtInf.Tp,
f.RmtInf.Ref,
f.RmtInf.AddInf.Ustrd,
- "EPD", // TODO: constant
+ f.RmtInf.AddInf.Trailer,
}, "\n"),
}, nil
}