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 @@
+
+
+
+
+
+
+
package gofatqr
+
+// TODO
+// Check what we can take form SAF-T to put here, maybe work on a SAF-T
+// implementation too, who knows amirite
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ dec "github.com/shopspring/decimal"
+)
+
+type FiscalSpace struct {
+ 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 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 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 {
+ 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
+}
+
+// TODO
+// Use flags like STRICT | VALIDATE to do conditional stuff
+func (fq *FatQR) Scan(s string) 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)
+}
+
+
+
package gofatqr
+
+func Parse(data string) {
+}
+
+
+
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/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)
+ }
+ }
+}