# Hosted Pay Later checkout — scope

Status: proposal (2026-10-12). Nothing built yet.

## Goal

Paystack-style checkout for partners. The partner's server creates a checkout session, gets a link,
and sends the buyer to it (redirect, later a popup). NextPayDay hosts every credit step and sends the
buyer back to the partner with the result. Partners that want their own UI keep using the existing
Partner Credit API (`docs/partner-credit-api.md`).

## Today (Presency)

Presency built the whole buyer flow itself (`waas/main/web/src/app/credit/*`, ~1,700 lines) and talks
to NextPayDay through its backend (`partner-credit.service.ts`, `nextpayday-partner.client.ts`).

| Step | Who does it now | Hosted checkout |
|---|---|---|
| Buyer sign-in (email OTP) | Presency | Partner (unchanged) |
| Buyer identity check (Paystack, `vendorKyc*`), sent to NPD as `customer.verification` | Presency | Partner assertion **or** NPD (decision 1) |
| Down payment / contribution split, delivery address | Presency | Partner (unchanged) |
| Create application + consent | Presency → API | Partner server → **checkout session** |
| Bank statement PDF upload (+ PDF password) → assessment | Presency UI → API | **NPD page** |
| Offer, terms, accept offer | Presency UI → API | **NPD page** |
| Mono + Paystack mandates (provider-hosted links, return to Presency) | Presency UI → API | **NPD page**, return to NPD |
| Request merchant payout | Presency server → API | Partner server (unchanged, decision 2) |
| Status page, manual repayment | Presency | Partner status page reads webhooks; buyer repays in NPD portal/app |

## What gets built

### 1. Checkout session API (NPD)

- `POST /api/v1/partner-credit/checkout-sessions`
  `{partner_reference, product: "buyer_bnpl", requested_amount_minor, currency, customer{first_name, last_name, email, phone, verification?}, consent, return_url, cancel_url, metadata}`
  → `{session_reference, application_reference, checkout_url, expires_at}`
- `GET /api/v1/partner-credit/checkout-sessions/{reference}` for status.
- Reuses application creation, the borrower account and setup email, and existing webhooks. Adds
  `checkout.completed` / `checkout.abandoned` events.
- `return_url` / `cancel_url` must match an allow-list on the partner (new `credit_partners` settings).

### 2. Hosted pages (NPD, Blade, same layout as the borrower portal)

1. **Open link**: signed, expiring session link. Re-opening later asks for an email code (no password).
2. **Identity** (only when the partner did not send a verified assertion): BVN + bank account via
   Paystack, reusing `PartnerBorrowerIdentityService::captureBvnAndBank` and the virtual-account job.
3. **Bank statement**: PDF upload with optional password → existing assessment service.
4. **Offer**: amount, schedule, total repayable, terms and consent → accept offer.
5. **Repayment setup**: Mono, then Paystack (or the order the facility requires), each via the
   provider's hosted page and back to NPD.
6. **Done**: redirect to `return_url?session=…&status=completed|declined|cancelled`. The partner must
   confirm with the webhook or `GET` (never trust the redirect alone).

Partner branding on every page: name, logo, accent colour (`credit_partners` settings).

### 3. Safety and recovery

Signed tokens with expiry, one live session per application, rate limits, no personal data in URLs,
idempotent resume after a closed tab, abandoned-session expiry plus webhook, admin view of sessions.

### 4. Presency switch-over

Behind a flag: keep sign-in, identity, contribution split and address; replace statement, offer and
mandate screens with a redirect to `checkout_url`; the status page reads webhook-updated state.
Payout request stays server-side. The old flow stays available until the new one is proven.

## Decisions needed

1. **Identity.** Accept a trusted partner's verified assertion (as Presency does today), or always run
   it at NPD? *Recommend:* accept it for partners flagged as trusted, run it at NPD for everyone else.
2. **Payout.** Partner-triggered (today) or automatic when checkout completes? *Recommend:* keep
   partner-triggered; the partner knows when the order is real.
3. **Down payment.** Stays with the partner (*recommend*) or NPD collects it.
4. **Popup vs redirect.** Redirect first. A popup window (not an iframe) later: Mono and Paystack
   authorization pages may refuse to load inside iframes.
5. **Seller stock credit** is out of scope; Presency generates its sales statement itself.

## Plan and estimate

| Phase | Work | Estimate |
|---|---|---|
| 1 | Session API, hosted statement → offer → mandates → return, partner-asserted identity only, tests | 3–5 days → working in sandbox |
| 2 | NPD-run identity, partner branding, resume/abandon handling, emails, admin view | 3–4 days |
| 3 | Presency switch-over behind a flag, end-to-end run, partner docs; optional popup script (+2 days) | 2–3 days |

About **2 weeks to production**.

## Risks

- Local NPD `.env` holds a **live** Mono key: mandate testing must stay on simulated mandates.
- Password-protected and scanned statement PDFs fail more often than app-linked statements.
- Provider pages and in-app browsers (Instagram/WhatsApp WebViews) can break redirects; test on phones.
- Moving identity checks to NPD (phase 2) adds Paystack identity cost per buyer.
