Skip to content

Commit

Permalink
Added Invoice Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
apardods committed Nov 21, 2024
1 parent 35045d9 commit 57f9991
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 280 deletions.
37 changes: 37 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
before:
hooks:
- go mod download
builds:
- id: gobl.verifactu
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
main: ./cmd/gobl.verifactu
binary: gobl.verifactu

archives:
- id: gobl.verifactu
builds:
- gobl.verifactu
format: tar.gz
name_template: "gobl.verifactu_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
wrap_in_directory: true

checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Tag }}"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
release:
github:
owner: invopop
name: gobl.verifactu
prerelease: auto
150 changes: 93 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (

"github.com/invopop/gobl"
verifactu "github.com/invopop/gobl.verifactu"
"github.com/invopop/gobl.verifactu/doc"
"github.com/invopop/xmldsig"
)

func main() {
Expand All @@ -44,60 +46,84 @@ func main() {

// Prepare software configuration:
soft := &verifactu.Software{
License: "XYZ", // provided by tax agency
NIF: "B123456789", // Software company's tax code
Name: "Invopop", // Name of application
Version: "v0.1.0", // Software version
NombreRazon: "Company LTD", // Company Name
NIF: "B123456789", // Software company's tax code
NombreSistemaInformatico: "Software Name", // Name of application
IdSistemaInformatico: "A1", // Software ID
Version: "1.0", // Software version
NumeroInstalacion: "00001", // Software installation number
}

// Load the certificate
cert, err := xmldsig.LoadCertificate(c.cert, c.password)
if err != nil {
return err
}

// Create the client with the software and certificate
opts := []verifactu.Option{
verifactu.WithCertificate(cert),
verifactu.WithSupplierIssuer(), // The issuer can be either the supplier, the
verifactu.InTesting(),
}

// Instantiate the TicketBAI client with sofrward config
// and specific zone.
c, err := verifactu.New(soft,
verifactu.WithSupplierIssuer(), // The issuer is the invoice's supplier
verifactu.InTesting(), // Use the tax agency testing environment
)

tc, err := verifactu.New(c.software(), opts...)
if err != nil {
panic(err)
return err
}

// Create a new Veri*Factu document:
doc, err := c.Convert(env)
td, err := tc.Convert(env)
if err != nil {
panic(err)
return err
}

c.previous = `{
"emisor": "B85905495",
"serie": "SAMPLE-001",
"fecha": "11-11-2024",
"huella": "13EC0696104D1E529667184C6CDFC67D08036BCA4CD1B7887DE9C6F8F7EEC69C"
}`

prev := new(doc.ChainData)
if err := json.Unmarshal([]byte(c.previous), prev); err != nil {
return err
}

// Create the document fingerprint
// Assume here that we don't have a previous chain data object.
if err = c.Fingerprint(doc, nil); err != nil {
panic(err)
err = tc.Fingerprint(td, prev)
if err != nil {
return err
}

// Sign the document:
if err := c.AddQR(doc, env); err != nil {
panic(err)
if err := tc.AddQR(td, env); err != nil {
return err
}

// Create the XML output
bytes, err := doc.BytesIndent()
out, err := c.openOutput(cmd, args)
if err != nil {
panic(err)
return err
}
defer out.Close() // nolint:errcheck

// Do something with the output, you probably want to store
// it somewhere.
fmt.Println("Document created:\n", string(bytes))
convOut, err := td.BytesIndent()
if err != nil {
return fmt.Errorf("generating verifactu xml: %w", err)
}

// Grab and persist the Chain Data somewhere so you can use this
// for the next call to the Fingerprint method.
cd := doc.ChainData()

// Send to Veri*Factu, if rejected, you'll want to fix any
// issues and send in a new XML document. The original
// version should not be modified.
if err := c.Post(ctx, doc); err != nil {
panic(err)
err = tc.Post(cmd.Context(), td)
if err != nil {
return err
}

data, err := json.Marshal(td.ChainData())
if err != nil {
return err
}
fmt.Printf("Generated document with fingerprint: \n%s\n", string(data))

return nil

}
```
Expand All @@ -114,11 +140,15 @@ We recommend using a `.env` file to prepare configuration settings, although all

```
SOFTWARE_COMPANY_NIF=B85905495
SOFTWARE_COMPANY_NAME="Invopop S.L."
SOFTWARE_NAME="Invopop"
SOFTWARE_ID_SISTEMA_INFORMATICO="IP"
SOFTWARE_NUMERO_INSTALACION="12345678"
SOFTWARE_VERSION="1.0"
SOFTWARE_COMPANY_NAME=Invopop S.L.
SOFTWARE_NAME=gobl.verifactu
SOFTWARE_VERSION=1.0
SOFTWARE_ID_SISTEMA_INFORMATICO=A1
SOFTWARE_NUMERO_INSTALACION=00001
CERTIFICATE_PATH=./xxxxxxxxx.p12
CERTIFICATE_PASSWORD=xxxxxxxx
```

To convert a document to XML, run:
Expand All @@ -145,35 +175,41 @@ gobl.verifactu send ./test/data/sample-invoice.json

In order to provide the supplier specific data required by Veri*Factu, invoices need to include a bit of extra data. We've managed to simplify these into specific cases.

<!-- ### Tax Tags
### Tax Tags

Invoice tax tags can be added to invoice documents in order to reflect a special situation. The following schemes are supported:

- `simplified-scheme` - a retailer operating under a simplified tax regime (regimen simplificado) that must indicate that all of their sales are under this scheme. This implies that all operations in the invoice will have the `OperacionEnRecargoDeEquivalenciaORegimenSimplificado` tag set to `S`.
- `simplified-scheme` - a retailer operating under a simplified tax regime (regimen simplificado) that must indicate that all of their sales are under this scheme. This implies that all operations in the invoice will have the `FacturaSinIdentifDestinatarioArt61d` tag set to `S`.
- `reverse-charge` - B2B services or goods sold to a tax registered EU member who will pay VAT on the suppliers behalf. Implies that all items will be classified under the `TipoNoExenta` value of `S2`.
- `customer-rates` - B2C services, specifically for the EU digital goods act (2015) which imply local taxes will be applied. All items will specify the `DetalleNoSujeta` cause of `RL`.

## Tax Extensions

The following extension can be applied to each line tax:

- `es-tbai-product` – allows to correctly group the invoice's lines taxes in the TicketBAI breakdowns (a.k.a. desgloses). These are the valid values:
- `es-verifactu-doc-type` – defines the type of invoice being sent. In most cases this will be set automatically by the GOBL add-on. These are the valid values:

- `F1` - Standard invoice.
- `F2` - Simplified invoice.
- `F3` - Invoice in substitution of simplified invoices.
- `R1` - Rectified invoice based on law and Article 80.1, 80.2 and 80.6 in the Spanish VAT Law ([LIVA](https://www.boe.es/buscar/act.php?id=BOE-A-1992-28740)).
- `R2` - Rectified invoice based on law and Article 80.3.
- `R3` - Rectified invoice based on law and Article 80.4.
- `R4` - Rectified invoice based on law and other reasons.
- `R5` - Rectified invoice based on simplified invoices.

- `services` - indicates that the product being sold is a service (as opposed to a physical good). Services are accounted in the `DesgloseTipoOperacion > PrestacionServicios` breakdown of invoices to foreign customers. By default, all items are considered services.
- `goods` - indicates that the product being sold is a physical good. Products are accounted in the `DesgloseTipoOperacion > Entrega` breakdown of invoices to foreign customers.
- `resale` - indicates that a line item is sold without modification from a provider under the Equalisation Charge scheme. (This implies that the `OperacionEnRecargoDeEquivalenciaORegimenSimplificado` tag will be set to `S`).
- `es-verifactu-tax-classification` - combines the tax classification and exemption codes used in Veri*Factu. These are the valid values:

- `es-tbai-exemption` - identifies the specific TicketBAI reason code as to why taxes should not be applied to the line according to the whole set of exemptions or not-subject scenarios defined in the law. It has to be set along with the tax rate value of `exempt`. These are the valid values:
- `E1` – Exenta por el artículo 20 de la Norma Foral del IVA
- `E2` – Exenta por el artículo 21 de la Norma Foral del IVA
- `E3` – Exenta por el artículo 22 de la Norma Foral del IVA
- `E4` – Exenta por el artículo 23 y 24 de la Norma Foral del IVA
- `E5` – Exenta por el artículo 25 de la Norma Foral del IVA
- `E6` – Exenta por otra causa
- `OT` – No sujeto por el artículo 7 de la Norma Foral de IVA / Otros supuestos
- `RL` – No sujeto por reglas de localización (\*)
- `S1` - Subject and not exempt - Without reverse charge
- `S2` - Subject and not exempt - With reverse charge
- `N1` - Not subject - Articles 7, 14, others
- `N2` - Not subject - Due to location rules
- `E1` - Exempt pursuant to Article 20 of the VAT Law
- `E2` - Exempt pursuant to Article 21 of the VAT Law
- `E3` - Exempt pursuant to Article 22 of the VAT Law
- `E4` - Exempt pursuant to Articles 23 and 24 of the VAT Law
- `E5` - Exempt pursuant to Article 25 of the VAT Law
- `E6` - Exempt for other reasons

_(\*) As noted elsewhere, `RL` will be set automatically set in invoices using the `customer-rates` tax tag. It can also be set explicitly using the `es-tbai-exemption` extension in invoices not using that tag._

### Use-Cases

Expand All @@ -194,4 +230,4 @@ Some sample test data is available in the `./test` directory. To update the JSON
go test ./examples_test.go --update
```

All generate XML documents will be validated against the TicketBAI XSD documents. -->
All generate XML documents will be validated against the Veri*Factu XSD documents.
13 changes: 12 additions & 1 deletion doc/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,18 @@ func (d *VeriFactu) fingerprintAnulacion(inv *RegistroAnulacion) error {
// GenerateHash generates the SHA-256 hash for the invoice data.
func (d *VeriFactu) GenerateHash(prev *ChainData) error {
if prev == nil {
return fmt.Errorf("previous document is required")
if d.RegistroFactura.RegistroAlta != nil {
s := "S"
d.RegistroFactura.RegistroAlta.Encadenamiento = &Encadenamiento{
PrimerRegistro: &s,
}
} else if d.RegistroFactura.RegistroAnulacion != nil {
s := "S"
d.RegistroFactura.RegistroAnulacion.Encadenamiento = &Encadenamiento{
PrimerRegistro: &s,
}
}
return nil
}
// Concatenate f according to Verifactu specifications
if d.RegistroFactura.RegistroAlta != nil {
Expand Down
10 changes: 2 additions & 8 deletions doc/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ package doc

import (
"fmt"
"slices"
"time"

"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/num"
)

var simplifiedTypes = []string{"F2", "R5"}

// NewRegistroAlta creates a new VeriFactu registration for an invoice.
func NewRegistroAlta(inv *bill.Invoice, ts time.Time, r IssuerRole, s *Software) (*RegistroAlta, error) {
description, err := newDescription(inv.Notes)
Expand Down Expand Up @@ -51,6 +48,8 @@ func NewRegistroAlta(inv *bill.Invoice, ts time.Time, r IssuerRole, s *Software)
IDDestinatario: d,
}
reg.Destinatarios = []*Destinatario{ds}
} else {
reg.FacturaSinIdentifDestinatarioArt61d = "S"
}

if r == IssuerRoleThirdParty {
Expand All @@ -62,11 +61,6 @@ func NewRegistroAlta(inv *bill.Invoice, ts time.Time, r IssuerRole, s *Software)
reg.Tercero = t
}

// Flag for simplified invoices.
if slices.Contains(simplifiedTypes, reg.TipoFactura) {
reg.FacturaSinIdentifDestinatarioArt61d = "S"
}

// Flag for operations with totals over 100,000,000€. Added with optimism.
if inv.Totals.TotalWithTax.Compare(num.MakeAmount(100000000, 0)) == 1 {
reg.Macrodato = "S"
Expand Down
59 changes: 59 additions & 0 deletions doc/invoice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package doc

import (
"testing"
"time"

"github.com/invopop/gobl.verifactu/test"
"github.com/invopop/gobl/tax"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewRegistroAlta(t *testing.T) {
ts, err := time.Parse(time.RFC3339, "2022-02-01T04:00:00Z")
require.NoError(t, err)
role := IssuerRoleSupplier
sw := &Software{}

t.Run("should contain basic document info", func(t *testing.T) {
inv := test.LoadInvoice("inv-base.json")
doc, err := NewDocument(inv, ts, role, sw)
require.NoError(t, err)

reg := doc.RegistroFactura.RegistroAlta
assert.Equal(t, "1.0", reg.IDVersion)
assert.Equal(t, "B85905495", reg.IDFactura.IDEmisorFactura)
assert.Equal(t, "SAMPLE-003", reg.IDFactura.NumSerieFactura)
assert.Equal(t, "13-11-2024", reg.IDFactura.FechaExpedicionFactura)
assert.Equal(t, "Invopop S.L.", reg.NombreRazonEmisor)
assert.Equal(t, "F1", reg.TipoFactura)
assert.Equal(t, "This is a sample invoice", reg.DescripcionOperacion)
assert.Equal(t, float64(378), reg.CuotaTotal)
assert.Equal(t, float64(2178), reg.ImporteTotal)

require.Len(t, reg.Destinatarios, 1)
dest := reg.Destinatarios[0].IDDestinatario
assert.Equal(t, "Sample Consumer", dest.NombreRazon)
assert.Equal(t, "B63272603", dest.NIF)

require.Len(t, reg.Desglose.DetalleDesglose, 1)
desg := reg.Desglose.DetalleDesglose[0]
assert.Equal(t, "01", desg.Impuesto)
assert.Equal(t, "01", desg.ClaveRegimen)
assert.Equal(t, "S1", desg.CalificacionOperacion)
assert.Equal(t, float64(21), desg.TipoImpositivo)
assert.Equal(t, float64(1800), desg.BaseImponibleOImporteNoSujeto)
assert.Equal(t, float64(378), desg.CuotaRepercutida)
})
t.Run("should handle simplified invoices", func(t *testing.T) {
inv := test.LoadInvoice("inv-base.json")
inv.SetTags(tax.TagSimplified)
inv.Customer = nil

doc, err := NewDocument(inv, ts, role, sw)
require.NoError(t, err)

assert.Equal(t, "S", doc.RegistroFactura.RegistroAlta.FacturaSinIdentifDestinatarioArt61d)
})
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/invopop/gobl v0.205.2-0.20241119180855-1b04b703647d
github.com/invopop/xmldsig v0.10.0
github.com/joho/godotenv v1.5.1
github.com/magefile/mage v1.15.0
github.com/nbio/xml v0.0.0-20241028124227-eac89c735a80
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
Expand All @@ -31,7 +32,6 @@ require (
github.com/invopop/validation v0.8.0 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 // indirect
Expand Down
2 changes: 1 addition & 1 deletion internal/gateways/gateways.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Environment string
// Environment to use for connections
const (
EnvironmentProduction Environment = "production"
EnvironmentTesting Environment = "testing"
EnvironmentSandbox Environment = "sandbox"

// Production environment not published yet
ProductionBaseURL = "xxxxxxxx"
Expand Down
Loading

0 comments on commit 57f9991

Please sign in to comment.