From 6dac3b5f6a8958212270aba8f08699ffdd928ee4 Mon Sep 17 00:00:00 2001 From: Menendez6 Date: Tue, 22 Oct 2024 15:37:14 +0000 Subject: [PATCH 1/7] Adding TIN algorithms --- regimes/at/at.go | 9 +- regimes/at/identities_test.go | 85 +++++++++++++ regimes/at/identitites.go | 80 ++++++++++++ regimes/fr/fr.go | 12 +- regimes/fr/identities.go | 77 ++++++++++++ regimes/fr/identities_test.go | 90 +++++++++++++ regimes/gb/gb.go | 8 +- regimes/gb/identities.go | 154 +++++++++++++++++++++++ regimes/gb/identities_test.go | 96 ++++++++++++++ regimes/ie/examples/invoice-b2b.yaml | 47 +++++++ regimes/ie/examples/out/invoice-b2b.json | 113 +++++++++++++++++ regimes/ie/ie.go | 61 +++++++++ regimes/ie/tax_categories.go | 94 ++++++++++++++ regimes/ie/tax_identity.go | 37 ++++++ regimes/ie/tax_identity_test.go | 56 +++++++++ regimes/pl/identities.go | 69 ++++++++++ regimes/pl/identities_test.go | 75 +++++++++++ regimes/pl/pl.go | 12 +- regimes/regimes.go | 1 + 19 files changed, 1167 insertions(+), 9 deletions(-) create mode 100644 regimes/at/identities_test.go create mode 100644 regimes/at/identitites.go create mode 100644 regimes/fr/identities.go create mode 100644 regimes/fr/identities_test.go create mode 100644 regimes/gb/identities.go create mode 100644 regimes/gb/identities_test.go create mode 100644 regimes/ie/examples/invoice-b2b.yaml create mode 100644 regimes/ie/examples/out/invoice-b2b.json create mode 100644 regimes/ie/ie.go create mode 100644 regimes/ie/tax_categories.go create mode 100644 regimes/ie/tax_identity.go create mode 100644 regimes/ie/tax_identity_test.go create mode 100644 regimes/pl/identities.go create mode 100644 regimes/pl/identities_test.go diff --git a/regimes/at/at.go b/regimes/at/at.go index d80fe4ac..02b2349b 100644 --- a/regimes/at/at.go +++ b/regimes/at/at.go @@ -6,6 +6,7 @@ import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -31,7 +32,8 @@ func New() *tax.RegimeDef { Tags: []*tax.TagSet{ common.InvoiceTags(), }, - Categories: taxCategories, + Categories: taxCategories, + IdentityKeys: identityKeyDefinitions, // identities.go Corrections: []*tax.CorrectionDefinition{ { Schema: bill.ShortSchemaInvoice, @@ -50,7 +52,10 @@ func Validate(doc any) error { return validateInvoice(obj) case *tax.Identity: return validateTaxIdentity(obj) + case *org.Identity: + return validateTaxNumber(obj) } + return nil } @@ -59,5 +64,7 @@ func Normalize(doc any) { switch obj := doc.(type) { case *tax.Identity: tax.NormalizeIdentity(obj) + case *org.Identity: + normalizeTaxNumber(obj) } } diff --git a/regimes/at/identities_test.go b/regimes/at/identities_test.go new file mode 100644 index 00000000..d11f798a --- /dev/null +++ b/regimes/at/identities_test.go @@ -0,0 +1,85 @@ +package at_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/at" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeAndValidateTaxNumber(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "Valid PESEL with hyphen", + input: "12-3456789", + wantErr: false, + }, + { + name: "Valid PESEL with spaces", + input: "12 345 6789", + wantErr: false, + }, + { + name: "Valid PESEL with mixed symbols", + input: "12.345.6789", + wantErr: false, + }, + { + name: "Invalid length", + input: "12-34567", // Less than 9 digits + wantErr: true, + }, + { + name: "Invalid length with extra digits", + input: "12-34567890", // More than 9 digits + wantErr: true, + }, + { + name: "Invalid tax office code", + input: "00-3456789", // Tax office code should be between 1 and 99 + wantErr: true, + }, + { + name: "Invalid taxpayer number", + input: "12-0000000", // Taxpayer number should be positive + wantErr: true, + }, + { + name: "Empty input", + input: "", + wantErr: false, + }, + { + name: "Nil identity", + input: "12-3456789", // This should not error out because we will check for nil + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + identity := &org.Identity{ + Key: at.IdentityKeyTaxNumber, + Code: cbc.Code(tt.input), + } + + // Normalize the tax number first + at.Normalize(identity) + + // Validate the tax number + err := at.Validate(identity) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/regimes/at/identitites.go b/regimes/at/identitites.go new file mode 100644 index 00000000..f3440966 --- /dev/null +++ b/regimes/at/identitites.go @@ -0,0 +1,80 @@ +package at + +import ( + "errors" + "regexp" + "strconv" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +const ( + // IdentityKeyTaxNumber represents the Austrian tax number (Steuernummer) issued to + // people that can be included on invoices inside Austria. For international + // sales, the registered VAT number (Umsatzsteueridentifikationsnummer) should + // be used instead. + IdentityKeyTaxNumber cbc.Key = "at-tax-number" +) + +var badCharsRegexPattern = regexp.MustCompile(`[^\d]`) + +var identityKeyDefinitions = []*cbc.KeyDefinition{ + { + Key: IdentityKeyTaxNumber, + Name: i18n.String{ + i18n.EN: "Tax Number", + i18n.DE: "Steuernummer", + }, + }, +} + +func normalizeTaxNumber(id *org.Identity) { + if id == nil || id.Key != IdentityKeyTaxNumber { + return + } + code := id.Code.String() + code = badCharsRegexPattern.ReplaceAllString(code, "") + id.Code = cbc.Code(code) +} + +func validateTaxNumber(id *org.Identity) error { + if id == nil || id.Key != IdentityKeyTaxNumber { + return nil + } + + return validation.ValidateStruct(id, + validation.Field(&id.Code, validation.By(validateTaxIdCode)), + ) +} + +// validateAustrianTaxIdCode validates the normalized tax ID code. +func validateTaxIdCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + // Austrian Steuernummer format: must have 9 digits (2 for tax office + 7 for taxpayer ID) + if len(val) != 9 { + return errors.New("length must be 9 digits") + } + + // Split into tax office code and taxpayer number + taxOffice, _ := strconv.Atoi(val[:2]) + taxpayerNumber, _ := strconv.Atoi(val[2:]) + + // Perform basic checks + if taxOffice < 1 || taxOffice > 99 { + return errors.New("invalid tax office code") + } + + if taxpayerNumber <= 0 { + return errors.New("invalid taxpayer number") + } + + return nil +} diff --git a/regimes/fr/fr.go b/regimes/fr/fr.go index f17f12dc..0dddb03a 100644 --- a/regimes/fr/fr.go +++ b/regimes/fr/fr.go @@ -6,6 +6,7 @@ import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" "github.com/invopop/gobl/pkg/here" "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" @@ -56,9 +57,10 @@ func New() *tax.RegimeDef { }, }, }, - Validator: Validate, - Normalizer: Normalize, - Categories: taxCategories, + Validator: Validate, + Normalizer: Normalize, + Categories: taxCategories, + IdentityKeys: identityKeyDefinitions, // identities.go } } @@ -69,6 +71,8 @@ func Validate(doc interface{}) error { return validateInvoice(obj) case *tax.Identity: return validateTaxIdentity(obj) + case *org.Identity: + return validateTaxNumber(obj) } return nil } @@ -78,5 +82,7 @@ func Normalize(doc any) { switch obj := doc.(type) { case *tax.Identity: normalizeTaxIdentity(obj) + case *org.Identity: + normalizeTaxNumber(obj) } } diff --git a/regimes/fr/identities.go b/regimes/fr/identities.go new file mode 100644 index 00000000..0ec43ebe --- /dev/null +++ b/regimes/fr/identities.go @@ -0,0 +1,77 @@ +package fr + +import ( + "errors" + "regexp" + "strconv" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +const ( + // IdentityKeyTaxNumber represents the French tax reference number (numéro fiscal de référence). + IdentityKeyTaxNumber cbc.Key = "fr-tax-number" +) + +var badCharsRegexPattern = regexp.MustCompile(`[^\d]`) + +var identityKeyDefinitions = []*cbc.KeyDefinition{ + { + Key: IdentityKeyTaxNumber, + Name: i18n.String{ + i18n.EN: "Tax Number", + i18n.FR: "Numéro fiscal de référence", + }, + }, +} + +// validateTaxNumber validates the French tax reference number. +func validateTaxNumber(id *org.Identity) error { + if id == nil || id.Key != IdentityKeyTaxNumber { + return nil + } + + return validation.ValidateStruct(id, + validation.Field(&id.Code, validation.By(validateTaxIdCode)), + ) +} + +// validateTaxIdCode validates the normalized tax ID code. +func validateTaxIdCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + // Check length + if len(val) != 13 { + return errors.New("length must be 13 digits") + } + + // Check that all characters are digits + if _, err := strconv.Atoi(val); err != nil { + return errors.New("must contain only digits") + } + + // Check that the first digit is 0, 1, 2, or 3 + firstDigit := val[0] + if firstDigit < '0' || firstDigit > '3' { + return errors.New("first digit must be 0, 1, 2, or 3") + } + + return nil +} + +// normalizeTaxNumber removes any non-digit characters from the tax number. +func normalizeTaxNumber(id *org.Identity) { + if id == nil || id.Key != IdentityKeyTaxNumber { + return + } + code := id.Code.String() + code = badCharsRegexPattern.ReplaceAllString(code, "") + id.Code = cbc.Code(code) +} diff --git a/regimes/fr/identities_test.go b/regimes/fr/identities_test.go new file mode 100644 index 00000000..6903a03e --- /dev/null +++ b/regimes/fr/identities_test.go @@ -0,0 +1,90 @@ +package fr_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/fr" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeAndValidateTaxNumber(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "Valid tax number with leading 0", + input: "0123456789012", + wantErr: false, + }, + { + name: "Valid tax number with leading 1", + input: "1234567890123", + wantErr: false, + }, + { + name: "Valid tax number with leading 2", + input: "2234567890123", + wantErr: false, + }, + { + name: "Valid tax number with leading 3", + input: "3234567890123", + wantErr: false, + }, + { + name: "Invalid tax number with leading 4", + input: "4234567890123", // First digit not allowed + wantErr: true, + }, + { + name: "Invalid length", + input: "123456789", // Less than 13 digits + wantErr: true, + }, + { + name: "Invalid length with extra digits", + input: "12345678901234", // More than 13 digits + wantErr: true, + }, + { + name: "Invalid characters", + input: "1234A6789012", // Contains a letter + wantErr: true, + }, + { + name: "Empty input", + input: "", + wantErr: false, + }, + { + name: "Nil identity", + input: "0123456789012", // This should not error out because we will check for nil + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + identity := &org.Identity{ + Key: fr.IdentityKeyTaxNumber, + Code: cbc.Code(tt.input), + } + + // Normalize the tax number first + fr.Normalize(identity) + + // Validate the tax number + err := fr.Validate(identity) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/regimes/gb/gb.go b/regimes/gb/gb.go index 8c6ed129..8ce75de7 100644 --- a/regimes/gb/gb.go +++ b/regimes/gb/gb.go @@ -7,6 +7,7 @@ import ( "github.com/invopop/gobl/currency" "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/org" "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -45,7 +46,8 @@ func New() *tax.RegimeDef { Tags: []*tax.TagSet{ common.InvoiceTags(), }, - Categories: taxCategories, + Categories: taxCategories, + IdentityKeys: identityKeyDefinitions, Corrections: []*tax.CorrectionDefinition{ { Schema: bill.ShortSchemaInvoice, @@ -63,6 +65,8 @@ func Validate(doc interface{}) error { switch obj := doc.(type) { case *tax.Identity: return validateTaxIdentity(obj) + case *org.Identity: + return validateTaxNumber(obj) } return nil } @@ -72,5 +76,7 @@ func Normalize(doc interface{}) { switch obj := doc.(type) { case *tax.Identity: tax.NormalizeIdentity(obj, altCountryCodes...) + case *org.Identity: + normalizeTaxNumber(obj) } } diff --git a/regimes/gb/identities.go b/regimes/gb/identities.go new file mode 100644 index 00000000..ec13ce3f --- /dev/null +++ b/regimes/gb/identities.go @@ -0,0 +1,154 @@ +package gb + +import ( + "errors" + "regexp" + "strings" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +const ( + // IdentityUTR represents the UK Unique Taxpayer Reference (UTR). + IdentityUTR cbc.Key = "gb-utr" + // IdentityNINO represents the UK National Insurance Number (NINO). + IdentityNINO cbc.Key = "gb-nino" +) + +var badCharsRegexPattern = regexp.MustCompile(`[^\d]`) +var ninoPattern = `^[A-CEGHJ-PR-TW-Z]{2}\d{6}[A-D]$` +var utrPattern = `^[1-9]\d{9}$` + +var identityKeyDefinitions = []*cbc.KeyDefinition{ + { + Key: IdentityUTR, + Name: i18n.String{ + i18n.EN: "Unique Taxpayer Reference", + }, + }, + { + Key: IdentityNINO, + Name: i18n.String{ + i18n.EN: "National Insurance Number", + }, + }, +} + +func normalizeTaxNumber(id *org.Identity) { + if id == nil || (id.Key != IdentityUTR && id.Key != IdentityNINO) { + return + } + + if id.Key == IdentityUTR { + code := id.Code.String() + code = badCharsRegexPattern.ReplaceAllString(code, "") + id.Code = cbc.Code(code) + } else if id.Key == IdentityNINO { + code := id.Code.String() + code = strings.ToUpper(code) + code = tax.IdentityCodeBadCharsRegexp.ReplaceAllString(code, "") + id.Code = cbc.Code(code) + } +} + +func validateTaxNumber(id *org.Identity) error { + if id == nil { + return nil + } + + if id.Key == IdentityNINO { + return validation.ValidateStruct(id, + validation.Field(&id.Code, validation.By(validateNinoCode)), + ) + } else if id.Key == IdentityUTR { + return validation.ValidateStruct(id, + validation.Field(&id.Code, validation.By(validateUtrCode)), + ) + } + + return nil +} + +// validateUtrCode validates the normalized Unique Taxpayer Reference (UTR). +func validateUtrCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + // UK UTR pattern: 10 digits, first digit cannot be 0 + + matched, err := regexp.MatchString(utrPattern, val) + if err != nil { + return err + } + + if !matched { + return errors.New("invalid UTR format") + } + + return nil +} + +// validateNinoCode validates the normalized National Insurance Number (NINO). +func validateNinoCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + // UK NINO pattern: Two prefix letters (valid), six digits, one suffix letter (A-D) + + matched, err := regexp.MatchString(ninoPattern, val) + if err != nil { + return err + } + + if !matched { + return errors.New("invalid NINO format") + } + + // Check prefix letters + if !isValidPrefix(val[:2]) { + return errors.New("invalid prefix letters") + } + + return nil +} + +// isValidPrefix checks if the prefix letters are valid according to the specified rules. +func isValidPrefix(prefix string) bool { + // Disallowed prefixes + disallowedPrefixes := []string{"BG", "GB", "NK", "KN", "TN", "NT", "ZZ"} + if contains(disallowedPrefixes, prefix) { + return false + } + + // First letter should not be D, F, I, Q, U, or V + if strings.ContainsAny(string(prefix[0]), "DFIQUV") { + return false + } + + // Second letter should not be D, F, I, Q, U, V or O + if strings.ContainsAny(string(prefix[1]), "DFIQUV") || prefix[1] == 'O' { + return false + } + + return true +} + +// contains checks if a slice contains a specific string. +func contains(slice []string, item string) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} diff --git a/regimes/gb/identities_test.go b/regimes/gb/identities_test.go new file mode 100644 index 00000000..6349f170 --- /dev/null +++ b/regimes/gb/identities_test.go @@ -0,0 +1,96 @@ +package gb_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/gb" + "github.com/stretchr/testify/assert" +) + +func TestUKIdentifiers(t *testing.T) { + tests := []struct { + name string + idKey cbc.Key + initialCode string + expectedCode string + expectedError string + }{ + { + name: "Normalize UTR - spaces removed", + idKey: gb.IdentityUTR, + initialCode: " 1234567890 ", + expectedCode: "1234567890", + expectedError: "", + }, + { + name: "Validate valid UTR", + idKey: gb.IdentityUTR, + initialCode: "1234567890", + expectedCode: "1234567890", + expectedError: "", + }, + { + name: "Validate invalid UTR - starts with 0", + idKey: gb.IdentityUTR, + initialCode: "0234567890", + expectedCode: "0234567890", + expectedError: "code: invalid UTR format.", + }, + { + name: "Normalize NINO - to uppercase", + idKey: gb.IdentityNINO, + initialCode: "ab123456c", + expectedCode: "AB123456C", + expectedError: "", + }, + { + name: "Validate valid NINO", + idKey: gb.IdentityNINO, + initialCode: "AB123456C", + expectedCode: "AB123456C", + expectedError: "", + }, + { + name: "Validate invalid NINO - disallowed prefix", + idKey: gb.IdentityNINO, + initialCode: "QQ123456Z", + expectedCode: "QQ123456Z", + expectedError: "code: invalid NINO format.", + }, + { + name: "Validate invalid NINO - incorrect format", + idKey: gb.IdentityNINO, + initialCode: "A123456C", + expectedCode: "A123456C", + expectedError: "code: invalid NINO format.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := &org.Identity{ + Key: tt.idKey, + Code: cbc.Code(tt.initialCode), + } + + // Normalize the identifier + gb.Normalize(id) + + // Check if the normalized code is as expected + assert.Equal(t, tt.expectedCode, id.Code.String()) + + // Validate the identifier + err := gb.Validate(id) + + // Check if the error matches expected + if tt.expectedError != "" { + assert.Error(t, err) + assert.Equal(t, tt.expectedError, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/regimes/ie/examples/invoice-b2b.yaml b/regimes/ie/examples/invoice-b2b.yaml new file mode 100644 index 00000000..99e71fd9 --- /dev/null +++ b/regimes/ie/examples/invoice-b2b.yaml @@ -0,0 +1,47 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "12345678-abcd-12ef-ab12-1234567890ab" +issue_date: "2024-07-31" +series: "SAMPLE" +code: "001" + +supplier: + tax_id: + country: "IE" + code: "1X23456L" + name: "Test Company Ltd." + emails: + - addr: "company@example.com" + addresses: + - num: "12" + street: "Main Street" + locality: "Dublin" + code: "D02 X285" + country: "IE" + +customer: + tax_id: + country: "IE" + code: "8Y87654B" + name: "Random Company Ltd." + emails: + - addr: "random@example.com" + addresses: + - num: "45" + street: "Another Street" + locality: "Cork" + code: "T12 RHK3" + country: "IE" + +lines: + - quantity: 20 + item: + name: "Development services" + price: "90.00" + unit: "h" + discounts: + - percent: "10%" + reason: "Special discount" + taxes: + - cat: VAT + rate: "standard" + percent: "23.0%" diff --git a/regimes/ie/examples/out/invoice-b2b.json b/regimes/ie/examples/out/invoice-b2b.json new file mode 100644 index 00000000..0cb95fee --- /dev/null +++ b/regimes/ie/examples/out/invoice-b2b.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "9b72fe31-3b38-11ee-be56-0242ac120003", + "dig": { + "alg": "sha256", + "val": "b2dd44d6fa187b19c233f03305f3c1a0531a844a4312312842cfc548b9b49572" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "IE", + "uuid": "12345678-abcd-12ef-ab12-1234567890ab", + "type": "standard", + "series": "SAMPLE", + "code": "001", + "issue_date": "2024-07-31", + "currency": "EUR", + "supplier": { + "name": "Test Company Ltd.", + "tax_id": { + "country": "IE", + "code": "1X23456L" + }, + "addresses": [ + { + "num": "12", + "street": "Main Street", + "locality": "Dublin", + "code": "D02 X285", + "country": "IE" + } + ], + "emails": [ + { + "addr": "company@example.com" + } + ] + }, + "customer": { + "name": "Random Company Ltd.", + "tax_id": { + "country": "IE", + "code": "8Y87654B" + }, + "addresses": [ + { + "num": "45", + "street": "Another Street", + "locality": "Cork", + "code": "T12 RHK3", + "country": "IE" + } + ], + "emails": [ + { + "addr": "random@example.com" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services", + "price": "90.00", + "unit": "h" + }, + "sum": "1800.00", + "discounts": [ + { + "percent": "10%", + "amount": "180.00", + "reason": "Special discount" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "23.0%" + } + ], + "total": "1620.00" + } + ], + "totals": { + "sum": "1620.00", + "total": "1620.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "1620.00", + "percent": "23.0%", + "amount": "372.60" + } + ], + "amount": "372.60" + } + ], + "sum": "372.60" + }, + "tax": "372.60", + "total_with_tax": "1992.60", + "payable": "1992.60" + } + } +} \ No newline at end of file diff --git a/regimes/ie/ie.go b/regimes/ie/ie.go new file mode 100644 index 00000000..cdc42e84 --- /dev/null +++ b/regimes/ie/ie.go @@ -0,0 +1,61 @@ +// Package gb provides the United Kingdom tax regime. +package ie + +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 +func New() *tax.RegimeDef { + return &tax.RegimeDef{ + Country: "IE", + Currency: currency.EUR, + Name: i18n.String{ + i18n.EN: "Ireland", + }, + TimeZone: "Europe/Dublin", + Validator: Validate, + Normalizer: Normalize, + Scenarios: []*tax.ScenarioSet{ + common.InvoiceScenarios(), + }, + Tags: []*tax.TagSet{ + common.InvoiceTags(), + }, + Categories: taxCategories, + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + }, + }, + }, + } +} + +// Validate checks the document type and determines if it can be validated. +func Validate(doc interface{}) error { + switch obj := doc.(type) { + case *tax.Identity: + return validateTaxIdentity(obj) + } + return nil +} + +// Normalize will attempt to clean the object passed to it. +func Normalize(doc interface{}) { + switch obj := doc.(type) { + case *tax.Identity: + tax.NormalizeIdentity(obj) + } +} diff --git a/regimes/ie/tax_categories.go b/regimes/ie/tax_categories.go new file mode 100644 index 00000000..1a838d13 --- /dev/null +++ b/regimes/ie/tax_categories.go @@ -0,0 +1,94 @@ +package ie + +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{ + // + // VAT + // + { + Code: tax.CategoryVAT, + Name: i18n.String{ + i18n.EN: "VAT", + }, + Title: i18n.String{ + i18n.EN: "Value Added Tax", + }, + Retained: false, + Rates: []*tax.RateDef{ + { + Key: tax.RateZero, + Name: i18n.String{ + i18n.EN: "Zero Rate", + }, + Values: []*tax.RateValueDef{ + { + Percent: num.MakePercentage(0, 3), + }, + }, + }, + { + Key: tax.RateStandard, + Name: i18n.String{ + i18n.EN: "Standard Rate", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2021, 2, 28), + Percent: num.MakePercentage(23, 2), + }, + { + // Due to Covid + Since: cal.NewDate(2020, 9, 1), + Percent: num.MakePercentage(21, 2), + }, + { + Since: cal.NewDate(2012, 1, 1), + Percent: num.MakePercentage(23, 2), + }, + }, + }, + { + Key: tax.RateReduced, + Name: i18n.String{ + i18n.EN: "Reduced Rate", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2003, 1, 1), + Percent: num.MakePercentage(135, 3), + }, + }, + }, + { + Key: tax.RateSuperReduced, + Name: i18n.String{ + i18n.EN: "Reduced Rate", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2011, 7, 1), + Percent: num.MakePercentage(9, 2), + }, + }, + }, + { + Key: tax.RateSpecial, + Name: i18n.String{ + i18n.EN: "Reduced Rate", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2005, 1, 1), + Percent: num.MakePercentage(48, 3), + }, + }, + }, + }, + }, +} diff --git a/regimes/ie/tax_identity.go b/regimes/ie/tax_identity.go new file mode 100644 index 00000000..ab3c0ded --- /dev/null +++ b/regimes/ie/tax_identity.go @@ -0,0 +1,37 @@ +package ie + +import ( + "errors" + "regexp" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +// Source: https://github.com/ltns35/go-vat + +var ( + taxCodeRegexp = `^(\d{7}[A-Z]{1,2}|\d{1}[A-Z]{1}\d{5}[A-Z]{1})$` +) + +// validateTaxIdentity checks to ensure the NIT code looks okay. +func validateTaxIdentity(tID *tax.Identity) error { + return validation.ValidateStruct(tID, + validation.Field(&tID.Code, validation.By(validateTaxCode)), + ) +} + +func validateTaxCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + if !regexp.MustCompile(taxCodeRegexp).MatchString(val) { + return errors.New("invalid format") + } + + return nil +} diff --git a/regimes/ie/tax_identity_test.go b/regimes/ie/tax_identity_test.go new file mode 100644 index 00000000..c1630d65 --- /dev/null +++ b/regimes/ie/tax_identity_test.go @@ -0,0 +1,56 @@ +package ie_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/regimes/ie" + "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: "valid ind old-style", code: "1234567T"}, + {name: "valid ind new-style", code: "1234567TW"}, + {name: "valid company", code: "1A23456T"}, + { + name: "too many digits", + code: "123456789", + err: "invalid format", + }, + { + name: "too few digits", + code: "12345T", + err: "invalid format", + }, + { + name: "no digits", + code: "ABCDEFGH", + err: "invalid format", + }, + { + name: "lower case", + code: "1234567t", + err: "invalid format", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "IE", Code: tt.code} + err := ie.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/pl/identities.go b/regimes/pl/identities.go new file mode 100644 index 00000000..fbbbce92 --- /dev/null +++ b/regimes/pl/identities.go @@ -0,0 +1,69 @@ +package pl + +import ( + "errors" + "strconv" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +const ( + // IdentityKeyTaxNumber represents the Polish tax number (PESEL). It is not + // required for invoices, but can be included for identification purposes. + IdentityKeyTaxNumber cbc.Key = "pl-tax-number" +) + +// Reference: https://en.wikipedia.org/wiki/PESEL + +var identityKeyDefinitions = []*cbc.KeyDefinition{ + { + Key: IdentityKeyTaxNumber, + Name: i18n.String{ + i18n.EN: "Tax Number", + i18n.PL: "Numer podatkowy", + }, + }, +} + +func validateTaxNumber(id *org.Identity) error { + if id == nil || id.Key != IdentityKeyTaxNumber { + return nil + } + + return validation.ValidateStruct(id, + validation.Field(&id.Code, validation.By(validateTaxIdCode)), + ) +} + +func validateTaxIdCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + if len(val) != 11 { + return errors.New("length must be 11") + } + + multipliers := []int{1, 3, 7, 9} + sum := 0 + + // Loop through the first 10 digits + for i := 0; i < 10; i++ { + digit, _ := strconv.Atoi(string(val[i])) + sum += digit * multipliers[i%4] + } + + modulo := sum % 10 + lastDigit, _ := strconv.Atoi(string(val[10])) + + if (modulo == 0 && lastDigit == 0) || lastDigit == 10-modulo { + return nil + } + + return errors.New("invalid checksum") +} diff --git a/regimes/pl/identities_test.go b/regimes/pl/identities_test.go new file mode 100644 index 00000000..dc29de6b --- /dev/null +++ b/regimes/pl/identities_test.go @@ -0,0 +1,75 @@ +package pl_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/pl" + "github.com/stretchr/testify/assert" +) + +func TestValidateTaxNumber(t *testing.T) { + tests := []struct { + name string + identity *org.Identity + wantErr bool + }{ + { + name: "Valid PESEL", + identity: &org.Identity{ + Key: pl.IdentityKeyTaxNumber, + Code: cbc.Code("44051401359"), // Replace with an actual valid PESEL number + }, + wantErr: false, + }, + { + name: "Invalid length PESEL", + identity: &org.Identity{ + Key: pl.IdentityKeyTaxNumber, + Code: cbc.Code("1234567890"), // Invalid PESEL with less than 11 digits + }, + wantErr: true, + }, + { + name: "Invalid checksum PESEL", + identity: &org.Identity{ + Key: pl.IdentityKeyTaxNumber, + Code: cbc.Code("44051401358"), // Incorrect checksum + }, + wantErr: true, + }, + { + name: "Empty PESEL code", + identity: &org.Identity{ + Key: pl.IdentityKeyTaxNumber, + Code: cbc.Code(""), + }, + wantErr: false, + }, + { + name: "Wrong Key Identity", + identity: &org.Identity{ + Key: cbc.Key("wrong-key"), + Code: cbc.Code("44051401359"), + }, + wantErr: false, + }, + { + name: "Nil Identity", + identity: nil, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := pl.Validate(tt.identity) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/regimes/pl/pl.go b/regimes/pl/pl.go index de90b279..a231b994 100644 --- a/regimes/pl/pl.go +++ b/regimes/pl/pl.go @@ -6,6 +6,7 @@ import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -43,10 +44,11 @@ func New() *tax.RegimeDef { Tags: []*tax.TagSet{ common.InvoiceTags().Merge(invoiceTags), }, - Scenarios: scenarios, // scenarios.go - Validator: Validate, - Normalizer: Normalize, - Categories: taxCategories, // tax_categories.go + Scenarios: scenarios, // scenarios.go + IdentityKeys: identityKeyDefinitions, // identities.go + Validator: Validate, + Normalizer: Normalize, + Categories: taxCategories, // tax_categories.go Corrections: []*tax.CorrectionDefinition{ { Schema: bill.ShortSchemaInvoice, @@ -72,6 +74,8 @@ func Validate(doc interface{}) error { return validateTaxIdentity(obj) case *bill.Invoice: return validateInvoice(obj) + case *org.Identity: + return validateTaxNumber(obj) // case *pay.Instructions: // return validatePayInstructions(obj) // case *pay.Advance: diff --git a/regimes/regimes.go b/regimes/regimes.go index 8a3b49c4..11a40de9 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -16,6 +16,7 @@ import ( _ "github.com/invopop/gobl/regimes/fr" _ "github.com/invopop/gobl/regimes/gb" _ "github.com/invopop/gobl/regimes/gr" + _ "github.com/invopop/gobl/regimes/ie" _ "github.com/invopop/gobl/regimes/it" _ "github.com/invopop/gobl/regimes/mx" _ "github.com/invopop/gobl/regimes/nl" From 0dc7d7f06535cab9a9885b6a35e9885c52a71fa8 Mon Sep 17 00:00:00 2001 From: Menendez6 Date: Tue, 22 Oct 2024 15:51:17 +0000 Subject: [PATCH 2/7] Solving linting and referencing --- data/regimes/at.json | 9 ++ data/regimes/fr.json | 9 ++ data/regimes/gb.json | 14 +++ data/regimes/ie.json | 172 +++++++++++++++++++++++++++++++++ data/regimes/pl.json | 9 ++ data/schemas/bill/invoice.json | 4 + regimes/at/identitites.go | 6 +- regimes/fr/identities.go | 8 +- regimes/gb/identities.go | 3 + regimes/ie/ie.go | 2 +- regimes/ie/tax_identity.go | 2 + regimes/pl/identities.go | 4 +- 12 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 data/regimes/ie.json diff --git a/data/regimes/at.json b/data/regimes/at.json index 507d6468..527e0fd4 100644 --- a/data/regimes/at.json +++ b/data/regimes/at.json @@ -64,6 +64,15 @@ ] } ], + "identity_keys": [ + { + "key": "at-tax-number", + "name": { + "de": "Steuernummer", + "en": "Tax Number" + } + } + ], "scenarios": [ { "schema": "bill/invoice", diff --git a/data/regimes/fr.json b/data/regimes/fr.json index 4eb9fa60..8eac6ad1 100644 --- a/data/regimes/fr.json +++ b/data/regimes/fr.json @@ -68,6 +68,15 @@ ] } ], + "identity_keys": [ + { + "key": "fr-tax-number", + "name": { + "en": "Tax Number", + "fr": "Numéro fiscal de référence" + } + } + ], "scenarios": [ { "schema": "bill/invoice", diff --git a/data/regimes/gb.json b/data/regimes/gb.json index 898b5736..dc305fa7 100644 --- a/data/regimes/gb.json +++ b/data/regimes/gb.json @@ -68,6 +68,20 @@ ] } ], + "identity_keys": [ + { + "key": "gb-utr", + "name": { + "en": "Unique Taxpayer Reference" + } + }, + { + "key": "gb-nino", + "name": { + "en": "National Insurance Number" + } + } + ], "scenarios": [ { "schema": "bill/invoice", diff --git a/data/regimes/ie.json b/data/regimes/ie.json new file mode 100644 index 00000000..29751f87 --- /dev/null +++ b/data/regimes/ie.json @@ -0,0 +1,172 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime-def", + "name": { + "en": "Ireland" + }, + "time_zone": "Europe/Dublin", + "country": "IE", + "currency": "EUR", + "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: Customer to account for VAT to the relevant tax authority." + } + } + ] + } + ], + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note" + ] + } + ], + "categories": [ + { + "code": "VAT", + "name": { + "en": "VAT" + }, + "title": { + "en": "Value Added Tax" + }, + "rates": [ + { + "key": "zero", + "name": { + "en": "Zero Rate" + }, + "values": [ + { + "percent": "0.0%" + } + ] + }, + { + "key": "standard", + "name": { + "en": "Standard Rate" + }, + "values": [ + { + "since": "2021-02-28", + "percent": "23%" + }, + { + "since": "2020-09-01", + "percent": "21%" + }, + { + "since": "2012-01-01", + "percent": "23%" + } + ] + }, + { + "key": "reduced", + "name": { + "en": "Reduced Rate" + }, + "values": [ + { + "since": "2003-01-01", + "percent": "13.5%" + } + ] + }, + { + "key": "super-reduced", + "name": { + "en": "Reduced Rate" + }, + "values": [ + { + "since": "2011-07-01", + "percent": "9%" + } + ] + }, + { + "key": "special", + "name": { + "en": "Reduced Rate" + }, + "values": [ + { + "since": "2005-01-01", + "percent": "4.8%" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/data/regimes/pl.json b/data/regimes/pl.json index 4b9ff349..5e3212e5 100644 --- a/data/regimes/pl.json +++ b/data/regimes/pl.json @@ -165,6 +165,15 @@ ] } ], + "identity_keys": [ + { + "key": "pl-tax-number", + "name": { + "en": "Tax Number", + "pl": "Numer podatkowy" + } + } + ], "payment_means_keys": [ { "key": "cash", diff --git a/data/schemas/bill/invoice.json b/data/schemas/bill/invoice.json index 50122c1e..76d8b62a 100644 --- a/data/schemas/bill/invoice.json +++ b/data/schemas/bill/invoice.json @@ -217,6 +217,10 @@ "const": "GB", "title": "United Kingdom" }, + { + "const": "IE", + "title": "Ireland" + }, { "const": "IT", "title": "Italy" diff --git a/regimes/at/identitites.go b/regimes/at/identitites.go index f3440966..205d78ac 100644 --- a/regimes/at/identitites.go +++ b/regimes/at/identitites.go @@ -19,6 +19,8 @@ const ( IdentityKeyTaxNumber cbc.Key = "at-tax-number" ) +// https://www.oecd.org/content/dam/oecd/en/topics/policy-issue-focus/aeoi/austria-tin.pdf + var badCharsRegexPattern = regexp.MustCompile(`[^\d]`) var identityKeyDefinitions = []*cbc.KeyDefinition{ @@ -46,12 +48,12 @@ func validateTaxNumber(id *org.Identity) error { } return validation.ValidateStruct(id, - validation.Field(&id.Code, validation.By(validateTaxIdCode)), + validation.Field(&id.Code, validation.By(validateTaxIDCode)), ) } // validateAustrianTaxIdCode validates the normalized tax ID code. -func validateTaxIdCode(value interface{}) error { +func validateTaxIDCode(value interface{}) error { code, ok := value.(cbc.Code) if !ok || code == "" { return nil diff --git a/regimes/fr/identities.go b/regimes/fr/identities.go index 0ec43ebe..c685677a 100644 --- a/regimes/fr/identities.go +++ b/regimes/fr/identities.go @@ -16,6 +16,8 @@ const ( IdentityKeyTaxNumber cbc.Key = "fr-tax-number" ) +// https://www.oecd.org/content/dam/oecd/en/topics/policy-issue-focus/aeoi/france-tin.pdf + var badCharsRegexPattern = regexp.MustCompile(`[^\d]`) var identityKeyDefinitions = []*cbc.KeyDefinition{ @@ -35,12 +37,12 @@ func validateTaxNumber(id *org.Identity) error { } return validation.ValidateStruct(id, - validation.Field(&id.Code, validation.By(validateTaxIdCode)), + validation.Field(&id.Code, validation.By(validateTaxIDCode)), ) } -// validateTaxIdCode validates the normalized tax ID code. -func validateTaxIdCode(value interface{}) error { +// validateTaxIDCode validates the normalized tax ID code. +func validateTaxIDCode(value interface{}) error { code, ok := value.(cbc.Code) if !ok || code == "" { return nil diff --git a/regimes/gb/identities.go b/regimes/gb/identities.go index ec13ce3f..cffec6e5 100644 --- a/regimes/gb/identities.go +++ b/regimes/gb/identities.go @@ -23,6 +23,9 @@ var badCharsRegexPattern = regexp.MustCompile(`[^\d]`) var ninoPattern = `^[A-CEGHJ-PR-TW-Z]{2}\d{6}[A-D]$` var utrPattern = `^[1-9]\d{9}$` +// https://design.tax.service.gov.uk/hmrc-design-patterns/unique-taxpayer-reference/ +// https://www.gov.uk/hmrc-internal-manuals/national-insurance-manual/nim39110 + var identityKeyDefinitions = []*cbc.KeyDefinition{ { Key: IdentityUTR, diff --git a/regimes/ie/ie.go b/regimes/ie/ie.go index cdc42e84..db63f50b 100644 --- a/regimes/ie/ie.go +++ b/regimes/ie/ie.go @@ -1,4 +1,4 @@ -// Package gb provides the United Kingdom tax regime. +// Package ie provides the United Kingdom tax regime. package ie import ( diff --git a/regimes/ie/tax_identity.go b/regimes/ie/tax_identity.go index ab3c0ded..5838bef3 100644 --- a/regimes/ie/tax_identity.go +++ b/regimes/ie/tax_identity.go @@ -15,6 +15,8 @@ var ( taxCodeRegexp = `^(\d{7}[A-Z]{1,2}|\d{1}[A-Z]{1}\d{5}[A-Z]{1})$` ) +// https://euipo.europa.eu/tunnel-web/secure/webdav/guest/document_library/Documents/COSME/VAT%20numbers%20EU.pdf + // validateTaxIdentity checks to ensure the NIT code looks okay. func validateTaxIdentity(tID *tax.Identity) error { return validation.ValidateStruct(tID, diff --git a/regimes/pl/identities.go b/regimes/pl/identities.go index fbbbce92..cd3068e5 100644 --- a/regimes/pl/identities.go +++ b/regimes/pl/identities.go @@ -34,11 +34,11 @@ func validateTaxNumber(id *org.Identity) error { } return validation.ValidateStruct(id, - validation.Field(&id.Code, validation.By(validateTaxIdCode)), + validation.Field(&id.Code, validation.By(validateTaxIDCode)), ) } -func validateTaxIdCode(value interface{}) error { +func validateTaxIDCode(value interface{}) error { code, ok := value.(cbc.Code) if !ok || code == "" { return nil From dbe2d475f1b703800784920d318e283fa7dbb8ea Mon Sep 17 00:00:00 2001 From: Menendez6 Date: Tue, 22 Oct 2024 15:55:59 +0000 Subject: [PATCH 3/7] Solving test issues --- regimes/ie/examples/invoice-b2b.yaml | 47 ---------- regimes/ie/examples/out/invoice-b2b.json | 113 ----------------------- 2 files changed, 160 deletions(-) delete mode 100644 regimes/ie/examples/invoice-b2b.yaml delete mode 100644 regimes/ie/examples/out/invoice-b2b.json diff --git a/regimes/ie/examples/invoice-b2b.yaml b/regimes/ie/examples/invoice-b2b.yaml deleted file mode 100644 index 99e71fd9..00000000 --- a/regimes/ie/examples/invoice-b2b.yaml +++ /dev/null @@ -1,47 +0,0 @@ -$schema: "https://gobl.org/draft-0/bill/invoice" -uuid: "12345678-abcd-12ef-ab12-1234567890ab" -issue_date: "2024-07-31" -series: "SAMPLE" -code: "001" - -supplier: - tax_id: - country: "IE" - code: "1X23456L" - name: "Test Company Ltd." - emails: - - addr: "company@example.com" - addresses: - - num: "12" - street: "Main Street" - locality: "Dublin" - code: "D02 X285" - country: "IE" - -customer: - tax_id: - country: "IE" - code: "8Y87654B" - name: "Random Company Ltd." - emails: - - addr: "random@example.com" - addresses: - - num: "45" - street: "Another Street" - locality: "Cork" - code: "T12 RHK3" - country: "IE" - -lines: - - quantity: 20 - item: - name: "Development services" - price: "90.00" - unit: "h" - discounts: - - percent: "10%" - reason: "Special discount" - taxes: - - cat: VAT - rate: "standard" - percent: "23.0%" diff --git a/regimes/ie/examples/out/invoice-b2b.json b/regimes/ie/examples/out/invoice-b2b.json deleted file mode 100644 index 0cb95fee..00000000 --- a/regimes/ie/examples/out/invoice-b2b.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "$schema": "https://gobl.org/draft-0/envelope", - "head": { - "uuid": "9b72fe31-3b38-11ee-be56-0242ac120003", - "dig": { - "alg": "sha256", - "val": "b2dd44d6fa187b19c233f03305f3c1a0531a844a4312312842cfc548b9b49572" - } - }, - "doc": { - "$schema": "https://gobl.org/draft-0/bill/invoice", - "$regime": "IE", - "uuid": "12345678-abcd-12ef-ab12-1234567890ab", - "type": "standard", - "series": "SAMPLE", - "code": "001", - "issue_date": "2024-07-31", - "currency": "EUR", - "supplier": { - "name": "Test Company Ltd.", - "tax_id": { - "country": "IE", - "code": "1X23456L" - }, - "addresses": [ - { - "num": "12", - "street": "Main Street", - "locality": "Dublin", - "code": "D02 X285", - "country": "IE" - } - ], - "emails": [ - { - "addr": "company@example.com" - } - ] - }, - "customer": { - "name": "Random Company Ltd.", - "tax_id": { - "country": "IE", - "code": "8Y87654B" - }, - "addresses": [ - { - "num": "45", - "street": "Another Street", - "locality": "Cork", - "code": "T12 RHK3", - "country": "IE" - } - ], - "emails": [ - { - "addr": "random@example.com" - } - ] - }, - "lines": [ - { - "i": 1, - "quantity": "20", - "item": { - "name": "Development services", - "price": "90.00", - "unit": "h" - }, - "sum": "1800.00", - "discounts": [ - { - "percent": "10%", - "amount": "180.00", - "reason": "Special discount" - } - ], - "taxes": [ - { - "cat": "VAT", - "rate": "standard", - "percent": "23.0%" - } - ], - "total": "1620.00" - } - ], - "totals": { - "sum": "1620.00", - "total": "1620.00", - "taxes": { - "categories": [ - { - "code": "VAT", - "rates": [ - { - "key": "standard", - "base": "1620.00", - "percent": "23.0%", - "amount": "372.60" - } - ], - "amount": "372.60" - } - ], - "sum": "372.60" - }, - "tax": "372.60", - "total_with_tax": "1992.60", - "payable": "1992.60" - } - } -} \ No newline at end of file From 19193e436d89d31843695b8ed051a81cf7cde42c Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 22 Oct 2024 16:50:44 +0000 Subject: [PATCH 4/7] Making a quick proposal on possible approach in France with different code validation --- regimes/fr/fr.go | 22 +++------ regimes/fr/identities.go | 89 ++++++++++++++++------------------- regimes/fr/identities_test.go | 2 +- tax/regime_def.go | 10 ++-- 4 files changed, 53 insertions(+), 70 deletions(-) diff --git a/regimes/fr/fr.go b/regimes/fr/fr.go index 0dddb03a..cfdeed6d 100644 --- a/regimes/fr/fr.go +++ b/regimes/fr/fr.go @@ -12,16 +12,6 @@ import ( "github.com/invopop/gobl/tax" ) -// Identification keys used for additional codes not -// covered by the standard fields. -const ( - IdentityTypeSIREN cbc.Code = "SIREN" // SIREN is the main local tax code used in france, we use the normalized VAT version for the tax ID. - IdentityTypeSIRET cbc.Code = "SIRET" // SIRET number combines the SIREN with a branch number. - IdentityTypeRCS cbc.Code = "RCS" // Trade and Companies Register. - IdentityTypeRM cbc.Code = "RM" // Directory of Traders. - IdentityTypeNAF cbc.Code = "NAF" // Identifies the main branch of activity of the company or self-employed person. -) - func init() { tax.RegisterRegimeDef(New()) } @@ -57,10 +47,10 @@ func New() *tax.RegimeDef { }, }, }, - Validator: Validate, - Normalizer: Normalize, - Categories: taxCategories, - IdentityKeys: identityKeyDefinitions, // identities.go + Validator: Validate, + Normalizer: Normalize, + Categories: taxCategories, + IdentityTypes: identityTypeDefinitions, // identities.go } } @@ -72,7 +62,7 @@ func Validate(doc interface{}) error { case *tax.Identity: return validateTaxIdentity(obj) case *org.Identity: - return validateTaxNumber(obj) + return validateIdentity(obj) } return nil } @@ -83,6 +73,6 @@ func Normalize(doc any) { case *tax.Identity: normalizeTaxIdentity(obj) case *org.Identity: - normalizeTaxNumber(obj) + normalizeIdentity(obj) } } diff --git a/regimes/fr/identities.go b/regimes/fr/identities.go index c685677a..646f06f6 100644 --- a/regimes/fr/identities.go +++ b/regimes/fr/identities.go @@ -1,9 +1,7 @@ package fr import ( - "errors" "regexp" - "strconv" "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/i18n" @@ -11,69 +9,64 @@ import ( "github.com/invopop/validation" ) +// Identification keys used for additional codes not +// covered by the standard fields. const ( - // IdentityKeyTaxNumber represents the French tax reference number (numéro fiscal de référence). - IdentityKeyTaxNumber cbc.Key = "fr-tax-number" + IdentityTypeSIREN cbc.Code = "SIREN" // SIREN is the main local tax code used in france, we use the normalized VAT version for the tax ID. + IdentityTypeSIRET cbc.Code = "SIRET" // SIRET number combines the SIREN with a branch number. + IdentityTypeRCS cbc.Code = "RCS" // Trade and Companies Register. + IdentityTypeRM cbc.Code = "RM" // Directory of Traders. + IdentityTypeNAF cbc.Code = "NAF" // Identifies the main branch of activity of the company or self-employed person. + IdentityTypeSPI cbc.Code = "SPI" // Système de Pilotage des Indices + IdentityTypeNIF cbc.Code = "NIF" // Numéro d'identification fiscale (people) ) -// https://www.oecd.org/content/dam/oecd/en/topics/policy-issue-focus/aeoi/france-tin.pdf +var ( + identityTypeSPIPattern = regexp.MustCompile(`^[0-3]\d{12}$`) +) var badCharsRegexPattern = regexp.MustCompile(`[^\d]`) -var identityKeyDefinitions = []*cbc.KeyDefinition{ +var identityTypeDefinitions = []*cbc.ValueDefinition{ { - Key: IdentityKeyTaxNumber, + // https://www.oecd.org/content/dam/oecd/en/topics/policy-issue-focus/aeoi/france-tin.pdf + Value: IdentityTypeSPI.String(), Name: i18n.String{ - i18n.EN: "Tax Number", - i18n.FR: "Numéro fiscal de référence", + i18n.EN: "Index Steering System", + i18n.FR: "Système de Pilotage des Indices", }, }, } -// validateTaxNumber validates the French tax reference number. -func validateTaxNumber(id *org.Identity) error { - if id == nil || id.Key != IdentityKeyTaxNumber { - return nil +func normalizeIdentity(id *org.Identity) { + if id == nil { + return } + switch id.Type { + case IdentityTypeSPI: + code := id.Code.String() + code = badCharsRegexPattern.ReplaceAllString(code, "") + id.Code = cbc.Code(code) + } +} +// validateIdentity performs basic validation checks on identities provided. +func validateIdentity(id *org.Identity) error { return validation.ValidateStruct(id, - validation.Field(&id.Code, validation.By(validateTaxIDCode)), + validation.Field(&id.Code, + validation.By(identityValidator(id.Type)), + validation.Skip, + ), ) } -// validateTaxIDCode validates the normalized tax ID code. -func validateTaxIDCode(value interface{}) error { - code, ok := value.(cbc.Code) - if !ok || code == "" { - return nil - } - val := code.String() - - // Check length - if len(val) != 13 { - return errors.New("length must be 13 digits") - } - - // Check that all characters are digits - if _, err := strconv.Atoi(val); err != nil { - return errors.New("must contain only digits") - } - - // Check that the first digit is 0, 1, 2, or 3 - firstDigit := val[0] - if firstDigit < '0' || firstDigit > '3' { - return errors.New("first digit must be 0, 1, 2, or 3") - } - - return nil -} - -// normalizeTaxNumber removes any non-digit characters from the tax number. -func normalizeTaxNumber(id *org.Identity) { - if id == nil || id.Key != IdentityKeyTaxNumber { - return +func identityValidator(typ cbc.Code) validation.RuleFunc { + return func(value interface{}) error { + switch typ { + case IdentityTypeSPI: + return validation.Validate(value, validation.Match(identityTypeSPIPattern)) + default: + return nil + } } - code := id.Code.String() - code = badCharsRegexPattern.ReplaceAllString(code, "") - id.Code = cbc.Code(code) } diff --git a/regimes/fr/identities_test.go b/regimes/fr/identities_test.go index 6903a03e..60022b74 100644 --- a/regimes/fr/identities_test.go +++ b/regimes/fr/identities_test.go @@ -70,7 +70,7 @@ func TestNormalizeAndValidateTaxNumber(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { identity := &org.Identity{ - Key: fr.IdentityKeyTaxNumber, + Type: fr.IdentityTypeSPI, Code: cbc.Code(tt.input), } diff --git a/tax/regime_def.go b/tax/regime_def.go index 90273dbe..ae68478b 100644 --- a/tax/regime_def.go +++ b/tax/regime_def.go @@ -61,14 +61,14 @@ type RegimeDef struct { // Typically these are used to define local codes for suppliers, customers, products, or tax rates. Extensions []*cbc.KeyDefinition `json:"extensions,omitempty" jsonschema:"title=Extensions"` - // Tax Identity types specific for the regime and may be validated - // against. - TaxIdentityTypeKeys []*cbc.KeyDefinition `json:"tax_identity_type_keys,omitempty" jsonschema:"title=Tax Identity Type Keys"` - // Identity keys used in addition to regular tax identities and specific for the // regime that may be validated against. IdentityKeys []*cbc.KeyDefinition `json:"identity_keys,omitempty" jsonschema:"title=Identity Keys"` + // Identity Types are used as an alternative to Identity Keys when there is a clear local + // definition of a specific code. + IdentityTypes []*cbc.ValueDefinition `json:"identity_types,omitempty" :jsonschema:"title=Identity Types"` + // Charge keys specific for the regime and may be validated or used in the UI as suggestions ChargeKeys []*cbc.KeyDefinition `json:"charge_keys,omitempty" jsonschema:"title=Charge Keys"` @@ -282,8 +282,8 @@ func (r *RegimeDef) ValidateWithContext(ctx context.Context) error { validation.Field(&r.Zone), validation.Field(&r.Currency), validation.Field(&r.Tags), - validation.Field(&r.TaxIdentityTypeKeys), validation.Field(&r.IdentityKeys), + validation.Field(&r.IdentityTypes), validation.Field(&r.Extensions), validation.Field(&r.ChargeKeys), validation.Field(&r.PaymentMeansKeys), From 2905228055a5473bb6f5851188fa4fc1c0497bdf Mon Sep 17 00:00:00 2001 From: Menendez6 Date: Tue, 22 Oct 2024 17:26:26 +0000 Subject: [PATCH 5/7] Modifying keys for types --- data/regimes/fr.json | 8 +++---- data/regimes/gb.json | 6 +++--- data/regimes/pl.json | 4 ++-- data/schemas/tax/regime-def.json | 13 ++++++------ regimes/fr/identities.go | 1 + regimes/gb/gb.go | 8 +++---- regimes/gb/identities.go | 36 ++++++++++++++++---------------- regimes/gb/identities_test.go | 18 ++++++++-------- regimes/pl/identities.go | 10 ++++----- regimes/pl/identities_test.go | 12 +++++------ regimes/pl/pl.go | 10 ++++----- 11 files changed, 63 insertions(+), 63 deletions(-) diff --git a/data/regimes/fr.json b/data/regimes/fr.json index 8eac6ad1..9437f651 100644 --- a/data/regimes/fr.json +++ b/data/regimes/fr.json @@ -68,12 +68,12 @@ ] } ], - "identity_keys": [ + "identity_types": [ { - "key": "fr-tax-number", + "value": "SPI", "name": { - "en": "Tax Number", - "fr": "Numéro fiscal de référence" + "en": "Index Steering System", + "fr": "Système de Pilotage des Indices" } } ], diff --git a/data/regimes/gb.json b/data/regimes/gb.json index dc305fa7..731e686d 100644 --- a/data/regimes/gb.json +++ b/data/regimes/gb.json @@ -68,15 +68,15 @@ ] } ], - "identity_keys": [ + "identity_types": [ { - "key": "gb-utr", + "value": "UTR", "name": { "en": "Unique Taxpayer Reference" } }, { - "key": "gb-nino", + "value": "NINO", "name": { "en": "National Insurance Number" } diff --git a/data/regimes/pl.json b/data/regimes/pl.json index 5e3212e5..8bdd8b53 100644 --- a/data/regimes/pl.json +++ b/data/regimes/pl.json @@ -165,9 +165,9 @@ ] } ], - "identity_keys": [ + "identity_types": [ { - "key": "pl-tax-number", + "value": "PESEL", "name": { "en": "Tax Number", "pl": "Numer podatkowy" diff --git a/data/schemas/tax/regime-def.json b/data/schemas/tax/regime-def.json index 6c05cf62..cec3a3e9 100644 --- a/data/schemas/tax/regime-def.json +++ b/data/schemas/tax/regime-def.json @@ -278,21 +278,20 @@ "title": "Extensions", "description": "Extensions defines the keys that can be used for extended or extra data inside the regime that\nis specific to the regime and cannot be easily determined from other GOBL structures.\nTypically these are used to define local codes for suppliers, customers, products, or tax rates." }, - "tax_identity_type_keys": { + "identity_keys": { "items": { "$ref": "https://gobl.org/draft-0/cbc/key-definition" }, "type": "array", - "title": "Tax Identity Type Keys", - "description": "Tax Identity types specific for the regime and may be validated\nagainst." + "title": "Identity Keys", + "description": "Identity keys used in addition to regular tax identities and specific for the\nregime that may be validated against." }, - "identity_keys": { + "identity_types": { "items": { - "$ref": "https://gobl.org/draft-0/cbc/key-definition" + "$ref": "https://gobl.org/draft-0/cbc/value-definition" }, "type": "array", - "title": "Identity Keys", - "description": "Identity keys used in addition to regular tax identities and specific for the\nregime that may be validated against." + "description": "Identity Types are used as an alternative to Identity Keys when there is a clear local\ndefinition of a specific code." }, "charge_keys": { "items": { diff --git a/regimes/fr/identities.go b/regimes/fr/identities.go index 646f06f6..03ba1fc4 100644 --- a/regimes/fr/identities.go +++ b/regimes/fr/identities.go @@ -65,6 +65,7 @@ func identityValidator(typ cbc.Code) validation.RuleFunc { switch typ { case IdentityTypeSPI: return validation.Validate(value, validation.Match(identityTypeSPIPattern)) + //TODO: Add the other types default: return nil } diff --git a/regimes/gb/gb.go b/regimes/gb/gb.go index 8ce75de7..ea1d3bc7 100644 --- a/regimes/gb/gb.go +++ b/regimes/gb/gb.go @@ -46,8 +46,8 @@ func New() *tax.RegimeDef { Tags: []*tax.TagSet{ common.InvoiceTags(), }, - Categories: taxCategories, - IdentityKeys: identityKeyDefinitions, + Categories: taxCategories, + IdentityTypes: identityTypeDefinitions, Corrections: []*tax.CorrectionDefinition{ { Schema: bill.ShortSchemaInvoice, @@ -66,7 +66,7 @@ func Validate(doc interface{}) error { case *tax.Identity: return validateTaxIdentity(obj) case *org.Identity: - return validateTaxNumber(obj) + return validateIdentity(obj) } return nil } @@ -77,6 +77,6 @@ func Normalize(doc interface{}) { case *tax.Identity: tax.NormalizeIdentity(obj, altCountryCodes...) case *org.Identity: - normalizeTaxNumber(obj) + normalizeIdentity(obj) } } diff --git a/regimes/gb/identities.go b/regimes/gb/identities.go index cffec6e5..fbe0b78e 100644 --- a/regimes/gb/identities.go +++ b/regimes/gb/identities.go @@ -14,9 +14,9 @@ import ( const ( // IdentityUTR represents the UK Unique Taxpayer Reference (UTR). - IdentityUTR cbc.Key = "gb-utr" + IdentityTypeUTR cbc.Code = "UTR" // IdentityNINO represents the UK National Insurance Number (NINO). - IdentityNINO cbc.Key = "gb-nino" + IdentityTypeNINO cbc.Code = "NINO" ) var badCharsRegexPattern = regexp.MustCompile(`[^\d]`) @@ -26,31 +26,31 @@ var utrPattern = `^[1-9]\d{9}$` // https://design.tax.service.gov.uk/hmrc-design-patterns/unique-taxpayer-reference/ // https://www.gov.uk/hmrc-internal-manuals/national-insurance-manual/nim39110 -var identityKeyDefinitions = []*cbc.KeyDefinition{ +var identityTypeDefinitions = []*cbc.ValueDefinition{ { - Key: IdentityUTR, + Value: IdentityTypeUTR.String(), Name: i18n.String{ i18n.EN: "Unique Taxpayer Reference", }, }, { - Key: IdentityNINO, + Value: IdentityTypeNINO.String(), Name: i18n.String{ i18n.EN: "National Insurance Number", }, }, } -func normalizeTaxNumber(id *org.Identity) { - if id == nil || (id.Key != IdentityUTR && id.Key != IdentityNINO) { +func normalizeIdentity(id *org.Identity) { + if id == nil || (id.Type != IdentityTypeUTR && id.Type != IdentityTypeNINO) { return } - if id.Key == IdentityUTR { + if id.Type == IdentityTypeUTR { code := id.Code.String() code = badCharsRegexPattern.ReplaceAllString(code, "") id.Code = cbc.Code(code) - } else if id.Key == IdentityNINO { + } else if id.Type == IdentityTypeNINO { code := id.Code.String() code = strings.ToUpper(code) code = tax.IdentityCodeBadCharsRegexp.ReplaceAllString(code, "") @@ -58,26 +58,26 @@ func normalizeTaxNumber(id *org.Identity) { } } -func validateTaxNumber(id *org.Identity) error { +func validateIdentity(id *org.Identity) error { if id == nil { return nil } - if id.Key == IdentityNINO { + if id.Type == IdentityTypeNINO { return validation.ValidateStruct(id, - validation.Field(&id.Code, validation.By(validateNinoCode)), + validation.Field(&id.Code, validation.By(validateNino)), ) - } else if id.Key == IdentityUTR { + } else if id.Type == IdentityTypeUTR { return validation.ValidateStruct(id, - validation.Field(&id.Code, validation.By(validateUtrCode)), + validation.Field(&id.Code, validation.By(validateUtr)), ) } return nil } -// validateUtrCode validates the normalized Unique Taxpayer Reference (UTR). -func validateUtrCode(value interface{}) error { +// validateUtr validates the normalized Unique Taxpayer Reference (UTR). +func validateUtr(value interface{}) error { code, ok := value.(cbc.Code) if !ok || code == "" { return nil @@ -98,8 +98,8 @@ func validateUtrCode(value interface{}) error { return nil } -// validateNinoCode validates the normalized National Insurance Number (NINO). -func validateNinoCode(value interface{}) error { +// validateNino validates the normalized National Insurance Number (NINO). +func validateNino(value interface{}) error { code, ok := value.(cbc.Code) if !ok || code == "" { return nil diff --git a/regimes/gb/identities_test.go b/regimes/gb/identities_test.go index 6349f170..4634f9a1 100644 --- a/regimes/gb/identities_test.go +++ b/regimes/gb/identities_test.go @@ -12,56 +12,56 @@ import ( func TestUKIdentifiers(t *testing.T) { tests := []struct { name string - idKey cbc.Key + idCode cbc.Code initialCode string expectedCode string expectedError string }{ { name: "Normalize UTR - spaces removed", - idKey: gb.IdentityUTR, + idCode: gb.IdentityTypeUTR, initialCode: " 1234567890 ", expectedCode: "1234567890", expectedError: "", }, { name: "Validate valid UTR", - idKey: gb.IdentityUTR, + idCode: gb.IdentityTypeUTR, initialCode: "1234567890", expectedCode: "1234567890", expectedError: "", }, { name: "Validate invalid UTR - starts with 0", - idKey: gb.IdentityUTR, + idCode: gb.IdentityTypeUTR, initialCode: "0234567890", expectedCode: "0234567890", expectedError: "code: invalid UTR format.", }, { name: "Normalize NINO - to uppercase", - idKey: gb.IdentityNINO, + idCode: gb.IdentityTypeNINO, initialCode: "ab123456c", expectedCode: "AB123456C", expectedError: "", }, { name: "Validate valid NINO", - idKey: gb.IdentityNINO, + idCode: gb.IdentityTypeNINO, initialCode: "AB123456C", expectedCode: "AB123456C", expectedError: "", }, { name: "Validate invalid NINO - disallowed prefix", - idKey: gb.IdentityNINO, + idCode: gb.IdentityTypeNINO, initialCode: "QQ123456Z", expectedCode: "QQ123456Z", expectedError: "code: invalid NINO format.", }, { name: "Validate invalid NINO - incorrect format", - idKey: gb.IdentityNINO, + idCode: gb.IdentityTypeNINO, initialCode: "A123456C", expectedCode: "A123456C", expectedError: "code: invalid NINO format.", @@ -71,7 +71,7 @@ func TestUKIdentifiers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { id := &org.Identity{ - Key: tt.idKey, + Type: tt.idCode, Code: cbc.Code(tt.initialCode), } diff --git a/regimes/pl/identities.go b/regimes/pl/identities.go index cd3068e5..f66b08c8 100644 --- a/regimes/pl/identities.go +++ b/regimes/pl/identities.go @@ -11,16 +11,16 @@ import ( ) const ( - // IdentityKeyTaxNumber represents the Polish tax number (PESEL). It is not + // IdentityTypePESEL represents the Polish tax number (PESEL). It is not // required for invoices, but can be included for identification purposes. - IdentityKeyTaxNumber cbc.Key = "pl-tax-number" + IdentityTypePESEL cbc.Code = "PESEL" ) // Reference: https://en.wikipedia.org/wiki/PESEL -var identityKeyDefinitions = []*cbc.KeyDefinition{ +var identityTypeDefinitions = []*cbc.ValueDefinition{ { - Key: IdentityKeyTaxNumber, + Value: IdentityTypePESEL.String(), Name: i18n.String{ i18n.EN: "Tax Number", i18n.PL: "Numer podatkowy", @@ -29,7 +29,7 @@ var identityKeyDefinitions = []*cbc.KeyDefinition{ } func validateTaxNumber(id *org.Identity) error { - if id == nil || id.Key != IdentityKeyTaxNumber { + if id == nil || id.Type != IdentityTypePESEL { return nil } diff --git a/regimes/pl/identities_test.go b/regimes/pl/identities_test.go index dc29de6b..33afe9da 100644 --- a/regimes/pl/identities_test.go +++ b/regimes/pl/identities_test.go @@ -18,7 +18,7 @@ func TestValidateTaxNumber(t *testing.T) { { name: "Valid PESEL", identity: &org.Identity{ - Key: pl.IdentityKeyTaxNumber, + Type: pl.IdentityTypePESEL, Code: cbc.Code("44051401359"), // Replace with an actual valid PESEL number }, wantErr: false, @@ -26,7 +26,7 @@ func TestValidateTaxNumber(t *testing.T) { { name: "Invalid length PESEL", identity: &org.Identity{ - Key: pl.IdentityKeyTaxNumber, + Type: pl.IdentityTypePESEL, Code: cbc.Code("1234567890"), // Invalid PESEL with less than 11 digits }, wantErr: true, @@ -34,7 +34,7 @@ func TestValidateTaxNumber(t *testing.T) { { name: "Invalid checksum PESEL", identity: &org.Identity{ - Key: pl.IdentityKeyTaxNumber, + Type: pl.IdentityTypePESEL, Code: cbc.Code("44051401358"), // Incorrect checksum }, wantErr: true, @@ -42,15 +42,15 @@ func TestValidateTaxNumber(t *testing.T) { { name: "Empty PESEL code", identity: &org.Identity{ - Key: pl.IdentityKeyTaxNumber, + Type: pl.IdentityTypePESEL, Code: cbc.Code(""), }, wantErr: false, }, { - name: "Wrong Key Identity", + name: "Wrong Code Identity", identity: &org.Identity{ - Key: cbc.Key("wrong-key"), + Type: cbc.Code("NINO"), Code: cbc.Code("44051401359"), }, wantErr: false, diff --git a/regimes/pl/pl.go b/regimes/pl/pl.go index a231b994..4504f68b 100644 --- a/regimes/pl/pl.go +++ b/regimes/pl/pl.go @@ -44,11 +44,11 @@ func New() *tax.RegimeDef { Tags: []*tax.TagSet{ common.InvoiceTags().Merge(invoiceTags), }, - Scenarios: scenarios, // scenarios.go - IdentityKeys: identityKeyDefinitions, // identities.go - Validator: Validate, - Normalizer: Normalize, - Categories: taxCategories, // tax_categories.go + Scenarios: scenarios, // scenarios.go + IdentityTypes: identityTypeDefinitions, // identities.go + Validator: Validate, + Normalizer: Normalize, + Categories: taxCategories, // tax_categories.go Corrections: []*tax.CorrectionDefinition{ { Schema: bill.ShortSchemaInvoice, From 9fa2bfeee6d438a75e5362c15a3cb037a2bd8299 Mon Sep 17 00:00:00 2001 From: Menendez6 Date: Tue, 22 Oct 2024 17:29:43 +0000 Subject: [PATCH 6/7] Linting --- regimes/gb/identities.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regimes/gb/identities.go b/regimes/gb/identities.go index fbe0b78e..24b678ae 100644 --- a/regimes/gb/identities.go +++ b/regimes/gb/identities.go @@ -13,9 +13,9 @@ import ( ) const ( - // IdentityUTR represents the UK Unique Taxpayer Reference (UTR). + // IdentityTypeUTR represents the UK Unique Taxpayer Reference (UTR). IdentityTypeUTR cbc.Code = "UTR" - // IdentityNINO represents the UK National Insurance Number (NINO). + // IdentityTypeNINO represents the UK National Insurance Number (NINO). IdentityTypeNINO cbc.Code = "NINO" ) From 05c13b6ee4bfb8b770f4238d383fbcca628d0799 Mon Sep 17 00:00:00 2001 From: Menendez6 Date: Tue, 22 Oct 2024 17:35:49 +0000 Subject: [PATCH 7/7] Linting issue IdentityTypes --- tax/regime_def.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tax/regime_def.go b/tax/regime_def.go index ae68478b..1e95df29 100644 --- a/tax/regime_def.go +++ b/tax/regime_def.go @@ -67,7 +67,7 @@ type RegimeDef struct { // Identity Types are used as an alternative to Identity Keys when there is a clear local // definition of a specific code. - IdentityTypes []*cbc.ValueDefinition `json:"identity_types,omitempty" :jsonschema:"title=Identity Types"` + IdentityTypes []*cbc.ValueDefinition `json:"identity_types,omitempty" jsonschema:"title=Identity Types"` // Charge keys specific for the regime and may be validated or used in the UI as suggestions ChargeKeys []*cbc.KeyDefinition `json:"charge_keys,omitempty" jsonschema:"title=Charge Keys"`