Files
baya-monorepo/dev/contracts/domains/payments.md
T
2026-07-06 21:17:00 +03:30

72 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Contract — Payments core: ledger, transactions, webhooks & card capture (backend phase b10)
> One-line: the inbound money rail — start a card payment against an accepted request, a PSP webhook confirms
> it, the balanced card-capture ledger group posts, and the booking confirms. Assumes
> [`../conventions/api-conventions.md`](../conventions/api-conventions.md) +
> [`../conventions/money-and-types.md`](../conventions/money-and-types.md). Machine schema:
> [`../openapi/swagger.v1.json`](../openapi/README.md).
**Status:** live as of backend-phase-b10 · **Frontend consumer:** frontend-phase-f9-b10
All money is **IRR Rials, integer, on the wire as a string of digits** (`"23300000"`). The card-capture ledger
group is always **balanced** (Σ debit = Σ credit). **Internal `account_type`s are never exposed to the
customer** — the checkout UI shows gross + the commission/VAT breakdown only. Timestamps are UTC ISO-8601.
## Enums used
- `payment` status (`payment_transactions.status`): `pending` | `succeeded` | `failed`.
- `payment_gateways.type`: `standard` (card IPG) | `bnpl`.
- `payment_webhook_events.processing_status`: `received` | `processed` | `failed` | `ignored`.
- `account_type` (internal, never on the customer wire): `escrow_held` | `platform_revenue` | `nurse_payable`
| `refund_payable` | `bnpl_fee_expense` | `psp_fee_expense` | `nurse_clawback_receivable` | `bad_debt`.
b10 posts only the first three (card capture); the rest are reserved for b11/b12/b13.
## Endpoints
### `POST api/v1/bookings/{bookingRequestId}/payments`
- **Purpose:** start a card payment for an `accepted_awaiting_payment` booking request owned by the caller.
A `bookings` row exists only on capture (b9), so payment is initiated against the **request**; the amount
charged is the request's **frozen gross** (variant price × session count), never client-supplied.
- **Auth:** authenticated (the owning customer) · **Rate-limited:** yes (sensitive) · **Idempotency:** send an
**`Idempotency-Key`** header — a retried start reuses the same attempt/reference.
- **Request body:** none (the id is in the route; the key is a header).
- **Success `200` (`data`):** `{ "transactionId": 42, "redirectUrl": "https://…", "gatewayReferenceCode": "…" }`.
No ledger rows yet; the booking is not created yet.
- **Failure:** `400` bad id, `401` unauth, `404` request not found / not the caller's, `409` already paid /
not awaiting payment / payment window lapsed, `400` no active gateway configured.
### `POST api/v1/webhooks/payments/{provider}`
- **Purpose:** the inbound PSP/BNPL callback — verify-then-dedup-then-mutate.
- **Auth:** **none (signature-authenticated)**, anonymous to the auth pipeline · **Rate-limited:** yes (global
per-IP) · **Idempotent:** yes, at-least-once tolerant.
- **Request:** the raw provider callback body (stored verbatim in `payload_json`); signature material in headers.
- **Behaviour:** upserts `payment_webhook_events` **first** on `(provider, external_event_id)` and **no-ops on
a duplicate**; an **invalid signature** is stored `ignored` and mutates nothing; on a **new success event**
it re-verifies server-side (never trusts the callback alone), then captures — posts the balanced card-capture
group and creates/confirms the booking — all under a `lock(booking:{id}:payment)` with the DB uniques as the
authoritative backstop.
- **Success `200` (`data`):** `{ "processingStatus": "processed" | "ignored" | "failed", "duplicate": false }`
(`duplicate: true` on a replayed event).
### `GET api/v1/nurses/{nurseId}/payable_balance`
- **Purpose:** the IRR balance currently owed a nurse — the **signed sum** over `nurse_payable` ledger legs
(credit adds, debit subtracts), **derived, never a stored column**. This is what b13 payouts read.
- **Auth:** authenticated — the **nurse themself or an admin/finance role** (`403` otherwise).
- **Success `200` (`data`):** `{ "nurseId": 7, "balanceIrr": "19805000" }`.
## The card-capture ledger group (posted on webhook confirm)
One `transaction_group_id`, `amount_irr` positive with `direction` carrying the sign, Σdebit = Σcredit:
```
DEBIT escrow_held gross_price_irr (e.g. 23300000)
CREDIT platform_revenue balinyaar_commission_irr (e.g. 3495000)
CREDIT nurse_payable nurse_payout_amount (e.g. 19805000, nurse_id set)
```
## 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.
- **A booking is created & confirmed on capture** (the webhook), not on initiate — after `initiate` the
redirect is shown; the booking appears once the PSP callback confirms.
- **The checkout shows gross + commission/VAT breakdown only** — never the internal `account_type`s.
- **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`.