Skip to content

Commit

Permalink
Making a quick proposal on possible approach in France with different…
Browse files Browse the repository at this point in the history
… code validation
  • Loading branch information
samlown committed Oct 22, 2024
1 parent dbe2d47 commit 19193e4
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 70 deletions.
22 changes: 6 additions & 16 deletions regimes/fr/fr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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
}
Expand All @@ -83,6 +73,6 @@ func Normalize(doc any) {
case *tax.Identity:
normalizeTaxIdentity(obj)
case *org.Identity:
normalizeTaxNumber(obj)
normalizeIdentity(obj)
}
}
89 changes: 41 additions & 48 deletions regimes/fr/identities.go
Original file line number Diff line number Diff line change
@@ -1,79 +1,72 @@
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"
)

// 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
}

Check warning on line 44 in regimes/fr/identities.go

View check run for this annotation

Codecov / codecov/patch

regimes/fr/identities.go#L43-L44

Added lines #L43 - L44 were not covered by tests
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

Check warning on line 69 in regimes/fr/identities.go

View check run for this annotation

Codecov / codecov/patch

regimes/fr/identities.go#L68-L69

Added lines #L68 - L69 were not covered by tests
}
}
code := id.Code.String()
code = badCharsRegexPattern.ReplaceAllString(code, "")
id.Code = cbc.Code(code)
}
2 changes: 1 addition & 1 deletion regimes/fr/identities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}

Expand Down
10 changes: 5 additions & 5 deletions tax/regime_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Check failure on line 70 in tax/regime_def.go

View workflow job for this annotation

GitHub Actions / golangci-lint

structtag: struct field tag `json:"identity_types,omitempty" :jsonschema:"title=Identity Types"` not compatible with reflect.StructTag.Get: bad syntax for struct tag key (govet)

// 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"`

Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit 19193e4

Please sign in to comment.