refinement phase 8

This commit is contained in:
hamid
2026-07-13 21:49:50 +03:30
parent 7edadadea1
commit ef3024ef2f
35 changed files with 2505 additions and 71 deletions
@@ -0,0 +1,56 @@
# 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.