frontend phase 5 & backend phase 12
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-12 — BNPL: provider-financed installments (mocked) — 2026-07-09
|
||||
- **Shipped:** `payments.BnplTransactions` (1:1 with `payment_transaction`, `UNIQUE(payment_transaction_id)`,
|
||||
settle-split CHECK, forward-only `BnplStatus` machine); `Features/Bnpl/*` (eligibility/initiate/verify/settle/
|
||||
revert/callback/status); `CheckoutBnplController` (customer) + `WebhooksBnplController` (anon, signed) +
|
||||
`AdminBnplController` (admin), all rate-limited; `LedgerPosting.BnplSettle` (net-of-fee group w/
|
||||
`bnpl_fee_expense`); extracted shared `Features/Bookings/BookingConversion` (used by b10 card + b12 settle).
|
||||
- **Contracts:** dev/contracts/domains/bnpl.md + openapi snapshot refreshed (yes).
|
||||
- **Mocked:** `IBnplProvider` (per `provider_code` via `IBnplProviderResolver`) + `ICurrencyNormalizer` → 🟡
|
||||
(configurable mock commission %, non-instant `settled_at`). See reports/mocks-registry.md.
|
||||
- **Gate:** build clean (0 new code warnings) / tests green (314 pass: 4 identity + 214 foundation + 96 api;
|
||||
+14 new). Migration `BnplTransactions` created (not applied to a live DB this session — SQLite
|
||||
`EnsureCreated` builds it for tests).
|
||||
- **Handoff:** backend/handoff/after-backend-phase-12.md
|
||||
- **Notes for frontend:** f11-b12 = the "pay with installments" checkout (`checkout_bnpl/eligibility` →
|
||||
`initiate` → provider redirect; declined → fall back to card), the customer order view (`checkout_bnpl/{id}`),
|
||||
and the admin BNPL revert path with the ~7–10-day ETA. Money is IRR digit-strings; `settledAt` is nullable
|
||||
(not instant). A BNPL revert opens a `refund_channel='bnpl_revert'` refund (read via `refunds/{id}/status`).
|
||||
|
||||
## backend-phase-11 — Refunds, invoices & nurse clawbacks — 2026-07-09
|
||||
- **Shipped:** the reversal leg via one migration in the **`payments`** schema — **3 tables** `Refunds`
|
||||
(fee-leg decomposition + `refund_channel` + `amount = fee_leg + payout_leg` CHECK + **nullable `ticket_id`, no FK**
|
||||
|
||||
@@ -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` (~7–10 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.
|
||||
Reference in New Issue
Block a user