diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..5754a73 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,12 @@ +version: "2" +linters: + default: all + disable: + - wsl + - depguard + - varnamelen + - wrapcheck + - wsl_v5 + - exhaustruct + - gochecknoglobals + - nlreturn diff --git a/fatQR.go b/fatQR.go index 19d94dd..a774de1 100644 --- a/fatQR.go +++ b/fatQR.go @@ -1,3 +1,7 @@ +// Package gofatqr offers a way to parse the QR code string from a portuguese +// invoice. It's up to date with the tech specs +// 'Portaria nº 195/2025 Versão 1.1 Outubro 2020' +// https://info.portaldasfinancas.gov.pt/pt/apoio_contribuinte/Novas_regras_faturacao/Documents/Especificacoes_Tecnicas_Codigo_QR.pdf package gofatqr // TODO @@ -14,9 +18,11 @@ import ( dec "github.com/shopspring/decimal" ) +// ScanMode defines the type used to configure stricter scanning and parsing of +// the code. type ScanMode uint32 -// Setting this up for the future +// Setting this up for the future. const ( Strict ScanMode = 1 << iota NifValidation @@ -27,6 +33,7 @@ const ( separator = "*" ) +// FiscalSpace Represents the each of the fiscal zones the invoice can have. type FiscalSpace struct { TaxCountryRegion string `json:"TaxCountryRegion"` // 1 TaxableBase *dec.Decimal `json:"TaxableBase"` // 2 @@ -38,6 +45,7 @@ type FiscalSpace struct { VatTotalNormalBase *dec.Decimal `json:"VatTotalNormalBase"` // 8 } +// FatQR represents the data from the QR code of an invoice. type FatQR struct { TaxRegistrationNumber string `json:"TaxRegistrationNumber"` // A CustomerTaxID string `json:"CustomerTaxID"` // B @@ -60,7 +68,7 @@ type FatQR struct { OtherInfo string `json:"OtherInfo"` // S } -type FieldCodec struct { +type fieldCodec struct { Required bool Parse func(f *FatQR, val string) error String func(f *FatQR) string @@ -77,7 +85,7 @@ var ( } ) -var fatQRFieldMap = map[string]FieldCodec{ +var fatQRFieldMap = map[string]fieldCodec{ "A": { Required: true, Parse: func(f *FatQR, val string) error { @@ -191,8 +199,7 @@ var fatQRFieldMap = map[string]FieldCodec{ "H": { Required: true, Parse: func(f *FatQR, val string) error { - // TODO - // Add some ATCUD validation + // TODO - add some ATCUD validation f.ATCUD = val return nil }, @@ -604,6 +611,10 @@ func parseDecimal(f **dec.Decimal, val string) error { if *f == nil { *f = new(dec.Decimal) } + + // Because software #2426 doesn't follow the spec correctly + val = strings.ReplaceAll(val, ",", ".") + return (*f).Scan(val) } @@ -611,15 +622,51 @@ func stringDecimal(d *dec.Decimal) string { return d.Truncate(decCount).StringFixed(decCount) } +// ToJSON returns a []byte with the FatQR data as JSON. func (fq *FatQR) ToJSON() []byte { jsondata, _ := json.Marshal(fq) return jsondata } +// FromJSON scans the data from a JSON []byte. func (fq *FatQR) FromJSON(data []byte) error { return json.Unmarshal(data, fq) } +// Scan scans a given string and applies the rules of `mode` (to be defined). +// +// returns an error if it fails to scan the given string. +func (fq *FatQR) Scan(s string, _ ScanMode) error { + parts := strings.Split(s, "*") + + err := fq.scanParts(parts) + if err != nil { + return err + } + + return nil +} + +func (fq *FatQR) String() string { + parts := make([]string, 0, len(fieldOrder)) + + for _, key := range fieldOrder { + codec, ok := fatQRFieldMap[key] + if !ok { + panic("invalid key in order") + } + + // TODO + // Using the flags I have to add, use it to enforce or not obligatory fields + // For now we are only going to stringify the data we have + if !codec.Empty(fq) { + parts = append(parts, codec.String(fq)) + } + } + + return strings.Join(parts, separator) +} + func (fq *FatQR) scanPart(part []string) error { val, ok := fatQRFieldMap[part[0]] if !ok { @@ -649,35 +696,3 @@ func (fq *FatQR) scanParts(parts []string) error { return nil } - -// Scan scans a given string and applies the rules of `mode` (to be defined) -func (fq *FatQR) Scan(s string, mode ScanMode) error { - parts := strings.Split(s, "*") - - err := fq.scanParts(parts) - if err != nil { - return err - } - - return nil -} - -func (fq *FatQR) String() string { - parts := make([]string, 0, len(fieldOrder)) - - for _, key := range fieldOrder { - codec, ok := fatQRFieldMap[key] - if !ok { - panic("invalid key in order") - } - - // TODO - // Using the flags I have to add, use it to enforce or not obligatory fields - // For now we are only going to stringify the data we have - if !codec.Empty(fq) { - parts = append(parts, codec.String(fq)) - } - } - - return strings.Join(parts, separator) -} diff --git a/fatQR_test.go b/fatQR_test.go index af44508..900ea27 100644 --- a/fatQR_test.go +++ b/fatQR_test.go @@ -11,14 +11,17 @@ import ( func TestScan(t *testing.T) { testCases := []struct { - name string - input string + name string + input string + output string // set output empty if its supposed to be equal to input + // once again, thanks to the fucking guys at 2426 }{ { name: "5.0. Example 0 - Some invoice I had laying around", input: "A:513847588*B:132772985*C:PT*D:FS*E:N*F:20250828*" + "G:FS 437251001/115666*H:0*I1:PT*I7:37.94*I8:8.73*" + "N:8.73*O:46.67*Q:mWUp*R:0177", + output: "", }, { name: "5.1. Example 1 - Invoice", @@ -30,12 +33,14 @@ func TestScan(t *testing.T) { "K5:25000.00*K6:3000.00*K7:40000.00*K8:8800.00*L:100.00*M:25.00*" + "N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" + "S:TB;PT00000000000000000000000;513500.58", + output: "", }, { name: "5.2. Example 2 - Simplified Invoice", input: "A:123456789*B:999999990*C:PT*D:FS*E:N*F:20190812*" + "G:FS CDVF/12345*H:CDF7T5HD-12345*I1:PT*I7:0.65*I8:0.15*N:0.15*O:0.80*" + "Q:YhGV*R:9999*S:NU;0.80", + output: "", }, { name: "5.3. Example 3 - Pro-form Invoice", @@ -43,29 +48,54 @@ func TestScan(t *testing.T) { "G:PF G2019CB/145789*H:HB6FT7RV-145789*I1:PT*I2:12345.34*I3:12532.65*" + "I4:751.96*I5:52789.00*I6:6862.57*I7:32425.69*I8:7457.91*N:15072.44*" + "O:125165.12*Q:r/fY*R:9999", + output: "", }, { name: "5.4. Example 4 - Transport Document", input: "A:500000000*B:123456789*C:PT*D:GT*E:N*F:20190720*" + "G:GT G234CB/50987*H:GTVX4Y8B-50987*I1:0*N:0.00*O:0.00*Q:5uIg*R:9999", + output: "", }, { name: "5.5. Example 5 - Invoice with foreign tax", input: "A:123456789*B:4443332215*C:FR*D:FT*E:N*F:20190526*" + "G:ABC BNH/4561*H:DK5ZJ2HN-4561*I1:FR*I7:100.00*I8:20.00*N:20.00*" + "O:120.00*Q:YJRE*R:9999", + output: "", }, { name: "5.6. Example 6 - Tax Rectification Debit Note", input: "A:123456789*B:500000000*C:PT*D:ND*E:N*F:20190216*" + "G:M1F KLG/6145*H: RQD8L6DG-6145*I1:PT-MA*I6:26.50*N:26.50*O:26.50*" + "Q:h1rB*R:9999", + output: "", }, { name: "5.7. Example 7 - Margin", input: "A:500000000*B:123456789*C:PT*D:FT*E:N*F:20191124*" + "G:NF 19A/789145*H:JL9DS4TT-789145*I1:PT-AC*I7:50.00*I8:9.00*L:1000.00*" + "N:9.00*O:1059.00*Q:d8/K*R:9999", + output: "", + }, + { + // Literally goes against the spec and we should be expecting failure here + name: "Some dev doesn't know how to read a spec file so periods are commas now", + input: "A:123456789*B:999999990*C:PT*D:FT*E:N*F:20191231*" + + "G:FT AB2019/0035*H:CSDF7T5H-0035*I1:PT*I2:12000,00*I3:15000,00*" + + "I4:900.00*I5:50000.00*I6:6500.00*I7:80000.00*I8:18400.00*J1:PT-AC*" + + "J2:10000.00*J3:25000.56*J4:1000.02*J5:75000.00*J6:6750,00*" + + "J7:100000.00*J8:18000.00*K1:PT-MA*K2:5000,00*K3:12500,00*K4:625.00*" + + "K5:25000.00*K6:3000,00*K7:40000,00*K8:8800,00*L:100.00*M:25.00*" + + "N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" + + "S:TB;PT00000000000000000000000;513500.58", + output: "A:123456789*B:999999990*C:PT*D:FT*E:N*F:20191231*" + + "G:FT AB2019/0035*H:CSDF7T5H-0035*I1:PT*I2:12000.00*I3:15000.00*" + + "I4:900.00*I5:50000.00*I6:6500.00*I7:80000.00*I8:18400.00*J1:PT-AC*" + + "J2:10000.00*J3:25000.56*J4:1000.02*J5:75000.00*J6:6750.00*" + + "J7:100000.00*J8:18000.00*K1:PT-MA*K2:5000.00*K3:12500.00*K4:625.00*" + + "K5:25000.00*K6:3000.00*K7:40000.00*K8:8800.00*L:100.00*M:25.00*" + + "N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" + + "S:TB;PT00000000000000000000000;513500.58", }, } @@ -77,8 +107,13 @@ func TestScan(t *testing.T) { t.Errorf("Should not have occurred an error for: %s, %+v", tc.input, err) } + expect := tc.input + if tc.output != "" { + expect = tc.output + } + stringified := qr.String() - if diff := cmp.Diff(tc.input, stringified); diff != "" { + if diff := cmp.Diff(expect, stringified); diff != "" { t.Errorf("String mismatch (-want +got):\n%s", diff) } }