frontend phase 5 & backend phase 12

This commit is contained in:
hamid
2026-07-09 03:05:14 +03:30
parent 465f75c29e
commit dc64472631
98 changed files with 11847 additions and 136 deletions
@@ -0,0 +1,52 @@
# Handoff — after backend-phase-12 (BNPL: provider-financed installments)
**BNPL checkout is live.** A family can now pay for a booking with a provider-financed installment plan
(SnappPay / Digipay / Tara / Torob Pay). The decisive, verified truth: an Iranian provider-financed BNPL order
**settles the full booking amount to Balinyaar in one lump, net of the provider's merchant commission**, and the
provider owns the customer's installments and **100% of default risk**. So in our books a BNPL order is **a card
payment that lands net-of-fee** — one `bnpl_transactions` row (1:1 with its `payment_transaction`), a forward-only
`eligible → token_issued → verified → settled → reverted` state machine, and a settle that posts the card-capture
ledger legs **plus** a `bnpl_fee_expense` leg so escrow reflects the *net* cash. **The nurse's payout is invariant
to payment method.** We do **not** model the customer's repayment schedule.
## What the frontend (f11-b12) can now build
- **"Pay with installments" option at checkout** — `POST checkout_bnpl/eligibility` returns
`eligible`/`not_eligible`/`ceiling_exceeded` + the plan summary ("4 interest-free installments, provider-financed",
`installmentCount`, `creditCeilingIrr`). On anything but `eligible`, **fall back to card**.
- **Start the plan + provider handoff** — `POST checkout_bnpl/initiate``token_issued` + `externalPaymentToken`
+ `redirectUrl` (send the customer to the provider). Carries an `Idempotency-Key` header.
- **Order status** — `GET checkout_bnpl/{id}` (customer, own order only): status, gross/net/commission, the
**non-instant** `settledAt`, and the revert audit + async customer ETA.
- **Admin BNPL console** — `POST admin_bnpl/{id}/verify|settle`, `POST admin_bnpl/{id}/revert` (full or partial),
`GET admin_bnpl/{id}`. The revert surfaces `expectedCustomerRefundEta` (~710 business days) and opens a
`refund_channel='bnpl_revert'` refund (visible via the b11 `GET refunds/{id}/status`).
## Live endpoints / contract
- Contract: **`dev/contracts/domains/bnpl.md`** (enums, DTO shapes, IRR digit-strings, nullable `settled_at`,
failure codes, the net-of-fee settle + customer↔provider↔Balinyaar refund routing). Machine schema:
`dev/contracts/openapi/swagger.v1.json` **refreshed**.
- All money is IRR integer, on the wire as a **digit-string**; `settled_at` is **nullable** (not instant);
`expectedCustomerRefundEta` is a **date**.
- Checkout is customer-scoped + **rate-limited**; the webhook is anonymous (signature-verified) + rate-limited;
admin endpoints are behind the admin policy + rate-limited.
## What is mocked / waiting
- **The provider + currency are mocked** behind **`IBnplProvider`** (per `provider_code` via
**`IBnplProviderResolver`**) and **`ICurrencyNormalizer`** — deterministic, no network; the settle commission is
a configurable mock % (`Seams:Bnpl:CommissionRate`, default 10%). See `reports/mocks-registry.md` for the exact
real-provider steps (SnappPay/Digipay verb sets, encrypted creds).
- **Settlement timing is not modelled as instant** — `settled_at` is nullable and read from the settlement
(`Seams:Bnpl:SettlementInstant` toggles the mock). **b13 must not assume BNPL cash funds a payout.**
- **`bnpl_settlement_entries`** (tranched settlement) is **DEFERRED — modeled-but-not-built**; adding it later is a
purely additive migration.
- **Multi-provider routing / failover** is DEFERRED — one active route, config-driven selection.
- The revert reuses the **b11 refund path**; `provider_commission_reversed_amount` is left null there (reconciled
from the provider response later).
## Notes for the next backend phases
- **b13 (payouts):** the `settled_at`-gates-payout coupling lives here — add the
`require_bnpl_settlement_for_payout` config flag and gate a BNPL booking's payout on `bnpl_transactions.settled_at`
actually being set (never pay a nurse before Balinyaar holds the cash). The `nurse_payable` accrual is already
identical to the card path, so payout amounts need no BNPL-specific logic.
- **Shared conversion:** b10's booking-creation was extracted to `Features/Bookings/BookingConversion` — both the
card capture and the BNPL settle use it. Reuse it, don't fork.