# 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.