Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2acdb8fb3a |
@@ -0,0 +1,12 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: all
|
||||
disable:
|
||||
- wsl
|
||||
- depguard
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
- wsl_v5
|
||||
- exhaustruct
|
||||
- gochecknoglobals
|
||||
- nlreturn
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+36
-1
@@ -13,12 +13,15 @@ func TestScan(t *testing.T) {
|
||||
testCases := []struct {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user