Skip to content

feat: strengthen EUID/VAT/SIREN/SIRET reliability - #3

Merged
euskadi31 merged 3 commits into
mainfrom
feature/more-test
Jul 3, 2026
Merged

feat: strengthen EUID/VAT/SIREN/SIRET reliability#3
euskadi31 merged 3 commits into
mainfrom
feature/more-test

Conversation

@euskadi31

@euskadi31 euskadi31 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Raises checksum confidence on EU business identifiers to ~95% by:

  • Implementing per-country VAT algorithms for 30 codes (EU-27 + XI, GB, NO, IS kennitala; LI stays format-only)
  • Making EUID a fully native meta-format: 27 embedded national-register validators, no external wiring required
  • Adding SIREN/SIRET dérogation infrastructure with La Poste built-in
  • Fixing every gap surfaced in the interim review

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:

  • Simple mod-11 / mod-10: AT, BE, DE, DK, EE, EL, FI, HU, IT (Luhn), LU, LV, NL, PL, PT, SI, SK
  • Multi-variant: BG (BULSTAT 9d + EGN 10d + foreigner LNCh 10d), CZ (legal 8d + special 9d + rodné číslo 10d), IE (7D+1L + 7D+2L), LV (legal + natural person personal code), LT (9d + 12d)
  • Complex mappings: CY (mod-26 char mapping), ES (DNI + NIE + CIF strict), IE (mod-23 with letter mapping)
  • Post-Brexit: GB and XI (dual 97 / 9755 algorithm)
  • EEA: NO (organization number mod-11), IS (kennitala 10-digit mod-11)

Canonicalization + aliases

  • GREL (Greece uses EL as VAT prefix)
  • UKGB (legacy systems still emit UK)
  • BE pre-2005 9-digit VAT → 10-digit form
  • CY prefix 12 rejected (reserved for legacy TINs)
  • IE +/* position-2 legacy layout via vat.WithLegacy()

SIREN / SIRET

  • SIRET NIC 00000 rejected at Format level (INSEE-reserved)
  • La Poste rule (356000000) supported: Luhn OR digit-sum divisible by 5
  • siren.WithDerogation / siret.WithDerogation for client-registered exceptions
  • Économat des Armées SIREN 542085907 verified Luhn-valid — documented

Option pattern

All four providers (vat, siren, siret, euid) accept New(opts ...Option) *Provider.

Numbers

  • 593 tests pass with -race (baseline: 259)
  • 0 lint issues
  • Fuzz tests on 4 providers (make fuzz at 5m, make fuzz-quick at 30s)
  • Per-country vectors sourced with // Source: <URL> comments

Test plan

  • make test — 593 tests pass
  • make lint — 0 issues
  • make fuzz — no panics at FUZZTIME=5m
  • Manual VIES spot-check for 3-5 countries with the sourced VAT numbers

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.
@coveralls

coveralls commented Jul 3, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 28648271794

Coverage remained the same at 100.0%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 1377 of 1377 lines across 10 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 2419
Covered Lines: 2419
Line Coverage: 100.0%
Coverage Strength: 19.6 hits per line

💛 - Coveralls

euskadi31 added 2 commits July 3, 2026 10:05
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
euskadi31 merged commit 740b78f into main Jul 3, 2026
2 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants