# CLAUDE.md – Bangla QR Platform

Context for any coding session on this repo. Read this first, then `README.md` and `docs/ARCHITECTURE.md`.

## What we are building
A Bangla QR (Bangladesh national QR standard v2.0, EMV merchant-presented) stack of **three separate systems**,
PostgreSQL, one database per system. Each system is ONE Phoenix 1.8 / LiveView 1.2 app (Bandit, Tailwind + daisyUI),
shaped like the owner's base app `D:\momentPay\technical\bangaldesh\zm\product_base_app-main_fresh` but on Postgres.
Modules are `BqrAcquirer` + `BqrAcquirerWeb` in the same app (not the `app`/`app_web` split).

| System | Project | Port | Notes |
|---|---|---|---|
| Acquirer | umbrella app `apps/bqr_acquirer` | 34100 | real product: merchants, QR generation, credits, merchant webhooks |
| Switch simulator | standalone Mix project `bqr_switch_sim/` (NOT in the umbrella) | 34200 | stands in for NPSB; depends ONLY on `bqr_protocol` via path dep (never `bqr_core`) |
| Issuer | umbrella app `apps/bqr_issuer` | 34300 | payer accounts, scan/validate, pay, hold/finalize; scan UI is a simulator-style LiveView |

Shared, dependency-free libs in the umbrella: `bqr_core` (QR codec), `bqr_protocol` (JSON contract, HMAC, codes).
Systems talk only over HTTP/JSON signed with HMAC. No shared tables or code between systems except those two libs.
Databases: `bqr_acquirer_dev/test`, `bqr_issuer_dev/test`, `bqr_switch_dev/test` on the local PostgreSQL (`postgres/postgres`, localhost:5432; no Docker).

## Decisions already made (do not re-litigate)
* Fresh repo, not a fork of the UPI platform (`upi_psp_platform`). Borrow patterns (adapter + sandbox, event chain,
  special-scenario handlers, LiveView admin, simulators), not code.
* Issuer is its own deployable system, not part of the simulator. Switch simulator is separate too.
* Validation on the issuer side = CRC + framing + field rules (`BqrCore.decode/2`). No pre-validation round trip to a switch.
* Tamper protection is server-side: active merchant lookup (acquirer_id + merchant_pan); for dynamic QRs the amount and
  reference must match the acquirer's stored QR record.
* PostgreSQL (not MySQL). Money is a decimal string with 2 decimals on the wire, integer minor units (poisha) in code.

## Payment flow (the rules that matter)
1 merchant → acquirer: generate QR (static once, dynamic per transaction, with expiry, single use) ·
2 payer scans in issuer, validates locally · 3 issuer places a HOLD, sends `pay_request` to switch ·
4 switch routes `credit_request` to acquirer · 5 acquirer credits merchant, answers · 6 switch answers issuer ·
7 issuer finalizes debit or releases hold, THEN tells the payer · 8 acquirer notifies merchant (webhook) after the credit.
* Never tell the payer "success" before the final response.
* `PENDING` is never final: status inquiry, then reverse.
* Every payment has a unique `txn_id`; switch and acquirer must be idempotent.
* Merchant notification must not affect the payment result.

## Status
* Phase 0–6 done and green: umbrella `mix test` = 38 (`bqr_core`) + 13 (`bqr_protocol`) + 156 (acquirer) + 160 (issuer);
  `cd bqr_switch_sim && mix test` = 112; `cd mobile/bqr_payer && flutter test` = 68; `node --test tools/test/signing.test.mjs` = 7.
  Each system answers `GET /health` as a separate process.
* Phase 3 (acquirer): partners, merchants, PAN/BIN allocation (regular vs micro), static + dynamic QR generation,
  HMAC partner API. Reference: `docs/ACQUIRER_API.md`.
* Phase 4 (switch + credit): switch participant registry, `POST /api/v1/pay`, routing, idempotency by `txn_id`, ledger
  (`docs/SWITCH_API.md`); acquirer `POST /api/v1/switch/credit` (tamper checks, atomic QR consume + credit, idempotent, `credits`
  table). Verified live across three separate processes (pay, replay, QR reuse, wrong amount, forged QR, acquirer down = PENDING).
  Dev data: `mix run priv/repo/seeds.exs` in `apps/bqr_acquirer` and in `bqr_switch_sim` (shared dev switch secret).
