package main
import (
"fmt"
"github.com/spf13/cobra"
)
//nolint:errcheck
func completionCmd(cmd *cobra.Command) *cobra.Command {
return &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash:
$ source <(%[1]s completion bash)
# To load completions for each session, execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect.
fish:
$ %[1]s completion fish | source
# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, cmd.Root().Name()),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(cmd.OutOrStdout())
case "zsh":
cmd.Root().GenZshCompletion(cmd.OutOrStdout())
case "fish":
cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(cmd.OutOrStdout())
}
},
}
}
package main
import (
"fmt"
"log"
"os"
"github.com/jovandeginste/payme/payment"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// CLI to generate SEPA payment QR codes, either as ASCII or PNG
const qrSize = 300
var (
// gitRef = "0.0.0-dev"
// gitRefType = "local"
gitRefName = "local"
gitCommit = "local"
buildTime = "manually"
)
type qrParams struct {
Payment *payment.Payment
OutputType string
OutputFile string
Debug bool
}
func main() {
q := qrParams{
Payment: payment.New(),
}
cmdRoot, err := newCommand(&q)
if err != nil {
log.Fatal(err)
}
if err := cmdRoot.Execute(); err != nil {
log.Fatal(err)
}
}
func newCommand(q *qrParams) (*cobra.Command, error) {
cmdRoot := &cobra.Command{
Use: "payme",
Version: fmt.Sprintf("%s (%s), built %s\n", gitRefName, gitCommit, buildTime),
Short: "Generate SEPA payment QR code",
Args: cobra.NoArgs,
Run: func(_ *cobra.Command, _ []string) {
q.generate()
},
}
cmdRoot.AddCommand(completionCmd(cmdRoot))
if err := q.init(cmdRoot); err != nil {
return nil, err
}
return cmdRoot, nil
}
func (q *qrParams) init(cmdRoot *cobra.Command) error {
viper.SetEnvPrefix("PAYME")
for _, e := range []string{"name", "bic", "iban"} {
if err := viper.BindEnv(e); err != nil {
return err
}
}
cmdRoot.Flags().StringVar(&q.OutputType, "output", "stdout", "output type: png or stdout")
cmdRoot.Flags().StringVar(&q.OutputFile, "file", "", "write code to file, leave empty for stdout")
cmdRoot.Flags().BoolVar(&q.Debug, "debug", false, "print debug output")
cmdRoot.Flags().IntVar(&q.Payment.CharacterSet, "character-set", 2, "QR code character set")
cmdRoot.Flags().IntVar(&q.Payment.Version, "qr-version", 2, "QR code version")
cmdRoot.Flags().StringVar(&q.Payment.NameBeneficiary, "name", viper.GetString("name"), "Name of the beneficiary")
cmdRoot.Flags().StringVar(&q.Payment.BICBeneficiary, "bic", viper.GetString("bic"), "BIC of the beneficiary")
cmdRoot.Flags().StringVar(&q.Payment.IBANBeneficiary, "iban", viper.GetString("iban"), "IBAN of the beneficiary")
cmdRoot.Flags().Float64Var(&q.Payment.EuroAmount, "amount", 0, "Amount of the transaction")
cmdRoot.Flags().StringVar(&q.Payment.Remittance, "remittance", "", "Remittance (message)")
cmdRoot.Flags().StringVar(&q.Payment.Purpose, "purpose", "", "Purpose of the transaction")
cmdRoot.Flags().BoolVar(&q.Payment.RemittanceIsStructured, "structured", false, "Make the remittance (message) structured")
return nil
}
func (q *qrParams) generate() {
var (
qr []byte
err error
)
if q.Debug {
log.Printf("%#v\n", q)
}
switch q.OutputType {
case "png":
qr, err = q.generateQRPNG()
case "stdout":
qr, err = q.generateQRStdout()
}
if err != nil {
log.Fatal(err)
}
if q.OutputFile == "" {
fmt.Fprintf(os.Stdout, "%s", qr)
return
}
err = os.WriteFile(q.OutputFile, qr, 0o600)
if err != nil {
log.Fatal(err)
}
}
func (q *qrParams) generateQRStdout() ([]byte, error) {
p := q.Payment
if q.Debug {
s, err := p.ToString()
if err != nil {
return nil, err
}
log.Print("Data: ", s)
}
return p.ToQRBytes()
}
func (q *qrParams) generateQRPNG() ([]byte, error) {
p := q.Payment
if q.Debug {
s, err := p.ToString()
if err != nil {
return nil, err
}
log.Print("Data: ", s)
}
return p.ToQRPNG(qrSize)
}
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"
"regexp"
"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
// commonSeparatorChars is a regular expression that matches common separator characters for IBAN
// These characters are removed before the IBAN is validated
var commonSeparatorChars = regexp.MustCompile("[ _-]")
// 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, sanitized IBAN of the beneficiary
func (p *Payment) IBAN() (*iban.IBAN, error) {
s := commonSeparatorChars.ReplaceAllString(p.IBANBeneficiary, "")
return iban.NewIBAN(s)
}
// 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
}