Skip to content

Commit

Permalink
Significant refactor to support missing regions and more concise API
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Nov 5, 2024
1 parent 293df7e commit d15e2a0
Show file tree
Hide file tree
Showing 67 changed files with 1,602 additions and 1,327 deletions.
67 changes: 49 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ Links to key information for each agency are described in the following subchapt

You must have first created a GOBL Envelope containing an Invoice that you'd like to send to one of the TicketBAI web services.

For the document to accepted, the supplier contained in the invoice should have a "Tax ID" that includes:
For the document to be converted, the supplier contained in the invoice should have a "Tax ID" with the country set to `ES`.

- A country code set to `ES`
- A zone code set to region of one of the three Basque Country tax agencies, i.e. `BI`, `SS`, or `VI`. (We don't consider the address field reliable for this.)
TicketBAI is used by three different tax agencies (Haciendas Forales), each of which has their own API and specific requirements. The `es-tbai-region` extension defined in the GOBL Invoice's `tax` property is used to set the determine the correct API to utilize. This will be set automatically in most cases.

The following is an example of how the GOBL TicketBAI package could be used:

Expand All @@ -47,13 +46,16 @@ import (
)

func main() {
ctx := context.Background()

// Load sample envelope:
data, _ := os.ReadFile("./test/data/sample-invoice.json")

env := new(gobl.Envelope)
if err := json.Unmarshal(data, env); err != nil {
panic(err)
}
zone := ticketbai.ZoneFor(env)

// Prepare software configuration:
soft := &ticketbai.Software{
Expand All @@ -70,8 +72,9 @@ func main() {
panic(err)
}

// Instantiate the TicketBAI client:
tbai, err := ticketbai.New(soft,
// Instantiate the TicketBAI client with sofrward config
// and specific zone.
tc, err := ticketbai.New(soft, zone,
ticketbai.WithCertificate(cert), // Use the certificate previously loaded
ticketbai.WithSupplierIssuer(), // The issuer is the invoice's supplier
ticketbai.InTesting(), // Use the tax agency testing environment
Expand All @@ -81,18 +84,19 @@ func main() {
}

// Create a new TBAI document:
doc, err := tbai.NewDocument(env)
doc, err := tc.Convert(env)
if err != nil {
panic(err)
}

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

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

Expand All @@ -102,8 +106,21 @@ func main() {
panic(err)
}

// Do something with the output
// Do something with the output, you probably want to store
// it somewhere.
fmt.Println("Document created:\n", string(bytes))

// 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 TicketBAI, 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 := tc.Post(ctx, doc); err != nil {
panic(err)
}

}
```

Expand All @@ -115,13 +132,29 @@ The GOBL TicketBAI package tool also includes a command line helper. You can fin
go install github.com/invopop/gobl.ticketbai
```

Usage is very straightforward:
We recommend using a `.env` file to prepare configuration settings, although all parameters can be set using command line flags. Heres an example:

```
CERTIFICATE_PATH="./test/certs/EntitateOrdezkaria_RepresentanteDeEntidad.p12"
CERTIFICATE_PASSWORD=IZDesa2021
SOFTWARE_COMPANY_NIF=B85905495
SOFTWARE_COMPANY_NAME="Invopop S.L."
SOFTWARE_NAME="Invopop"
SOFTWARE_LICENSE="TBAIBI00000000PRUEBA" # BI & SS
SOFTWARE_VERSION="1.0"
```

To convert a document to XML, run:

```bash
gobl.ticketbai convert ./test/data/invoice.json
gobl.ticketbai convert ./test/data/sample-invoice.json
```

At the moment, it's not possible to add a fingerprint or sign TicketBAI files using the CLI.
To submit to the tax agency testing environment:

```bash
gobl.ticketbai send ./test/data/sample-invoice.json
```

## Limitations

Expand Down Expand Up @@ -180,12 +213,10 @@ Under what situations should the TicketBAI system be expected to function:

## Test Data

Some sample test data is available in the `./test` directory.

If you make any modifications to the source YAML files, the JSON envelopes will need to be updated, for example:
Some sample test data is available in the `./test` directory. To update the JSON documents and regenerate the XML files for testing, use the following command:

```bash
gobl build -i --envelop test/data/invoice-es-es-b2c.yaml > test/data/invoice-es-es-b2c.json
go test ./examples_test.go --update
```

Make sure you have the GOBL CLI installed ([more details](https://docs.gobl.org/quick-start/cli)).
All generate XML documents will be validated against the TicketBAI XSD documents.
71 changes: 31 additions & 40 deletions cancel_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,77 +6,68 @@ import (
"time"

"github.com/invopop/gobl"
"github.com/invopop/gobl.ticketbai/internal/doc"
"github.com/invopop/gobl.ticketbai/doc"
"github.com/invopop/gobl/addons/es/tbai"
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/l10n"
"github.com/invopop/xmldsig"
)

// CancelDocument is a wrapper around the internal AnulaTicketBAI document.
type CancelDocument struct {
env *gobl.Envelope
inv *bill.Invoice
tbai *doc.AnulaTicketBAI // output

client *Client
}

// NewCancelDocument creates a new AnulaTicketBAI document from the provided

Check failure on line 16 in cancel_document.go

View workflow job for this annotation

GitHub Actions / golangci-lint

exported: comment on exported method Client.GenerateCancel should be of the form "GenerateCancel ..." (revive)
// GOBL Envelope.
func (c *Client) NewCancelDocument(env *gobl.Envelope) (*CancelDocument, error) {
d := new(CancelDocument)

// Set the client for later use
d.client = c

func (c *Client) GenerateCancel(env *gobl.Envelope) (*doc.AnulaTicketBAI, error) {
// Extract the Invoice
var ok bool
d.env = env
d.inv, ok = d.env.Extract().(*bill.Invoice)
inv, ok := env.Extract().(*bill.Invoice)
if !ok {
return nil, ErrOnlyInvoices
return nil, ErrValidation.withMessage("only invoices are supported")
}

if d.inv.Supplier.TaxID.Country != l10n.ES.Tax() {
return nil, ErrNotSpanish
if inv.Supplier.TaxID.Country != l10n.ES.Tax() {
return nil, ErrValidation.withMessage("only spanish invoices are supported")
}
zone := zoneFor(inv)
if zone == "" {
return nil, ErrValidation.withMessage("invalid zone")
}

// Extract the time when the invoice was posted to TicketBAI gateway
ts, err := extractPostTime(d.env)
ts, err := extractPostTime(env)
if err != nil {
return nil, err
}

// Create the document
d.tbai, err = doc.NewAnulaTicketBAI(d.inv, ts)
cd, err := doc.NewAnulaTicketBAI(inv, ts)
if err != nil {
return nil, err
}

return d, nil
return cd, nil
}

// Fingerprint generates a finger print for the TicketBAI document using the

Check failure on line 47 in cancel_document.go

View workflow job for this annotation

GitHub Actions / golangci-lint

exported: comment on exported method Client.FingerprintCancel should be of the form "FingerprintCancel ..." (revive)
// data provided from the previous invoice data.
func (d *CancelDocument) Fingerprint() error {
c := d.client // shortcut

conf := &doc.FingerprintConfig{
License: c.software.License,
NIF: c.software.NIF,
SoftwareName: c.software.Name,
SoftwareVersion: c.software.Version,
func (c *Client) FingerprintCancel(cd *doc.AnulaTicketBAI) error {
conf := &doc.Software{
License: c.software.License,
NIF: c.software.NIF,
Name: c.software.Name,
Version: c.software.Version,
}
return d.tbai.Fingerprint(conf)
return cd.Fingerprint(conf)
}

// Sign is used to generate the XML DSig components of the final XML document.

Check failure on line 59 in cancel_document.go

View workflow job for this annotation

GitHub Actions / golangci-lint

exported: comment on exported method Client.SignCancel should be of the form "SignCancel ..." (revive)
func (d *CancelDocument) Sign() error {
c := d.client // shortcut

dID := d.env.Head.UUID.String()
if err := d.tbai.Sign(dID, c.cert, c.issuerRole, xmldsig.WithCurrentTime(c.CurrentTime)); err != nil {
func (c *Client) SignCancel(cd *doc.AnulaTicketBAI, env *gobl.Envelope) error {
inv, ok := env.Extract().(*bill.Invoice)
if !ok {
return ErrValidation.withMessage("only invoices are supported")
}
zone := zoneFor(inv)
if zone == "" {
return ErrValidation.withMessage("invalid zone: '%s'", zone)
}
dID := env.Head.UUID.String()
if err := cd.Sign(dID, c.cert, c.issuerRole, zone, xmldsig.WithCurrentTime(c.CurrentTime)); err != nil {
return fmt.Errorf("signing: %w", err)
}

Expand Down
16 changes: 9 additions & 7 deletions cmd/gobl.ticketbai/cancel.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/invopop/gobl"
ticketbai "github.com/invopop/gobl.ticketbai"
"github.com/invopop/gobl/l10n"
"github.com/invopop/xmldsig"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -50,6 +49,10 @@ func (c *cancelOpts) runE(cmd *cobra.Command, args []string) error {
if err := json.Unmarshal(buf.Bytes(), env); err != nil {
return fmt.Errorf("unmarshaling gobl envelope: %w", err)
}
zone := ticketbai.ZoneFor(env)
if zone == "" {
return fmt.Errorf("no zone found in envelope")
}

cert, err := xmldsig.LoadCertificate(c.cert, c.password)
if err != nil {
Expand All @@ -58,7 +61,6 @@ func (c *cancelOpts) runE(cmd *cobra.Command, args []string) error {

opts := []ticketbai.Option{
ticketbai.WithCertificate(cert),
ticketbai.WithZone(l10n.Code(c.zone)),
ticketbai.WithThirdPartyIssuer(),
}

Expand All @@ -68,26 +70,26 @@ func (c *cancelOpts) runE(cmd *cobra.Command, args []string) error {
opts = append(opts, ticketbai.InTesting())
}

tbai, err := ticketbai.New(c.software(), opts...)
tc, err := ticketbai.New(c.software(), zone, opts...)
if err != nil {
panic(err)
}

doc, err := tbai.NewCancelDocument(env)
tcd, err := tc.GenerateCancel(env)
if err != nil {
panic(err)
}

err = doc.Fingerprint()
err = tc.FingerprintCancel(tcd)
if err != nil {
panic(err)
}

if err := doc.Sign(); err != nil {
if err := tc.SignCancel(tcd, env); err != nil {
panic(err)
}

err = tbai.Cancel(cmd.Context(), doc)
err = tc.Cancel(cmd.Context(), tcd)
if err != nil {
panic(err)
}
Expand Down
10 changes: 7 additions & 3 deletions cmd/gobl.ticketbai/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,22 @@ func (c *convertOpts) runE(cmd *cobra.Command, args []string) error {
if err := json.Unmarshal(buf.Bytes(), env); err != nil {
return fmt.Errorf("unmarshaling gobl envelope: %w", err)
}
zone := ticketbai.ZoneFor(env)
if zone == "" {
return fmt.Errorf("no zone found in envelope")
}

tbai, err := ticketbai.New(&ticketbai.Software{})
tc, err := ticketbai.New(&ticketbai.Software{}, zone)
if err != nil {
return fmt.Errorf("creating ticketbai client: %w", err)
}

doc, err := tbai.NewDocument(env)
td, err := tc.Convert(env)
if err != nil {
panic(err)
}

data, err := doc.BytesIndent()
data, err := td.BytesIndent()
if err != nil {
return fmt.Errorf("generating ticketbai xml: %w", err)
}
Expand Down
Loading

0 comments on commit d15e2a0

Please sign in to comment.