# Issuer (phases 5 and 6)

Base URL `http://localhost:34300`. The payer's side of a Bangla QR payment, as a simulator-style LiveView app. There is
no login: you pick which payer you are acting as. The issuer exposes no payment API of its own; it is a client of the switch
([SWITCH_API.md](SWITCH_API.md)).

## Screens

| Path | What |
|---|---|
| `/` | Accounts: create a payer (with an opening balance), top up, see balance / held / available, jump to pay |
| `/payers/:id/pay` | Scan and pay: camera or pasted QR, review, confirm, result |
| `/payments` (`?payer=<id>`) | History, live. An unconfirmed payment has **Check again** |

## The payment flow

```
scan ─▶ review ─▶ HOLD_PLACED ─▶ SENT ─▶ COMPLETED   switch said SUCCESS: hold becomes a debit
                                      ├▶ RELEASED    switch said FAILED:  hold is given back
                                      └▶ UNKNOWN     PENDING / timeout / error: hold is KEPT
              REJECTED  (refused locally: insufficient funds, blocked payer; nothing held, nothing sent)
UNKNOWN ─▶ COMPLETED | RELEASED   found by status inquiry, or reversed after the window (phase 6)
```

1. **Scan.** The QR comes from the camera (browser JS, `jsQR`) or a pasted string. Either way the *server* decodes it with
   `BqrCore.decode/2` (CRC, framing, field rules). Nothing from the browser is trusted, and there is no round trip to the switch.
   A dynamic QR fixes the amount; for a static QR the payer enters it.
2. **Hold.** One atomic statement takes the hold only if `balance - held >= amount`. Concurrent payments therefore cannot
   overspend, and the database also refuses `held > balance` (check constraint). Not enough money: the payment is `REJECTED`,
   nothing is held and nothing is sent.
3. **Send.** The state is written as `SENT` *before* the signed `pay_request` goes out, from a supervised background task
   that does not depend on the browser tab. The screen learns the result over PubSub (and re-reads every 4 s as a backstop).
4. **Final answer.** `SUCCESS` turns the hold into a debit; `FAILED` releases it. **Only then** does the screen say anything final.

### Rules the code enforces

* The payer is told "successful" only when the payment is `COMPLETED`. A `FAILED`/`REJECTED` result says "you were not charged".
  Anything else says the money is held and the outcome is not yet known.
* **`UNKNOWN` is never final and never a guess.** `PENDING`, a timeout, a refused connection, a non-2xx, an unreadable body, a
  `txn_id` that does not match, a contradictory status/code, or a crash while sending all keep the hold. The money is neither
  debited nor released until the switch gives a final answer.
* `FAILED` with code `DUPLICATE_TXN` is also `UNKNOWN`, not released: it only says the `txn_id` was used by *another* request,
  which does not prove this payment failed.
* State changes are conditional updates, so a duplicate or late answer cannot debit twice, release twice, or do both. A final answer
  that contradicts an already-final payment is logged at error level and changes nothing.
* **Resolving `UNKNOWN` (phase 6).** `BqrIssuer.Resolver` works through unconfirmed payments by itself: a **status inquiry** to the
  switch (`POST /api/v1/status`, same `txn_id`) after 5 s, 15 s, 30 s, 60 s, then every 60 s; `SUCCESS` debits, `FAILED` (any code
  other than `DUPLICATE_TXN`) releases, `PENDING` or no answer keeps the hold and schedules the next look. A payment unknown for
  `reverse_after_s` (180 s, longer than the switch's own 120 s so the switch normally settles first) is **reversed**
  (`POST /api/v1/reverse`): `FAILED REVERSED` releases, `SUCCESS` (it had in fact been credited) debits, anything else keeps
  the hold and tries again every 30 s. The money is never released on a guess: only the switch's word moves it.
* **Interrupted payments.** A payment stuck in `HOLD_PLACED` / `SENT` for longer than `inflight_window_s` (the node crashed
  between taking the hold and hearing back) is resolved the same way. A status inquiry for a `txn_id` the switch never received
  is answered `TXN_NOT_FOUND` *and blocks it there*, so releasing the hold is safe even if the original request is still on
  the wire.
* **"Check again"** (web and `POST /payments/:id/retry`) is a status inquiry right now, not a second `pay_request`: it can never charge twice.
  `send_payment/1` can still re-send an `UNKNOWN` payment with the same `txn_id` (the switch replays what it stored), but nothing in the UI uses it.
* The payer screen follows along: when the resolver settles a payment the open page changes from "Waiting for confirmation" to the
  result without a refresh.

## Mobile API

The payer app ([mobile/bqr_payer](../mobile/bqr_payer/README.md)) uses a small JSON API under `/api/v1`. It can spend a payer's money, so:

* every call needs `x-api-key: <key>`; wrong or missing is `401 unauthorized`;
* **with no key configured the API is off** (`503 api_disabled`), never open. Dev has a fixed key in `config/dev.exs`
  (`dev-only-mobile-api-key-0123456789`); in production set `ISSUER_MOBILE_API_KEY`;
* this is simulator access control, not customer authentication: whoever has the key can act as any payer.

Errors are `{"error": {"code": "...", "message": "...", "details": {...}}}`. Money is a 2-decimal string.

| Call | Does |
|---|---|
| `GET /api/v1/payers` | `{"data": [{id, name, mobile, status, balance, held, available}]}` |
| `POST /api/v1/qr/verify` `{qr_payload, payer_id?}` | The issuer's local check (`BqrCore.decode/2`: CRC, framing, field rules). Never touches money or the switch. `200` with `{valid, type, merchant_name, merchant_city, amount, amount_required, reference, currency, acquirer_id, merchant_pan, warnings}` and, with a `payer_id`, `{available, can_afford}`. Unverifiable QR: `422 invalid_qr` with a plain-language message |
| `POST /api/v1/payments` `{payer_id, qr_payload, amount?}` | Verifies, holds the money, and starts sending in the background. `202` with the payment (`outcome: "processing"`); `200` with `outcome: "not_charged"` if refused up front (insufficient funds, blocked payer). `amount` is only used for a QR that carries none (a JSON number is rejected). `404 payer_not_found`, `422 invalid_qr / amount_required / invalid_amount` |
| `GET /api/v1/payments/:id` | The payment: `{id, txn_id, status, outcome, final, code, message, amount, merchant_name, ...}` |
| `POST /api/v1/payments/:id/retry` | Re-checks an `unconfirmed` payment now (a status inquiry for the same `txn_id`; never a second charge). `202`; `409 not_retryable` for anything else. The server also keeps checking by itself, so a client may simply keep polling `GET` |

**`outcome` is what a client should act on:** `processing` (keep polling), `paid` (the **only** success), `not_charged` (declined or refused;
nothing charged), `unconfirmed` (outcome unknown; money held; can be re-checked). `status` is the raw state (`COMPLETED`, `RELEASED`, `UNKNOWN`...).

**Browsers and phones.** In development the issuer answers CORS preflights (`dev_cors`, dev only) so the app can run in Chrome. To reach it from a
phone on your Wi-Fi start it with `ISSUER_BIND_ALL=1` (`scripts\dev.ps1 -Lan`): by default it listens on loopback only. That also exposes the
login-less web screens to your network, so use it on a trusted network.

## Data

* `payers` (`mobile` is sent as `payer_ref`), `accounts` (`balance_minor`, `held_minor`, integer poisha).
* `payments` – one per attempt, with a snapshot of everything sent to the switch.
* `ledger_entries` – `topup`, `hold`, `release`, `debit`, each with the balances after it. At most one of each kind per payment
  (unique index), so a payment cannot be both released and debited. Replaying the ledger always reproduces the account
  (`balance = topups - debits`, `held = holds - releases - debits`); tests assert this.

## Configuration

`config :bqr_issuer, BqrIssuer.SwitchClient`: `url`, `issuer_id`, `secret`, `connect_timeout`, `receive_timeout` (default 25 s,
which must stay above the switch's worst case of 2 credit attempts x 10 s, or the issuer gives up before the switch answers). In dev the
secret is set in `config/dev.exs`; in prod `ISSUER_SWITCH_URL`, `ISSUER_PARTICIPANT_ID` and `ISSUER_SWITCH_SECRET` are required and
must match a participant registered on the switch.

`config :bqr_issuer, BqrIssuer.Resolver`: `enabled`, `tick_ms` (2 s), `batch`, `status_schedule_s` (`[5, 15, 30, 60]`), `reverse_after_s`
(180), `reverse_retry_s` (30), `inflight_window_s` (45), `time_scale` (1.0; dev reads `BQR_TIME_SCALE`, `0.05` = 20x faster).
`payments` carries `unknown_since`, `resolve_attempts`, `last_checked_at`, `next_check_at` for this.

## Camera scanning

Uses `jsQR` 1.4.0 (Apache-2.0), vendored at `assets/vendor/jsQR.js` with its license. Verified against npm's published SHA-512
when fetched. The camera needs a secure context (`https://` or `http://localhost`); otherwise the button explains and pasting still
works. The decoding loop is browser code and is not covered by the Elixir tests (they cover the `qr_scanned` event it sends).

## Trying it

```bash
(cd apps/bqr_acquirer && mix ecto.migrate && mix run priv/repo/seeds.exs)
(cd bqr_switch_sim    && mix ecto.migrate && mix run priv/repo/seeds.exs)
(cd apps/bqr_issuer   && mix ecto.migrate && mix run priv/repo/seeds.exs)
scripts/dev.ps1     # or `mix phx.server` in each
```

Open http://localhost:34300, pick *Demo Payer*, **Scan & pay**, **Use the demo merchant's QR** (dev only), enter an amount, **Pay**.
For a dynamic QR, create one with the acquirer's partner API ([ACQUIRER_API.md](ACQUIRER_API.md)) and paste its `payload`.
