From 33d32f7cd32e5893ce5ee1d78e5203f7576a2559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Cavall=C3=A9?= Date: Tue, 3 Oct 2023 08:01:06 +0000 Subject: [PATCH] Add food vouchers complement to MX regime --- .../mx/examples/food-vouchers-complement.yaml | 54 ++++++++ .../out/food-vouchers-complement.json | 123 ++++++++++++++++++ regimes/mx/food_vouchers_complement.go | 122 +++++++++++++++++ regimes/mx/food_vouchers_complement_test.go | 70 ++++++++++ regimes/mx/mx.go | 5 +- 5 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 regimes/mx/examples/food-vouchers-complement.yaml create mode 100644 regimes/mx/examples/out/food-vouchers-complement.json create mode 100644 regimes/mx/food_vouchers_complement.go create mode 100644 regimes/mx/food_vouchers_complement_test.go diff --git a/regimes/mx/examples/food-vouchers-complement.yaml b/regimes/mx/examples/food-vouchers-complement.yaml new file mode 100644 index 00000000..0700a5a2 --- /dev/null +++ b/regimes/mx/examples/food-vouchers-complement.yaml @@ -0,0 +1,54 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +issue_date: "2023-07-10" +series: "TEST" +code: "00002" +supplier: + name: "ESCUELA KEMPER URGATE" + ext: + mx-cfdi-fiscal-regime: "601" + tax_id: + country: "MX" + code: "EKU9003173C9" + zone: "21000" +customer: + name: "UNIVERSIDAD ROBOTICA ESPAÑOLA" + ext: + mx-cfdi-fiscal-regime: "601" + mx-cfdi-use: "G01" + tax_id: + country: "MX" + code: "URE180429TM6" + zone: "86991" +lines: + - quantity: "1" + item: + name: "Comisión servicio de monedero electrónico" + price: "10.00" + ext: + mx-cfdi-prod-serv: "84141602" + taxes: + - cat: "VAT" + rate: "standard" +payment: + terms: + notes: "Condiciones de pago" + instructions: + key: "online+wallet" +complements: + - $schema: "https://gobl.org/draft-0/regimes/mx/food-vouchers-complement" + employer_registration: "12345678901234567890" + account_number: "0123456789" + lines: + - e_wallet_id: "ABC1234" + issue_date_time: "2022-07-19T10:20:30" + employee_tax_code: "JUFA7608212V6" + employee_curp: "JUFA760821MDFRRR00" + employee_name: "Adriana Juarez Fernández" + employee_social_security: "12345678901" + amount: 10.123 + - e_wallet_id: "BCD4321" + issue_date_time: "2022-08-20T11:20:30" + employee_tax_code: "KAHO641101B39" + employee_curp: "KAHO641101HDFRRR00" + employee_name: "Oscar Kala Haak" + amount: 20.4 diff --git a/regimes/mx/examples/out/food-vouchers-complement.json b/regimes/mx/examples/out/food-vouchers-complement.json new file mode 100644 index 00000000..8c3331d6 --- /dev/null +++ b/regimes/mx/examples/out/food-vouchers-complement.json @@ -0,0 +1,123 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "51b99ed26d97e48f28365eed31e5166c8ed8a68f52c9db47c1e7708e53592c56" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "type": "standard", + "series": "TEST", + "code": "00002", + "issue_date": "2023-07-10", + "currency": "MXN", + "supplier": { + "name": "ESCUELA KEMPER URGATE", + "tax_id": { + "country": "MX", + "zone": "21000", + "code": "EKU9003173C9" + }, + "ext": { + "mx-cfdi-fiscal-regime": "601" + } + }, + "customer": { + "name": "UNIVERSIDAD ROBOTICA ESPAÑOLA", + "tax_id": { + "country": "MX", + "zone": "86991", + "code": "URE180429TM6" + }, + "ext": { + "mx-cfdi-fiscal-regime": "601", + "mx-cfdi-use": "G01" + } + }, + "lines": [ + { + "i": 1, + "quantity": "1", + "item": { + "name": "Comisión servicio de monedero electrónico", + "price": "10.00", + "ext": { + "mx-cfdi-prod-serv": "84141602" + } + }, + "sum": "10.00", + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "16.0%" + } + ], + "total": "10.00" + } + ], + "payment": { + "terms": { + "notes": "Condiciones de pago" + }, + "instructions": { + "key": "online+wallet" + } + }, + "totals": { + "sum": "10.00", + "total": "10.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "10.00", + "percent": "16.0%", + "amount": "1.60" + } + ], + "amount": "1.60" + } + ], + "sum": "1.60" + }, + "tax": "1.60", + "total_with_tax": "11.60", + "payable": "11.60" + }, + "complements": [ + { + "$schema": "https://gobl.org/draft-0/regimes/mx/food-vouchers-complement", + "employer_registration": "12345678901234567890", + "account_number": "0123456789", + "total": "30.52", + "lines": [ + { + "e_wallet_id": "ABC1234", + "issue_date_time": "2022-07-19T10:20:30", + "employee_tax_code": "JUFA7608212V6", + "employee_curp": "JUFA760821MDFRRR00", + "employee_name": "Adriana Juarez Fernández", + "employee_social_security": "12345678901", + "amount": "10.12" + }, + { + "e_wallet_id": "BCD4321", + "issue_date_time": "2022-08-20T11:20:30", + "employee_tax_code": "KAHO641101B39", + "employee_curp": "KAHO641101HDFRRR00", + "employee_name": "Oscar Kala Haak", + "amount": "20.40" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/regimes/mx/food_vouchers_complement.go b/regimes/mx/food_vouchers_complement.go new file mode 100644 index 00000000..67e5b28c --- /dev/null +++ b/regimes/mx/food_vouchers_complement.go @@ -0,0 +1,122 @@ +package mx + +import ( + "regexp" + + "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/num" + "github.com/invopop/validation" +) + +// Constants for the precision of the complement's amounts +const ( + FoodVouchersFinalPrecision = 2 +) + +// Complement's Codes Patterns +const ( + CURPPattern = "^[A-Z][A,E,I,O,U,X][A-Z]{2}[0-9]{2}[0-1][0-9][0-3][0-9][M,H][A-Z]{2}[B,C,D,F,G,H,J,K,L,M,N,Ñ,P,Q,R,S,T,V,W,X,Y,Z]{3}[0-9,A-Z][0-9]$" + SocialSecurityPattern = "^[0-9]{11}$" +) + +// Complement's Codes Regexps +var ( + CURPRegexp = regexp.MustCompile(CURPPattern) + SocialSecurityRegexp = regexp.MustCompile(SocialSecurityPattern) +) + +// FoodVouchersComplement carries the data to produce a CFDI's "Complemento de +// Vales de Despensa" (version 1.0) providing detailed information about food +// vouchers issued by an e-wallet supplier to its customer's employees. +// +// This struct maps to the `ValesDeDespensa` root node in the CFDI's complement. +type FoodVouchersComplement struct { + // Customer's employer registration number (maps to `registroPatronal`). + EmployerRegistration string `json:"employer_registration,omitempty" jsonschema:"title=Employer Registration"` + // Customer's account number (maps to `numeroDeCuenta`). + AccountNumber string `json:"account_number" jsonschema:"title=Account Number"` + // Sum of all line amounts (calculated, maps to `total`). + Total num.Amount `json:"total" jsonschema:"title=Total" jsonschema_extras:"calculated=true"` + // List of food vouchers issued to the customer's employees (maps to `Conceptos`). + Lines []*FoodVouchersLine `json:"lines" jsonschema:"title=Lines"` +} + +// FoodVouchersLine represents a single food voucher issued to the e-wallet of +// one of the customer's employees. It maps to one `Concepto` node in the CFDI's +// complement. +type FoodVouchersLine struct { + // 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`). + IssueDateTime cal.DateTime `json:"issue_date_time" jsonschema:"title=Issue Date and Time"` + // Employee's tax identity code (maps to `rfc`). + EmployeeTaxCode cbc.Code `json:"employee_tax_code" jsonschema:"title=Employee's Tax Identity Code"` + // Employee's CURP ("Clave Única de Registro de Población", maps to `curp`). + EmployeeCURP cbc.Code `json:"employee_curp" jsonschema:"title=Employee's CURP"` + // Employee's name (maps to `nombre`). + EmployeeName string `json:"employee_name" jsonschema:"title=Employee's Name"` + // Employee's Social Security Number (maps to `numSeguridadSocial`). + EmployeeSocialSecurity cbc.Code `json:"employee_social_security,omitempty" jsonschema:"title=Employee's Social Security Number"` + // Amount of the food voucher (maps to `importe`). + Amount num.Amount `json:"amount" jsonschema:"title=Amount"` +} + +// Validate checks the FoodVouchersComplement data according to the SAT's +// rules for the "Complemento de Vales de Despensa". +func (fvc *FoodVouchersComplement) Validate() error { + return validation.ValidateStruct(fvc, + validation.Field(&fvc.EmployerRegistration, validation.Length(0, 20)), + validation.Field(&fvc.AccountNumber, + validation.Required, + validation.Length(0, 20), + ), + validation.Field(&fvc.Total, validation.Required), + validation.Field(&fvc.Lines, + validation.Required, + validation.Each(validation.By(validateFoodVouchersLine)), + ), + ) +} + +func validateFoodVouchersLine(value interface{}) error { + line := value.(*FoodVouchersLine) + if line == nil { + return nil + } + + return validation.ValidateStruct(line, + validation.Field(&line.EWalletID, + validation.Required, + validation.Length(0, 20), + ), + validation.Field(&line.IssueDateTime, cal.DateTimeNotZero()), + validation.Field(&line.EmployeeTaxCode, + validation.Required, + validation.By(validateTaxCode), + ), + validation.Field(&line.EmployeeCURP, + validation.Required, + validation.Match(CURPRegexp), + ), + validation.Field(&line.EmployeeName, + validation.Required, + validation.Length(0, 100), + ), + validation.Field(&line.EmployeeSocialSecurity, validation.Match(SocialSecurityRegexp)), + validation.Field(&line.Amount, validation.Required), + ) +} + +// Calculate performs the complement's calculations and normalisations. +func (fvc *FoodVouchersComplement) Calculate() error { + fvc.Total = num.MakeAmount(0, FoodVouchersFinalPrecision) + + for _, l := range fvc.Lines { + l.Amount = l.Amount.Rescale(FoodVouchersFinalPrecision) + + fvc.Total = fvc.Total.Add(l.Amount) + } + + return nil +} diff --git a/regimes/mx/food_vouchers_complement_test.go b/regimes/mx/food_vouchers_complement_test.go new file mode 100644 index 00000000..1d0d4a4f --- /dev/null +++ b/regimes/mx/food_vouchers_complement_test.go @@ -0,0 +1,70 @@ +package mx_test + +import ( + "testing" + + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/regimes/mx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInvalidFoodVouchersComplement(t *testing.T) { + fvc := &mx.FoodVouchersComplement{} + + err := fvc.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "account_number: cannot be blank") + assert.Contains(t, err.Error(), "lines: cannot be blank") + + fvc.EmployerRegistration = "123456789012345678901" + fvc.AccountNumber = "012345678901234567891" + + err = fvc.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "employer_registration: the length must be no more than 20") + assert.Contains(t, err.Error(), "account_number: the length must be no more than 20") +} + +func TestInvalidFoodVouchersLine(t *testing.T) { + fvc := &mx.FoodVouchersComplement{Lines: []*mx.FoodVouchersLine{{}}} + + err := fvc.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "e_wallet_id: cannot be blank") + assert.Contains(t, err.Error(), "issue_date_time: required") + assert.Contains(t, err.Error(), "employee_tax_code: cannot be blank") + assert.Contains(t, err.Error(), "employee_curp: cannot be blank") + assert.Contains(t, err.Error(), "employee_name: cannot be blank") + + fvc.Lines[0].EWalletID = "123456789012345678901" + fvc.Lines[0].EmployeeTaxCode = "INVALID1" + fvc.Lines[0].EmployeeCURP = "INVALID2" + fvc.Lines[0].EmployeeSocialSecurity = "INVALID3" + + err = fvc.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "e_wallet_id: the length must be no more than 20") + assert.Contains(t, err.Error(), "employee_tax_code: invalid tax identity code") + assert.Contains(t, err.Error(), "employee_curp: must be in a valid format") + assert.Contains(t, err.Error(), "employee_social_security: must be in a valid format") +} + +func TestCalculateFoodVouchersComplement(t *testing.T) { + fvc := &mx.FoodVouchersComplement{ + Lines: []*mx.FoodVouchersLine{ + {Amount: num.MakeAmount(1234, 3)}, + {Amount: num.MakeAmount(4321, 3)}, + }, + } + + err := fvc.Calculate() + + require.NoError(t, err) + assert.Equal(t, num.MakeAmount(123, 2), fvc.Lines[0].Amount) + assert.Equal(t, num.MakeAmount(555, 2), fvc.Total) +} diff --git a/regimes/mx/mx.go b/regimes/mx/mx.go index 09a88c0a..5fbd38c0 100644 --- a/regimes/mx/mx.go +++ b/regimes/mx/mx.go @@ -17,7 +17,10 @@ func init() { tax.RegisterRegime(New()) // MX GOBL Schema Complements - schema.Register(schema.GOBL.Add("regimes/mx"), FuelAccountBalance{}) + schema.Register(schema.GOBL.Add("regimes/mx"), + FuelAccountBalance{}, + FoodVouchersComplement{}, + ) } // Custom keys used typically in meta or codes information.