Skip to content

Commit

Permalink
Add food vouchers complement to MX regime
Browse files Browse the repository at this point in the history
  • Loading branch information
cavalle committed Oct 3, 2023
1 parent 8f2e39d commit 33d32f7
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 1 deletion.
54 changes: 54 additions & 0 deletions regimes/mx/examples/food-vouchers-complement.yaml
Original file line number Diff line number Diff line change
@@ -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
123 changes: 123 additions & 0 deletions regimes/mx/examples/out/food-vouchers-complement.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
}
122 changes: 122 additions & 0 deletions regimes/mx/food_vouchers_complement.go
Original file line number Diff line number Diff line change
@@ -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
}
70 changes: 70 additions & 0 deletions regimes/mx/food_vouchers_complement_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
5 changes: 4 additions & 1 deletion regimes/mx/mx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 33d32f7

Please sign in to comment.