docs(phase9): document commerce integration and Winter Break launch readiness
New: PHASE9_COMMERCE_AND_LAUNCH.md, WEBSITE_REGISTRATION_AND_CHECKOUT.md, STRIPE_INTEGRATION_RUNBOOK.md, WINTER_BREAK_LAUNCH_CHECKLIST.md. Updated: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (commerce boundary + PHASE 9 BUILT rows), OFFERING_AND_REGISTRATION_MODEL.md (pricing, the payment/commerce boundary, three-status principle), WEBSITE_ENROLLMENT_INTEGRATION.md (price fields, real agreement/checkout/webhook sections replacing Phase 8 placeholders), WINTER_BREAK_CAMP_OPERATIONS.md (real payment step replacing the manual placeholder).
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
# Offering & Registration Model
|
||||
|
||||
Phase 7 Steps 3/6/12-14/18-20 (extended in Phase 8) — the schema and lifecycle for offerings,
|
||||
capacity, registrations, agreements, and curriculum packages. See `platform/api/migrations/
|
||||
034_phase7_offerings_registrations_agreements.sql` and `035_phase8_enrollment_operations.sql`
|
||||
for the exact DDL and `platform/api/src/offerings.js` for the implementation.
|
||||
Phase 7 Steps 3/6/12-14/18-20 (extended in Phase 8, extended again in Phase 9) — the schema and
|
||||
lifecycle for offerings, capacity, registrations, agreements, and curriculum packages. See
|
||||
`platform/api/migrations/034_phase7_offerings_registrations_agreements.sql`,
|
||||
`035_phase8_enrollment_operations.sql`, and `037_phase9_commerce.sql` for the exact DDL and
|
||||
`platform/api/src/offerings.js`/`commerce.js` for the implementation.
|
||||
|
||||
**Updated for Phase 8**: registration is now server-authoritative (draft/cancelled/completed/
|
||||
full/window-closed offerings reject registration attempts with distinct error codes — see
|
||||
@@ -14,6 +15,14 @@ registration integration (`POST /api/integrations/registrations`) is live — se
|
||||
WEBSITE_ENROLLMENT_INTEGRATION.md`; and launch readiness is now server-computed
|
||||
(`computeOfferingReadiness()`), exposed at `GET /api/admin/offerings/:id/readiness`.
|
||||
|
||||
**Updated for Phase 9**: an offering can carry a real price (`price_amount`/`price_currency`); a
|
||||
paid offering's registration reserves its seat with a bounded expiry
|
||||
(`reservation_expires_at`) instead of holding it forever; agreement acceptance is now reachable
|
||||
from the public registration contract (closing Phase 8's one open gap); and a verified payment
|
||||
webhook — never a browser redirect — is what actually confirms a registration and activates the
|
||||
enrollment. See "Pricing" and "Payment and the commerce boundary" below, and `docs/
|
||||
PHASE9_COMMERCE_AND_LAUNCH.md` for the full phase report.
|
||||
|
||||
## Offering lifecycle (server-defined transitions only)
|
||||
|
||||
```
|
||||
@@ -77,6 +86,16 @@ Not every offering needs every stage — a simple self-paced offering can go `st
|
||||
`confirmed` → `enrollment_activated` directly; a stage is a real state a registration CAN pass
|
||||
through, not one it MUST. `enrollment_activated` and `cancelled` are terminal.
|
||||
|
||||
**Phase 9** — for a PRICED offering, a verified `checkout.session.completed` (or the test
|
||||
provider's equivalent) webhook event is what actually walks the registration through whichever
|
||||
of `submitted`/`agreements_pending`/`reviewed`/`payment_pending` it's currently sitting in,
|
||||
straight to `confirmed`, using the exact same `transitionRegistration()` table every other path
|
||||
already enforces — never a shortcut that skips a real state. A failed payment does NOT cancel
|
||||
the registration or release its seat (the registrant can retry checkout against the same
|
||||
reservation); only an abandoned/expired checkout session, or the reservation reaper timing out an
|
||||
un-paid reservation, cancels the registration and releases the seat. See "Payment and the
|
||||
commerce boundary" below.
|
||||
|
||||
A registration may exist with `student_id = NULL` (a guardian registering a not-yet-created
|
||||
student). `convertRegistrationToEnrollment()` is the one place a registration becomes a real
|
||||
academic `enrollment` row — it reuses `enrollment.js`'s existing `resolveOrCreateStudent`/
|
||||
@@ -129,8 +148,43 @@ For cohort/camp/workshop delivery, an offering's `cohort_id` (nullable) points a
|
||||
up on the real classroom roster automatically, with zero duplicated roster state. A `self_paced`
|
||||
offering simply has `cohort_id = NULL`; there is no cohort to join.
|
||||
|
||||
## Pricing (Phase 9)
|
||||
|
||||
`offerings.price_amount` (integer minor units — cents, matching Stripe's own convention;
|
||||
`NULL` = free/no checkout) and `offerings.price_currency` (default `usd`), both editable via
|
||||
`PATCH /api/admin/offerings/:id`. A registration's checkout amount always comes from the
|
||||
offering's OWN server-side price at checkout-creation time — never from anything the browser or
|
||||
website backend supplies (`commerce.createCheckoutSession()` reads it directly from the
|
||||
`offerings` row; there is no `amount` field anywhere in the checkout request the website sends).
|
||||
`computeOfferingReadiness()` gained matching `price` and `payment_provider` checks — a priced
|
||||
offering isn't launch-ready until a real price is set and the configured payment provider is
|
||||
actually usable (the test provider always is; Stripe requires both `STRIPE_SECRET_KEY` and
|
||||
`STRIPE_WEBHOOK_SECRET` to be set).
|
||||
|
||||
## Payment and the commerce boundary (Phase 9)
|
||||
|
||||
**Payment is a narrow, provider-agnostic layer bolted onto the existing registration model, not
|
||||
a redesign of it.** `payment_transactions` (one row per checkout attempt: amount/currency
|
||||
snapshotted at creation, provider, provider_reference, status) and `payment_webhook_events` (one
|
||||
row per provider event id — the ENTIRE webhook idempotency mechanism, a real unique primary key,
|
||||
not a "check then insert") are the only two new tables. See `docs/PHASE9_COMMERCE_AND_LAUNCH.md`
|
||||
for the payment-provider abstraction, the webhook contract, and the reservation-reaper design in
|
||||
full, and `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` for the website's own checkout contract.
|
||||
|
||||
**Three separately-tracked statuses, never auto-cascading into each other:** the registration's
|
||||
own `status` (this document), the enrollment's own `status` (unchanged, `enrollment.js`), and a
|
||||
payment transaction's own `status` (`payment_transactions.status`). A refund updates ONLY the
|
||||
transaction's status — it never automatically cancels the registration or the enrollment; an
|
||||
admin decides separately whether a refund also means "this student is no longer enrolled,"
|
||||
because the two are genuinely different real-world decisions (a partial-camp refund for a missed
|
||||
day is not the same decision as pulling a student's access).
|
||||
|
||||
## What was deliberately not built
|
||||
|
||||
Payment processing, a public-facing registration form, pricing display, and subscription
|
||||
billing — all explicitly out of Phase 7's scope. `offerings.metadata` (JSONB) has room for
|
||||
future descriptive pricing/marketing fields, but nothing writes payment data into it.
|
||||
Subscriptions, membership billing, coupons/discount codes, gift cards, installment/financing
|
||||
plans, complex tax automation, and accounting-system (QuickBooks etc.) integration — all
|
||||
explicitly out of scope, per Phase 9's own authorization. A waitlist was considered and
|
||||
deliberately deferred (see `docs/PHASE9_COMMERCE_AND_LAUNCH.md`'s "What was deliberately not
|
||||
built" section for the reasoning). `offerings.metadata` (JSONB) still has room for future
|
||||
descriptive marketing fields, but nothing writes payment data into it — payment data lives only
|
||||
in `payment_transactions`.
|
||||
|
||||
Reference in New Issue
Block a user