# Switch simulator API (phases 4 and 6)

Base URL `http://localhost:34200`. Stands in for NPSB. Depends only on `bqr_protocol`: it never decodes a QR, it routes
on the `acquirer_id` the issuer sends and passes the scanned `qr_payload` through for the acquirer to verify.

```
Issuer ──pay_request──▶ Switch ──credit_request──▶ Acquirer
       ◀─txn_response──        ◀──txn_response────

Issuer ──status_request / reverse_request──▶ Switch ──status_request / reverse_request──▶ Acquirer     (phase 6)
```

## Authentication

Same scheme as the acquirer partner API (`BqrProtocol.Signature`, see [ACQUIRER_API.md](ACQUIRER_API.md)): headers
`x-bqr-participant`, `x-bqr-timestamp` (ms, ±300 s), `x-bqr-nonce` (single use per participant, max 128 chars),
`x-bqr-signature` = `hex(HMAC-SHA256(secret, timestamp \n nonce \n raw_body))`. 401 codes: `missing_x_bqr_*`,
`bad_signature`, `bad_timestamp`, `timestamp_out_of_range`, `replayed_nonce`, `bad_nonce`, `unauthorized` (unknown or
suspended participant). **Responses are not signed** (the contract only signs requests; owner decision).

Switch to acquirer calls are signed as participant `bqr-switch` (`:switch_participant_id`) with the secret the switch holds
for that acquirer (`participants.secret`). The acquirer must have a participant `bqr-switch` with the same secret.

## `POST /api/v1/pay`

Caller must be an **issuer** and `issuer_id` in the body must be itself (else 403 `issuer_mismatch` / `forbidden`).
Body is a `pay_request` (`BqrProtocol.Messages`): `txn_id qr_payload qr_type acquirer_id merchant_pan amount currency issuer_id
payer_ref initiated_at`, optional `reference_label terminal_label`. `amount` is a decimal string with 2 decimals, greater than zero,
max 13 characters.

**Every request that carries a `txn_id` gets HTTP 200 and a `txn_response`**, whatever happened; read `status` and `code`:

```json
{"txn_id": "T1", "status": "SUCCESS", "code": "SUCCESS", "switch_ref": "SW…", "acquirer_ref": "…"}
{"txn_id": "T2", "status": "FAILED",  "code": "QR_ALREADY_USED", "message": "QR already paid", "switch_ref": "SW…"}
{"txn_id": "T3", "status": "PENDING", "code": "TIMEOUT", "message": "acquirer did not answer in time", "switch_ref": "SW…"}
```

HTTP errors are only for what cannot be tied to a payment: 401 (auth), 403, 422 `invalid_message` (no usable `txn_id`).
An invalid message *with* a `txn_id` is `200 FAILED INVALID_MESSAGE` listing the bad fields (nothing is stored).

### Outcomes

| Situation | Result |
|---|---|
| Acquirer answers `SUCCESS` | `SUCCESS`; ledger written |
| Acquirer answers `FAILED` + code | `FAILED` + that code (`QR_ALREADY_USED`, `QR_EXPIRED`, `QR_NOT_FOUND`, `AMOUNT_MISMATCH`, `INVALID_MERCHANT`, `INVALID_MESSAGE`, `DUPLICATE_TXN`, …) |
| Acquirer answers `PENDING` | `PENDING` |
| No active acquirer for `acquirer_id` (nothing was sent) | `FAILED UNKNOWN_PARTICIPANT` |
| Timeout | `PENDING TIMEOUT` |
| Connection refused / unreachable, non-2xx, not JSON, wrong `txn_id`, status and code contradict, missing fields | `PENDING SYSTEM_ERROR` (or `TIMEOUT`; on Windows a closed port often times out instead of refusing) |

**`PENDING` is never final.** The acquirer may have credited, so the issuer must not release its hold on a guess. All
transport failures are `PENDING` (owner decision), including a refused connection. A `PENDING` payment is resolved by status
inquiry and then reversal (below). Before it answers `PENDING` the switch sends the `credit_request` again, up to
`credit_attempts` times in all (default 2): safe, because the acquirer is idempotent by `txn_id`.

### Idempotency by `txn_id`

The first request claims the `txn_id` atomically (unique index). A repeat:

* identical and already `SUCCESS` / `FAILED`: the stored answer, no new call to the acquirer;
* identical and `PENDING` (or interrupted by a crash): the acquirer is **asked right now** (status inquiry, see below) and the
  answer returned. The `credit_request` is never repeated this way;
* identical but the first is still running (within `inflight_window_s`): `PENDING` "payment is still being processed", never `FAILED`;
* different content (fingerprint of `txn_id qr_payload qr_type acquirer_id merchant_pan amount currency issuer_id payer_ref`):
  `FAILED DUPLICATE_TXN`, original untouched. Another issuer reusing a `txn_id` gets the same.

## Resolving `PENDING`: `POST /api/v1/status` and `POST /api/v1/reverse` (phase 6)

Issuer only (403 `forbidden` otherwise), same signature scheme. Bodies are `status_request` `{"txn_id"}` and
`reverse_request` `{"txn_id", "reason"}`. Like `pay`, a request with a `txn_id` always gets HTTP 200 and a `txn_response`
(422 `invalid_message` only without one):

| Answer | Meaning for the issuer |
|---|---|
| `SUCCESS` | the merchant was credited; debit the payer |
| `FAILED` + code | nothing moved and nothing will; release the hold. `REVERSED` = undone, `TXN_NOT_FOUND` = never received |
| `PENDING` | still not known; ask again later |

