Files
baya-monorepo/dev/shared-working-context/backend/handoff/after-backend-phase-12.md
T
2026-07-09 03:05:14 +03:30

53 lines
4.2 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.
# 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.