Skip to content

Commit

Permalink
Automatically determine Italian ID type
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Jan 25, 2024
1 parent beb799f commit 28a2b2a
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 15 deletions.
6 changes: 3 additions & 3 deletions regimes/it/invoice_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,12 @@ func (v *invoiceValidator) customer(value interface{}) error {
return nil
}

// Customers must have a tax ID (PartitaIVA) if they are Italian legal entities
// like government offices and companies.
// Customers must have a tax ID (PartitaIVA)
return validation.ValidateStruct(customer,
validation.Field(&customer.TaxID,
validation.Required,
validation.When(
isItalianParty(customer),
validation.Required,
tax.RequireIdentityCode,
tax.IdentityTypeIn(
TaxIdentityTypeBusiness,
Expand All @@ -95,6 +94,7 @@ func (v *invoiceValidator) customer(value interface{}) error {
validation.Field(&customer.Addresses,
validation.When(
isItalianParty(customer),
// TODO: address not required for simplified invoices
validation.By(validateAddress),
),
),
Expand Down
4 changes: 4 additions & 0 deletions regimes/it/invoice_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func TestCustomerValidation(t *testing.T) {
require.NoError(t, inv.Calculate())
require.NoError(t, inv.Validate())

inv.Customer.TaxID = nil
err := inv.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "customer: (tax_id: cannot be blank.)")
}

func TestSupplierValidation(t *testing.T) {
Expand Down
42 changes: 30 additions & 12 deletions regimes/it/tax_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ var taxIdentityTypeDefinitions = []*tax.KeyDefinition{
},
}

const (
taxIDEvenChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
taxIDOddChars = "BAKPLCQDREVOSFTGUHMINJWZYX"
taxIDCharCode = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
taxIDCRCMod = 26
)

// source http://blog.marketto.it/2016/01/regex-validazione-codice-fiscale-con-omocodia/
var taxIDPersonRegexPattern = regexp.MustCompile(`^(?:[A-Z][AEIOU][AEIOUX]|[AEIOU]X{2}|[B-DF-HJ-NP-TV-Z]{2}[A-Z]){2}(?:[\dLMNP-V]{2}(?:[A-EHLMPR-T](?:[04LQ][1-9MNP-V]|[15MR][\dLMNP-V]|[26NS][0-8LMNP-U])|[DHPS][37PT][0L]|[ACELMRT][37PT][01LM]|[AC-EHLMPR-T][26NS][9V])|(?:[02468LNQSU][048LQU]|[13579MPRTV][26NS])B[26NS][9V])(?:[A-MZ][1-9MNP-V][\dLMNP-V]{2}|[A-M][0L](?:[1-9MNP-V][\dLMNP-V]|[0L][1-9MNP-V]))[A-Z]$`)

// validateTaxIdentity performs checks on the tax codes according to the type
// that was set. Additional validation is laid out at the invoice layer.
func validateTaxIdentity(tID *tax.Identity) error {
Expand All @@ -67,7 +77,24 @@ func validateTaxIdentity(tID *tax.Identity) error {
// normalizeTaxIdentity removes any whitespace or separation characters and ensures all letters are
// uppercase.
func normalizeTaxIdentity(tID *tax.Identity) error {
return common.NormalizeTaxIdentity(tID)
if err := common.NormalizeTaxIdentity(tID); err != nil {
return err
}

// try to determine the type automatically
if tID.Type == cbc.KeyEmpty {
if tID.Code == "" {
return nil
}
// note that we don't yet have a way to determine government codes
if taxIDPersonRegexPattern.MatchString(tID.Code.String()) {
tID.Type = TaxIdentityTypeIndividual
} else {
tID.Type = TaxIdentityTypeBusiness
}
}

return nil
}

// source: https://it.wikipedia.org/wiki/Partita_IVA#Struttura_del_codice_identificativo_di_partita_IVA
Expand All @@ -78,6 +105,7 @@ func validateTaxCode(value interface{}) error {
}
str := code.String()

// Check code is just numbers
for _, v := range str {
x := v - 48
if x < 0 || x > 9 {
Expand All @@ -97,16 +125,6 @@ func validateTaxCode(value interface{}) error {
return nil
}

const (
taxIDEvenChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
taxIDOddChars = "BAKPLCQDREVOSFTGUHMINJWZYX"
taxIDCharCode = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
taxIDCRCMod = 26
)

// source http://blog.marketto.it/2016/01/regex-validazione-codice-fiscale-con-omocodia/
var taxIDRegexPattern = regexp.MustCompile(`^(?:[A-Z][AEIOU][AEIOUX]|[AEIOU]X{2}|[B-DF-HJ-NP-TV-Z]{2}[A-Z]){2}(?:[\dLMNP-V]{2}(?:[A-EHLMPR-T](?:[04LQ][1-9MNP-V]|[15MR][\dLMNP-V]|[26NS][0-8LMNP-U])|[DHPS][37PT][0L]|[ACELMRT][37PT][01LM]|[AC-EHLMPR-T][26NS][9V])|(?:[02468LNQSU][048LQU]|[13579MPRTV][26NS])B[26NS][9V])(?:[A-MZ][1-9MNP-V][\dLMNP-V]{2}|[A-M][0L](?:[1-9MNP-V][\dLMNP-V]|[0L][1-9MNP-V]))[A-Z]$`)

// Based on details at https://en.wikipedia.org/wiki/Italian_fiscal_code
func validateIndividualTaxCode(value interface{}) error {
val, ok := value.(cbc.Code)
Expand All @@ -115,7 +133,7 @@ func validateIndividualTaxCode(value interface{}) error {
}
code := val.String()

matched := taxIDRegexPattern.MatchString(code)
matched := taxIDPersonRegexPattern.MatchString(code)
if !matched {
return errors.New("invalid format")
}
Expand Down
16 changes: 16 additions & 0 deletions regimes/it/tax_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,45 @@ func TestNormalizeTaxIdentity(t *testing.T) {
tests := []struct {
code cbc.Code
expected cbc.Code
typ cbc.Key
}{
{
code: "12345678901",
expected: "12345678901",
typ: it.TaxIdentityTypeBusiness,
},
{
code: "123-456-789-01",
expected: "12345678901",
typ: it.TaxIdentityTypeBusiness,
},
{
code: "123456 789 01",
expected: "12345678901",
typ: it.TaxIdentityTypeBusiness,
},
{
code: "IT 12345678901",
expected: "12345678901",
typ: it.TaxIdentityTypeBusiness,
},
{
code: "RSSMRA74D22A001Q",
expected: "RSSMRA74D22A001Q",
typ: it.TaxIdentityTypeIndividual,
},
{
code: " RSS-MRA 74D22 A00 1Q ",
expected: "RSSMRA74D22A001Q",
typ: it.TaxIdentityTypeIndividual,
},
}
for _, ts := range tests {
tID := &tax.Identity{Country: l10n.IT, Code: ts.code}
err := it.Calculate(tID)
assert.NoError(t, err)
assert.Equal(t, ts.expected, tID.Code)
assert.Equal(t, ts.typ, tID.Type)
}
}

Expand Down

0 comments on commit 28a2b2a

Please sign in to comment.