* Phase 5 (issuer): payers/accounts/payments/ledger, hold -> debit | release | UNKNOWN (hold kept), LiveView screens (`docs/ISSUER.md`),
  a payer-app JSON API under `/api/v1` guarded by `x-api-key` (off unless a key is configured), dev-only CORS on issuer+acquirer,
  `GET /api/v1/merchants` on the acquirer. Ledger/payment ordering uses a DB `seq` (timestamps tie on Windows).
  Verified live across all three processes with `.\scripts\e2e.ps1`: QR generated by the HTML tool in headless Chrome, verified and paid
  through the app's own API client (dynamic and static -> `COMPLETED/paid`), replay of a paid QR -> `RELEASED/QR_ALREADY_USED`, money balanced.
* Tools: `tools/qr-generator.html` (XHR + in-browser HMAC) and `mobile/bqr_payer` (Flutter; `flutter analyze` + `flutter build web` pass).
  NOT exercised: the camera and the Android build (licences not accepted, no device), and the Windows desktop build.
  Run guide: `docs/RUNNING.md`. Launcher: `scripts/dev.ps1 [-Setup] [-Lan]`.
* Phase 6 (resolve PENDING), owner decisions: a reversed credit keeps its QR **used**; resolve window ~2 min, configurable
  (status checks at 5/15/30/60 s, then reverse; issuer reverses at 180 s as a backstop; `time_scale`/`BQR_TIME_SCALE` shrinks all);
  fault injection through a control page/API, not magic amounts. Built: acquirer `POST /api/v1/switch/status|reverse` + `txn_blocks`
  late-arrival guard (a status/reverse for an unseen txn_id blocks it, a later credit is refused `TXN_NOT_FOUND`) + `credits.status=reversed`;
  switch `POST /api/v1/status|reverse`, `Resolution` + `Resolver` (background), credit retries (`credit_attempts`), `Faults`
  (`/faults` page, `/api/v1/admin/faults` with `x-admin-key`, dev/test only); issuer `Payments.resolve/2`, `BqrIssuer.Resolver`, stuck
  `HOLD_PLACED`/`SENT` recovery, "Check again" = status inquiry (never a re-send); Flutter keeps polling unconfirmed payments.
  Docs: `docs/SWITCH_API.md`, `ACQUIRER_API.md`, `ISSUER.md`. `scripts\e2e.ps1 -Scenario none|lost-answer|lost-request|stuck-then-reversed|late-delivery|slow-switch`
  (start the servers with `scripts\dev.ps1 -TimeScale 0.03`).
  **Verified live** (3 processes, local Postgres on a scratch instance): none, lost-answer, stuck-then-reversed, late-delivery. **Not yet re-run
  cleanly:** lost-request (behaviour seen correct, check script fixed afterwards) and slow-switch (issuer_response delay 30 s vs the 25 s issuer timeout).
  Run those on a machine with a stable Postgres first.
* Phase 7 not started (webhooks, reconciliation, dashboards): see `docs/ARCHITECTURE.md`.
* Decided in phase 4 (owner): every transport failure to the acquirer (timeout, refused, bad status, bad body) is `PENDING`, never
  `FAILED`; responses are not signed; `QR_NOT_FOUND` was added to `BqrProtocol.Codes`.
* Not built yet: merchant onboarding endpoint / admin UI (merchants via `Merchants.create_merchant/2` or seeds), participant secret
  encryption at rest, HMAC does not bind method/path (protocol limitation), merchant webhooks (phase 7; `credits` is the basis).

## Conventions
* Elixir 1.18 / OTP 27, Node 22, Phoenix 1.8.15, LiveView 1.2. `bqr_core`/`bqr_protocol` stay Elixir ~> 1.14 compatible.
* Each system has `Health` (`GET /health`, `db_check` `{m,f,a}` in app env stubbed in tests), `Repo`, `Application`, Phoenix endpoint/router.
* Run one system: `cd apps/bqr_acquirer && mix phx.server` (also `apps/bqr_issuer`, `bqr_switch_sim`), or `scripts/dev.ps1` / `dev.sh` for all three.
  Releases: `mix release acquirer|issuer` from the umbrella; the switch has its own release in `bqr_switch_sim/`.
