# YSP card integration: Sprint 1 steps (Postman → middle layer → simulator)

Run these one at a time, in order. Each step says what changes, how it is tested, and what "done" looks like. No commits or pushes unless asked. Sprints 2–4 are in `CLAUDE.md` and are not started.

Simulator contract used throughout (headers `apikey`, `clientid` 6 alphanumerics, `channeltype: API`, `netid`, `timestamp` 12–16 digits; body `msgHdr{msgId,timestamp,channelType}` + `txnRefNo`, 1–32 chars, single use):

| Action | Simulator route | apikey | Body fields |
|---|---|---|---|
| Balance | GET `/naradaapiservices/api/v1/cardbalance` (JSON body) | `APACBL` | `proxyNumber` |
| Load | POST `.../accountfunding` | `APLCRD` | `proxyNumber`, `productInfo{productCode, billingAmount, billingCurrency, loadAmount, loadCurrency}` |
| Block | POST `.../cardlockunlock` | `ACDSTS` | `proxyNumber`, `flag` (`L`, `UL`, `BL`, `BS`, `BD`), `reason` |

Seed card: proxy `000000012845`, product `MCCAED`, AED, status `I`.

## Step 1: Baseline check (no edits)
- Run `mix test` in the repo root and `mix test` in `ysp-simulator/`. Record which tests pass and fail before any change.
- Check Postgres is reachable for `mix phx.server`.
- Done when: baseline results are written down, so later failures are known to be new or old.

## Step 2: Config
- `config/runtime.exs`: add, above the `config_env() == :prod` block, `config :da_product_app, :ysp_card, base_url: ..., client_id: ..., net_id: ..., timeout_ms: ...` from `YSP_CARD_BASE_URL` (default `http://localhost:4180`), `YSP_CARD_CLIENT_ID` (`SNDBOX`), `YSP_CARD_NET_ID` (`APIUSER`), `YSP_CARD_TIMEOUT_MS` (`5000`).
- Tests (`test/da_product_app/ysp_card/config_test.exs`): defaults are used when env vars are unset; each env var overrides its value.
- Done when: `mix compile` is clean and config tests pass.

## Step 3: Request builder (pure functions, no HTTP)
- New `lib/da_product_app/ysp_card/request.ex`: builds headers (apikey per endpoint, clientid, channeltype, netid, timestamp) and `msgHdr`, and fills `txnRefNo` from `TxnRefGenerator` when the caller doesn't give one.
- Tests (`request_test.exs`): each header present and valid against the simulator's regexes; `timestamp` is 12–16 digits; `msgHdr` has msgId, timestamp and channelType; generated `txnRefNo` is 1–32 chars; a supplied `txnRefNo` is kept; two generated values differ; the correct apikey for balance, load and block.
- Done when: all pass with no network.

## Step 4: Client module `DaProductApp.YspCard`
- New `lib/da_product_app/ysp_card.ex` with `balance/1`, `load/1`, `block/1`, using Req. Balance sends GET with a JSON body. No retry. Short explicit timeout from config. Returns `{:ok, http_status, decoded_body}` or `{:error, :timeout | :connection_refused | {:bad_response, body} | other}`.
- Tests (`ysp_card_test.exs`, using a small `Plug.Router` stub on the Req `:plug` option, so no real server): success for each action returns the stub's decoded body; correct method, path and headers reach the stub; stub returns an error code → still `{:ok, 200, body}` (YSP errors are passed through); non-JSON response → `{:bad_response, _}`; unreachable host → `:connection_refused`; slow stub → `:timeout`; confirms exactly one request is made on a failure (no retry).
- Risk: JSON body on GET. If Req can't, use a Finch/Mint fallback. Decide here, before the controller.
- Done when: client tests pass.

## Step 5: Controller `YspCardController`
- New `lib/da_product_app_web/controllers/ysp_card_controller.ex` with `balance`, `load`, `block`. It checks required fields (400 with `{error, message}` if missing or malformed), calls `YspCard`, and returns the simulator body unchanged with 200. Timeout → 504, connection failure → 502, both with plain-language messages.
- Tests (`ysp_card_controller_test.exs`, with the client pointed at the stub): each action happy path (body passed through unchanged); missing `proxyNumber`; missing `productInfo` fields on load; bad `flag` or missing `reason` on block; simulator error code (e.g. `INVCRD`) passed through with the simulator's text; timeout → 504; refused → 502; no stack trace in any error body.
- Done when: controller tests pass.

