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

4.2 KiB
Raw Blame History

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 checkoutPOST 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 handoffPOST checkout_bnpl/initiatetoken_issued + externalPaymentToken
    • redirectUrl (send the customer to the provider). Carries an Idempotency-Key header.
  • Order statusGET checkout_bnpl/{id} (customer, own order only): status, gross/net/commission, the non-instant settledAt, and the revert audit + async customer ETA.
  • Admin BNPL consolePOST 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 instantsettled_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.