diff --git a/CHANGELOG.md b/CHANGELOG.md index a46fcace8..6c6b0b6c0 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/cl`: new tax regime for Chile (CL), including RUT tax identity validation (modulo 11 check digit) and the single-rate IVA category. - `org`: new `Attribute` model with `Item.Attributes` for named item features such as color or size (EN 16931 BG-32). Attributes replace the previous practice of mapping `Item.Meta` into output formats — meta is internal-only data. Each attribute is identified by a `key` or `type` and holds exactly one of a `text`, `code`, `amount` (with optional `unit`), or `date` value. Standard keys cover physical properties of the item, dates, nutritional declarations, and CO2e emissions. Item attribute keys must be unique. - `org`: `kj` (kilojoule) and `kcal` (kilocalorie) units. diff --git a/data/regimes/cl.json b/data/regimes/cl.json new file mode 100644 index 000000000..d20a86767 --- /dev/null +++ b/data/regimes/cl.json @@ -0,0 +1,141 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime-def", + "name": { + "en": "Chile", + "es": "Chile" + }, + "description": { + "en": "Chile's tax system is administered by the Servicio de Impuestos\nInternos (SII). Businesses and individuals are identified by their\nRUT (Rol Único Tributario), a numeric code followed by a single\ncheck digit (0-9 or K) calculated with a modulo 11 algorithm.\n\nVAT (Impuesto al Valor Agregado, IVA) is a single, flat-rate tax\napplied uniformly to sales and services, with no reduced or\nintermediate rates.\n\nChile's electronic invoicing system (Documentos Tributarios\nElectrónicos, DTE) requires invoices to be digitally signed and\nsent to the SII for clearance before being delivered to the\ncustomer. That clearance flow — including certificate\nmanagement, CAF folio ranges, and the SII's SOAP/XML web\nservices — is a separate integration concern from the core tax\nrules modelled here, and is intentionally out of scope for this\nregime. It would be a natural candidate for a future GOBL addon\n(similar to how es/tbai or es/verifactu extend the Spanish\nregime), not part of the base regime itself.\n\nBoth credit notes and debit notes are supported for invoice\ncorrections, mirroring the SII's Nota de Crédito Electrónica and\nNota de Débito Electrónica document types." + }, + "sources": [ + { + "title": { + "en": "SII - What is the VAT rate?", + "es": "SII - ¿Cuál es la tasa del Impuesto al Valor Agregado (IVA)?" + }, + "url": "https://www.sii.cl/preguntas_frecuentes/impuestos_mensuales/001_130_0572.htm" + } + ], + "time_zone": "America/Santiago", + "country": "CL", + "currency": "CLP", + "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." + } + } + ] + } + ], + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note", + "debit-note" + ] + } + ], + "categories": [ + { + "code": "VAT", + "name": { + "en": "VAT", + "es": "IVA" + }, + "title": { + "en": "Value Added Tax", + "es": "Impuesto al Valor Agregado" + }, + "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", + "es": "Tasa General" + }, + "values": [ + { + "since": "2003-10-01", + "percent": "19.0%" + } + ] + } + ], + "sources": [ + { + "title": { + "en": "SII - What is the VAT rate?", + "es": "SII - ¿Cuál es la tasa del Impuesto al Valor Agregado (IVA)?" + }, + "url": "https://www.sii.cl/preguntas_frecuentes/impuestos_mensuales/001_130_0572.htm" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/cl/credit-note-cl.yaml b/examples/cl/credit-note-cl.yaml new file mode 100644 index 000000000..3baa82fff --- /dev/null +++ b/examples/cl/credit-note-cl.yaml @@ -0,0 +1,51 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "5c1f0a2e-1d4b-4a9e-8f3c-2b6d7e8a9c01" +type: "credit-note" +currency: "CLP" +series: "SAMPLE" +code: "002" +issue_date: "2026-03-15" + +preceding: + - series: "SAMPLE" + code: "001" + issue_date: "2026-03-01" + +supplier: + tax_id: + country: "CL" + code: "760864285" + name: "Comercial Andina Limitada" + emails: + - addr: "facturacion@andina.example.cl" + addresses: + - num: "1234" + street: "Avenida Providencia" + locality: "Providencia" + region: "Región Metropolitana" + code: "7500000" + country: "CL" + +customer: + tax_id: + country: "CL" + code: "187654327" + name: "Distribuidora del Sur SpA" + emails: + - addr: "compras@delsur.example.cl" + addresses: + - num: "88" + street: "Calle Los Alerces" + locality: "Concepción" + region: "Biobío" + code: "4030000" + country: "CL" + +lines: + - quantity: 3 + item: + name: "Licencia anual de software" + price: "120000" + taxes: + - cat: VAT + rate: "standard" diff --git a/examples/cl/debit-note-cl.yaml b/examples/cl/debit-note-cl.yaml new file mode 100644 index 000000000..a15881ea3 --- /dev/null +++ b/examples/cl/debit-note-cl.yaml @@ -0,0 +1,51 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "6d2a1b3f-2e5c-4b0f-9a4d-3c7e8f9b0d12" +type: "debit-note" +currency: "CLP" +series: "SAMPLE" +code: "003" +issue_date: "2026-03-20" + +preceding: + - series: "SAMPLE" + code: "001" + issue_date: "2026-03-01" + +supplier: + tax_id: + country: "CL" + code: "760864285" + name: "Comercial Andina Limitada" + emails: + - addr: "facturacion@andina.example.cl" + addresses: + - num: "1234" + street: "Avenida Providencia" + locality: "Providencia" + region: "Región Metropolitana" + code: "7500000" + country: "CL" + +customer: + tax_id: + country: "CL" + code: "187654327" + name: "Distribuidora del Sur SpA" + emails: + - addr: "compras@delsur.example.cl" + addresses: + - num: "88" + street: "Calle Los Alerces" + locality: "Concepción" + region: "Biobío" + code: "4030000" + country: "CL" + +lines: + - quantity: 5 + item: + name: "Recargo por interés de mora" + price: "8000" + taxes: + - cat: VAT + rate: "standard" diff --git a/examples/cl/invoice-cl-cl.yaml b/examples/cl/invoice-cl-cl.yaml new file mode 100644 index 000000000..de7258a8b --- /dev/null +++ b/examples/cl/invoice-cl-cl.yaml @@ -0,0 +1,53 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852c1" +currency: "CLP" +series: "SAMPLE" +code: "001" +issue_date: "2026-03-01" + +supplier: + tax_id: + country: "CL" + code: "760864285" + name: "Comercial Andina Limitada" + emails: + - addr: "facturacion@andina.example.cl" + addresses: + - num: "1234" + street: "Avenida Providencia" + locality: "Providencia" + region: "Región Metropolitana" + code: "7500000" + country: "CL" + +customer: + tax_id: + country: "CL" + code: "187654327" + name: "Distribuidora del Sur SpA" + emails: + - addr: "compras@delsur.example.cl" + addresses: + - num: "88" + street: "Calle Los Alerces" + locality: "Concepción" + region: "Biobío" + code: "4030000" + country: "CL" + +lines: + - quantity: 15 + item: + name: "Consultoría en logística" + price: "45000" + unit: "h" + taxes: + - cat: VAT + rate: "standard" + - quantity: 3 + item: + name: "Licencia anual de software" + price: "120000" + taxes: + - cat: VAT + rate: "standard" diff --git a/examples/cl/out/credit-note-cl.json b/examples/cl/out/credit-note-cl.json new file mode 100644 index 000000000..b1f6783c6 --- /dev/null +++ b/examples/cl/out/credit-note-cl.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "37e1e7bba7e040c95c1ec55fde45c508c8928c491fe88e405a65689a3a56b793" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "CL", + "uuid": "5c1f0a2e-1d4b-4a9e-8f3c-2b6d7e8a9c01", + "type": "credit-note", + "series": "SAMPLE", + "code": "002", + "issue_date": "2026-03-15", + "currency": "CLP", + "preceding": [ + { + "issue_date": "2026-03-01", + "series": "SAMPLE", + "code": "001" + } + ], + "supplier": { + "name": "Comercial Andina Limitada", + "tax_id": { + "country": "CL", + "code": "760864285" + }, + "addresses": [ + { + "num": "1234", + "street": "Avenida Providencia", + "locality": "Providencia", + "region": "Región Metropolitana", + "code": "7500000", + "country": "CL" + } + ], + "emails": [ + { + "addr": "facturacion@andina.example.cl" + } + ] + }, + "customer": { + "name": "Distribuidora del Sur SpA", + "tax_id": { + "country": "CL", + "code": "187654327" + }, + "addresses": [ + { + "num": "88", + "street": "Calle Los Alerces", + "locality": "Concepción", + "region": "Biobío", + "code": "4030000", + "country": "CL" + } + ], + "emails": [ + { + "addr": "compras@delsur.example.cl" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "3", + "item": { + "name": "Licencia anual de software", + "price": "120000" + }, + "sum": "360000", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "general", + "percent": "19.0%" + } + ], + "total": "360000" + } + ], + "totals": { + "sum": "360000", + "total": "360000", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "360000", + "percent": "19.0%", + "amount": "68400" + } + ], + "amount": "68400" + } + ], + "sum": "68400" + }, + "tax": "68400", + "total_with_tax": "428400", + "payable": "428400" + } + } +} \ No newline at end of file diff --git a/examples/cl/out/debit-note-cl.json b/examples/cl/out/debit-note-cl.json new file mode 100644 index 000000000..6b7ff3dca --- /dev/null +++ b/examples/cl/out/debit-note-cl.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "a0dafbe89312ebfc032d5a899bec1131e52d2871cc421724bc8433c7ac0c6f19" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "CL", + "uuid": "6d2a1b3f-2e5c-4b0f-9a4d-3c7e8f9b0d12", + "type": "debit-note", + "series": "SAMPLE", + "code": "003", + "issue_date": "2026-03-20", + "currency": "CLP", + "preceding": [ + { + "issue_date": "2026-03-01", + "series": "SAMPLE", + "code": "001" + } + ], + "supplier": { + "name": "Comercial Andina Limitada", + "tax_id": { + "country": "CL", + "code": "760864285" + }, + "addresses": [ + { + "num": "1234", + "street": "Avenida Providencia", + "locality": "Providencia", + "region": "Región Metropolitana", + "code": "7500000", + "country": "CL" + } + ], + "emails": [ + { + "addr": "facturacion@andina.example.cl" + } + ] + }, + "customer": { + "name": "Distribuidora del Sur SpA", + "tax_id": { + "country": "CL", + "code": "187654327" + }, + "addresses": [ + { + "num": "88", + "street": "Calle Los Alerces", + "locality": "Concepción", + "region": "Biobío", + "code": "4030000", + "country": "CL" + } + ], + "emails": [ + { + "addr": "compras@delsur.example.cl" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "5", + "item": { + "name": "Recargo por interés de mora", + "price": "8000" + }, + "sum": "40000", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "general", + "percent": "19.0%" + } + ], + "total": "40000" + } + ], + "totals": { + "sum": "40000", + "total": "40000", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "40000", + "percent": "19.0%", + "amount": "7600" + } + ], + "amount": "7600" + } + ], + "sum": "7600" + }, + "tax": "7600", + "total_with_tax": "47600", + "payable": "47600" + } + } +} \ No newline at end of file diff --git a/examples/cl/out/invoice-cl-cl.json b/examples/cl/out/invoice-cl-cl.json new file mode 100644 index 000000000..c4d4bc684 --- /dev/null +++ b/examples/cl/out/invoice-cl-cl.json @@ -0,0 +1,127 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "6e6eb71ff6a44bad51e42b7652735541f8ec4bfefe9adc2736b4cd44284cebc6" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "CL", + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852c1", + "type": "standard", + "series": "SAMPLE", + "code": "001", + "issue_date": "2026-03-01", + "currency": "CLP", + "supplier": { + "name": "Comercial Andina Limitada", + "tax_id": { + "country": "CL", + "code": "760864285" + }, + "addresses": [ + { + "num": "1234", + "street": "Avenida Providencia", + "locality": "Providencia", + "region": "Región Metropolitana", + "code": "7500000", + "country": "CL" + } + ], + "emails": [ + { + "addr": "facturacion@andina.example.cl" + } + ] + }, + "customer": { + "name": "Distribuidora del Sur SpA", + "tax_id": { + "country": "CL", + "code": "187654327" + }, + "addresses": [ + { + "num": "88", + "street": "Calle Los Alerces", + "locality": "Concepción", + "region": "Biobío", + "code": "4030000", + "country": "CL" + } + ], + "emails": [ + { + "addr": "compras@delsur.example.cl" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "15", + "item": { + "name": "Consultoría en logística", + "price": "45000", + "unit": "h" + }, + "sum": "675000", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "general", + "percent": "19.0%" + } + ], + "total": "675000" + }, + { + "i": 2, + "quantity": "3", + "item": { + "name": "Licencia anual de software", + "price": "120000" + }, + "sum": "360000", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "general", + "percent": "19.0%" + } + ], + "total": "360000" + } + ], + "totals": { + "sum": "1035000", + "total": "1035000", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "1035000", + "percent": "19.0%", + "amount": "196650" + } + ], + "amount": "196650" + } + ], + "sum": "196650" + }, + "tax": "196650", + "total_with_tax": "1231650", + "payable": "1231650" + } + } +} \ No newline at end of file diff --git a/regimes/cl/cl.go b/regimes/cl/cl.go new file mode 100644 index 000000000..fb89314c6 --- /dev/null +++ b/regimes/cl/cl.go @@ -0,0 +1,87 @@ +// Package cl provides the Chile tax regime. +package cl + +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 Chile. +const CountryCode = "CL" + +func init() { + tax.RegisterRegimeDef(New()) + rules.Register("cl", 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 regime definition for Chile. +func New() *tax.RegimeDef { + return &tax.RegimeDef{ + Country: CountryCode, + Currency: currency.CLP, + TaxScheme: tax.CategoryVAT, + Name: i18n.String{ + i18n.EN: "Chile", + i18n.ES: "Chile", + }, + Description: i18n.String{ + i18n.EN: here.Doc(` + Chile's tax system is administered by the Servicio de Impuestos + Internos (SII). Businesses and individuals are identified by their + RUT (Rol Único Tributario), a numeric code followed by a single + check digit (0-9 or K) calculated with a modulo 11 algorithm. + + VAT (Impuesto al Valor Agregado, IVA) is a single, flat-rate tax + applied uniformly to sales and services, with no reduced or + intermediate rates. + + Chile's electronic invoicing system (Documentos Tributarios + Electrónicos, DTE) requires invoices to be digitally signed and + sent to the SII for clearance before being delivered to the + customer. That clearance flow — including certificate + management, CAF folio ranges, and the SII's SOAP/XML web + services — is a separate integration concern from the core tax + rules modelled here, and is intentionally out of scope for this + regime. It would be a natural candidate for a future GOBL addon + (similar to how es/tbai or es/verifactu extend the Spanish + regime), not part of the base regime itself. + + Both credit notes and debit notes are supported for invoice + corrections, mirroring the SII's Nota de Crédito Electrónica and + Nota de Débito Electrónica document types. + `), + }, + Sources: []*cbc.Source{ + { + Title: i18n.String{ + i18n.EN: "SII - What is the VAT rate?", + i18n.ES: "SII - ¿Cuál es la tasa del Impuesto al Valor Agregado (IVA)?", + }, + URL: "https://www.sii.cl/preguntas_frecuentes/impuestos_mensuales/001_130_0572.htm", + }, + }, + TimeZone: "America/Santiago", + Scenarios: []*tax.ScenarioSet{ + bill.InvoiceScenarios(), + }, + Categories: taxCategories, + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + bill.InvoiceTypeDebitNote, + }, + }, + }, + } +} diff --git a/regimes/cl/invoices_test.go b/regimes/cl/invoices_test.go new file mode 100644 index 000000000..75e67a644 --- /dev/null +++ b/regimes/cl/invoices_test.go @@ -0,0 +1,61 @@ +package cl_test + +import ( + "testing" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/org" + _ "github.com/invopop/gobl/regimes/cl" + "github.com/invopop/gobl/rules" + "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: "0001", + Supplier: &org.Party{ + Name: "Test Supplier", + TaxID: &tax.Identity{ + Country: "CL", + Code: "760864285", + }, + }, + Customer: &org.Party{ + Name: "Test Customer", + TaxID: &tax.Identity{ + Country: "CL", + Code: "187654327", + }, + }, + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(1, 0), + Item: &org.Item{ + Name: "bogus", + Price: num.NewAmount(10000, 0), + }, + Taxes: tax.Set{ + { + Category: "VAT", + Rate: "standard", + }, + }, + }, + }, + } +} + +func TestInvoiceValidation(t *testing.T) { + inv := validInvoice() + require.NoError(t, inv.Calculate()) + assert.NoError(t, rules.Validate(inv)) + + inv = validInvoice() + inv.Customer.TaxID.Code = "invalid" + require.NoError(t, inv.Calculate()) + assert.ErrorContains(t, rules.Validate(inv), "invalid Chilean RUT") +} diff --git a/regimes/cl/tax_categories.go b/regimes/cl/tax_categories.go new file mode 100644 index 000000000..41d58c969 --- /dev/null +++ b/regimes/cl/tax_categories.go @@ -0,0 +1,56 @@ +package cl + +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.ES: "IVA", + }, + Title: i18n.String{ + i18n.EN: "Value Added Tax", + i18n.ES: "Impuesto al Valor Agregado", + }, + Sources: []*cbc.Source{ + { + Title: i18n.String{ + i18n.EN: "SII - What is the VAT rate?", + i18n.ES: "SII - ¿Cuál es la tasa del Impuesto al Valor Agregado (IVA)?", + }, + URL: "https://www.sii.cl/preguntas_frecuentes/impuestos_mensuales/001_130_0572.htm", + }, + }, + Retained: false, + Keys: tax.GlobalVATKeys(), + Rates: []*tax.RateDef{ + { + Keys: []cbc.Key{tax.KeyStandard}, + Rate: tax.RateGeneral, + Name: i18n.String{ + i18n.EN: "General Rate", + i18n.ES: "Tasa General", + }, + Values: []*tax.RateValueDef{ + { + // Ley N° 19.888, published 2003-08-13, effective from + // 2003-10-01. Chile has a single flat VAT rate with no + // reduced or intermediate bands. + Since: cal.NewDate(2003, 10, 1), + Percent: num.MakePercentage(190, 3), + }, + }, + }, + }, + }, +} diff --git a/regimes/cl/tax_identity.go b/regimes/cl/tax_identity.go new file mode 100644 index 000000000..19f1e6850 --- /dev/null +++ b/regimes/cl/tax_identity.go @@ -0,0 +1,87 @@ +package cl + +import ( + "errors" + "regexp" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/rules" + "github.com/invopop/gobl/rules/is" + "github.com/invopop/gobl/tax" +) + +// The RUT (Rol Único Tributario, e.g. "76.086.428-5") identifies both +// individuals and companies in Chile. Modern bodies have 7-8 digits, but +// shorter historic ones exist (e.g. "1-9"), so 1-8 digits are accepted. +// +// Validation expects a normalized code ("760864285"): tax.NormalizeIdentity +// (registered in cl.go) strips punctuation and uppercases beforehand, and +// codes still containing separators are rejected. +// +// The check digit uses modulo 11: body digits, right to left, are multiplied +// by the repeating sequence 2-7 and summed; the digit is 11 minus (sum mod +// 11), where 11 maps to "0" and 10 to "K" (always uppercase). +// +// Sources: +// - SII DTE spec (modulo 11, uppercase "K"): +// https://www.sii.cl/factura_electronica/Webservice_Registro_Reclamo_DTE_V1.2.pdf +// - Full algorithm (verified against this implementation): +// https://lookuptax.com/docs/es/numero-identificacion-fiscal/guia-rut-chile + +var rutFormatRegexp = regexp.MustCompile(`^\d{1,8}[0-9K]$`) + +var rutMultipliers = []int{2, 3, 4, 5, 6, 7} + +func taxIdentityRules() *rules.Set { + return rules.For(new(tax.Identity), + rules.When(tax.IdentityIn(CountryCode), + rules.Field("code", + rules.AssertIfPresent("01", "invalid Chilean RUT", + is.Func("valid", isValidTaxIdentityCode), + ), + ), + ), + ) +} + +func isValidTaxIdentityCode(value any) bool { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return false + } + return validateRUT(code.String()) == nil +} + +func validateRUT(val string) error { + if !rutFormatRegexp.MatchString(val) { + return errors.New("invalid format") + } + + body := val[:len(val)-1] + check := val[len(val)-1] + + if check != rutCheckDigit(body) { + return errors.New("checksum mismatch") + } + + return nil +} + +// rutCheckDigit calculates the expected check digit for a RUT body using +// the modulo 11 algorithm described above. +func rutCheckDigit(body string) byte { + sum := 0 + for i, j := len(body)-1, 0; i >= 0; i, j = i-1, j+1 { + digit := int(body[i] - '0') + sum += digit * rutMultipliers[j%len(rutMultipliers)] + } + + switch result := 11 - (sum % 11); result { + case 11: + return '0' + case 10: + return 'K' + default: + return byte('0' + result) + } +} diff --git a/regimes/cl/tax_identity_test.go b/regimes/cl/tax_identity_test.go new file mode 100644 index 000000000..97e5afae8 --- /dev/null +++ b/regimes/cl/tax_identity_test.go @@ -0,0 +1,97 @@ +package cl_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/norm" + _ "github.com/invopop/gobl/regimes/cl" + "github.com/invopop/gobl/rules" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeTaxIdentity(t *testing.T) { + var tID *tax.Identity + assert.NotPanics(t, func() { + norm.Normalize(tID) + }, "nil tax identity") + + tests := []struct { + Code cbc.Code + Expected cbc.Code + }{ + { + Code: "76.086.428-5", + Expected: "760864285", + }, + { + Code: "12.345.670-k", + Expected: "12345670K", + }, + { + Code: " 18.765.432-7 ", + Expected: "187654327", + }, + } + for _, ts := range tests { + tID := &tax.Identity{Country: "CL", Code: ts.Code} + norm.Normalize(tID) + assert.Equal(t, ts.Expected, tID.Code) + } +} + +func TestTaxIdentityRules(t *testing.T) { + tests := []struct { + name string + code cbc.Code + err string + }{ + // The historic "1-9" RUT (Bernardo O'Higgins), frequently used in + // Chile as a test/example identifier — single-digit body. + {name: "good 1", code: "19"}, + {name: "good 2 (company)", code: "760864285"}, + {name: "good 3 (check digit K)", code: "12345670K"}, + {name: "good 4 (check digit 0)", code: "761234560"}, + {name: "good 5", code: "187654327"}, + { + name: "too short", + code: "1", + err: "IDENTITY-01", + }, + { + name: "body too long", + code: "123456789K", + err: "IDENTITY-01", + }, + { + name: "invalid check character", + code: "76086428X", + err: "IDENTITY-01", + }, + { + name: "not normalized", + code: "76.086.428-5", + err: "IDENTITY-01", + }, + { + name: "bad checksum", + code: "760864281", + err: "IDENTITY-01", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "CL", Code: tt.code} + err := rules.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 a6b6d7203..e203b5928 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -12,6 +12,7 @@ import ( _ "github.com/invopop/gobl/regimes/br" _ "github.com/invopop/gobl/regimes/ca" _ "github.com/invopop/gobl/regimes/ch" + _ "github.com/invopop/gobl/regimes/cl" _ "github.com/invopop/gobl/regimes/co" _ "github.com/invopop/gobl/regimes/de" _ "github.com/invopop/gobl/regimes/dk"