## Step 6: Routes
- `router.ex`: add `scope "/api/card", DaProductAppWeb do pipe_through :api` with `POST /balance`, `/load`, `/block`. Existing `/api` routes have no auth, so these match for now; flag it in the summary.
- Tests (`router_test` or inside the controller test): the three routes resolve to the right actions; GET on them returns 404/405; non-JSON content type is rejected by the `:api` pipeline.
- Done when: `mix phx.routes | grep card` shows the three routes.

## Step 7: Real simulator integration tests
- Tagged `@tag :integration` and excluded by default (`test_helper.exs`: `ExUnit.configure(exclude: [:integration])`). They run against a live simulator on 4180 (`mix test --include integration`).
- Tests: reset simulator → balance equals the seed amount; load adds the amount and balance matches; block with `L` succeeds and the second `L` returns the simulator's error code; unknown proxy → `INVCRD`; replaying the same `txnRefNo` → `TIDEXT` (passed through); `/_sim/requests` shows each call with the right apikey.
- Done when: they pass against a running simulator and are skipped in a normal `mix test`.

## Step 8: Postman collection and README
- `docs/postman/bank-bridge-ysp-card.postman_collection.json`: Reset note, Balance, Load, Balance-after, Block, Block-again, Unknown card; `baseUrl` variable (`http://localhost:4006`); `txnRefNo` omitted so it's generated; Postman test scripts that assert the status code and `responseCode` in each request.
- README snippet: start order, env var, how to import and run.
- Tests: validate the JSON parses and each request URL is under `/api/card/`. Run the collection with Newman if installed, otherwise the equivalent curl commands.
- Done when: running the collection top to bottom passes.

## Step 9: End-to-end run (the demo)
- Start the simulator (4180) and reset it; start `YSP_CARD_BASE_URL=http://localhost:4180 mix phx.server` (4006).
- Run the collection and read `GET localhost:4180/_sim/requests`.
- Stop the simulator and call balance: expect a clear 502/504 message.
- Done when: all expected results from Step 8 are seen and the error case is clean.

## Step 10: Full regression and wrap-up
- `mix format --check-formatted` on new files, `mix compile --warnings-as-errors` for new modules, `mix test` in the repo root and in `ysp-simulator/` and compare with Step 1.
- Write the list of changed files and a short status note. Stop and wait before Sprint 2.
- Done when: no new failures versus baseline.

## Test count target
Roughly 45–55 tests: config (4), request builder (9), client (10), controller (12), routes (4), integration (6), plus the Postman assertions.

