package payment import ( "bytes" "image/png" "strings" "github.com/boombuler/barcode" "github.com/boombuler/barcode/qr" "github.com/mdp/qrterminal/v3" ) // ToString returns the content of the QR code as string // Use this to then generate the QR code in the form you need func (p *Payment) ToString() (string, error) { if err := p.IsValid(); err != nil { return "", err } fields := []string{ p.ServiceTag, p.VersionString(), p.CharacterSetString(), p.IdentificationCode, p.BICBeneficiaryString(), p.NameBeneficiary, p.IBANBeneficiaryString(), p.EuroAmountString(), p.PurposeString(), p.RemittanceStructured(), p.RemittanceText(), p.B2OInformation, } return strings.Join(fields, "\n"), nil } // ToQRBytes returns an ASCII representation of the QR code // You can print this to the console, save to a file, etc. func (p *Payment) ToQRBytes() ([]byte, error) { var result bytes.Buffer t, err := p.ToString() if err != nil { return nil, err } qrterminal.GenerateHalfBlock(t, qrterminal.M, &result) return result.Bytes(), nil } // ToQRPNG returns an PNG representation of the QR code // You should save this to a file, or pass it to an image processing library func (p *Payment) ToQRPNG(qrSize int) ([]byte, error) { t, err := p.ToString() if err != nil { return nil, err } // Create the barcode qrCode, err := qr.Encode(t, qr.M, qr.Auto) if err != nil { return nil, err } // Scale the barcode to qrSize x qrSize pixels qrCode, err = barcode.Scale(qrCode, qrSize, qrSize) if err != nil { return nil, err } var b bytes.Buffer // encode the barcode as png err = png.Encode(&b, qrCode) return b.Bytes(), err }
package payment import ( "fmt" "strconv" "strings" "github.com/almerlucke/go-iban/iban" ) // See: https://www.europeanpaymentscouncil.eu/document-library/guidance-documents/quick-response-code-guidelines-enable-data-capture-initiation // Payment encapsulates all fields needed to generate the QR code type Payment struct { // ServiceTag should always be BCD ServiceTag string // Version should be v1 or v2 Version int /* 1: UTF-8 5: ISO 8859-5 2: ISO 8859-1 6: ISO 8859-7 3: ISO 8859-2 7: ISO 8859-10 4: ISO 8859-4 8: ISO 8859-15 */ CharacterSet int // IdentificationCode should always be SCT (SEPA Credit Transfer) IdentificationCode string // AT-23 BIC of the Beneficiary Bank [optional in Version 2] // The BIC will continue to be mandatory for SEPA payment transactions involving non-EEA countries. BICBeneficiary string // AT-21 Name of the Beneficiary NameBeneficiary string // AT-20 Account number of the Beneficiary // Only IBAN is allowed. IBANBeneficiary string // AT-04 Amount of the Credit Transfer in Euro [optional] // Amount must be 0.01 or more and 999999999.99 or less EuroAmount float64 // AT-44 Purpose of the Credit Transfer [optional] Purpose string // AT-05 Remittance Information (Structured) [optional] // Creditor Reference (ISO 11649 RFCreditor Reference may be used // *or* // AT-05 Remittance Information (Unstructured) [optional] Remittance string // Beneficiary to originator information [optional] B2OInformation string // Defines whether the Remittance Information is Structured or Unstructured RemittanceIsStructured bool } // NewStructured returns a default Payment with the Structured flag enabled func NewStructured() *Payment { p := New() p.RemittanceIsStructured = true return p } // New returns a new Payment struct with default values for version 2 func New() *Payment { return &Payment{ ServiceTag: "BCD", Version: 2, CharacterSet: 2, IdentificationCode: "SCT", RemittanceIsStructured: false, } } // IBANBeneficiaryString returns the IBAN of the beneficiary in a standardized form func (p *Payment) IBANBeneficiaryString() string { i, err := p.IBAN() if err != nil { return "" } return i.PrintCode } // IBAN returns the parsed IBAN of the beneficiary func (p *Payment) IBAN() (*iban.IBAN, error) { return iban.NewIBAN(p.IBANBeneficiary) } // PurposeString returns the parsed purpose func (p *Payment) PurposeString() string { return strings.ReplaceAll(p.Purpose, " ", "") } // VersionString returns the version converted to a 3-digit number with leading zeros func (p *Payment) VersionString() string { return fmt.Sprintf("%03d", p.Version) } // CharacterSetString returns the character set converted to string func (p *Payment) CharacterSetString() string { return strconv.Itoa(p.CharacterSet) } // EuroAmountString returns the set amount in financial format (eg. EUR12.34) // or an empty string if the amount is 0 func (p *Payment) EuroAmountString() string { return fmt.Sprintf("EUR%.2f", p.EuroAmount) } // RemittanceStructured returns the value for the structured remittance line func (p *Payment) RemittanceStructured() string { return p.RemittanceString(true) } // RemittanceText returns the value for the unstructured (freeform) remittance line func (p *Payment) RemittanceText() string { return p.RemittanceString(false) } // RemittanceString returns the value for the remittance field, independing on being structured func (p *Payment) RemittanceString(structured bool) string { if p.RemittanceIsStructured != structured { return "" } return p.Remittance } // BICBeneficiaryString returns the BIC of the beneficiary, depending on the version of the QR code func (p *Payment) BICBeneficiaryString() string { if p.Version != 1 { return "" } return p.BICBeneficiary }
package payment import ( "errors" "regexp" ) const ( specialChars = `@&+()"':?.,-/` ) var ( stringValidator = regexp.MustCompile(`^[\p{L}\d ` + specialChars + `]+$`) // ErrValidationServiceTag is returned when ServiceTag is not the correct value ErrValidationServiceTag = errors.New("field 'ServiceTag' should be BCD") // ErrValidationCharacterSet is returned when CharacterSet is not in the allowed range ErrValidationCharacterSet = errors.New("field 'CharacterSet' should be 1..8") // ErrValidationVersion is returned when Version is not 1 or 2 ErrValidationVersion = errors.New("field 'Version' should be 1 or 2") // ErrValidationIdentificationCode is returned when IdentificationCode is not the correct value ErrValidationIdentificationCode = errors.New("field 'IdentificationCode' should be SCT") // ErrValidationBICBeneficiary is returned when BICBeneficiary is not set ErrValidationBICBeneficiary = errors.New("field 'BICBeneficiary' is required when version is 1") // ErrValidationEuroAmount is returned when EuroAmount is not a valid amount ErrValidationEuroAmount = errors.New("field 'EuroAmount' must be 0.01 or more and 999999999.99 or less") // ErrValidationPurpose is returned when Purpose is not within bounds ErrValidationPurpose = errors.New("field 'Purpose' should not exceed 4 characters") // ErrValidationRemittanceRequired is returned when Remittance is empty ErrValidationRemittanceRequired = errors.New("field 'Remittance' is required") // ErrValidationRemittanceStructuredTooLong is returned when Remittance is not within bounds for structured field ErrValidationRemittanceStructuredTooLong = errors.New("structured 'Remittance' should not exceed 35 characters") // ErrValidationRemittanceUnstructuredTooLong is returned when Remittance is not within bounds for unstructured field ErrValidationRemittanceUnstructuredTooLong = errors.New("unstructured 'Remittance' should not exceed 140 characters") // ErrValidationRemittanceUnstructuredCharacters is returned when Remittance contains invalid characters ErrValidationRemittanceUnstructuredCharacters = errors.New("unstructured 'Remittance' should only contain alpha-numerics, spaces and/or " + specialChars) // ErrValidationNameBeneficiaryRequired is returned when NameBeneficiary is empty ErrValidationNameBeneficiaryRequired = errors.New("field 'NameBeneficiary' is required") // ErrValidationNameBeneficiaryTooLong is returned when NameBeneficiary is not within bounds ErrValidationNameBeneficiaryTooLong = errors.New("field 'NameBeneficiary' should not exceed 70 characers") // ErrValidationNameBeneficiaryCharacters is returned when NameBeneficiary contains invalid characters ErrValidationNameBeneficiaryCharacters = errors.New("field 'NameBeneficiary' should not only contain alpha-numerics, spaces and/or " + specialChars) ) // IsValid checks if all fields in the payment are consistent and meet the requirements. // It returns the first error it encounters, or nil if all is well. func (p *Payment) IsValid() error { return p.validateFields() } func (p *Payment) validateFields() error { if err := p.validateHeader(); err != nil { return err } if err := p.validateBeneficiary(); err != nil { return err } if p.EuroAmount < 0.01 || p.EuroAmount > 999999999.99 { return ErrValidationEuroAmount } if len(p.PurposeString()) > 4 { return ErrValidationPurpose } return p.validateRemittance() } func (p *Payment) validateHeader() error { if p.ServiceTag != "BCD" { return ErrValidationServiceTag } if p.CharacterSet < 1 || p.CharacterSet > 8 { return ErrValidationCharacterSet } if p.Version != 1 && p.Version != 2 { return ErrValidationVersion } if p.IdentificationCode != "SCT" { return ErrValidationIdentificationCode } if p.Version == 1 && p.BICBeneficiary == "" { return ErrValidationBICBeneficiary } return nil } func (p *Payment) validateRemittance() error { if p.Remittance == "" { return ErrValidationRemittanceRequired } if p.RemittanceIsStructured && len(p.Remittance) > 35 { return ErrValidationRemittanceStructuredTooLong } if !p.RemittanceIsStructured { if len(p.Remittance) > 140 { return ErrValidationRemittanceUnstructuredTooLong } if !stringValidator.MatchString(p.Remittance) { return ErrValidationRemittanceUnstructuredCharacters } } return nil } func (p *Payment) validateBeneficiary() error { if p.NameBeneficiary == "" { return ErrValidationNameBeneficiaryRequired } if len(p.NameBeneficiary) > 70 { return ErrValidationNameBeneficiaryTooLong } if !stringValidator.MatchString(p.NameBeneficiary) { return ErrValidationNameBeneficiaryCharacters } return p.validateIBAN() } func (p *Payment) validateIBAN() error { _, err := p.IBAN() return err }