# After refinement-phase-8 — External rails go real (config-selected vendor adapters)

**For the frontend / next backend phase. Backend-owned; frontend reads.**

## What changed for a client

**Nothing user-facing changed by default** — the mocks stay the default registration, so every existing flow
behaves exactly as before. This phase makes each vendor rail *swappable to real by config*, not on by default.

One **new endpoint** (backend-to-backend, not for the browser): `POST /api/v1/webhooks/payouts/{provider}` — the
async PAYA/SATNA reconciliation callback (signature-authenticated, anonymous, `webhook` rate policy). It flips a
`submitted` payout to `paid`/`failed`. No client calls it.

## How a rail goes real (ops)

Set the rail's **`Seams:{rail}:Provider`** + its credentials (user-secrets/env) and restart — no code change,
no deploy of new binaries. Provider tokens (mock stays default; a typo falls closed to mock):

| Rail | Key | Real value | Also needs |
| --- | --- | --- | --- |
| SMS (**launch-critical**) | `Seams:Sms:Provider` | `kavenegar` | `Seams:Sms:{ApiKey,SenderLine,OtpTemplate}` |
| Shahkar / e-KYC / شبا | `Seams:{Shahkar,IdentityKyc,BankOwnership}:Provider` | `finnotech` | `Seams:Finnotech:{BaseUrl,ClientId,AccessToken}` |
| Geocoding | `Seams:Geocoding:Provider` | `neshan` | `Seams:Geocoding:{ApiKey}` |
| Object storage | `Seams:ObjectStorage:Provider` | `s3` | `Seams:ObjectStorage:{ServiceUrl,Bucket,Region,AccessKey,SecretKey}` |
| Card PSP (+ HMAC webhook + تسهیم) | `Seams:Payments:Provider` | `zarinpal` | `Seams:Payments:{MerchantId,CallbackUrl,WebhookSigningSecrets}` |
| BNPL | `Seams:Bnpl:Provider` | `real` | `Seams:Bnpl:Providers:{snapppay,digipay}:*` (creds via gateway config) |
| Payout rail | `Seams:BankTransfer:Provider` | `jibit` | `Seams:BankTransfer:{ApiKey,SourceSettlementAccount}` + webhook secret |
| Moadian | `Seams:Moadian:Provider` | `moadian` | `Seams:Moadian:{MemoryId,AccessToken}` + signing cert |

**Two hard rules baked in:**
- **SMS real ⇒ the OTP is never logged.** The Development OTP-in-logs bridge (`GET dev/last_otp`) runs **only while
  the mock SMS sender is selected**. Once `Seams:Sms:Provider=kavenegar`, the code only leaves the process over the
  SMS wire.
- **Money callbacks fail closed.** The payout reconciliation + PSP webhook verify a per-provider HMAC over the raw
  body; an invalid signature mutates nothing. The confirm path still re-verifies the amount server-side.

## Behavioural notes the next phase should know

- **Payout rail is async now (when real).** A real `JibitBankTransferProvider` accepts a transfer as `submitted`;
  the ledger posts only when the reconciliation callback confirms `paid`. The `ExecutePayoutBatch` handler already
  handled this (`MarkSubmitted` first, ledger on `paid`) — it was unchanged.
- **`bookings/convert` is Dev/Testing only.** `IPaymentCaptureSimulator` is out of the production registration
  (prod = fail-closed `DisabledPaymentCaptureSimulator`). Production converts via the b10 payment **webhook confirm**
  calling `ConvertRequestToBooking` directly — do not build a client convert flow.
- **`balinyaar` BNPL = in-house.** In real BNPL mode `provider_code=balinyaar` resolves to the deterministic
  net-of-fee model (no external API); `tara`/`torobpay` are unbuilt and rejected cleanly.
- **New cron:** `MoadianReconciliationJob` (6 h) walks `pending/submitted` invoices to `registered` — registered
  the phase-7 way (`IRecurringJob` + one `AddSingleton`), no migration.

## Follow-ups carried forward

- Per-code BNPL revert (the b11 refund path uses the SnappPay default); SMS.ir/Ghasedak adapters; Finnotech/Moadian
  token exchange + Moadian signing cert; the **refund-settlement poll** (BNPL `processing → succeeded`, pairs with
  Moadian — the confirm command exists, the poll job is the remaining wiring); a dedicated **center-settlement
  payout** (deferred per 6.6 — MoR centers settle via a تسهیم split leg, non-MoR have no separate money path).
- **Redis** stays the >1-instance gate; **Elasticsearch** is never MVP.