## Step 1 results (baseline, 2026-10-30, branch task/ysp-card-postman-flow)
- Simulator (`cd ysp-simulator && mix test`): 367 tests, 0 failures.
- Middle layer: the `test` alias fails at `ecto.migrate` (three migrations share version 20250707). Bypassed with `MIX_ENV=test mix run --no-start -e 'Mix.Tasks.Test.run([])'`. Result: 172 tests, 148 failures before any change of ours. Errors: 133 `MyXQL.Error` (the test database isn't migrated), 8 `UndefinedFunctionError`, 3 `ArgumentError`. Most failures are in `UsersTest`, `UserAuthTest`, `YspNotificationServiceTest`, `UserSessionControllerTest`, `TransactionsTest`.
- Judge our work against this: new tests must pass, and the 148 existing failures must not grow. Our tests must not need a migrated database.
- Reminder before each commit: use `git add <specific paths>`, never `git add -A`. `_build/` and `deps/` files are tracked in this repo and `mix deps.get` modified them.

## Steps 2-10 results (2026-10-30)
- Steps 2-7 and 8 are committed. Step 8: Postman collection (`docs/postman/`) and README section; Newman run: 8 requests, 9 assertions, 0 failures.
- Step 9 (end to end, own ports 4014/4181): collection passes. With YSP down, balance and load return HTTP 502 `ysp_unreachable` ("The request was not sent") in about 0.2 s and the log has no stack traces. A live 504 timeout was not tried; the controller tests cover it.
- Step 10 (regression):
  - YSP tests (config, request, client, controller, routes): 64 tests, 0 failures. Integration tests (live simulator): 6, 0 failures.
  - Middle layer full suite: 242 tests (172 before + 64 new + 6 integration excluded), 148 failures, the same count as the Step 1 baseline. No failure is in a YSP test file.
  - The names of the 148 failing tests are not stable: they depend on test order and the unmigrated database, so a name-by-name comparison with the baseline is unreliable. Two runs with the same seed differed by 2 names.
  - Simulator: 367 tests, 0 failures. New files are `mix format` clean and add no compile warnings.
- Not done / open: `/api/card/*` has no authentication; duplicate migration version 20250707 breaks `mix test`'s migrate step; live 504 not exercised.

# Add the remaining 37 card endpoints (Steps 11-19)

Goal: every simulator card endpoint (40 in total; balance, load and block are done) reachable from Postman/Bruno through `/api/card/*`. Safeguards for critical calls (call log, unknown-outcome handling, auth) are deliberately left for later, when the simulator is replaced. Simulator only until then.

Design: one table, `DaProductApp.YspCard.Endpoints`, with one entry per endpoint: name, YSP method and path, `apikey`, criticality (`:critical` or `:standard`, per CLAUDE.md), and required top-level fields. The request builder, the client, the controller and the Postman collection all read it. Balance, load and block keep their own stricter validators. The other 37 go through one generic action `POST /api/card/:endpoint`, which looks the name up in the table (unknown name = 404 JSON), checks required fields, forwards one request (no retry) and returns YSP's body unchanged. Sample bodies come from `ysp-simulator/demo.sh`, `docs/endpoints.md` and the simulator tests. Where the simulator accepts GET and POST, use the method the spec's sample uses (GET).

## Step 11: Endpoint table
- New `lib/da_product_app/ysp_card/endpoints.ex` with all 40 entries (names below), `fetch/1`, `names/0`, `critical?/1`. Move the 3 existing entries into it and make `Request` read from it. Behaviour unchanged.
- Tests: 40 entries, names unique; every `apikey` unique and 6 characters; each path is unique; criticality set for all; balance/load/block entries equal what Step 3 used; the 55 existing YSP tests still pass.
- Done when: table and existing tests pass. Cross-check: a test compares each entry's path and apikey with the routes in `ysp-simulator/lib/ysp_simulator/router.ex` (parsed from the file, skipped if the folder is missing).

## Step 12: Generic call, controller action and route
- `YspCard.call(name, params)` (one request, no retry, same return values as before). Controller action `call` and route `post "/:endpoint"` added after the three explicit routes. Unknown name: 404 `{"error":"unknown_endpoint"}`. Missing required fields: 400 naming them. Same 502/504 messages; for critical entries the timeout and bad-response messages keep the "may or may not have been applied" wording.
- Tests (stub server, no DB): path/method/apikey per endpoint for a sample of each kind (GET, POST, critical, standard); unknown name 404; missing field 400 and nothing sent; YSP error code passed through; timeout 504 wording differs for critical vs standard; no retry on failure; `msgHdr` cannot be forged; the three explicit routes still win over `:endpoint`; route table test.
- Done when: new tests and the old 64 pass.

## Step 13: Batch A, read-only (11 endpoints)
carddetails (APCHDT), transactiondetails (APXTXN), exchangerate (APFREX), statusrequest (APSTRQ), cardnumberdetail (APIGCN), cscenquiry (ACSENQ), cardorderstatus (APCOST), getlimit (AGTLMT), stockdetails (APGTSD), accessrights (APACRT), retrievewebprofile (APUSPF). All `:standard`.
- Table entries with required fields taken from each simulator handler, and one sample body each in `test/support/ysp_card_samples.ex` (shared by tests and the Postman generator).
- Tests: unit tests for the entries; integration tests (`:integration`, live simulator) call each endpoint with its sample body and assert a `responseCode` of `00` (or the documented success code) and that the simulator log shows the right path and apikey. About 22 tests.

## Step 14: Batch B, card setup and settings (11 endpoints)
customerregistration (APNCRD, purchase card, `:critical` because it loads money), customerupdate (APUPCR), cardactivation (APCDAC, `:critical`), cardreplacement (APCDRP, `:critical`), accountaddition (APACAD), cardorder (APCROR), cardreceipt (APSTRC), updatestockdetails (APSKUP), alertsettings (APALTS), setlimit (ASTLMT), assignoverridesettings (AOVRST).
- Several depend on earlier ones (purchase a card, then activate it). Integration tests reset the simulator and run these as short flows. About 24 tests.

## Step 15: Batch C, money movement (4 endpoints, all `:critical`)
amountrefund (APCRND), accountclosure (APACCL), debitcreditaccount (APCRDR), fundtransfer (APFDTR).
- Integration tests check balance before and after, and that a replayed `txnRefNo` returns `TIDEXT` and applies only once. About 10 tests. No new safeguards in this step.

## Step 16: Batch D, PIN and authentication (11 endpoints, all `:critical`)
atmpinset (APINST), atmpinchange (APICHG), atmpinverification (APIVER), ivrpinset (APIIVR), ivrpinchange (APIIVC), ivrpinvalidation (APIPNV), pinretrieval (APIPIR), newotprequest (AFPOTP), otpvalidation (AVPOTP, reads the OTP from the simulator's `/_sim/otps`), changepassword (ACHPSS, uses `netid`), resetwebpassword (APFORP).
- The ATM PIN calls need an encoded PIN block (see `ysp-simulator/docs/pins.md`). The test support builds one with the simulator's own `PinBlock` logic (copied into test support, or computed in the test). Hardest batch, and the most likely to need adjustments. About 22 tests.
- PIN blocks and OTPs are secrets: sample values are fake and the controller does not log request bodies (no logging of bodies is added in this work).

## Step 17: Postman collection for all endpoints
- A generator script `docs/postman/generate.exs` (run with `mix run`) builds the collection from the table and the samples so the two stay in sync. Output: folders A to D plus the existing Sprint 1 requests, a reset request, and `pm.test` checks on status and `responseCode`. Committed alongside the generated JSON.
- Verified with Newman (installed in a throwaway folder and deleted afterwards): all requests pass, in order, against a fresh simulator. About 45 requests.
- README: endpoint table for all 40, grouped, with criticality.

## Step 18: Full integration sweep
- One integration test loops over every table entry with its sample body and asserts the endpoint is recognised (no `INVAPI`, no 404) and that the simulator log has exactly one request for it. Catches any wrong path, method or apikey in the table.

## Step 19: Regression
- Same as Step 10: YSP tests, integration tests, simulator suite (367), full suite count against the 148-failure baseline, format check, compile warnings. Record results here and commit.

## Test count target
About 110 new tests on top of the current 64 (plus 6 integration): table 8, generic call 14, integration 70 (Steps 13-16), sweep 2, Postman 45 requests by Newman.

## Risks
- Required fields per endpoint come from reading each handler; a few handlers accept many optional fields. Validation stays at top-level required fields only, and YSP's own validation answers the rest.
- Steps 14 and 16 involve multi-step flows (purchase, activate, OTP, PIN block) and may need more than one pass.
- Critical endpoints work but have no safeguards. Do not point `YSP_CARD_BASE_URL` at real YSP until the call log, unknown-outcome handling and authentication are in.

## Steps 11-13 results (2026-10-30)
- **Step 11 (endpoint table), `030e245`.** `DaProductApp.YspCard.Endpoints` lists all 40 simulator card endpoints (method, path, apikey, criticality: 20 critical, 20 standard). `Request` reads from it; balance, load and block behave as before. 16 new tests, including one that parses `ysp-simulator/lib/ysp_simulator/router.ex` and checks every path, apikey and method. One old test changed: `Request.actions/0` now returns all 40 names. YSP tests: 80, 0 failures.
- **Step 12 (generic route), `2b6f694` and format fix `1e69498`.** `POST /api/card/:endpoint` goes to `YspCardController.proxy` (after the explicit balance, load and block routes). Unknown name: 404. Missing required field: 400 naming it, nothing sent. Body forwarded unchanged, `msgHdr` cannot be forged, one request per call with no retry, and timeout/unreadable-answer messages say "may or may not have been applied" for critical endpoints. `YspCard.call/2` is public. 25 new tests. YSP tests: 105, 0 failures.
- **Step 13 (Batch A, 11 read-only endpoints), `27ddbca`.** Required fields set for the 11 endpoints, each checked against its simulator handler (`carddetails` has none: it takes `proxyNumber` or `customerId`). 3 more unit tests, 13 integration tests against a live simulator (19 integration tests in total, 0 failures), and a Postman folder "A. Read-only endpoints" (13 requests). Newman through the real middle layer: 21 requests, 24 assertions, 0 failures. YSP unit tests: 108, 0 failures.
- **Finding:** `transactiondetails` follows the spec's date rules in the simulator: `fromDate` no more than six months back and at most 90 days between the dates, otherwise `INVDAT`. The tests and the Postman request compute yesterday and today.
- **Still open:** Steps 14 (card setup and settings) and 15 (money movement) not started. `/api/card/*` still has no authentication. The `20250707` migration clash still breaks `mix test`'s migrate step. Critical endpoints have no call log, unknown-outcome handling or retry rules beyond "never retry", to be added when the simulator is replaced.
- Steps 16 to 19 (PIN and authentication batch, generated Postman collection, full sweep, final regression) are out of scope for now.

## Step 14 results (2026-11-02)
- **Batch B (11 endpoints) done.** Required fields set in `Endpoints` for customerregistration, customerupdate, cardactivation, cardreplacement, accountaddition, cardorder, cardreceipt, updatestockdetails, alertsettings, setlimit and assignoverridesettings, each checked against its simulator handler. Criticality unchanged from Step 11 (customerregistration, cardactivation, cardreplacement critical; the rest standard).
- Required-field choices: `customerupdate` requires `customerInfo` only (the card is found by `proxyNumber` or `customerId`). `alertsettings` and `assignoverridesettings` also require `productCode`, because the simulator needs it to find the account.
- Tests: 2 new unit tests in `endpoints_test.exs`, 26 new integration tests in `test/da_product_app/ysp_card_batch_b_integration_test.exs` (live simulator: purchase, activate, replace, order, receipt and damaged stock run as flows). Integration run: 51 tests, 0 failures. YSP unit tests: 98, 0 failures. The other failures seen in `test/da_product_app_web/controllers` are the old login and notify_payment ones from the Step 1 baseline.
- Postman: folder "B. Card setup and settings" (B1-B11) added before "4. Block". Newman through the real middle layer against a fresh simulator: 32 requests, 37 assertions, 0 failures.
- Notes: B4 (account addition) needs a freshly reset simulator, since a card can hold a product once. Every other B request can be run again; B10 builds its pack numbers from the clock.
- Not committed.

## Step 15 results (2026-11-02)
- **Batch C (4 critical money-movement endpoints) done.** Required fields set for amountrefund, accountclosure, debitcreditaccount and fundtransfer, checked against the simulator handlers. All four are `:critical` (unchanged from Step 11). No new safeguards, as planned.
- Tests: 2 new unit tests in `endpoints_test.exs`, 24 new integration tests in `test/da_product_app/ysp_card_batch_c_integration_test.exs`. They check the balance before and after each call. For refund, debit/credit and fund transfer, a replayed `txnRefNo` returns `TIDEXT` and the amount is applied once. Also covered: `**` full refund, `EXEAMT`, `LSSBAL`, `NOEXRT`, conversion at a saved exchange rate, `NOTSAM`, `ZETRAM`, closing an account with a balance (`BALNT0`), and a closed card rejecting later changes (`CRDCLD`). Integration run: 74 tests, 0 failures. YSP unit tests: 100, 0 failures.
- Two older tests in `ysp_card_proxy_test.exs` called `fundtransfer` and `amountrefund` with a bare `proxyNumber`. Those endpoints now have required fields, so the calls were rejected with 400 before reaching the stub. Their bodies were completed; what they check is unchanged.
- Postman: folder "C. Money movement" (C1-C9) added before "4. Block", including a replayed txnRefNo (C3 returns `TIDEXT`). Newman through the real middle layer against a fresh simulator: 41 requests, 49 assertions, 0 failures.
- Reminder: these endpoints still have no call log, unknown-outcome handling or authentication. Do not point `YSP_CARD_BASE_URL` at real YSP.

## Step 16 results (2026-11-02)
- **Batch D (11 critical PIN and authentication endpoints) done.** Required fields set for atmpinset, atmpinchange, atmpinverification, ivrpinset, ivrpinchange, ivrpinvalidation, pinretrieval, newotprequest, otpvalidation, changepassword and resetwebpassword, checked against the simulator handlers. All `:critical` (unchanged from Step 11).
- Required-field choices: `atmpinchange` requires only `proxyNumber`, because the old and new PIN blocks have two accepted names each. `otpvalidation` requires `txnRefNo` (it must be the one `newotprequest` used, so it is never generated). With this batch, only `balance`, `load`, `block` (own validators) and `carddetails` (alternatives) have an empty list.
- PIN blocks: `test/support/ysp_pin_block.ex` (`DaProductApp.YspPinBlock`) builds and reads format-0, triple-DES blocks under the simulator's test key, copied from the simulator's logic. A test checks it against the worked example in `ysp-simulator/docs/pins.md` (1234 on 4111111111111111 gives 2a3d408a1977dde9).
- Secrets: all PINs, PIN blocks, OTPs and passwords in tests and Postman are fake. A new controller test checks that none of them reach the log when the call fails (non-JSON answer or 503) for atmpinset, ivrpinset, otpvalidation and changepassword.
- Tests: 3 new unit tests in `endpoints_test.exs`, 4 in `ysp_pin_block_test.exs`, 1 in `ysp_card_proxy_test.exs`, 26 new integration tests in `test/da_product_app/ysp_card_batch_d_integration_test.exs` (set, verify and change ATM PIN; IVR PIN set, validate and change; OTP issue, validate once, replay; password change and history; web password reset). Integration run: 100 tests, 0 failures. YSP unit tests: 108, 0 failures.
- Postman: folder "D. PIN and authentication" (D1-D15) before "4. Block". PIN blocks and password hashes are built in the pre-request scripts with the sandbox's CryptoJS (a `const CryptoJS` redeclaration is a SyntaxError there, so the global is used). D11 reads the OTP straight from the simulator. Newman through the real middle layer against a fresh simulator: 56 requests, 65 assertions, 0 failures.
- Limits of the Postman folder: D6 (pinretrieval) asserts `103` for a wrong CVV, because the simulator derives the CVV from the card number and Postman cannot compute it. The success path is in the integration tests. D14 needs a freshly reset simulator. D15 (resetwebpassword) asserts `INVUSR`, because the middle layer sends netid `APIUSER`, which has no web profile; start it with `YSP_CARD_NET_ID=CUSTOMER1` to see `00`.
- Reminder: these endpoints still have no call log, unknown-outcome handling or authentication. Do not point `YSP_CARD_BASE_URL` at real YSP.

## Step 17 results (2026-11-02)
- **The Postman collection is now generated** from the endpoint table: `mix run --no-start docs/postman/generate.exs` writes `docs/postman/bank-bridge-ysp-card.postman_collection.json` and the endpoint tables in `README.md` (between the `ysp-endpoints` markers). The requests and scripts live in `docs/postman/builder.exs`. Each request's URL, and the method, apikey and criticality in its description, come from `Endpoints`. The builder raises if a table endpoint has no request, or a request names an endpoint that is not in the table.
- The generated collection has the same 56 requests and 65 assertions as the hand-edited one (requests 0-7, folders A to D). Newman through the real middle layer against a fresh simulator: 56 requests, 65 assertions, 0 failures, twice in a row (request 0 resets the simulator, so it can be re-run). All 40 endpoints have at least one request; D11 and request 0 go straight to the simulator.
- README: heading is now "YSP card API (all 40 endpoints)", with a generated table of all 40 grouped by folder (route, YSP call, apikey, criticality, required fields) and a note on the 404/400/no-retry behaviour.
- Tests (`test/da_product_app/ysp_card/postman_collection_test.exs`, 8 tests, no network): the committed JSON and the README tables equal the generator's output (so a change to the table without regenerating fails), every table endpoint has a request, every request URL is a table endpoint or `/_sim/`, every middle-layer request is a POST with a JSON body and a test script, descriptions carry the table's criticality and apikey, and bodies contain each endpoint's required fields (request 7 leaves one out on purpose). YSP unit tests: 116, 0 failures.
- Deviation from the plan: the plan said sample bodies would live in `test/support/ysp_card_samples.ex`, shared by tests and the generator. They do not: the integration tests build their bodies inline (several depend on earlier calls and fresh ids), and the Postman bodies are in `builder.exs`. The Postman bodies are checked against the table's required fields by the test above.
- Newman was installed in a throwaway folder and deleted afterwards.

## Step 18 results (2026-11-02)
- **Full integration sweep** (`test/da_product_app/ysp_card_sweep_integration_test.exs`, 3 tests, live simulator): calls every one of the 40 table entries once with its required fields filled with dummy values.
  - Every call gets a normal YSP answer (HTTP 200 with a `responseCode`) that is not `INVAPI`. Most answer with a data error (`INVCRD`, `11`, ...), which is expected: the sweep checks the endpoint is recognised, not that it succeeds.
  - The simulator log has exactly 40 requests, one per endpoint, and their method, path and apikey equal the table's.
  - A control test sends a wrong apikey straight to the simulator and expects `INVAPI`, so the "not INVAPI" check can fail.
- Checked that the sweep catches a mistake: with the `fundtransfer` apikey changed in the table to a wrong value, the test failed naming `fundtransfer` and the apikey. The table was restored afterwards (no change to `endpoints.ex`).
- Integration run (all batches plus the sweep): 103 tests, 0 failures. YSP unit tests: 116, 0 failures.
- Step 18 found no errors in the table.

## Step 19 results (2026-11-02): final regression for Steps 11-18
- **Simulator suite** (`cd ysp-simulator && mix test`): 367 tests, 0 failures (same as Step 1).
- **Middle layer full suite** (run through `Mix.Tasks.Test.run([])`, because the `test` alias fails at the duplicate 20250707 migration): 403 tests, 148 failures, 103 excluded (the integration tests). The failure count is the same as the Step 1 baseline (148). Total tests went from 172 to 403 (+231: 128 unit and 103 integration; the integration ones are excluded here).
- **No new failures:** no failure is in a YSP card, PIN block or Postman test file. The failing files are the baseline ones (users, user auth, live views, sessions, transactions, software, sbom, alipay webhook, page controller, `YspNotificationServiceTest`, 10 failures, the old notification service, not the card client). As noted in Step 10, the names of the failing tests are not stable between runs (unmigrated database), so the comparison is by count and by file.
- **YSP unit tests** (config, request, client, controller, routes, endpoints, PIN block, Postman collection): 116, 0 failures.
- **Integration tests** (live simulator, batches A to D and the sweep): 103, 0 failures (last run at Step 18; no code changed since).
- **Format:** every YSP file changed since Step 10 is `mix format` clean, plus `docs/postman/*.exs`. **Compile:** a forced recompile shows no warning from any YSP file (the repo has many older warnings elsewhere, so `--warnings-as-errors` is not usable repo-wide).
- **Postman:** Newman, 56 requests, 65 assertions, 0 failures, twice in a row (Step 17).

### Where things stand after Step 19
- All 40 simulator card endpoints are reachable through `/api/card/*`, tested, and in the generated Postman collection.
- Still open, as noted from the start: `/api/card/*` has no authentication; critical endpoints have no call log, unknown-outcome handling or retry rules beyond "never retry"; the duplicate 20250707 migration still breaks `mix test`'s migrate step; a live 504 timeout was only covered by controller tests; the collection has not been tried in Bruno. Do not point `YSP_CARD_BASE_URL` at real YSP until the call log, unknown-outcome handling and authentication are in (Sprints 2 to 4 in CLAUDE.md).

## Replay demo added to the Postman collection (2026-11-02)
- Request 2 (load) leaves `txnRefNo` out on purpose, so every call is a new load and all of them succeed. That is not a replay demo. Added requests 3a (load 10 AED with a fixed `txnRefNo`), 3b (the same call again: `TIDEXT`) and 3c (balance went up by 10 only once). The collection now has 59 requests, 68 assertions. In a Newman run, 3a to 3c passed. A clean full-collection run on an isolated port was not completed (see below), so the full 59/68 figures are not re-confirmed.
- Lesson: before starting a middle layer for a test, check that port 4006 is free. A second app instance also uses the same MQTT `client_id` (`lib/da_product_app/mqtt.ex`) as any other running instance, which makes the broker drop the other connection.

### Clean full-collection re-run (2026-11-02)
- Newman, 59 requests, 68 assertions, 0 failures, twice in a row, through the real middle layer against a fresh simulator on own ports (4014 / 4181). This closes the open re-run from the replay demo.
- The middle layer was started without the MQTT children (`mqtt.ex` hardcodes host `demo.ctrmv.com` and a fixed `client_id`, so a second normal instance disconnects any other running one). Only Endpoint, Repo, PubSub, Finch, Cachex and Telemetry were started. Newman was installed in a throwaway folder and deleted afterwards.
