Skip to content

Commit

Permalink
Adding PL tax code tests
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Dec 7, 2023
1 parent e319db8 commit 3ffa46a
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 76 deletions.
4 changes: 3 additions & 1 deletion regimes/nl/nl.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func New() *tax.Regime {
// 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)
case *bill.Invoice:
return validateInvoice(obj)
}
Expand All @@ -102,7 +104,7 @@ func Validate(doc interface{}) error {
func Calculate(doc interface{}) error {
switch obj := doc.(type) {
case *tax.Identity:
return NormalizeTaxIdentity(obj)
return common.NormalizeTaxIdentity(obj)
}
return nil
}
14 changes: 1 addition & 13 deletions regimes/nl/tax_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/invopop/validation"

"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/regimes/common"
"github.com/invopop/gobl/tax"
)

Expand All @@ -18,11 +17,7 @@ const (

var errInvalidVAT = errors.New("invalid VAT number")

// ValidateTaxIdentity looks at the provided code, determines the type, and performs
// the calculations required to determine if it is valid.
// These methods assume the code has already been cleaned and only
// contains upper-case letters and numbers.
func ValidateTaxIdentity(tID *tax.Identity) error {
func validateTaxIdentity(tID *tax.Identity) error {
return validation.ValidateStruct(tID,
validation.Field(&tID.Code, validation.By(validateTaxCode)),
)
Expand All @@ -45,13 +40,6 @@ func validateTaxCode(value interface{}) error {
return validateDigits(code[0:9], code[10:12])
}

// NormalizeTaxIdentity removes any whitespace or separation characters and ensures all letters are
// uppercase. It'll also remove the "NL" part at beginning if present such as required
// for EU VIES system which is redundant and not used in the validation process.
func NormalizeTaxIdentity(tID *tax.Identity) error {
return common.NormalizeTaxIdentity(tID)
}

func validateDigits(code, check cbc.Code) error {
num, err := strconv.ParseInt(string(code), 10, 64)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions regimes/nl/tax_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestNormalizeTaxIdentity(t *testing.T) {
}
for _, ts := range tests {
tID := &tax.Identity{Country: l10n.NL, Code: ts.Code}
err := nl.NormalizeTaxIdentity(tID)
err := nl.Calculate(tID)
assert.NoError(t, err)
assert.Equal(t, ts.Expected, tID.Code)
}
Expand Down Expand Up @@ -89,7 +89,7 @@ func TestValidateTaxIdentity(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tID := &tax.Identity{Country: l10n.NL, Code: tt.code}
err := nl.ValidateTaxIdentity(tID)
err := nl.Validate(tID)
if tt.err == "" {
assert.NoError(t, err)
} else {
Expand Down
6 changes: 0 additions & 6 deletions regimes/pl/invoices.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package pl
import (
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/currency"
"github.com/invopop/gobl/l10n"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/tax"
"github.com/invopop/validation"
Expand Down Expand Up @@ -56,7 +55,6 @@ func (v *invoiceValidator) supplier(value interface{}) error {
validation.Field(&obj.TaxID,
validation.Required,
tax.RequireIdentityCode,
validation.By(validatePolishTaxIdentity),
),
)
}
Expand All @@ -73,10 +71,6 @@ func (v *invoiceValidator) commercialCustomer(value interface{}) error {
return validation.ValidateStruct(obj,
validation.Field(&obj.TaxID,
validation.Required,
validation.When(
obj.TaxID.Country.In(l10n.PL),
validation.By(validatePolishTaxIdentity),
), // TODO check if id is valid when other entity
),
)
}
Expand Down
19 changes: 10 additions & 9 deletions regimes/pl/pl.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/invopop/gobl/currency"
"github.com/invopop/gobl/i18n"
"github.com/invopop/gobl/l10n"
"github.com/invopop/gobl/regimes/common"
"github.com/invopop/gobl/tax"
)

Expand Down Expand Up @@ -36,8 +37,8 @@ func New() *tax.Regime {
Tags: invoiceTags,
Scenarios: scenarios, // scenarios.go
Validator: Validate,
// Calculator: Calculate,
Categories: taxCategories, // tax_categories.go
Calculator: Calculate,
Categories: taxCategories, // tax_categories.go
}
}

Expand All @@ -57,10 +58,10 @@ func Validate(doc interface{}) error {
}

// Calculate will perform any regime specific calculations.
// func Calculate(doc interface{}) error {
// switch obj := doc.(type) {
// case *tax.Identity:
// return normalizeTaxIdentity(obj)
// }
// return nil
// }
func Calculate(doc interface{}) error {
switch obj := doc.(type) {
case *tax.Identity:
return common.NormalizeTaxIdentity(obj)
}
return nil
}
52 changes: 7 additions & 45 deletions regimes/pl/tax_identity.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pl

import (
"errors"
"regexp"
"strconv"
"unicode"
Expand All @@ -13,27 +14,16 @@ import (
/*
* Sources of data:
*
* - https://tramites.aguascalientes.gob.mx/download/documentos/D20230407194800_Estructura%20RFC.pdf
* - https://pl.wikipedia.org/wiki/Numer_identyfikacji_podatkowej
*
*/

// Tax Identity Type
const (
TaxIdentityTypePolish cbc.Key = "polish"
TaxIdentityTypeOther cbc.Key = "other"
taxIdentityPattern = `^[1-9]((\d[1-9])|([1-9]\d))\d{7}$`
)

// Tax Identity Patterns
const (
TaxIdentityPatternPolish = `^[1-9]((\d[1-9])|([1-9]\d))\d{7}$`
TaxIdentityPatternOther = `^.{1,50}$`
)

// Tax Identity Regexp
var (
TaxIdentityRegexpPolish = regexp.MustCompile(TaxIdentityPatternPolish)
TaxIdentityRegexpOther = regexp.MustCompile(TaxIdentityPatternOther)
taxIdentityRegexp = regexp.MustCompile(taxIdentityPattern)
)

func validateTaxIdentity(tID *tax.Identity) error {
Expand All @@ -45,18 +35,6 @@ func validateTaxIdentity(tID *tax.Identity) error {
)
}

func validatePolishTaxIdentity(value interface{}) error {
code, ok := value.(cbc.Code)
str := code.String()
if !ok {
return nil
}
if TaxIdentityRegexpPolish.MatchString(str) && validateNIPChecksum(code) {
return nil
}
return tax.ErrIdentityCodeInvalid
}

func validateTaxCode(value interface{}) error {
code, ok := value.(cbc.Code)
if !ok {
Expand All @@ -65,31 +43,15 @@ func validateTaxCode(value interface{}) error {
if code == "" {
return nil
}
typ := DetermineTaxCodeType(code)
if typ.IsEmpty() {
return tax.ErrIdentityCodeInvalid
}
if typ == TaxIdentityTypePolish {

if taxIdentityRegexp.MatchString(code.String()) {
if validateNIPChecksum(code) {
return nil
}
return tax.ErrIdentityCodeInvalid
return errors.New("checksum mismatch")
}
return nil
}

// DetermineTaxCodeType determines the type of tax code or provides
// an empty key if it looks invalid.
func DetermineTaxCodeType(code cbc.Code) cbc.Key {
str := code.String()
switch {
case TaxIdentityRegexpPolish.MatchString(str):
return TaxIdentityTypePolish
case TaxIdentityRegexpOther.MatchString(str):
return TaxIdentityTypeOther
default:
return cbc.KeyEmpty
}
return errors.New("invalid format")
}

func validateNIPChecksum(code cbc.Code) bool {
Expand Down
94 changes: 94 additions & 0 deletions regimes/pl/tax_identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package pl_test

import (
"testing"

"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/l10n"
"github.com/invopop/gobl/regimes/pl"
"github.com/invopop/gobl/tax"
"github.com/stretchr/testify/assert"
)

func TestNormalizeTaxIdentity(t *testing.T) {
tests := []struct {
Code cbc.Code
Expected cbc.Code
}{
{
Code: "9551893317",
Expected: "9551893317",
},
{
Code: "PL9551893317",
Expected: "9551893317",
},
{
Code: "955-189-33.17",
Expected: "9551893317",
},
}
for _, ts := range tests {
tID := &tax.Identity{Country: l10n.PL, Code: ts.Code}
err := pl.Calculate(tID)
assert.NoError(t, err)
assert.Equal(t, ts.Expected, tID.Code)
}
}

func TestTaxIdentityValidation(t *testing.T) {
tests := []struct {
name string
code cbc.Code
err string
}{
{name: "good 1", code: "9551893317"},
{name: "good 2", code: "1132191233"},
{name: "good 3", code: "5841896486"},
{name: "good 4", code: "7010009325"},
{
name: "bad mid length",
code: "12345678910",
err: "invalid format",
},
{
name: "too long",
code: "1234567890123",
err: "invalid format",
},
{
name: "too short",
code: "123456",
err: "invalid format",
},
{
name: "not normalized",
code: "12.449.965-4",
err: "invalid format",
},
{
name: "bad format",
code: "1002191233",
err: "invalid format",
},
{
name: "bad checksum",
code: "9551893318",
err: "checksum mismatch",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tID := &tax.Identity{Country: l10n.PL, Code: tt.code}
err := pl.Validate(tID)
if tt.err == "" {
assert.NoError(t, err)
} else {
if assert.Error(t, err) {
assert.Contains(t, err.Error(), tt.err)
}
}
})
}
}

0 comments on commit 3ffa46a

Please sign in to comment.