// 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 // Check what we can take form SAF-T to put here, maybe work on a SAF-T // implementation too, who knows amirite import ( "encoding/json" "fmt" "strconv" "strings" "time" 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. const ( Strict ScanMode = 1 << iota NifValidation ) const ( decCount = 2 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 TaxableReduced *dec.Decimal `json:"TaxableReduced"` // 3 VatTotalReducedTax *dec.Decimal `json:"VatTotalReducedTax"` // 4 TaxableIntermediateBase *dec.Decimal `json:"TaxableIntermediateBase"` // 5 VatTotalIntermediateTax *dec.Decimal `json:"VatTotalIntermediateTax"` // 6 TaxableNormalBase *dec.Decimal `json:"TaxableNormalBase"` // 7 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 Country string `json:"Country"` // C country ISO 3166-1 alpha2 InvoiceType string `json:"InvoiceType"` // D InvoiceStatus string `json:"InvoiceStatus"` // E InvoiceDate time.Time `json:"InvoiceDate"` // F InvoiceNo string `json:"InvoiceNo"` // G ATCUD string `json:"ATCUD"` // H IFiscalSpace FiscalSpace `json:"IFiscalSpace"` // I1-I8 JFiscalSpace FiscalSpace `json:"JFiscalSpace"` // I1-I8 KFiscalSpace FiscalSpace `json:"KFiscalSpace"` // I1-I8 NotTaxable *dec.Decimal `json:"NotTaxable"` // L StampDuty *dec.Decimal `json:"StampDuty"` // M "Imposto de Selo" TaxPayable *dec.Decimal `json:"TaxPayable"` // N GrossTotal *dec.Decimal `json:"GrossTotal"` // O WithholdingTaxAmount *dec.Decimal `json:"WithholdingTaxAmount"` // P HashQuartet string `json:"HashQuartet"` // Q SWCertNo int64 `json:"SWCertNo"` // R OtherInfo string `json:"OtherInfo"` // 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", } ) 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.SWCertNo, err = strconv.ParseInt(val, 10, 16) if err != nil { return err } return nil }, String: func(f *FatQR) string { return "R:" + fmt.Sprintf("%04d", f.SWCertNo) }, Empty: func(f *FatQR) bool { return f.SWCertNo < 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) } // Because software #2426 doesn't follow the spec correctly val = strings.ReplaceAll(val, ",", ".") return (*f).Scan(val) } 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 { 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.SplitN(parts[k], ":", 2) 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 }