# Handoff — after backend phase 10 (Payments core: ledger, transactions, webhooks & card capture) **The money core is live.** A family can now pay the gross price by card: **initiate → PSP webhook confirms → the balanced card-capture ledger group posts → the booking converts/confirms**. The PSP acquirer, تسهیم split, webhook signature verify, and the distributed lock are **mocked behind seams**; the DB constraints are the authoritative money-path backstops. ## What f9-b10 can now build - **Summary & pay (C6)** — after the nurse accepts, `POST api/v1/bookings/{bookingRequestId}/payments` (as the customer, with an `Idempotency-Key` header) → `{ transactionId, redirectUrl, gatewayReferenceCode }`. Show the checkout summary (gross + commission/VAT/escrow notice) and send the payer to `redirectUrl`. **Never show the internal `account_type`s** — gross + the commission/VAT breakdown only. - **Card payment redirect + confirmation** — the (mock) redirect completes; the PSP callback hits `POST api/v1/webhooks/payments/{provider}` and confirms. Poll the booking (b9 `bookings/list` / `bookings/get`) → it appears **`confirmed`** once the webhook captures. (A booking exists only on capture.) - **Nurse payable balance** — `GET api/v1/nurses/{nurseId}/payable_balance` (the nurse themself or admin) → `{ nurseId, balanceIrr }` as a digit-string, derived from the ledger. ## Live endpoints / contracts - Contract: [`dev/contracts/domains/payments.md`](../../contracts/domains/payments.md). - Enums: `payment` status (`pending`/`succeeded`/`failed`), `payment_gateways.type` (`standard`/`bnpl`), `processing_status` (`received`/`processed`/`failed`/`ignored`), the eight `account_type`s (internal). ## Load-bearing rules the client must honour - **Money is IRR integer, on the wire as a digit-string.** Never coerce to a JS number for math. - **The booking is created & confirmed on capture (the webhook), not on initiate.** After `initiate` you have a redirect only; the `bookings` row appears when the PSP callback confirms. - **Payment is idempotent end-to-end** — a retried `initiate` (same `Idempotency-Key`) reuses the attempt; a replayed webhook is a no-op; a repeat `initiate` after capture is a `409`. - **Internal account types are never exposed to the customer** — checkout shows gross + commission/VAT only. ## Design note (reconciling b10 with b9's as-built) b9 creates the `bookings` row **on capture** (it never persists a standalone `pending_payment` booking). So b10 initiates payment against the `accepted_awaiting_payment` **request**; `payment_transactions.booking_id` is **nullable**, bound only at confirm. Confirm reuses b9's conversion/amount logic through the extracted **`BookingFactory`** (no duplication) and posts the ledger + registers the split. The mock `IPaymentCaptureSimulator` + the `POST bookings/convert` endpoint **stay** (b9's own tests use them); the real capture path uses `BookingFactory` directly, not that seam. ## Mocked here → make real later (see reports/mocks-registry.md) - **`IPaymentProvider`** (🟡) — deterministic ref + fake redirect; `VerifyAsync` instant-succeeds. Real: ZarinPal/Sadad/Vandar/Jibit acquirer-with-تسهیم, merchant id from encrypted `config_json`, server-side verify. - **`ISettlementSplitProvider`** (🟡) — records split intent, returns `Settled`. Real: split-by-ratio to registered SHEBAs; provider credits IBANs directly. - **`IWebhookVerifier`** (🟡) — signature valid unless a marker; parses a test JSON body. Real: per-provider HMAC / mandatory server-side verify. - **`IDistributedLock`** (🟡) — in-process semaphore. Real: StackExchange.Redis lease, key `booking:{id}:payment`. ## Consumed by later backend phases - **b11** — refunds/clawbacks/invoices post against the ledger (`refund_payable`/`nurse_clawback_receivable` account types are already defined); `IPaymentProvider.RefundAsync` is ready to call. - **b12** — BNPL settle routes callbacks through the **same** `payment_webhook_events` idempotency store and posts the BNPL-settle group (adds `bnpl_fee_expense`). - **b13** — payouts read `GetNursePayableBalance` (the signed `nurse_payable` ledger sum) and post DEBIT `nurse_payable` / CREDIT `escrow_held`, gated on b9's `dispute_window_ends_at`.