├── .gitignore ├── qr-payment.png ├── .travis.yml ├── Makefile ├── codecov.yml ├── go.mod ├── base ├── utils.go ├── utils_test.go ├── payment_test.go └── payment.go ├── example └── main.go ├── LICENSE ├── go.sum ├── payment_test.go ├── epc ├── epc.go └── epc_test.go ├── README.md ├── payment.go └── spayd ├── spayd_test.go └── spayd.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /coverage.txt 3 | 4 | -------------------------------------------------------------------------------- /qr-payment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dundee/qrpay/HEAD/qr-payment.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | 6 | env: 7 | - GO111MODULE=on 8 | script: 9 | - make coverage 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | go test ./... 4 | 5 | coverage: 6 | go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 7 | 8 | coverage-html: coverage 9 | go tool cover -html=coverage.txt -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 2% 7 | informational: true 8 | patch: 9 | default: 10 | informational: true -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dundee/qrpay 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/jbub/banking v0.6.0 7 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 8 | github.com/stretchr/testify v1.7.0 9 | ) 10 | -------------------------------------------------------------------------------- /base/utils.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | // TrimToLength shortends given string to given length 4 | func TrimToLength(value string, length int) string { 5 | if len(value) > length { 6 | return value[:length] 7 | } 8 | return value 9 | } 10 | -------------------------------------------------------------------------------- /base/utils_test.go: -------------------------------------------------------------------------------- 1 | package base_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dundee/qrpay/base" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTrimToLength(t *testing.T) { 11 | s := base.TrimToLength("12345", 3) 12 | assert.Equal(t, "123", s) 13 | 14 | s = base.TrimToLength("321", 5) 15 | assert.Equal(t, "321", s) 16 | } 17 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | payment "github.com/dundee/qrpay" 8 | ) 9 | 10 | func main() { 11 | p := payment.NewSpaydPayment() 12 | 13 | p.SetIBAN("CZ5855000000001265098001") 14 | p.SetAmount("100") 15 | p.SetDate(time.Date(2021, 12, 24, 0, 0, 0, 0, time.UTC)) 16 | p.SetMessage("M") 17 | p.SetRecipientName("go") 18 | p.SetNofificationType('E') 19 | p.SetNotificationValue("daniel@milde.cz") 20 | p.SetExtendedAttribute("vs", "1234567890") 21 | 22 | if err := payment.SaveQRCodeImageToFile(p, "qr-payment.png"); err != nil { 23 | fmt.Printf("could not generate payment QR code: %v", err) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daniel Milde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /base/payment_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestWrongIban(t *testing.T) { 10 | p := NewPayment() 11 | err := p.SetIBAN("111") 12 | assert.Equal(t, "iban: iban too short", err.Error()) 13 | errors := p.GetErrors() 14 | assert.Equal(t, "iban: iban too short", errors["iban"].Error()) 15 | } 16 | 17 | func TestWrongBic(t *testing.T) { 18 | p := NewPayment() 19 | err := p.SetBIC("111") 20 | assert.Equal(t, "swift: invalid length", err.Error()) 21 | errors := p.GetErrors() 22 | assert.Equal(t, "swift: invalid length", errors["bic"].Error()) 23 | } 24 | 25 | func TestMethods(t *testing.T) { 26 | p := NewPayment() 27 | p.SetIBAN("CZ5855000000001265098001") 28 | p.SetBIC("BHBLDEHHXXX") 29 | p.SetCurrency("EUR") 30 | p.SetAmount("10.8") 31 | p.SetMessage("M") 32 | p.SetRecipientName("go") 33 | p.SetSenderReference("RF:111") 34 | 35 | assert.Equal(t, "CZ5855000000001265098001", p.IBAN) 36 | assert.Equal(t, "BHBLDEHHXXX", p.BIC) 37 | assert.Equal(t, "EUR", p.Currency) 38 | assert.Equal(t, "10.8", p.Amount) 39 | assert.Equal(t, "M", p.Msg) 40 | assert.Equal(t, "go", p.Recipient) 41 | assert.Equal(t, "RF:111", p.Reference) 42 | } 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/jbub/banking v0.6.0 h1:JN7FZ30+w9b0kkUsE3NHRd2XZcdLrzaxVLSCxttE0o8= 4 | github.com/jbub/banking v0.6.0/go.mod h1:19/mEIFSyT8JLWERPDWJe/PH0MSA9kO5h1mN065IXMA= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 8 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 11 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /base/payment.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/jbub/banking/iban" 5 | "github.com/jbub/banking/swift" 6 | ) 7 | 8 | type Payment struct { 9 | IBAN string 10 | BIC string 11 | Amount string 12 | Currency string 13 | Reference string 14 | Recipient string 15 | Msg string 16 | Errors map[string]error 17 | } 18 | 19 | func NewPayment() *Payment { 20 | return &Payment{ 21 | Errors: make(map[string]error), 22 | } 23 | } 24 | 25 | func (p *Payment) SetIBAN(value string) error { 26 | _, err := iban.Parse(value) 27 | if err != nil { 28 | p.Errors["iban"] = err 29 | } else { 30 | p.IBAN = value 31 | } 32 | return err 33 | } 34 | 35 | func (p *Payment) SetBIC(value string) error { 36 | _, err := swift.Parse(value) 37 | if err != nil { 38 | p.Errors["bic"] = err 39 | } else { 40 | p.BIC = value 41 | } 42 | return err 43 | } 44 | 45 | func (p *Payment) SetAmount(value string) error { 46 | p.Amount = value 47 | return nil 48 | } 49 | 50 | func (p *Payment) SetCurrency(value string) error { 51 | p.Currency = value 52 | return nil 53 | } 54 | 55 | func (p *Payment) SetSenderReference(value string) error { 56 | p.Reference = value 57 | return nil 58 | } 59 | 60 | func (p *Payment) SetRecipientName(value string) error { 61 | p.Recipient = value 62 | return nil 63 | } 64 | 65 | func (p *Payment) SetMessage(value string) error { 66 | p.Msg = value 67 | return nil 68 | } 69 | 70 | func (p *Payment) GetErrors() map[string]error { 71 | return p.Errors 72 | } 73 | -------------------------------------------------------------------------------- /payment_test.go: -------------------------------------------------------------------------------- 1 | package qrpay_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/dundee/qrpay" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetQRCodeImage(t *testing.T) { 12 | p := qrpay.NewSpaydPayment() 13 | p.SetIBAN("CZ5855000000001265098001") 14 | 15 | b, _ := qrpay.GetQRCodeImage(p) 16 | 17 | // check start of PNG image 18 | assert.True(t, len(b) > 10) 19 | assert.Equal(t, uint8(0x89), b[0]) 20 | assert.Equal(t, uint8(0x50), b[1]) 21 | } 22 | 23 | func TestGetQRCodeImageWithErr(t *testing.T) { 24 | p := qrpay.NewSpaydPayment() 25 | 26 | b, err := qrpay.GetQRCodeImage(p) 27 | 28 | assert.Nil(t, b) 29 | assert.Equal(t, "IBAN is mandatory", err.Error()) 30 | } 31 | 32 | func TestSaveQRCode(t *testing.T) { 33 | path := "qr.jpeg" 34 | defer os.Remove(path) 35 | 36 | p := qrpay.NewSpaydPayment() 37 | p.SetIBAN("CZ5855000000001265098001") 38 | 39 | qrpay.SaveQRCodeImageToFile(p, path) 40 | 41 | assert.FileExists(t, path) 42 | } 43 | 44 | func TestSaveEpcQRCode(t *testing.T) { 45 | path := "qr.jpeg" 46 | defer os.Remove(path) 47 | 48 | p := qrpay.NewEpcPayment() 49 | p.SetRecipientName("Red Cross") 50 | p.SetIBAN("CZ5855000000001265098001") 51 | 52 | qrpay.SaveQRCodeImageToFile(p, path) 53 | 54 | assert.FileExists(t, path) 55 | } 56 | 57 | func TestSaveQRCodeWithErr(t *testing.T) { 58 | path := "qr.jpeg" 59 | p := qrpay.NewSpaydPayment() 60 | err := qrpay.SaveQRCodeImageToFile(p, path) 61 | 62 | assert.Equal(t, "IBAN is mandatory", err.Error()) 63 | } 64 | -------------------------------------------------------------------------------- /epc/epc.go: -------------------------------------------------------------------------------- 1 | package epc 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/dundee/qrpay/base" 8 | ) 9 | 10 | const EpcHeader = `BCD 11 | 002 12 | 1 13 | SCT 14 | ` 15 | 16 | type EpcPayment struct { 17 | *base.Payment 18 | Purpose string 19 | } 20 | 21 | func NewEpcPayment() *EpcPayment { 22 | return &EpcPayment{ 23 | Payment: &base.Payment{ 24 | Errors: make(map[string]error), 25 | }, 26 | } 27 | } 28 | 29 | func (p *EpcPayment) SetPurpose(value string) { 30 | p.Purpose = value 31 | } 32 | 33 | func (p *EpcPayment) GenerateString() (string, error) { 34 | res := strings.Builder{} 35 | res.WriteString(EpcHeader) 36 | 37 | if p.BIC != "" { 38 | res.WriteString(base.TrimToLength(p.BIC, 11)) 39 | } 40 | res.WriteString("\n") 41 | 42 | if p.Recipient == "" { 43 | return "", errors.New("name of the beneficiary is mandatory") 44 | } 45 | res.WriteString(base.TrimToLength(p.Recipient, 70) + "\n") 46 | 47 | if p.IBAN == "" { 48 | return "", errors.New("IBAN is mandatory") 49 | } 50 | res.WriteString(base.TrimToLength(p.IBAN, 34) + "\n") 51 | 52 | if p.Amount != "" { 53 | if p.Currency != "" { 54 | res.WriteString(base.TrimToLength(p.Currency, 3)) 55 | } 56 | res.WriteString(base.TrimToLength(p.Amount, 12)) 57 | } 58 | res.WriteString("\n") 59 | 60 | if p.Purpose != "" { 61 | res.WriteString(base.TrimToLength(p.Purpose, 4)) 62 | } 63 | res.WriteString("\n") 64 | 65 | if p.Reference != "" { 66 | res.WriteString(base.TrimToLength(p.Reference, 140)) 67 | } 68 | res.WriteString("\n\n") 69 | 70 | if p.Msg != "" { 71 | res.WriteString(base.TrimToLength(p.Msg, 70)) 72 | } 73 | 74 | return strings.TrimSpace(res.String()), nil 75 | } 76 | -------------------------------------------------------------------------------- /epc/epc_test.go: -------------------------------------------------------------------------------- 1 | package epc_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dundee/qrpay/epc" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSettingLongIBAN(t *testing.T) { 11 | p := epc.NewEpcPayment() 12 | p.SetRecipientName("XX") 13 | p.IBAN = "12345678901234567890123456789012345678901234567890" // 50 chars 14 | 15 | s, _ := p.GenerateString() 16 | // trimmed to 46 chars 17 | assert.Equal(t, "BCD\n002\n1\nSCT\n\nXX\n1234567890123456789012345678901234", s) 18 | } 19 | 20 | func TestGenerateString(t *testing.T) { 21 | p := epc.NewEpcPayment() 22 | p.SetRecipientName("Red Cross") 23 | p.SetIBAN("CZ5855000000001265098001") 24 | p.SetBIC("RZBCCZPP") 25 | p.SetAmount("100.0") 26 | 27 | s, _ := p.GenerateString() 28 | assert.Equal(t, "BCD\n002\n1\nSCT\nRZBCCZPP\nRed Cross\nCZ5855000000001265098001\n100.0", s) 29 | } 30 | 31 | func TestDEPayment(t *testing.T) { 32 | p := epc.NewEpcPayment() 33 | p.SetRecipientName("Franz Mustermänn") 34 | p.SetIBAN("DE71110220330123456789") 35 | p.SetBIC("BHBLDEHHXXX") 36 | p.SetCurrency("EUR") 37 | p.SetAmount("10.8") 38 | 39 | s, _ := p.GenerateString() 40 | assert.Equal(t, "BCD\n002\n1\nSCT\nBHBLDEHHXXX\nFranz Mustermänn\nDE71110220330123456789\nEUR10.8", s) 41 | } 42 | 43 | func TestOtherParams(t *testing.T) { 44 | p := epc.NewEpcPayment() 45 | p.SetIBAN("CZ5855000000001265098001") 46 | p.SetSenderReference("111111") 47 | p.SetMessage("M") 48 | p.SetRecipientName("go") 49 | p.SetPurpose("GDDS") 50 | 51 | s, _ := p.GenerateString() 52 | assert.Equal( 53 | t, 54 | "BCD\n002\n1\nSCT\n\ngo\nCZ5855000000001265098001\n\nGDDS\n111111\n\nM", 55 | s, 56 | ) 57 | } 58 | 59 | func TestGenerateStringWithoutIBAN(t *testing.T) { 60 | p := epc.NewEpcPayment() 61 | p.SetRecipientName("xxx") 62 | s, err := p.GenerateString() 63 | assert.Equal(t, "", s) 64 | assert.Equal(t, "IBAN is mandatory", err.Error()) 65 | } 66 | 67 | func TestGenerateStringWithoutRecipient(t *testing.T) { 68 | p := epc.NewEpcPayment() 69 | s, err := p.GenerateString() 70 | assert.Equal(t, "", s) 71 | assert.Equal(t, "name of the beneficiary is mandatory", err.Error()) 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QR code for payment 2 | 3 | # Payment QR code for Go 4 | 5 | [![Build Status](https://travis-ci.com/dundee/qrpay.svg?branch=master)](https://travis-ci.com/dundee/qrpay) 6 | [![codecov](https://codecov.io/gh/dundee/qrpay/branch/master/graph/badge.svg)](https://codecov.io/gh/dundee/qrpay) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/dundee/qrpay)](https://goreportcard.com/report/github.com/dundee/qrpay) 8 | [![Maintainability](https://api.codeclimate.com/v1/badges/6b488832f724e32e0c6e/maintainability)](https://codeclimate.com/github/dundee/qrpay/maintainability) 9 | [![CodeScene Code Health](https://codescene.io/projects/14391/status-badges/code-health)](https://codescene.io/projects/14391) 10 | 11 | Golang library for creating QR codes for payments. 12 | 13 | [Short Payment Descriptor](https://en.wikipedia.org/wiki/Short_Payment_Descriptor) format and 14 | [EPC QR Code](https://en.wikipedia.org/wiki/EPC_QR_code) (SEPA) format is supported. 15 | 16 | ## Installation 17 | 18 | go get -u github.com/dundee/qrpay 19 | 20 | ## Usage 21 | 22 | ### Generating QR code image for Short Payment Descriptor format 23 | 24 | ```Go 25 | import "github.com/dundee/qrpay" 26 | 27 | p := qrpay.NewSpaydPayment() 28 | p.SetIBAN("CZ5855000000001265098001") 29 | p.SetAmount("10.8") 30 | p.SetDate(time.Date(2021, 12, 24, 0, 0, 0, 0, time.UTC)) 31 | p.SetMessage("M") 32 | p.SetRecipientName("go") 33 | p.SetNofificationType('E') 34 | p.SetNotificationValue("daniel@milde.cz") 35 | p.SetExtendedAttribute("vs", "1234567890") 36 | 37 | qrpay.SaveQRCodeImageToFile(p, "qr-payment.png") 38 | ``` 39 | 40 | ### Generating QR code image for EPC QR Code 41 | 42 | ```Go 43 | import "github.com/dundee/qrpay" 44 | 45 | p := qrpay.NewEpcPayment() 46 | p.SetIBAN("CZ5855000000001265098001") 47 | p.SetAmount("10.8") 48 | p.SetMessage("M") 49 | p.SetRecipientName("go") 50 | 51 | qrpay.SaveQRCodeImageToFile(p, "qr-payment.png") 52 | ``` 53 | 54 | QR code image encoding uses [skip2/go-qrcode](https://github.com/skip2/go-qrcode). 55 | 56 | ### Getting QR code content for Short Payment Descriptor format 57 | 58 | ```Go 59 | import "github.com/dundee/qrpay" 60 | 61 | p := qrpay.NewSpaydPayment() 62 | p.SetIBAN("CZ5855000000001265098001") 63 | p.SetAmount("108") 64 | 65 | fmt.Println(qrpay.GenerateString()) 66 | // Output: SPD*1.0*ACC:CZ5855000000001265098001*AM:108* 67 | ``` -------------------------------------------------------------------------------- /payment.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package for creating QR codes for payments. 3 | 4 | Short Payment Descriptor format and EPC QR Code (SEPA) format is supported. 5 | 6 | - Generating QR code image for Short Payment Descriptor format 7 | 8 | import "github.com/dundee/qrpay" 9 | 10 | p := qrpay.NewSpaydPayment() 11 | p.SetIBAN("CZ5855000000001265098001") 12 | p.SetAmount("10.8") 13 | p.SetDate(time.Date(2021, 12, 24, 0, 0, 0, 0, time.UTC)) 14 | p.SetMessage("M") 15 | p.SetRecipientName("go") 16 | p.SetNofificationType('E') 17 | p.SetNotificationValue("daniel@milde.cz") 18 | p.SetExtendedAttribute("vs", "1234567890") 19 | 20 | qrpay.SaveQRCodeImageToFile(p, "qr-payment.png") 21 | 22 | - Generating QR code image for EPC QR Code 23 | 24 | import "github.com/dundee/qrpay" 25 | 26 | p := qrpay.NewEpcPayment() 27 | p.SetIBAN("CZ5855000000001265098001") 28 | p.SetAmount("10.8") 29 | p.SetMessage("M") 30 | p.SetRecipientName("go") 31 | 32 | qrpay.SaveQRCodeImageToFile(p, "qr-payment.png") 33 | 34 | QR code image encoding uses https://github.com/skip2/go-qrcode 35 | 36 | - Getting QR code content for Short Payment Descriptor format 37 | 38 | import "github.com/dundee/qrpay" 39 | 40 | p := qrpay.NewSpaydPayment() 41 | p.SetIBAN("CZ5855000000001265098001") 42 | p.SetAmount("108") 43 | 44 | fmt.Println(qrpay.GenerateString()) 45 | // Output: SPD*1.0*ACC:CZ5855000000001265098001*AM:108* 46 | 47 | */ 48 | package qrpay 49 | 50 | import ( 51 | "github.com/dundee/qrpay/epc" 52 | "github.com/dundee/qrpay/spayd" 53 | 54 | qrcode "github.com/skip2/go-qrcode" 55 | ) 56 | 57 | type Payment interface { 58 | SetIBAN(string) error 59 | SetBIC(string) error 60 | SetAmount(value string) error 61 | SetCurrency(value string) error 62 | SetSenderReference(value string) error 63 | SetRecipientName(value string) error 64 | SetMessage(value string) error 65 | 66 | GetErrors() map[string]error 67 | GenerateString() (string, error) 68 | } 69 | 70 | func NewSpaydPayment() *spayd.SpaydPayment { 71 | return spayd.NewSpaydPayment() 72 | } 73 | 74 | func NewEpcPayment() *epc.EpcPayment { 75 | return epc.NewEpcPayment() 76 | } 77 | 78 | func SaveQRCodeImageToFile(payment Payment, path string) error { 79 | content, err := payment.GenerateString() 80 | if err != nil { 81 | return err 82 | } 83 | return qrcode.WriteFile(content, qrcode.Medium, 400, path) 84 | } 85 | 86 | func GetQRCodeImage(payment Payment) ([]byte, error) { 87 | content, err := payment.GenerateString() 88 | if err != nil { 89 | return nil, err 90 | } 91 | return qrcode.Encode(content, qrcode.Medium, 400) 92 | } 93 | 94 | // check validity 95 | var _ Payment = (*spayd.SpaydPayment)(nil) 96 | var _ Payment = (*epc.EpcPayment)(nil) 97 | -------------------------------------------------------------------------------- /spayd/spayd_test.go: -------------------------------------------------------------------------------- 1 | package spayd_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/dundee/qrpay/spayd" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSettingLongAcc(t *testing.T) { 13 | p := spayd.NewSpaydPayment() 14 | p.IBAN = "12345678901234567890123456789012345678901234567890" // 50 chars 15 | 16 | s, _ := p.GenerateString() 17 | // trimmed to 46 chars 18 | assert.Equal(t, "SPD*1.0*ACC:1234567890123456789012345678901234567890123456*", s) 19 | } 20 | 21 | func ExampleSpaydPayment_GenerateString() { 22 | p := spayd.NewSpaydPayment() 23 | p.SetIBAN("CZ5855000000001265098001") 24 | p.SetBIC("RZBCCZPP") 25 | p.SetAmount("100.0") 26 | 27 | s, _ := p.GenerateString() 28 | fmt.Println(s) 29 | // Output: SPD*1.0*ACC:CZ5855000000001265098001+RZBCCZPP*AM:100.0* 30 | } 31 | 32 | func TestDEPayment(t *testing.T) { 33 | p := spayd.NewSpaydPayment() 34 | p.SetIBAN("DE71110220330123456789") 35 | p.SetBIC("BHBLDEHHXXX") 36 | p.SetCurrency("EUR") 37 | p.SetAmount("10.8") 38 | 39 | s, _ := p.GenerateString() 40 | assert.Equal(t, "SPD*1.0*ACC:DE71110220330123456789+BHBLDEHHXXX*AM:10.8*CC:EUR*", s) 41 | } 42 | 43 | func TestOtherParams(t *testing.T) { 44 | p := spayd.NewSpaydPayment() 45 | p.SetIBAN("CZ5855000000001265098001") 46 | p.SetDate(time.Date(2021, 12, 24, 0, 0, 0, 0, time.UTC)) 47 | p.SetPaymentType("P2P") 48 | p.SetSenderReference("111") 49 | p.SetMessage("M") 50 | p.SetRecipientName("go") 51 | p.SetNofificationType('E') 52 | p.SetNotificationValue("daniel@milde.cz") 53 | 54 | s, _ := p.GenerateString() 55 | assert.Equal( 56 | t, 57 | "SPD*1.0*ACC:CZ5855000000001265098001*RF:111*RN:GO*DT:20211224*MSG:M*PT:P2P*NT:E*NT:DANIEL@MILDE.CZ*", 58 | s, 59 | ) 60 | } 61 | 62 | func TestExtended(t *testing.T) { 63 | p := spayd.NewSpaydPayment() 64 | p.SetIBAN("CZ5855000000001265098001") 65 | p.SetExtendedAttribute("vs", "1234567890") 66 | 67 | s, _ := p.GenerateString() 68 | assert.Equal(t, "SPD*1.0*ACC:CZ5855000000001265098001*X-VS:1234567890*", s) 69 | } 70 | 71 | func TestGenerateStringWithourIBAN(t *testing.T) { 72 | p := spayd.NewSpaydPayment() 73 | s, err := p.GenerateString() 74 | assert.Equal(t, "", s) 75 | assert.Equal(t, "IBAN is mandatory", err.Error()) 76 | } 77 | 78 | func TestWrongNotificationType(t *testing.T) { 79 | defer func() { 80 | err := recover() 81 | assert.NotNil(t, err) 82 | assert.Equal(t, "nofification type 'X' is not supported (E | P)", err) 83 | }() 84 | 85 | p := spayd.NewSpaydPayment() 86 | p.SetNofificationType('X') 87 | } 88 | 89 | func TestAsteriskInValue(t *testing.T) { 90 | p := spayd.NewSpaydPayment() 91 | p.SetIBAN("CZ5855000000001265098001") 92 | p.SetMessage("aaa*bbb") 93 | 94 | s, err := p.GenerateString() 95 | assert.Nil(t, err) 96 | assert.Equal(t, "SPD*1.0*ACC:CZ5855000000001265098001*MSG:AAA%2ABBB*", s) 97 | } 98 | -------------------------------------------------------------------------------- /spayd/spayd.go: -------------------------------------------------------------------------------- 1 | package spayd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "github.com/dundee/qrpay/base" 11 | ) 12 | 13 | const SpaydHeader = "SPD*1.0*" 14 | 15 | type SpaydPayment struct { 16 | *base.Payment 17 | Date time.Time 18 | PaymentType string 19 | NotifType rune 20 | NotifValue string 21 | Extended map[string]string 22 | } 23 | 24 | func NewSpaydPayment() *SpaydPayment { 25 | return &SpaydPayment{ 26 | Payment: &base.Payment{ 27 | Errors: make(map[string]error), 28 | }, 29 | Extended: make(map[string]string), 30 | } 31 | } 32 | 33 | func (s *SpaydPayment) SetDate(value time.Time) { 34 | s.Date = value 35 | } 36 | 37 | func (s *SpaydPayment) SetPaymentType(value string) { 38 | s.PaymentType = value 39 | } 40 | 41 | func (s *SpaydPayment) SetNofificationType(value rune) { 42 | if value != 'P' && value != 'E' { 43 | panic("nofification type '" + string(value) + "' is not supported (E | P)") 44 | } 45 | s.NotifType = value 46 | } 47 | 48 | func (s *SpaydPayment) SetExtendedAttribute(name string, value string) { 49 | s.Extended[name] = value 50 | } 51 | 52 | func (s *SpaydPayment) SetNotificationValue(value string) { 53 | s.NotifValue = value 54 | } 55 | 56 | func (s *SpaydPayment) GenerateString() (string, error) { 57 | res := strings.Builder{} 58 | res.WriteString(SpaydHeader) 59 | 60 | if s.IBAN == "" { 61 | return "", errors.New("IBAN is mandatory") 62 | } 63 | 64 | acc := s.IBAN 65 | if s.BIC != "" { 66 | acc += "+" + s.BIC 67 | } 68 | res.WriteString("ACC:" + base.TrimToLength(convertValue(acc), 46) + "*") 69 | 70 | if s.Amount != "" { 71 | res.WriteString("AM:" + base.TrimToLength(convertValue(s.Amount), 10) + "*") 72 | } 73 | 74 | if s.Currency != "" { 75 | res.WriteString("CC:" + base.TrimToLength(convertValue(s.Currency), 3) + "*") 76 | } 77 | 78 | if s.Reference != "" { 79 | res.WriteString("RF:" + base.TrimToLength(convertValue(s.Reference), 16) + "*") 80 | } 81 | 82 | if s.Recipient != "" { 83 | res.WriteString("RN:" + base.TrimToLength(convertValue(s.Recipient), 35) + "*") 84 | } 85 | 86 | if s.Date.Year() > 1 { 87 | year, month, day := s.Date.Date() 88 | res.WriteString(fmt.Sprintf("DT:%4d%d%d*", year, month, day)) 89 | } 90 | 91 | if s.Msg != "" { 92 | res.WriteString("MSG:" + base.TrimToLength(convertValue(s.Msg), 60) + "*") 93 | } 94 | 95 | if s.PaymentType != "" { 96 | res.WriteString("PT:" + base.TrimToLength(convertValue(s.PaymentType), 3) + "*") 97 | } 98 | 99 | if s.NotifType > 0 { 100 | res.WriteString("NT:" + string(s.NotifType) + "*") 101 | } 102 | if s.NotifValue != "" { 103 | res.WriteString("NT:" + base.TrimToLength(convertValue(s.NotifValue), 320) + "*") 104 | } 105 | 106 | for name := range s.Extended { 107 | res.WriteString("X-" + convertValue(name) + ":" + convertValue(s.Extended[name]) + "*") 108 | } 109 | 110 | return res.String(), nil 111 | } 112 | 113 | func convertValue(value string) string { 114 | value = strings.ToUpper(value) 115 | value = url.PathEscape(value) 116 | return value 117 | } 118 | --------------------------------------------------------------------------------