ADR-0039: Resilience Rollout Completion + KBC Acquiring Gap Signals
Status: Accepted Date: 2026-04-14
Context
Following the initial circuit breaker rollout (ADR-0032) and the shell company detector (ADR-0038), five related improvements were needed in the same session:
- Resilience coverage gap -- only 6 of ~20 external services were wrapped with circuit breakers. A Czech ARES, Estonian Ariregister, or BODACC outage could still stall an investigation for the full Temporal activity timeout.
- Conditional approval restrictions not surfaced --
MerchantRestrictionswere persisted correctly tocases.decision_restrictionsbut no dashboard component displayed the captured blocked MCCs, volume caps, or secondary-review flags. - Establishment data under-exploited -- the shell detector produces one binary HIGH finding, but the Gino/KBC acquiring meeting (2026-04-13) identified additional signals: geographic concentration and recent establishment bursts.
- Czech demo narrative thin -- the Prague demo needed richer Czech-specific evidence; the sbírka listin PDF download was a known gap.
- NL KvK rate limit ignored -- the Dutch Chamber of Commerce API enforces 3 req/sec; 429s were expected during any real run.
Decision
Ship all five improvements as focused, independent merges:
-
Complete circuit breaker rollout -- apply the ADR-0032 pattern to 15 additional services (NBB, PEPPOL directory, CZ Justice/ISIR, SK RUZ, CH Zefix, FR INPI/INSEE/BODACC, DK CVR, EE Ariregister, FI YTJ, NL KvK, NO BRREG, RO ANAF). Pure orchestrators are skipped since their downstreams have their own breakers. Final coverage: 39
circuit_registry.callsites across 25 unique breaker names -- every external network-calling service is wrapped. -
RestrictionsSummaryCard -- add
decision_restrictions: MerchantRestrictions | NonetoCaseResponse(Pydantic v2 coerces the JSONB dict). The orange-bordered shadcnCardrenders conditionally and cites EU-AMLR Art. 26 + Art. 8(3). -
Establishment enrichment service --
analyze_establishments(establishments, country)returnslist[Finding]:establishment_concentration(MEDIUM) when ≥3 establishments share a postal code (AMLR Art. 28 §2(a)), andestablishment_recent_burst(MEDIUM) when all ≥3 dated establishments were created within 365 days (Art. 28 §2(b)). Wired in after shell detection with guard-and-swallow. -
CZ Justice sbírka listin PDF download --
download_justice_document()resolves the detail href, extracts the PDF URL from the<iframe src>, downloads the bytes, and uploads to MinIO. Reuses the existingcz_justicebreaker. -
NL KvK sliding-window rate limiter --
_kvk_rate_limit()uses adeque(maxlen=3)of monotonic timestamps under anasyncio.Lock, sleeping when the 3-req/sec window is saturated.
Consequences
Positive
- Complete resilience coverage -- no external service can stall the pipeline for more than ~30s
- Officers can now see captured conditional-approval restrictions, including on case re-open
- Three additional AMLR Art. 28 signals (concentration, recent burst, shell detection) each cite their regulatory basis
- Demo-ready Czech narrative with full sbírka listin documents; graceful KvK throughput with no 429 storms
Negative
- Breaker state still resets on worker restart (known ADR-0032 gap; Redis-backed state is a follow-up)
- The rate limiter is per-process -- three workers each get a 3 req/sec bucket (9 req/sec effective); global coordination is deferred
- Establishment signals use only postal code and start date -- city-name and industry clustering deferred
Risks
- False positives on legitimate structures (e.g. a law firm with 3 notarial offices at one postal code) -- mitigated by MEDIUM severity and officer-final decisioning
- The recent-burst threshold is hardcoded at 365 days and may need per-segment tuning
- PDF download depends on the Justice.cz iframe pattern; an HTML change makes downloads silently return None (caught by the guard)