From 0125a2035a83098ecc9b8d6897ad012c47279ac0 Mon Sep 17 00:00:00 2001 From: richifernandez Date: Tue, 26 Nov 2024 22:01:41 +0100 Subject: [PATCH 01/11] add examples for Indian tax regime --- examples/in/invoice-in-in-simplified.yaml | 42 +++++++++++++ examples/in/invoice-in-in-stdr.yaml | 57 ++++++++++++++++++ examples/in/out/invoice-in-in-simplified.json | Bin 0 -> 3628 bytes examples/in/out/invoice-in-in-stdr.json | Bin 0 -> 4390 bytes 4 files changed, 99 insertions(+) create mode 100644 examples/in/invoice-in-in-simplified.yaml create mode 100644 examples/in/invoice-in-in-stdr.yaml create mode 100644 examples/in/out/invoice-in-in-simplified.json create mode 100644 examples/in/out/invoice-in-in-stdr.json diff --git a/examples/in/invoice-in-in-simplified.yaml b/examples/in/invoice-in-in-simplified.yaml new file mode 100644 index 00000000..16e878b8 --- /dev/null +++ b/examples/in/invoice-in-in-simplified.yaml @@ -0,0 +1,42 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +currency: "INR" +issue_date: "2022-02-01" +series: "SAMPLE" +code: "001" +tax: + ext: + in-supply-place: "Maharashtra" + +supplier: + tax_id: + country: "IN" + code: "27AAPFU0939F1ZV" + name: "Provide One LLC" + emails: + - addr: "billing@example.in" + addresses: + - num: "16" + street: "Baner Road" + locality: "Baner" + code: "411045" + region: "Maharashtra" + country: "IN" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + identities: + - type: "HSN" + code: "123456" + discounts: + - percent: "5%" + reason: "Special discount" + taxes: + - cat: CGST + percent: 9% + - cat: SGST + percent: 9% diff --git a/examples/in/invoice-in-in-stdr.yaml b/examples/in/invoice-in-in-stdr.yaml new file mode 100644 index 00000000..b8e654ff --- /dev/null +++ b/examples/in/invoice-in-in-stdr.yaml @@ -0,0 +1,57 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +currency: "INR" +issue_date: "2022-02-01" +series: "SAMPLE" +code: "001" +tax: + ext: + in-supply-place: "Maharashtra" + +supplier: + tax_id: + country: "IN" + code: "27AAPFU0939F1ZV" + name: "Provide One LLC" + emails: + - addr: "billing@example.in" + addresses: + - num: "101" + street: "Dr. Annie Besant Road" + locality: "Worli" + code: "400018" + region: "Maharashtra" + country: "IN" + +customer: + tax_id: + country: "IN" + code: "27AAPFU0939F1ZV" + name: "Sample Consumer" + emails: + - addr: "email@sample.in" + addresses: + - num: "202" + street: "MG Road" + locality: "Bengaluru" + code: "560001" + region: "Karnataka" + country: "IN" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + identities: + - type: "HSN" + code: "123456" + discounts: + - percent: "5%" + reason: "Special discount" + taxes: + - cat: CGST + percent: 9% + - cat: SGST + percent: 9% diff --git a/examples/in/out/invoice-in-in-simplified.json b/examples/in/out/invoice-in-in-simplified.json new file mode 100644 index 0000000000000000000000000000000000000000..d307b621012ff7e95b95cd728ffb40789e00d524 GIT binary patch literal 3628 zcmeHKT~8B16uqxC@jsA_zFFwDRD>rHMNB{mRUagVEbT&@6k6MY5W`6Ja5Sg&V89-j4`(*YZ&J;l2aMV zDy|betw|5pLe3ynmj<3sWq|o1?gnxx*N_`PZwXz`jI9aYoMYy~&3Iey z-TTslof5|eY_#P$?6qYR^Dl6>j&~)FI^HyJ?cweWGiTDox#6VRxG!*TJ(n_3b_e9DuIT|cVt@*6V3hE*iwM*kB6t*8VKg`MdCfJmUE=L5dCwVUt?oP<1DPST*WI<4 zXVK$y0KI*OQ2|-9ge=JB8Lr9Q{O-W&tSQT7;y-x5jaE^l;GnBNbtIk zv4i!>DEjRqJu!?4@p7ISxx#vS8Y5=)sHDwZ6O7x&IFw!agp6!T3&$?<`9(fMv#%h= zZ2c+VGi!rvA#;>7$M^Czu^!U$B|N;q)nlB4#;~ZQr%n zdHx1x&5cH+J&W8wmzO~D#xarnFVXAxKIA#}!uu9-pE-P)jnosHZ&sSOpPNXS;cs98 z^Es>JiCrPajXKyjlrvVp=TlryuOWMlgG?B^N~4t(cuMZD&e*BxcQu#30@Wwm11+DK z_t*!|gld=FlTOs@DdmT+Q(JvGI$+CjhSd<5KzZ#leywE$-(-l9{o3z7zNWZ4tm3F5 zNojTm;;F3YNA^O7y>50h#XSSBxyvlpouL}p?E}M8W>9Apxy;o$`>U#@CjoxQ{bUX>vw;rOH~|Yz}M&V*IruFRFvZ(XuxM?{?%ZSn_e9 zheRNaXVb+KdlyHSA{^W7{1cnOgA|bwB4h_vTyyL#?1{=rU8}eT4a8GdD}O>f+!ta# z#(Dz|R?jjoVrhOsge(S|3|6|AMWD_E)G-Nx4mc1~m&&#I$p;5)&0XvtWv;2o_~ z8%FRWf!%F9hj@mxBhU?DQHp37eN(n%PxfV3o=3DT_}vcVR-v~RQFI|?1P@a=a@xiL z^)jfJaWSI$jCot&8{$e~X$u~hHH_dvq=nvGPWLU>%yx>^ac-SE%v!}+Hb7*0kX~`` zWST{d(;nn@T#ORX5+y`Id79ypTu;BQgRIkAmB|>IenS-PR!@%t#CU(X&x}mKPfO#7g>sBC%-u2&w}oq8UdRVzWL@gGUm%~KkM&?OKVg07R`WbozkNtI`dOIpQBnJ|wXp&} zwLaDgteclNi@e8MNxno`&~C<#beH&B&dek0Wvq!)*+e`q9YR?bDdNp_pa0kjwasIK z2%Naw^&Q6Vg8ycqZ2^d9v{^;$n&kJ9{TK1CT`=@=HvaeI<-IV@8ewjUe)iN!6jjw% zg!MPz7=klF#U%0-cizq{&Ut1T(u=8Q^{2m$b6+K!z!!V){SeL$!VaCUwXoCijL{0K z1qfy*`iA&xzlu8%kq}3WGI(NIw+!V|QuZX-0qq5D_PT+qAMJ{Irt^g1j6%D8O=>)KZhNpkn$w?dApxoK{;*~U`DTh>g=?N8{J2jmBbxbzIP1t2fZ|AG>hCPofW%$TR zMdvO(O*j`VI{f8*B<|>)^}SA{&-XButsTf{Yi_FcfAMlWm1`$7vDfxJKK&Y#%2*t>j$W1Y=EqSITDk4PULGH({w8m1~vIEG2% zHDr4UjHhuMe!M@NhvKOzmh0mX%9&+FOwHNZXVL2N1(9eHKtWcyZJH Date: Tue, 26 Nov 2024 22:02:37 +0100 Subject: [PATCH 02/11] add Indian tax regime --- data/regimes/in.json | 216 ++++++++++++++++++ data/schemas/bill/invoice.json | 4 + data/schemas/regimes/mx/food-vouchers.json | 44 ++-- .../regimes/mx/fuel-account-balance.json | 68 ++---- regimes/in/README.md | 69 ++++++ regimes/in/extensions.go | 30 +++ regimes/in/identities.go | 48 ++++ regimes/in/identities_test.go | 78 +++++++ regimes/in/in.go | 73 ++++++ regimes/in/invoices.go | 14 ++ regimes/in/invoices_test.go | 77 +++++++ regimes/in/item.go | 56 +++++ regimes/in/item_test.go | 74 ++++++ regimes/in/scenarios.go | 77 +++++++ regimes/in/tax_categories.go | 89 ++++++++ regimes/in/tax_identity.go | 95 ++++++++ regimes/in/tax_identity_test.go | 84 +++++++ regimes/regimes.go | 1 + 18 files changed, 1120 insertions(+), 77 deletions(-) create mode 100644 data/regimes/in.json create mode 100644 regimes/in/README.md create mode 100644 regimes/in/extensions.go create mode 100644 regimes/in/identities.go create mode 100644 regimes/in/identities_test.go create mode 100644 regimes/in/in.go create mode 100644 regimes/in/invoices.go create mode 100644 regimes/in/invoices_test.go create mode 100644 regimes/in/item.go create mode 100644 regimes/in/item_test.go create mode 100644 regimes/in/scenarios.go create mode 100644 regimes/in/tax_categories.go create mode 100644 regimes/in/tax_identity.go create mode 100644 regimes/in/tax_identity_test.go diff --git a/data/regimes/in.json b/data/regimes/in.json new file mode 100644 index 00000000..b98b870c --- /dev/null +++ b/data/regimes/in.json @@ -0,0 +1,216 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime-def", + "name": { + "en": "India" + }, + "time_zone": "Asia/Kolkata", + "country": "IN", + "currency": "INR", + "tags": [ + { + "schema": "bill/invoice", + "list": [ + { + "key": "simplified", + "name": { + "de": "Vereinfachte Rechnung", + "en": "Simplified Invoice", + "es": "Factura Simplificada", + "it": "Fattura Semplificata" + }, + "desc": { + "de": "Wird für B2C-Transaktionen verwendet, wenn die Kundendaten nicht verfügbar sind. Bitte wenden Sie sich an die örtlichen Behörden, um die Grenzwerte zu ermitteln.", + "en": "Used for B2C transactions when the client details are not available, check with local authorities for limits.", + "es": "Usado para transacciones B2C cuando los detalles del cliente no están disponibles, consulte con las autoridades locales para los límites.", + "it": "Utilizzato per le transazioni B2C quando i dettagli del cliente non sono disponibili, controllare con le autorità locali per i limiti." + } + }, + { + "key": "reverse-charge", + "name": { + "de": "Umkehr der Steuerschuld", + "en": "Reverse Charge", + "es": "Inversión del Sujeto Pasivo", + "it": "Inversione del soggetto passivo" + } + }, + { + "key": "self-billed", + "name": { + "de": "Rechnung durch den Leistungsempfänger", + "en": "Self-billed", + "es": "Facturación por el destinatario", + "it": "Autofattura" + } + }, + { + "key": "customer-rates", + "name": { + "de": "Kundensätze", + "en": "Customer rates", + "es": "Tarifas aplicables al destinatario", + "it": "Aliquote applicabili al destinatario" + } + }, + { + "key": "partial", + "name": { + "de": "Teilweise", + "en": "Partial", + "es": "Parcial", + "it": "Parziale" + } + }, + { + "key": "bill-of-supply", + "name": { + "en": "Bill of Supply", + "hi": "आपूर्ति का बिल" + } + }, + { + "key": "invoice-cum-bill-of-supply", + "name": { + "en": "Invoice-cum-bill of supply", + "hi": "चालान-सह-आपूर्ति का बिल" + } + } + ] + } + ], + "extensions": [ + { + "key": "in-supply-place", + "name": { + "en": "Place of Supply", + "hi": "आपूर्ति का स्थान" + }, + "desc": { + "en": "The location to which the goods or services are supplied. In GST, this is referred to as the 'Place of Supply'.", + "hi": "वह स्थान जहां वस्तुएं या सेवाएं प्रदान की जाती हैं। GST में इसे 'आपूर्ति का स्थान' कहा जाता है।" + } + } + ], + "identity_keys": [ + { + "key": "in-pan", + "name": { + "en": "Permanent Account Number", + "hi": "स्थायी खाता संख्या" + } + } + ], + "scenarios": [ + { + "schema": "bill/invoice", + "list": [ + { + "tags": [ + "reverse-charge" + ], + "note": { + "key": "legal", + "src": "reverse-charge", + "text": "Reverse Charge" + } + }, + { + "tags": [ + "simplified" + ], + "note": { + "key": "legal", + "src": "simplified", + "text": "Simplified Tax Invoice" + } + }, + { + "tags": [ + "bill-of-supply" + ], + "note": { + "key": "legal", + "src": "bill-of-supply", + "text": "Bill Of Supply" + } + }, + { + "tags": [ + "invoice-cum-bill-of-supply" + ], + "note": { + "key": "legal", + "src": "invoice-cum-bill-of-supply", + "text": "Invoice-cum-bill Of Supply" + } + } + ] + } + ], + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note", + "debit-note" + ] + } + ], + "categories": [ + { + "code": "CGST", + "name": { + "en": "CGST", + "hi": "सीजीएसटी" + }, + "title": { + "en": "Central Goods and Services Tax", + "hi": "केंद्रीय माल और सेवा कर" + } + }, + { + "code": "SGST", + "name": { + "en": "SGST", + "hi": "एसजीएसटी" + }, + "title": { + "en": "State Goods and Services Tax", + "hi": "राज्य माल और सेवा कर" + } + }, + { + "code": "IGST", + "name": { + "en": "IGST", + "hi": "आईजीएसटी" + }, + "title": { + "en": "Integrated Goods and Services Tax", + "hi": "एकीकृत माल और सेवा कर" + } + }, + { + "code": "UTGST", + "name": { + "en": "UTGST", + "hi": "यूटीजीएसटी" + }, + "title": { + "en": "Union Territory Goods and Services Tax", + "hi": "केंद्र शासित प्रदेश माल और सेवा कर" + } + }, + { + "code": "CESS", + "name": { + "en": "Cess", + "hi": "उपकर" + }, + "title": { + "en": "Cess on Luxury or Specific Goods", + "hi": "विलासिता या विशेष वस्तुओं पर उपकर" + } + } + ] +} \ No newline at end of file diff --git a/data/schemas/bill/invoice.json b/data/schemas/bill/invoice.json index 236fb15f..87508bb6 100644 --- a/data/schemas/bill/invoice.json +++ b/data/schemas/bill/invoice.json @@ -338,6 +338,10 @@ "const": "GB", "title": "United Kingdom" }, + { + "const": "IN", + "title": "India" + }, { "const": "IT", "title": "Italy" diff --git a/data/schemas/regimes/mx/food-vouchers.json b/data/schemas/regimes/mx/food-vouchers.json index 9cedd146..07de9d70 100644 --- a/data/schemas/regimes/mx/food-vouchers.json +++ b/data/schemas/regimes/mx/food-vouchers.json @@ -7,18 +7,15 @@ "properties": { "employer_registration": { "type": "string", - "title": "Employer Registration", - "description": "Customer's employer registration number (maps to `registroPatronal`)." + "title": "Employer Registration" }, "account_number": { "type": "string", - "title": "Account Number", - "description": "Customer's account number (maps to `numeroDeCuenta`)." + "title": "Account Number" }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", - "description": "Sum of all line amounts (calculated, maps to `total`).", "calculated": true }, "lines": { @@ -26,8 +23,7 @@ "$ref": "#/$defs/FoodVouchersLine" }, "type": "array", - "title": "Lines", - "description": "List of food vouchers issued to the customer's employees (maps to `Conceptos`)." + "title": "Lines" } }, "type": "object", @@ -35,30 +31,25 @@ "account_number", "total", "lines" - ], - "description": "FoodVouchers 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." + ] }, "FoodVouchersEmployee": { "properties": { "tax_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's Tax Identity Code", - "description": "Employee's tax identity code (maps to `rfc`)." + "title": "Employee's Tax Identity Code" }, "curp": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's CURP", - "description": "Employee's CURP (\"Clave Única de Registro de Población\", maps to `curp`)." + "title": "Employee's CURP" }, "name": { "type": "string", - "title": "Employee's Name", - "description": "Employee's name (maps to `nombre`)." + "title": "Employee's Name" }, "social_security": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's Social Security Number", - "description": "Employee's Social Security Number (maps to `numSeguridadSocial`)." + "title": "Employee's Social Security Number" } }, "type": "object", @@ -66,36 +57,30 @@ "tax_code", "curp", "name" - ], - "description": "FoodVouchersEmployee represents an employee that received a food voucher." + ] }, "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", - "description": "Identifier of the e-wallet that received the food voucher (maps to `Identificador`)." + "title": "E-wallet Identifier" }, "issue_date_time": { "$ref": "https://gobl.org/draft-0/cal/date-time", - "title": "Issue Date and Time", - "description": "Date and time of the food voucher's issue (maps to `Fecha`)." + "title": "Issue Date and Time" }, "employee": { "$ref": "#/$defs/FoodVouchersEmployee", - "title": "Employee", - "description": "Employee that received the food voucher." + "title": "Employee" }, "amount": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Amount", - "description": "Amount of the food voucher (maps to `importe`)." + "title": "Amount" } }, "type": "object", @@ -104,8 +89,7 @@ "e_wallet_id", "issue_date_time", "amount" - ], - "description": "FoodVouchersLine represents a single food voucher issued to the e-wallet of one of the customer's employees." + ] } } } \ No newline at end of file diff --git a/data/schemas/regimes/mx/fuel-account-balance.json b/data/schemas/regimes/mx/fuel-account-balance.json index e8c05531..7fd39dd6 100644 --- a/data/schemas/regimes/mx/fuel-account-balance.json +++ b/data/schemas/regimes/mx/fuel-account-balance.json @@ -7,19 +7,16 @@ "properties": { "account_number": { "type": "string", - "title": "Account Number", - "description": "Customer's account number (maps to `NumeroDeCuenta`)." + "title": "Account Number" }, "subtotal": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Subtotal", - "description": "Sum of all line totals (i.e. taxes not included) (calculated, maps to `SubTotal`).", "calculated": true }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", - "description": "Grand total after taxes have been applied (calculated, maps to `Total`).", "calculated": true }, "lines": { @@ -27,8 +24,7 @@ "$ref": "#/$defs/FuelAccountLine" }, "type": "array", - "title": "Lines", - "description": "List of fuel purchases made with the customer's e-wallets (maps to `Conceptos`)." + "title": "Lines" } }, "type": "object", @@ -37,30 +33,25 @@ "subtotal", "total", "lines" - ], - "description": "FuelAccountBalance carries the data to produce a CFDI's \"Complemento de Estado de Cuenta de Combustibles para Monederos Electrónicos\" (version 1.2 revision B) providing detailed information about fuel purchases made with electronic wallets." + ] }, "FuelAccountItem": { "properties": { "type": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Type", - "description": "Type of fuel (one of `c_ClaveTipoCombustible` codes, maps to `TipoCombustible`)." + "title": "Type" }, "unit": { "$ref": "https://gobl.org/draft-0/org/unit", - "title": "Unit", - "description": "Reference unit of measure used in the price and the quantity (maps to `Unidad`)." + "title": "Unit" }, "name": { "type": "string", - "title": "Name", - "description": "Name of the fuel (maps to `NombreCombustible`)." + "title": "Name" }, "price": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Price", - "description": "Base price of a single unit of the fuel without taxes (maps to `ValorUnitario`)." + "title": "Price" } }, "type": "object", @@ -68,56 +59,46 @@ "type", "name", "price" - ], - "description": "FuelAccountItem provides the details of a fuel purchase." + ] }, "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", - "description": "Identifier of the e-wallet used to make the purchase (maps to `Identificador`)." + "title": "E-wallet Identifier" }, "purchase_date_time": { "$ref": "https://gobl.org/draft-0/cal/date-time", - "title": "Purchase Date and Time", - "description": "Date and time of the purchase (maps to `Fecha`)." + "title": "Purchase Date and Time" }, "vendor_tax_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Vendor's Tax Identity Code", - "description": "Tax Identity Code of the fuel's vendor (maps to `Rfc`)" + "title": "Vendor's Tax Identity Code" }, "service_station_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Service Station Code", - "description": "Code of the service station where the purchase was made (maps to `ClaveEstacion`)." + "title": "Service Station Code" }, "quantity": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Quantity", - "description": "Amount of fuel units purchased (maps to `Cantidad`)" + "title": "Quantity" }, "item": { "$ref": "#/$defs/FuelAccountItem", - "title": "Item", - "description": "Details of the fuel purchased." + "title": "Item" }, "purchase_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Purchase Code", - "description": "Identifier of the purchase (maps to `FolioOperacion`)." + "title": "Purchase Code" }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", - "description": "Result of quantity multiplied by the unit price (maps to `Importe`).", "calculated": true }, "taxes": { @@ -125,8 +106,7 @@ "$ref": "#/$defs/FuelAccountTax" }, "type": "array", - "title": "Taxes", - "description": "Map of taxes applied to the purchase (maps to `Traslados`)." + "title": "Taxes" } }, "type": "object", @@ -141,30 +121,25 @@ "purchase_code", "total", "taxes" - ], - "description": "FuelAccountLine represents a single fuel purchase made with an e-wallet issued by the invoice's supplier." + ] }, "FuelAccountTax": { "properties": { "cat": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Category", - "description": "Category that identifies the tax (\"VAT\" or \"IEPS\", maps to `Impuesto`)" + "title": "Category" }, "percent": { "$ref": "https://gobl.org/draft-0/num/percentage", - "title": "Percent", - "description": "Percent applicable to the line total (tasa) to use instead of Rate (maps to `TasaoCuota`)" + "title": "Percent" }, "rate": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Rate", - "description": "Rate is a fixed fee to apply to the line quantity (cuota) (maps to `TasaOCuota`)" + "title": "Rate" }, "amount": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Amount", - "description": "Total amount of the tax once the percent or rate has been applied (maps to `Importe`).", "calculated": true } }, @@ -172,8 +147,7 @@ "required": [ "cat", "amount" - ], - "description": "FuelAccountTax represents a single tax applied to a fuel purchase." + ] } } } \ No newline at end of file diff --git a/regimes/in/README.md b/regimes/in/README.md new file mode 100644 index 00000000..921a91b0 --- /dev/null +++ b/regimes/in/README.md @@ -0,0 +1,69 @@ +# India (IN) Tax Regime + +This document provides an overview of the tax regime in India. + +--- + +## Overview of GST + +India follows a **dual GST model**, where both the Central and State Governments levy taxes on a shared tax base: + +- **Central GST (CGST)**: Levied by the Central Government. +- **State GST (SGST) / Union Territory GST (UTGST)**: Levied by State or Union Territory Governments. +- **Integrated GST (IGST)**: Levied by the Central Government on **interstate supplies** and imports. + +### Application of GST + +- **Intrastate Supplies**: Subject to CGST and SGST/UTGST in equal proportions. +- **Interstate Supplies and Imports**: Subject to IGST, equivalent to CGST + SGST. +- **Compensation Cess**: Additional tax on luxury and sin goods, such as tobacco and motor vehicles. + +--- + +## Rates and Categories + +### Taxable Rates + +1. **0.25%–3%**: Precious metals like gold and diamonds. +2. **5%**: Basic goods and services (e.g., economy air travel, basic restaurants). +3. **12%–18%**: Standard services and goods (e.g., hotels, banking, construction). +4. **28%**: Luxury items (e.g., air conditioners, motor vehicles). + +### Zero-Rated Supplies + +- Exports. +- Supplies to Special Economic Zones (SEZs). + +### Exempt Supplies + +- Fresh fruits and vegetables. +- Educational services. +- Public road tolls. + +### Note on GOBL Tax Categories + +Due to the **dual GST model**, which divides taxes between the Central and State Governments, GOBL does not include predefined rate values for tax categories (e.g., CGST, SGST/UTGST, IGST). This choice prioritizes simplicity, avoiding the added complexity of managing split tax rate allocations. + +--- + +### GSTIN (Goods and Services Tax Identification Number) + +The GSTIN is a unique 15-digit identifier assigned to every registered taxpayer under the GST system. + +#### Validation + +GOBL includes built-in validation for the GSTIN field to ensure compliance with the GST system. This validation verifies the format and checksum of the GSTIN, ensuring that only correctly structured identifiers are accepted. + +--- + +### HSN (Harmonized System of Nomenclature) Code + +The HSN code is an internationally recognized system for classifying goods, adopted in India under the GST regime. It is used to identify and categorize items systematically, ensuring the correct application of tax rates. + +Under the Indian GST system, the HSN code is mandatory for each item on a tax invoice, helping maintain uniformity and compliance across goods and services transactions. + +--- + +Find example IN GOBL files in the [`examples`](../../examples/in) (uncalculated documents) and [`examples/out`](../../examples/in/out) (calculated envelopes) subdirectories. + +For additional details, visit the official [GST Portal](https://www.gst.gov.in/). diff --git a/regimes/in/extensions.go b/regimes/in/extensions.go new file mode 100644 index 00000000..914ef0ec --- /dev/null +++ b/regimes/in/extensions.go @@ -0,0 +1,30 @@ +package in + +import ( + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/pkg/here" +) + +// Extension keys used in India. +const ( + ExtKeySupplyPlace cbc.Key = "in-supply-place" +) + +var extensions = []*cbc.KeyDefinition{ + { + Key: ExtKeySupplyPlace, + Name: i18n.String{ + i18n.EN: "Place of Supply", + i18n.HI: "आपूर्ति का स्थान", + }, + Desc: i18n.String{ + i18n.EN: here.Doc(` + The location to which the goods or services are supplied. In GST, this is referred to as the 'Place of Supply'. + `), + i18n.HI: here.Doc(` + वह स्थान जहां वस्तुएं या सेवाएं प्रदान की जाती हैं। GST में इसे 'आपूर्ति का स्थान' कहा जाता है। + `), + }, + }, +} diff --git a/regimes/in/identities.go b/regimes/in/identities.go new file mode 100644 index 00000000..971cc393 --- /dev/null +++ b/regimes/in/identities.go @@ -0,0 +1,48 @@ +package in + +import ( + "regexp" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +const ( + // IdentityKeyPAN represents the Indian Permanent Account Number (PAN). It is a unique identifier assigned + // to individuals, companies, and other entities. + IdentityKeyPAN cbc.Key = "in-pan" +) + +var panRegexPattern = regexp.MustCompile(`^[A-Z]{5}[0-9]{4}[A-Z]$`) + +var identityKeyDefinitions = []*cbc.KeyDefinition{ + { + Key: IdentityKeyPAN, + Name: i18n.String{ + i18n.EN: "Permanent Account Number", + i18n.HI: "स्थायी खाता संख्या", + }, + }, +} + +func normalizePAN(id *org.Identity) { + if id == nil || id.Key != IdentityKeyPAN { + return + } + code := cbc.NormalizeAlphanumericalCode(id.Code).String() + id.Code = cbc.Code(code) +} + +func validatePAN(id *org.Identity) error { + if id == nil || id.Key != IdentityKeyPAN { + return nil + } + + return validation.ValidateStruct(id, + validation.Field(&id.Code, + validation.Match(panRegexPattern), + ), + ) +} diff --git a/regimes/in/identities_test.go b/regimes/in/identities_test.go new file mode 100644 index 00000000..2b5a94b7 --- /dev/null +++ b/regimes/in/identities_test.go @@ -0,0 +1,78 @@ +package in_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/in" + "github.com/stretchr/testify/assert" +) + +func TestNormalizePAN(t *testing.T) { + tests := []struct { + name string + input cbc.Code + expected cbc.Code + }{ + {name: "already normalized", input: "ABCDE1234F", expected: "ABCDE1234F"}, + {name: "lowercase input", input: "abcde1234f", expected: "ABCDE1234F"}, + {name: "mixed case input", input: "AbCdE1234f", expected: "ABCDE1234F"}, + {name: "extra spaces", input: " ABCDE1234F ", expected: "ABCDE1234F"}, + {name: "special characters", input: "AB-CDE1234F", expected: "ABCDE1234F"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := &org.Identity{Key: in.IdentityKeyPAN, Code: tt.input} + in.Normalize(id) + assert.Equal(t, tt.expected, id.Code) + }) + } +} + +func TestValidatePAN(t *testing.T) { + tests := []struct { + name string + code cbc.Code + err string + }{ + {name: "valid PAN 1", code: "BAJPC4350M"}, + {name: "valid PAN 2", code: "DAJPC4150P"}, + {name: "valid PAN 3", code: "XGZFE7225A"}, + {name: "valid PAN 4", code: "CTUGE1616Y"}, + + { + name: "too short", + code: "ABC1234F", + err: "code: must be in a valid format.", + }, + { + name: "contains spaces", + code: "ABCDE 1234F", + err: "code: must be in a valid format.", + }, + { + name: "extra characters", + code: "ABCDE1234F12", + err: "code: must be in a valid format.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := &org.Identity{Key: in.IdentityKeyPAN, Code: tt.code} + + // Validar el PAN + err := in.Validate(id) + + if tt.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tt.err) + } + } + }) + } +} diff --git a/regimes/in/in.go b/regimes/in/in.go new file mode 100644 index 00000000..05e610e6 --- /dev/null +++ b/regimes/in/in.go @@ -0,0 +1,73 @@ +// Package in provides models for dealing with India. +package in + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/currency" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/common" + "github.com/invopop/gobl/tax" +) + +func init() { + tax.RegisterRegimeDef(New()) +} + +// New provides the tax region definition for IN. +func New() *tax.RegimeDef { + return &tax.RegimeDef{ + Country: "IN", + Currency: currency.INR, + Name: i18n.String{ + i18n.EN: "India", + }, + TimeZone: "Asia/Kolkata", + Tags: []*tax.TagSet{ + common.InvoiceTags().Merge(invoiceTags), + }, + Scenarios: []*tax.ScenarioSet{ + invoiceScenarios, + }, + IdentityKeys: identityKeyDefinitions, + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + bill.InvoiceTypeDebitNote, + }, + }, + }, + Validator: Validate, + Normalizer: Normalize, + Categories: taxCategories, + Extensions: extensions, + } +} + +// Validate function assesses the document type to determine if validation is required. +func Validate(doc interface{}) error { + switch obj := doc.(type) { + case *bill.Invoice: + return validateInvoice(obj) + case *tax.Identity: + return validateTaxIdentity(obj) + case *org.Identity: + return validatePAN(obj) + case *org.Item: + return validateItem(obj) + } + return nil +} + +// Normalize attempts to clean up the object passed to it. +func Normalize(doc interface{}) { + switch obj := doc.(type) { + case *tax.Identity: + normalizeTaxIdentity(obj) + case *org.Identity: + normalizePAN(obj) + } +} diff --git a/regimes/in/invoices.go b/regimes/in/invoices.go new file mode 100644 index 00000000..e71a06c0 --- /dev/null +++ b/regimes/in/invoices.go @@ -0,0 +1,14 @@ +package in + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/validation" +) + +func validateInvoice(inv *bill.Invoice) error { + return validation.ValidateStruct(inv, + validation.Field(&inv.Supplier, + validation.Required, + ), + ) +} diff --git a/regimes/in/invoices_test.go b/regimes/in/invoices_test.go new file mode 100644 index 00000000..95307227 --- /dev/null +++ b/regimes/in/invoices_test.go @@ -0,0 +1,77 @@ +package in_test + +import ( + "testing" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func validInvoice() *bill.Invoice { + return &bill.Invoice{ + Series: "TEST", + Code: "0002", + Currency: "INR", + Supplier: &org.Party{ + Name: "Test Supplier", + TaxID: &tax.Identity{ + Country: "IN", + Code: "27AAPFU0939F1ZV", + }, + }, + Tax: &bill.Tax{ + Ext: tax.Extensions{ + "in-supply-place": "Ciudad prueba", + }, + }, + Customer: &org.Party{ + Name: "Test Customer", + TaxID: &tax.Identity{ + Country: "IN", + Code: "27AAPFU0939F1ZV", + }, + }, + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(1, 0), + Item: &org.Item{ + Name: "Development services", + Price: num.MakeAmount(10000, 2), + Unit: org.UnitPackage, + Identities: []*org.Identity{ + { + Type: "HSN", + Code: "12345678", + }, + }, + }, + Taxes: tax.Set{ + { + Category: "CGST", + Percent: num.NewPercentage(9, 0), + }, + { + Category: "SGST", + Percent: num.NewPercentage(9, 0), + }, + }, + }, + }, + } +} + +func TestInvoiceValidation(t *testing.T) { + inv := validInvoice() + require.NoError(t, inv.Calculate()) + assert.NoError(t, inv.Validate()) + + inv = validInvoice() + inv.Supplier = nil + require.NoError(t, inv.Calculate()) + assert.ErrorContains(t, inv.Validate(), "supplier: cannot be blank.") + +} diff --git a/regimes/in/item.go b/regimes/in/item.go new file mode 100644 index 00000000..68b0a70c --- /dev/null +++ b/regimes/in/item.go @@ -0,0 +1,56 @@ +package in + +import ( + "errors" + "regexp" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +// Identity type used in India to classify products and services. +const ( + IdentityTypeHSN = cbc.Code("HSN") +) + +// HSNCodeRegexp defines the regular expression to validate HSN codes. +var HSNCodeRegexp = regexp.MustCompile(`^(?:\d{4}|\d{6}|\d{8})$`) + +func validateItem(value any) error { + item, ok := value.(*org.Item) + if !ok || item == nil { + return nil + } + + return validation.ValidateStruct(item, + validation.Field(&item.Identities, + validation.By(validItemIdentities), + validation.Skip, + ), + ) +} + +func validItemIdentities(value interface{}) error { + identities, ok := value.([]*org.Identity) + if !ok { + return nil + } + + for _, identity := range identities { + if identity == nil { + continue + } + + if identity.Type == IdentityTypeHSN { + val := string(identity.Code) + + if !HSNCodeRegexp.MatchString(val) { + return errors.New("must be a 4, 6, or 8-digit number") + } + break + } + } + + return nil +} diff --git a/regimes/in/item_test.go b/regimes/in/item_test.go new file mode 100644 index 00000000..b98c45d9 --- /dev/null +++ b/regimes/in/item_test.go @@ -0,0 +1,74 @@ +package in_test + +import ( + "testing" + + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/in" + "github.com/stretchr/testify/assert" +) + +func TestItemValidation(t *testing.T) { + tests := []struct { + name string + item *org.Item + err string + }{ + { + name: "valid HSN code", + item: &org.Item{ + Identities: []*org.Identity{ + { + Type: "HSN", + Code: "12345678", + }, + }, + }, + err: "", + }, + { + name: "valid HSN code with 4 digits", + item: &org.Item{ + Identities: []*org.Identity{ + { + Type: "HSN", + Code: "1234", + }, + }, + }, + err: "", + }, + { + name: "invalid HSN code format", + item: &org.Item{ + Identities: []*org.Identity{ + { + Type: "HSN", + Code: "12A456", + }, + }, + }, + err: "must be a 4, 6, or 8-digit number", + }, + { + name: "missing HSN identity", + item: &org.Item{ + Identities: []*org.Identity{}, + }, + err: "", // No error expected since it's not mandatory + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := in.Validate(tc.item) + if tc.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tc.err) + } + } + }) + } +} diff --git a/regimes/in/scenarios.go b/regimes/in/scenarios.go new file mode 100644 index 00000000..19d38228 --- /dev/null +++ b/regimes/in/scenarios.go @@ -0,0 +1,77 @@ +// Package in provides tax scenarios specific to India GST regulations. +package in + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/tax" +) + +// Tax tags that can be applied in India. +const ( + TagBillOfSupply cbc.Key = "bill-of-supply" + TagInvoiceCumBillOfSupply cbc.Key = "invoice-cum-bill-of-supply" +) + +var invoiceTags = &tax.TagSet{ + Schema: bill.ShortSchemaInvoice, + List: []*cbc.KeyDefinition{ + { + Key: TagBillOfSupply, + Name: i18n.String{ + i18n.EN: "Bill of Supply", + i18n.HI: "आपूर्ति का बिल", + }, + }, + { + Key: TagInvoiceCumBillOfSupply, + Name: i18n.String{ + i18n.EN: "Invoice-cum-bill of supply", + i18n.HI: "चालान-सह-आपूर्ति का बिल", + }, + }, + }, +} + +var invoiceScenarios = &tax.ScenarioSet{ + Schema: bill.ShortSchemaInvoice, + List: []*tax.Scenario{ + // Reverse Charges + { + Tags: []cbc.Key{tax.TagReverseCharge}, + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: tax.TagReverseCharge, + Text: "Reverse Charge", + }, + }, + // Simplified Tax Invoice + { + Tags: []cbc.Key{tax.TagSimplified}, + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: tax.TagSimplified, + Text: "Simplified Tax Invoice", + }, + }, + // Bill of Supply + { + Tags: []cbc.Key{TagBillOfSupply}, + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: TagBillOfSupply, + Text: "Bill Of Supply", + }, + }, + // Invoice-cum-bill of Supply + { + Tags: []cbc.Key{TagInvoiceCumBillOfSupply}, + Note: &cbc.Note{ + Key: cbc.NoteKeyLegal, + Src: TagInvoiceCumBillOfSupply, + Text: "Invoice-cum-bill Of Supply", + }, + }, + }, +} diff --git a/regimes/in/tax_categories.go b/regimes/in/tax_categories.go new file mode 100644 index 00000000..9c516511 --- /dev/null +++ b/regimes/in/tax_categories.go @@ -0,0 +1,89 @@ +// Package in defines GST tax categories specific to India. +package in + +import ( + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/tax" +) + +// Tax categories specific for India. +const ( + TaxCategoryCGST cbc.Code = "CGST" + TaxCategorySGST cbc.Code = "SGST" + TaxCategoryIGST cbc.Code = "IGST" + TaxCategoryUTGST cbc.Code = "UTGST" + TaxCategoryCess cbc.Code = "CESS" +) + +var taxCategories = []*tax.CategoryDef{ + // Central Goods and Services Tax (CGST) + { + Code: TaxCategoryCGST, + Name: i18n.String{ + i18n.EN: "CGST", + i18n.HI: "सीजीएसटी", + }, + Title: i18n.String{ + i18n.EN: "Central Goods and Services Tax", + i18n.HI: "केंद्रीय माल और सेवा कर", + }, + Rates: []*tax.RateDef{}, + }, + + // State Goods and Services Tax (SGST) + { + Code: TaxCategorySGST, + Name: i18n.String{ + i18n.EN: "SGST", + i18n.HI: "एसजीएसटी", + }, + Title: i18n.String{ + i18n.EN: "State Goods and Services Tax", + i18n.HI: "राज्य माल और सेवा कर", + }, + Rates: []*tax.RateDef{}, + }, + + // Integrated Goods and Services Tax (IGST) + { + Code: TaxCategoryIGST, + Name: i18n.String{ + i18n.EN: "IGST", + i18n.HI: "आईजीएसटी", + }, + Title: i18n.String{ + i18n.EN: "Integrated Goods and Services Tax", + i18n.HI: "एकीकृत माल और सेवा कर", + }, + Rates: []*tax.RateDef{}, + }, + + // Union Territory Goods and Services Tax (UTGST) + { + Code: TaxCategoryUTGST, + Name: i18n.String{ + i18n.EN: "UTGST", + i18n.HI: "यूटीजीएसटी", + }, + Title: i18n.String{ + i18n.EN: "Union Territory Goods and Services Tax", + i18n.HI: "केंद्र शासित प्रदेश माल और सेवा कर", + }, + Rates: []*tax.RateDef{}, + }, + + // Cess (Additional Tax for Luxury or Specific Goods) + { + Code: TaxCategoryCess, + Name: i18n.String{ + i18n.EN: "Cess", + i18n.HI: "उपकर", + }, + Title: i18n.String{ + i18n.EN: "Cess on Luxury or Specific Goods", + i18n.HI: "विलासिता या विशेष वस्तुओं पर उपकर", + }, + Rates: []*tax.RateDef{}, + }, +} diff --git a/regimes/in/tax_identity.go b/regimes/in/tax_identity.go new file mode 100644 index 00000000..06e24f7d --- /dev/null +++ b/regimes/in/tax_identity.go @@ -0,0 +1,95 @@ +// Package in provides the tax identity validation specific to India. +package in + +import ( + "errors" + "regexp" + "strings" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +var ( + taxCodeRegexp = regexp.MustCompile(`^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$`) + + conversionTable = map[rune]int{ + '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, + 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'I': 18, + 'J': 19, 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, 'P': 25, 'Q': 26, 'R': 27, + 'S': 28, 'T': 29, 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 'Z': 35, + } +) + +func normalizeTaxIdentity(tID *tax.Identity) { + if tID == nil { + return + } + tax.NormalizeIdentity(tID, l10n.IN) + tID.Code = cbc.Code(strings.ToUpper(tID.Code.String())) + tID.Country = "IN" +} + +func validateTaxIdentity(tID *tax.Identity) error { + return validation.ValidateStruct(tID, + validation.Field(&tID.Code, validation.By(validateTaxCode)), + ) +} + +func validateTaxCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + if !taxCodeRegexp.MatchString(val) { + return errors.New("invalid GSTIN format") + } + + if !hasValidChecksum(val) { + return errors.New("checksum mismatch") + } + + return nil +} + +func hasValidChecksum(gstin string) bool { + if len(gstin) != 15 { + return false + } + + sum := 0 + for i, char := range gstin[:14] { + value, exists := conversionTable[char] + if !exists { + return false + } + + multiplier := 1 + if i%2 != 0 { + multiplier = 2 + } + + product := value * multiplier + sum += product/36 + product%36 + } + + remainder := sum % 36 + calculatedChecksum := (36 - remainder) % 36 + + checksumChar := findCharByValue(calculatedChecksum) + + return checksumChar == rune(gstin[14]) +} + +func findCharByValue(value int) rune { + for char, num := range conversionTable { + if num == value { + return char + } + } + return ' ' +} diff --git a/regimes/in/tax_identity_test.go b/regimes/in/tax_identity_test.go new file mode 100644 index 00000000..63576963 --- /dev/null +++ b/regimes/in/tax_identity_test.go @@ -0,0 +1,84 @@ +package in_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/regimes/in" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeTaxIdentity(t *testing.T) { + tests := []struct { + name string + input cbc.Code + expected cbc.Code + }{ + {name: "already normalized", input: "27AAPFU0939F1ZV", expected: "27AAPFU0939F1ZV"}, + {name: "lowercase input", input: "27aapfu0939f1zv", expected: "27AAPFU0939F1ZV"}, + {name: "mixed case input", input: "27AaPfU0939F1zV", expected: "27AAPFU0939F1ZV"}, + {name: "extra spaces", input: " 27AAPFU0939F1ZV ", expected: "27AAPFU0939F1ZV"}, + {name: "special characters", input: "27-AAPFU0939F1-ZV", expected: "27AAPFU0939F1ZV"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "IN", Code: tt.input} + + in.Normalize(tID) + + assert.Equal(t, tt.expected, tID.Code) + }) + } +} + +func TestValidateTaxIdentity(t *testing.T) { + tests := []struct { + name string + code cbc.Code + err string + }{ + {name: "valid GSTIN 1", code: "27AAPFU0939F1ZV"}, + {name: "valid GSTIN 2", code: "29AAGCB7383J1Z4"}, + {name: "valid GSTIN 3", code: "10AABCU9355J1Z9"}, + {name: "valid GSTIN 4", code: "09AABCU9355J1ZS"}, + + { + name: "too short", + code: "27AAPFU0939F", + err: "invalid GSTIN format", + }, + { + name: "state code not numeric", + code: "AAAPFU0939F1ZV", + err: "invalid GSTIN format", + }, + { + name: "invalid checksum", + code: "27AAPFU0939F1Z0", + err: "checksum mismatch", + }, + { + name: "too long", + code: "27AAPFU0939F1ZV12", + err: "invalid GSTIN format", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "IN", Code: tt.code} + + err := in.Validate(tID) + + if tt.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tt.err) + } + } + }) + } +} diff --git a/regimes/regimes.go b/regimes/regimes.go index 9f6279ab..358f69eb 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -17,6 +17,7 @@ import ( _ "github.com/invopop/gobl/regimes/fr" _ "github.com/invopop/gobl/regimes/gb" _ "github.com/invopop/gobl/regimes/gr" + _ "github.com/invopop/gobl/regimes/in" _ "github.com/invopop/gobl/regimes/it" _ "github.com/invopop/gobl/regimes/mx" _ "github.com/invopop/gobl/regimes/nl" From 24e97ca14a7ac1995798e89867d2e1872155b86a Mon Sep 17 00:00:00 2001 From: richifernandez Date: Tue, 26 Nov 2024 22:11:46 +0100 Subject: [PATCH 03/11] add Indian tax regime --- data/schemas/regimes/mx/food-vouchers.json | 44 ++++++++---- .../regimes/mx/fuel-account-balance.json | 68 +++++++++++++------ 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/data/schemas/regimes/mx/food-vouchers.json b/data/schemas/regimes/mx/food-vouchers.json index 07de9d70..9cedd146 100644 --- a/data/schemas/regimes/mx/food-vouchers.json +++ b/data/schemas/regimes/mx/food-vouchers.json @@ -7,15 +7,18 @@ "properties": { "employer_registration": { "type": "string", - "title": "Employer Registration" + "title": "Employer Registration", + "description": "Customer's employer registration number (maps to `registroPatronal`)." }, "account_number": { "type": "string", - "title": "Account Number" + "title": "Account Number", + "description": "Customer's account number (maps to `numeroDeCuenta`)." }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", + "description": "Sum of all line amounts (calculated, maps to `total`).", "calculated": true }, "lines": { @@ -23,7 +26,8 @@ "$ref": "#/$defs/FoodVouchersLine" }, "type": "array", - "title": "Lines" + "title": "Lines", + "description": "List of food vouchers issued to the customer's employees (maps to `Conceptos`)." } }, "type": "object", @@ -31,25 +35,30 @@ "account_number", "total", "lines" - ] + ], + "description": "FoodVouchers 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." }, "FoodVouchersEmployee": { "properties": { "tax_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's Tax Identity Code" + "title": "Employee's Tax Identity Code", + "description": "Employee's tax identity code (maps to `rfc`)." }, "curp": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's CURP" + "title": "Employee's CURP", + "description": "Employee's CURP (\"Clave Única de Registro de Población\", maps to `curp`)." }, "name": { "type": "string", - "title": "Employee's Name" + "title": "Employee's Name", + "description": "Employee's name (maps to `nombre`)." }, "social_security": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's Social Security Number" + "title": "Employee's Social Security Number", + "description": "Employee's Social Security Number (maps to `numSeguridadSocial`)." } }, "type": "object", @@ -57,30 +66,36 @@ "tax_code", "curp", "name" - ] + ], + "description": "FoodVouchersEmployee represents an employee that received a food voucher." }, "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" + "title": "E-wallet Identifier", + "description": "Identifier of the e-wallet that received the food voucher (maps to `Identificador`)." }, "issue_date_time": { "$ref": "https://gobl.org/draft-0/cal/date-time", - "title": "Issue Date and Time" + "title": "Issue Date and Time", + "description": "Date and time of the food voucher's issue (maps to `Fecha`)." }, "employee": { "$ref": "#/$defs/FoodVouchersEmployee", - "title": "Employee" + "title": "Employee", + "description": "Employee that received the food voucher." }, "amount": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Amount" + "title": "Amount", + "description": "Amount of the food voucher (maps to `importe`)." } }, "type": "object", @@ -89,7 +104,8 @@ "e_wallet_id", "issue_date_time", "amount" - ] + ], + "description": "FoodVouchersLine represents a single food voucher issued to the e-wallet of one of the customer's employees." } } } \ No newline at end of file diff --git a/data/schemas/regimes/mx/fuel-account-balance.json b/data/schemas/regimes/mx/fuel-account-balance.json index 7fd39dd6..e8c05531 100644 --- a/data/schemas/regimes/mx/fuel-account-balance.json +++ b/data/schemas/regimes/mx/fuel-account-balance.json @@ -7,16 +7,19 @@ "properties": { "account_number": { "type": "string", - "title": "Account Number" + "title": "Account Number", + "description": "Customer's account number (maps to `NumeroDeCuenta`)." }, "subtotal": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Subtotal", + "description": "Sum of all line totals (i.e. taxes not included) (calculated, maps to `SubTotal`).", "calculated": true }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", + "description": "Grand total after taxes have been applied (calculated, maps to `Total`).", "calculated": true }, "lines": { @@ -24,7 +27,8 @@ "$ref": "#/$defs/FuelAccountLine" }, "type": "array", - "title": "Lines" + "title": "Lines", + "description": "List of fuel purchases made with the customer's e-wallets (maps to `Conceptos`)." } }, "type": "object", @@ -33,25 +37,30 @@ "subtotal", "total", "lines" - ] + ], + "description": "FuelAccountBalance carries the data to produce a CFDI's \"Complemento de Estado de Cuenta de Combustibles para Monederos Electrónicos\" (version 1.2 revision B) providing detailed information about fuel purchases made with electronic wallets." }, "FuelAccountItem": { "properties": { "type": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Type" + "title": "Type", + "description": "Type of fuel (one of `c_ClaveTipoCombustible` codes, maps to `TipoCombustible`)." }, "unit": { "$ref": "https://gobl.org/draft-0/org/unit", - "title": "Unit" + "title": "Unit", + "description": "Reference unit of measure used in the price and the quantity (maps to `Unidad`)." }, "name": { "type": "string", - "title": "Name" + "title": "Name", + "description": "Name of the fuel (maps to `NombreCombustible`)." }, "price": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Price" + "title": "Price", + "description": "Base price of a single unit of the fuel without taxes (maps to `ValorUnitario`)." } }, "type": "object", @@ -59,46 +68,56 @@ "type", "name", "price" - ] + ], + "description": "FuelAccountItem provides the details of a fuel purchase." }, "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" + "title": "E-wallet Identifier", + "description": "Identifier of the e-wallet used to make the purchase (maps to `Identificador`)." }, "purchase_date_time": { "$ref": "https://gobl.org/draft-0/cal/date-time", - "title": "Purchase Date and Time" + "title": "Purchase Date and Time", + "description": "Date and time of the purchase (maps to `Fecha`)." }, "vendor_tax_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Vendor's Tax Identity Code" + "title": "Vendor's Tax Identity Code", + "description": "Tax Identity Code of the fuel's vendor (maps to `Rfc`)" }, "service_station_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Service Station Code" + "title": "Service Station Code", + "description": "Code of the service station where the purchase was made (maps to `ClaveEstacion`)." }, "quantity": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Quantity" + "title": "Quantity", + "description": "Amount of fuel units purchased (maps to `Cantidad`)" }, "item": { "$ref": "#/$defs/FuelAccountItem", - "title": "Item" + "title": "Item", + "description": "Details of the fuel purchased." }, "purchase_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Purchase Code" + "title": "Purchase Code", + "description": "Identifier of the purchase (maps to `FolioOperacion`)." }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", + "description": "Result of quantity multiplied by the unit price (maps to `Importe`).", "calculated": true }, "taxes": { @@ -106,7 +125,8 @@ "$ref": "#/$defs/FuelAccountTax" }, "type": "array", - "title": "Taxes" + "title": "Taxes", + "description": "Map of taxes applied to the purchase (maps to `Traslados`)." } }, "type": "object", @@ -121,25 +141,30 @@ "purchase_code", "total", "taxes" - ] + ], + "description": "FuelAccountLine represents a single fuel purchase made with an e-wallet issued by the invoice's supplier." }, "FuelAccountTax": { "properties": { "cat": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Category" + "title": "Category", + "description": "Category that identifies the tax (\"VAT\" or \"IEPS\", maps to `Impuesto`)" }, "percent": { "$ref": "https://gobl.org/draft-0/num/percentage", - "title": "Percent" + "title": "Percent", + "description": "Percent applicable to the line total (tasa) to use instead of Rate (maps to `TasaoCuota`)" }, "rate": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Rate" + "title": "Rate", + "description": "Rate is a fixed fee to apply to the line quantity (cuota) (maps to `TasaOCuota`)" }, "amount": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Amount", + "description": "Total amount of the tax once the percent or rate has been applied (maps to `Importe`).", "calculated": true } }, @@ -147,7 +172,8 @@ "required": [ "cat", "amount" - ] + ], + "description": "FuelAccountTax represents a single tax applied to a fuel purchase." } } } \ No newline at end of file From 9500c8b7abbcd4e526381301850f28f2ab3e625a Mon Sep 17 00:00:00 2001 From: richifernandez Date: Tue, 26 Nov 2024 22:19:26 +0100 Subject: [PATCH 04/11] add Indian tax regime --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24065645..c56bbfc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `tax`: `ExtValue.In` for comparing extension values. - `bill`: `Tax.MergeExtensions` convenience method for adding extensions to tax objects and avoid nil panics. - `cbc`: `Key.Pop` method for splitting keys with sub-keys, e.g. `cbc.Key("a+b").Pop() == cbc.Key("a")`. +- `in`: added Indian regime ### Changed From 22e46be4a42a181ed9d0926aa6511c6bc4380b8c Mon Sep 17 00:00:00 2001 From: richifernandez Date: Tue, 26 Nov 2024 22:27:06 +0100 Subject: [PATCH 05/11] add Indian tax regime --- regimes/in/identities_test.go | 2 -- regimes/in/item_test.go | 2 +- regimes/in/tax_identity.go | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/regimes/in/identities_test.go b/regimes/in/identities_test.go index 2b5a94b7..094733b5 100644 --- a/regimes/in/identities_test.go +++ b/regimes/in/identities_test.go @@ -62,8 +62,6 @@ func TestValidatePAN(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { id := &org.Identity{Key: in.IdentityKeyPAN, Code: tt.code} - - // Validar el PAN err := in.Validate(id) if tt.err == "" { diff --git a/regimes/in/item_test.go b/regimes/in/item_test.go index b98c45d9..cf383bd4 100644 --- a/regimes/in/item_test.go +++ b/regimes/in/item_test.go @@ -55,7 +55,7 @@ func TestItemValidation(t *testing.T) { item: &org.Item{ Identities: []*org.Identity{}, }, - err: "", // No error expected since it's not mandatory + err: "", // No error expected since it's not mandatory in some specific cases }, } diff --git a/regimes/in/tax_identity.go b/regimes/in/tax_identity.go index 06e24f7d..be6337c6 100644 --- a/regimes/in/tax_identity.go +++ b/regimes/in/tax_identity.go @@ -79,7 +79,6 @@ func hasValidChecksum(gstin string) bool { remainder := sum % 36 calculatedChecksum := (36 - remainder) % 36 - checksumChar := findCharByValue(calculatedChecksum) return checksumChar == rune(gstin[14]) From dc89a7d2397045c0275ca12d62227356a32fbd48 Mon Sep 17 00:00:00 2001 From: richifernandez Date: Thu, 28 Nov 2024 18:30:45 +0100 Subject: [PATCH 06/11] add examples for Indian tax regime --- examples/in/out/invoice-in-in-simplified.json | Bin 3628 -> 1706 bytes examples/in/out/invoice-in-in-stdr.json | Bin 4390 -> 2065 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/examples/in/out/invoice-in-in-simplified.json b/examples/in/out/invoice-in-in-simplified.json index d307b621012ff7e95b95cd728ffb40789e00d524..700a43e214a3dfaa8d94d907b41337ee3171a1b1 100644 GIT binary patch literal 1706 zcmd5+&2Q5%6hEhbMP;!YX`QrP+npGL0n!H33LHSFdP%(2QtAZT=~_+u-|>5P(sn}P z%GAU1{62mk&wlDf?3KyaVgu}=&(_w~=3+QpRarTx^lDgWc(5@aX0j}YQf(`l3-%g| zv{=cFz@kr|Dbh5uz|Ro~m}Zmncrq_$@i-G%Jm*;vKg=G|882p&6gU;weNFYof+|24 z6f{ChVX)8Q^7iiLJq7cspiIsa`j*BtVp)KtXv)(x=I-YY^G0i-^1X+4??=JTeLnS~ zh>4vI|3*xz*fe!r?&G=yN6byVg*9j}Ypa2|^ygmmjC3hY3f*>5;JM%FYAXxT|Dr_y z=H?2AMa;r(Nm}5*h~-tItPXf617n_1I=#HSyZ*-Kqxp66<2#L{9#9lo7(-J0#5U9` zVyf97%;elRa>%R}0x7Y#poH$Q987pUwsDg1@uV#| zWr3Mao9GvTb=xCLH0>BEGQbJAI#R9Ph#hR|QVgU*mcBZu>&`o(UNh+`6T-uOH=t}F zk<#WkjM56!p%o>#@lI?-S=Ae%Y~L-?)-9`P@jI8WjbARp*=p+AwX67XLA#7H96Ur#rmi+MUqUF8^RaN6t;lz>vEwR8Jmv<5ZeM{Gfg^84czrHMNB{mRUagVEbT&@6k6MY5W`6Ja5Sg&V89-j4`(*YZ&J;l2aMV zDy|betw|5pLe3ynmj<3sWq|o1?gnxx*N_`PZwXz`jI9aYoMYy~&3Iey z-TTslof5|eY_#P$?6qYR^Dl6>j&~)FI^HyJ?cweWGiTDox#6VRxG!*TJ(n_3b_e9DuIT|cVt@*6V3hE*iwM*kB6t*8VKg`MdCfJmUE=L5dCwVUt?oP<1DPST*WI<4 zXVK$y0KI*OQ2|-9ge=JB8Lr9Q{O-W&tSQT7;y-x5jaE^l;GnBNbtIk zv4i!>DEjRqJu!?4@p7ISxx#vS8Y5=)sHDwZ6O7x&IFw!agp6!T3&$?<`9(fMv#%h= zZ2c+VGi!rvA#;>7$M^Czu^!U$B|N;q)nlB4#;~ZQr%n zdHx1x&5cH+J&W8wmzO~D#xarnFVXAxKIA#}!uu9-pE-P)jnosHZ&sSOpPNXS;cs98 z^Es>JiCrPajXKyjlrvVp=TlryuOWMlgG?B^N~4t(cuMZD&e*BxcQu#30@Wwm11+DK z_t*!|gld=FlTOs@DdmT+Q(JvGI$+CjhSd<5KzZ#leywE$-(-l9{o3z7zNWZ4tm3F5 zNojTm;;F3YNA^O7y>50h#XSSBxyvlpouL}p?E}M8W>9Apxy;o$`>U#@CjoxQ{bUX>vw;rOH~|Yz}M&V*IruFRFvZ(XuxM?{?%ZSn_e9 zheRNaXVb+KdlyHSA{^W7{1cnOgA|bwB4h_vTyyL#?1{=rU8}eT4a8GdD}O>f+!ta# z#(Dz|R?jjoVrhOsge(S|43>L*a?-zP8$TZyAUNo4?JReB4E@Udg z*A%41Laqb_z5mFTs*)Lf9fE+v`RJfGnq=eN{#?v^lW3mxZpXLDILgMO1R@TwcO}Og z3n~MhvC#-Eg`qsN>E+eM8#bmz#-33W^LJ@XC2lgXY)zsh=|%W)k7=c~Q0bj(?b?-s z4Sv1v1VJb^*8dxXQuRz#mib+;%mIO6)k|1{2D7vpLUe!V1P=t4-K5ZU76lIbl`hsY z6WvcrbT2MWsaOzN*xWD|R4@qBqEc4-e%OO-4pDMAonD=Pi6+CzdHnq=m*gCfWm*`+ zRQ#YcoGS=bwW2m-2bR(stA!v;;hFAtr%FlDJrM?!?Oqqatdd(^q>xL?ZG0Esn(oR%I^wQW0Vo z?U_z1V~f>Ore_?{JuQ@}==onUjq0jlw@hEYeU{r3p%#!=x~ijEem{U#VBr_kPqhk0 z2Sjgc-PTLP&&%vG^~dH}YLWUzPfnT=hZ6Is!`~Gu#_J>D1;z18D@bm&_PprN#9HJ< zxe_Qb+}AbU1XDMU%Rp(|4zmPm>0-KiO?kb{BZAjfzdwXR_%4NJC>rQaqJKNu(eSt(KpdAIiUfUvmGDPp3m zqrJuoE86O|CmIv$*sTE{frjjcf-nV+Iek0(+@#tnFxhkX>E_^{;~gn-QL~q#D2TTZ z%NG`MX5uhrCh!fHlwRE%aY6aHnq{fj#_Llv4H0)0zuNpUinuBNk9<%Ga-J$nC>UL|YepbWi sus>$d426DhXw=@^PVnZBw96a+nnW>2@pB1xFwgzZr?cbz?L2h;0(~palK=n! literal 4390 zcmeHLTTfF#5T4hX_#a54Zx&ig3|6|AMWD_E)G-Nx4mc1~m&&#I$p;5)&0XvtWv;2o_~ z8%FRWf!%F9hj@mxBhU?DQHp37eN(n%PxfV3o=3DT_}vcVR-v~RQFI|?1P@a=a@xiL z^)jfJaWSI$jCot&8{$e~X$u~hHH_dvq=nvGPWLU>%yx>^ac-SE%v!}+Hb7*0kX~`` zWST{d(;nn@T#ORX5+y`Id79ypTu;BQgRIkAmB|>IenS-PR!@%t#CU(X&x}mKPfO#7g>sBC%-u2&w}oq8UdRVzWL@gGUm%~KkM&?OKVg07R`WbozkNtI`dOIpQBnJ|wXp&} zwLaDgteclNi@e8MNxno`&~C<#beH&B&dek0Wvq!)*+e`q9YR?bDdNp_pa0kjwasIK z2%Naw^&Q6Vg8ycqZ2^d9v{^;$n&kJ9{TK1CT`=@=HvaeI<-IV@8ewjUe)iN!6jjw% zg!MPz7=klF#U%0-cizq{&Ut1T(u=8Q^{2m$b6+K!z!!V){SeL$!VaCUwXoCijL{0K z1qfy*`iA&xzlu8%kq}3WGI(NIw+!V|QuZX-0qq5D_PT+qAMJ{Irt^g1j6%D8O=>)KZhNpkn$w?dApxoK{;*~U`DTh>g=?N8{J2jmBbxbzIP1t2fZ|AG>hCPofW%$TR zMdvO(O*j`VI{f8*B<|>)^}SA{&-XButsTf{Yi_FcfAMlWm1`$7vDfxJKK&Y#%2*t>j$W1Y=EqSITDk4PULGH({w8m1~vIEG2% zHDr4UjHhuMe!M@NhvKOzmh0mX%9&+FOwHNZXVL2N1(9eHKtWcyZJH Date: Thu, 28 Nov 2024 18:30:59 +0100 Subject: [PATCH 07/11] add Indian tax regime --- data/regimes/in.json | 59 +++++++- data/schemas/regimes/mx/food-vouchers.json | 44 ++---- .../regimes/mx/fuel-account-balance.json | 68 +++------- regimes/in/in.go | 2 - regimes/in/invoices.go | 14 -- regimes/in/item.go | 33 ++--- regimes/in/scenarios_test.go | 128 ++++++++++++++++++ regimes/in/tax_categories.go | 54 +++++++- regimes/in/tax_identity.go | 45 +++--- 9 files changed, 297 insertions(+), 150 deletions(-) delete mode 100644 regimes/in/invoices.go create mode 100644 regimes/in/scenarios_test.go diff --git a/data/regimes/in.json b/data/regimes/in.json index b98b870c..0e62f423 100644 --- a/data/regimes/in.json +++ b/data/regimes/in.json @@ -166,7 +166,16 @@ "title": { "en": "Central Goods and Services Tax", "hi": "केंद्रीय माल और सेवा कर" - } + }, + "sources": [ + { + "title": { + "en": "Central GST Regulations", + "hi": "केंद्रीय जीएसटी नियमावली" + }, + "url": "https://gstcouncil.gov.in/central-gst" + } + ] }, { "code": "SGST", @@ -177,7 +186,16 @@ "title": { "en": "State Goods and Services Tax", "hi": "राज्य माल और सेवा कर" - } + }, + "sources": [ + { + "title": { + "en": "State GST Regulations", + "hi": "राज्य जीएसटी नियमावली" + }, + "url": "https://gstcouncil.gov.in/sgst" + } + ] }, { "code": "IGST", @@ -188,7 +206,16 @@ "title": { "en": "Integrated Goods and Services Tax", "hi": "एकीकृत माल और सेवा कर" - } + }, + "sources": [ + { + "title": { + "en": "Integrated GST Regulations", + "hi": "एकीकृत जीएसटी नियमावली" + }, + "url": "https://gstcouncil.gov.in/gst_council/igst" + } + ] }, { "code": "UTGST", @@ -199,7 +226,16 @@ "title": { "en": "Union Territory Goods and Services Tax", "hi": "केंद्र शासित प्रदेश माल और सेवा कर" - } + }, + "sources": [ + { + "title": { + "en": "Union Territory GST Regulations", + "hi": "यूटीजीएसटी नियमावली" + }, + "url": "https://gstcouncil.gov.in/utgst" + } + ] }, { "code": "CESS", @@ -208,9 +244,18 @@ "hi": "उपकर" }, "title": { - "en": "Cess on Luxury or Specific Goods", - "hi": "विलासिता या विशेष वस्तुओं पर उपकर" - } + "en": "GST Compensation Cess on Luxury or Specific Goods", + "hi": "विलासिता या विशेष वस्तुओं पर जीएसटी मुआवजा उपकर" + }, + "sources": [ + { + "title": { + "en": "GST Compensation Cess Regulations", + "hi": "जीएसटी मुआवजा उपकर नियमावली" + }, + "url": "https://gstcouncil.gov.in" + } + ] } ] } \ No newline at end of file diff --git a/data/schemas/regimes/mx/food-vouchers.json b/data/schemas/regimes/mx/food-vouchers.json index 9cedd146..07de9d70 100644 --- a/data/schemas/regimes/mx/food-vouchers.json +++ b/data/schemas/regimes/mx/food-vouchers.json @@ -7,18 +7,15 @@ "properties": { "employer_registration": { "type": "string", - "title": "Employer Registration", - "description": "Customer's employer registration number (maps to `registroPatronal`)." + "title": "Employer Registration" }, "account_number": { "type": "string", - "title": "Account Number", - "description": "Customer's account number (maps to `numeroDeCuenta`)." + "title": "Account Number" }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", - "description": "Sum of all line amounts (calculated, maps to `total`).", "calculated": true }, "lines": { @@ -26,8 +23,7 @@ "$ref": "#/$defs/FoodVouchersLine" }, "type": "array", - "title": "Lines", - "description": "List of food vouchers issued to the customer's employees (maps to `Conceptos`)." + "title": "Lines" } }, "type": "object", @@ -35,30 +31,25 @@ "account_number", "total", "lines" - ], - "description": "FoodVouchers 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." + ] }, "FoodVouchersEmployee": { "properties": { "tax_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's Tax Identity Code", - "description": "Employee's tax identity code (maps to `rfc`)." + "title": "Employee's Tax Identity Code" }, "curp": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's CURP", - "description": "Employee's CURP (\"Clave Única de Registro de Población\", maps to `curp`)." + "title": "Employee's CURP" }, "name": { "type": "string", - "title": "Employee's Name", - "description": "Employee's name (maps to `nombre`)." + "title": "Employee's Name" }, "social_security": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's Social Security Number", - "description": "Employee's Social Security Number (maps to `numSeguridadSocial`)." + "title": "Employee's Social Security Number" } }, "type": "object", @@ -66,36 +57,30 @@ "tax_code", "curp", "name" - ], - "description": "FoodVouchersEmployee represents an employee that received a food voucher." + ] }, "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", - "description": "Identifier of the e-wallet that received the food voucher (maps to `Identificador`)." + "title": "E-wallet Identifier" }, "issue_date_time": { "$ref": "https://gobl.org/draft-0/cal/date-time", - "title": "Issue Date and Time", - "description": "Date and time of the food voucher's issue (maps to `Fecha`)." + "title": "Issue Date and Time" }, "employee": { "$ref": "#/$defs/FoodVouchersEmployee", - "title": "Employee", - "description": "Employee that received the food voucher." + "title": "Employee" }, "amount": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Amount", - "description": "Amount of the food voucher (maps to `importe`)." + "title": "Amount" } }, "type": "object", @@ -104,8 +89,7 @@ "e_wallet_id", "issue_date_time", "amount" - ], - "description": "FoodVouchersLine represents a single food voucher issued to the e-wallet of one of the customer's employees." + ] } } } \ No newline at end of file diff --git a/data/schemas/regimes/mx/fuel-account-balance.json b/data/schemas/regimes/mx/fuel-account-balance.json index e8c05531..7fd39dd6 100644 --- a/data/schemas/regimes/mx/fuel-account-balance.json +++ b/data/schemas/regimes/mx/fuel-account-balance.json @@ -7,19 +7,16 @@ "properties": { "account_number": { "type": "string", - "title": "Account Number", - "description": "Customer's account number (maps to `NumeroDeCuenta`)." + "title": "Account Number" }, "subtotal": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Subtotal", - "description": "Sum of all line totals (i.e. taxes not included) (calculated, maps to `SubTotal`).", "calculated": true }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", - "description": "Grand total after taxes have been applied (calculated, maps to `Total`).", "calculated": true }, "lines": { @@ -27,8 +24,7 @@ "$ref": "#/$defs/FuelAccountLine" }, "type": "array", - "title": "Lines", - "description": "List of fuel purchases made with the customer's e-wallets (maps to `Conceptos`)." + "title": "Lines" } }, "type": "object", @@ -37,30 +33,25 @@ "subtotal", "total", "lines" - ], - "description": "FuelAccountBalance carries the data to produce a CFDI's \"Complemento de Estado de Cuenta de Combustibles para Monederos Electrónicos\" (version 1.2 revision B) providing detailed information about fuel purchases made with electronic wallets." + ] }, "FuelAccountItem": { "properties": { "type": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Type", - "description": "Type of fuel (one of `c_ClaveTipoCombustible` codes, maps to `TipoCombustible`)." + "title": "Type" }, "unit": { "$ref": "https://gobl.org/draft-0/org/unit", - "title": "Unit", - "description": "Reference unit of measure used in the price and the quantity (maps to `Unidad`)." + "title": "Unit" }, "name": { "type": "string", - "title": "Name", - "description": "Name of the fuel (maps to `NombreCombustible`)." + "title": "Name" }, "price": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Price", - "description": "Base price of a single unit of the fuel without taxes (maps to `ValorUnitario`)." + "title": "Price" } }, "type": "object", @@ -68,56 +59,46 @@ "type", "name", "price" - ], - "description": "FuelAccountItem provides the details of a fuel purchase." + ] }, "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", - "description": "Identifier of the e-wallet used to make the purchase (maps to `Identificador`)." + "title": "E-wallet Identifier" }, "purchase_date_time": { "$ref": "https://gobl.org/draft-0/cal/date-time", - "title": "Purchase Date and Time", - "description": "Date and time of the purchase (maps to `Fecha`)." + "title": "Purchase Date and Time" }, "vendor_tax_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Vendor's Tax Identity Code", - "description": "Tax Identity Code of the fuel's vendor (maps to `Rfc`)" + "title": "Vendor's Tax Identity Code" }, "service_station_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Service Station Code", - "description": "Code of the service station where the purchase was made (maps to `ClaveEstacion`)." + "title": "Service Station Code" }, "quantity": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Quantity", - "description": "Amount of fuel units purchased (maps to `Cantidad`)" + "title": "Quantity" }, "item": { "$ref": "#/$defs/FuelAccountItem", - "title": "Item", - "description": "Details of the fuel purchased." + "title": "Item" }, "purchase_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Purchase Code", - "description": "Identifier of the purchase (maps to `FolioOperacion`)." + "title": "Purchase Code" }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", - "description": "Result of quantity multiplied by the unit price (maps to `Importe`).", "calculated": true }, "taxes": { @@ -125,8 +106,7 @@ "$ref": "#/$defs/FuelAccountTax" }, "type": "array", - "title": "Taxes", - "description": "Map of taxes applied to the purchase (maps to `Traslados`)." + "title": "Taxes" } }, "type": "object", @@ -141,30 +121,25 @@ "purchase_code", "total", "taxes" - ], - "description": "FuelAccountLine represents a single fuel purchase made with an e-wallet issued by the invoice's supplier." + ] }, "FuelAccountTax": { "properties": { "cat": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Category", - "description": "Category that identifies the tax (\"VAT\" or \"IEPS\", maps to `Impuesto`)" + "title": "Category" }, "percent": { "$ref": "https://gobl.org/draft-0/num/percentage", - "title": "Percent", - "description": "Percent applicable to the line total (tasa) to use instead of Rate (maps to `TasaoCuota`)" + "title": "Percent" }, "rate": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Rate", - "description": "Rate is a fixed fee to apply to the line quantity (cuota) (maps to `TasaOCuota`)" + "title": "Rate" }, "amount": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Amount", - "description": "Total amount of the tax once the percent or rate has been applied (maps to `Importe`).", "calculated": true } }, @@ -172,8 +147,7 @@ "required": [ "cat", "amount" - ], - "description": "FuelAccountTax represents a single tax applied to a fuel purchase." + ] } } } \ No newline at end of file diff --git a/regimes/in/in.go b/regimes/in/in.go index 05e610e6..2d9de951 100644 --- a/regimes/in/in.go +++ b/regimes/in/in.go @@ -50,8 +50,6 @@ func New() *tax.RegimeDef { // Validate function assesses the document type to determine if validation is required. func Validate(doc interface{}) error { switch obj := doc.(type) { - case *bill.Invoice: - return validateInvoice(obj) case *tax.Identity: return validateTaxIdentity(obj) case *org.Identity: diff --git a/regimes/in/invoices.go b/regimes/in/invoices.go deleted file mode 100644 index e71a06c0..00000000 --- a/regimes/in/invoices.go +++ /dev/null @@ -1,14 +0,0 @@ -package in - -import ( - "github.com/invopop/gobl/bill" - "github.com/invopop/validation" -) - -func validateInvoice(inv *bill.Invoice) error { - return validation.ValidateStruct(inv, - validation.Field(&inv.Supplier, - validation.Required, - ), - ) -} diff --git a/regimes/in/item.go b/regimes/in/item.go index 68b0a70c..3e74d03c 100644 --- a/regimes/in/item.go +++ b/regimes/in/item.go @@ -17,39 +17,32 @@ const ( // HSNCodeRegexp defines the regular expression to validate HSN codes. var HSNCodeRegexp = regexp.MustCompile(`^(?:\d{4}|\d{6}|\d{8})$`) -func validateItem(value any) error { - item, ok := value.(*org.Item) - if !ok || item == nil { - return nil - } +func validateItem(it *org.Item) error { + + return validation.ValidateStruct(it, + validation.Field(&it.Identities, + validation.Each( - return validation.ValidateStruct(item, - validation.Field(&item.Identities, - validation.By(validItemIdentities), + validation.By(validItemIdentities), + ), validation.Skip, ), ) } func validItemIdentities(value interface{}) error { - identities, ok := value.([]*org.Identity) + id, ok := value.(*org.Identity) if !ok { return nil } - for _, identity := range identities { - if identity == nil { - continue - } - - if identity.Type == IdentityTypeHSN { - val := string(identity.Code) + if id.Type == IdentityTypeHSN { + val := string(id.Code) - if !HSNCodeRegexp.MatchString(val) { - return errors.New("must be a 4, 6, or 8-digit number") - } - break + if !HSNCodeRegexp.MatchString(val) { + return errors.New("must be a 4, 6, or 8-digit number") } + } return nil diff --git a/regimes/in/scenarios_test.go b/regimes/in/scenarios_test.go new file mode 100644 index 00000000..33d1b23c --- /dev/null +++ b/regimes/in/scenarios_test.go @@ -0,0 +1,128 @@ +package in_test + +import ( + "testing" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testInvoiceStandard(t *testing.T) *bill.Invoice { + t.Helper() + i := &bill.Invoice{ + Series: "TEST", + Code: "0002", + Currency: "INR", + Supplier: &org.Party{ + Name: "Test Supplier", + TaxID: &tax.Identity{ + Country: "IN", + Code: "27AAPFU0939F1ZV", + }, + }, + Tax: &bill.Tax{ + Ext: tax.Extensions{ + "in-supply-place": "Ciudad prueba", + }, + }, + Customer: &org.Party{ + Name: "Test Customer", + TaxID: &tax.Identity{ + Country: "IN", + Code: "27AAPFU0939F1ZV", + }, + }, + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(1, 0), + Item: &org.Item{ + Name: "Development services", + Price: num.MakeAmount(10000, 2), + Unit: org.UnitPackage, + Identities: []*org.Identity{ + { + Type: "HSN", + Code: "12345678", + }, + }, + }, + Taxes: tax.Set{ + { + Category: "CGST", + Percent: num.NewPercentage(9, 0), + }, + { + Category: "SGST", + Percent: num.NewPercentage(9, 0), + }, + }, + }, + }, + } + return i +} + +func testInvoiceSimplified(t *testing.T) *bill.Invoice { + t.Helper() + i := &bill.Invoice{ + Series: "TEST", + Code: "0002", + Currency: "INR", + Supplier: &org.Party{ + Name: "Test Supplier", + TaxID: &tax.Identity{ + Country: "IN", + Code: "27AAPFU0939F1ZV", + }, + }, + Tax: &bill.Tax{ + Ext: tax.Extensions{ + "in-supply-place": "Ciudad prueba", + }, + }, + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(1, 0), + Item: &org.Item{ + Name: "Development services", + Price: num.MakeAmount(10000, 2), + Unit: org.UnitPackage, + Identities: []*org.Identity{ + { + Type: "HSN", + Code: "12345678", + }, + }, + }, + Taxes: tax.Set{ + { + Category: "CGST", + Percent: num.NewPercentage(9, 0), + }, + { + Category: "SGST", + Percent: num.NewPercentage(9, 0), + }, + }, + }, + }, + } + return i +} + +func TestInvoiceDocumentScenarios(t *testing.T) { + i := testInvoiceStandard(t) + require.NoError(t, i.Calculate()) + require.NoError(t, i.Validate()) + + i = testInvoiceSimplified(t) + i.SetTags(tax.TagSimplified) + require.NoError(t, i.Calculate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Notes[0].Src, tax.TagSimplified) + assert.Equal(t, i.Notes[0].Text, "Simplified Tax Invoice") +} diff --git a/regimes/in/tax_categories.go b/regimes/in/tax_categories.go index 9c516511..eda7cbba 100644 --- a/regimes/in/tax_categories.go +++ b/regimes/in/tax_categories.go @@ -28,7 +28,15 @@ var taxCategories = []*tax.CategoryDef{ i18n.EN: "Central Goods and Services Tax", i18n.HI: "केंद्रीय माल और सेवा कर", }, - Rates: []*tax.RateDef{}, + Sources: []*tax.Source{ + { + Title: i18n.String{ + i18n.EN: "Central GST Regulations", + i18n.HI: "केंद्रीय जीएसटी नियमावली", + }, + URL: "https://gstcouncil.gov.in/central-gst", + }, + }, }, // State Goods and Services Tax (SGST) @@ -42,7 +50,15 @@ var taxCategories = []*tax.CategoryDef{ i18n.EN: "State Goods and Services Tax", i18n.HI: "राज्य माल और सेवा कर", }, - Rates: []*tax.RateDef{}, + Sources: []*tax.Source{ + { + Title: i18n.String{ + i18n.EN: "State GST Regulations", + i18n.HI: "राज्य जीएसटी नियमावली", + }, + URL: "https://gstcouncil.gov.in/sgst", + }, + }, }, // Integrated Goods and Services Tax (IGST) @@ -56,7 +72,15 @@ var taxCategories = []*tax.CategoryDef{ i18n.EN: "Integrated Goods and Services Tax", i18n.HI: "एकीकृत माल और सेवा कर", }, - Rates: []*tax.RateDef{}, + Sources: []*tax.Source{ + { + Title: i18n.String{ + i18n.EN: "Integrated GST Regulations", + i18n.HI: "एकीकृत जीएसटी नियमावली", + }, + URL: "https://gstcouncil.gov.in/gst_council/igst", + }, + }, }, // Union Territory Goods and Services Tax (UTGST) @@ -70,7 +94,15 @@ var taxCategories = []*tax.CategoryDef{ i18n.EN: "Union Territory Goods and Services Tax", i18n.HI: "केंद्र शासित प्रदेश माल और सेवा कर", }, - Rates: []*tax.RateDef{}, + Sources: []*tax.Source{ + { + Title: i18n.String{ + i18n.EN: "Union Territory GST Regulations", + i18n.HI: "यूटीजीएसटी नियमावली", + }, + URL: "https://gstcouncil.gov.in/utgst", + }, + }, }, // Cess (Additional Tax for Luxury or Specific Goods) @@ -81,9 +113,17 @@ var taxCategories = []*tax.CategoryDef{ i18n.HI: "उपकर", }, Title: i18n.String{ - i18n.EN: "Cess on Luxury or Specific Goods", - i18n.HI: "विलासिता या विशेष वस्तुओं पर उपकर", + i18n.EN: "GST Compensation Cess on Luxury or Specific Goods", + i18n.HI: "विलासिता या विशेष वस्तुओं पर जीएसटी मुआवजा उपकर", + }, + Sources: []*tax.Source{ + { + Title: i18n.String{ + i18n.EN: "GST Compensation Cess Regulations", + i18n.HI: "जीएसटी मुआवजा उपकर नियमावली", + }, + URL: "https://gstcouncil.gov.in", + }, }, - Rates: []*tax.RateDef{}, }, } diff --git a/regimes/in/tax_identity.go b/regimes/in/tax_identity.go index be6337c6..d4c90f8e 100644 --- a/regimes/in/tax_identity.go +++ b/regimes/in/tax_identity.go @@ -14,13 +14,6 @@ import ( var ( taxCodeRegexp = regexp.MustCompile(`^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$`) - - conversionTable = map[rune]int{ - '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, - 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'I': 18, - 'J': 19, 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, 'P': 25, 'Q': 26, 'R': 27, - 'S': 28, 'T': 29, 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 'Z': 35, - } ) func normalizeTaxIdentity(tID *tax.Identity) { @@ -49,24 +42,21 @@ func validateTaxCode(value interface{}) error { return errors.New("invalid GSTIN format") } - if !hasValidChecksum(val) { - return errors.New("checksum mismatch") + if err := hasValidChecksum(val); err != nil { + return err } return nil } -func hasValidChecksum(gstin string) bool { +func hasValidChecksum(gstin string) error { if len(gstin) != 15 { - return false + return errors.New("invalid GSTIN length") } sum := 0 for i, char := range gstin[:14] { - value, exists := conversionTable[char] - if !exists { - return false - } + value := charToValue(char) multiplier := 1 if i%2 != 0 { @@ -79,16 +69,25 @@ func hasValidChecksum(gstin string) bool { remainder := sum % 36 calculatedChecksum := (36 - remainder) % 36 - checksumChar := findCharByValue(calculatedChecksum) + checksumChar := valueToChar(calculatedChecksum) + + if checksumChar != rune(gstin[14]) { + return errors.New("checksum mismatch") + } - return checksumChar == rune(gstin[14]) + return nil } -func findCharByValue(value int) rune { - for char, num := range conversionTable { - if num == value { - return char - } +func charToValue(char rune) int { + if char >= '0' && char <= '9' { + return int(char - '0') + } + return int(char - 'A' + 10) +} + +func valueToChar(value int) rune { + if value >= 0 && value <= 9 { + return rune('0' + value) } - return ' ' + return rune('A' + value - 10) } From 6ac04a62c4f17e6ea7f1ae6dc33b6228e6aed9fb Mon Sep 17 00:00:00 2001 From: richifernandez Date: Thu, 28 Nov 2024 18:33:51 +0100 Subject: [PATCH 08/11] add Indian tax regime --- data/schemas/regimes/mx/food-vouchers.json | 44 ++++++++---- .../regimes/mx/fuel-account-balance.json | 68 +++++++++++++------ 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/data/schemas/regimes/mx/food-vouchers.json b/data/schemas/regimes/mx/food-vouchers.json index 07de9d70..9cedd146 100644 --- a/data/schemas/regimes/mx/food-vouchers.json +++ b/data/schemas/regimes/mx/food-vouchers.json @@ -7,15 +7,18 @@ "properties": { "employer_registration": { "type": "string", - "title": "Employer Registration" + "title": "Employer Registration", + "description": "Customer's employer registration number (maps to `registroPatronal`)." }, "account_number": { "type": "string", - "title": "Account Number" + "title": "Account Number", + "description": "Customer's account number (maps to `numeroDeCuenta`)." }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", + "description": "Sum of all line amounts (calculated, maps to `total`).", "calculated": true }, "lines": { @@ -23,7 +26,8 @@ "$ref": "#/$defs/FoodVouchersLine" }, "type": "array", - "title": "Lines" + "title": "Lines", + "description": "List of food vouchers issued to the customer's employees (maps to `Conceptos`)." } }, "type": "object", @@ -31,25 +35,30 @@ "account_number", "total", "lines" - ] + ], + "description": "FoodVouchers 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." }, "FoodVouchersEmployee": { "properties": { "tax_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's Tax Identity Code" + "title": "Employee's Tax Identity Code", + "description": "Employee's tax identity code (maps to `rfc`)." }, "curp": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's CURP" + "title": "Employee's CURP", + "description": "Employee's CURP (\"Clave Única de Registro de Población\", maps to `curp`)." }, "name": { "type": "string", - "title": "Employee's Name" + "title": "Employee's Name", + "description": "Employee's name (maps to `nombre`)." }, "social_security": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Employee's Social Security Number" + "title": "Employee's Social Security Number", + "description": "Employee's Social Security Number (maps to `numSeguridadSocial`)." } }, "type": "object", @@ -57,30 +66,36 @@ "tax_code", "curp", "name" - ] + ], + "description": "FoodVouchersEmployee represents an employee that received a food voucher." }, "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" + "title": "E-wallet Identifier", + "description": "Identifier of the e-wallet that received the food voucher (maps to `Identificador`)." }, "issue_date_time": { "$ref": "https://gobl.org/draft-0/cal/date-time", - "title": "Issue Date and Time" + "title": "Issue Date and Time", + "description": "Date and time of the food voucher's issue (maps to `Fecha`)." }, "employee": { "$ref": "#/$defs/FoodVouchersEmployee", - "title": "Employee" + "title": "Employee", + "description": "Employee that received the food voucher." }, "amount": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Amount" + "title": "Amount", + "description": "Amount of the food voucher (maps to `importe`)." } }, "type": "object", @@ -89,7 +104,8 @@ "e_wallet_id", "issue_date_time", "amount" - ] + ], + "description": "FoodVouchersLine represents a single food voucher issued to the e-wallet of one of the customer's employees." } } } \ No newline at end of file diff --git a/data/schemas/regimes/mx/fuel-account-balance.json b/data/schemas/regimes/mx/fuel-account-balance.json index 7fd39dd6..e8c05531 100644 --- a/data/schemas/regimes/mx/fuel-account-balance.json +++ b/data/schemas/regimes/mx/fuel-account-balance.json @@ -7,16 +7,19 @@ "properties": { "account_number": { "type": "string", - "title": "Account Number" + "title": "Account Number", + "description": "Customer's account number (maps to `NumeroDeCuenta`)." }, "subtotal": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Subtotal", + "description": "Sum of all line totals (i.e. taxes not included) (calculated, maps to `SubTotal`).", "calculated": true }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", + "description": "Grand total after taxes have been applied (calculated, maps to `Total`).", "calculated": true }, "lines": { @@ -24,7 +27,8 @@ "$ref": "#/$defs/FuelAccountLine" }, "type": "array", - "title": "Lines" + "title": "Lines", + "description": "List of fuel purchases made with the customer's e-wallets (maps to `Conceptos`)." } }, "type": "object", @@ -33,25 +37,30 @@ "subtotal", "total", "lines" - ] + ], + "description": "FuelAccountBalance carries the data to produce a CFDI's \"Complemento de Estado de Cuenta de Combustibles para Monederos Electrónicos\" (version 1.2 revision B) providing detailed information about fuel purchases made with electronic wallets." }, "FuelAccountItem": { "properties": { "type": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Type" + "title": "Type", + "description": "Type of fuel (one of `c_ClaveTipoCombustible` codes, maps to `TipoCombustible`)." }, "unit": { "$ref": "https://gobl.org/draft-0/org/unit", - "title": "Unit" + "title": "Unit", + "description": "Reference unit of measure used in the price and the quantity (maps to `Unidad`)." }, "name": { "type": "string", - "title": "Name" + "title": "Name", + "description": "Name of the fuel (maps to `NombreCombustible`)." }, "price": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Price" + "title": "Price", + "description": "Base price of a single unit of the fuel without taxes (maps to `ValorUnitario`)." } }, "type": "object", @@ -59,46 +68,56 @@ "type", "name", "price" - ] + ], + "description": "FuelAccountItem provides the details of a fuel purchase." }, "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" + "title": "E-wallet Identifier", + "description": "Identifier of the e-wallet used to make the purchase (maps to `Identificador`)." }, "purchase_date_time": { "$ref": "https://gobl.org/draft-0/cal/date-time", - "title": "Purchase Date and Time" + "title": "Purchase Date and Time", + "description": "Date and time of the purchase (maps to `Fecha`)." }, "vendor_tax_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Vendor's Tax Identity Code" + "title": "Vendor's Tax Identity Code", + "description": "Tax Identity Code of the fuel's vendor (maps to `Rfc`)" }, "service_station_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Service Station Code" + "title": "Service Station Code", + "description": "Code of the service station where the purchase was made (maps to `ClaveEstacion`)." }, "quantity": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Quantity" + "title": "Quantity", + "description": "Amount of fuel units purchased (maps to `Cantidad`)" }, "item": { "$ref": "#/$defs/FuelAccountItem", - "title": "Item" + "title": "Item", + "description": "Details of the fuel purchased." }, "purchase_code": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Purchase Code" + "title": "Purchase Code", + "description": "Identifier of the purchase (maps to `FolioOperacion`)." }, "total": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Total", + "description": "Result of quantity multiplied by the unit price (maps to `Importe`).", "calculated": true }, "taxes": { @@ -106,7 +125,8 @@ "$ref": "#/$defs/FuelAccountTax" }, "type": "array", - "title": "Taxes" + "title": "Taxes", + "description": "Map of taxes applied to the purchase (maps to `Traslados`)." } }, "type": "object", @@ -121,25 +141,30 @@ "purchase_code", "total", "taxes" - ] + ], + "description": "FuelAccountLine represents a single fuel purchase made with an e-wallet issued by the invoice's supplier." }, "FuelAccountTax": { "properties": { "cat": { "$ref": "https://gobl.org/draft-0/cbc/code", - "title": "Category" + "title": "Category", + "description": "Category that identifies the tax (\"VAT\" or \"IEPS\", maps to `Impuesto`)" }, "percent": { "$ref": "https://gobl.org/draft-0/num/percentage", - "title": "Percent" + "title": "Percent", + "description": "Percent applicable to the line total (tasa) to use instead of Rate (maps to `TasaoCuota`)" }, "rate": { "$ref": "https://gobl.org/draft-0/num/amount", - "title": "Rate" + "title": "Rate", + "description": "Rate is a fixed fee to apply to the line quantity (cuota) (maps to `TasaOCuota`)" }, "amount": { "$ref": "https://gobl.org/draft-0/num/amount", "title": "Amount", + "description": "Total amount of the tax once the percent or rate has been applied (maps to `Importe`).", "calculated": true } }, @@ -147,7 +172,8 @@ "required": [ "cat", "amount" - ] + ], + "description": "FuelAccountTax represents a single tax applied to a fuel purchase." } } } \ No newline at end of file From 9fc53bf7506b9143cab2caa61e78c2a873a23e33 Mon Sep 17 00:00:00 2001 From: richifernandez Date: Thu, 28 Nov 2024 19:12:34 +0100 Subject: [PATCH 09/11] add Indian tax regime --- regimes/in/invoices_test.go | 77 ------------------------------------ regimes/in/scenarios_test.go | 51 +----------------------- 2 files changed, 2 insertions(+), 126 deletions(-) delete mode 100644 regimes/in/invoices_test.go diff --git a/regimes/in/invoices_test.go b/regimes/in/invoices_test.go deleted file mode 100644 index 95307227..00000000 --- a/regimes/in/invoices_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package in_test - -import ( - "testing" - - "github.com/invopop/gobl/bill" - "github.com/invopop/gobl/num" - "github.com/invopop/gobl/org" - "github.com/invopop/gobl/tax" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func validInvoice() *bill.Invoice { - return &bill.Invoice{ - Series: "TEST", - Code: "0002", - Currency: "INR", - Supplier: &org.Party{ - Name: "Test Supplier", - TaxID: &tax.Identity{ - Country: "IN", - Code: "27AAPFU0939F1ZV", - }, - }, - Tax: &bill.Tax{ - Ext: tax.Extensions{ - "in-supply-place": "Ciudad prueba", - }, - }, - Customer: &org.Party{ - Name: "Test Customer", - TaxID: &tax.Identity{ - Country: "IN", - Code: "27AAPFU0939F1ZV", - }, - }, - Lines: []*bill.Line{ - { - Quantity: num.MakeAmount(1, 0), - Item: &org.Item{ - Name: "Development services", - Price: num.MakeAmount(10000, 2), - Unit: org.UnitPackage, - Identities: []*org.Identity{ - { - Type: "HSN", - Code: "12345678", - }, - }, - }, - Taxes: tax.Set{ - { - Category: "CGST", - Percent: num.NewPercentage(9, 0), - }, - { - Category: "SGST", - Percent: num.NewPercentage(9, 0), - }, - }, - }, - }, - } -} - -func TestInvoiceValidation(t *testing.T) { - inv := validInvoice() - require.NoError(t, inv.Calculate()) - assert.NoError(t, inv.Validate()) - - inv = validInvoice() - inv.Supplier = nil - require.NoError(t, inv.Calculate()) - assert.ErrorContains(t, inv.Validate(), "supplier: cannot be blank.") - -} diff --git a/regimes/in/scenarios_test.go b/regimes/in/scenarios_test.go index 33d1b23c..cce7cf7a 100644 --- a/regimes/in/scenarios_test.go +++ b/regimes/in/scenarios_test.go @@ -66,61 +66,14 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { return i } -func testInvoiceSimplified(t *testing.T) *bill.Invoice { - t.Helper() - i := &bill.Invoice{ - Series: "TEST", - Code: "0002", - Currency: "INR", - Supplier: &org.Party{ - Name: "Test Supplier", - TaxID: &tax.Identity{ - Country: "IN", - Code: "27AAPFU0939F1ZV", - }, - }, - Tax: &bill.Tax{ - Ext: tax.Extensions{ - "in-supply-place": "Ciudad prueba", - }, - }, - Lines: []*bill.Line{ - { - Quantity: num.MakeAmount(1, 0), - Item: &org.Item{ - Name: "Development services", - Price: num.MakeAmount(10000, 2), - Unit: org.UnitPackage, - Identities: []*org.Identity{ - { - Type: "HSN", - Code: "12345678", - }, - }, - }, - Taxes: tax.Set{ - { - Category: "CGST", - Percent: num.NewPercentage(9, 0), - }, - { - Category: "SGST", - Percent: num.NewPercentage(9, 0), - }, - }, - }, - }, - } - return i -} - func TestInvoiceDocumentScenarios(t *testing.T) { i := testInvoiceStandard(t) require.NoError(t, i.Calculate()) require.NoError(t, i.Validate()) - i = testInvoiceSimplified(t) + i = testInvoiceStandard(t) i.SetTags(tax.TagSimplified) + i.Customer = nil require.NoError(t, i.Calculate()) assert.Len(t, i.Notes, 1) assert.Equal(t, i.Notes[0].Src, tax.TagSimplified) From 5e9e22bed3939f741af07b2e4d11c6bd71aae98e Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 5 Dec 2024 15:33:47 +0000 Subject: [PATCH 10/11] Refactoring Indian org Identities and Items --- regimes/in/bill.go | 13 ++++ regimes/in/extensions.go | 30 -------- regimes/in/identities.go | 48 ------------ regimes/in/identities_test.go | 76 ------------------- regimes/in/in.go | 10 +-- regimes/in/item.go | 49 ------------ regimes/in/item_test.go | 74 ------------------ regimes/in/org_identities.go | 61 +++++++++++++++ regimes/in/org_identities_test.go | 120 ++++++++++++++++++++++++++++++ regimes/in/org_item.go | 15 ++++ regimes/in/org_item_test.go | 39 ++++++++++ tax/regime_def.go | 5 -- 12 files changed, 252 insertions(+), 288 deletions(-) create mode 100644 regimes/in/bill.go delete mode 100644 regimes/in/extensions.go delete mode 100644 regimes/in/identities.go delete mode 100644 regimes/in/identities_test.go delete mode 100644 regimes/in/item.go delete mode 100644 regimes/in/item_test.go create mode 100644 regimes/in/org_identities.go create mode 100644 regimes/in/org_identities_test.go create mode 100644 regimes/in/org_item.go create mode 100644 regimes/in/org_item_test.go diff --git a/regimes/in/bill.go b/regimes/in/bill.go new file mode 100644 index 00000000..bc45ec69 --- /dev/null +++ b/regimes/in/bill.go @@ -0,0 +1,13 @@ +package in + +import "github.com/invopop/gobl/cbc" + +const ( + // ChargeKeyCompoensationCess is used for addtional charges added to an invoice for the special + // compensation "cess" (cess means tax or levy) which may be appled as a percentage or specific + // amount based on valumes or other criteria. + // + // Typically this tariff is applied to luxury goods, tobacco, and other items that are considered + // harmful to the environment or society. + ChargeKeyCompensationCess cbc.Key = "compensation-cess" +) diff --git a/regimes/in/extensions.go b/regimes/in/extensions.go deleted file mode 100644 index 914ef0ec..00000000 --- a/regimes/in/extensions.go +++ /dev/null @@ -1,30 +0,0 @@ -package in - -import ( - "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/i18n" - "github.com/invopop/gobl/pkg/here" -) - -// Extension keys used in India. -const ( - ExtKeySupplyPlace cbc.Key = "in-supply-place" -) - -var extensions = []*cbc.KeyDefinition{ - { - Key: ExtKeySupplyPlace, - Name: i18n.String{ - i18n.EN: "Place of Supply", - i18n.HI: "आपूर्ति का स्थान", - }, - Desc: i18n.String{ - i18n.EN: here.Doc(` - The location to which the goods or services are supplied. In GST, this is referred to as the 'Place of Supply'. - `), - i18n.HI: here.Doc(` - वह स्थान जहां वस्तुएं या सेवाएं प्रदान की जाती हैं। GST में इसे 'आपूर्ति का स्थान' कहा जाता है। - `), - }, - }, -} diff --git a/regimes/in/identities.go b/regimes/in/identities.go deleted file mode 100644 index 971cc393..00000000 --- a/regimes/in/identities.go +++ /dev/null @@ -1,48 +0,0 @@ -package in - -import ( - "regexp" - - "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/i18n" - "github.com/invopop/gobl/org" - "github.com/invopop/validation" -) - -const ( - // IdentityKeyPAN represents the Indian Permanent Account Number (PAN). It is a unique identifier assigned - // to individuals, companies, and other entities. - IdentityKeyPAN cbc.Key = "in-pan" -) - -var panRegexPattern = regexp.MustCompile(`^[A-Z]{5}[0-9]{4}[A-Z]$`) - -var identityKeyDefinitions = []*cbc.KeyDefinition{ - { - Key: IdentityKeyPAN, - Name: i18n.String{ - i18n.EN: "Permanent Account Number", - i18n.HI: "स्थायी खाता संख्या", - }, - }, -} - -func normalizePAN(id *org.Identity) { - if id == nil || id.Key != IdentityKeyPAN { - return - } - code := cbc.NormalizeAlphanumericalCode(id.Code).String() - id.Code = cbc.Code(code) -} - -func validatePAN(id *org.Identity) error { - if id == nil || id.Key != IdentityKeyPAN { - return nil - } - - return validation.ValidateStruct(id, - validation.Field(&id.Code, - validation.Match(panRegexPattern), - ), - ) -} diff --git a/regimes/in/identities_test.go b/regimes/in/identities_test.go deleted file mode 100644 index 094733b5..00000000 --- a/regimes/in/identities_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package in_test - -import ( - "testing" - - "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/org" - "github.com/invopop/gobl/regimes/in" - "github.com/stretchr/testify/assert" -) - -func TestNormalizePAN(t *testing.T) { - tests := []struct { - name string - input cbc.Code - expected cbc.Code - }{ - {name: "already normalized", input: "ABCDE1234F", expected: "ABCDE1234F"}, - {name: "lowercase input", input: "abcde1234f", expected: "ABCDE1234F"}, - {name: "mixed case input", input: "AbCdE1234f", expected: "ABCDE1234F"}, - {name: "extra spaces", input: " ABCDE1234F ", expected: "ABCDE1234F"}, - {name: "special characters", input: "AB-CDE1234F", expected: "ABCDE1234F"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - id := &org.Identity{Key: in.IdentityKeyPAN, Code: tt.input} - in.Normalize(id) - assert.Equal(t, tt.expected, id.Code) - }) - } -} - -func TestValidatePAN(t *testing.T) { - tests := []struct { - name string - code cbc.Code - err string - }{ - {name: "valid PAN 1", code: "BAJPC4350M"}, - {name: "valid PAN 2", code: "DAJPC4150P"}, - {name: "valid PAN 3", code: "XGZFE7225A"}, - {name: "valid PAN 4", code: "CTUGE1616Y"}, - - { - name: "too short", - code: "ABC1234F", - err: "code: must be in a valid format.", - }, - { - name: "contains spaces", - code: "ABCDE 1234F", - err: "code: must be in a valid format.", - }, - { - name: "extra characters", - code: "ABCDE1234F12", - err: "code: must be in a valid format.", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - id := &org.Identity{Key: in.IdentityKeyPAN, Code: tt.code} - err := in.Validate(id) - - if tt.err == "" { - assert.NoError(t, err) - } else { - if assert.Error(t, err) { - assert.Contains(t, err.Error(), tt.err) - } - } - }) - } -} diff --git a/regimes/in/in.go b/regimes/in/in.go index 2d9de951..be553830 100644 --- a/regimes/in/in.go +++ b/regimes/in/in.go @@ -15,7 +15,7 @@ func init() { tax.RegisterRegimeDef(New()) } -// New provides the tax region definition for IN. +// New provides the tax region definition for India. func New() *tax.RegimeDef { return &tax.RegimeDef{ Country: "IN", @@ -30,7 +30,6 @@ func New() *tax.RegimeDef { Scenarios: []*tax.ScenarioSet{ invoiceScenarios, }, - IdentityKeys: identityKeyDefinitions, Corrections: []*tax.CorrectionDefinition{ { Schema: bill.ShortSchemaInvoice, @@ -43,7 +42,6 @@ func New() *tax.RegimeDef { Validator: Validate, Normalizer: Normalize, Categories: taxCategories, - Extensions: extensions, } } @@ -53,9 +51,9 @@ func Validate(doc interface{}) error { case *tax.Identity: return validateTaxIdentity(obj) case *org.Identity: - return validatePAN(obj) + return validateOrgIdentity(obj) case *org.Item: - return validateItem(obj) + return validateOrgItem(obj) } return nil } @@ -66,6 +64,6 @@ func Normalize(doc interface{}) { case *tax.Identity: normalizeTaxIdentity(obj) case *org.Identity: - normalizePAN(obj) + normalizeOrgIdentity(obj) } } diff --git a/regimes/in/item.go b/regimes/in/item.go deleted file mode 100644 index 3e74d03c..00000000 --- a/regimes/in/item.go +++ /dev/null @@ -1,49 +0,0 @@ -package in - -import ( - "errors" - "regexp" - - "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/org" - "github.com/invopop/validation" -) - -// Identity type used in India to classify products and services. -const ( - IdentityTypeHSN = cbc.Code("HSN") -) - -// HSNCodeRegexp defines the regular expression to validate HSN codes. -var HSNCodeRegexp = regexp.MustCompile(`^(?:\d{4}|\d{6}|\d{8})$`) - -func validateItem(it *org.Item) error { - - return validation.ValidateStruct(it, - validation.Field(&it.Identities, - validation.Each( - - validation.By(validItemIdentities), - ), - validation.Skip, - ), - ) -} - -func validItemIdentities(value interface{}) error { - id, ok := value.(*org.Identity) - if !ok { - return nil - } - - if id.Type == IdentityTypeHSN { - val := string(id.Code) - - if !HSNCodeRegexp.MatchString(val) { - return errors.New("must be a 4, 6, or 8-digit number") - } - - } - - return nil -} diff --git a/regimes/in/item_test.go b/regimes/in/item_test.go deleted file mode 100644 index cf383bd4..00000000 --- a/regimes/in/item_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package in_test - -import ( - "testing" - - "github.com/invopop/gobl/org" - "github.com/invopop/gobl/regimes/in" - "github.com/stretchr/testify/assert" -) - -func TestItemValidation(t *testing.T) { - tests := []struct { - name string - item *org.Item - err string - }{ - { - name: "valid HSN code", - item: &org.Item{ - Identities: []*org.Identity{ - { - Type: "HSN", - Code: "12345678", - }, - }, - }, - err: "", - }, - { - name: "valid HSN code with 4 digits", - item: &org.Item{ - Identities: []*org.Identity{ - { - Type: "HSN", - Code: "1234", - }, - }, - }, - err: "", - }, - { - name: "invalid HSN code format", - item: &org.Item{ - Identities: []*org.Identity{ - { - Type: "HSN", - Code: "12A456", - }, - }, - }, - err: "must be a 4, 6, or 8-digit number", - }, - { - name: "missing HSN identity", - item: &org.Item{ - Identities: []*org.Identity{}, - }, - err: "", // No error expected since it's not mandatory in some specific cases - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := in.Validate(tc.item) - if tc.err == "" { - assert.NoError(t, err) - } else { - if assert.Error(t, err) { - assert.Contains(t, err.Error(), tc.err) - } - } - }) - } -} diff --git a/regimes/in/org_identities.go b/regimes/in/org_identities.go new file mode 100644 index 00000000..79482a66 --- /dev/null +++ b/regimes/in/org_identities.go @@ -0,0 +1,61 @@ +package in + +import ( + "regexp" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +const ( + // IdentityTypePAN represents the Indian Permanent Account Number (PAN). It is a unique identifier assigned + // to individuals, companies, and other entities. + IdentityTypePAN cbc.Code = "PAN" + + // IdentityTypeHSN represents the Harmonized System of Nomenclature (HSN) code. It is used to classify products + // or services for taxation purposes. + // + // HSN codes for India can be found using the online service here: + // https://services.gst.gov.in/services/searchhsnsac + // + // The SAC (Service Accounting Code) is a similar classification system for services, which has been replaced + // by the HSN code. + IdentityTypeHSN cbc.Code = "HSN" +) + +var ( + identityRegexpPAN = regexp.MustCompile(`^[A-Z]{5}[0-9]{4}[A-Z]$`) + identityRegexpHSN = regexp.MustCompile(`^(?:\d{4}|\d{6}|\d{8})$`) +) + +func normalizeOrgIdentity(id *org.Identity) { + if id == nil { + return + } + switch id.Type { + case IdentityTypePAN: + id.Code = cbc.NormalizeAlphanumericalCode(id.Code) + case IdentityTypeHSN: + id.Code = cbc.NormalizeNumericalCode(id.Code) + } +} + +func validateOrgIdentity(id *org.Identity) error { + if id == nil { + return nil + } + return validation.ValidateStruct(id, + validation.Field(&id.Code, + validation.When( + id.Type == IdentityTypePAN, + validation.Match(identityRegexpPAN), + ), + validation.When( + id.Type == IdentityTypeHSN, + validation.Match(identityRegexpHSN).Error("must be a 4, 6, or 8 digit number"), + ), + validation.Skip, + ), + ) +} diff --git a/regimes/in/org_identities_test.go b/regimes/in/org_identities_test.go new file mode 100644 index 00000000..7b595d7e --- /dev/null +++ b/regimes/in/org_identities_test.go @@ -0,0 +1,120 @@ +package in_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/in" + "github.com/stretchr/testify/assert" +) + +func TestOrgIdentityNormalize(t *testing.T) { + tests := []struct { + name string + typ cbc.Code + input cbc.Code + expected cbc.Code + }{ + {name: "PAN already normalized", input: "ABCDE1234F", typ: in.IdentityTypePAN, expected: "ABCDE1234F"}, + {name: "PAN lowercase input", input: "abcde1234f", typ: in.IdentityTypePAN, expected: "ABCDE1234F"}, + {name: "PAN mixed case input", typ: in.IdentityTypePAN, input: "AbCdE1234f", expected: "ABCDE1234F"}, + {name: "PAN extra spaces", typ: in.IdentityTypePAN, input: " ABCDE1234F ", expected: "ABCDE1234F"}, + {name: "PAN special characters", typ: in.IdentityTypePAN, input: "AB-CDE1234F", expected: "ABCDE1234F"}, + + {name: "HSN already normalized", input: "12345678", typ: in.IdentityTypeHSN, expected: "12345678"}, + {name: "HSN symbols", input: "1234-5678", typ: in.IdentityTypeHSN, expected: "12345678"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := &org.Identity{Type: in.IdentityTypePAN, Code: tt.input} + in.Normalize(id) + assert.Equal(t, tt.expected, id.Code) + }) + } +} + +func TestOrgIdentityValidate(t *testing.T) { + tests := []struct { + name string + typ cbc.Code + code cbc.Code + err string + }{ + {name: "not HSN nor PAN", typ: "XYZ", code: "1234"}, + + {name: "valid PAN 1", typ: in.IdentityTypePAN, code: "BAJPC4350M"}, + {name: "valid PAN 2", typ: in.IdentityTypePAN, code: "DAJPC4150P"}, + {name: "valid PAN 3", typ: in.IdentityTypePAN, code: "XGZFE7225A"}, + {name: "valid PAN 4", typ: in.IdentityTypePAN, code: "CTUGE1616Y"}, + + {name: "valid HSN 1", typ: in.IdentityTypeHSN, code: "1234"}, + {name: "valid HSN 2", typ: in.IdentityTypeHSN, code: "123456"}, + {name: "valid HSN 3", typ: in.IdentityTypeHSN, code: "12345678"}, + + { + name: "PAN too short", + typ: in.IdentityTypePAN, + code: "ABC1234F", + err: "code: must be in a valid format.", + }, + { + name: "PAN contains spaces", + typ: in.IdentityTypePAN, + code: "ABCDE 1234F", + err: "code: must be in a valid format.", + }, + { + name: "PAN extra characters", + typ: in.IdentityTypePAN, + code: "ABCDE1234F12", + err: "code: must be in a valid format.", + }, + { + name: "HSN too short", + typ: in.IdentityTypeHSN, + code: "123", + err: "code: must be a 4, 6, or 8 digit number", + }, + { + name: "HSN mid", + typ: in.IdentityTypeHSN, + code: "12345", + err: "code: must be a 4, 6, or 8 digit number", + }, + { + name: "HSN mid 2", + typ: in.IdentityTypeHSN, + code: "1234567", + err: "code: must be a 4, 6, or 8 digit number", + }, + { + name: "HSN long", + typ: in.IdentityTypeHSN, + code: "123456789", + err: "code: must be a 4, 6, or 8 digit number", + }, + { + name: "HSN contains letters", + typ: in.IdentityTypeHSN, + code: "1234A6", + err: "code: must be a 4, 6, or 8 digit number", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := &org.Identity{Type: tt.typ, Code: tt.code} + err := in.Validate(id) + + if tt.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tt.err) + } + } + }) + } +} diff --git a/regimes/in/org_item.go b/regimes/in/org_item.go new file mode 100644 index 00000000..c104dcea --- /dev/null +++ b/regimes/in/org_item.go @@ -0,0 +1,15 @@ +package in + +import ( + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +func validateOrgItem(it *org.Item) error { + return validation.ValidateStruct(it, + validation.Field(&it.Identities, + org.RequireIdentityType(IdentityTypeHSN), + validation.Skip, + ), + ) +} diff --git a/regimes/in/org_item_test.go b/regimes/in/org_item_test.go new file mode 100644 index 00000000..77dc6caf --- /dev/null +++ b/regimes/in/org_item_test.go @@ -0,0 +1,39 @@ +package in_test + +import ( + "testing" + + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/in" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestOrgItemValidation(t *testing.T) { + tr := tax.RegimeDefFor("IN") + t.Run("valid", func(t *testing.T) { + i := &org.Item{ + Identities: []*org.Identity{ + { + Type: in.IdentityTypeHSN, + Code: "1234", + }, + }, + } + err := tr.ValidateObject(i) + assert.NoError(t, err) + }) + + t.Run("invalid", func(t *testing.T) { + i := &org.Item{ + Identities: []*org.Identity{ + { + Type: in.IdentityTypePAN, + Code: "1234", + }, + }, + } + err := tr.ValidateObject(i) + assert.ErrorContains(t, err, "identities: missing type HSN.") + }) +} diff --git a/tax/regime_def.go b/tax/regime_def.go index 94d68308..beb7374e 100644 --- a/tax/regime_def.go +++ b/tax/regime_def.go @@ -62,10 +62,6 @@ type RegimeDef struct { // Typically these are used to define local codes for suppliers, customers, products, or tax rates. Extensions []*cbc.KeyDefinition `json:"extensions,omitempty" jsonschema:"title=Extensions"` - // Tax Identity types specific for the regime and may be validated - // against. - TaxIdentityTypeKeys []*cbc.KeyDefinition `json:"tax_identity_type_keys,omitempty" jsonschema:"title=Tax Identity Type Keys"` - // Identity keys used in addition to regular tax identities and specific for the // regime that may be validated against. IdentityKeys []*cbc.KeyDefinition `json:"identity_keys,omitempty" jsonschema:"title=Identity Keys"` @@ -280,7 +276,6 @@ func (r *RegimeDef) ValidateWithContext(ctx context.Context) error { validation.Field(&r.Zone), validation.Field(&r.Currency), validation.Field(&r.Tags), - validation.Field(&r.TaxIdentityTypeKeys), validation.Field(&r.IdentityKeys), validation.Field(&r.Extensions), validation.Field(&r.PaymentMeansKeys), From eb70ac6acfb385ed2bbd112d17661b400e8b05a9 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 5 Dec 2024 16:45:57 +0000 Subject: [PATCH 11/11] Simplifying and fixing tests --- examples/in/invoice-in-in-simplified.yaml | 3 - examples/in/invoice-in-in-stdr.yaml | 3 - examples/in/out/invoice-in-in-simplified.json | 191 ++++++++------- examples/in/out/invoice-in-in-stdr.json | 229 +++++++++--------- regimes/in/bill.go | 2 +- regimes/in/in.go | 4 - regimes/in/scenarios.go | 45 ---- regimes/in/scenarios_test.go | 5 - regimes/in/tax_categories.go | 23 -- 9 files changed, 216 insertions(+), 289 deletions(-) diff --git a/examples/in/invoice-in-in-simplified.yaml b/examples/in/invoice-in-in-simplified.yaml index 16e878b8..087fec45 100644 --- a/examples/in/invoice-in-in-simplified.yaml +++ b/examples/in/invoice-in-in-simplified.yaml @@ -4,9 +4,6 @@ currency: "INR" issue_date: "2022-02-01" series: "SAMPLE" code: "001" -tax: - ext: - in-supply-place: "Maharashtra" supplier: tax_id: diff --git a/examples/in/invoice-in-in-stdr.yaml b/examples/in/invoice-in-in-stdr.yaml index b8e654ff..0d834ffc 100644 --- a/examples/in/invoice-in-in-stdr.yaml +++ b/examples/in/invoice-in-in-stdr.yaml @@ -4,9 +4,6 @@ currency: "INR" issue_date: "2022-02-01" series: "SAMPLE" code: "001" -tax: - ext: - in-supply-place: "Maharashtra" supplier: tax_id: diff --git a/examples/in/out/invoice-in-in-simplified.json b/examples/in/out/invoice-in-in-simplified.json index 700a43e2..65bfd107 100644 --- a/examples/in/out/invoice-in-in-simplified.json +++ b/examples/in/out/invoice-in-in-simplified.json @@ -1,107 +1,112 @@ { - "$schema": "https://gobl.org/draft-0/bill/invoice", - "$regime": "IN", - "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", - "type": "standard", - "series": "SAMPLE", - "code": "001", - "issue_date": "2022-02-01", - "currency": "INR", - "tax": { - "ext": { - "in-supply-place": "Maharashtra" + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "07b1373e399a192b6860e8870e4312547670a5e6be5ad2a186f991377e34ebaa" } }, - "supplier": { - "name": "Provide One LLC", - "tax_id": { - "country": "IN", - "code": "27AAPFU0939F1ZV" - }, - "addresses": [ - { - "num": "16", - "street": "Baner Road", - "locality": "Baner", - "region": "Maharashtra", - "code": "411045", - "country": "IN" - } - ], - "emails": [ - { - "addr": "billing@example.in" - } - ] - }, - "lines": [ - { - "i": 1, - "quantity": "20", - "item": { - "name": "Development services", - "identities": [ - { - "type": "HSN", - "code": "123456" - } - ], - "price": "90.00", - "unit": "h" + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "IN", + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "type": "standard", + "series": "SAMPLE", + "code": "001", + "issue_date": "2022-02-01", + "currency": "INR", + "supplier": { + "name": "Provide One LLC", + "tax_id": { + "country": "IN", + "code": "27AAPFU0939F1ZV" }, - "sum": "1800.00", - "discounts": [ + "addresses": [ { - "reason": "Special discount", - "percent": "5%", - "amount": "90.00" + "num": "16", + "street": "Baner Road", + "locality": "Baner", + "region": "Maharashtra", + "code": "411045", + "country": "IN" } ], - "taxes": [ + "emails": [ { - "cat": "CGST", - "percent": "9%" - }, - { - "cat": "SGST", - "percent": "9%" + "addr": "billing@example.in" } - ], - "total": "1710.00" - } - ], - "totals": { - "sum": "1710.00", - "total": "1710.00", - "taxes": { - "categories": [ - { - "code": "CGST", - "rates": [ + ] + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "identities": [ { - "base": "1710.00", - "percent": "9%", - "amount": "153.90" + "type": "HSN", + "code": "123456" } ], - "amount": "153.90" + "price": "90.00", + "unit": "h" }, - { - "code": "SGST", - "rates": [ - { - "base": "1710.00", - "percent": "9%", - "amount": "153.90" - } - ], - "amount": "153.90" - } - ], - "sum": "307.80" - }, - "tax": "307.80", - "total_with_tax": "2017.80", - "payable": "2017.80" + "sum": "1800.00", + "discounts": [ + { + "reason": "Special discount", + "percent": "5%", + "amount": "90.00" + } + ], + "taxes": [ + { + "cat": "CGST", + "percent": "9%" + }, + { + "cat": "SGST", + "percent": "9%" + } + ], + "total": "1710.00" + } + ], + "totals": { + "sum": "1710.00", + "total": "1710.00", + "taxes": { + "categories": [ + { + "code": "CGST", + "rates": [ + { + "base": "1710.00", + "percent": "9%", + "amount": "153.90" + } + ], + "amount": "153.90" + }, + { + "code": "SGST", + "rates": [ + { + "base": "1710.00", + "percent": "9%", + "amount": "153.90" + } + ], + "amount": "153.90" + } + ], + "sum": "307.80" + }, + "tax": "307.80", + "total_with_tax": "2017.80", + "payable": "2017.80" + } } -} +} \ No newline at end of file diff --git a/examples/in/out/invoice-in-in-stdr.json b/examples/in/out/invoice-in-in-stdr.json index 50e04383..840b0200 100644 --- a/examples/in/out/invoice-in-in-stdr.json +++ b/examples/in/out/invoice-in-in-stdr.json @@ -1,129 +1,134 @@ { - "$schema": "https://gobl.org/draft-0/bill/invoice", - "$regime": "IN", - "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", - "type": "standard", - "series": "SAMPLE", - "code": "001", - "issue_date": "2022-02-01", - "currency": "INR", - "tax": { - "ext": { - "in-supply-place": "Maharashtra" + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "9ae32594671727c34c4ad306ca1b9b6b5d8d1990c14ba0dffe6ef074d89657d5" } }, - "supplier": { - "name": "Provide One LLC", - "tax_id": { - "country": "IN", - "code": "27AAPFU0939F1ZV" - }, - "addresses": [ - { - "num": "101", - "street": "Dr. Annie Besant Road", - "locality": "Worli", - "region": "Maharashtra", - "code": "400018", - "country": "IN" - } - ], - "emails": [ - { - "addr": "billing@example.in" - } - ] - }, - "customer": { - "name": "Sample Consumer", - "tax_id": { - "country": "IN", - "code": "27AAPFU0939F1ZV" - }, - "addresses": [ - { - "num": "202", - "street": "MG Road", - "locality": "Bengaluru", - "region": "Karnataka", - "code": "560001", - "country": "IN" - } - ], - "emails": [ - { - "addr": "email@sample.in" - } - ] - }, - "lines": [ - { - "i": 1, - "quantity": "20", - "item": { - "name": "Development services", - "identities": [ - { - "type": "HSN", - "code": "123456" - } - ], - "price": "90.00", - "unit": "h" + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "IN", + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "type": "standard", + "series": "SAMPLE", + "code": "001", + "issue_date": "2022-02-01", + "currency": "INR", + "supplier": { + "name": "Provide One LLC", + "tax_id": { + "country": "IN", + "code": "27AAPFU0939F1ZV" }, - "sum": "1800.00", - "discounts": [ + "addresses": [ { - "reason": "Special discount", - "percent": "5%", - "amount": "90.00" + "num": "101", + "street": "Dr. Annie Besant Road", + "locality": "Worli", + "region": "Maharashtra", + "code": "400018", + "country": "IN" } ], - "taxes": [ + "emails": [ { - "cat": "CGST", - "percent": "9%" - }, + "addr": "billing@example.in" + } + ] + }, + "customer": { + "name": "Sample Consumer", + "tax_id": { + "country": "IN", + "code": "27AAPFU0939F1ZV" + }, + "addresses": [ { - "cat": "SGST", - "percent": "9%" + "num": "202", + "street": "MG Road", + "locality": "Bengaluru", + "region": "Karnataka", + "code": "560001", + "country": "IN" } ], - "total": "1710.00" - } - ], - "totals": { - "sum": "1710.00", - "total": "1710.00", - "taxes": { - "categories": [ + "emails": [ { - "code": "CGST", - "rates": [ + "addr": "email@sample.in" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "identities": [ { - "base": "1710.00", - "percent": "9%", - "amount": "153.90" + "type": "HSN", + "code": "123456" } ], - "amount": "153.90" + "price": "90.00", + "unit": "h" }, - { - "code": "SGST", - "rates": [ - { - "base": "1710.00", - "percent": "9%", - "amount": "153.90" - } - ], - "amount": "153.90" - } - ], - "sum": "307.80" - }, - "tax": "307.80", - "total_with_tax": "2017.80", - "payable": "2017.80" + "sum": "1800.00", + "discounts": [ + { + "reason": "Special discount", + "percent": "5%", + "amount": "90.00" + } + ], + "taxes": [ + { + "cat": "CGST", + "percent": "9%" + }, + { + "cat": "SGST", + "percent": "9%" + } + ], + "total": "1710.00" + } + ], + "totals": { + "sum": "1710.00", + "total": "1710.00", + "taxes": { + "categories": [ + { + "code": "CGST", + "rates": [ + { + "base": "1710.00", + "percent": "9%", + "amount": "153.90" + } + ], + "amount": "153.90" + }, + { + "code": "SGST", + "rates": [ + { + "base": "1710.00", + "percent": "9%", + "amount": "153.90" + } + ], + "amount": "153.90" + } + ], + "sum": "307.80" + }, + "tax": "307.80", + "total_with_tax": "2017.80", + "payable": "2017.80" + } } -} +} \ No newline at end of file diff --git a/regimes/in/bill.go b/regimes/in/bill.go index bc45ec69..f7434f71 100644 --- a/regimes/in/bill.go +++ b/regimes/in/bill.go @@ -3,7 +3,7 @@ package in import "github.com/invopop/gobl/cbc" const ( - // ChargeKeyCompoensationCess is used for addtional charges added to an invoice for the special + // ChargeKeyCompensationCess is used for addtional charges added to an invoice for the special // compensation "cess" (cess means tax or levy) which may be appled as a percentage or specific // amount based on valumes or other criteria. // diff --git a/regimes/in/in.go b/regimes/in/in.go index be553830..d58b92ab 100644 --- a/regimes/in/in.go +++ b/regimes/in/in.go @@ -7,7 +7,6 @@ import ( "github.com/invopop/gobl/currency" "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/org" - "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -24,9 +23,6 @@ func New() *tax.RegimeDef { i18n.EN: "India", }, TimeZone: "Asia/Kolkata", - Tags: []*tax.TagSet{ - common.InvoiceTags().Merge(invoiceTags), - }, Scenarios: []*tax.ScenarioSet{ invoiceScenarios, }, diff --git a/regimes/in/scenarios.go b/regimes/in/scenarios.go index 19d38228..e5a5e29d 100644 --- a/regimes/in/scenarios.go +++ b/regimes/in/scenarios.go @@ -4,36 +4,9 @@ package in import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/tax" ) -// Tax tags that can be applied in India. -const ( - TagBillOfSupply cbc.Key = "bill-of-supply" - TagInvoiceCumBillOfSupply cbc.Key = "invoice-cum-bill-of-supply" -) - -var invoiceTags = &tax.TagSet{ - Schema: bill.ShortSchemaInvoice, - List: []*cbc.KeyDefinition{ - { - Key: TagBillOfSupply, - Name: i18n.String{ - i18n.EN: "Bill of Supply", - i18n.HI: "आपूर्ति का बिल", - }, - }, - { - Key: TagInvoiceCumBillOfSupply, - Name: i18n.String{ - i18n.EN: "Invoice-cum-bill of supply", - i18n.HI: "चालान-सह-आपूर्ति का बिल", - }, - }, - }, -} - var invoiceScenarios = &tax.ScenarioSet{ Schema: bill.ShortSchemaInvoice, List: []*tax.Scenario{ @@ -55,23 +28,5 @@ var invoiceScenarios = &tax.ScenarioSet{ Text: "Simplified Tax Invoice", }, }, - // Bill of Supply - { - Tags: []cbc.Key{TagBillOfSupply}, - Note: &cbc.Note{ - Key: cbc.NoteKeyLegal, - Src: TagBillOfSupply, - Text: "Bill Of Supply", - }, - }, - // Invoice-cum-bill of Supply - { - Tags: []cbc.Key{TagInvoiceCumBillOfSupply}, - Note: &cbc.Note{ - Key: cbc.NoteKeyLegal, - Src: TagInvoiceCumBillOfSupply, - Text: "Invoice-cum-bill Of Supply", - }, - }, }, } diff --git a/regimes/in/scenarios_test.go b/regimes/in/scenarios_test.go index cce7cf7a..a681841b 100644 --- a/regimes/in/scenarios_test.go +++ b/regimes/in/scenarios_test.go @@ -24,11 +24,6 @@ func testInvoiceStandard(t *testing.T) *bill.Invoice { Code: "27AAPFU0939F1ZV", }, }, - Tax: &bill.Tax{ - Ext: tax.Extensions{ - "in-supply-place": "Ciudad prueba", - }, - }, Customer: &org.Party{ Name: "Test Customer", TaxID: &tax.Identity{ diff --git a/regimes/in/tax_categories.go b/regimes/in/tax_categories.go index eda7cbba..4b96b084 100644 --- a/regimes/in/tax_categories.go +++ b/regimes/in/tax_categories.go @@ -13,7 +13,6 @@ const ( TaxCategorySGST cbc.Code = "SGST" TaxCategoryIGST cbc.Code = "IGST" TaxCategoryUTGST cbc.Code = "UTGST" - TaxCategoryCess cbc.Code = "CESS" ) var taxCategories = []*tax.CategoryDef{ @@ -104,26 +103,4 @@ var taxCategories = []*tax.CategoryDef{ }, }, }, - - // Cess (Additional Tax for Luxury or Specific Goods) - { - Code: TaxCategoryCess, - Name: i18n.String{ - i18n.EN: "Cess", - i18n.HI: "उपकर", - }, - Title: i18n.String{ - i18n.EN: "GST Compensation Cess on Luxury or Specific Goods", - i18n.HI: "विलासिता या विशेष वस्तुओं पर जीएसटी मुआवजा उपकर", - }, - Sources: []*tax.Source{ - { - Title: i18n.String{ - i18n.EN: "GST Compensation Cess Regulations", - i18n.HI: "जीएसटी मुआवजा उपकर नियमावली", - }, - URL: "https://gstcouncil.gov.in", - }, - }, - }, }