Skip to content

Commit

Permalink
Significant refactor of extension key-value handling
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Dec 5, 2023
1 parent d958dfd commit 611bffe
Show file tree
Hide file tree
Showing 24 changed files with 419 additions and 170 deletions.
54 changes: 54 additions & 0 deletions cbc/key_or_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cbc

import (
"errors"

"github.com/invopop/jsonschema"
)

// KeyOrCode is a special type that can be either a Key or a Code. This is meant
// for situations when the value to be used has to be flexible enough
// to either a key defined by GOBL, or a code usually defined by an external
// entity.
type KeyOrCode string

// String provides the string representation.
func (kc KeyOrCode) String() string {
return string(kc)
}

// Key returns the key value or empty if the value is a Code.
func (kc KeyOrCode) Key() Key {
k := Key(kc)
if err := k.Validate(); err == nil {
return k
}
return KeyEmpty
}

// Code returns the code value or empty if the value is a Key.
func (kc KeyOrCode) Code() Code {
c := Code(kc)
if err := c.Validate(); err == nil {
return c
}
return CodeEmpty
}

// Validate ensures the value is either a key or a code.
func (kc KeyOrCode) Validate() error {
if err := Key(kc).Validate(); err == nil {
return nil
}
if err := Code(kc).Validate(); err == nil {
return nil
}
return errors.New("value is not a key or code")
}

func (KeyOrCode) JSONSchemaExtend(schema *jsonschema.Schema) {

Check warning on line 49 in cbc/key_or_code.go

View workflow job for this annotation

GitHub Actions / golangci-lint

exported: exported method KeyOrCode.JSONSchemaExtend should have comment or be unexported (revive)
schema.OneOf = []*jsonschema.Schema{
KeyEmpty.JSONSchema(),
CodeEmpty.JSONSchema(),
}
}
29 changes: 29 additions & 0 deletions cbc/key_or_code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cbc_test

import (
"testing"

"github.com/invopop/gobl/cbc"
"github.com/stretchr/testify/assert"
)

