feat: strengthen EUID/VAT/SIREN/SIRET reliability - #3
Merged
Conversation
Reach ~90% checksum confidence on EU business identifiers by implementing per-country VAT algorithms, delegating EUID validation to national sub-validators, and hardening SIREN/SIRET with reserved-NIC rejection and an extensible dérogation table. Changes per provider: * VAT: implement checksums for 28 codes (EU-27 + XI + GB). Each algorithm sourced from the national tax authority and cross-verified against python-stdnum test vectors. Add GR→EL alias in Canonicalize. Add WithLegacy() option accepting the pre-2013 Irish '+' and '*' position-2 characters. NO/IS/LI stay format-only (no published algorithm). * EUID: reframe as a meta-format. Country code selects a sub-validator (SIREN wired for FR by defaults.New). ValidateFormat and ValidateChecksum delegate the REGISTRATION segment. Add WithSubValidator() option. Registration charset now accepts '+'. * SIRET: reject NIC "00000" (INSEE-reserved) at Format level. Move La Poste rule to an extensible dérogation table. Add WithDerogation() option for client-registered exceptions. * SIREN: add WithDerogation() option and dérogation table (empty by default; extensible for historical entities). * All 4 providers gain New(opts ...Option) constructors. Testing: * +250 new test cases (per-country VAT format + checksum, EUID meta-format delegation, SIREN/SIRET dérogation and NIC). * Fuzz tests on the 4 providers via `make fuzz`. * All 556 tests pass with -race, lint clean. * Per-country VAT vectors documented with `// Source: <URL>` comments referencing national tax authorities and imprints. Docs: README expanded with a per-country checksum coverage statement and the option-pattern examples.
Coverage Report for CI Build 28648271794Coverage remained the same at 100.0%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Follow-up to #3 that closes every gap surfaced in that PR's report: EUID meta-format made fully native: * Drop WithSubValidator. EUID now embeds native format+checksum validators for the 27 EU national business registers (Firmenbuch, KBO, IČO, KVK, KRS, GEMI, CRO, RCSL, Handelsregister, SIREN, …). New file providers/euid/nationals.go carries the table and all 27 functions. Delegation is native — `euid.New()` alone suffices. * Escape hatch WithCountryValidator(cc, rv) covers non-EU codes and bespoke overrides. * Registration charset accepts `+`. * 57+ per-country test cases (format + checksum + unsupported). VAT algorithms completed: * LV: implement natural-person personal code (previously returned false for first digit ≤ 3). * CZ: implement 9-digit special (prefix 6) and 10-digit rodné číslo (divisibility by 11 + date plausibility). * NO: implement organization-number mod-11 checksum. * IS: implement kennitala 10-digit mod-11 checksum (VSK 5/6 digits stay format-only). * ES CIF: tighten to python-stdnum strict rules — digit-only for A/B/E/H and middle group, letter-only for P/Q/R/S/W. * BG: 3 variants (BULSTAT 9d, EGN 10d, foreigner LNCh 10d) verified with vectors. * IT: replace unreliable Ferrari vector with real 00159820174 CF. * AT: add second verified vector ATU30525107. Canonicalization / aliases: * UK → GB alias (mirrors GR → EL). * BE pre-2005 9-digit VAT auto-canonicalized to 10 digits. * CY: reject prefix "12" (reserved for legacy TINs). Housekeeping: * Makefile fuzz target defaults to FUZZTIME=5m; new fuzz-quick target at 30s for local dev. * README rewritten with the 30-code checksum matrix, native EUID register table, and aliases section. * Économat des Armées documented: real SIREN 542085907 (from annuaire-entreprises.data.gouv.fr) passes standard Luhn — no dérogation needed. Testing: 593 tests pass with -race; lint clean; fuzz (30s per provider) clean.
Add public tests for every code branch reachable through the public API, and internal tests for defensive fallthroughs (format-check gated paths) so they remain safe under future refactors. - providers/vat: +40 checksum cases covering DNI/NIE prefixes, BG alt-alt path, CZ 8d/9d/10d switch branches (r==0, r==1, r==10, alt month codes 51-62/21-32/71-82), IE 7D+2L path, IE 'W' check, GB algo 97 (non-9755), GB 12d, NO r==0, IS kennitala, LV natural person, PT/SI/PL/NL boundary branches, CY letter mismatch, plus a checksums_internal_test.go for helpers gated by format. - providers/euid: +30 register cases (BG checksum path, ES DNI/NIE, CZ 8d IČO r==0/r==1/r==10, EE alt-weights, LT alt-weights, PT check-zero, per-country InvalidChars branches) plus nationals_internal_test.go for defensive helpers. - providers/siren: add TestValidateChecksumWithDerogation and siren_internal_test.go covering the nil-map fallback in WithDerogation. - providers/siret: add siret_internal_test.go for the equivalent nil-map branch. Also add a public NewRegisterValidator constructor so EUID callers can build register-validators for non-EU codes via the escape hatch. Numbers: 722 tests pass with -race, coverage 100.0% (statements), lint clean.
euskadi31
added a commit
that referenced
this pull request
Jul 3, 2026
Follow-up to #3 that closes every gap surfaced in that PR's report: EUID meta-format made fully native: * Drop WithSubValidator. EUID now embeds native format+checksum validators for the 27 EU national business registers (Firmenbuch, KBO, IČO, KVK, KRS, GEMI, CRO, RCSL, Handelsregister, SIREN, …). New file providers/euid/nationals.go carries the table and all 27 functions. Delegation is native — `euid.New()` alone suffices. * Escape hatch WithCountryValidator(cc, rv) covers non-EU codes and bespoke overrides. * Registration charset accepts `+`. * 57+ per-country test cases (format + checksum + unsupported). VAT algorithms completed: * LV: implement natural-person personal code (previously returned false for first digit ≤ 3). * CZ: implement 9-digit special (prefix 6) and 10-digit rodné číslo (divisibility by 11 + date plausibility). * NO: implement organization-number mod-11 checksum. * IS: implement kennitala 10-digit mod-11 checksum (VSK 5/6 digits stay format-only). * ES CIF: tighten to python-stdnum strict rules — digit-only for A/B/E/H and middle group, letter-only for P/Q/R/S/W. * BG: 3 variants (BULSTAT 9d, EGN 10d, foreigner LNCh 10d) verified with vectors. * IT: replace unreliable Ferrari vector with real 00159820174 CF. * AT: add second verified vector ATU30525107. Canonicalization / aliases: * UK → GB alias (mirrors GR → EL). * BE pre-2005 9-digit VAT auto-canonicalized to 10 digits. * CY: reject prefix "12" (reserved for legacy TINs). Housekeeping: * Makefile fuzz target defaults to FUZZTIME=5m; new fuzz-quick target at 30s for local dev. * README rewritten with the 30-code checksum matrix, native EUID register table, and aliases section. * Économat des Armées documented: real SIREN 542085907 (from annuaire-entreprises.data.gouv.fr) passes standard Luhn — no dérogation needed. Testing: 593 tests pass with -race; lint clean; fuzz (30s per provider) clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Raises checksum confidence on EU business identifiers to ~95% by:
Registry integration (VIES, INSEE Sirene, BRIS) remains deliberately out of scope.
EUID — native validators for EU-27
euid.New()alone now validates a full BRIS<CC><REGISTER>.<REGISTRATION>end-to-end. All 27 EU national registers have embedded format checks (and checksums where documented): Firmenbuch (AT), KBO/BCE (BE), IČO (CZ/SK), CVR (DK), Registrikood (EE), GEMI (EL), NIF/CIF (ES), Y-tunnus (FI), SIREN (FR), Cégjegyzékszám (HU), CRO (IE), Codice Fiscale (IT), Juridinio asmens kodas (LT), RCSL (LU), Reģistrācijas numurs (LV), Company number (MT), KVK (NL), KRS (PL), NIPC (PT), CUI (RO), Organisationsnummer (SE), Matična številka (SI), Handelsregister (DE), MBS (HR), HE (CY), EIK (BG).Escape hatch:
euid.WithCountryValidator(cc, rv)for non-EU codes.Registration charset now accepts
+.VAT — 30 checksum algorithms
Sourced from national tax authorities, cross-verified against python-stdnum test vectors:
Canonicalization + aliases
GR→EL(Greece uses EL as VAT prefix)UK→GB(legacy systems still emit UK)12rejected (reserved for legacy TINs)+/*position-2 legacy layout viavat.WithLegacy()SIREN / SIRET
00000rejected at Format level (INSEE-reserved)356000000) supported: Luhn OR digit-sum divisible by 5siren.WithDerogation/siret.WithDerogationfor client-registered exceptions542085907verified Luhn-valid — documentedOption pattern
All four providers (
vat,siren,siret,euid) acceptNew(opts ...Option) *Provider.Numbers
-race(baseline: 259)make fuzzat 5m,make fuzz-quickat 30s)// Source: <URL>commentsTest plan
make test— 593 tests passmake lint— 0 issuesmake fuzz— no panics at FUZZTIME=5m