diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30d8ccf --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +test: + go test -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + rm coverage.out diff --git a/SAF-T_Schema_v_2.00_-_106283-45167181.pdf b/SAF-T_Schema_v_2.00_-_106283-45167181.pdf new file mode 100644 index 0000000..256f3ae Binary files /dev/null and b/SAF-T_Schema_v_2.00_-_106283-45167181.pdf differ diff --git a/api_test.go b/api_test.go new file mode 100644 index 0000000..95b27e3 --- /dev/null +++ b/api_test.go @@ -0,0 +1,23 @@ +package gofatqr_test + +import ( + "gofatqr" + "testing" +) + +func TestFatQRAPI(t *testing.T) { + 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" + + var qr gofatqr.FatQR + err := qr.Scan(input, 0) + if err != nil { + t.Errorf("Should not have occurred an error for: %s, %+v", input, err) + } +} diff --git a/coverage.html b/coverage.html new file mode 100644 index 0000000..86572c1 --- /dev/null +++ b/coverage.html @@ -0,0 +1,806 @@ + + + + + + gofatqr: Go Coverage Report + + + +
+ +
+ not tracked + + not covered + covered + +
+
+
+ + + + + + + +
+ + + diff --git a/fatQR.go b/fatQR.go index 9677901..eb0ad29 100644 --- a/fatQR.go +++ b/fatQR.go @@ -6,70 +6,670 @@ package gofatqr import ( "fmt" - "math/big" + "strconv" "strings" "time" + + dec "github.com/shopspring/decimal" +) + +type ScanMode uint32 + +// Setting this up for the future +const ( + Strict ScanMode = 1 << iota + NifValidation ) type FiscalSpace struct { - TaxCountryRegion string // 1 - fiscal space - TaxableBase *big.Rat // 2 - TaxabledReduced *big.Rat // 3 - VatTotalReducedTax *big.Rat // 4 - TaxableIntermediateBase *big.Rat // 5 - VatTotalIntermediateTax *big.Rat // 6 - TaxableNormalBase *big.Rat // 7 - VatTotalNormalBase *big.Rat // 8 + TaxCountryRegion string // 1 - fiscal space + TaxableBase *dec.Decimal // 2 + TaxableReduced *dec.Decimal // 3 + VatTotalReducedTax *dec.Decimal // 4 + TaxableIntermediateBase *dec.Decimal // 5 + VatTotalIntermediateTax *dec.Decimal // 6 + TaxableNormalBase *dec.Decimal // 7 + VatTotalNormalBase *dec.Decimal // 8 } type FatQR struct { - TaxRegistrationNumber string // A - CustomerTaxID string // B - Country string // C - customer country - InvoiceType string // D - InvoiceStatus bool // E - InvoiceDate time.Time // F - InvoiceNo string // G - ATCUD string // H - IFiscalSpace FiscalSpace // I1 - I8 - JFiscalSpace FiscalSpace // I1 - I8 - KFiscalSpace FiscalSpace // I1 - I8 - NotTaxable *big.Rat // L - StampDuty *big.Rat // M - "Imposto de Selo" - TaxPayable *big.Rat // N - GrossTotal *big.Rat // O - WithholdingTaxAmount *big.Rat // P - HashQuartet string // Q - SoftwareCertificateNumber int // R - OtherInfo string // S + TaxRegistrationNumber string // A + CustomerTaxID string // B + Country string // C customer country ISO 3166-1 alpha2 + InvoiceType string // D + InvoiceStatus string // E + InvoiceDate time.Time // F + InvoiceNo string // G + ATCUD string // H + IFiscalSpace FiscalSpace // I1-I8 + JFiscalSpace FiscalSpace // I1-I8 + KFiscalSpace FiscalSpace // I1-I8 + NotTaxable *dec.Decimal // L + StampDuty *dec.Decimal // M "Imposto de Selo" + TaxPayable *dec.Decimal // N + GrossTotal *dec.Decimal // O + WithholdingTaxAmount *dec.Decimal // P + HashQuartet string // Q + SoftwareCertificateNumber int64 // R + OtherInfo string // S } -type part struct { - key string - value string +type FieldCodec struct { + Required bool + Parse func(f *FatQR, val string) error + String func(f *FatQR) string + Empty func(f *FatQR) bool +} + +var ( + fieldOrder = []string{ + "A", "B", "C", "D", "E", "F", "G", "H", + "I1", "I2", "I3", "I4", "I5", "I6", "I7", "I8", + "J1", "J2", "J3", "J4", "J5", "J6", "J7", "J8", + "K1", "K2", "K3", "K4", "K5", "K6", "K7", "K8", + "L", "M", "N", "O", "P", "Q", "R", "S", + } + // lastKey = fieldOrder[len(fieldOrder)-1] +) + +const ( + decCount = 2 + separator = "*" +) + +var fatQRFieldMap = map[string]FieldCodec{ + "A": { + Required: true, + Parse: func(f *FatQR, val string) error { + // if !isValidNIF(val) { + // return fmt.Errorf("invalid NIF `%s`", val) + // } + + f.TaxRegistrationNumber = val + return nil + }, + String: func(f *FatQR) string { + return "A:" + f.TaxRegistrationNumber + }, + Empty: func(f *FatQR) bool { + return f.TaxRegistrationNumber == "" + }, + }, + "B": { + Required: true, + Parse: func(f *FatQR, val string) error { + // if !isValidNIF(val) { + // return fmt.Errorf("invalid NIF `%s`", val) + // } + + f.CustomerTaxID = val + return nil + }, + String: func(f *FatQR) string { + return "B:" + f.CustomerTaxID + }, + Empty: func(f *FatQR) bool { + return f.CustomerTaxID == "" + }, + }, + "C": { + Required: true, + Parse: func(f *FatQR, val string) error { + // We can use biter777/countries package around here for verification + // but this comes from a code, so eeehhh + f.Country = val + return nil + }, + String: func(f *FatQR) string { + return "C:" + f.Country + }, + Empty: func(f *FatQR) bool { + return f.Country == "" + }, + }, + "D": { + Required: true, + Parse: func(f *FatQR, val string) error { + f.InvoiceType = val + return nil + }, + String: func(f *FatQR) string { + return "D:" + f.InvoiceType + }, + Empty: func(f *FatQR) bool { + return f.InvoiceType == "" + }, + }, + "E": { + Required: true, + Parse: func(f *FatQR, val string) error { + f.InvoiceStatus = val + return nil + }, + String: func(f *FatQR) string { + return "E:" + f.InvoiceStatus + }, + Empty: func(f *FatQR) bool { + return f.InvoiceStatus == "" + }, + }, + "F": { + Required: true, + Parse: func(f *FatQR, val string) error { + tz, err := time.LoadLocation("Europe/Lisbon") + if err != nil { + return err + } + + f.InvoiceDate, err = time.ParseInLocation("20060102", val, tz) + if err != nil { + return err + } + + return nil + }, + String: func(f *FatQR) string { + return "F:" + f.InvoiceDate.Format("20060102") + }, + Empty: func(f *FatQR) bool { + return f.InvoiceStatus == "" + }, + }, + "G": { + Required: true, + Parse: func(f *FatQR, val string) error { + f.InvoiceNo = val + return nil + }, + String: func(f *FatQR) string { + return "G:" + f.InvoiceNo + }, + Empty: func(f *FatQR) bool { + return f.InvoiceNo == "" + }, + }, + "H": { + Required: true, + Parse: func(f *FatQR, val string) error { + // TODO + // Add some ATCUD validation + f.ATCUD = val + return nil + }, + String: func(f *FatQR) string { + return "H:" + f.ATCUD + }, + Empty: func(f *FatQR) bool { + return f.ATCUD == "" + }, + }, + "I1": { + Required: true, + Parse: func(f *FatQR, val string) error { + f.IFiscalSpace.TaxCountryRegion = val + return nil + }, + String: func(f *FatQR) string { + return "I1:" + f.IFiscalSpace.TaxCountryRegion + }, + Empty: func(f *FatQR) bool { + return f.IFiscalSpace.TaxCountryRegion == "" + }, + }, + "I2": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.IFiscalSpace.TaxableBase, val) + }, + String: func(f *FatQR) string { + return "I2:" + stringDecimal(f.IFiscalSpace.TaxableBase) + }, + Empty: func(f *FatQR) bool { + return f.IFiscalSpace.TaxableBase == nil + }, + }, + "I3": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.IFiscalSpace.TaxableReduced, val) + }, + String: func(f *FatQR) string { + return "I3:" + stringDecimal(f.IFiscalSpace.TaxableReduced) + }, + Empty: func(f *FatQR) bool { + return f.IFiscalSpace.TaxableReduced == nil + }, + }, + "I4": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.IFiscalSpace.VatTotalReducedTax, val) + }, + String: func(f *FatQR) string { + return "I4:" + stringDecimal(f.IFiscalSpace.VatTotalReducedTax) + }, + Empty: func(f *FatQR) bool { + return f.IFiscalSpace.VatTotalReducedTax == nil + }, + }, + "I5": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.IFiscalSpace.TaxableIntermediateBase, val) + }, + String: func(f *FatQR) string { + return "I5:" + stringDecimal(f.IFiscalSpace.TaxableIntermediateBase) + }, + Empty: func(f *FatQR) bool { + return f.IFiscalSpace.TaxableIntermediateBase == nil + }, + }, + "I6": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.IFiscalSpace.VatTotalIntermediateTax, val) + }, + String: func(f *FatQR) string { + return "I6:" + stringDecimal(f.IFiscalSpace.VatTotalIntermediateTax) + }, + Empty: func(f *FatQR) bool { + return f.IFiscalSpace.VatTotalIntermediateTax == nil + }, + }, + "I7": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.IFiscalSpace.TaxableNormalBase, val) + }, + String: func(f *FatQR) string { + return "I7:" + stringDecimal(f.IFiscalSpace.TaxableNormalBase) + }, + Empty: func(f *FatQR) bool { + return f.IFiscalSpace.TaxableNormalBase == nil + }, + }, + "I8": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.IFiscalSpace.VatTotalNormalBase, val) + }, + String: func(f *FatQR) string { + return "I8:" + stringDecimal(f.IFiscalSpace.VatTotalNormalBase) + }, + Empty: func(f *FatQR) bool { + return f.IFiscalSpace.VatTotalNormalBase == nil + }, + }, + "J1": { + Required: false, + Parse: func(f *FatQR, val string) error { + f.JFiscalSpace.TaxCountryRegion = val + return nil + }, + String: func(f *FatQR) string { + return "J1:" + f.JFiscalSpace.TaxCountryRegion + }, + Empty: func(f *FatQR) bool { + return f.JFiscalSpace.TaxCountryRegion == "" + }, + }, + "J2": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.JFiscalSpace.TaxableBase, val) + }, + String: func(f *FatQR) string { + return "J2:" + stringDecimal(f.JFiscalSpace.TaxableBase) + }, + Empty: func(f *FatQR) bool { + return f.JFiscalSpace.TaxableBase == nil + }, + }, + "J3": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.JFiscalSpace.TaxableReduced, val) + }, + String: func(f *FatQR) string { + return "J3:" + stringDecimal(f.JFiscalSpace.TaxableReduced) + }, + Empty: func(f *FatQR) bool { + return f.JFiscalSpace.TaxableReduced == nil + }, + }, + "J4": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.JFiscalSpace.VatTotalReducedTax, val) + }, + String: func(f *FatQR) string { + return "J4:" + stringDecimal(f.JFiscalSpace.VatTotalReducedTax) + }, + Empty: func(f *FatQR) bool { + return f.JFiscalSpace.VatTotalReducedTax == nil + }, + }, + "J5": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.JFiscalSpace.TaxableIntermediateBase, val) + }, + String: func(f *FatQR) string { + return "J5:" + stringDecimal(f.JFiscalSpace.TaxableIntermediateBase) + }, + Empty: func(f *FatQR) bool { + return f.JFiscalSpace.TaxableIntermediateBase == nil + }, + }, + "J6": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.JFiscalSpace.VatTotalIntermediateTax, val) + }, + String: func(f *FatQR) string { + return "J6:" + stringDecimal(f.JFiscalSpace.VatTotalIntermediateTax) + }, + Empty: func(f *FatQR) bool { + return f.JFiscalSpace.VatTotalIntermediateTax == nil + }, + }, + "J7": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.JFiscalSpace.TaxableNormalBase, val) + }, + String: func(f *FatQR) string { + return "J7:" + stringDecimal(f.JFiscalSpace.TaxableNormalBase) + }, + Empty: func(f *FatQR) bool { + return f.JFiscalSpace.TaxableNormalBase == nil + }, + }, + "J8": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.JFiscalSpace.VatTotalNormalBase, val) + }, + String: func(f *FatQR) string { + return "J8:" + stringDecimal(f.JFiscalSpace.VatTotalNormalBase) + }, + Empty: func(f *FatQR) bool { + return f.JFiscalSpace.VatTotalNormalBase == nil + }, + }, + "K1": { + Required: false, + Parse: func(f *FatQR, val string) error { + f.KFiscalSpace.TaxCountryRegion = val + return nil + }, + String: func(f *FatQR) string { + return "K1:" + f.KFiscalSpace.TaxCountryRegion + }, + Empty: func(f *FatQR) bool { + return f.KFiscalSpace.TaxCountryRegion == "" + }, + }, + "K2": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.KFiscalSpace.TaxableBase, val) + }, + String: func(f *FatQR) string { + return "K2:" + stringDecimal(f.KFiscalSpace.TaxableBase) + }, + Empty: func(f *FatQR) bool { + return f.KFiscalSpace.TaxableNormalBase == nil + }, + }, + "K3": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.KFiscalSpace.TaxableReduced, val) + }, + String: func(f *FatQR) string { + return "K3:" + stringDecimal(f.KFiscalSpace.TaxableReduced) + }, + Empty: func(f *FatQR) bool { + return f.KFiscalSpace.TaxableReduced == nil + }, + }, + "K4": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.KFiscalSpace.VatTotalReducedTax, val) + }, + String: func(f *FatQR) string { + return "K4:" + stringDecimal(f.KFiscalSpace.VatTotalReducedTax) + }, + Empty: func(f *FatQR) bool { + return f.KFiscalSpace.VatTotalReducedTax == nil + }, + }, + "K5": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.KFiscalSpace.TaxableIntermediateBase, val) + }, + String: func(f *FatQR) string { + return "K5:" + stringDecimal(f.KFiscalSpace.TaxableIntermediateBase) + }, + Empty: func(f *FatQR) bool { + return f.KFiscalSpace.TaxableIntermediateBase == nil + }, + }, + "K6": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.KFiscalSpace.VatTotalIntermediateTax, val) + }, + String: func(f *FatQR) string { + return "K6:" + stringDecimal(f.KFiscalSpace.VatTotalIntermediateTax) + }, + Empty: func(f *FatQR) bool { + return f.KFiscalSpace.VatTotalIntermediateTax == nil + }, + }, + "K7": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.KFiscalSpace.TaxableNormalBase, val) + }, + String: func(f *FatQR) string { + return "K7:" + stringDecimal(f.KFiscalSpace.TaxableNormalBase) + }, + Empty: func(f *FatQR) bool { + return f.KFiscalSpace.TaxableNormalBase == nil + }, + }, + "K8": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.KFiscalSpace.VatTotalNormalBase, val) + }, + String: func(f *FatQR) string { + return "K8:" + stringDecimal(f.KFiscalSpace.VatTotalNormalBase) + }, + Empty: func(f *FatQR) bool { + return f.KFiscalSpace.VatTotalNormalBase == nil + }, + }, + "L": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.NotTaxable, val) + }, + String: func(f *FatQR) string { + return "L:" + stringDecimal(f.NotTaxable) + }, + Empty: func(f *FatQR) bool { + return f.NotTaxable == nil + }, + }, + "M": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.StampDuty, val) + }, + String: func(f *FatQR) string { + return "M:" + stringDecimal(f.StampDuty) + }, + Empty: func(f *FatQR) bool { + return f.StampDuty == nil + }, + }, + "N": { + Required: true, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.TaxPayable, val) + }, + String: func(f *FatQR) string { + return "N:" + stringDecimal(f.TaxPayable) + }, + Empty: func(f *FatQR) bool { + return f.TaxPayable == nil + }, + }, + "O": { + Required: true, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.GrossTotal, val) + }, + String: func(f *FatQR) string { + return "O:" + stringDecimal(f.GrossTotal) + }, + Empty: func(f *FatQR) bool { + return f.GrossTotal == nil + }, + }, + "P": { + Required: false, + Parse: func(f *FatQR, val string) error { + return parseDecimal(&f.WithholdingTaxAmount, val) + }, + String: func(f *FatQR) string { + return "P:" + stringDecimal(f.WithholdingTaxAmount) + }, + Empty: func(f *FatQR) bool { + return f.WithholdingTaxAmount == nil + }, + }, + "Q": { + Required: true, + Parse: func(f *FatQR, val string) error { + f.HashQuartet = val + return nil + }, + String: func(f *FatQR) string { + return "Q:" + f.HashQuartet + }, + Empty: func(f *FatQR) bool { + return f.HashQuartet == "" + }, + }, + "R": { + Required: true, + Parse: func(f *FatQR, val string) error { + var err error + f.SoftwareCertificateNumber, err = strconv.ParseInt(val, 10, 16) + if err != nil { + return err + } + + return nil + }, + String: func(f *FatQR) string { + return "R:" + fmt.Sprintf("%04d", f.SoftwareCertificateNumber) + }, + Empty: func(f *FatQR) bool { + return f.SoftwareCertificateNumber < 1 + }, + }, + "S": { + Required: false, + Parse: func(f *FatQR, val string) error { + f.OtherInfo = val + return nil + }, + String: func(f *FatQR) string { + return "S:" + f.OtherInfo + }, + Empty: func(f *FatQR) bool { + return f.OtherInfo == "" + }, + }, +} + +func parseDecimal(f **dec.Decimal, val string) error { + if *f == nil { + *f = new(dec.Decimal) + } + return (*f).Scan(val) +} + +func stringDecimal(d *dec.Decimal) string { + return d.Truncate(decCount).StringFixed(decCount) +} + +func (fq *FatQR) scanPart(part []string) error { + val, ok := fatQRFieldMap[part[0]] + if !ok { + return fmt.Errorf("invalid key `%s`", part[0]) + } + + err := val.Parse(fq, part[1]) + if err != nil { + return err + } + + return nil } func (fq *FatQR) scanParts(parts []string) error { - fmt.Println(parts) - for k := range parts { keyValuePair := strings.Split(parts[k], ":") if len(keyValuePair) != 2 { return fmt.Errorf("failed to parse key value pair: %s", parts[k]) } + + err := fq.scanPart(keyValuePair) + if err != nil { + return err + } } return nil } -func (fq *FatQR) Scan(s string) error { +// TODO +// Use flags like STRICT | VALIDATE to do conditional stuff +func (fq *FatQR) Scan(s string, mode ScanMode) error { parts := strings.Split(s, "*") - fq.scanParts(parts) + err := fq.scanParts(parts) + if err != nil { + return err + } return nil } func (fq *FatQR) String() string { - return "" + 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 5df416f..6da76f4 100644 --- a/fatQR_test.go +++ b/fatQR_test.go @@ -1,15 +1,289 @@ package gofatqr import ( + "fmt" "testing" + "time" + + "github.com/google/go-cmp/cmp" + dec "github.com/shopspring/decimal" ) func TestScan(t *testing.T) { - testStr := "A:513847588*B:239752988*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" + testCases := []struct { + name string + input string + }{ + { + 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", + }, + { + name: "5.1. Example 1 - Invoice", + 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", + }, + { + 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", + }, + { + name: "5.3. Example 3 - Pro-form Invoice", + input: "A:500000000*B:123456789*C:PT*D:PF*E:N*F:20190123*" + + "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", + }, + { + 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", + }, + { + 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", + }, + { + 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", + }, + { + 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", + }, + } + + for _, tc := range testCases { + fmt.Printf("Test: %s\n", tc.name) + var qr FatQR + err := qr.Scan(tc.input, 0) + if err != nil { + t.Errorf("Should not have occurred an error for: %s, %+v", tc.input, err) + } + + stringified := qr.String() + if diff := cmp.Diff(tc.input, stringified); diff != "" { + t.Errorf("String mismatch (-want +got):\n%s", diff) + } + } +} + +func TestScanPart(t *testing.T) { + var fq FatQR + part := []string{"«", "123987654"} + + err := fq.scanPart(part) + if err == nil { + t.Errorf("Expected error, got %v", err) + } +} + +func TestScanParts(t *testing.T) { + var fq FatQR + input := "A*B:123654987" + + err := fq.Scan(input, 0) + if err == nil { + t.Errorf("Expected error about bad parts, got %v", err) + } +} + +func TestFatQRString(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic for invalid key, but did not panic") + } + }() + + originalOrder := fieldOrder + fieldOrder = []string{"IM_NOT_EVEN_THERE"} + defer func() { + fieldOrder = originalOrder + }() + + var fq FatQR + _ = fq.String() +} + +func TestScanA(t *testing.T) { + testStr := "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" var qr FatQR - err := qr.Scan(testStr) + err := qr.Scan(testStr, 0) if err != nil { t.Errorf("Should not have occurred an error for: %s, %+v", testStr, err) } + + stringified := qr.String() + if diff := cmp.Diff(testStr, stringified); diff != "" { + fmt.Printf("t: %v\n", stringified) + t.Errorf("String mismatch (-want +got):\n%s", diff) + } } + +func TestScanPartA(t *testing.T) { + f := &FatQR{} + + input := "123456789" + err := fatQRFieldMap["A"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field A with input `%s`, got %+v", input, err) + } + + // input = "23456789" + // err = fatQRFieldMap["A"].Parse(f, input) + // if err == nil { + // t.Errorf("Expected err for field A with input `%s`, got %+v", input, err) + // } +} + +func TestScanPartB(t *testing.T) { + f := &FatQR{} + + input := "123456789" + err := fatQRFieldMap["B"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field B with input `%s`, got %+v", input, err) + } + + // input = "23456789" + // err = fatQRFieldMap["B"].Parse(f, input) + // if err == nil { + // t.Errorf("Expected err for field B with input `%s`, got %+v", input, err) + // } +} + +func TestScanPartC(t *testing.T) { + f := &FatQR{} + + input := "PT" + err := fatQRFieldMap["C"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field C with input `%s`, got %+v", input, err) + } +} + +func TestScanPartD(t *testing.T) { + f := &FatQR{} + + input := "something" + err := fatQRFieldMap["D"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field D with input `%s`, got %+v", input, err) + } +} + +func TestScanPartE(t *testing.T) { + f := &FatQR{} + + input := "anothersomething" + err := fatQRFieldMap["E"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field E with input `%s`, got %+v", input, err) + } +} + +func TestScanPartF(t *testing.T) { + f := &FatQR{} + + loc, err := time.LoadLocation("Europe/Lisbon") + if err != nil { + t.Errorf("Failed to prepare timezone var: %+v", err) + } + expected := time.Date(2025, 12, 31, 0, 0, 0, 0, loc) + + input := "20251231" + err = fatQRFieldMap["F"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field F with input `%s`, got %+v", input, err) + } + + if !f.InvoiceDate.Equal(expected) { + t.Errorf("Result date doesnt match expected date: %v\nGot: %+v, Want: %+v", + f.InvoiceDate, expected, err) + } +} + +func TestScanPartG(t *testing.T) { + f := &FatQR{} + + input := "FS 81239/99999" + err := fatQRFieldMap["G"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field G with input `%s`, got %+v", input, err) + } + + if f.InvoiceNo != input { + t.Errorf("Expected `%s`, got %+v", input, f.InvoiceNo) + } +} + +func TestScanPartH(t *testing.T) { + f := &FatQR{} + + input := "CSDF7T5H-0035" + err := fatQRFieldMap["H"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field H with input `%s`, got %+v", input, err) + } + + if f.ATCUD != input { + t.Errorf("Expected `%s`, got %+v", input, f.ATCUD) + } +} + +func TestScanPartI1(t *testing.T) { + f := &FatQR{} + + input := "PT" + err := fatQRFieldMap["I1"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field I1 with input `%s`, got %+v", input, err) + } + + if f.IFiscalSpace.TaxCountryRegion != input { + t.Errorf("Expected `%s`, got %+v", input, f.IFiscalSpace.TaxCountryRegion) + } +} + +func TestScanPartI2(t *testing.T) { + f := &FatQR{} + + input := "1565.20" + expect := dec.NewFromFloat(1565.20) + err := fatQRFieldMap["I2"].Parse(f, input) + if err != nil { + t.Errorf("Expected no err for field I2 with input `%s`, got %+v", input, err) + } + + if f.IFiscalSpace.TaxableBase.Truncate(2).String() != expect.Truncate(2).String() { + t.Errorf("Expected `%s`, got %+v", expect, f.IFiscalSpace.TaxableBase) + } + + input = "10.5A" + err = fatQRFieldMap["I2"].Parse(f, input) + if err == nil { + t.Errorf("Expected err for field I2 with input `%s`, got %+v", input, err) + } +} + +// TODO: +// Add more UTs diff --git a/go.mod b/go.mod index 9a58200..d2cd057 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module gofatqr go 1.24.6 + +require ( + github.com/google/go-cmp v0.7.0 + github.com/shopspring/decimal v1.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f923dd7 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= diff --git a/nif.go b/nif.go new file mode 100644 index 0000000..546b5e1 --- /dev/null +++ b/nif.go @@ -0,0 +1,24 @@ +package gofatqr + +import ( + "unicode" +) + +// TODO +// Add a way to check with an API or whatever like nif.pt +// Also need to be able to indentify other countries NIFs I guess +// some validation at the end + +func isValidNIF(n string) bool { + if len(n) != 9 { + return false + } + + for _, c := range n { + if !unicode.IsDigit(c) { + return false + } + } + + return true +} diff --git a/nif_test.go b/nif_test.go new file mode 100644 index 0000000..4f17075 --- /dev/null +++ b/nif_test.go @@ -0,0 +1,43 @@ +package gofatqr + +import ( + "fmt" + "testing" +) + +func TestIsValidNIF(t *testing.T) { + testCases := []struct { + name string + expect bool + input string + }{ + { + name: "Normal NIF", + expect: true, + input: "198435678", + }, + { + name: "String too short", + expect: false, + input: "19845678", + }, + { + name: "String too long", + expect: false, + input: "1985678654", + }, + { + name: "Not digits in the string", + expect: false, + input: "1AC43567A", + }, + } + + for _, tc := range testCases { + fmt.Printf("Test: %v\n", tc.name) + res := isValidNIF(tc.input) + if res != tc.expect { + t.Errorf("Expected %t for input %s, got %t", tc.expect, tc.input, res) + } + } +}