Skip to content

Commit

Permalink
Adding TIN algorithms
Browse files Browse the repository at this point in the history
  • Loading branch information
Menendez6 committed Oct 22, 2024
1 parent 74c80f7 commit 6dac3b5
Show file tree
Hide file tree
Showing 19 changed files with 1,167 additions and 9 deletions.
9 changes: 8 additions & 1 deletion regimes/at/at.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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,
Expand All @@ -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
}

Expand All @@ -59,5 +64,7 @@ func Normalize(doc any) {
switch obj := doc.(type) {
case *tax.Identity:
tax.NormalizeIdentity(obj)
case *org.Identity:
normalizeTaxNumber(obj)
}
}
85 changes: 85 additions & 0 deletions regimes/at/identities_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
80 changes: 80 additions & 0 deletions regimes/at/identitites.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 9 additions & 3 deletions regimes/fr/fr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -56,9 +57,10 @@ func New() *tax.RegimeDef {
},
},
},
Validator: Validate,
Normalizer: Normalize,
Categories: taxCategories,
Validator: Validate,
Normalizer: Normalize,
Categories: taxCategories,
IdentityKeys: identityKeyDefinitions, // identities.go
}
}

Expand All @@ -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
}
Expand All @@ -78,5 +82,7 @@ func Normalize(doc any) {
switch obj := doc.(type) {
case *tax.Identity:
normalizeTaxIdentity(obj)
case *org.Identity:
normalizeTaxNumber(obj)
}
}
77 changes: 77 additions & 0 deletions regimes/fr/identities.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 6dac3b5

Please sign in to comment.