From 8f690f00007291702ae92c3b734da9f195980fea Mon Sep 17 00:00:00 2001 From: pmenendz Date: Tue, 30 Jun 2026 09:02:10 +0000 Subject: [PATCH] Route us_ein tax IDs to org.Identity US Employer Identification Numbers (us_ein) were being mapped to a GOBL tax.Identity, whose code must match ^[A-Z0-9]+$. Because the US regime defines no normalizer, the hyphen in EIN values (e.g. "41-4637166") was left in place and validation failed with "code: must be in a valid format", surfacing as a 422 when creating the silo entry. US EINs don't belong to a VAT-like tax scheme, so they should be represented as a party org.Identity (type EIN) rather than a tax.Identity code, matching how the German Steuernummer (de_stn) is already handled. org.Identity codes permit hyphens as separators, so the value validates. Both org-identity mappings now also set the identity's country to make them self-describing. Co-Authored-By: Claude Opus 4.8 (1M context) --- party.go | 25 ++++++++++++++++++++++--- party_test.go | 23 ++++++++++++++++------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/party.go b/party.go index 7363660..f9e27c2 100644 --- a/party.go +++ b/party.go @@ -8,13 +8,18 @@ import ( "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/org" "github.com/invopop/gobl/regimes/de" + "github.com/invopop/gobl/regimes/us" "github.com/invopop/gobl/tax" "github.com/stripe/stripe-go/v81" ) // For more details on Customer Tax IDs in Stripe, see: https://docs.stripe.com/billing/customer/tax-ids -var orgIDKeys = []stripe.TaxIDType{"de_stn"} +// orgIDKeys lists the Stripe tax ID types that GOBL represents as party +// identities (org.Identity) rather than tax identities (tax.Identity). These +// are codes that don't belong to a VAT-like tax scheme, such as the German +// Steuernummer (de_stn) or the US Employer Identification Number (us_ein). +var orgIDKeys = []stripe.TaxIDType{stripe.TaxIDTypeDEStn, stripe.TaxIDTypeUSEIN} // Lookup map for country code to Stripe tax ID type var taxIDMapGOBLToStripe = map[l10n.Code]stripe.TaxIDType{ @@ -143,6 +148,13 @@ func ToTaxIDFromOrg(id *org.Identity) *stripe.TaxID { Value: id.Code.String(), } } + switch id.Type { + case us.IdentityTypeEIN: + return &stripe.TaxID{ + Type: stripe.TaxIDTypeUSEIN, + Value: id.Code.String(), + } + } return nil } @@ -196,8 +208,15 @@ func FromTaxIDToOrg(taxID *stripe.TaxID) *org.Identity { switch taxID.Type { case stripe.TaxIDTypeDEStn: oid = &org.Identity{ - Key: de.IdentityKeyTaxNumber, - Code: cbc.Code(taxID.Value), + Country: l10n.DE.ISO(), + Key: de.IdentityKeyTaxNumber, + Code: cbc.Code(taxID.Value), + } + case stripe.TaxIDTypeUSEIN: + oid = &org.Identity{ + Country: l10n.US.ISO(), + Type: us.IdentityTypeEIN, + Code: cbc.Code(taxID.Value), } } oid.Normalize() diff --git a/party_test.go b/party_test.go index 8db6331..9a69cf6 100644 --- a/party_test.go +++ b/party_test.go @@ -8,6 +8,7 @@ import ( "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/org" "github.com/invopop/gobl/regimes/de" + "github.com/invopop/gobl/regimes/us" "github.com/invopop/gobl/tax" "github.com/stretchr/testify/assert" stripe "github.com/stripe/stripe-go/v81" @@ -169,7 +170,12 @@ func TestFromTaxIDToOrg(t *testing.T) { { "valid DE STN", &stripe.TaxID{Type: "de_stn", Value: "123456789"}, - &org.Identity{Key: de.IdentityKeyTaxNumber, Code: cbc.Code("123456789")}, + &org.Identity{Country: l10n.DE.ISO(), Key: de.IdentityKeyTaxNumber, Code: cbc.Code("123456789")}, + }, + { + "valid US EIN", + &stripe.TaxID{Type: "us_ein", Value: "41-4637166"}, + &org.Identity{Country: l10n.US.ISO(), Type: us.IdentityTypeEIN, Code: cbc.Code("41-4637166")}, }, { "invalid type", @@ -294,8 +300,9 @@ func TestFromCustomer(t *testing.T) { }, Identities: []*org.Identity{ { - Key: de.IdentityKeyTaxNumber, - Code: "123456789", + Country: l10n.DE.ISO(), + Key: de.IdentityKeyTaxNumber, + Code: "123456789", }, }, }, @@ -579,8 +586,9 @@ func TestFromCustomerItalyTaxIDLogic(t *testing.T) { }, Identities: []*org.Identity{ { - Key: de.IdentityKeyTaxNumber, - Code: "123456789", + Country: l10n.DE.ISO(), + Key: de.IdentityKeyTaxNumber, + Code: "123456789", }, }, }, @@ -690,8 +698,9 @@ func TestNewSupplierFromAccount(t *testing.T) { Name: "German Business", Identities: []*org.Identity{ { - Key: de.IdentityKeyTaxNumber, - Code: "123456789", + Country: l10n.DE.ISO(), + Key: de.IdentityKeyTaxNumber, + Code: "123456789", }, }, },