backend phase 10
This commit is contained in:
@@ -12,6 +12,24 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
|
||||
- **Notes for frontend:** <anything load-bearing>
|
||||
-->
|
||||
|
||||
## backend-phase-10 — Payments core: ledger, transactions, webhooks & card capture — 2026-07-06
|
||||
- **Shipped:** the money core via one migration in a new **`payments`** schema — **4 tables** `PaymentGateways`
|
||||
(encrypted `config_json`) / `PaymentTransactions` (the **two filtered uniques** — `gateway_reference_code` WHERE
|
||||
NOT NULL, `booking_id` WHERE status='succeeded') / `PaymentWebhookEvents` (**UNIQUE(provider_code,
|
||||
external_event_id)**) / **append-only** `LedgerEntries`. Features `InitiatePayment`, `HandlePaymentWebhook`,
|
||||
`ConfirmPaymentAndPostLedger` (internal), `GetNursePayableBalance`; controllers
|
||||
`POST bookings/{id}/payments`, public `POST webhooks/payments/{provider}`, `GET nurses/{id}/payable_balance`.
|
||||
Extracted **`BookingFactory`** so b10's real capture reuses b9's conversion (b9 unchanged).
|
||||
- **Contracts:** `dev/contracts/domains/payments.md` written; **openapi snapshot refreshed**
|
||||
(`dev/contracts/openapi/swagger.v1.json` — the three b10 paths + DTOs present).
|
||||
- **Mocked:** `IPaymentProvider`, `ISettlementSplitProvider`, `IWebhookVerifier`, `IDistributedLock` → 🟡
|
||||
(see reports/mocks-registry.md). `IPaymentCaptureSimulator` (b9) retained for b9's Convert path/tests.
|
||||
- **Gate:** build clean (0 new warnings) / tests green (Foundation 198, Identity 4, Api 83; +12 Foundation +4 Api new).
|
||||
- **Handoff:** backend/handoff/after-backend-phase-10.md
|
||||
- **Notes for frontend:** money is an **IRR digit-string**; a booking exists only **on capture** (pay against the
|
||||
accepted **request** id; the booking appears `confirmed` after the webhook); **never expose internal
|
||||
`account_type`s** (checkout = gross + commission/VAT); payment idempotent end-to-end (`Idempotency-Key` header).
|
||||
|
||||
## backend-phase-9 — Bookings, sessions, care instructions & EVV — 2026-07-06
|
||||
- **Shipped:** the post-payment engine via one additive migration in the **`booking`** schema — **5 tables**
|
||||
`Bookings` / `BookingSessions` / `BookingCareInstructions` / `VisitVerifications` / `CancellationPolicies`
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user