Repository navigation
v2.1.1 — signup self-heals a dangling identity link
Signup no longer dead-ends when a login points at a customer that no longer exists. An identity whose linked crm.customer row is gone made the returning-customer branch fail on CRM's 422 customer.kyc.not_found — a rule with no copy registered, so it rendered the generic "Sorry, something went wrong". The identity was bricked permanently, and because reserve-at-pick runs before the customer lookup, every retry burned another MSISDN hold.
Highlights
- Self-heal instead of dead-end. Signup now treats that rule as a stale link:
ERRORlogportal.signup.identity_link_dangling, aportal_actionaudit row, drop the link, fall through to the normal create-customer path, re-bind. The customer completes signup unaided; the underlying data fault stays loudly visible rather than papered over. - One narrow carve-out, no takeover surface.
bss_portal_auth::relink_orphaned_to_customeris the single exception tolink_to_customer's "links are 1:1 and not reassignable from this surface" rule. The UPDATE is a compare-and-swap on the ghost customer id, so the only reachable transition is ghost → new customer. An identity bound to a live customer can't be moved through it — the CAS misses and returnsAlreadyLinkednaming the real holder — even though the caller picksnew_customer_id. - Copy for
customer.kyc.not_found. Signup repairs the state, but the CSR portal and thecustomer.get_kyc_statustool read a linked customer without repairing it, so the rule needed real copy rather than the generic fallback. - Mock-mode PAN guard re-applied on the fall-through, since it's skipped upstream while the identity still looks like a returning customer.
Why it happened
Not a signup regression — no code change caused it. The trigger was a data state. The v2.1.1 notes originally said the cause was unknown / done out-of-band; that was wrong, and v2.1.2 corrects it: make scenarios-hero truncated the customers during v2.0.0 release verification.
Guards
The self-heal keys off CRM's exact rule string across a service boundary — a rename in services/crm/src/service.rs would silently restore the original defect — so dangling_link_rule_is_pinned_and_has_copy pins the literal and its copy-map entry in CI. CAS semantics are pinned by three cases in the live audit_link_live round-trip: CAS miss refuses and leaves the link untouched, CAS hit repairs, unknown identity errors rather than silently no-opping.
Compatibility
SemVer patch. No new verb, no wire contract, event, payload, or DB schema change. relink_orphaned_to_customer is internal-crate API with no external surface. customer_self_serve untouched. BSS_RELEASE 2.1.0 → 2.1.1.
Gate
cargo fmt --all --check · clippy -D warnings (0) · 14 doctrine guards · live audit_link_live run against a real DB.
Known defect in this cut: it was tagged with chunker_matches_python_oracle failing (96 chunks vs 95). The full suite was run before the DECISIONS.md entry was appended and only the two changed crates re-run afterward, so this tag's commit message claim of "540 pass / 0 fail" did not describe the tree that shipped. Fixed in v2.1.2.