diff --git a/CHANGELOG.md b/CHANGELOG.md index 0984f4a59..e6d45a142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added +- `regimes/lv`: new tax regime for Latvia (LV), including VAT standard (21%), intermediate (12%), and reduced (5%) rates with comprehensive tax identity validation for legal entities (MOD-11 checksum) and individuals (date validation). - `bill`: `PaymentDetails.Payer` party — the party responsible for making payment of the invoice if not the customer, the counterpart of the existing `Payee`. ## [v0.501.0] - 2026-06-16 diff --git a/data/regimes/lv.json b/data/regimes/lv.json new file mode 100644 index 000000000..f2f5ec5dc --- /dev/null +++ b/data/regimes/lv.json @@ -0,0 +1,174 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime-def", + "name": { + "en": "Latvia", + "lv": "Latvija" + }, + "description": { + "en": "Latvia's tax system is administered by the State Revenue Service\n(Valsts ieņēmumu dienests, VID). As an EU member state, Latvia\nfollows the EU VAT Directive with standard, reduced, and\nsuper-reduced rates.\n\nVAT (Pievienotās vertības nodoklis, PVN) applies to most goods and services. Businesses are\nidentified by their VAT registration number, which follows the format of the country code\n\"LV\" followed by 11 digits." + }, + "sources": [ + { + "title": { + "en": "State Revenue Service - VAT overview" + }, + "url": "https://www.vid.gov.lv/en/value-added-tax" + } + ], + "time_zone": "Europe/Riga", + "country": "LV", + "currency": "EUR", + "tax_scheme": "VAT", + "scenarios": [ + { + "schema": "bill/invoice", + "list": [ + { + "tags": [ + "reverse-charge" + ], + "cat": [ + "VAT" + ], + "note": { + "cat": "VAT", + "key": "reverse-charge", + "text": "Reverse charge: Customer to account for VAT to the relevant tax authority." + } + } + ] + } + ], + "categories": [ + { + "code": "VAT", + "name": { + "en": "VAT", + "lv": "PVN" + }, + "title": { + "en": "Value Added Tax", + "lv": "Pievienotās Vertības Nodoklis" + }, + "keys": [ + { + "key": "standard", + "name": { + "en": "Standard" + } + }, + { + "key": "zero", + "name": { + "en": "Zero" + } + }, + { + "key": "reverse-charge", + "name": { + "en": "Reverse charge" + }, + "no_percent": true + }, + { + "key": "exempt", + "name": { + "en": "Exempt" + }, + "no_percent": true + }, + { + "key": "export", + "name": { + "en": "Export" + }, + "no_percent": true + }, + { + "key": "intra-community", + "name": { + "en": "Intra-community" + }, + "no_percent": true + }, + { + "key": "outside-scope", + "name": { + "en": "Outside scope" + }, + "no_percent": true + } + ], + "rates": [ + { + "rate": "general", + "keys": [ + "standard" + ], + "name": { + "en": "General Rate", + "lv": "Nodokļa Standartlikmi" + }, + "values": [ + { + "since": "2013-01-01", + "percent": "21.0%" + } + ] + }, + { + "rate": "intermediate", + "keys": [ + "standard" + ], + "name": { + "en": "First Reduced Rate", + "lv": "Pirmā samazinātā likme" + }, + "values": [ + { + "since": "2013-01-01", + "percent": "12.0%" + } + ] + }, + { + "rate": "reduced", + "keys": [ + "standard" + ], + "name": { + "en": "Second Reduced Rate", + "lv": "Otrā samazinātā likme" + }, + "values": [ + { + "since": "2018-01-01", + "percent": "5.0%" + } + ] + } + ], + "sources": [ + { + "title": { + "en": "State Revenue Service - VAT overview" + }, + "url": "https://www.fm.gov.lv/lv/tax-rates" + }, + { + "title": { + "en": "Value Added Tax Law" + }, + "url": "https://likumi.lv/ta/en/en/id/253451-value-added-tax-law" + }, + { + "title": { + "en": "EU Council Directive on the common system of value added tax" + }, + "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32006L0112" + } + ] + } + ] +} \ No newline at end of file diff --git a/data/rules/lv.json b/data/rules/lv.json new file mode 100644 index 000000000..e3533d7d6 --- /dev/null +++ b/data/rules/lv.json @@ -0,0 +1,32 @@ +{ + "id": "GOBL-LV", + "package": "lv", + "subsets": [ + { + "id": "GOBL-LV-TAX-IDENTITY", + "object": "tax.Identity", + "subsets": [ + { + "guard": "code in [LV]", + "subsets": [ + { + "field": "code", + "subsets": [ + { + "guard": "present", + "assert": [ + { + "id": "GOBL-LV-TAX-IDENTITY-01", + "desc": "invalid VAT identity code", + "tests": "valid" + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/data/schemas/tax/regime-code.json b/data/schemas/tax/regime-code.json index c4edd54f5..8733a4cbd 100644 --- a/data/schemas/tax/regime-code.json +++ b/data/schemas/tax/regime-code.json @@ -73,6 +73,10 @@ "const": "IT", "title": "Italy" }, + { + "const": "LV", + "title": "Latvia" + }, { "const": "MX", "title": "Mexico" diff --git a/examples/lv/invoice-intermediate.json b/examples/lv/invoice-intermediate.json new file mode 100644 index 000000000..e31a9c1ad --- /dev/null +++ b/examples/lv/invoice-intermediate.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "11111111-2222-3333-4444-555555555555", + "type": "standard", + "series": "LV2025", + "code": "003", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Supermarket SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Consumer", + "addresses": [ + { + "num": "10", + "street": "Rātslaukuma iela", + "locality": "Rīga", + "code": "1050", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "2", + "item": { + "name": "Food products", + "price": "15.00" + }, + "taxes": [ + { + "cat": "VAT", + "rate": "intermediate", + "percent": "12.0%" + } + ] + }, + { + "i": 2, + "quantity": "1", + "item": { + "name": "Medicines", + "price": "25.00" + }, + "taxes": [ + { + "cat": "VAT", + "rate": "intermediate", + "percent": "12.0%" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/lv/invoice-reduced.json b/examples/lv/invoice-reduced.json new file mode 100644 index 000000000..9129f8462 --- /dev/null +++ b/examples/lv/invoice-reduced.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "87654321-4321-4321-4321-210987654321", + "type": "standard", + "series": "LV2025", + "code": "002", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Bookstore SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Customer", + "addresses": [ + { + "num": "10", + "street": "Rātslaukuma iela", + "locality": "Rīga", + "code": "1050", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "5", + "item": { + "name": "Technical books", + "price": "25.00" + }, + "taxes": [ + { + "cat": "VAT", + "rate": "reduced", + "percent": "5.0%" + } + ] + }, + { + "i": 2, + "quantity": "10", + "item": { + "name": "Food supplies", + "price": "5.00" + }, + "taxes": [ + { + "cat": "VAT", + "rate": "intermediate", + "percent": "12.0%" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/lv/invoice-standard.json b/examples/lv/invoice-standard.json new file mode 100644 index 000000000..54664f285 --- /dev/null +++ b/examples/lv/invoice-standard.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "12345678-1234-1234-1234-123456789012", + "type": "standard", + "series": "LV2025", + "code": "001", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Services SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Customer Ltd", + "tax_id": { + "country": "LV", + "code": "52000000000" + }, + "addresses": [ + { + "num": "5", + "street": "Lāčplēša iela", + "locality": "Liepāja", + "code": "LV-3400", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "10", + "item": { + "name": "Consulting services", + "price": "100.00", + "unit": "h" + }, + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "21.0%" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/lv/out/invoice-intermediate.json b/examples/lv/out/invoice-intermediate.json new file mode 100644 index 000000000..600a0ebd3 --- /dev/null +++ b/examples/lv/out/invoice-intermediate.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "9a2bbb65412e5f499cb7366ad16be25b645bde530dc9889785f832c6b4710de5" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "11111111-2222-3333-4444-555555555555", + "type": "standard", + "series": "LV2025", + "code": "003", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Supermarket SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Consumer", + "addresses": [ + { + "num": "10", + "street": "Rātslaukuma iela", + "locality": "Rīga", + "code": "1050", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "2", + "item": { + "name": "Food products", + "price": "15.00" + }, + "sum": "30.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "intermediate", + "percent": "12.0%" + } + ], + "total": "30.00" + }, + { + "i": 2, + "quantity": "1", + "item": { + "name": "Medicines", + "price": "25.00" + }, + "sum": "25.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "intermediate", + "percent": "12.0%" + } + ], + "total": "25.00" + } + ], + "totals": { + "sum": "55.00", + "total": "55.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "55.00", + "percent": "12.0%", + "amount": "6.60" + } + ], + "amount": "6.60" + } + ], + "sum": "6.60" + }, + "tax": "6.60", + "total_with_tax": "61.60", + "payable": "61.60" + } + } +} \ No newline at end of file diff --git a/examples/lv/out/invoice-reduced.json b/examples/lv/out/invoice-reduced.json new file mode 100644 index 000000000..98b548237 --- /dev/null +++ b/examples/lv/out/invoice-reduced.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "07ceeff574313f3ab9d81d48fd353ace6ef880ea6d0d75e2a4a6041988573bec" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "87654321-4321-4321-4321-210987654321", + "type": "standard", + "series": "LV2025", + "code": "002", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Bookstore SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Customer", + "addresses": [ + { + "num": "10", + "street": "Rātslaukuma iela", + "locality": "Rīga", + "code": "1050", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "5", + "item": { + "name": "Technical books", + "price": "25.00" + }, + "sum": "125.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "reduced", + "percent": "5.0%" + } + ], + "total": "125.00" + }, + { + "i": 2, + "quantity": "10", + "item": { + "name": "Food supplies", + "price": "5.00" + }, + "sum": "50.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "intermediate", + "percent": "12.0%" + } + ], + "total": "50.00" + } + ], + "totals": { + "sum": "175.00", + "total": "175.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "125.00", + "percent": "5.0%", + "amount": "6.25" + }, + { + "key": "standard", + "base": "50.00", + "percent": "12.0%", + "amount": "6.00" + } + ], + "amount": "12.25" + } + ], + "sum": "12.25" + }, + "tax": "12.25", + "total_with_tax": "187.25", + "payable": "187.25" + } + } +} \ No newline at end of file diff --git a/examples/lv/out/invoice-standard.json b/examples/lv/out/invoice-standard.json new file mode 100644 index 000000000..d4dbf9433 --- /dev/null +++ b/examples/lv/out/invoice-standard.json @@ -0,0 +1,102 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "579bc077fed8b2c3d812ebc1234f284d87101154b1cc558b72c8c31b3359f599" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "12345678-1234-1234-1234-123456789012", + "type": "standard", + "series": "LV2025", + "code": "001", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Services SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Customer Ltd", + "tax_id": { + "country": "LV", + "code": "52000000000" + }, + "addresses": [ + { + "num": "5", + "street": "Lāčplēša iela", + "locality": "Liepāja", + "code": "LV-3400", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "10", + "item": { + "name": "Consulting services", + "price": "100.00", + "unit": "h" + }, + "sum": "1000.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "general", + "percent": "21.0%" + } + ], + "total": "1000.00" + } + ], + "totals": { + "sum": "1000.00", + "total": "1000.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "1000.00", + "percent": "21.0%", + "amount": "210.00" + } + ], + "amount": "210.00" + } + ], + "sum": "210.00" + }, + "tax": "210.00", + "total_with_tax": "1210.00", + "payable": "1210.00" + } + } +} \ No newline at end of file diff --git a/examples/lv/out/out/invoice-intermediate.json b/examples/lv/out/out/invoice-intermediate.json new file mode 100644 index 000000000..2720aecba --- /dev/null +++ b/examples/lv/out/out/invoice-intermediate.json @@ -0,0 +1,126 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "a33f0b56f526f3c8b0133790b489b16b7267bd5aec9e2f5f05b0b592011f6165" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/envelope", + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "9a2bbb65412e5f499cb7366ad16be25b645bde530dc9889785f832c6b4710de5" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "11111111-2222-3333-4444-555555555555", + "type": "standard", + "series": "LV2025", + "code": "003", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Supermarket SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Consumer", + "addresses": [ + { + "num": "10", + "street": "Rātslaukuma iela", + "locality": "Rīga", + "code": "1050", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "2", + "item": { + "name": "Food products", + "price": "15.00" + }, + "sum": "30.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "intermediate", + "percent": "12.0%" + } + ], + "total": "30.00" + }, + { + "i": 2, + "quantity": "1", + "item": { + "name": "Medicines", + "price": "25.00" + }, + "sum": "25.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "intermediate", + "percent": "12.0%" + } + ], + "total": "25.00" + } + ], + "totals": { + "sum": "55.00", + "total": "55.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "55.00", + "percent": "12.0%", + "amount": "6.60" + } + ], + "amount": "6.60" + } + ], + "sum": "6.60" + }, + "tax": "6.60", + "total_with_tax": "61.60", + "payable": "61.60" + } + } + } +} \ No newline at end of file diff --git a/examples/lv/out/out/invoice-reduced.json b/examples/lv/out/out/invoice-reduced.json new file mode 100644 index 000000000..ceea2ad4e --- /dev/null +++ b/examples/lv/out/out/invoice-reduced.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "b9afa6e81fcc41d6579a03f51b930731e29c1550610cfaa96900198bb8ce43ab" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/envelope", + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "07ceeff574313f3ab9d81d48fd353ace6ef880ea6d0d75e2a4a6041988573bec" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "87654321-4321-4321-4321-210987654321", + "type": "standard", + "series": "LV2025", + "code": "002", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Bookstore SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Customer", + "addresses": [ + { + "num": "10", + "street": "Rātslaukuma iela", + "locality": "Rīga", + "code": "1050", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "5", + "item": { + "name": "Technical books", + "price": "25.00" + }, + "sum": "125.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "reduced", + "percent": "5.0%" + } + ], + "total": "125.00" + }, + { + "i": 2, + "quantity": "10", + "item": { + "name": "Food supplies", + "price": "5.00" + }, + "sum": "50.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "intermediate", + "percent": "12.0%" + } + ], + "total": "50.00" + } + ], + "totals": { + "sum": "175.00", + "total": "175.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "125.00", + "percent": "5.0%", + "amount": "6.25" + }, + { + "key": "standard", + "base": "50.00", + "percent": "12.0%", + "amount": "6.00" + } + ], + "amount": "12.25" + } + ], + "sum": "12.25" + }, + "tax": "12.25", + "total_with_tax": "187.25", + "payable": "187.25" + } + } + } +} \ No newline at end of file diff --git a/examples/lv/out/out/invoice-standard.json b/examples/lv/out/out/invoice-standard.json new file mode 100644 index 000000000..d88f4926b --- /dev/null +++ b/examples/lv/out/out/invoice-standard.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "137ddf733768b43dc6c994bc6af2bf124745540c43e9244e3f8ca2313a847ffd" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/envelope", + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "579bc077fed8b2c3d812ebc1234f284d87101154b1cc558b72c8c31b3359f599" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "LV", + "uuid": "12345678-1234-1234-1234-123456789012", + "type": "standard", + "series": "LV2025", + "code": "001", + "issue_date": "2025-06-15", + "currency": "EUR", + "supplier": { + "name": "Latvian Services SIA", + "tax_id": { + "country": "LV", + "code": "40003032065" + }, + "addresses": [ + { + "num": "1", + "street": "Brīvības iela", + "locality": "Rīga", + "code": "1010", + "country": "LV" + } + ], + "emails": [ + { + "addr": "billing@example.lv" + } + ] + }, + "customer": { + "name": "Sample Customer Ltd", + "tax_id": { + "country": "LV", + "code": "52000000000" + }, + "addresses": [ + { + "num": "5", + "street": "Lāčplēša iela", + "locality": "Liepāja", + "code": "LV-3400", + "country": "LV" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "10", + "item": { + "name": "Consulting services", + "price": "100.00", + "unit": "h" + }, + "sum": "1000.00", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "general", + "percent": "21.0%" + } + ], + "total": "1000.00" + } + ], + "totals": { + "sum": "1000.00", + "total": "1000.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "1000.00", + "percent": "21.0%", + "amount": "210.00" + } + ], + "amount": "210.00" + } + ], + "sum": "210.00" + }, + "tax": "210.00", + "total_with_tax": "1210.00", + "payable": "1210.00" + } + } + } +} \ No newline at end of file diff --git a/regimes/lv/lv.go b/regimes/lv/lv.go new file mode 100644 index 000000000..024830b53 --- /dev/null +++ b/regimes/lv/lv.go @@ -0,0 +1,60 @@ +// Package lv provides the tax region definition for Latvia. +package lv + +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/norm" + "github.com/invopop/gobl/pkg/here" + "github.com/invopop/gobl/rules" + "github.com/invopop/gobl/tax" +) + +// CountryCode is the tax country code for Latvia. +const CountryCode = "LV" + +func init() { + tax.RegisterRegimeDef(New()) + rules.Register("lv", rules.GOBL.Add(CountryCode), taxIdentityRules()) + norm.Register( + norm.When(tax.IdentityIn(CountryCode), norm.For(func(id *tax.Identity) { tax.NormalizeIdentity(id) })), + ) +} + +// New provides the tax region definition +func New() *tax.RegimeDef { + return &tax.RegimeDef{ + Country: CountryCode, + Currency: currency.EUR, + TaxScheme: tax.CategoryVAT, + Name: i18n.String{ + i18n.EN: "Latvia", + i18n.LV: "Latvija", + }, + Description: i18n.String{ + i18n.EN: here.Doc(` + Latvia's tax system is administered by the State Revenue Service + (Valsts ieņēmumu dienests, VID). As an EU member state, Latvia + follows the EU VAT Directive with standard, reduced, and + super-reduced rates. + + VAT (Pievienotās vertības nodoklis, PVN) applies to most goods and services. Businesses are + identified by their VAT registration number, which follows the format of the country code + "LV" followed by 11 digits. + `), + }, + Sources: []*cbc.Source{ + { + Title: i18n.NewString("State Revenue Service - VAT overview"), + URL: "https://www.vid.gov.lv/en/value-added-tax", + }, + }, + TimeZone: "Europe/Riga", + Scenarios: []*tax.ScenarioSet{ + bill.InvoiceScenarios(), + }, + Categories: taxCategories, + } +} diff --git a/regimes/lv/tax_categories.go b/regimes/lv/tax_categories.go new file mode 100644 index 000000000..0e6f3ea51 --- /dev/null +++ b/regimes/lv/tax_categories.go @@ -0,0 +1,100 @@ +package lv + +import ( + "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/tax" +) + +var taxCategories = []*tax.CategoryDef{ + // + // VAT + // + { + Code: tax.CategoryVAT, + Name: i18n.String{ + i18n.EN: "VAT", + i18n.LV: "PVN", + }, + Title: i18n.String{ + i18n.EN: "Value Added Tax", + i18n.LV: "Pievienotās Vertības Nodoklis", + }, + Sources: []*cbc.Source{ + { + Title: i18n.String{ + i18n.EN: "State Revenue Service - VAT overview", + }, + URL: "https://www.fm.gov.lv/lv/tax-rates", + }, + { + Title: i18n.String{ + i18n.EN: "Value Added Tax Law", + }, + URL: "https://likumi.lv/ta/en/en/id/253451-value-added-tax-law", + }, + { + Title: i18n.String{ + i18n.EN: "EU Council Directive on the common system of value added tax", + }, + URL: "https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32006L0112", + }, + }, + Retained: false, + Keys: tax.GlobalVATKeys(), + Rates: []*tax.RateDef{ + // STANDARD TAX RATE - GENERAL (Minimum 15%) + //Applied to most goods and services in Latvia. + { + Keys: []cbc.Key{tax.KeyStandard}, + Rate: tax.RateGeneral, + Name: i18n.String{ + i18n.EN: "General Rate", + i18n.LV: "Nodokļa Standartlikme", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2013, 1, 1), + Percent: num.MakePercentage(210, 3), + }, + }, + }, + // STANDARD TAX RATE - HIGHEST REDUCED RATE + //Applied to essential goods and services (medicines, food supplies, etc). + { + Keys: []cbc.Key{tax.KeyStandard}, + Rate: tax.RateIntermediate, + Name: i18n.String{ + i18n.EN: "First Reduced Rate", + i18n.LV: "Pirmā samazinātā likme", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2013, 1, 1), + Percent: num.MakePercentage(120, 3), + }, + }, + }, + // STANDARD TAX RATE - LOWEST REDUCED RATE + //Applied to book supplies, press and media publications and related annexes. + { + Keys: []cbc.Key{tax.KeyStandard}, + Rate: tax.RateReduced, + Name: i18n.String{ + i18n.EN: "Second Reduced Rate", + i18n.LV: "Otrā samazinātā likme", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2018, 1, 1), + Percent: num.MakePercentage(50, 3), + }, + }, + }, + //NO SUPER-REDUCED TAX RATE (Maximum 5%) + //ZERO TAX RATE FOR EXPORT OR INTRACOMMUNITY. + }, + }, +} diff --git a/regimes/lv/tax_identity.go b/regimes/lv/tax_identity.go new file mode 100644 index 000000000..36ca3bcb4 --- /dev/null +++ b/regimes/lv/tax_identity.go @@ -0,0 +1,159 @@ +package lv + +import ( + "errors" + "regexp" + "strconv" + "time" + + "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/rules" + "github.com/invopop/gobl/rules/is" + "github.com/invopop/gobl/tax" +) + +// References: +// - https://www.vid.gov.lv/lv/pievienotas-vertibas-nodokla-maksataji +// - https://lookuptax.com/docs/tax-identification-number/latvia-tax-id-guide +// - https://vatdb.com/guides/validate-lv-vat-number/ + +var ( + taxCodeRegexps = []*regexp.Regexp{ + regexp.MustCompile(`^LV\d{11}$`), + regexp.MustCompile(`^\d{11}$`), + } + // Weights for the Mod-11 checksum calculation (positions 1-10) + taxCodeWeights = []int{9, 1, 4, 8, 3, 10, 2, 5, 7, 6} +) + +func taxIdentityRules() *rules.Set { + return rules.For(new(tax.Identity), + rules.When(tax.IdentityIn(CountryCode), + rules.Field("code", + rules.AssertIfPresent("01", "invalid VAT identity code", + is.Func("valid", isValidTaxIdentityCode), + ), + ), + ), + ) +} + +func isValidTaxIdentityCode(value any) bool { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return false + } + return validateTaxCode(code) == nil +} + +func validateTaxCode(value any) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + // Check format - must be LV + 11 digits, or just 11 digits + match := false + for _, re := range taxCodeRegexps { + if re.MatchString(val) { + match = true + break + } + } + if !match { + return errors.New("invalid format") + } + + // Strip LV prefix if present + if len(val) == 13 { + val = val[2:] + } + + // Get first two digits + firstDigit, _ := strconv.Atoi(string(val[0])) + + // Personal codes (first digit 0-3) - no checksum validation needed + // These follow the format similar to personas kods without the hyphen + if firstDigit >= 0 && firstDigit <= 3 { + return validatePersonalCode(val) + } + + // Legal entities (first digit 4-9) - validate Mod-11 checksum + return validateMod11Checksum(val) +} + +// validatePersonalCode validates personal codes (starting with 0-3) +func validatePersonalCode(val string) error { + + firstDigits, err := strconv.Atoi(val[0:2]) + if err != nil { + return errors.New("invalid first two digits") + } + + // For modern personal codes, prefix 3X (new format from 1 July 2017) + // The second digit is random between 2-9, digits 3-11 are random 0-9 + if firstDigits >= 32 && firstDigits <= 39 { + return nil + } + + // For legacy personal codes, validate date components + // Format: DDMMYYCXXXX where digits 1-2 are day, 3-4 are month, 5-6 are year, + // digit 7 is century (0=18xx, 1=19xx, 2=20xx) + day, _ := strconv.Atoi(val[0:2]) + month, _ := strconv.Atoi(val[2:4]) + year, _ := strconv.Atoi(val[4:6]) + century, _ := strconv.Atoi(val[6:7]) + + // Determine full year from century digit + // Century digit: 0 = 18th century (1800-1899), 1 = 19th century (1900-1999), 2 = 21st century (2000-2099) + var fullYear int + switch century { + case 0: + fullYear = 1800 + year + case 1: + fullYear = 1900 + year + case 2: + fullYear = 2000 + year + default: + return errors.New("invalid century digit in personal code") + } + + // Validate the date using cal package which handles proper date validation + // including leap years and month-specific day limits + d := cal.MakeDate(fullYear, time.Month(month), day) + if !d.IsValid() { + return errors.New("invalid birth date in personal code") + } + + return nil +} + +func validateMod11Checksum(val string) error { + var sum int + for i := 0; i < 10; i++ { + digit := int(val[i] - '0') + sum += taxCodeWeights[i] * digit + } + + remainder := sum % 11 + checkDigit := 3 - remainder + + if checkDigit < 0 { + checkDigit += 11 + } + + // If the calculated checksum is 10 or 11, the number is invalid + if checkDigit >= 10 { + return errors.New("calculated checksum results in an invalid corporate digit") + } + + actualCheck := int(val[10] - '0') + + if checkDigit != actualCheck { + return errors.New("checksum mismatch") + } + + return nil +} diff --git a/regimes/lv/tax_identity_test.go b/regimes/lv/tax_identity_test.go new file mode 100644 index 000000000..9c268d43c --- /dev/null +++ b/regimes/lv/tax_identity_test.go @@ -0,0 +1,136 @@ +package lv_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/norm" + _ "github.com/invopop/gobl/regimes/lv" + "github.com/invopop/gobl/rules" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestTaxIdentityNormalization(t *testing.T) { + tests := []struct { + name string + input cbc.Code + expected cbc.Code + }{ + {name: "legal entity with uppercase LV prefix", input: "LV40003032065", expected: "40003032065"}, + {name: "legacy personal with uppercase-lowercase LV prefix", input: "Lv01019012345", expected: "01019012345"}, + {name: "modern personal with lowecase LV prefix", input: "lv32123456789", expected: "32123456789"}, + {name: "legal entity without prefix", input: "40003032065", expected: "40003032065"}, + {name: "legacy personal without prefix", input: "01019012345", expected: "01019012345"}, + {name: "modern personal without prefix", input: "32123456789", expected: "32123456789"}, + {name: "legacy personal with hyphen and LV prefix", input: "LV010190-12345", expected: "01019012345"}, + {name: "legacy personal with hyphen no prefix", input: "010190-12345", expected: "01019012345"}, + {name: "legacy personal with hyphen 15 June", input: "LV150690-12345", expected: "15069012345"}, + {name: "legacy personal with hyphen no prefix 15 June", input: "150690-12345", expected: "15069012345"}, + {name: "modern personal with hyphen and LV prefix", input: "LV3212345-6789", expected: "32123456789"}, + {name: "modern personal with hyphen no prefix", input: "3212345-6789", expected: "32123456789"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "LV", Code: tt.input} + norm.Normalize(tID) + assert.Equal(t, tt.expected, tID.Code) + }) + } +} + +func TestTaxIdentityRules(t *testing.T) { + tests := []struct { + name string + input cbc.Code + expected string // error code or empty for valid + }{ + // Legal entity codes (starting with 4-9) + {name: "valid legal entity with prefix 4", input: "LV40003032065", expected: ""}, + {name: "valid legal entity with prefix 9", input: "LV99000000001", expected: ""}, + {name: "valid legal entity with prefix 5", input: "LV52000000000", expected: ""}, + {name: "valid legal entity with prefix 6", input: "LV64000000000", expected: ""}, + {name: "valid legal entity with prefix 4 - alt", input: "LV40003521600", expected: ""}, + + // Legacy personal codes (DDMMYYCXXXX format, pre-2017) + // Format: DDMMYYCXXXX where C is century (0=18xx, 1=19xx, 2=20xx) + {name: "valid legacy personal - 1 Jan 1990", input: "LV01019012345", expected: ""}, + {name: "valid legacy personal - 15 June 1990", input: "LV15069012345", expected: ""}, + {name: "valid legacy personal - 29 Feb 2000 leap year", input: "LV29020021234", expected: ""}, + + // Invalid legacy personal codes - month validation + {name: "invalid legacy - month 0", input: "LV01009012345", expected: "IDENTITY-01"}, + {name: "invalid legacy - month 13", input: "LV01139012345", expected: "IDENTITY-01"}, + // Invalid legacy personal codes - day validation + {name: "invalid legacy - day 0", input: "LV00019012345", expected: "IDENTITY-01"}, + {name: "invalid legacy - April 31", input: "LV31040012345", expected: "IDENTITY-01"}, + {name: "invalid legacy - June 31", input: "LV31060012345", expected: "IDENTITY-01"}, + {name: "invalid legacy - Sep 31", input: "LV31090012345", expected: "IDENTITY-01"}, + {name: "invalid legacy - Nov 31", input: "LV31110012345", expected: "IDENTITY-01"}, + // Feb 29 in non-leap years + {name: "invalid legacy - Feb 29 non-leap 1900", input: "LV29020012345", expected: "IDENTITY-01"}, + {name: "invalid legacy - Feb 29 non-leap 2014", input: "LV29021421234", expected: "IDENTITY-01"}, + + // Invalid legacy personal codes - century digit validation + // Century digit must be 0, 1, or 2 (18xx, 19xx, 20xx respectively) + {name: "invalid legacy - century digit 3", input: "LV01011431234", expected: "IDENTITY-01"}, + {name: "invalid legacy - century digit 4", input: "LV01011441234", expected: "IDENTITY-01"}, + {name: "invalid legacy - century digit 5", input: "LV01011451234", expected: "IDENTITY-01"}, + {name: "invalid legacy - century digit 6", input: "LV01011461234", expected: "IDENTITY-01"}, + {name: "invalid legacy - century digit 7", input: "LV01011471234", expected: "IDENTITY-01"}, + {name: "invalid legacy - century digit 8", input: "LV01011481234", expected: "IDENTITY-01"}, + {name: "invalid legacy - century digit 9", input: "LV01011491234", expected: "IDENTITY-01"}, + + // Invalid checksum for legal entities + // For Mod-11 checksum: checkDigit = 3 - remainder, if remainder = 4, checkDigit = 10 (invalid) + // "40000000021" gives sum = 48, 48 % 11 = 4, checkDigit = 10 (invalid) + {name: "invalid checksum - results in digit 10", input: "LV40000000021", expected: "IDENTITY-01"}, + + // Modern personal codes (starting with 3X, post-1 July 2017) + {name: "valid modern personal code 32X", input: "LV32123456789", expected: ""}, + {name: "valid modern personal code 33X", input: "LV33000000000", expected: ""}, + {name: "valid modern personal code 34X", input: "LV34000000000", expected: ""}, + {name: "valid modern personal code 35X", input: "LV35000000000", expected: ""}, + {name: "valid modern personal code 36X", input: "LV36000000000", expected: ""}, + {name: "valid modern personal code 37X", input: "LV37000000000", expected: ""}, + {name: "valid modern personal code 38X", input: "LV38000000000", expected: ""}, + {name: "valid modern personal code 39X", input: "LV39000000000", expected: ""}, + + // Invalid modern personal codes - second digit must be 2-9 + {name: "invalid modern personal - second digit 0", input: "LV30000000000", expected: "IDENTITY-01"}, + {name: "invalid modern personal - second digit 1", input: "LV31000000000", expected: "IDENTITY-01"}, + + // Invalid checksum for legal entities + {name: "bad checksum for legal entity", input: "LV40003032066", expected: "IDENTITY-01"}, + + // Invalid formats + {name: "too long", input: "LV123456789012", expected: "IDENTITY-01"}, + {name: "too short", input: "LV1234567890", expected: "IDENTITY-01"}, + {name: "not enough digits", input: "LV123456789", expected: "IDENTITY-01"}, + {name: "letters in code", input: "LVAAAAAAAAAA", expected: "IDENTITY-01"}, + // Letters interleaved in the code sequence + {name: "letters interleaved - letter in middle (business)", input: "LV4000A032065", expected: "IDENTITY-01"}, + {name: "letters interleaved - multiple letters (business)", input: "LV4AB0C0D2065", expected: "IDENTITY-01"}, + {name: "letters interleaved - letter in middle (natural persona)", input: "LV3200A032065", expected: "IDENTITY-01"}, + // Valid format but wrong country prefix - not an LV tax identity + {name: "wrong country - ES prefix", input: "ES12345678901", expected: "IDENTITY-01"}, + {name: "wrong country - DE prefix", input: "DE123456789", expected: "IDENTITY-01"}, + {name: "wrong country - FR prefix", input: "FR12345678901", expected: "IDENTITY-01"}, + // Empty code is valid (not required) + {name: "empty code", input: "", expected: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "LV", Code: tt.input} + err := rules.Validate(tID) + if tt.expected == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tt.expected) + } + } + }) + } +} diff --git a/regimes/regimes.go b/regimes/regimes.go index 1ca5cdd5d..5f9c948c4 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -22,6 +22,7 @@ import ( _ "github.com/invopop/gobl/regimes/ie" _ "github.com/invopop/gobl/regimes/in" _ "github.com/invopop/gobl/regimes/it" + _ "github.com/invopop/gobl/regimes/lv" _ "github.com/invopop/gobl/regimes/mx" _ "github.com/invopop/gobl/regimes/nl" _ "github.com/invopop/gobl/regimes/no"