Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Food vouchers complement in MX #211

Merged
merged 4 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
cavalle marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading