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 }