# Acquiring Platform Transformation — Progress Tracker

Single at-a-glance status across all four workstreams. Each workstream's own doc
(`docs/<Workstream>/...`) has the full design detail — this file only tracks what's
actually **done** vs **pending**. Update this file, not just the workstream docs, whenever
a phase's status changes.

**Last updated:** 2026-08-09 (Workstream C complete + illustrative seed data added — TerminalCertification/KeyRotationSchedule/KeyCeremonyLog, expiry worker verified end-to-end)

---

## Workstream A — Scheme Management (`scheme_core`)

Full detail: [`docs/Scheme-System/implementation-plan.md`](Scheme-System/implementation-plan.md)

| Phase | Scope | Status |
|---|---|---|
| **1** | Foundation — app scaffold, Scheme/Country/Currency masters, mappings, audit log, dashboard, CRUD LiveViews, permissions, seed data | ✅ **Done** — built, migrated, seeded (13 schemes/9 countries/10 currencies), LiveView-tested (create/edit/activate/suspend/deactivate/search), no regressions on existing pages |
| 2 | Card Products, BIN/IIN, Card Ranges | ✅ **Done** — `CardProduct`/`BinRange` schemas (unified BIN+range model), overlap-safe context, `mix load_scheme_bin_ranges` CSV bulk loader (batch-validates before writing, refuses on any overlap), CRUD LiveViews + PAN lookup tool + CSV export, seeded with 5 card products and 12 BIN ranges transcribed directly from `mercury_device_middlelayer`'s hardcoded `routing_rules.ex` (real IIN ranges, verified against standard published test PANs for Visa/Mastercard/Amex/Discover/Diners), 6 new LiveView/controller tests, no regressions |
| 2a | Real Mastercard BIN data import | ✅ **Done** — decoded a real Mastercard MPE parameter extract (`apps/scheme_core/assets/MCI.AR.T067...`, table `IP0040T1` Account Range Table) using two GitHub tools the user pointed to (`cardutil`, `MPEimporter` — saved to auto-memory as `mastercard_mpe_parsing_tools` for reuse) — a 1014-blocked, "compressed" record format that neither tool's defaults handled without adjustment; decode logic re-verified byte-for-byte in Elixir against the original Python investigation before touching the DB. **312 unique real 19-digit account ranges imported as `status: draft`** (19 countries, not UAE-specific). Two real schema/logic issues found and fixed as part of this: (1) `BinRange.start_bin`/`end_bin` only supported 6-8 digits — widened to 6-19 to fit real precise account ranges (ISO/IEC 7812 extended precision), migration + regex + form maxlength all updated; (2) `BinRange`'s overlap check (built in Phase 2, before the status-gated pattern was established in Phases 3-5) blocked against draft rows too, unlike every later phase's schemas — fixed to only check active-vs-active, consistent with `ContactlessParameter`/`RoutingConfig`/`InterchangeRate`/`SchemeFee`. The 312 new precise ranges nest inside the existing coarse Phase-2 ranges (e.g. Mastercard's 222100-272099) — landed as draft deliberately, real hierarchy/activation decision deferred, not defaulted silently. Full regression suite still shows only the same 8 pre-existing unrelated failures. |
| 3 | EMV & Contactless Parameters | ✅ **Done** — `EmvAid` (AID/TAC/risk params), `Capk` (RID+index unique), `ContactlessParameter` (MySQL NULL-safe `<=>` duplicate-active check across nullable country/currency) schemas + CRUD LiveViews. 6 new tests, no regressions. **Superseded by real data**, see next row — the "illustrative placeholder" contactless rows and empty-CAPK-by-design state from initial ship no longer apply. |
| 3a | Real EMV config data import | ✅ **Done** — `scheme_core_real_emv_config_seed.exs` parses the platform's actual terminal config files (`priv/static/mf919/1.0.1/YSDK_L3_configuration.xml`, `priv/static/Mypinpad/1.0.0/YSDK_L3_configuration.json`) and seeds real data: **24 AIDs**, **57 CAPKs** (real scheme-published keys, checksums spot-verified against source), **2 country-scoped (AE) contactless parameter sets** with real limits. New `interac` scheme created (found in real data, wasn't in the original 13). One mislabeled source entry ("CAPK RUPAY 5A") correctly resolved to Diners via its RID, not the file's own wrong label. Superseded the Phase 3 baseline seed's illustrative contactless placeholders (removed from that script). A real bug was caught and fixed mid-run: Kernel 2/Mastercard's DF8123-DF8126 tags are BCD-encoded per EMV Book C-2, not raw hex — initial run produced 161061273 instead of 9999999 for the contactless limit; caught by cross-checking against the same file's own metadata block, fixed, DB cleaned and re-seeded. TAC default/denial/online deliberately left blank — the XML's DF11/12/13 tags aren't confirmed against a vendor tag dictionary. |
| 4 | Network Routing & Network Parameters | ✅ **Done** — `RoutingConfig` and `NetworkParameter` schemas (both with the established MySQL NULL-safe `<=>` duplicate-active-row pattern), CRUD LiveViews. Seeded with real, honestly-scoped facts only: `RoutingConfig` reflects today's actual state (all confirmed-live schemes route via YSP — no fabricated scheme-specific split); `NetworkParameter`'s ISO version/timeout values are real but explicitly labeled as sourced from `mercury_device_middlelayer`'s *sandboxed, not-yet-live* direct-connect config, not asserted as confirmed for the live YSP path. 4 new tests, no regressions. Note: TAC ambiguity from Phase 3's real EMV config import has been handed to the user's L3 Certification team for confirmation — not yet resolved, tracked there not here. |
| 5 | Interchange & Scheme Fee Tables | ✅ **Done** — `InterchangeRate` (deliberately a separate table from `settlement_core.InterchangeRate`, richer dimensionality: card product/country/currency/MCC/transaction type/domestic-international scope; wide NULL-safe duplicate-active check across all 7 dimensions) and `SchemeFee` (all 8 fee types from §7.14; "at least one of rate_percentage/fixed_amount" validation) schemas + CRUD LiveViews. Confirmed via research: fully greenfield, no prior fee/rate module exists beyond `settlement_core`'s own (which stays untouched, per this doc's own earlier decision). No rates seeded — real interchange/scheme fee figures aren't available (same "PENDING — awaiting data file from YSP" gap `settlement_core.InterchangeRate` has had since creation), ships empty by design like Phase 3's CAPK page. 5 new tests, no regressions. **Bug found in `settlement_core`, not fixed (out of scope):** its own `load_interchange_rates.ex` Mix task is stale — references field names (`card_category`, `interchange_percentage`, `is_active`, etc.) removed in a March 2026 schema refactor; would fail or silently drop data if run today. Flagged for whoever owns `settlement_core`. **Follow-up investigation (2026-08-09):** user provided a real Mastercard MPE extract to check for actual interchange data — confirmed table `IP0052T1` (Interchange Fee Group Param) exists but only holds a classification code (IRD), not a percentage; confirmed the real rate-bearing table `IP0053T1` (`FeePointer`/`RATE`/`UNITFEE`/`MINFEE`/`MAXFEE`) is a genuine, now-documented Mastercard table format, but it's **not present** in the file the user has — still no real rates available. No change to this phase's empty-by-design status; the schema+format knowledge is saved to memory for if/when a file containing `IP0053T1` shows up. |
| 6 | Reason Codes & Dispute Parameters (dispute_core reconciliation decision) | ✅ **Done** — `ReasonCode` (hard `scheme_id` FK, `dispute_category`/`time_limits`/`required_documents`) and `DisputeParameter` (one active row per scheme: response time/SLA/chargeback+arbitration windows/evidence requirements) schemas + CRUD LiveViews. **Reconciliation decision made, not deferred**: re-checked `dispute_core`'s current state first — its `Scheme`/`ReasonCode` are uncommitted, actively in-flight (real live consumer: SLA due-date auto-computation), deliberately soft/no-FK by design. Decided to keep them **permanently separate** — different purposes (dispute_core: fast never-fail lookup for the live case engine; scheme_core: governed, versioned master reference data) — not a "revisit later" deferral. `dispute_core` fully confirmed untouched (still one single untracked directory in git status). Seeded 7 real, publicly-documented Visa/Mastercard reason codes (code+description+category only — `time_limits`/`required_documents` deliberately left blank, no fabricated day-counts). 5 new tests, no regressions, existing `dispute_core` admin pages verified still working. |
| 7 | Settlement Calendar & Holiday Calendar | ✅ **Done** — `SettlementCalendar` (per-cycle cutoff time/processing window/settlement lag, optionally scoped to country/currency, NULL-safe duplicate-active check on scheme+cycle_code+country+currency) and `HolidayCalendar` (national/scheme/bank/settlement holidays, scheme_id and country_id both nullable but at least one required, NULL-safe duplicate-active check on date+type+scope) schemas + CRUD LiveViews. **Investigated first**: user supplied 4 real Mastercard sample reports (`IP142110-AA.txt`, `IP727010-AA.txt`, `IP727020-AA.txt`, `SWCHD363.txt`) + a bonus announcement PDF (`m_an1026_en-us (1).pdf`, AN 1026 — Transaction Integrity Class) hoping they were Phase 7 material; none were — they're clearing-cycle acknowledgement/notification reports and a settlement control report (all after-the-fact records of cycles that already happened, not calendar *inputs* like cycle schedules/cutoffs/holidays) and an interchange-rate-classification announcement, respectively. Findings reported to user; more relevant to Workstream B (`clearing_core`, not yet built) and to Phase 5's `InterchangeRate` as a future enhancement. **Ships empty by design, same as Phase 5** — no cycle/holiday data seeded. User is collecting a real Mastercard settlement calendar document; architecture is deliberately scheme-agnostic (`scheme_id` FK, free-text-safe) so other schemes need zero schema changes once their calendars arrive — this satisfies the user's explicit instruction to "keep system architecture for other schemes as stub" while only Mastercard's real document is in hand. 5 new tests (create/list, duplicate-active rejection for both schemas, scope-required rejection for holidays), no regressions (`scheme_live` suite: 34/34 pass; full `platform_web` suite: 51 tests, same pre-existing 8 failures as bug #5, nothing new). |
| 8 | Certification & Compliance Management | ✅ **Done** — `Certification` (program-level L2/L3/host/contactless/softpos certs, `cert_id`/issued/expiry dates, `renewal_status` distinct from the generic workflow `status`) and `ComplianceRecord` (PCI DSS/PCI PTS/EMV/scheme/key_rotation, `compliance_status`, audit/next-review dates) schemas + CRUD LiveViews. **First phase to pull in Oban**, as the plan doc anticipated: `SchemeCore.Workers.CertificationExpiryWorker` (new `scheme_compliance` queue, daily cron `0 4 * * *`) recomputes `renewal_status` from `expiry_date` (`expired`/`expiring_soon` within 30 days/`current`), deliberately never touching operator-set `renewal_in_progress`. Caught and fixed a real bug before it shipped: the worker's first draft compared `Date` structs with `<`/`<=`, which does field-by-field struct comparison (day before month/year) instead of calendar order — replaced with `Date.compare/2`. No `TmsCore.AlertsCore` notification wiring — that's explicitly Phase 9 scope (the `tms_core` dependency decision point), not duplicated here. 9 new tests (2 LiveViews + a direct worker test covering all 4 renewal_status transitions including the do-not-touch case), no regressions (`platform_web` suite: 55 tests, same pre-existing 8 failures as bug #5, nothing new). |
| 9 | Scheme Bulletins + Notifications | ✅ **Done (bulletins + 3 of 8 notification types — rest genuinely blocked, see below)** — `Bulletin` (bulletin_number/date, description, impact, `implementation_status` distinct from the generic workflow `status`, effective_date) schema + CRUD LiveView. **Dependency decision made per user instruction ("AlertsCore is better place... it has been defined separately")**: added `{:tms_core, in_umbrella: true}` to `scheme_core/mix.exs` (safe — `tms_core` only depends on `platform_core`, no cycle) and reused `TmsCore.AlertsCore` end-to-end rather than building a parallel dispatcher. Seeded 3 alert rules (`priv/repo/seeds/scheme_core_alert_rules_seed.exs`, same `enabled: false`-so-the-generic-evaluator-skips-them pattern as `dispute_alert_rules_seed.exs`): wired 3 of requirement §14's 8 notification types — **Certification expiry** (extended Phase 8's `CertificationExpiryWorker` to fire on transition into `expiring_soon`/`expired`), **Compliance expiry** (new `ComplianceReviewWorker`, daily cron, deliberately read-only — `compliance_status` is a human-set audit fact that can't be derived from `next_review_date` alone, unlike `Certification.renewal_status`), and **Bulletin release** (`Context.create_bulletin/2`/`update_bulletin/3` fire on transition into `status: "active"`, not on every save of an already-active row). **The other 5 types are explicitly deferred, not stubbed**: Configuration approval/rejection and Version publication need Phase 12's maker/checker workflow (doesn't exist yet); Import completion/failure need Phase 13's bulk import tasks (doesn't exist yet) — wiring notifications for events that can't happen yet would be exactly the kind of ahead-of-real-functionality build this project has avoided throughout. Caught a real bug in the Phase 8 worker while extending it (documented in that phase's row) — no new bugs found here. 3 new tests (bulletin release notification fires only on the draft→active transition; certification-expiry test extended to assert `Alert` rows on expired/expiring_soon and their absence on current/renewal_in_progress; compliance-review worker notifies without mutating `compliance_status`), no regressions (`platform_web` suite: 57 tests, same pre-existing 8 failures as bug #5; `agent_core`+`tms_core`: 315 tests, 0 failures, unaffected by the new Oban queue/cron entries). |
| 10 | Merchant Category Rules | ✅ **Done** — `MccRule` (mcc, description, `allowed_transactions`/`restrictions` as JSON-array-backed fields, `cashback_rules`/`installment_rules` as free-form JSON, `risk_category`) schema + CRUD LiveView. **Deliberately not scheme-scoped**, matching the plan doc's own schema — MCC is a shared ISO 18245 classification, not scheme-published data; one row per `mcc`, enforced by a DB unique index (same shape as `ReasonCode`'s hard uniqueness) rather than the active-row-only pattern. Built after Phase 2 (Card Product) as the plan doc's sequencing note recommended. 2 new tests (CRUD + duplicate-mcc rejection), no regressions (`platform_web` suite: 59 tests, same pre-existing 8 failures as bug #5). |
| 11 | Reports & Administration | ⬜ Not started |
| 12 | Maker/Checker Approval Workflow (retrofit) | ⬜ Not started |
| 13 | External Lookup APIs & Bulk Import/Export | ⬜ Not started |

### Illustrative placeholder seed data (Workstream A Phases 5-10, Workstream C) — added 2026-08-09, explicitly requested by the user

The user asked for verification-only seed data for phases that had shipped empty (real
source files still pending) so the implementation can be exercised end-to-end in the UI
before real data arrives: **"I will come with actual file then change we will modify
this. minimum, it will help me to verify the implementation."**

| Phase | File | What's real vs. placeholder |
|---|---|---|
| 5 | `scheme_core_phase5_illustrative_seed.exs` | Everything illustrative — 6 `InterchangeRate` + 6 `SchemeFee` rows, invented percentages/fees. |
| 6 | `scheme_core_phase6_dispute_parameters_illustrative_seed.exs` | Everything illustrative — 2 `DisputeParameter` rows (visa/mastercard), invented SLA windows. (Reason codes themselves were already real, seeded earlier in Phase 6 proper.) |
| 7 | `scheme_core_phase7_illustrative_seed.exs` | Mixed — cycle code `"003"` (Mastercard) is real, matches the sample report `IP727010-AA.txt`; its cutoff time/window are invented. Holiday **dates** are real (New Year's Day, Christmas, UAE National Day); their settlement applicability isn't confirmed. |
| 8 | `scheme_core_phase8_illustrative_seed.exs` | Everything illustrative — 2 `Certification` + 3 `ComplianceRecord` rows, dates deliberately spread around `Date.utc_today/0` so `CertificationExpiryWorker`/`ComplianceReviewWorker` have something to act on. |
| 9 | `scheme_core_phase9_seed.exs` | Mixed — the Mastercard "AN 1026" bulletin is **real**, transcribed from the sample PDF the user provided (bulletin date, description, effective date); seeded `status: "active"`, which correctly fired a real "Scheme Bulletin Released" notification through `AlertsCore` on seed. One Visa row is explicitly illustrative and seeded `status: "draft"` specifically to demonstrate that a draft bulletin does *not* notify. |
| 10 | `scheme_core_phase10_seed.exs` | Mixed — the 6 MCC codes/descriptions are real (ISO 18245 standard: grocery, restaurants, service stations, ATM, gambling, money transfer); the business rules attached to each (`allowed_transactions`, `cashback_rules`, `risk_category`, etc.) are illustrative placeholders. |
| C (Workstream C) | `tms_acquirer_core_illustrative_seed.exs` | Mixed — terminal models (MF919/Morefun, MyPinPad, SR600) are real device models this project has real config files for; cert IDs/dates, key rotation schedules, and ceremony custodian names are all invented. 3 `TerminalCertification` + 3 `KeyRotationSchedule` + 2 `KeyCeremonyLog` rows, expiry dates spread around today so `TmsAcquirerCore.Workers.CertificationExpiryWorker` has something to act on. |

Every placeholder row lands `status: "draft"` (never `"active"`) so nothing reads as
authoritative in the UI. All seeds are idempotent (re-run safely, verified by running each
twice) and located under `apps/da_product_app/priv/repo/seeds/`. **When the user's real
source files arrive, replace the seed file's content — don't extend it with more invented
rows.** Also ran `CertificationExpiryWorker`/`ComplianceReviewWorker` (`scheme_core`) and
`TmsAcquirerCore.Workers.CertificationExpiryWorker` manually against their respective seed
data to confirm the full pipeline (status transitions + `AlertsCore` notifications) works
end-to-end, not just the CRUD paths — confirmed: the SR600 cert (seeded with a past expiry
date but initial `renewal_status: "current"`) correctly flipped to `"expired"`, and MyPinPad's
flipped to `"expiring_soon"`, after one worker run.

**Environment note, not a code issue**: seeding via `mix run` under `MIX_ENV=dev` fails with
`PromEx` `:eaddrinuse` on this machine — another long-running process (pre-existing, not
started by this work) already holds port 4021. Worked around by starting only the specific
OTP apps a given seed needs (e.g. `Application.ensure_all_started(:tms_acquirer_core)`) via
`mix run --no-start -e '...'` rather than the full `platform_web` supervision tree, which is
what pulls in `PromEx`.

## Workstream B — Clearing & Settlement Extension (`clearing_core`)

Full detail: [`docs/Clearing-System/requirement-summary.md`](Clearing-System/requirement-summary.md)

**Revised and rebuilt 2026-08-09** — the original B0-B5 roadmap assumed raw Mastercard
Base II; real documents supplied by the user showed the Mastercard sample was actually MPGS
Gateway data (DCF), not Base II, and the user redirected to Visa TC-33 + Mastercard IPM
(built from a DE/PDS catalog) first, with DCF explicitly deferred. See
`docs/Clearing-System/requirement-summary.md` for the full as-built detail.

| Phase | Scope | Status |
|---|---|---|
| C1 | App scaffold — `clearing_core` app, `ClearingBatch`/`ClearingException` schemas | ✅ **Done** |
| C2 | Visa TC-33 inbound parser (TCR0/TCR1) + `Tc33Record` + `mix load_visa_tc33` | ✅ **Done** — validated against synthetic fixtures matching the real spec; real sample smoke-tested (parses without crashing; documented why it decodes to 0 records — an ASCII-rendering artifact, not a parser bug) |
| C3 | Mastercard IPM/T112 inbound parser (DE-keyed, no byte spec available) + `IpmRecord` | ✅ **Done** — built from a combined DE/PDS catalog (user-supplied table + independent DCF-research corroboration), honestly scoped as decoding pre-delimited DE maps, not raw file bytes (no spec/sample exists) |
| C4 | Reconciliation bridge into `settlement_core` (`ClearingBridge`) | ✅ **Done** — same `COALESCE(actual, estimate)` override pattern as YSP; found and fixed 2 real bugs along the way (see below) |
| Ops UI | Clearing dashboard, batch list/detail, exception review queue | ✅ **Done** — `/admin/clearing/*`, new `clearing.*` permissions, not originally a separate phase but built per explicit user request for ops usability |
| C5 | Visa TC-33 outbound builder (presentment submission) | ✅ **Done** — round-trips through the parser; actual VisaNet transmission still certification-gated |
| C6 | Mastercard IPM outbound builder | ✅ **Done** — same DE catalog, submission direction; actual transmission still certification-gated |
| C7 | Exception management UI | ✅ **Done** (folded into "Ops UI" above) |
| DCF | MPGS Draft Capture File ingestion | ⬜ **Explicitly deferred by the user** — 160-page spec read in full, 2 real samples decoded structurally, research preserved in `docs/Clearing-System/requirement-summary.md` §5 for whenever it's picked back up |

**Real bugs found while building this** (full detail in the requirement doc §4):
1. `core_transactions.rrn` is `varchar(12)` — cannot hold Visa's 23-digit ARN. Fixed with an
   additive `acquirer_reference_number` column, used only by the Visa match path.
2. `visa_tc33_records`/`mastercard_ipm_records` decimal columns had no explicit
   `precision`/`scale`, silently defaulting to MySQL's `decimal(10,0)` — every fractional
   amount rounded to a whole number on write. Fixed with a corrective migration.

Both were caught by the reconciliation bridge's own tests before shipping, not in production.

## Workstream C — Terminal Management Extension (new app `tms_acquirer_core`)

Full detail: [`docs/Terminal-Management/direct-acquiring-extension.md`](Terminal-Management/direct-acquiring-extension.md)

**Revised 2026-08-09**: built as a new standalone app `tms_acquirer_core`, not additive
modules inside `tms_core` as originally scoped — user-suggested, confirmed and extended in
planning to cover both C1 and C2 as one bounded context, matching the `scheme_core`/
`clearing_core` pattern. `tms_core` does **not** currently depend on it — see the stub-
mismatch finding below.

| Item | Scope | Status |
|---|---|---|
| C2 | Device/Terminal Certification Tracking | ✅ **Done** — `TerminalCertification` schema (terminal-model/kernel certification, optional `scheme_id` cross-reference to Workstream A Phase 8's program-level `Certification`), CRUD LiveView at `/admin/acquirer/certifications`, daily expiry worker (same `Date.compare/2` transition logic as `SchemeCore.Workers.CertificationExpiryWorker`). **`tms_core`'s stub is *not* wired to it** — the plan assumed `get_certification_status/1` in `terminal_management.ex` was about terminal-model certification; it's actually one helper inside `generate_compliance_report/2`, a separate, already-fake compliance-framework-scoring feature (hardcoded PCI-DSS/GDPR/SOX/ISO27001 percentages, fake digital signature) unrelated to terminal hardware — wiring `TerminalCertification` into it would have been a wrong conceptual fit. `TmsAcquirerCore.Context.certification_status/1` is real and correct; it's just not called from `tms_core` yet, surfaced only via its own UI until a genuine terminal-model-certification call site exists. |
| C1 | Real HSM / Scheme Key Management (Verisec 10xPay REST + Thales TCP stub) | ✅ **Built and live-verified, UI complete** — `HsmClient` behaviour, `Hsm.Stub` (dev/test default), `Hsm.Verisec` (real REST+mTLS via `Req`), `Hsm.ThalesTcp` (stub only, unchanged from the original design). `KeyRotationSchedule`/`KeyCeremonyLog` schemas store lifecycle metadata only, never key material — now with CRUD/create-only LiveViews at `/admin/acquirer/key-rotation-schedules` and `/admin/acquirer/key-ceremony-logs` (permission `acquirer.keys.view`, separate from `acquirer.certifications.view`). **Live sandbox verification performed**: the Middle East endpoint (`:32600`) times out at the TCP level from this dev environment (the EU endpoint, `:443`, connects fine — possibly IP-allowlisting/routing specific to this network, not necessarily true from Mercury's own infra); against the EU endpoint, the full mTLS handshake completes and Verisec's server returns `SERVER ALERT: Fatal - Certificate Unknown` — confirmed independently via raw `curl --cert/--key` and via the actual `Hsm.Verisec` module, identical result both ways. Root cause confirmed directly: `client_cert.pem` (converted from the supplied `.pfx` via `openssl pkcs12`, since Erlang/OTP's `:public_key` has no PKCS12 support on this release) **expired 2026-01-09**. This proves the adapter's request-building/credential-loading/connection logic is correct — the only blocker is the expired credential, an operational renewal task, not a code defect. Production key ceremonies remain gated on the security-led review the original design called for. |
| C3 | EMV config generation / terminal risk parameters from scheme masters | ✅ **Built (enrichment scope), per explicit follow-up request** — `TmsAcquirerCore.EmvConfig.Generator` (lives in `tms_acquirer_core`, not `tms_core`: `scheme_core` already depends on `tms_core`, so `tms_core → scheme_core` is a dependency cycle Mix itself rejected when first attempted; `tms_acquirer_core` already depends on `scheme_core` and safely adds a `tms_core` dependency too). **Deliberately conservative, a surgical merge not a rewrite**: parses the *current active* `ConfigFileVersion` file and updates only fields with unambiguous `scheme_core` provenance — CAPK `modulus`/`exponent`/`checksum` (both MF919 XML and MyPinPad JSON, matched by `(rid, capk_index)`) and, for MF919 XML only, the 4 standard EMVCo Book 3 tags `9F1B`/`DF15`/`DF16`/`DF17` (floor limit, threshold value, target/max-target percentage — standard BCD-numeric encoding, *not* the ambiguous Mastercard-kernel-tag category that caused Phase 3a's BCD bug). TAC tags (`DF11`/`12`/`13`) are never touched, carrying forward the existing file's values, per explicit user decision — `scheme_core.EmvAid.tac_*` stays unconfirmed pending the L3 cert team. MyPinPad's per-transaction-type reader limits are explicitly **not attempted** (structure doesn't map onto `EmvAid`'s fields with the same confidence). New AID/CAPK entries in `scheme_core` absent from the current file are never appended, only existing entries enriched. **Lands as a new, `is_active: false` `ConfigFileVersion` row — never auto-activates**, per explicit user decision; `AutoPushService`/the MQTT push path are completely untouched, confirmed by 7 new tests including one proving the original active version stays active after generation. Validated two ways: synthetic fixtures (exact BCD-hex encoding correctness) and the **real MF919 file + real seeded `visa` scheme data** (Phase 3a) — round-trips CAPK data with zero changes (proves fidelity) and confirms `floor_limit` et al. are nil for real AIDs (Phase 3a's import never populated them), so the generator correctly no-ops rather than blanking anything. **Real bug caught and fixed by this module's own tests**: `Enum.map_reduce/3` returns `{mapped_list, final_acc}`, and an early draft destructured the tuple positions backwards, silently swapping the rewritten XML content with the change-log — caught immediately by a test asserting byte-identical output on a no-match case. Full design in `docs/Terminal-Management/direct-acquiring-extension.md` §4a. Trigger-on-certification (tying generation eligibility to C2's `TerminalCertification`) and terminal risk parameters from scheme masters remain unbuilt/lower-priority. **Branch note** (not a real bug, see below): `AutoPushService.push_mf919_emv_config/4` calls `TmsCore.TerminalManagement.L3ConfigZipBuilder.build_and_store/3`, absent on this branch only — user confirmed it exists and works on their current branch, will arrive on merge. |

## Workstream D — Switch Direct Connectivity

Full detail: [`docs/Switch-System/direct-connect-worklist.md`](Switch-System/direct-connect-worklist.md)

**Explicitly deferred** — no work started or planned until picked up by name.

| Item | Scope | Status |
|---|---|---|
| 1 | Real network credentials/certs + MIP/VAP certification testing | ⬜ Deferred |
| 2 | New adapter modules (`mastercard_mip/`, `visa_vap/`) | ⬜ Deferred |
| 3 | Replace static BIN routing with live `scheme_core` lookups | ⬜ Deferred — data dependency (Workstream A Phase 2) is now satisfied, but this item stays deferred until Workstream D itself is explicitly picked up |
| 4 | Switch-side transaction dump export (feeds Workstream B's B0) | ⬜ Deferred |

---

## Pre-existing bugs found during this work (tracked here, not workstream-specific)

| # | Bug | Status |
|---|---|---|
| 1 | HSM credentials untracked but not gitignored | ✅ Fixed |
| 2 | `phoenix_live_view` 1.1.x missing `lazy_html` test dep — blocked all LiveView testing umbrella-wide | ✅ Fixed |
| 3 | `UsersFixtures.valid_user_attributes/0` missing required `name` field — broke `user_fixture/1` broadly | ✅ Fixed |
| 4 | `ilike` raises on MySQL — 29 call sites across 12 files (`da_product_app`, `settlement_core`, `tms_core`, `platform_web`) | ⬜ **Not fixed — needs a decision.** Any search box on those existing pages crashes when actually used. |
| 5 | `user_permissions` missing `granted_by`/`granted_at` columns in **both dev and test DB** — `register_user/1` crashes for any role with default permissions | ⬜ **Not fixed — needs schema owner decision.** Production-path bug, not test-only. |
| 6 | Scheme-picker dropdowns (Card Products, BIN Ranges forms) only listed **active** schemes — a newly-created scheme defaults to `draft`, so an admin building a new scheme end-to-end couldn't attach card products/BIN ranges to it until separately activating it first | ✅ Fixed — pickers now list all schemes/countries/currencies regardless of status |
| 7 | `Phoenix.LiveViewTest`'s `form/3` validates submitted values against the rendered `<select>`'s actual options (matching real browser behavior) — not a bug, but worth noting as a testing gotcha: use `render_submit(view, event, params)` directly to test server-side rejection of an out-of-enum value | ℹ️ Not a bug — test-writing note only |
| 8 | `Ecto.Changeset.validate_format/3` has no `allow_blank:` option (unlike some other frameworks) — passing one is silently ignored. `validate_change` skips `nil` automatically but **not** `""`, so an HTML form's blank optional field (submitted as `""`, not `nil`) would fail format validation unless explicitly normalized first | ✅ Caught via Ecto source review before shipping (`SchemeCore.EmvAid`'s TAC fields) — fixed with a `blank_to_nil/2` step before validation, same pattern reused in `SchemeCore.Capk` |
| 9 | MySQL identifier length limit (64 chars) — an auto-generated compound index name (`scheme_contactless_parameters_scheme_id_country_id_currency_id_index`) exceeded it, failing the migration after two tables had already been created (MySQL DDL isn't transactional, so partial state stuck) | ✅ Fixed — explicit short index name; had to drop the 3 partially-created tables and re-run cleanly since the migration wasn't marked applied. **Lesson for future migrations in this codebase:** give multi-column indexes on long table/column name combinations an explicit `name:` up front |
| 10 | EMV Kernel 2 (Mastercard) amount tags (DF8123-DF8126) in the real Mypinpad config are BCD-encoded (per EMV Book C-2), not raw hex like standard EMV Book 4 tags — treating them as hex gave `161061273` instead of `9999999` for the contactless limit | ✅ Caught by cross-checking the parsed value against the same file's own `metadata.properties.limits` block (which independently states the same figure in decimal) before treating the seed as trustworthy — not caught by a test, caught by not taking the first successful-looking run at face value. Fixed, DB cleaned, re-seeded, re-verified. |
| 11 | `settlement_core`'s `load_interchange_rates.ex` Mix task is stale relative to its own `InterchangeRate` schema — a March 2026 field-rename refactor (`interchange_percentage`→`rate_percentage`, `interchange_fixed_fee`→`fixed_fee`, `effective_date`→`effective_from`, dropped `card_category`/`is_active`/`loaded_at`/`loaded_by`) was never reflected in the task, which still builds `insert_all` attrs and documents a CSV format using the old names | ⬜ **Not fixed — out of scope, belongs to `settlement_core`.** Running the task today would fail or silently write nothing. Found while researching Phase 5 to confirm no existing fee/rate table would collide with the new `scheme_core` ones. |
| 12 | `BinRange.start_bin`/`end_bin` validated 6-8 digits only, based on the original assumption that BINs are always brand-level 6-8 digit prefixes — real Mastercard Account Range Table data uses full 19-digit precise issuer-level ranges (valid under ISO/IEC 7812 extended precision) | ✅ Fixed — widened to 6-19 digits (schema regex, DB column size via migration, `canonical_length` for overlap/PAN-lookup padding, CSV Mix task regex, LiveView form `maxlength`). Found while importing real Mastercard `IP0040T1` data. |
| 13 | `BinRange`'s overlap-duplicate check (built in Phase 2, before the status-gated pattern was established in Phases 3-5) blocked new rows against existing **draft** ranges too, not just active ones — inconsistent with every later schema (`ContactlessParameter`/`RoutingConfig`/`InterchangeRate`/`SchemeFee`), which only check active-vs-active | ✅ Fixed — `find_overlapping_bin_ranges` now only matches `status == "active"`, and `validate_no_overlap` skips the check entirely for non-active new rows, matching the established pattern. Found because 312 real, precise Mastercard sub-ranges legitimately nest inside existing coarse active ranges and need to land as draft pending a hierarchy/activation decision, without being blocked by the coarse row they nest inside. |
| 14 | `TmsCore.TerminalManagement.AutoPushService.push_mf919_emv_config/4` (`apps/tms_core/lib/tms_core/terminal_management/auto_push_service.ex:274,288`) aliases and calls `TmsCore.TerminalManagement.L3ConfigZipBuilder.build_and_store/3` — that module is not defined anywhere on **this branch** (`feature/dispute-processing-system`), confirmed by a repo-wide `grep`/`defmodule` search. | ℹ️ **Not a real bug — branch artifact.** User confirmed L3 config push works fine on their current working branch; `L3ConfigZipBuilder` lives there and just hasn't been merged into this branch yet. Will resolve itself on merge — no action needed here, kept only as a note in case this branch's push path is exercised before that merge happens. |
| 15 | `TmsAcquirerCore.EmvConfig.Generator`'s XML entry-rewriting helper destructured `Enum.map_reduce/3`'s return value backwards — `Enum.map_reduce/3` returns `{mapped_list, final_acc}` in that order, but the code wrote `{reversed_changes, updated_parts} = Enum.map_reduce(...)`, silently swapping the rewritten XML content with the change-log list | ✅ Fixed — corrected the destructuring order (`{updated_parts, reversed_changes}`). Caught immediately by this module's own test suite: a test asserting a no-match case returns byte-identical output instead returned `""`, and a separate test crashed with `Protocol.UndefinedError` (`Enum.join/2` receiving a change-record map instead of a string) — never shipped, caught before any real file was ever touched. |

---

## How to keep this current

- When a phase moves from ⬜ to 🔶 (in progress) or ✅ (done), update its row here first.
- The workstream docs hold the *design*; this file holds the *status*. Don't let them drift —
  if a workstream doc's own `**Status:**` line needs updating, do it in the same edit.
