-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add food vouchers complement to MX regime
- Loading branch information
Showing
5 changed files
with
373 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters