4.2 KiB
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/eligibilityreturnseligible/not_eligible/ceiling_exceeded+ the plan summary ("4 interest-free installments, provider-financed",installmentCount,creditCeilingIrr). On anything buteligible, fall back to card. - Start the plan + provider handoff —
POST checkout_bnpl/initiate→token_issued+externalPaymentTokenredirectUrl(send the customer to the provider). Carries anIdempotency-Keyheader.
- Order status —
GET checkout_bnpl/{id}(customer, own order only): status, gross/net/commission, the non-instantsettledAt, 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 surfacesexpectedCustomerRefundEta(~7–10 business days) and opens arefund_channel='bnpl_revert'refund (visible via the b11GET refunds/{id}/status).
Live endpoints / contract
- Contract:
dev/contracts/domains/bnpl.md(enums, DTO shapes, IRR digit-strings, nullablesettled_at, failure codes, the net-of-fee settle + customer↔provider↔Balinyaar refund routing). Machine schema:dev/contracts/openapi/swagger.v1.jsonrefreshed. - All money is IRR integer, on the wire as a digit-string;
settled_atis nullable (not instant);expectedCustomerRefundEtais 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(perprovider_codeviaIBnplProviderResolver) andICurrencyNormalizer— deterministic, no network; the settle commission is a configurable mock % (Seams:Bnpl:CommissionRate, default 10%). Seereports/mocks-registry.mdfor the exact real-provider steps (SnappPay/Digipay verb sets, encrypted creds). - Settlement timing is not modelled as instant —
settled_atis nullable and read from the settlement (Seams:Bnpl:SettlementInstanttoggles 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_amountis 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 therequire_bnpl_settlement_for_payoutconfig flag and gate a BNPL booking's payout onbnpl_transactions.settled_atactually being set (never pay a nurse before Balinyaar holds the cash). Thenurse_payableaccrual 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.