diff --git a/CHANGELOG.md b/CHANGELOG.md index 24194897..2f87c4e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added -- `org`: `Address` includes `LineOne()`, `LineTwo()`, `CompleteNumber()` methods to help with conversion to other formats with some regional formatting. +- `ae`: added UAE regime - `br-nfse-v1`: new extensions, validations & identities for the typical service note and supplier. +## [v0.205.1] - 2024-11-19 + +### Added + +- `org`: `Address` includes `LineOne()`, `LineTwo()`, `CompleteNumber()` methods to help with conversion to other formats with some regional formatting. + ### Changes - `bill`: `Invoice` can now have empty lines if discounts or charges present. ### Fixes +- `ch`: Deleted Supplier validation (not needed for under 2300 CHF/year) - `bill`: `Invoice` `GetExtensions` method now works correctly if missing totals [Issue #424](https://github.com/invopop/gobl/issues/424). -## [v0.205.0] +## [v0.205.0] - 2024-11-12 ### Added @@ -37,7 +44,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `cal`: Fixing json schema issue with date times. -## [v0.204.1] +## [v0.204.1] - 2024-11-04 ### Added @@ -47,7 +54,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `tax`: identity code handling will skip default validation for specific countries that use special characters. -## [v0.204.0] +## [v0.204.0] - 2024-10-31 ### Added @@ -86,7 +93,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `mx`: Tax ID validation now correctly supports `&` and `Ñ` symbols in codes. -## [v0.203.0] +## [v0.203.0] - 2024-20-21 ### Added @@ -100,7 +107,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `tax.Identity`: support Calculate method to normalize IDs. - `tax.Regime`: properly set regime when alternative codes is given. -## [v0.202.0] +## [v0.202.0] - 2024-10-14 ### Changed @@ -116,7 +123,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `mx`: fixed panic when normalizing an invoice with `tax` but no `ext` inside. -## [v0.201.0] +## [v0.201.0] - 2024-10-07 ### Fixed @@ -129,13 +136,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `pt`: reduced rate category for PT-MA was updated to reflect latest value of 4% - `co-dian-v2`: moved from `co` tax regime into own addon. -## [v0.200.1] +## [v0.200.1] - 2024-09-30 ### Fixed - `pt`: moving invoice tags from saft addon to regime, ensure defaults present. -## [v0.200.0] +## [v0.200.0] - 2024-09-26 Another ~~significant~~ epic release. Introducing "add-ons" which move the normalization and validation rules from Tax Regimes to specific packages that need to be enabled inside a document to be used. @@ -177,13 +184,13 @@ Finally, the `draft` flag has been removed from the header, and much more emphas - `org`: `DocumentRef` consolidates references to previous documents in a single place. - `bill`: invoice type option `other` for usage when regular scenarios do not apply. -## [v0.115.1] +## [v0.115.1] - 2024-09-10 ### Fixes - `tax`: totals calculator was ignoring tax combos with rate and percent, when they should be classed as exempt. -## [v0.115.0] +## [v0.115.0] - 2024-09-10 This one is big... @@ -236,20 +243,20 @@ Invoices in GOBL can now also finally produced for any country in the world, eve - `tax.Scenario`: potential issue around matching notes. - `tax.Set`: improved validation embedded error handling. -## [v0.114.0] +## [v0.114.0] - 2024-08-26 ### Changed - `org.Name`: either given **or** surname are required, as opposed to both at the same time. -## [v0.113.0] +## [v0.113.0] - 2024-08-01 ### Added - `head`: validation rule to check for the presence of stamps - GR: support for credit notes -## [v0.112.0] +## [v0.112.0] - 2024-07-29 Significant set of small changes related to renaming of the `l10n.CountryCode` type. The main reason for this is an attempt to reduce confusion between regular ISO country selection, and the specific country codes used for tax purposes. Normally they coincide, but exception cases like for Greece, whose ISO code is `GR` but use `EL` for tax purposes, or `XI` for companies in Northern Ireland, mean that there needs to be a clear selection. @@ -265,6 +272,8 @@ Significant set of small changes related to renaming of the `l10n.CountryCode` t - GR: support for simplified invoices - `l10n`: ISO and Tax lists of country definitions available, e.g. `l10n.Countries().ISO()` - `tax`: support for alternative country codes +- `tax`: Scenarios now handle extension key and value for filtering. +- PT: exemption text handling moved to scenarios. ### Upgraded @@ -274,13 +283,6 @@ Significant set of small changes related to renaming of the `l10n.CountryCode` t - GR: fixed certain tax combos not getting calculated by the regime -## [v0.112.0] - 2024-07-26 - -### Added - -- `tax`: Scenarios now handle extension key and value for filtering. -- PT: exemption text handling moved to scenarios. - ## [v0.111.1] - 2024-07-25 ### Added diff --git a/data/regimes/ae.json b/data/regimes/ae.json new file mode 100644 index 00000000..c8af2bc0 --- /dev/null +++ b/data/regimes/ae.json @@ -0,0 +1,172 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime-def", + "name": { + "ar": "الإمارات العربية المتحدة", + "en": "United Arab Emirates" + }, + "time_zone": "Asia/Dubai", + "country": "AE", + "currency": "AED", + "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" + } + } + ] + } + ], + "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" + } + } + ] + } + ], + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note" + ] + } + ], + "categories": [ + { + "code": "VAT", + "name": { + "ar": "ضريبة القيمة المضافة", + "en": "VAT" + }, + "title": { + "ar": "ضريبة القيمة المضافة", + "en": "Value Added Tax" + }, + "rates": [ + { + "key": "zero", + "name": { + "ar": "معدل صفر", + "en": "Zero Rate" + }, + "desc": { + "ar": "نسبة ضريبة قيمة مضافة 0٪ تطبق على الصادرات المحددة والمناطق المعينة والخدمات الأساسية.", + "en": "A VAT rate of 0% applicable to specific exports, designated areas, and essential services." + }, + "values": [ + { + "percent": "0.0%" + } + ] + }, + { + "key": "standard", + "name": { + "ar": "معدل قياسي", + "en": "Standard Rate" + }, + "desc": { + "ar": "ينطبق على معظم السلع والخدمات ما لم ينص على خلاف ذلك.", + "en": "Applies to most goods and services unless specified otherwise." + }, + "values": [ + { + "since": "2018-01-01", + "percent": "5%" + } + ] + }, + { + "key": "exempt", + "name": { + "ar": "معفى", + "en": "Exempt" + }, + "desc": { + "ar": "بعض السلع والخدمات، مثل الخدمات المالية والعقارات السكنية، معفاة من ضريبة القيمة المضافة.", + "en": "Certain goods and services, such as financial services and residential real estate, are exempt from VAT." + }, + "exempt": true + } + ], + "sources": [ + { + "title": { + "ar": "الهيئة الاتحادية للضرائب", + "en": "Federal Tax Authority - UAE VAT Regulations" + }, + "url": "https://www.tax.gov.ae" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/ae/invoice-ae-ae-stnr.yaml b/examples/ae/invoice-ae-ae-stnr.yaml new file mode 100644 index 00000000..a0de1e97 --- /dev/null +++ b/examples/ae/invoice-ae-ae-stnr.yaml @@ -0,0 +1,47 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +currency: "AED" +issue_date: "2022-02-01" +series: "SAMPLE" +code: "001" + +supplier: + tax_id: + country: "AE" + code: "123456789012346" + name: "Provide One LLC" + emails: + - addr: "billing@example.ae" + addresses: + - num: "16" + street: "Sheikh Zayed Road" + locality: "Dubai" + code: "00000" + country: "AE" + +customer: + tax_id: + country: "AE" + code: "123456789012345" + name: "Sample Consumer" + emails: + - addr: "email@sample.ae" + addresses: + - num: "25" + street: "Al Maryah Island" + locality: "Abu Dhabi" + code: "00000" + country: "AE" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "5%" + reason: "Special discount" + taxes: + - cat: VAT + rate: standard diff --git a/examples/ae/invoice-ae-simplified.yaml b/examples/ae/invoice-ae-simplified.yaml new file mode 100644 index 00000000..a3f2a6a8 --- /dev/null +++ b/examples/ae/invoice-ae-simplified.yaml @@ -0,0 +1,35 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +currency: "AED" +issue_date: "2022-02-01" +series: "SAMPLE" +code: "001" +tax: + tags: + - "simplified" +supplier: + tax_id: + country: "AE" + code: "123456789012346" + name: "Provide One LLC" + emails: + - addr: "billing@example.ae" + addresses: + - num: "16" + street: "Sheikh Zayed Road" + locality: "Dubai" + code: "00000" + country: "AE" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "10%" + reason: "Special discount" + taxes: + - cat: VAT + rate: "standard" \ No newline at end of file diff --git a/examples/ae/out/invoice-ae-ae-stnr.json b/examples/ae/out/invoice-ae-ae-stnr.json new file mode 100644 index 00000000..7412c118 --- /dev/null +++ b/examples/ae/out/invoice-ae-ae-stnr.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "b296aae4afcad5a2047e317d515510607fb61a7bbfc5c1b8db586a69610820bc" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "AE", + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "type": "standard", + "series": "SAMPLE", + "code": "001", + "issue_date": "2022-02-01", + "currency": "AED", + "supplier": { + "name": "Provide One LLC", + "tax_id": { + "country": "AE", + "code": "123456789012346" + }, + "addresses": [ + { + "num": "16", + "street": "Sheikh Zayed Road", + "locality": "Dubai", + "code": "00000", + "country": "AE" + } + ], + "emails": [ + { + "addr": "billing@example.ae" + } + ] + }, + "customer": { + "name": "Sample Consumer", + "tax_id": { + "country": "AE", + "code": "123456789012345" + }, + "addresses": [ + { + "num": "25", + "street": "Al Maryah Island", + "locality": "Abu Dhabi", + "code": "00000", + "country": "AE" + } + ], + "emails": [ + { + "addr": "email@sample.ae" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "price": "90.00", + "unit": "h" + }, + "sum": "1800.00", + "discounts": [ + { + "reason": "Special discount", + "percent": "5%", + "amount": "90.00" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "5%" + } + ], + "total": "1710.00" + } + ], + "totals": { + "sum": "1710.00", + "total": "1710.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "1710.00", + "percent": "5%", + "amount": "85.50" + } + ], + "amount": "85.50" + } + ], + "sum": "85.50" + }, + "tax": "85.50", + "total_with_tax": "1795.50", + "payable": "1795.50" + } + } +} \ No newline at end of file diff --git a/examples/ae/out/invoice-ae-simplified.json b/examples/ae/out/invoice-ae-simplified.json new file mode 100644 index 00000000..170474c8 --- /dev/null +++ b/examples/ae/out/invoice-ae-simplified.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "a71f905663e90f1d1d015acd89adaa57adc65ddcf6874d1b0a5efe8241e79968" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "AE", + "$tags": [ + "simplified" + ], + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "type": "standard", + "series": "SAMPLE", + "code": "001", + "issue_date": "2022-02-01", + "currency": "AED", + "tax": {}, + "supplier": { + "name": "Provide One LLC", + "tax_id": { + "country": "AE", + "code": "123456789012346" + }, + "addresses": [ + { + "num": "16", + "street": "Sheikh Zayed Road", + "locality": "Dubai", + "code": "00000", + "country": "AE" + } + ], + "emails": [ + { + "addr": "billing@example.ae" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "price": "90.00", + "unit": "h" + }, + "sum": "1800.00", + "discounts": [ + { + "reason": "Special discount", + "percent": "10%", + "amount": "180.00" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "5%" + } + ], + "total": "1620.00" + } + ], + "totals": { + "sum": "1620.00", + "total": "1620.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "1620.00", + "percent": "5%", + "amount": "81.00" + } + ], + "amount": "81.00" + } + ], + "sum": "81.00" + }, + "tax": "81.00", + "total_with_tax": "1701.00", + "payable": "1701.00" + }, + "notes": [ + { + "key": "legal", + "src": "simplified", + "text": "Simplified Tax Invoice" + } + ] + } +} \ No newline at end of file diff --git a/regimes/ae/README.md b/regimes/ae/README.md new file mode 100644 index 00000000..401f40c2 --- /dev/null +++ b/regimes/ae/README.md @@ -0,0 +1,41 @@ +# United Arab Emirates (AE) Tax Regime + +This document provides an overview of the tax regime in the United Arab Emirates. + +## Value-Added Tax (VAT) + +The UAE VAT system categorizes goods and services into three main VAT rates: + +- **Standard Rate (5%)**: Applies to most goods and services in the UAE. +- **Zero Rate (0%)**: Applies to certain essential goods, exports, and specific services. +- **Exempt**: Certain types of goods and services are exempt from VAT, including some financial services or residential real estate. + +Businesses required to charge VAT must register with the UAE Federal Tax Authority to obtain a Tax Registration Number (TRN). Registration can be completed online via the [VAT registration portal](https://tax.gov.ae/en/services/vat.registration.aspx). + +## VAT Registration Requirements + +Businesses in the UAE must evaluate their eligibility for VAT registration based on their revenue and expenses. The registration requirements are as follows: + +- **Mandatory Registration**: Businesses are required to register for VAT if the total value of taxable supplies and imports exceeds **AED 375,000** in a 12-month period. +- **Voluntary Registration**: Businesses may choose to register voluntarily if the total value of taxable supplies and imports, or taxable expenses, exceeds **AED 187,500** within a 12-month period. + +**Note:** Businesses below the voluntary registration threshold are not permitted to register for VAT and, therefore, will issue invoices without a TRN. + +For more information, visit the [Federal Tax Authority website](https://tax.gov.ae/en/taxes/Vat/vat.topics/registration.for.vat.aspx) + +### TRN Validation + +Currently, no checksum method is available for validating the TRN. Therefore, verification must be performed directly through the official UAE government website. + +### Alcohol Sales Tax + +In Dubai, a **30% sales tax** on alcohol previously existed but has since been abolished. Some sources suggest that a similar tax persists in Abu Dhabi, but without official confirmation, this has not been included in the UAE tax regime. + +## VAT Invoicing Requirements + +There are two types of VAT invoices: the standard and the simplified invoice. + +**Simplified VAT Invoice**: Allowed in the following cases: + +- When the recipient of goods or services is **not VAT registered**. +- When the recipient of goods or services **is VAT registered**, and the transaction value does not exceed **AED 10,000**. diff --git a/regimes/ae/ae.go b/regimes/ae/ae.go new file mode 100644 index 00000000..23fe55f5 --- /dev/null +++ b/regimes/ae/ae.go @@ -0,0 +1,64 @@ +// Package ae provides the tax region definition for United Arab Emirates. +package ae + +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/regimes/common" + "github.com/invopop/gobl/tax" +) + +func init() { + tax.RegisterRegimeDef(New()) +} + +// New provides the tax region definition for AE. +func New() *tax.RegimeDef { + return &tax.RegimeDef{ + Country: "AE", + Currency: currency.AED, + Name: i18n.String{ + i18n.EN: "United Arab Emirates", + i18n.AR: "الإمارات العربية المتحدة", + }, + TimeZone: "Asia/Dubai", + Tags: []*tax.TagSet{ + common.InvoiceTags(), + }, + Scenarios: []*tax.ScenarioSet{ + invoiceScenarios, + }, + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + }, + }, + }, + Validator: Validate, + Normalizer: Normalize, + Categories: taxCategories, + } +} + +// Validate function assesses the document type to determine if validation is required. +// Note that, under the AE tax regime, validation of the supplier's tax ID is not necessary if it does not meet the specified threshold (refer to the README section for more details). +func Validate(doc interface{}) error { + switch obj := doc.(type) { + case *tax.Identity: + return validateTaxIdentity(obj) + } + return nil +} + +// Normalize attempts to clean up the object passed to it. +func Normalize(doc any) { + switch obj := doc.(type) { + case *tax.Identity: + tax.NormalizeIdentity(obj) + + } +} diff --git a/regimes/ae/scenarios.go b/regimes/ae/scenarios.go new file mode 100644 index 00000000..071fb240 --- /dev/null +++ b/regimes/ae/scenarios.go @@ -0,0 +1,32 @@ +// Package ae provides tax scenarios specific to UAE VAT regulations. +package ae + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" +) + +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", + }, + }, + }, +} diff --git a/regimes/ae/scenarios_test.go b/regimes/ae/scenarios_test.go new file mode 100644 index 00000000..1d02870b --- /dev/null +++ b/regimes/ae/scenarios_test.go @@ -0,0 +1,101 @@ +package ae_test + +import ( + "testing" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cal" + "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 testInvoiceReverseCharge(t *testing.T) *bill.Invoice { + t.Helper() + return &bill.Invoice{ + Code: "123TEST", + Currency: "AED", + Supplier: &org.Party{ + Name: "Test Supplier", + TaxID: &tax.Identity{ + Country: "AE", + Code: "123456789012345", + }, + }, + Customer: &org.Party{ + Name: "Test Customer", + TaxID: &tax.Identity{ + Country: "AE", + Code: "123456789012346", + }, + }, + IssueDate: cal.MakeDate(2023, 1, 15), + Tags: tax.WithTags(tax.TagReverseCharge), + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(5, 0), + Item: &org.Item{ + Name: "Service Item", + Price: num.MakeAmount(5000, 2), + }, + Taxes: tax.Set{ + { + Category: "VAT", + Rate: "standard", + }, + }, + }, + }, + } +} + +func testInvoiceSimplified(t *testing.T) *bill.Invoice { + t.Helper() + return &bill.Invoice{ + Code: "123TEST", + Currency: "AED", + Supplier: &org.Party{ + Name: "Test Supplier", + TaxID: &tax.Identity{ + Country: "AE", + Code: "123456789012345", + }, + }, + IssueDate: cal.MakeDate(2023, 1, 15), + Tags: tax.WithTags(tax.TagSimplified), + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(3, 0), + Item: &org.Item{ + Name: "Product Item", + Price: num.MakeAmount(2000, 2), + }, + Taxes: tax.Set{ + { + Category: "VAT", + Rate: "standard", + }, + }, + }, + }, + } +} + +func TestInvoiceScenarios(t *testing.T) { + i := testInvoiceReverseCharge(t) + require.NoError(t, i.Calculate()) + require.NoError(t, i.Validate()) + assert.Len(t, i.Notes, 1) + assert.Equal(t, i.Notes[0].Src, tax.TagReverseCharge) + assert.Equal(t, i.Notes[0].Text, "Reverse Charge") + + i = testInvoiceSimplified(t) + require.NoError(t, i.Calculate()) + require.NoError(t, i.Validate()) + 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/ae/tax_categories.go b/regimes/ae/tax_categories.go new file mode 100644 index 00000000..69154e49 --- /dev/null +++ b/regimes/ae/tax_categories.go @@ -0,0 +1,80 @@ +// Package ae defines VAT tax categories specific to the United Arab Emirates. +package ae + +import ( + "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/tax" +) + +var taxCategories = []*tax.CategoryDef{ + { + Code: tax.CategoryVAT, + Name: i18n.String{ + i18n.EN: "VAT", + i18n.AR: "ضريبة القيمة المضافة", + }, + Title: i18n.String{ + i18n.EN: "Value Added Tax", + i18n.AR: "ضريبة القيمة المضافة", + }, + Sources: []*tax.Source{ + { + Title: i18n.String{ + i18n.EN: "Federal Tax Authority - UAE VAT Regulations", + i18n.AR: "الهيئة الاتحادية للضرائب", + }, + URL: "https://www.tax.gov.ae", + }, + }, + Retained: false, + Rates: []*tax.RateDef{ + { + Key: tax.RateZero, + Name: i18n.String{ + i18n.EN: "Zero Rate", + i18n.AR: "معدل صفر", + }, + Description: i18n.String{ + i18n.EN: "A VAT rate of 0% applicable to specific exports, designated areas, and essential services.", + i18n.AR: "نسبة ضريبة قيمة مضافة 0٪ تطبق على الصادرات المحددة والمناطق المعينة والخدمات الأساسية.", + }, + Values: []*tax.RateValueDef{ + { + Percent: num.MakePercentage(0, 3), + }, + }, + }, + { + Key: tax.RateStandard, + Name: i18n.String{ + i18n.EN: "Standard Rate", + i18n.AR: "معدل قياسي", + }, + Description: i18n.String{ + i18n.EN: "Applies to most goods and services unless specified otherwise.", + i18n.AR: "ينطبق على معظم السلع والخدمات ما لم ينص على خلاف ذلك.", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2018, 1, 1), + Percent: num.MakePercentage(5, 2), + }, + }, + }, + { + Key: tax.RateExempt, + Name: i18n.String{ + i18n.EN: "Exempt", + i18n.AR: "معفى", + }, + Exempt: true, + Description: i18n.String{ + i18n.EN: "Certain goods and services, such as financial services and residential real estate, are exempt from VAT.", + i18n.AR: "بعض السلع والخدمات، مثل الخدمات المالية والعقارات السكنية، معفاة من ضريبة القيمة المضافة.", + }, + }, + }, + }, +} diff --git a/regimes/ae/tax_identity.go b/regimes/ae/tax_identity.go new file mode 100644 index 00000000..52580e58 --- /dev/null +++ b/regimes/ae/tax_identity.go @@ -0,0 +1,39 @@ +// Package ae provides the tax identity validation specific to the United Arab Emirates. +package ae + +import ( + "errors" + "regexp" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +var ( + // TRN in UAE is a 15-digit number + trnRegex = regexp.MustCompile(`^\d{15}$`) +) + +// validateTaxIdentity checks to ensure the UAE TRN format is correct. +func validateTaxIdentity(tID *tax.Identity) error { + return validation.ValidateStruct(tID, + validation.Field(&tID.Code, validation.By(validateTRNCode)), + ) +} + +// validateTRNCode checks that the TRN is a valid 15-digit format. +func validateTRNCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + // Check if TRN matches the 15-digit pattern + if !trnRegex.MatchString(val) { + return errors.New("must be a 15-digit number") + } + + return nil +} diff --git a/regimes/ae/tax_identity_test.go b/regimes/ae/tax_identity_test.go new file mode 100644 index 00000000..fc305160 --- /dev/null +++ b/regimes/ae/tax_identity_test.go @@ -0,0 +1,43 @@ +// Package ae_test provides tests for the UAE TRN (Tax Registration Number) validation. +package ae_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/regimes/ae" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestValidateTaxIdentity(t *testing.T) { + tests := []struct { + name string + code cbc.Code + err string + }{ + {name: "good 1", code: "123456789012345"}, + {name: "good 2", code: "187654321098765"}, + {name: "good 3", code: "100111222333444"}, + + // Invalid formats + {name: "too short", code: "12345678901234", err: "must be a 15-digit number"}, + {name: "too long", code: "1234567890123456", err: "must be a 15-digit number"}, + {name: "non-numeric", code: "12345678ABCD345", err: "must be a 15-digit number"}, + {name: "not normalized", code: "1234-5678-9012-345", err: "must be a 15-digit number"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "AE", Code: tt.code} + err := ae.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/ch/ch.go b/regimes/ch/ch.go index 0b89dd05..666da54c 100644 --- a/regimes/ch/ch.go +++ b/regimes/ch/ch.go @@ -46,8 +46,6 @@ func New() *tax.RegimeDef { // Validate checks the document type and determines if it can be validated. func Validate(doc any) error { switch obj := doc.(type) { - case *bill.Invoice: - return validateInvoice(obj) case *tax.Identity: return validateTaxIdentity(obj) } diff --git a/regimes/ch/invoices.go b/regimes/ch/invoices.go deleted file mode 100644 index e3fd9c5d..00000000 --- a/regimes/ch/invoices.go +++ /dev/null @@ -1,31 +0,0 @@ -package ch - -import ( - "github.com/invopop/gobl/bill" - "github.com/invopop/gobl/org" - "github.com/invopop/gobl/tax" - "github.com/invopop/validation" -) - -func validateInvoice(inv *bill.Invoice) error { - return validation.ValidateStruct(inv, - validation.Field(&inv.Supplier, - validation.By(validateInvoiceSupplier), - validation.Skip, - ), - ) -} - -func validateInvoiceSupplier(value any) error { - p, ok := value.(*org.Party) - if !ok || p == nil { - return nil - } - return validation.ValidateStruct(p, - validation.Field(&p.TaxID, - validation.Required, - tax.RequireIdentityCode, - validation.Skip, - ), - ) -} diff --git a/regimes/ch/invoices_test.go b/regimes/ch/invoices_test.go index df71a841..7305a0e6 100644 --- a/regimes/ch/invoices_test.go +++ b/regimes/ch/invoices_test.go @@ -56,5 +56,5 @@ func TestInvoiceValidation(t *testing.T) { inv = validInvoice() inv.Supplier.TaxID.Code = "" require.NoError(t, inv.Calculate()) - assert.ErrorContains(t, inv.Validate(), "supplier: (tax_id: (code: cannot be blank.).)") + assert.NoError(t, inv.Validate()) } diff --git a/regimes/regimes.go b/regimes/regimes.go index 8a3b49c4..9f6279ab 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -5,6 +5,7 @@ package regimes import ( // Import all the regime definitions which will automatically // add themselves to the tax regime register. + _ "github.com/invopop/gobl/regimes/ae" _ "github.com/invopop/gobl/regimes/at" _ "github.com/invopop/gobl/regimes/be" _ "github.com/invopop/gobl/regimes/br" diff --git a/version.go b/version.go index f112a3e3..e118e41e 100644 --- a/version.go +++ b/version.go @@ -8,7 +8,7 @@ import ( type Version string // VERSION is the current version of the GOBL library. -const VERSION Version = "v0.205.0" +const VERSION Version = "v0.205.1" // Semver parses and returns semver func (v Version) Semver() *semver.Version {