4.0 KiB
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. OnceSeams: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
JibitBankTransferProvideraccepts a transfer assubmitted; the ledger posts only when the reconciliation callback confirmspaid. TheExecutePayoutBatchhandler already handled this (MarkSubmittedfirst, ledger onpaid) — it was unchanged. bookings/convertis Dev/Testing only.IPaymentCaptureSimulatoris out of the production registration (prod = fail-closedDisabledPaymentCaptureSimulator). Production converts via the b10 payment webhook confirm callingConvertRequestToBookingdirectly — do not build a client convert flow.balinyaarBNPL = in-house. In real BNPL modeprovider_code=balinyaarresolves to the deterministic net-of-fee model (no external API);tara/torobpayare unbuilt and rejected cleanly.- New cron:
MoadianReconciliationJob(6 h) walkspending/submittedinvoices toregistered— registered the phase-7 way (IRecurringJob+ oneAddSingleton), 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.