Check and link company register numbers per country, offline (#935) #1022

Merged
tiagoagueda merged 2 commits from 935-register-links into main 2026-10-20 17:02:50 +00:00
Owner

Refs #935 (the online half stays open, tracked in #1021)

What changed

The Company register number scheme (register), inside the identifiers module and still offline:

  • Check digits per country, replacing the bare prefix check: France SIREN and SIRET (Luhn, with La Poste's exception), Czechia IČO, Norway organisasjonsnummer, Finland Y-tunnus, Estonia registrikood, Poland KRS (ten digits, shape only). Any other country is held to its two-letter prefix, as before. A failure is refused with a clear message.
  • A link per country to the register's own page: FR (annuaire-entreprises, SIREN or SIRET), NO (Brønnøysund), EE (e-Äriregister), GB (Companies House), NL (KVK search), DK (Virk CVR), BE (KBO Public Search). Every other EU/EEA country leads to the EU business registers search (BRIS); a country BRIS does not cover (CH, US) has no link. Every number is reduced to digits (or letters and digits) before it goes into an address. Links are drawn by the existing identifier badge (rel="noopener noreferrer external").
  • Scheme gains link_by, a function from the value to an address, for a link that depends on the value; other schemes are unchanged.
  • Docs: the Tracking applications wiki page (committed locally to the wiki repo, not pushed).

Not in this PR: the LEI cross-check. An LEI's first four characters name its issuing LOU and not a country, so nothing sound can be said offline; it needs GLEIF's registeredAs and belongs to #1021.

URLs checked against the live registers: NO, EE, BE (page titles read), GB, NL, FR (reachable). Dropped as unverifiable: Finland (tietopalvelu.ytj.fi deep link not documented anywhere I could find), Czechia (the ARES web ?ico= parameter is undocumented; its REST API is not a page), Germany and Switzerland (no stable per-company address). DK is kept on the strength of the documented datacvr.virk.dk/enhed/virksomhed/<cvr> pattern; the site answers 403 to a script, so it was not opened here.

Library check

python-stdnum (LGPL-2.1+, pure Python, maintained) covers FR, CZ, NO, FI and EE, but not Poland's KRS, and ships some two hundred formats for six short sums. I wrote the six as small pure functions with their sources named, and ran them against python-stdnum on 20,000 random numbers each: they agree (the Estonian first-digit rule and the SIRET's SIREN rule were taken from it after the first run disagreed). Czech IČO differs only on numbers starting with 9, which stdnum rejects as a DIČ rule and the IČO algorithm does not.

Tests

tests/test_register_numbers.py: valid and invalid numbers per country (including wrong length and the La Poste and Estonian second-pass cases), every link, the BRIS fallback, no link outside it, hostile numbers never reaching an address, the badge markup, linked(). Full unit suite green (19460 passed).

Model use

Written with Claude Code, reviewed by the person who opens the PR.

🤖 Generated with Claude Code

Refs #935 (the online half stays open, tracked in #1021) ## What changed The *Company register number* scheme (`register`), inside the identifiers module and still offline: - **Check digits per country**, replacing the bare prefix check: France SIREN and SIRET (Luhn, with La Poste's exception), Czechia IČO, Norway organisasjonsnummer, Finland Y-tunnus, Estonia registrikood, Poland KRS (ten digits, shape only). Any other country is held to its two-letter prefix, as before. A failure is refused with a clear message. - **A link per country** to the register's own page: FR (annuaire-entreprises, SIREN or SIRET), NO (Brønnøysund), EE (e-Äriregister), GB (Companies House), NL (KVK search), DK (Virk CVR), BE (KBO Public Search). Every other EU/EEA country leads to the EU business registers search (BRIS); a country BRIS does not cover (CH, US) has no link. Every number is reduced to digits (or letters and digits) before it goes into an address. Links are drawn by the existing identifier badge (`rel="noopener noreferrer external"`). - `Scheme` gains `link_by`, a function from the value to an address, for a link that depends on the value; other schemes are unchanged. - Docs: the *Tracking applications* wiki page (committed locally to the wiki repo, not pushed). Not in this PR: the LEI cross-check. An LEI's first four characters name its issuing LOU and not a country, so nothing sound can be said offline; it needs GLEIF's `registeredAs` and belongs to #1021. URLs checked against the live registers: NO, EE, BE (page titles read), GB, NL, FR (reachable). Dropped as unverifiable: Finland (`tietopalvelu.ytj.fi` deep link not documented anywhere I could find), Czechia (the ARES web `?ico=` parameter is undocumented; its REST API is not a page), Germany and Switzerland (no stable per-company address). DK is kept on the strength of the documented `datacvr.virk.dk/enhed/virksomhed/<cvr>` pattern; the site answers 403 to a script, so it was not opened here. ## Library check python-stdnum (LGPL-2.1+, pure Python, maintained) covers FR, CZ, NO, FI and EE, but not Poland's KRS, and ships some two hundred formats for six short sums. I wrote the six as small pure functions with their sources named, and ran them against python-stdnum on 20,000 random numbers each: they agree (the Estonian first-digit rule and the SIRET's SIREN rule were taken from it after the first run disagreed). Czech IČO differs only on numbers starting with 9, which stdnum rejects as a DIČ rule and the IČO algorithm does not. ## Tests `tests/test_register_numbers.py`: valid and invalid numbers per country (including wrong length and the La Poste and Estonian second-pass cases), every link, the BRIS fallback, no link outside it, hostile numbers never reaching an address, the badge markup, `linked()`. Full unit suite green (19460 passed). ## Model use Written with Claude Code, reviewed by the person who opens the PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Check and link company register numbers per country, offline
Some checks failed
CI / Browser tests (Chromium) (push) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (push) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (push) Has been cancelled
CI / Database tests on PostgreSQL (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Every Python / Unit tests (Python 3.12) (push) Has been cancelled
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
2bd655da93
The Company register number scheme only checked the two-letter prefix and
linked nowhere. A typo in a SIREN or an IČO went through, and the number
could not be opened in the register that issued it.

registers.py now holds, without any request, the public check-digit rules
for France (SIREN and SIRET, Luhn), Czechia (IČO), Norway
(organisasjonsnummer), Finland (Y-tunnus), Estonia (registrikood) and the
shape of Poland's KRS, each a small function with its source named, and the
register's own page for France, Norway, Estonia, the United Kingdom, the
Netherlands, Denmark and Belgium. Any other country the EU business
registers search covers links there; the others have no link. Scheme gains
link_by for an address that depends on the value.

The rules were checked against python-stdnum on 20,000 random numbers each
and agree; the library was not adopted as a dependency for six short sums.
No LEI cross-check is added: an LEI carries no country offline, so that
belongs to the online lookup, which stays open in the follow-up.

Refs #935

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed 935-register-links from 2bd655da93
Some checks failed
CI / Browser tests (Chromium) (push) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (push) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (push) Has been cancelled
CI / Database tests on PostgreSQL (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Every Python / Unit tests (Python 3.12) (push) Has been cancelled
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
to 69acb053ea
All checks were successful
CI / Unit tests and coverage (Python 3.14) (push) Successful in 14m23s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m22s
CI / Database tests on PostgreSQL (push) Successful in 3m25s
CI / Security audit (push) Successful in 44s
CI / Browser tests (Chromium) (push) Successful in 30m29s
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 12m31s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m35s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m43s
CI / Security audit (pull_request) Successful in 59s
CI / Browser tests (Chromium) (pull_request) Successful in 30m16s
Every Python / Unit tests (Python 3.12) (push) Successful in 12m37s
Every Python / Unit tests (Python 3.13) (push) Successful in 12m28s
2026-10-19 19:26:30 +00:00
Compare
tiagoagueda force-pushed 935-register-links from 69acb053ea
All checks were successful
CI / Unit tests and coverage (Python 3.14) (push) Successful in 14m23s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m22s
CI / Database tests on PostgreSQL (push) Successful in 3m25s
CI / Security audit (push) Successful in 44s
CI / Browser tests (Chromium) (push) Successful in 30m29s
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 12m31s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m35s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m43s
CI / Security audit (pull_request) Successful in 59s
CI / Browser tests (Chromium) (pull_request) Successful in 30m16s
Every Python / Unit tests (Python 3.12) (push) Successful in 12m37s
Every Python / Unit tests (Python 3.13) (push) Successful in 12m28s
to 5a2c922c7e
All checks were successful
CI / Unit tests and coverage (Python 3.14) (push) Successful in 12m17s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m26s
CI / Database tests on PostgreSQL (push) Successful in 3m42s
CI / Security audit (push) Successful in 51s
CI / Browser tests (Chromium) (push) Successful in 29m19s
Every Python / Unit tests (Python 3.12) (push) Successful in 13m51s
Every Python / Unit tests (Python 3.13) (push) Successful in 12m35s
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 12m40s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m37s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m46s
CI / Security audit (pull_request) Successful in 51s
CI / Browser tests (Chromium) (pull_request) Successful in 30m56s
2026-10-20 06:23:17 +00:00
Compare
Merge main into 935-register-links
All checks were successful
CI / Unit tests and coverage (Python 3.14) (push) Successful in 13m44s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m47s
CI / Database tests on PostgreSQL (push) Successful in 3m46s
CI / Security audit (push) Successful in 53s
CI / Browser tests (Chromium) (push) Successful in 28m55s
Every Python / Unit tests (Python 3.12) (push) Successful in 13m39s
Every Python / Unit tests (Python 3.13) (push) Successful in 12m6s
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 13m27s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m31s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m48s
CI / Security audit (pull_request) Successful in 1m0s
CI / Browser tests (Chromium) (pull_request) Successful in 29m13s
ea6231b85e
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tiagoagueda deleted branch 935-register-links 2026-10-20 17:02:50 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo!1022
No description provided.