* Config: root `config/*.exs` serves acquirer + issuer; `bqr_switch_sim/config/` serves the switch. Env vars `<NAME>_PORT/_DATABASE_URL/_SECRET_KEY_BASE/_HOST` (NAME = ACQUIRER, ISSUER, SWITCH).
* daisyUI is **vendored** in each app's `assets/vendor/` (the `daisyui` git dep cloned 5 GB and filled the disk). Do not re-add it as a git dep.
* Windows gotchas: the Tailwind exe fails if `TEMP` is overridden; Mix's own temp files are sometimes denied (transient, retry);
  `phx.new` for standalone apps must run from a scratch dir, never the umbrella root; editor git auto-fetch can lock nested `.git` folders.
* Local Postgres on this machine can PANIC with `could not open file "global/pg_control": Permission denied` (on Windows every new
  Postgres process re-reads that file; something intermittently denies it, likely the antivirus, Quick Heal). Connection errors /
  `57P03 recovery mode` / `tcp recv: closed` in a test run are that, not the code: wait for `pg_isready` and re-run. It recovers in
  seconds, but a postmaster ORPHANED by a service/system crash can loop forever: if the service shows STOPPED yet `postgres.exe` is running,
  kill all `postgres.exe` as admin (or restart Windows) and start the service (`docs/RUNNING.md`). Real fix: an antivirus exclusion for
  the Postgres data dir. `autovacuum_naptime = '15min'` is set to cut background process starts.
* Tests can use another Postgres via `BQR_DB_PORT` (default 5432) and a smaller pool via `BQR_DB_POOL` (fewer connections = fewer
  process spawns = fewer crashes).
* Repo: `https://github.com/momentpay/zztlbd` (branch `main`). `.gitattributes` forces LF. `docker-compose.yml` is obsolete and untracked; delete it.
* Never commit `*.pem`/secrets. (The UPI project has a `private.pem` at its root; do not copy it.)
* Confirm before large design changes: the owner wants a confirmation step before complex design work.

## Unverified assumptions (confirm with NPSB / Bangladesh Bank)
1. TLV length counts characters, not bytes (`BqrCore.TLV`).
2. Tag 26 GUID `BD.NPSB.QR` is a placeholder (`config :bqr_core, npsb_guid`).
3. Merchant PAN = 6-digit BIN + zero-padded serial, 16 chars, no check digit; the acquirer ID list (spec Annex B) may be outdated.
4. `bqr_protocol` JSON/HMAC contract is invented for the simulator; real NPSB interface is unknown.
5. Fees (zero interchange from 1 Oct 2026, MDR rules) come from press reports; read the BB circulars before coding fees.

## Source material
* `BanglaQR – National QR Code Standard for Retail Payments in Bangladesh v2.0` (Nov 2021), in the owner's `…\bangaldesh\zm` folder.
* `Brac Bank Middleware sample data_for_push.pdf`: a merchant soundbox/IoT push payload (JSON via MQTT), a model for the
  acquirer's merchant notification.
* `HANDOVER.md` records the restructure session; its §3 steps 1–8 are now done.
* Reference UPI implementation: `D:\momentPay\Products\Acuiring-Switch\upi-switch\upi_psp_platform`.

## Next task
1. On a machine with a stable Postgres: `mix test` in `apps/*` and `bqr_switch_sim`, then `scripts\e2e.ps1 -Scenario lost-request` and
   `-Scenario slow-switch` (and the others) against servers started with `scripts\dev.ps1 -TimeScale 0.03`.
2. Phase 7: merchant webhooks after the credit (must not affect the payment result; `credits` is the basis), reconciliation, dashboards.
3. Open: Flutter app on a real phone (accept Android licences, `scripts\dev.ps1 -Lan`).
Confirm the design with the owner before phase 7 (see "Confirm before large design changes").
