Skip to content

Commit

Permalink
Merge pull request #405 from invopop/fix-mx-rfc-symbol
Browse files Browse the repository at this point in the history
Fix MX RFC symbol
  • Loading branch information
samlown authored Oct 31, 2024
2 parents 2c62b5c + 66f928a commit fd93c96
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- `tax`: Regime `ChargeKeys` removed. Keys now provided in `bill` package.
- `it`: Charge keys no longer defined, no migration required, already supported.

### Fixed

- `mx`: Tax ID validation now correctly supports `&` and `Ñ` symbols in codes.

## [v0.203.0]

### Added
Expand Down
5 changes: 4 additions & 1 deletion addons/mx/cfdi/food_vouchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type FoodVouchers struct {
// one of the customer's employees. It maps to one `Concepto` node in the CFDI's
// complement.
type FoodVouchersLine struct {
// Line number starting from 1 (calculated).
Index int `json:"i" jsonschema:"title=Index" jsonschema_extras:"calculated=true"`
// Identifier of the e-wallet that received the food voucher (maps to `Identificador`).
EWalletID cbc.Code `json:"e_wallet_id" jsonschema:"title=E-wallet Identifier"`
// Date and time of the food voucher's issue (maps to `Fecha`).
Expand Down Expand Up @@ -125,7 +127,8 @@ func (fve *FoodVouchersEmployee) Validate() error {
func (fvc *FoodVouchers) Calculate() error {
fvc.Total = num.MakeAmount(0, FoodVouchersFinalPrecision)

for _, l := range fvc.Lines {
for i, l := range fvc.Lines {
l.Index = i + 1
l.Amount = l.Amount.Rescale(FoodVouchersFinalPrecision)

fvc.Total = fvc.Total.Add(l.Amount)
Expand Down
6 changes: 5 additions & 1 deletion addons/mx/cfdi/fuel_account_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type FuelAccountBalance struct {
// issued by the invoice's supplier. It maps to one
// `ConceptoEstadoDeCuentaCombustible` node in the CFDI's complement.
type FuelAccountLine struct {
// Index of the line starting from 1 (calculated)
Index int `json:"i" jsonschema:"title=Index" jsonschema_extras:"calculated=true"`
// Identifier of the e-wallet used to make the purchase (maps to `Identificador`).
EWalletID cbc.Code `json:"e_wallet_id" jsonschema:"title=E-wallet Identifier"`
// Date and time of the purchase (maps to `Fecha`).
Expand Down Expand Up @@ -113,6 +115,7 @@ func (fal *FuelAccountLine) Validate() error {
validation.Field(&fal.VendorTaxCode,
validation.Required,
validation.By(mx.ValidateTaxCode),
validation.Skip, // don't use default code validations
),
validation.Field(&fal.ServiceStationCode,
validation.Required,
Expand Down Expand Up @@ -179,7 +182,8 @@ func (fab *FuelAccountBalance) Calculate() error {
taxtotal := num.MakeAmount(0, FuelAccountTotalsPrecision)
fab.Subtotal = num.MakeAmount(0, FuelAccountTotalsPrecision)

for _, l := range fab.Lines {
for i, l := range fab.Lines {
l.Index = i + 1
// Normalise amounts to the expected precision
l.Quantity = l.Quantity.RescaleUp(FuelAccountPriceMinimumPrecision)
if l.Item != nil {
Expand Down
8 changes: 7 additions & 1 deletion addons/mx/cfdi/fuel_account_balance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestInvalidComplement(t *testing.T) {
assert.Contains(t, err.Error(), "lines: cannot be blank")
}

func TestInvalidLine(t *testing.T) {
func TestFuelAccountInvalidLine(t *testing.T) {
fab := &cfdi.FuelAccountBalance{Lines: []*cfdi.FuelAccountLine{{}}}

err := fab.Validate()
Expand All @@ -47,6 +47,10 @@ func TestInvalidLine(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "vendor_tax_code: invalid tax identity code")
assert.Contains(t, err.Error(), "total: must be quantity x unit_price")

fab.Lines[0].VendorTaxCode = "K&A010301I16" // with symbols
err = fab.Validate()
assert.NotContains(t, err.Error(), "vendor_tax_code")
}

func TestInvalidItem(t *testing.T) {
Expand Down Expand Up @@ -270,6 +274,7 @@ func TestCalculate(t *testing.T) {
"total": "12.34",
"lines": [
{
"i": 1,
"e_wallet_id": "",
"purchase_date_time": "0000-00-00T00:00:00",
"vendor_tax_code": "",
Expand Down Expand Up @@ -349,6 +354,7 @@ func TestCalculate(t *testing.T) {
"total": "3832.93",
"lines": [
{
"i": 1,
"e_wallet_id": "",
"purchase_date_time": "0000-00-00T00:00:00",
"vendor_tax_code": "",
Expand Down
7 changes: 7 additions & 0 deletions data/schemas/regimes/mx/food-vouchers.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
},
"FoodVouchersLine": {
"properties": {
"i": {
"type": "integer",
"title": "Index",
"description": "Line number starting from 1 (calculated).",
"calculated": true
},
"e_wallet_id": {
"$ref": "https://gobl.org/draft-0/cbc/code",
"title": "E-wallet Identifier",
Expand All @@ -94,6 +100,7 @@
},
"type": "object",
"required": [
"i",
"e_wallet_id",
"issue_date_time",
"amount"
Expand Down
7 changes: 7 additions & 0 deletions data/schemas/regimes/mx/fuel-account-balance.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@
},
"FuelAccountLine": {
"properties": {
"i": {
"type": "integer",
"title": "Index",
"description": "Index of the line starting from 1 (calculated)",
"calculated": true
},
"e_wallet_id": {
"$ref": "https://gobl.org/draft-0/cbc/code",
"title": "E-wallet Identifier",
Expand Down Expand Up @@ -125,6 +131,7 @@
},
"type": "object",
"required": [
"i",
"e_wallet_id",
"purchase_date_time",
"vendor_tax_code",
Expand Down
4 changes: 3 additions & 1 deletion regimes/mx/examples/out/food-vouchers.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"uuid": "8a51fd30-2a27-11ee-be56-0242ac120002",
"dig": {
"alg": "sha256",
"val": "24c15ebc61934602b26715e5a0231761475ef5239084daee7eb2f021acbdc78b"
"val": "386eef12c2b78902dfa2ef3b46eb3f06ebcd6909b1d40069417347493f607a54"
}
},
"doc": {
Expand Down Expand Up @@ -110,6 +110,7 @@
"total": "30.52",
"lines": [
{
"i": 1,
"e_wallet_id": "ABC1234",
"issue_date_time": "2022-07-19T10:20:30",
"employee": {
Expand All @@ -121,6 +122,7 @@
"amount": "10.12"
},
{
"i": 2,
"e_wallet_id": "BCD4321",
"issue_date_time": "2022-08-20T11:20:30",
"employee": {
Expand Down
4 changes: 3 additions & 1 deletion regimes/mx/examples/out/fuel-account-balance.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"uuid": "8a51fd30-2a27-11ee-be56-0242ac120002",
"dig": {
"alg": "sha256",
"val": "a0b87156fa951dd596f01301595f2fd83eac97fb4395558ea77df63e4b7d0acd"
"val": "274ebf8cbe4ac7864a1f9d9835116f0e86277a06026012e08c8ba566f1833747"
}
},
"doc": {
Expand Down Expand Up @@ -110,6 +110,7 @@
"total": "400.00",
"lines": [
{
"i": 1,
"e_wallet_id": "1234",
"purchase_date_time": "2022-07-19T10:20:30",
"vendor_tax_code": "RWT860605OF5",
Expand Down Expand Up @@ -137,6 +138,7 @@
]
},
{
"i": 2,
"e_wallet_id": "1234",
"purchase_date_time": "2022-08-19T10:20:30",
"vendor_tax_code": "DJV320816JT1",
Expand Down
2 changes: 1 addition & 1 deletion regimes/mx/mx.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func Normalize(doc any) {
case *bill.Invoice:
normalizeInvoice(obj)
case *tax.Identity:
tax.NormalizeIdentity(obj)
NormalizeTaxIdentity(obj)
}
}

Expand Down
26 changes: 24 additions & 2 deletions regimes/mx/tax_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mx

import (
"regexp"
"strings"

"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/tax"
Expand Down Expand Up @@ -30,25 +31,46 @@ const (

// Tax Identity Patterns
const (
TaxIdentityPatternPerson = `^([A-ZÑ&]{4})([0-9]{6})([A-Z0-9]{3})$`
TaxIdentityPatternCompany = `^([A-ZÑ&]{3})([0-9]{6})([A-Z0-9]{3})$`
TaxIdentityPatternPerson = `^([A-ZÑ\&]{4})([0-9]{6})([A-Z0-9]{3})$`
TaxIdentityPatternCompany = `^([A-ZÑ\&]{3})([0-9]{6})([A-Z0-9]{3})$`
)

// Tax Identity Regexp
var (
TaxIdentityRegexpPerson = regexp.MustCompile(TaxIdentityPatternPerson)
TaxIdentityRegexpCompany = regexp.MustCompile(TaxIdentityPatternCompany)
TaxCodeBadCharsRegexp = regexp.MustCompile(`[^A-ZÑ\&0-9]+`)
)

// ValidateTaxIdentity validates a tax identity for SAT.
func ValidateTaxIdentity(tID *tax.Identity) error {
if tID == nil {
return nil
}
return validation.ValidateStruct(tID,
validation.Field(&tID.Code,
validation.By(ValidateTaxCode),
validation.Skip, // don't apply regular code validation
),
)
}

// NormalizeTaxIdentity ensures the tax code is good for mexico
func NormalizeTaxIdentity(tID *tax.Identity) {
if tID == nil {
return
}
tID.Code = NormalizeTaxCode(tID.Code)
}

// NormalizeTaxCode normalizes a tax code for SAT using the special
// rules it requires.
func NormalizeTaxCode(code cbc.Code) cbc.Code {
c := strings.ToUpper(code.String())
c = TaxCodeBadCharsRegexp.ReplaceAllString(c, "")
return cbc.Code(c)
}

// ValidateTaxCode validates a tax code according to the rules
// defined by the Mexican SAT.
func ValidateTaxCode(value interface{}) error {
Expand Down
25 changes: 25 additions & 0 deletions regimes/mx/tax_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ func TestTaxIdentityNormalization(t *testing.T) {
Code: "GHI-701231-23Z",
Expected: "GHI70123123Z",
},
{
Code: "K&A010301I16",
Expected: "K&A010301I16",
},
}
for _, ts := range tests {
tID := &tax.Identity{Country: "MX", Code: ts.Code}
Expand All @@ -36,7 +40,20 @@ func TestTaxIdentityNormalization(t *testing.T) {
}
}

func TestNormalizeTaxIdentity(t *testing.T) {
t.Run("nil", func(t *testing.T) {
tID := (*tax.Identity)(nil)
assert.NotPanics(t, func() {
mx.NormalizeTaxIdentity(tID)
})
})
}

func TestTaxIdentityValidation(t *testing.T) {
t.Run("nil", func(t *testing.T) {
tID := (*tax.Identity)(nil)
assert.NoError(t, mx.Validate(tID))
})
tests := []struct {
name string
code cbc.Code
Expand All @@ -47,6 +64,7 @@ func TestTaxIdentityValidation(t *testing.T) {
{name: "valid code 1", code: "MNOP8201019HJ"},
{name: "valid code 2", code: "UVWX610715JKL"},
{name: "valid code 3", code: "STU760612MN1"},
{name: "with symbol", code: "K&A010301I16"},
{
name: "invalid code 1",
code: "STU760612MN",
Expand Down Expand Up @@ -86,6 +104,13 @@ func TestTaxIdentityValidation(t *testing.T) {
}
}

func TestValidateTaxCode(t *testing.T) {
t.Run("empty", func(t *testing.T) {
err := mx.ValidateTaxCode("")
assert.NoError(t, err)
})
}

func TestTaxIdentityDetermineType(t *testing.T) {
tests := []struct {
Code cbc.Code
Expand Down

0 comments on commit fd93c96

Please sign in to comment.