Basic functionality

User can pass a string to the Scanner and get the struced filled
This commit is contained in:
2025-09-05 18:51:51 +01:00
parent 9f731d053f
commit f61535e7e7
10 changed files with 1821 additions and 38 deletions
+4
View File
@@ -0,0 +1,4 @@
test:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
rm coverage.out
Binary file not shown.
+23
View File
@@ -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)
}
}
+806
View File
@@ -0,0 +1,806 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>gofatqr: Go Coverage Report</title>
<style>
body {
background: black;
color: rgb(80, 80, 80);
}
body, pre, #legend span {
font-family: Menlo, monospace;
font-weight: bold;
}
#topbar {
background: black;
position: fixed;
top: 0; left: 0; right: 0;
height: 42px;
border-bottom: 1px solid rgb(80, 80, 80);
}
#content {
margin-top: 50px;
}
#nav, #legend {
float: left;
margin-left: 10px;
}
#legend {
margin-top: 12px;
}
#nav {
margin-top: 10px;
}
#legend span {
margin: 0 5px;
}
.cov0 { color: rgb(192, 0, 0) }
.cov1 { color: rgb(128, 128, 128) }
.cov2 { color: rgb(116, 140, 131) }
.cov3 { color: rgb(104, 152, 134) }
.cov4 { color: rgb(92, 164, 137) }
.cov5 { color: rgb(80, 176, 140) }
.cov6 { color: rgb(68, 188, 143) }
.cov7 { color: rgb(56, 200, 146) }
.cov8 { color: rgb(44, 212, 149) }
.cov9 { color: rgb(32, 224, 152) }
.cov10 { color: rgb(20, 236, 155) }
</style>
</head>
<body>
<div id="topbar">
<div id="nav">
<select id="files">
<option value="file0">gofatqr/fatQR.go (97.1%)</option>
<option value="file1">gofatqr/main.go (0.0%)</option>
<option value="file2">gofatqr/nif.go (100.0%)</option>
</select>
</div>
<div id="legend">
<span>not tracked</span>
<span class="cov0">not covered</span>
<span class="cov8">covered</span>
</div>
</div>
<div id="content">
<pre class="file" id="file0" style="display: none">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 <span class="cov8" title="1">{
// if !isValidNIF(val) {
// return fmt.Errorf("invalid NIF `%s`", val)
// }
f.TaxRegistrationNumber = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "A:" + f.TaxRegistrationNumber
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.TaxRegistrationNumber == ""
}</span>,
},
"B": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
// if !isValidNIF(val) {
// return fmt.Errorf("invalid NIF `%s`", val)
// }
f.CustomerTaxID = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "B:" + f.CustomerTaxID
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.CustomerTaxID == ""
}</span>,
},
"C": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
// We can use biter777/countries package around here for verification
// but this comes from a code, so eeehhh
f.Country = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "C:" + f.Country
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.Country == ""
}</span>,
},
"D": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
f.InvoiceType = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "D:" + f.InvoiceType
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.InvoiceType == ""
}</span>,
},
"E": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
f.InvoiceStatus = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "E:" + f.InvoiceStatus
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.InvoiceStatus == ""
}</span>,
},
"F": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
tz, err := time.LoadLocation("Europe/Lisbon")
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
<span class="cov8" title="1">f.InvoiceDate, err = time.ParseInLocation("20060102", val, tz)
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
<span class="cov8" title="1">return nil</span>
},
String: func(f *FatQR) string <span class="cov8" title="1">{
return "F:" + f.InvoiceDate.Format("20060102")
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.InvoiceStatus == ""
}</span>,
},
"G": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
f.InvoiceNo = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "G:" + f.InvoiceNo
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.InvoiceNo == ""
}</span>,
},
"H": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
// TODO
// Add some ATCUD validation
f.ATCUD = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "H:" + f.ATCUD
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.ATCUD == ""
}</span>,
},
"I1": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
f.IFiscalSpace.TaxCountryRegion = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "I1:" + f.IFiscalSpace.TaxCountryRegion
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.IFiscalSpace.TaxCountryRegion == ""
}</span>,
},
"I2": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.IFiscalSpace.TaxableBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "I2:" + stringDecimal(f.IFiscalSpace.TaxableBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.IFiscalSpace.TaxableBase == nil
}</span>,
},
"I3": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.IFiscalSpace.TaxableReduced, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "I3:" + stringDecimal(f.IFiscalSpace.TaxableReduced)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.IFiscalSpace.TaxableReduced == nil
}</span>,
},
"I4": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.IFiscalSpace.VatTotalReducedTax, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "I4:" + stringDecimal(f.IFiscalSpace.VatTotalReducedTax)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.IFiscalSpace.VatTotalReducedTax == nil
}</span>,
},
"I5": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.IFiscalSpace.TaxableIntermediateBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "I5:" + stringDecimal(f.IFiscalSpace.TaxableIntermediateBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.IFiscalSpace.TaxableIntermediateBase == nil
}</span>,
},
"I6": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.IFiscalSpace.VatTotalIntermediateTax, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "I6:" + stringDecimal(f.IFiscalSpace.VatTotalIntermediateTax)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.IFiscalSpace.VatTotalIntermediateTax == nil
}</span>,
},
"I7": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.IFiscalSpace.TaxableNormalBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "I7:" + stringDecimal(f.IFiscalSpace.TaxableNormalBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.IFiscalSpace.TaxableNormalBase == nil
}</span>,
},
"I8": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.IFiscalSpace.VatTotalNormalBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "I8:" + stringDecimal(f.IFiscalSpace.VatTotalNormalBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.IFiscalSpace.VatTotalNormalBase == nil
}</span>,
},
"J1": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
f.JFiscalSpace.TaxCountryRegion = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "J1:" + f.JFiscalSpace.TaxCountryRegion
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.JFiscalSpace.TaxCountryRegion == ""
}</span>,
},
"J2": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.JFiscalSpace.TaxableBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "J2:" + stringDecimal(f.JFiscalSpace.TaxableBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.JFiscalSpace.TaxableBase == nil
}</span>,
},
"J3": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.JFiscalSpace.TaxableReduced, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "J3:" + stringDecimal(f.JFiscalSpace.TaxableReduced)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.JFiscalSpace.TaxableReduced == nil
}</span>,
},
"J4": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.JFiscalSpace.VatTotalReducedTax, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "J4:" + stringDecimal(f.JFiscalSpace.VatTotalReducedTax)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.JFiscalSpace.VatTotalReducedTax == nil
}</span>,
},
"J5": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.JFiscalSpace.TaxableIntermediateBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "J5:" + stringDecimal(f.JFiscalSpace.TaxableIntermediateBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.JFiscalSpace.TaxableIntermediateBase == nil
}</span>,
},
"J6": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.JFiscalSpace.VatTotalIntermediateTax, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "J6:" + stringDecimal(f.JFiscalSpace.VatTotalIntermediateTax)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.JFiscalSpace.VatTotalIntermediateTax == nil
}</span>,
},
"J7": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.JFiscalSpace.TaxableNormalBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "J7:" + stringDecimal(f.JFiscalSpace.TaxableNormalBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.JFiscalSpace.TaxableNormalBase == nil
}</span>,
},
"J8": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.JFiscalSpace.VatTotalNormalBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "J8:" + stringDecimal(f.JFiscalSpace.VatTotalNormalBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.JFiscalSpace.VatTotalNormalBase == nil
}</span>,
},
"K1": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
f.KFiscalSpace.TaxCountryRegion = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "K1:" + f.KFiscalSpace.TaxCountryRegion
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.KFiscalSpace.TaxCountryRegion == ""
}</span>,
},
"K2": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.KFiscalSpace.TaxableBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "K2:" + stringDecimal(f.KFiscalSpace.TaxableBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.KFiscalSpace.TaxableNormalBase == nil
}</span>,
},
"K3": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.KFiscalSpace.TaxableReduced, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "K3:" + stringDecimal(f.KFiscalSpace.TaxableReduced)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.KFiscalSpace.TaxableReduced == nil
}</span>,
},
"K4": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.KFiscalSpace.VatTotalReducedTax, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "K4:" + stringDecimal(f.KFiscalSpace.VatTotalReducedTax)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.KFiscalSpace.VatTotalReducedTax == nil
}</span>,
},
"K5": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.KFiscalSpace.TaxableIntermediateBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "K5:" + stringDecimal(f.KFiscalSpace.TaxableIntermediateBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.KFiscalSpace.TaxableIntermediateBase == nil
}</span>,
},
"K6": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.KFiscalSpace.VatTotalIntermediateTax, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "K6:" + stringDecimal(f.KFiscalSpace.VatTotalIntermediateTax)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.KFiscalSpace.VatTotalIntermediateTax == nil
}</span>,
},
"K7": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.KFiscalSpace.TaxableNormalBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "K7:" + stringDecimal(f.KFiscalSpace.TaxableNormalBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.KFiscalSpace.TaxableNormalBase == nil
}</span>,
},
"K8": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.KFiscalSpace.VatTotalNormalBase, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "K8:" + stringDecimal(f.KFiscalSpace.VatTotalNormalBase)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.KFiscalSpace.VatTotalNormalBase == nil
}</span>,
},
"L": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.NotTaxable, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "L:" + stringDecimal(f.NotTaxable)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.NotTaxable == nil
}</span>,
},
"M": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.StampDuty, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "M:" + stringDecimal(f.StampDuty)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.StampDuty == nil
}</span>,
},
"N": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.TaxPayable, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "N:" + stringDecimal(f.TaxPayable)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.TaxPayable == nil
}</span>,
},
"O": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.GrossTotal, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "O:" + stringDecimal(f.GrossTotal)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.GrossTotal == nil
}</span>,
},
"P": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
return parseDecimal(&amp;f.WithholdingTaxAmount, val)
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "P:" + stringDecimal(f.WithholdingTaxAmount)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.WithholdingTaxAmount == nil
}</span>,
},
"Q": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
f.HashQuartet = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "Q:" + f.HashQuartet
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.HashQuartet == ""
}</span>,
},
"R": {
Required: true,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
var err error
f.SoftwareCertificateNumber, err = strconv.ParseInt(val, 10, 16)
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
<span class="cov8" title="1">return nil</span>
},
String: func(f *FatQR) string <span class="cov8" title="1">{
return "R:" + fmt.Sprintf("%04d", f.SoftwareCertificateNumber)
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.SoftwareCertificateNumber &lt; 1
}</span>,
},
"S": {
Required: false,
Parse: func(f *FatQR, val string) error <span class="cov8" title="1">{
f.OtherInfo = val
return nil
}</span>,
String: func(f *FatQR) string <span class="cov8" title="1">{
return "S:" + f.OtherInfo
}</span>,
Empty: func(f *FatQR) bool <span class="cov8" title="1">{
return f.OtherInfo == ""
}</span>,
},
}
func parseDecimal(f **dec.Decimal, val string) error <span class="cov8" title="1">{
if *f == nil </span><span class="cov8" title="1">{
*f = new(dec.Decimal)
}</span>
<span class="cov8" title="1">return (*f).Scan(val)</span>
}
func stringDecimal(d *dec.Decimal) string <span class="cov8" title="1">{
return d.Truncate(decCount).StringFixed(decCount)
}</span>
func (fq *FatQR) scanPart(part []string) error <span class="cov8" title="1">{
val, ok := fatQRFieldMap[part[0]]
if !ok </span><span class="cov8" title="1">{
return fmt.Errorf("invalid key `%s`", part[0])
}</span>
<span class="cov8" title="1">err := val.Parse(fq, part[1])
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
<span class="cov8" title="1">return nil</span>
}
func (fq *FatQR) scanParts(parts []string) error <span class="cov8" title="1">{
for k := range parts </span><span class="cov8" title="1">{
keyValuePair := strings.Split(parts[k], ":")
if len(keyValuePair) != 2 </span><span class="cov8" title="1">{
return fmt.Errorf("failed to parse key value pair: %s", parts[k])
}</span>
<span class="cov8" title="1">err := fq.scanPart(keyValuePair)
if err != nil </span><span class="cov0" title="0">{
return err
}</span>
}
<span class="cov8" title="1">return nil</span>
}
// TODO
// Use flags like STRICT | VALIDATE to do conditional stuff
func (fq *FatQR) Scan(s string) error <span class="cov8" title="1">{
parts := strings.Split(s, "*")
err := fq.scanParts(parts)
if err != nil </span><span class="cov8" title="1">{
return err
}</span>
<span class="cov8" title="1">return nil</span>
}
func (fq *FatQR) String() string <span class="cov8" title="1">{
parts := make([]string, 0, len(fieldOrder))
for _, key := range fieldOrder </span><span class="cov8" title="1">{
codec, ok := fatQRFieldMap[key]
if !ok </span><span class="cov8" title="1">{
panic("invalid key in order")</span>
}
// 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
<span class="cov8" title="1">if !codec.Empty(fq) </span><span class="cov8" title="1">{
parts = append(parts, codec.String(fq))
}</span>
}
<span class="cov8" title="1">return strings.Join(parts, separator)</span>
}
</pre>
<pre class="file" id="file1" style="display: none">package gofatqr
func Parse(data string) {<span class="cov0" title="0">
}</span>
</pre>
<pre class="file" id="file2" style="display: none">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 <span class="cov8" title="1">{
if len(n) != 9 </span><span class="cov8" title="1">{
return false
}</span>
<span class="cov8" title="1">for _, c := range n </span><span class="cov8" title="1">{
if !unicode.IsDigit(c) </span><span class="cov8" title="1">{
return false
}</span>
}
<span class="cov8" title="1">return true</span>
}
</pre>
</div>
</body>
<script>
(function() {
var files = document.getElementById('files');
var visible;
files.addEventListener('change', onChange, false);
function select(part) {
if (visible)
visible.style.display = 'none';
visible = document.getElementById(part);
if (!visible)
return;
files.value = part;
visible.style.display = 'block';
location.hash = part;
}
function onChange() {
select(files.value);
window.scrollTo(0, 0);
}
if (location.hash != "") {
select(location.hash.substr(1));
}
if (!visible) {
select("file0");
}
})();
</script>
</html>
+636 -36
View File
@@ -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)
}
+276 -2
View File
@@ -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
+5
View File
@@ -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
)
+4
View File
@@ -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=
+24
View File
@@ -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
}
+43
View File
@@ -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)
}
}
}