diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be3c657..d72e06af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/addons/mx/cfdi/food_vouchers.go b/addons/mx/cfdi/food_vouchers.go index e64e542a..2beae37c 100644 --- a/addons/mx/cfdi/food_vouchers.go +++ b/addons/mx/cfdi/food_vouchers.go @@ -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`). @@ -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) diff --git a/addons/mx/cfdi/fuel_account_balance.go b/addons/mx/cfdi/fuel_account_balance.go index 4972e6fe..921691e6 100644 --- a/addons/mx/cfdi/fuel_account_balance.go +++ b/addons/mx/cfdi/fuel_account_balance.go @@ -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`). @@ -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, @@ -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 { diff --git a/addons/mx/cfdi/fuel_account_balance_test.go b/addons/mx/cfdi/fuel_account_balance_test.go index 8aea9049..63bf758f 100644 --- a/addons/mx/cfdi/fuel_account_balance_test.go +++ b/addons/mx/cfdi/fuel_account_balance_test.go @@ -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() @@ -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) { @@ -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": "", @@ -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": "", diff --git a/data/schemas/regimes/mx/food-vouchers.json b/data/schemas/regimes/mx/food-vouchers.json index 0f879976..9cedd146 100644 --- a/data/schemas/regimes/mx/food-vouchers.json +++ b/data/schemas/regimes/mx/food-vouchers.json @@ -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", @@ -94,6 +100,7 @@ }, "type": "object", "required": [ + "i", "e_wallet_id", "issue_date_time", "amount" diff --git a/data/schemas/regimes/mx/fuel-account-balance.json b/data/schemas/regimes/mx/fuel-account-balance.json index 82e9e503..e8c05531 100644 --- a/data/schemas/regimes/mx/fuel-account-balance.json +++ b/data/schemas/regimes/mx/fuel-account-balance.json @@ -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", @@ -125,6 +131,7 @@ }, "type": "object", "required": [ + "i", "e_wallet_id", "purchase_date_time", "vendor_tax_code", diff --git a/regimes/mx/examples/out/food-vouchers.json b/regimes/mx/examples/out/food-vouchers.json index 7ddaf99d..4ec30623 100644 --- a/regimes/mx/examples/out/food-vouchers.json +++ b/regimes/mx/examples/out/food-vouchers.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "24c15ebc61934602b26715e5a0231761475ef5239084daee7eb2f021acbdc78b" + "val": "386eef12c2b78902dfa2ef3b46eb3f06ebcd6909b1d40069417347493f607a54" } }, "doc": { @@ -110,6 +110,7 @@ "total": "30.52", "lines": [ { + "i": 1, "e_wallet_id": "ABC1234", "issue_date_time": "2022-07-19T10:20:30", "employee": { @@ -121,6 +122,7 @@ "amount": "10.12" }, { + "i": 2, "e_wallet_id": "BCD4321", "issue_date_time": "2022-08-20T11:20:30", "employee": { diff --git a/regimes/mx/examples/out/fuel-account-balance.json b/regimes/mx/examples/out/fuel-account-balance.json index 8336c02d..0b6c34f7 100644 --- a/regimes/mx/examples/out/fuel-account-balance.json +++ b/regimes/mx/examples/out/fuel-account-balance.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "a0b87156fa951dd596f01301595f2fd83eac97fb4395558ea77df63e4b7d0acd" + "val": "274ebf8cbe4ac7864a1f9d9835116f0e86277a06026012e08c8ba566f1833747" } }, "doc": { @@ -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", @@ -137,6 +138,7 @@ ] }, { + "i": 2, "e_wallet_id": "1234", "purchase_date_time": "2022-08-19T10:20:30", "vendor_tax_code": "DJV320816JT1", diff --git a/regimes/mx/mx.go b/regimes/mx/mx.go index f065c5fb..9dec90e0 100644 --- a/regimes/mx/mx.go +++ b/regimes/mx/mx.go @@ -58,7 +58,7 @@ func Normalize(doc any) { case *bill.Invoice: normalizeInvoice(obj) case *tax.Identity: - tax.NormalizeIdentity(obj) + NormalizeTaxIdentity(obj) } } diff --git a/regimes/mx/tax_identity.go b/regimes/mx/tax_identity.go index fe9e794a..e51c53ff 100644 --- a/regimes/mx/tax_identity.go +++ b/regimes/mx/tax_identity.go @@ -2,6 +2,7 @@ package mx import ( "regexp" + "strings" "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/tax" @@ -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 { diff --git a/regimes/mx/tax_identity_test.go b/regimes/mx/tax_identity_test.go index e43b14ba..592245ec 100644 --- a/regimes/mx/tax_identity_test.go +++ b/regimes/mx/tax_identity_test.go @@ -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} @@ -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 @@ -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", @@ -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