# Checkout

Live path is Whish. Homepage/pricing CTAs go to `/checkout?plan=`. How this grants `user_access` and lands on dashboard: [00-how-everything-connects.md](./00-how-everything-connects.md).

Plans (source `lib/payments/catalog.ts`; live overlays via `getPaymentPlans()` apply to homepage, pricing, and checkout charges):

| URL slug | Name | Price | Courses | Days |
|---|---|---|---|---|
| `focus` | Focus | $10 | 1 | 120 |
| `balance` | Balance | $17 | 2 | 120 |
| `fullload` | Full Load | $25 | up to 6 | 120 |
| `crunch` | Crunch | $14 | up to 4 | 21 |

---

PAGE: /checkout
Purpose: Pick courses, collect details, start Whish payment.
Access: logged-in. Missing/invalid `?plan=` → `/pricing`. No session → `/login?next=/checkout?plan=…`.
Entry points: Homepage **Get Focus** / **Get Balance** / **Cover my semester** / **Prep for Finals**; pricing same labels; course detail **Get access →** with `&course={courses.id}`; payment failure **Return to checkout**.

SECTION: Header
  Copy: Logo alt `Gradeful`
  Data shown: static

  ELEMENT: Gradeful (logo)
    Action: navigate
    Destination: `/`
    Writes to: none
    Reads from: none

  ELEMENT: Contact us
    Action: navigate
    Destination: `/contact`
    Writes to: none
    Reads from: none

  ELEMENT: Back to plans
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

SECTION: Step indicator
  Copy: `Your course` `Your details` `Payment`
  Data shown: current step (1–2 in UI; payment is Whish, not a third in-app step)

SECTION: Step 1 — course picker
  Copy by plan: `coursePickerCopy` in `lib/payments/catalog.ts`
  Focus: `Which course do you need?` `You're getting 1 course — choose the one you need most.`
  Balance: `Which courses do you need?` `You're getting 2 courses — pick them now.`
  Full Load: `Which courses are you taking?` `Pick up to 6 courses. The plan covers what you select — not the whole catalog.`
  Crunch: `Which courses are you sitting?` `Pick up to 4 exam courses. You don't need to upload a schedule.`
  Search placeholder: `Search by code — e.g. MATH201`
  Status: `Loading courses…` `No courses found.` `Courses couldn't be loaded. Refresh and try again.`
  Data shown: `courses` (`is_active=true`) with `course_packs` and `universities`; optional `initialCourseId` from `?course=`

  ELEMENT: Search by code (input)
    Action: toggle
    Destination: none (filters list)
    Writes to: none
    Reads from: `courses.code`, `courses.title`

  ELEMENT: Selected course chip (code + remove)
    Action: toggle
    Destination: none (deselect)
    Writes to: none
    Reads from: `courses.code`

  ELEMENT: Course row (university, code, optional `{n} cr`, title)
    Action: toggle
    Destination: none (select/deselect)
    Writes to: none
    Reads from: `courses.*`, `universities.short_name`, credit hours on the course
    Client limits: `You can only pick 1 course on Focus` `You can only pick 2 courses on Balance` `You can pick up to {n} courses.`

  ELEMENT: Try again
    Action: toggle
    Destination: none (reload courses)
    Writes to: none
    Reads from: visible when load failed

  ELEMENT: Continue
    Action: toggle
    Destination: step 2
    Writes to: none (client state + sessionStorage `gradeful.checkout.plan`)
    Reads from: selection validity

SECTION: Step 2 — details
  Copy: Order summary shows plan name, included line, price, duration, selected courses. Empty `No courses selected`. No registered-credits or screenshot fields.
  Included lines: Focus `1 course · all content types` `/1 course` `Full semester · 120 days`; Balance `2 courses · all content types` `/2 courses`; Full Load / Crunch `The courses you pick · all content types` `/your courses`; Crunch duration `21-day sprint`.
  Fields: `Full name` placeholder `Your name` (prefilled from profile, editable, does not write profiles); `Signed-in email` readonly `This is the email on your Gradeful account. It can't be changed here.` empty `No email on this account`; `University` hidden/read-only when profile already has AUB/LAU, else `Select university` / `AUB` / `LAU` / `Other`; `WhatsApp (optional)` `+961 XX XXX XXX` `Optional, for account support. Payment uses the phone you enter on Whish.`; institution email hidden when login already matches campus, else shown for receipt; checkbox personal tutoring-access sentence plus `CHECKOUT_INDEPENDENCE_ACK`.
  Footnote: `You'll continue to a secure payment page to complete the purchase.` `Need help?`
  Data shown: Auth email; selected courses; `PLANS` price

  ELEMENT: Change courses
    Action: toggle
    Destination: step 1
    Writes to: none
    Reads from: none

  ELEMENT: Full name
    Action: toggle
    Destination: none
    Writes to: `orders.name`
    Reads from: none
    Error: `Full name is required.`

  ELEMENT: Signed-in email (readonly)
    Action: none
    Destination: none
    Writes to: none
    Reads from: Auth email
    Error: `Sign in to continue checkout.`

  ELEMENT: University
    Action: toggle
    Destination: none
    Writes to: `orders.university`
    Reads from: none
    Error: `Select your university.`

  ELEMENT: WhatsApp number
    Action: toggle
    Destination: none
    Writes to: `orders.whatsapp`
    Reads from: none
    Error: `WhatsApp number is required.`

  ELEMENT: Your uni email
    Action: toggle
    Destination: none
    Writes to: `orders.email` (this is the uni email, not Auth email)
    Reads from: none
    Errors: `Your uni email is required.` `Use a university email ending in .edu.`

  ELEMENT: I understand this material is for personal study only...
    Action: toggle
    Destination: none
    Writes to: none (must be true to submit)
    Reads from: none
    Error: `You must agree to continue.`

  ELEMENT: Continue to secure payment / Preparing secure payment…
    Action: submit
    Destination: Whish `collectUrl` (`window.location.assign`)
    Writes to: `orders` via `POST /api/payments/whish/create` (`user_id`, `name`, `email`, `university`, `plan`, `course_ids`, `registered_credits` null, `whatsapp`, `status=pending`, `payment_provider=whish`, `payment_status=pending`, `external_id`, `amount` from live plan, `currency=USD`, `collect_url`, `provider_metadata.durationDays`, `provider_metadata.catalogPrice`)
    Reads from: form + Auth session
    Errors: `An order was already submitted recently. Please wait and try again.` `Checkout is temporarily unavailable. Please try again shortly.` `Payment could not be started. Please try again.` `Unable to connect. Please check your connection.` plus server `error` ≤160 chars. 401 → login with checkout `next`.

  ELEMENT: Contact us
    Action: navigate
    Destination: `/contact`
    Writes to: none
    Reads from: none

API errors from create (also shown when returned): `Sign in to continue checkout.` `Invalid request.` `Checkout details are incomplete.` `Use your AUB or LAU email for this payment. It is not your login.` `Selected courses are not available.` `One or more selected courses are not available for purchase.` `You already have access to those courses. Open them from My courses instead of paying again.` `Order could not be created. Try again.` `Something went wrong. Please try again.` Entitlement: `Remove duplicate courses and try again.` `Select at least one course for this plan.` `Select 1 course for this plan.` `Select {n} courses for this plan.` `You can pick up to {n} courses.`

---

PAGE: /checkout/payment/success
Purpose: Return from Whish; poll until paid/failed/timeout. Arriving here does not mean paid by itself.
Access: logged-in (else login with `next` = this URL). Query `orderId` (UUID).
Entry points: Whish redirect after collect.

SECTION: PaymentReturnStatus (variant success)
  Copy by state:
  invalid-order: `We couldn't find that payment` `Open checkout again from your plan, or return to pricing to start over.`
  paid: `Payment confirmed` `Access is ready. You can open your courses now.` `Reference {8-char}`
  failed: `Payment wasn't completed` `No access was granted. You can try payment again, or return to checkout to start over.`
  pending: `We're confirming your payment` `Whish is still processing your payment. Keep this page open. If you haven't finished paying, you can return to Whish. This is not a failed payment.`
  unavailable (timeout): `We couldn't confirm the payment yet` `We haven't confirmed this payment yet. That doesn't mean it failed. If you already paid, wait a minute and refresh, or contact support. If you haven't finished on Whish, you can continue there.`
  unavailable (other): `We could not verify your payment right now. That doesn't mean it failed. Continue on Whish if you still need to pay, or contact support if you were charged.`
  checking: `Checking payment status` `Hang tight — we're verifying with the payment provider.`
  Hint: `Arriving here doesn't confirm payment by itself — we verify status securely.`
  Data shown: `GET /api/payments/whish/status?orderId=` → `orders`; reconcile may write `user_access`

  ELEMENT: Return to pricing
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

  ELEMENT: Go to my courses
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: shown when `accessGranted` / paid

  ELEMENT: Continue payment on Whish
    Action: external link
    Destination: Whish `collectUrl` / `resumePaymentUrl`
    Writes to: none
    Reads from: `orders.collect_url`

  ELEMENT: Return to checkout
    Action: navigate
    Destination: `/checkout` with remembered plan
    Writes to: none
    Reads from: sessionStorage plan

  ELEMENT: Contact Whish support
    Action: external link
    Destination: `https://www.whish.money/contact-us`
    Writes to: none
    Reads from: none

Poll may call `reconcileWhishOrder` → `grantAccess` → `user_access`. New grants do not insert `schedule_verifications`.

---

PAGE: /checkout/payment/failure
Purpose: Same status poll as success; copy hint differs.
Access: logged-in
Entry points: Whish failure redirect.

SECTION: PaymentReturnStatus (variant failure)
  Copy: Same state machine as success. Hint: `Arriving here doesn't mean the payment permanently failed — we verify status securely.`
  Data shown: same as success
  ELEMENT: same buttons as `/checkout/payment/success`