`status` asks the acquirer (`POST …/switch/status`) and settles the transaction on its answer. `reverse` asks the acquirer to
reverse (`POST …/switch/reverse`); a payment already `SUCCESS` is not reversed (the answer stays `SUCCESS`, "cannot be
reversed"). If the acquirer cannot be reached the answer is `PENDING`: the switch never decides on a guess.

**A `txn_id` the switch never received** gets `FAILED TXN_NOT_FOUND` and is **blocked**: a `pay_request` for it that
arrives afterwards (a slow network, a retry that was still on its way) is refused with the same answer, so what the issuer was
told stays true. Another issuer's `txn_id` is reported as not found too, and is not blocked.

**The background resolver** (`BqrSwitchSim.Resolver`, every `tick_ms`) does the same without being asked. A `PENDING`
transaction is inquired about after 5 s, 15 s, 30 s, 60 s and then every 60 s; once it has been unresolved for `reverse_after_s`
(120 s) it is **reversed** instead, and retried every `reverse_retry_s` until the acquirer answers. A transaction found
`RECEIVED` / `ROUTED` long after it started (the node died) is treated as `PENDING`. Only the acquirer's answer settles a
transaction, through one conditional update, so the resolver, an issuer's request and a delayed original answer can race
freely: whichever arrives first wins, a final state never changes (a contradicting late answer is logged for investigation),
and the ledger is written once.

Timings: `config :bqr_switch_sim, BqrSwitchSim.Resolver, status_schedule_s: [5, 15, 30, 60], reverse_after_s: 120, reverse_retry_s: 30,
inflight_window_s: 45, time_scale: 1.0, tick_ms: 2_000`. `time_scale` shrinks everything: dev reads `BQR_TIME_SCALE`
(`0.05` makes the two minutes pass in six seconds).

## Fault injection (dev and test only)

Makes the switch misbehave on demand to exercise all of the above. Off unless `config :bqr_switch_sim, :fault_injection` is
`true` (dev and test). Control page: `http://localhost:34200/faults`. Admin API, header `x-admin-key` (`config :bqr_switch_sim,
:admin_key`; dev `dev-admin-key`), 404 when injection is off:

```
GET    /api/v1/admin/faults                     list rules
POST   /api/v1/admin/faults                     {"stage","action","acquirer_id","delay_ms","remaining","note"}
DELETE /api/v1/admin/faults                     remove all
DELETE /api/v1/admin/faults/:id
POST   /api/v1/admin/faults/:id/enable | /disable
```

A rule fires for a `stage` (`credit`, `status`, `reverse`: the switch's call to the acquirer; `issuer_response`: its answer to
the issuer), for one `acquirer_id` or any, `remaining` times (empty = always), first live match in creation order:

| action | effect |
|---|---|
| `drop` | the request is never sent; the switch sees a timeout |
| `lose_response` | the request **is** sent and processed; the answer is thrown away (a timeout). The classic "credited but unknown" |
| `error_status` | the switch sees an HTTP error |
| `garbage` | processed, but the answer is unreadable |
| `delay` | wait `delay_ms`, then carry on (at `issuer_response`: longer than the issuer's timeout) |
| `late` | the switch sees a timeout now; the request really goes out `delay_ms` later (tests the late-arrival guard) |

`status` and `reverse` accept `drop error_status garbage delay`; `issuer_response` only `delay`. Nothing in the code above
`BqrSwitchSim.AcquirerGateway` can tell an injected fault from a real one.

## Data

* `participants` – `issuer` or `acquirer`; acquirers have `acquirer_id` (routing key) and `callback_url`. `secret` is the shared HMAC key.
* `transactions` – one per `txn_id`: `RECEIVED → ROUTED → SUCCESS | FAILED | PENDING`, with the request, outcome, `switch_ref`, `acquirer_ref`,
  and for `PENDING` ones `pending_since`, `resolve_attempts`, `last_checked_at`, `next_check_at`, `reversal_requested_at`.
* `txn_blocks` – `txn_id`s answered `TXN_NOT_FOUND` before they arrived; a `pay_request` for one is refused.
* `fault_rules` – the fault-injection rules.
* `ledger_entries` – on `SUCCESS` only, written in the same DB transaction as the outcome: debit issuer, credit acquirer, same amount.
  `BqrSwitchSim.Ledger.positions/0` gives the net per participant (positive = owed); `balanced?/0` is always true.
* `api_nonces` – replay protection, pruned by `BqrSwitchSim.Janitor`.

Participants are registered with `BqrSwitchSim.Participants.create_participant/1` or the seeds (no admin UI until phase 7).

## Configuration

`config :bqr_switch_sim, BqrSwitchSim.AcquirerClient, connect_timeout: 3_000, receive_timeout: 10_000, credit_attempts: 2,
retry_delay_ms: 500` – the issuer's own timeout must be longer than this times the attempts plus processing time, or the
issuer will see its own timeout before the switch's answer.
`config :bqr_switch_sim, :switch_participant_id, "bqr-switch"`.

## Trying it

```bash
(cd apps/bqr_acquirer && mix ecto.migrate && mix run priv/repo/seeds.exs)   # partner, merchant, BINs, switch participant
(cd bqr_switch_sim    && mix ecto.migrate && mix run priv/repo/seeds.exs)   # demo-issuer, demo-acquirer
scripts/dev.ps1     # or run each: mix phx.server
```

Dev-only credentials: issuer `demo-issuer` / `dev-only-demo-issuer-secret-0123456789`; the switch and acquirer share
`dev-only-switch-secret-0123456789abcdef`.
