Skip to main content

D81 — Jurisdiction enrichment: M2M bloc membership + currency/region/name data + aliases + generator command (RESOLVED, evolves D80)

Architecture decision record, relocated verbatim from the retired single-file docs/tenancy/ decision log. Status, thematic clusters, and how to record a new ADR: the decision log index.

D80's single parent_id conflated two different relationships: geographic containment and bloc membership. But a real country belongs to several overlapping blocs at once (Germany ∈ EU ∧ Eurozone ∧ EEA ∧ Schengen), which one self-FK cannot express. So parent_id is narrowed to pure geographic containment — a subdivision points at its country, a nested subdivision at its parent subdivision (e.g. AZ-BABAZ-NX); countries and blocs are geographic roots (parent_id NULL). Bloc membership moves to a new central many-to-many table jurisdiction_memberships (member_id, bloc_id; model JurisdictionMembership; relations Jurisdiction::blocs() / Jurisdiction::members()), and 13 supranational blocs are now seeded (was just EU): EU, EZ (Eurozone), EEA, SCHENGEN, EFTA, GCC, ECOWAS, EAC, SADC, ASEAN, MERCOSUR, WAEMU, CEMAC. Totals: 5 308 jurisdictions (13 blocs + 249 countries + 5 046 subdivisions) and 184 curated memberships. The narrowing is backward-compatible — a country was only ever a geographic root anyway, so its parent_id was already NULL, and existing tenants.jurisdiction = 'ZA' still resolves unchanged.

Enriched country columns (additive, nullable, country-only). official_name / common_name (the ISO 3166-1 long + colloquial names); currency_code / currency_numeric / currency_name / currency_minor_units (ISO 4217 — currency_minor_units drives exact-decimal rounding, e.g. JPY 0, BHD 3); region / subregion (UN M49 grouping for reporting + coarse corridor rollups). Per-field provenance: structure + names + subdivisions come from Debian iso-codes; the country→currency mapping + minor units from ICU/CLDR via ext-intl (Debian iso-codes does not carry a country→currency mapping); currency numeric + name from ISO 4217; region/subregion from the UN M49 dataset (lukes/ISO-3166-Countries-with-Regional-Codes). region is NULL where UN M49 has no entry (Antarctica, Taiwan); currency is NULL for Antarctica.

Subdivision nesting. ISO 3166-2 sub-subdivisions (1 456 of the 5 046 carry a parent field in the two committed data files) are now wired through parent_id, so the gate walks subdivision → parent subdivision → country.

Non-canonical aliases. A new table jurisdiction_aliases (model JurisdictionAlias) maps non-ISO codes to their canonical jurisdiction (UKGB, ELGR). The gate resolves an alias to its canonical jurisdiction before gating, so a tenant assigned UK is not failed closed on a technicality.

Gate change — two-axis union. Support\JurisdictionGate::permittedFeatureKeys() now returns the union of the jurisdiction's own allow-list + every active geographic ancestor + every active bloc it (or an active ancestor country) is a member of, resolving aliases first; a new resolves(?string): bool helper reports whether a code resolves at all. Still Eloquent-only (within the sanctioned Support carve-out), fail-closed, memoised by the Features CatalogCache; compliance still beats commerce (D34).

Provenance + maintenance. Two commands: jurisdictions:build-catalog (in the jurisdictions module) regenerates the committed database/data/iso-3166-*.json snapshots from the ISO/CLDR/UN M49 sources — replacing the by-hand regeneration recipe; needs ext-intl + network. jurisdictions:verify-assignments (in the features module) audits that every tenants.jurisdiction value still resolves via the gate and fails CI on an orphan. Two smaller improvements ride along: an index on parent_id, and a soft-delete-idempotent seeder (it loads existing rows withTrashed() and restores a soft-deleted catalog row instead of inserting a duplicate).

Proven by the updated JurisdictionGateTest (two-axis geo ∪ bloc inheritance + alias resolution), JurisdictionSeederTest (full catalog + 13 blocs + memberships + subdivision nesting + soft-delete idempotency), JurisdictionCatalogDataTest (snapshot integrity incl. currency / region / parent), and the features module's VerifyJurisdictionAssignmentsTest. Multi-jurisdiction tenants remain the tracked follow-up — a tenant still has exactly one tenants.jurisdiction (docs/tracking/feature-entitlements-rollout.md item 4).


← Decision log index