func TestKeyOrCode(t *testing.T) {
kc := cbc.KeyOrCode("IT")
assert.Equal(t, "IT", kc.String())
assert.NoError(t, kc.Validate())
assert.Equal(t, cbc.Code("IT"), kc.Code())
assert.Equal(t, cbc.KeyEmpty, kc.Key())

kc = cbc.KeyOrCode("testing")
assert.Equal(t, "testing", kc.String())
assert.NoError(t, kc.Validate())
assert.Equal(t, cbc.Key("testing"), kc.Key())
assert.Equal(t, cbc.CodeEmpty, kc.Code())

kc = cbc.KeyOrCode("INvalid")
if assert.Error(t, kc.Validate()) {
assert.Contains(t, kc.Validate().Error(), "value is not a key or code")
}
assert.Equal(t, cbc.CodeEmpty, kc.Code())
assert.Equal(t, cbc.KeyEmpty, kc.Key())
}
2 changes: 1 addition & 1 deletion org/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type Item struct {
// Country code of where this item was from originally.
Origin l10n.CountryCode `json:"origin,omitempty" jsonschema:"title=Country of Origin"`
// Extension code map for any additional regime specific codes that may be required.
Ext cbc.CodeMap `json:"ext,omitempty" jsonschema:"title=Ext"`
Ext tax.ExtMap `json:"ext,omitempty" jsonschema:"title=Ext"`
// Additional meta information that may be useful
Meta cbc.Meta `json:"meta,omitempty" jsonschema:"title=Meta"`
}
Expand Down
2 changes: 1 addition & 1 deletion org/party.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type Party struct {
// Images that can be used to identify the party visually.
Logos []*Image `json:"logos,omitempty" jsonschema:"title=Logos"`
// Extension code map for any additional regime specific codes that may be required.
Ext cbc.CodeMap `json:"ext,omitempty" jsonschema:"title=Ext"`
Ext tax.ExtMap `json:"ext,omitempty" jsonschema:"title=Ext"`
// Any additional semi-structured information that does not fit into the rest of the party.
Meta cbc.Meta `json:"meta,omitempty" jsonschema:"title=Meta"`
}
Expand Down
24 changes: 24 additions & 0 deletions regimes/es/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,33 @@ import (
// Spanish regime extension codes for local electronic formats.
const (
ExtKeyTBAIExemption = "es-tbai-exemption"
ExtKeyTBAIProduct = "es-tbai-product"
)

var extensionKeys = []*tax.KeyDefinition{
{
Key: ExtKeyTBAIProduct,
Name: i18n.String{
i18n.EN: "TicketBAI Product Key",
i18n.ES: "Clave de Producto TicketBAI",
},
Keys: []*tax.KeyDefinition{
{
Key: "goods",
Name: i18n.String{
i18n.ES: "Entrega de bienes",
i18n.EN: "Delivery of goods",
},
},
{
Key: "services",
Name: i18n.String{
i18n.ES: "Prestacion de servicios",
i18n.EN: "Provision of services",
},
},
},
},
{
Key: ExtKeyTBAIExemption,
Name: i18n.String{
Expand Down
3 changes: 1 addition & 2 deletions regimes/it/invoice_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cal"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/l10n"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/org"
Expand Down Expand Up @@ -128,7 +127,7 @@ func TestRetainedTaxesValidation(t *testing.T) {
inv = testInvoiceStandard(t)
inv.Lines[0].Taxes = append(inv.Lines[0].Taxes, &tax.Combo{
Category: "IRPEF",
Ext: cbc.CodeMap{
Ext: tax.ExtMap{
it.ExtKeySDIRetainedTax: "A",
},
Percent: num.NewPercentage(20, 2),
Expand Down
5 changes: 2 additions & 3 deletions regimes/mx/invoice_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"

"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/currency"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/org"
Expand Down Expand Up @@ -71,7 +70,7 @@ func (v *invoiceValidator) validCustomer(value interface{}) error {
validation.Skip,
),
validation.Field(&obj.Ext,
cbc.CodeMapHas(ExtKeyCFDIFiscalRegime, ExtKeyCFDIUse),
tax.ExtMapHas(ExtKeyCFDIFiscalRegime, ExtKeyCFDIUse),
),
)
}
Expand All @@ -87,7 +86,7 @@ func (v *invoiceValidator) validSupplier(value interface{}) error {
validation.Skip,
),
validation.Field(&obj.Ext,
cbc.CodeMapHas(ExtKeyCFDIFiscalRegime),
tax.ExtMapHas(ExtKeyCFDIFiscalRegime),
),
)
}
Expand Down
9 changes: 4 additions & 5 deletions regimes/mx/invoice_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cal"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/head"
"github.com/invopop/gobl/l10n"
"github.com/invopop/gobl/num"
Expand All @@ -25,7 +24,7 @@ func validInvoice() *bill.Invoice {
IssueDate: cal.MakeDate(2023, 1, 1),
Supplier: &org.Party{
Name: "Test Supplier",
Ext: cbc.CodeMap{
Ext: tax.ExtMap{
mx.ExtKeyCFDIFiscalRegime: "601",
},
TaxID: &tax.Identity{
Expand All @@ -36,7 +35,7 @@ func validInvoice() *bill.Invoice {
},
Customer: &org.Party{
Name: "Test Customer",
Ext: cbc.CodeMap{
Ext: tax.ExtMap{
mx.ExtKeyCFDIFiscalRegime: "608",
mx.ExtKeyCFDIUse: "G01",
},
Expand All @@ -53,7 +52,7 @@ func validInvoice() *bill.Invoice {
Name: "bogus",
Price: num.MakeAmount(10000, 2),
Unit: org.UnitPackage,
Ext: cbc.CodeMap{
Ext: tax.ExtMap{
mx.ExtKeyCFDIProdServ: "01010101",
},
},
Expand Down Expand Up @@ -146,7 +145,7 @@ func TestPaymentTermsValidation(t *testing.T) {
func TestUsoCFDIScenarioValidation(t *testing.T) {
inv := validInvoice()

inv.Customer.Ext = cbc.CodeMap{
inv.Customer.Ext = tax.ExtMap{
mx.ExtKeyCFDIFiscalRegime: "601",
}
assertValidationError(t, inv, "ext: (mx-cfdi-use: required.)")
Expand Down
11 changes: 6 additions & 5 deletions regimes/mx/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/tax"
"github.com/invopop/validation"
)

Expand All @@ -17,15 +18,15 @@ var (
func validateItem(item *org.Item) error {
return validation.ValidateStruct(item,
validation.Field(&item.Ext,
cbc.CodeMapHas(ExtKeyCFDIProdServ),
tax.ExtMapHas(ExtKeyCFDIProdServ),
validation.By(validItemExtensions),
validation.Skip,
),
)
}

func validItemExtensions(value interface{}) error {
ids, ok := value.(cbc.CodeMap)
ids, ok := value.(tax.ExtMap)
if !ok {
return nil
}
Expand Down Expand Up @@ -57,9 +58,9 @@ func normalizeItem(item *org.Item) error {
for _, v := range item.Identities {
if v.Key.In(migratedExtensionKeys...) {
if item.Ext == nil {
item.Ext = make(cbc.CodeMap)
item.Ext = make(tax.ExtMap)
}
item.Ext[v.Key] = v.Code
item.Ext[v.Key] = cbc.KeyOrCode(v.Code)
} else {
idents = append(idents, v)
}
Expand All @@ -69,7 +70,7 @@ func normalizeItem(item *org.Item) error {
for k, v := range item.Ext {
if k == ExtKeyCFDIProdServ {
if itemExtensionNormalizableCodeRegexp.MatchString(v.String()) {
item.Ext[k] = cbc.Code(v.String() + "00")
item.Ext[k] = cbc.KeyOrCode(v.String() + "00")
}
}
}
Expand Down
21 changes: 11 additions & 10 deletions regimes/mx/item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/regimes/mx"
"github.com/invopop/gobl/tax"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -19,7 +20,7 @@ func TestItemValidation(t *testing.T) {
{
name: "valid item",
item: &org.Item{
Ext: cbc.CodeMap{
Ext: tax.ExtMap{
mx.ExtKeyCFDIProdServ: "12345678",
},
},
Expand All @@ -32,14 +33,14 @@ func TestItemValidation(t *testing.T) {
{
name: "empty extension",
item: &org.Item{
Ext: cbc.CodeMap{},
Ext: tax.ExtMap{},
},
err: "ext: (mx-cfdi-prod-serv: required.)",
},
{
name: "missing SAT identity",
item: &org.Item{
Ext: cbc.CodeMap{
Ext: tax.ExtMap{
"random": "12345678",
},
},
Expand All @@ -48,8 +49,8 @@ func TestItemValidation(t *testing.T) {
{
name: "invalid code format",
item: &org.Item{
Ext: cbc.CodeMap{
mx.ExtKeyCFDIProdServ: "ABC2",
Ext: tax.ExtMap{
mx.ExtKeyCFDIProdServ: "AbC2",
},
},
err: "ext: (mx-cfdi-prod-serv: must have 8 digits.)",
Expand All @@ -73,8 +74,8 @@ func TestItemValidation(t *testing.T) {
func TestItemIdentityNormalization(t *testing.T) {
r := mx.New()
tests := []struct {
Code cbc.Code
Expected cbc.Code
Code cbc.KeyOrCode
Expected cbc.KeyOrCode
}{
{
Code: "123456",
Expand All @@ -90,7 +91,7 @@ func TestItemIdentityNormalization(t *testing.T) {
},
}
for _, ts := range tests {
item := &org.Item{Ext: cbc.CodeMap{mx.ExtKeyCFDIProdServ: ts.Code}}
item := &org.Item{Ext: tax.ExtMap{mx.ExtKeyCFDIProdServ: ts.Code}}
err := r.CalculateObject(item)
assert.NoError(t, err)
assert.Equal(t, ts.Expected, item.Ext[mx.ExtKeyCFDIProdServ])
Expand All @@ -101,7 +102,7 @@ func TestItemIdentityNormalization(t *testing.T) {
inv.Lines[0].Item.Ext[mx.ExtKeyCFDIProdServ] = "010101"
err := inv.Calculate()
require.NoError(t, err)
assert.Equal(t, cbc.Code("01010100"), inv.Lines[0].Item.Ext[mx.ExtKeyCFDIProdServ])
assert.Equal(t, cbc.KeyOrCode("01010100"), inv.Lines[0].Item.Ext[mx.ExtKeyCFDIProdServ])
}

func TestItemIdentityMigration(t *testing.T) {
Expand All @@ -117,5 +118,5 @@ func TestItemIdentityMigration(t *testing.T) {

err := inv.Calculate()
require.NoError(t, err)
assert.Equal(t, cbc.Code("01010101"), inv.Lines[0].Item.Ext[mx.ExtKeyCFDIProdServ])
assert.Equal(t, cbc.KeyOrCode("01010101"), inv.Lines[0].Item.Ext[mx.ExtKeyCFDIProdServ])
}
5 changes: 3 additions & 2 deletions regimes/mx/party.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mx
import (
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/tax"
)

func normalizeParty(p *org.Party) error {
Expand All @@ -12,9 +13,9 @@ func normalizeParty(p *org.Party) error {
for _, v := range p.Identities {
if v.Key.In(migratedExtensionKeys...) {
if p.Ext == nil {
p.Ext = make(cbc.CodeMap)
p.Ext = make(tax.ExtMap)
}
p.Ext[v.Key] = v.Code
p.Ext[v.Key] = cbc.KeyOrCode(v.Code)
} else {
idents = append(idents, v)
}
Expand Down
Loading

0 comments on commit 611bffe

Please sign in to comment.