Files
baya-monorepo/dev/shared-working-context/backend/handoff/after-backend-phase-10.md
T
2026-07-06 21:17:00 +03:30

4.2 KiB

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_types — 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 balanceGET 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.
  • Enums: payment status (pending/succeeded/failed), payment_gateways.type (standard/bnpl), processing_status (received/processed/failed/ignored), the eight account_types (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.