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