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`.
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
# Phase 9 — Commerce Integration, Website Registration & Winter Break Launch
|
||||
|
||||
Phase 9's mission: turn Phase 8's operable-but-unpaid enrollment system into a REAL commerce
|
||||
pipeline — offering price → website registration → agreement acceptance (closing Phase 8's one
|
||||
open gap) → hosted checkout → a verified, idempotent payment webhook → registration confirmation
|
||||
→ enrollment activation — without ever trusting a browser for price, payment status, or
|
||||
agreement acceptance, and without touching a single payment card number anywhere in this
|
||||
codebase.
|
||||
|
||||
## Baseline
|
||||
|
||||
Confirmed at start: branch `main`, clean tree, HEAD = `origin/main` = Phase 8's
|
||||
`788fb197e0a270423c1ff55829dc1d5dc380591b`.
|
||||
|
||||
## What was built
|
||||
|
||||
- **Migration 037** — `offerings.price_amount`/`price_currency` (nonnegative CHECK),
|
||||
`offering_registrations.reservation_expires_at`, and two new tables:
|
||||
`payment_transactions` (one row per checkout attempt — amount/currency snapshotted at
|
||||
creation, provider, provider_reference with a unique partial index, status) and
|
||||
`payment_webhook_events` (one row per provider event id, a real PK — the entire webhook
|
||||
idempotency mechanism, never a "check then insert").
|
||||
- **`payment-provider.js`** (new) — the payment-provider abstraction. `StripeProvider` (real
|
||||
Stripe SDK, lazy-constructed so a test-provider dev stack never needs the real package
|
||||
configured, real HMAC webhook verification via Stripe's own SDK helper, a narrow
|
||||
`normalizeEvent()` mapping Stripe's event vocabulary to `{eventId, eventType,
|
||||
providerReference, normalizedStatus}`) and `TestProvider` (deterministic, in-process, no
|
||||
network, no real money — the default, matching `EMAIL_PROVIDER`'s existing "dev by default,
|
||||
real provider opt-in" precedent). `commerce.js` and everything above it call the SAME methods
|
||||
regardless of which is active — swapping providers is an environment variable, not a code
|
||||
change.
|
||||
- **`commerce.js`** (new) — the business-logic layer combining `offerings.js` and
|
||||
`payment-provider.js`: `createCheckoutSession()` (validates state/agreements, snapshots the
|
||||
offering's own price, never a caller-supplied amount), `processWebhookEvent()` (the idempotent
|
||||
webhook entry point), `handlePaymentSuccess/Failure/CheckoutAbandoned/Refund()`,
|
||||
`recordManualRefund()`, `reapExpiredReservations()`.
|
||||
- **`webhook-routes.js`** (new) — `POST /api/webhooks/payment`. Reads the raw request body
|
||||
(Express raw-body middleware scoped to only this one path, registered before the global JSON
|
||||
parser in `server.js`), checks `Stripe-Signature`/`X-Test-Payment-Signature`, treats a bad
|
||||
signature as a clean, quiet `400` (never a 500, never a hint about why), always responds `200`
|
||||
once verification passes (a duplicate/ignored event is recorded, not retried forever).
|
||||
- **`integration-routes.js` extended** — `POST /api/integrations/registrations/:id/agreements`
|
||||
(closes Phase 8's explicit gap — agreement acceptance is now on the public registration
|
||||
contract; who accepts is server-derived from the registration's own `is_minor`, never
|
||||
caller-supplied), `POST /api/integrations/registrations/:id/checkout` (server-to-server only,
|
||||
the amount always comes from the offering's own price), and `GET
|
||||
/api/integrations/registrations/:id/status` (a deliberately minimal, non-admin status lookup
|
||||
for the website's success page — no PII, no amount, no provider reference — see "Real bugs
|
||||
found" below for why this exists).
|
||||
- **`offerings.js` extended** — `reservation_expires_at` computed only for priced offerings
|
||||
(`computeReservationExpiry()`); `OFFERING_EDITABLE_FIELDS`/`updateOffering()` (dynamic,
|
||||
allowlisted `PATCH`, never touches status/slug/program_id); `computeOfferingReadiness()`
|
||||
gained `price` and `payment_provider` checks; `convertRegistrationToEnrollment()` now fires the
|
||||
same activation-or-confirmation email every other enrollment path already triggers (closing a
|
||||
second real gap — see below).
|
||||
- **Admin UI** — a Pricing card on the offering detail view (set/clear price), a Payments card on
|
||||
the registration detail view (transaction history, a "Record refund" action for paid
|
||||
transactions), both admin/owner-gated, both reusing the existing `Admin.module.css`/`adminApi`
|
||||
conventions exactly.
|
||||
- **Reservation reaper** — a `setInterval` poller (`server.js`) releasing any paid-offering
|
||||
reservation whose `reservation_expires_at` has passed and which never reached a terminal state
|
||||
(`enrollment_activated`/`cancelled`/`confirmed` excluded).
|
||||
- **Prometheus metrics** — `rootnode_checkout_sessions_total`, `rootnode_payments_total`,
|
||||
`rootnode_payment_webhooks_total`, `rootnode_payment_conversion_total`,
|
||||
`rootnode_seats_released_total` — every label a small fixed enum (result/status/provider/
|
||||
event_type/reason), never a registration_id, customer_id, or payment_intent_id.
|
||||
|
||||
## Three real bugs found and fixed during this phase's own testing (before any commit)
|
||||
|
||||
1. **Duplicate `registration_confirmed` notification.** `handlePaymentSuccess` originally fired
|
||||
an explicit `registration_confirmed` notification AFTER `transitionRegistration('confirmed')`
|
||||
already fires one internally — found via direct smoke-test output showing the notification
|
||||
twice on one registration. Fixed by deleting the redundant explicit call.
|
||||
2. **A converted registration never sent an activation email.** `convertRegistrationToEnrollment`
|
||||
created the real student/enrollment/cohort rows but never called `notifyForEnrollment` — every
|
||||
OTHER enrollment path (the admin manual-enroll route, `/api/integrations/enrollments`) already
|
||||
does. Fixed by adding the call — which then surfaced bug 3 below, found immediately by the
|
||||
Winter Break commerce rehearsal (not by the narrower e2e suite, which happened not to exercise
|
||||
a brand-new minor-guardian conversion through this exact path):
|
||||
3. **A converted minor student had no real guardian link.** `convertRegistrationToEnrollment`
|
||||
creates the new student and enrollment but never promoted the registration's own `guardian_id`
|
||||
into the relational `student_guardians` table — so `notifications.js`'s guardian-recipient
|
||||
fallback (`resolveRecipient()`) had nothing to resolve to, and the new activation email failed
|
||||
with `no_recipient_address`. Fixed by calling the existing `linkStudentGuardian()` helper
|
||||
(already used elsewhere for admin-managed guardian links, never previously wired into
|
||||
conversion) whenever `reg.is_minor && reg.guardian_id`. This is genuine plumbing, not a
|
||||
business decision — a minor student converting through a paid registration now has the exact
|
||||
same real guardian relationship a student created any other way already has.
|
||||
|
||||
Found via the Winter Break commerce rehearsal specifically, not the narrower e2e suite — the
|
||||
rehearsal's own multi-step, real-webhook, real-notification-worker structure exercises the exact
|
||||
timing and data shape a real minor registration produces, where a synthetic unit-style test can
|
||||
accidentally supply a field (like `registrant_email`) the real website contract never actually
|
||||
sends for a minor.
|
||||
|
||||
## Design decisions worth stating explicitly
|
||||
|
||||
- **Seat reservation timing unchanged from Phase 7/8**: a seat is still reserved AT
|
||||
REGISTRATION, not at checkout. Phase 9 layers a bounded, configurable expiration
|
||||
(`reservation_expires_at`, paid offerings only) on top rather than redesigning the reservation
|
||||
model — a registrant who starts checkout keeps their seat for
|
||||
`REGISTRATION_RESERVATION_TIMEOUT_MINUTES` (default 30) even if they never complete payment,
|
||||
released either by an abandoned-checkout webhook event or the reaper.
|
||||
- **A failed payment does NOT release the seat.** The registrant can retry checkout against the
|
||||
same reservation. Only an abandoned/expired checkout session or reaper timeout releases it.
|
||||
Proved directly in both `e2e-phase9.js` and the commerce rehearsal, as two genuinely different
|
||||
code paths (`handlePaymentFailure` vs. `handleCheckoutAbandoned`/the reaper), not one path with
|
||||
a flag.
|
||||
- **Three separately-tracked statuses, never auto-cascading**: registration status
|
||||
(`offering_registrations.status`), enrollment status (`enrollments.status`), payment status
|
||||
(`payment_transactions.status`). A manual refund updates ONLY the transaction's status —
|
||||
proved directly by querying both other statuses unchanged after a refund in `e2e-phase9.js`,
|
||||
not just asserted by comment.
|
||||
- **No waitlist was built.** Considered and deliberately deferred — the existing
|
||||
`cancelled`-releases-a-seat mechanism, now paired with the reservation reaper's automatic
|
||||
release of abandoned checkouts, covers the real near-term operational need (a freed seat
|
||||
reopens registration automatically) without adding a new status-machine branch this phase has
|
||||
no concrete evidence would be used for a single-camp launch. Revisit once real Winter Break
|
||||
operational experience exists.
|
||||
|
||||
## Testing
|
||||
|
||||
`platform/test/e2e-phase9.js` — 19/19 passing (fresh stack): offering pricing in the public
|
||||
serializer (no internal field leakage), checkout blocked until agreements are accepted,
|
||||
registration rejected on draft/closed/full offerings, the server-authoritative amount (a
|
||||
caller-supplied `amount` is silently ignored), webhook signature required/rejected-if-invalid/
|
||||
idempotent-on-replay, payment success driving real conversion and activation with exactly one
|
||||
`registration_confirmed` notification (no duplicate), a failed payment leaving the seat intact
|
||||
and the registration retriable, a last-seat concurrent checkout race (`[201,409]`), the
|
||||
reservation reaper releasing an expired unpaid reservation, a manual refund with no cascade into
|
||||
registration/enrollment status, a minor payment confirmation routing to the guardian (never
|
||||
writing to `students.email`), the admin payment view (amount/status/provider, never card data),
|
||||
RBAC (a plain instructor denied payment data, the public API never exposing it), audit/metric
|
||||
hygiene (no `sk_`/`whsec_`/fake-card patterns, no registration_id as a metric label), the new
|
||||
website status-lookup endpoint's minimal safe shape, and confirmation that ROOT_ V1 self-paced
|
||||
enrollment, a free (Phase-8-style) offering, and classroom/cohort flows are all unaffected.
|
||||
|
||||
## Winter Break commercial-path rehearsal
|
||||
|
||||
`infra/scripts/drills/test-phase9-winter-camp-commerce.js` — **13/13 passing** against a real,
|
||||
freshly-built stack, `PAYMENT_PROVIDER=test` (no live money, no Stripe account needed): admin
|
||||
configures a real priced offering (location, capacity, instructor, required agreement, price,
|
||||
payment provider) and readiness honestly reports `ready_to_open_registration: false` (dates/
|
||||
registration-window are deliberately unset — a real business decision this phase does not
|
||||
invent) while every check it DID configure shows ready; admin opens registration; a minor
|
||||
registers via the website integration, checkout is correctly blocked before the agreement is
|
||||
accepted, the guardian accepts it, checkout is created; the test payment provider confirms
|
||||
payment via a REAL webhook call — registration converts, the real activation token is retrieved
|
||||
via the same dev-preview mechanism `e2e-phase5c.js` already established (never a raw-token
|
||||
shortcut), and the minor's account activates; the exact same webhook event replayed (a real
|
||||
Stripe retry scenario) changes nothing further; an adult registers, pays, and converts
|
||||
independently with no guardian involved; a failed payment leaves a retriable registration without
|
||||
consuming a phantom extra seat; an abandoned checkout (expired session) releases the seat; two
|
||||
students genuinely compete for the final seat of a paid offering and exactly one wins; the
|
||||
instructor starts a real classroom session and Prepares Labs; the minor student joins, completes
|
||||
real orientation plus a REAL Lab task (`linux-inspect-hardware`) inside a real disposable Lab
|
||||
container, and the instructor sees the real roster, payment, and progress state for the camp.
|
||||
|
||||
## Regression
|
||||
|
||||
Fresh-stack reruns after every fix: `e2e-phase8.js` 16/16, `e2e-phase7.js` 17/17, `e2e-phase6f.js`
|
||||
17/17, `e2e-phase5c.js` 29/29, `e2e-phase5b.js` 32/32 — every existing flow this phase's changes
|
||||
touch (`offerings.js`, `notifications.js`) reconfirmed byte-for-byte unaffected. `e2e-phase5a.js`
|
||||
not independently rerun, per the same established convention as every prior phase's own doc — its
|
||||
checks are embedded in and reconfirmed by 5B's/5C's own "Phase 5A ... still works unmodified"
|
||||
assertions, and no code Phase 5A itself owns was touched. Course-content validator
|
||||
(`infra/scripts/validate-course-content.js`) — clean, 0 errors, unaffected by this phase.
|
||||
Migration idempotency — verified by restarting the API container against an already-migrated
|
||||
database (no `down -v`) and confirming a clean startup with no migration errors, in addition to
|
||||
every fresh `down -v`/`up --build` cycle across this phase's own testing applying all 37
|
||||
migrations cleanly from empty every time.
|
||||
|
||||
## Frontend build
|
||||
|
||||
`npm run build` clean — the new Pricing/Payments admin UI cards compile without warning; the
|
||||
only new warning is the pre-existing "chunk larger than 500kB" advisory, unchanged in cause from
|
||||
every prior phase.
|
||||
|
||||
## Real Lab result
|
||||
|
||||
The Winter Break commerce rehearsal drives a real disposable Lab container through a genuine
|
||||
`linux-inspect-hardware` scenario setup/validate cycle, reached this time through the full,
|
||||
real commerce path (checkout → webhook → conversion → activation → classroom join) rather than a
|
||||
simulated payment step — confirmed passing for real, not simulated.
|
||||
|
||||
## Security/privacy findings
|
||||
|
||||
- No payment-card data exists anywhere in this codebase — not in a request field, not in a
|
||||
database column, not in a log line, not in an audit-event metadata blob. Stripe's hosted
|
||||
Checkout page is the only thing that ever sees a real card number.
|
||||
- The price is read server-side from the offering at checkout-creation time; a caller-supplied
|
||||
`amount` field (tested directly) is silently ignored — a browser cannot choose its own price.
|
||||
- The webhook route requires a verified signature on every request; an invalid/forged signature
|
||||
is rejected with a generic `400`, never a 500 and never a hint about why verification failed
|
||||
(matching this codebase's existing anti-enumeration precedent).
|
||||
- The webhook is idempotent via a real database primary key on the provider's own event id — a
|
||||
retried delivery (Stripe's own retry behavior on anything but a `200`) can never double-process.
|
||||
- Admin payment views require admin/owner role; a plain instructor is denied (`403`, tested
|
||||
directly); the public offerings API never exposes payment/transaction data at all.
|
||||
- Metrics and audit events use only small, fixed-enum labels — no registration_id, customer_id,
|
||||
payment_intent_id, or email ever appears as a metric label or in audit metadata (tested
|
||||
directly via regex against both).
|
||||
- Cross-student/guardian isolation holds: a minor's payment confirmation routes the activation
|
||||
email to the guardian, never writing the guardian's email into the student's own identity
|
||||
record — the same invariant Phase 8 already established, reconfirmed under the new payment
|
||||
path.
|
||||
|
||||
## Legal-review requirements — unchanged, restated
|
||||
|
||||
See `docs/STUDENT_GUARDIAN_DATA_MODEL.md`'s list in full, still entirely unresolved by this
|
||||
phase's work: the actual legal text of any participation/media/liability agreement,
|
||||
jurisdiction-specific minor-consent requirements, data-retention/deletion policy, and whether
|
||||
emergency-contact fields need additional encryption at rest. Nothing about accepting payment
|
||||
changes this list.
|
||||
|
||||
## What was deliberately NOT built (explicit, per this phase's own authorization)
|
||||
|
||||
Subscriptions, membership billing, coupons/discount codes, gift cards, installment/financing
|
||||
plans, complex tax automation, accounting-system (QuickBooks etc.) integration, a Parent Portal
|
||||
or guardian login, a CRM, marketing email/SMS, certificates, INIT_/COMPILE_ curriculum, ROOT
|
||||
08/11, production deployment, a mobile app, and an AI tutor — none of these were touched, added,
|
||||
or scaffolded "for later." A waitlist was considered and deliberately deferred (see "Design
|
||||
decisions" above).
|
||||
|
||||
## Documentation
|
||||
|
||||
New: `PHASE9_COMMERCE_AND_LAUNCH.md` (this document), `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, the three-status
|
||||
principle, the updated "not built" list), `WEBSITE_ENROLLMENT_INTEGRATION.md` (price/availability
|
||||
fields, the real agreement/checkout/webhook/confirmation sections replacing Phase 8's
|
||||
placeholders), `WINTER_BREAK_CAMP_OPERATIONS.md` (the real payment step replacing the old manual
|
||||
placeholder, updated blockers), `platform/README.md` (test list). Reviewed, no change needed:
|
||||
`docs/ENGINEERING_SOP.md` (its own numbered-section convention has not been extended past Phase
|
||||
6B for any prior phase either — Phase 7 and 8 made the same call; its Roadmap Status table
|
||||
doesn't itemize commerce as a tracked line item to begin with).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- The website's own registration/checkout UI does not exist yet in this repository
|
||||
(rootnodeacademy.com is built separately, not RootNode-owned in this repo) — the contract is
|
||||
ready and tested, nobody has built the website-side form against it yet.
|
||||
- No real Stripe credentials exist — the entire commerce pipeline is built and tested against the
|
||||
deterministic test provider, which exercises the exact same `commerce.js`/webhook-route code
|
||||
path real Stripe traffic would, but has never made a real network call to Stripe's API or
|
||||
received a real Stripe-signed webhook. See `docs/STRIPE_INTEGRATION_RUNBOOK.md`.
|
||||
- No waitlist (deliberate, see "Design decisions" above).
|
||||
- `registration_needs_action` still has no automatic trigger, unchanged from Phase 8 — still a
|
||||
deliberate admin action, not something a failed payment forces (a failed payment DOES send this
|
||||
notification automatically, but broader "needs action" cases remain admin-initiated).
|
||||
|
||||
## Remaining blockers before opening Winter Break registration to the public
|
||||
|
||||
See `docs/WINTER_BREAK_LAUNCH_CHECKLIST.md` for the full, itemized list. In summary: real dates/
|
||||
venue/capacity/price (business decisions), the agreement's actual legal text (legal review), real
|
||||
Stripe credentials (external dependency), and the website's own registration form (separate,
|
||||
not-yet-built repo). None of these are platform readiness gaps — the platform is ready to operate
|
||||
the full commerce flow the moment those four external decisions/assets exist.
|
||||
|
||||
## Commits
|
||||
|
||||
See `git log` for this phase's exact commit sequence (migrations/payment-provider/commerce,
|
||||
integration routes, admin UI, tests + rehearsal, docs) — grouped per
|
||||
`docs/ENGINEERING_SOP.md`'s existing convention.
|
||||
|
||||
## Recommended next phase
|
||||
|
||||
Real Stripe live-mode validation once credentials exist (`docs/STRIPE_INTEGRATION_RUNBOOK.md`'s
|
||||
"Going live" section); the actual website-side registration/checkout form; the agreement's legal
|
||||
text once reviewed; and, informed by real Winter Break Camp commercial operation, whether a
|
||||
waitlist is actually worth building.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 9 APPLICATION BUILD COMPLETE / LIVE STRIPE VALIDATION PENDING
|
||||
@@ -98,15 +98,19 @@ unchanged). A registration becomes a real enrollment only once `confirmed`, via
|
||||
`agreement_definitions`/`agreement_acceptances`): a versioned ACCEPTANCE RECORD, never legal
|
||||
text authored by this codebase. See `docs/OFFERING_AND_REGISTRATION_MODEL.md`.
|
||||
|
||||
## Commerce boundary — unchanged, extended in documentation only
|
||||
## Commerce boundary — real as of Phase 9
|
||||
|
||||
The existing boundary (`docs/WEBSITE_ENROLLMENT_INTEGRATION.md`) stands: the website owns
|
||||
marketing/pricing/checkout, RootNode's platform owns everything after a verified commerce event.
|
||||
Phase 7 adds a documented FUTURE flow (website → offering selection → registration → checkout →
|
||||
verified commerce event → registration confirmed → enrollment activated) without implementing
|
||||
any payment code. **BUILT NOW**: the existing `/api/integrations/enrollments` endpoint,
|
||||
unchanged. **PHASE 7 BUILT**: the registration/offering model that future flow will plug into.
|
||||
**FUTURE**: an actual commerce-event webhook confirming a registration.
|
||||
The boundary itself is unchanged: the website owns marketing/pricing DISPLAY and the hosted
|
||||
checkout experience, RootNode's platform owns the actual price (server-controlled, never
|
||||
browser-supplied), payment verification, and everything after a verified commerce event. Phase 7
|
||||
documented the flow; **Phase 9 built it for real**: an offering can carry a price
|
||||
(`offerings.price_amount`/`price_currency`), a payment-provider abstraction (Stripe or a
|
||||
deterministic in-process test provider — see `docs/PHASE9_COMMERCE_AND_LAUNCH.md`) creates a
|
||||
hosted checkout session server-side, and a verified, idempotent webhook is the ONLY thing that
|
||||
ever confirms payment and drives `registration confirmed → enrollment activated` — a browser
|
||||
`?paid=true` redirect proves nothing and is never trusted. See `docs/
|
||||
WEBSITE_REGISTRATION_AND_CHECKOUT.md` for the website's contract and `docs/
|
||||
STRIPE_INTEGRATION_RUNBOOK.md` for going live with real Stripe.
|
||||
|
||||
## First commercial target: Winter Break Camp
|
||||
|
||||
@@ -141,6 +145,13 @@ planned this phase.
|
||||
| Guardian communications (5 new notification templates, guardian recipient fallback) | PHASE 8 BUILT |
|
||||
| Public offering read API + website registration submission contract | PHASE 8 BUILT (`docs/WEBSITE_ENROLLMENT_INTEGRATION.md`) |
|
||||
| Offering agreement requirements + conversion blocking | PHASE 8 BUILT |
|
||||
| Public registration flow (a real website-side form), payments, subscriptions | FUTURE |
|
||||
| Offering pricing, payment-provider abstraction, hosted checkout, verified webhook | **PHASE 9 BUILT** (`docs/PHASE9_COMMERCE_AND_LAUNCH.md`) — Stripe test-mode + a deterministic in-process test provider; real Stripe live-mode validation is pending real credentials. |
|
||||
| Agreement acceptance on the public registration contract | **PHASE 9 BUILT** — closes the gap Phase 8 left open. |
|
||||
| Refund/cancellation recording (commerce status only, never auto-cascading) | PHASE 9 BUILT |
|
||||
| Abandoned-reservation reaper, reservation expiry for paid offerings | PHASE 9 BUILT |
|
||||
| Admin payment UI (price, payment/refund status, no card data) | PHASE 9 BUILT |
|
||||
| A real website-side registration/checkout form (rootnodeacademy.com itself) | FUTURE — contract ready, form not built in this repo (separate repo, not RootNode-owned). |
|
||||
| Live Stripe validation (real credentials, real card, real webhook from Stripe's servers) | FUTURE — application layer is done and Stripe-test-mode-tested; pending real Stripe credentials. |
|
||||
| Subscriptions, membership billing, coupons, gift cards, installment plans, tax automation | FUTURE, explicitly out of scope |
|
||||
| INIT_/COMPILE_ curriculum | FUTURE |
|
||||
| School/library SIS integrations, parent portal | FUTURE, explicitly out of scope |
|
||||
| School/library SIS integrations, parent portal, guardian login | FUTURE, explicitly out of scope |
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# Stripe Integration Runbook
|
||||
|
||||
Operational reference for RootNode's payment-provider abstraction (`platform/api/src/
|
||||
payment-provider.js`) — configuration, local testing against both the deterministic test
|
||||
provider and real Stripe test mode, webhook setup, and what's left before a real, live-money
|
||||
launch. See `docs/PHASE9_COMMERCE_AND_LAUNCH.md` for the design/architecture story and
|
||||
`docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` for the website's own contract.
|
||||
|
||||
## The two providers
|
||||
|
||||
| | `PAYMENT_PROVIDER=test` (default) | `PAYMENT_PROVIDER=stripe` |
|
||||
|---|---|---|
|
||||
| Network calls | None — fully in-process | Real calls to Stripe's API |
|
||||
| Needs credentials | No | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` |
|
||||
| Checkout URL | A fake URL pointing back at your own `success_url` | A real Stripe-hosted Checkout page |
|
||||
| Webhook signature | A static shared secret (`TEST_PAYMENT_WEBHOOK_SECRET`, header `X-Test-Payment-Signature`) | Real Stripe HMAC signature (header `Stripe-Signature`), verified via Stripe's own SDK |
|
||||
| Real money | Never | Real in live mode; fake (test cards) in Stripe's own test mode |
|
||||
| Used by | Every automated test/rehearsal in this repo | Nothing yet — pending real credentials |
|
||||
|
||||
`commerce.js` and every route above it call the SAME methods regardless of which provider is
|
||||
active (`createCheckoutSession`, `verifyAndParseWebhook`) — switching providers is an environment
|
||||
variable, never a code change.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `PAYMENT_PROVIDER` | `test` | `test` or `stripe`. |
|
||||
| `STRIPE_SECRET_KEY` | unset | Required for `stripe`. Server-side only — never exposed to any browser-served code. |
|
||||
| `STRIPE_WEBHOOK_SECRET` | unset | Required for `stripe`. The signing secret Stripe gives you for a specific webhook endpoint (dashboard or `stripe listen`, see below) — NOT your API secret key. |
|
||||
| `TEST_PAYMENT_WEBHOOK_SECRET` | `test-payment-secret-not-for-production` | Only used by the `test` provider. Change if you need to isolate parallel test runs; otherwise the default is fine. |
|
||||
| `REGISTRATION_RESERVATION_TIMEOUT_MINUTES` | `30` | How long a paid offering's seat stays reserved before the reaper releases it if checkout is never completed. |
|
||||
| `RESERVATION_REAPER_POLL_MS` | `60000` | How often the reservation reaper checks for expired, unpaid reservations. |
|
||||
|
||||
None of these belong in `frontend/` or anywhere a browser can read them — the Stripe secret key
|
||||
and webhook secret are server-only, exactly like `INTEGRATION_SHARED_SECRET`/
|
||||
`PROVISIONER_SHARED_SECRET` already are in this codebase.
|
||||
|
||||
## Local testing against the test provider (no Stripe account needed)
|
||||
|
||||
This is what every e2e suite and rehearsal in this repo actually runs against. Leave
|
||||
`PAYMENT_PROVIDER` unset (defaults to `test`) and drive the full cycle directly:
|
||||
|
||||
```bash
|
||||
# 1. Create a checkout session (server-to-server, same as the website would)
|
||||
curl -s -X POST http://localhost:4200/api/integrations/registrations/<id>/checkout \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'X-RootNode-Integration-Key: dev-integration-secret-not-for-production' \
|
||||
-d '{"success_url":"https://example.com/s","cancel_url":"https://example.com/c"}'
|
||||
|
||||
# 2. Look up the provider_reference the checkout just created
|
||||
docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform \
|
||||
-c "SELECT provider_reference FROM payment_transactions WHERE registration_id = '<id>' ORDER BY created_at DESC LIMIT 1;"
|
||||
|
||||
# 3. Build and POST a fake "payment succeeded" webhook event (Node, using the same provider
|
||||
# module the real server uses — see platform/test/e2e-phase9.js for the full pattern)
|
||||
node -e "
|
||||
const { getPaymentProvider } = require('./platform/api/src/payment-provider');
|
||||
const p = getPaymentProvider();
|
||||
const event = p.buildTestEvent({ providerReference: '<provider_reference from step 2>', eventType: 'checkout.session.completed' });
|
||||
console.log(JSON.stringify(event));
|
||||
"
|
||||
curl -s -X POST http://localhost:4200/api/webhooks/payment \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "X-Test-Payment-Signature: <event.signature from above>" \
|
||||
-d '<event.body from above>'
|
||||
```
|
||||
|
||||
To simulate a **failed payment** or **abandoned checkout**, use `eventType:
|
||||
'payment_intent.payment_failed'` or `'checkout.session.expired'` in step 3 instead — see
|
||||
`platform/test/e2e-phase9.js` for both, and for the reservation-reaper path (never send a webhook
|
||||
at all; wait past `REGISTRATION_RESERVATION_TIMEOUT_MINUTES`, or lower it for a faster local
|
||||
test).
|
||||
|
||||
## Local testing against real Stripe TEST mode (once credentials exist)
|
||||
|
||||
Not yet exercised in this repo — no Stripe account/credentials have been provided as of this
|
||||
phase. Once they exist:
|
||||
|
||||
1. Create (or use an existing) Stripe account, switch to **Test mode** in the dashboard.
|
||||
2. Copy the test-mode **Secret key** (`sk_test_...`) into `STRIPE_SECRET_KEY`.
|
||||
3. Install the [Stripe CLI](https://stripe.com/docs/stripe-cli) and run:
|
||||
```bash
|
||||
stripe login
|
||||
stripe listen --forward-to localhost:4200/api/webhooks/payment
|
||||
```
|
||||
The CLI prints a webhook signing secret (`whsec_...`) — set that as `STRIPE_WEBHOOK_SECRET`
|
||||
for the duration of the local session (it's ephemeral per `stripe listen` run; a real
|
||||
deployment instead creates a permanent webhook endpoint in the Stripe dashboard and uses ITS
|
||||
signing secret).
|
||||
4. Set `PAYMENT_PROVIDER=stripe` and restart the API.
|
||||
5. Drive the checkout flow through steps 2-4 of `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` — you
|
||||
now get back a REAL `checkout_url`. Open it in a browser and pay with a Stripe test card
|
||||
(`4242 4242 4242 4242`, any future expiry, any CVC, any ZIP).
|
||||
6. `stripe listen` forwards Stripe's real webhook event to your local server; watch the server
|
||||
log and `payment_webhook_events` table for `processed`.
|
||||
7. To simulate a failure without a real failing card, use Stripe's documented test card numbers
|
||||
for declines (e.g. `4000 0000 0000 0002`), or `stripe trigger payment_intent.payment_failed`.
|
||||
|
||||
This is the recommended path to real validation before going live — it exercises the actual
|
||||
Stripe SDK, actual signature verification, and actual Stripe event delivery, with zero real
|
||||
money at risk.
|
||||
|
||||
## Going live (real money)
|
||||
|
||||
1. Switch the Stripe dashboard to **Live mode**, generate a live **Secret key**
|
||||
(`sk_live_...`), and create a permanent webhook endpoint pointed at the production API's
|
||||
`/api/webhooks/payment` URL — copy ITS signing secret (`whsec_...`, different from test mode's).
|
||||
2. Set `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (live values) and `PAYMENT_PROVIDER=stripe` in
|
||||
production configuration only — never commit these to git, never put them in a `.env` file
|
||||
that gets version-controlled (matching this repo's existing secret-handling convention for
|
||||
`PROVISIONER_SHARED_SECRET`/`INTEGRATION_SHARED_SECRET`).
|
||||
3. Set a real price on the live offering (`PATCH /api/admin/offerings/:id`) and confirm
|
||||
`GET /api/admin/offerings/:id/readiness` shows `payment_provider: ready`.
|
||||
4. Run one real, small, genuinely-refundable transaction against a real card before opening
|
||||
registration publicly, and confirm the full pipeline (checkout → webhook → confirmation →
|
||||
enrollment → activation email) end to end, then refund it via the admin UI or the Stripe
|
||||
dashboard directly.
|
||||
5. Monitor `rootnode_payment_webhooks_total`/`rootnode_payments_total` (Prometheus metrics, see
|
||||
`docs/PHASE9_COMMERCE_AND_LAUNCH.md`) after opening registration.
|
||||
|
||||
**This phase does not perform steps 1-4 above** — no real Stripe credentials were provided, and
|
||||
inventing/obtaining them is a business decision reserved for Chase, not something this phase
|
||||
invents. Everything up to this runbook is built and tested against the test provider; live
|
||||
validation is the one explicitly pending item.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause |
|
||||
|---|---|
|
||||
| `503 PROVIDER_NOT_CONFIGURED` on checkout | `PAYMENT_PROVIDER=stripe` but `STRIPE_SECRET_KEY` unset, or the `stripe` npm package failed to load. |
|
||||
| `400 invalid_signature` on every real Stripe webhook | `STRIPE_WEBHOOK_SECRET` doesn't match the endpoint Stripe actually delivered to (test-mode secret used against a live endpoint, or vice versa — each `stripe listen`/dashboard endpoint has its OWN secret). |
|
||||
| Webhook received but nothing changes | Check `payment_webhook_events.result` for that event id — `duplicate` means it was already processed (expected on a retry); check server logs for an exception if `result` is missing entirely. |
|
||||
| Seat never releases after an abandoned checkout | Confirm the reservation reaper is running (`RESERVATION_REAPER_POLL_MS` log line on server startup) and that `REGISTRATION_RESERVATION_TIMEOUT_MINUTES` hasn't been set unreasonably high. |
|
||||
@@ -212,14 +212,21 @@ appear; `draft` and `cancelled` never do. Example response:
|
||||
"registration_open": true,
|
||||
"availability": "limited",
|
||||
"location_name": "Community Room B",
|
||||
"location_type": "community_center"
|
||||
"location_type": "community_center",
|
||||
"price_amount": 24900,
|
||||
"price_currency": "usd",
|
||||
"requires_payment": true
|
||||
}
|
||||
```
|
||||
|
||||
`availability` is `"open"` / `"limited"` / `"full"` — never a raw seat count (avoids inviting
|
||||
scraping/timing games against a public endpoint). No internal notes, no organization contact
|
||||
info, no instructor identity, no pricing — the website owns pricing display entirely; this
|
||||
endpoint doesn't return it.
|
||||
info, no instructor identity — just enough to build a pricing/availability display. **As of Phase
|
||||
9**: `price_amount` is an integer in minor currency units (cents — `24900` = $249.00), `null` for
|
||||
a free offering; `price_currency` is a lowercase ISO code (`"usd"`); `requires_payment` is a
|
||||
plain derived boolean (`price_amount !== null`) so the website never has to reimplement that
|
||||
check. The actual checkout amount is always read server-side from this same offering row at
|
||||
checkout-creation time — the website never sends an amount anywhere in this contract.
|
||||
|
||||
### 2. Select an offering, then submit a registration (server-to-server)
|
||||
|
||||
@@ -285,35 +292,63 @@ required; `phone` optional). RootNode matches an existing guardian by email firs
|
||||
family doesn't get a duplicate guardian record per sibling/per camp) or creates a new one. The
|
||||
guardian's email is never written to the eventual student's own identity record.
|
||||
|
||||
### 6. Agreement flow
|
||||
### 6. Agreement flow (Phase 9 — live)
|
||||
|
||||
Not yet exposed on this endpoint — an agreement acceptance is currently recorded by RootNode
|
||||
staff (or a future self-service step) after the registration is submitted, before it can be
|
||||
confirmed. If your registration flow collects agreement acceptance at submission time, talk to
|
||||
the RootNode team about extending this contract before building around a workaround.
|
||||
```
|
||||
POST /api/integrations/registrations/:id/agreements
|
||||
Body: { "agreement_id": "winter-camp-waiver-2026" }
|
||||
```
|
||||
|
||||
### 7. Future payment step
|
||||
Same shared-secret header as every other route in this section. Which party accepts is
|
||||
server-derived from the registration's own `is_minor`, never a value you send: an adult accepts
|
||||
as themselves (before their student account even exists yet), a minor's registration is accepted
|
||||
by the guardian already on file from submission. `201 { "accepted": true, "agreement_id": ...,
|
||||
"accepted_version": ... }` on success; `404 AGREEMENT_NOT_FOUND` if the id doesn't match a real
|
||||
agreement configured on this offering. **Checkout is blocked (`409 AGREEMENTS_INCOMPLETE`) until
|
||||
every required agreement is accepted** — call this before calling checkout below.
|
||||
|
||||
Not built. Today, a registration is manually moved to `confirmed` by RootNode staff once payment
|
||||
is verified through whatever process exists outside the platform. A future phase's real payment
|
||||
integration would do this automatically on a verified commerce event.
|
||||
### 7. Checkout (Phase 9 — live)
|
||||
|
||||
### 8. Confirmation
|
||||
```
|
||||
POST /api/integrations/registrations/:id/checkout
|
||||
Body: { "success_url": "https://rootnodeacademy.com/enroll/success", "cancel_url": "https://rootnodeacademy.com/enroll/cancelled" }
|
||||
```
|
||||
|
||||
Once `confirmed` (manually today, automatically in a future payment-integrated phase) and every
|
||||
required agreement is accepted, the registration can convert to a real enrollment.
|
||||
Server-to-server only — the browser never calls this directly, and never supplies a price. The
|
||||
amount, currency, and product description all come from the offering's own server-side
|
||||
configuration. Success: `201 { "checkout_url": "...", "transaction_id": "..." }` — redirect the
|
||||
customer's browser to `checkout_url` (RootNode's configured payment provider's own hosted
|
||||
checkout page; today that's Stripe Checkout in test mode, or a fake URL from the deterministic
|
||||
test provider in local dev — see `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md`). A free offering
|
||||
(`price_amount: null`) returns `409 NO_PRICE_CONFIGURED` — never call this for a free offering;
|
||||
just proceed straight to confirmation. Full error table, the success-page design (never trust a
|
||||
returning `?paid=true`), and the status-lookup endpoint are in `docs/
|
||||
WEBSITE_REGISTRATION_AND_CHECKOUT.md`.
|
||||
|
||||
### 9. Enrollment activation
|
||||
### 8. Payment confirmation — a verified webhook, never a browser redirect
|
||||
|
||||
Conversion creates (or matches) the real student account and a real academic enrollment, reusing
|
||||
the exact same activation-email machinery `/api/integrations/enrollments` already triggers — no
|
||||
second activation path exists.
|
||||
RootNode's own webhook (not something the website calls) is the ONLY thing that confirms
|
||||
payment. Once confirmed, the registration walks forward to `confirmed` and converts to a real
|
||||
enrollment automatically — reusing the exact activation-email flow described below, no separate
|
||||
"payment succeeded" email exists. See `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` for how to
|
||||
build a safe success page against this (poll the status-lookup endpoint; never derive success
|
||||
from the URL alone).
|
||||
|
||||
### 9. Confirmation and enrollment activation
|
||||
|
||||
Once `confirmed` (via the webhook above for a priced offering, or a manual admin transition for
|
||||
a free one) and every required agreement is accepted, the registration converts to a real
|
||||
enrollment — creating or matching the real student account and reusing the exact same
|
||||
activation-email machinery `/api/integrations/enrollments` already triggers. No second
|
||||
confirmation path and no second activation path exist.
|
||||
|
||||
### 10. What this endpoint does NOT do
|
||||
|
||||
No payment-card data (never sent, never has a field for it). No password (RootNode issues its
|
||||
own credential/activation flow). No arbitrary registration status (server-defined transitions
|
||||
only — see `docs/OFFERING_AND_REGISTRATION_MODEL.md`). All example data above is fake — do not
|
||||
No payment-card data (never sent, never has a field for it — see `docs/
|
||||
WEBSITE_REGISTRATION_AND_CHECKOUT.md` for the full payment-security posture). No password
|
||||
(RootNode issues its own credential/activation flow). No arbitrary registration status
|
||||
(server-defined transitions only — see `docs/OFFERING_AND_REGISTRATION_MODEL.md`). All example
|
||||
data above is fake — do not
|
||||
reuse these values against a real deployment.
|
||||
|
||||
## What the student experiences after you call this (as of Phase 5C)
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# Website → RootNode Registration & Checkout
|
||||
|
||||
Audience: whoever builds `rootnodeacademy.com`'s registration/checkout flow for a paid offering
|
||||
(Winter Break Camp and anything after it). This is the Phase 9 companion to
|
||||
`docs/WEBSITE_ENROLLMENT_INTEGRATION.md` — read that document first for the shared trust
|
||||
boundary, authentication header, and the base `/api/integrations/registrations` contract. This
|
||||
document covers everything Phase 9 adds on top: pricing display, the agreement step, checkout,
|
||||
the success page, and status lookup.
|
||||
|
||||
## The end-to-end flow
|
||||
|
||||
```
|
||||
1. GET /api/public/offerings/:slug — display program, price, availability (no auth, browser-safe)
|
||||
2. POST /api/integrations/registrations — submit the registration (server-to-server)
|
||||
3. POST /api/integrations/registrations/:id/agreements — accept the required agreement(s)
|
||||
4. POST /api/integrations/registrations/:id/checkout — create a hosted checkout session
|
||||
5. redirect the customer's browser to the returned checkout_url
|
||||
6. RootNode's webhook confirms payment — NOT something the website calls
|
||||
7. GET /api/integrations/registrations/:id/status — poll from the success page
|
||||
```
|
||||
|
||||
Steps 2-4 and 7 are server-to-server only, using the same `X-RootNode-Integration-Key` header as
|
||||
every other integration route. Steps 1 and 5 are the only things that ever touch the customer's
|
||||
browser directly.
|
||||
|
||||
## 1. Price and availability display
|
||||
|
||||
```
|
||||
GET /api/public/offerings/:slug
|
||||
```
|
||||
|
||||
No auth needed. As of Phase 9, the response includes:
|
||||
|
||||
```json
|
||||
{
|
||||
"price_amount": 24900,
|
||||
"price_currency": "usd",
|
||||
"requires_payment": true
|
||||
}
|
||||
```
|
||||
|
||||
`price_amount` is an integer in minor currency units (cents) — divide by 100 and format with
|
||||
`price_currency` for display (`$249.00`). `price_amount: null` means a free offering —
|
||||
`requires_payment` is `false` and you should skip steps 3/4/5 entirely (call `checkout` on a free
|
||||
offering and you'll get `409 NO_PRICE_CONFIGURED`; there's nothing to check out).
|
||||
|
||||
**This price is display-only.** The actual amount charged always comes from the SAME offering
|
||||
row, read server-side at checkout-creation time (step 4) — there is no field anywhere in this
|
||||
contract for the website to submit an amount. A browser (or a compromised website backend)
|
||||
cannot pay a different price than what's configured on the offering.
|
||||
|
||||
## 2. Submit the registration
|
||||
|
||||
Unchanged from `docs/WEBSITE_ENROLLMENT_INTEGRATION.md` §2 — adult or minor/guardian flow, same
|
||||
`event_id` idempotency. You'll get back a `registration_id`; hold onto it for every step below.
|
||||
|
||||
## 3. Accept the agreement(s)
|
||||
|
||||
```
|
||||
POST /api/integrations/registrations/:id/agreements
|
||||
Header: X-RootNode-Integration-Key: <secret>
|
||||
Body: { "agreement_id": "winter-camp-waiver-2026" }
|
||||
```
|
||||
|
||||
Ask RootNode for the current `agreement_id`(s) configured on the offering — there is no
|
||||
"list required agreements" endpoint on this public contract yet; if your flow needs to render the
|
||||
agreement text/checkbox dynamically, ask the RootNode team before building a workaround.
|
||||
|
||||
Which party is recorded as accepting is **server-derived from the registration's own
|
||||
`is_minor`** (set at submission in step 2) — you never send who's accepting. `201` on success:
|
||||
|
||||
```json
|
||||
{ "accepted": true, "agreement_id": "winter-camp-waiver-2026", "accepted_version": 1 }
|
||||
```
|
||||
|
||||
`404 AGREEMENT_NOT_FOUND` if the id is wrong or not configured on this offering.
|
||||
|
||||
**Checkout is blocked until every required agreement is accepted** (`409 AGREEMENTS_INCOMPLETE`
|
||||
from step 4). Call this before checkout, always.
|
||||
|
||||
## 4. Create the checkout session
|
||||
|
||||
```
|
||||
POST /api/integrations/registrations/:id/checkout
|
||||
Header: X-RootNode-Integration-Key: <secret>
|
||||
Body: { "success_url": "https://rootnodeacademy.com/enroll/success", "cancel_url": "https://rootnodeacademy.com/enroll/cancelled" }
|
||||
```
|
||||
|
||||
Both URLs are **yours** — RootNode's payment provider redirects the customer's browser back to
|
||||
one of them after checkout. `success_url` and `cancel_url` are required; nothing else is. There
|
||||
is no `amount` field — see "Price and availability display" above.
|
||||
|
||||
Success (`201`):
|
||||
|
||||
```json
|
||||
{ "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...", "transaction_id": "..." }
|
||||
```
|
||||
|
||||
Redirect the customer's browser to `checkout_url` immediately (a `302`, or a client-side
|
||||
`window.location`). In local/staging use with the deterministic test provider (see
|
||||
`docs/STRIPE_INTEGRATION_RUNBOOK.md`), `checkout_url` is a fake URL pointing straight back at
|
||||
your own `success_url` with a `?test_session=...` param appended — there is no real hosted page
|
||||
to visit, which is fine for automated testing but means you can't manually click through a
|
||||
"real" checkout page against the test provider.
|
||||
|
||||
**Errors:**
|
||||
|
||||
| Status | `error` | Meaning |
|
||||
|---|---|---|
|
||||
| `401` | `unauthorized` | Missing/wrong integration key. |
|
||||
| `400` | `VALIDATION` | `success_url`/`cancel_url` missing. |
|
||||
| `404` | `NOT_FOUND` / `OFFERING_NOT_FOUND` | Bad registration id, or its offering vanished. |
|
||||
| `409` | `NO_PRICE_CONFIGURED` | This offering is free — don't call checkout for it. |
|
||||
| `409` | `AGREEMENTS_INCOMPLETE` | Call step 3 first. |
|
||||
| `409` | `INVALID_STATE` | The registration is already past the point where checkout makes sense (e.g. already confirmed). |
|
||||
| `409` | `OFFERING_NOT_OPEN` / `OFFERING_CANCELLED` / `OFFERING_COMPLETED` / `OFFERING_FULL` / `REGISTRATION_NOT_YET_OPEN` / `REGISTRATION_CLOSED` | The offering/registration window state changed since step 2. |
|
||||
| `503` | `PROVIDER_NOT_CONFIGURED` | RootNode's payment provider isn't configured yet — a RootNode-side problem, not yours. Show a generic "checkout is temporarily unavailable" message and alert RootNode. |
|
||||
|
||||
## 5. Payment confirmation — never trust the redirect
|
||||
|
||||
**RootNode's own webhook is the only thing that ever confirms payment.** When the payment
|
||||
provider redirects the customer back to your `success_url`, that redirect proves nothing on its
|
||||
own — a customer can reload, bookmark, or manually construct that URL without ever paying, and a
|
||||
successful redirect can race ahead of the webhook actually being processed. **Never render "you're
|
||||
enrolled" or grant anything based on the URL/query string alone.**
|
||||
|
||||
## 6. Building a safe success page
|
||||
|
||||
```
|
||||
GET /api/integrations/registrations/:id/status
|
||||
Header: X-RootNode-Integration-Key: <secret>
|
||||
```
|
||||
|
||||
Call this from your OWN backend (never expose the integration key to the browser) when the
|
||||
customer lands on your success page, and poll it every couple of seconds for a short window (the
|
||||
webhook is normally processed within a second or two of the customer completing checkout — see
|
||||
`docs/STRIPE_INTEGRATION_RUNBOOK.md` for real-world Stripe delivery latency). Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"registration_id": "6dd6d898-f895-477f-97d8-1e5ce89c27cd",
|
||||
"status": "confirmed",
|
||||
"payment_status": "paid",
|
||||
"enrollment_activated": false
|
||||
}
|
||||
```
|
||||
|
||||
Deliberately minimal — no name, no email, no amount, no provider reference. Enough to render one
|
||||
of three states on your success page:
|
||||
|
||||
- **`payment_status: "paid"` (or `status: "enrollment_activated"`)** — "You're all set — check
|
||||
your email for your RootNode Academy activation link."
|
||||
- **`payment_status: "pending"` or missing, `status` still pre-confirmation** — "Processing your
|
||||
payment — this page will update automatically" (keep polling, or tell the customer to check
|
||||
back / check email shortly).
|
||||
- **`payment_status: "failed"`** — "Your payment didn't go through. You can try again;" — for a
|
||||
priced offering, a failed payment does NOT release the seat, so the SAME registration can
|
||||
retry checkout (back to step 4) without losing its place.
|
||||
|
||||
A cancelled/expired checkout (customer closed the tab, or the provider's own session timeout)
|
||||
shows up as `status: "cancelled"` once RootNode's reservation reaper or the provider's own
|
||||
"expired" webhook processes it — at that point the seat has been released and the customer needs
|
||||
to start a new registration if they still want in.
|
||||
|
||||
`401` (bad/missing key) and `404 NOT_FOUND` (bad registration id) are the only error responses.
|
||||
|
||||
## 7. What you must NOT do
|
||||
|
||||
- Never store, log, or transmit card numbers, CVVs, or any other payment-card data — this
|
||||
contract has no field for it anywhere, on purpose. RootNode never touches card data either;
|
||||
Stripe's hosted Checkout page collects it directly.
|
||||
- Never send an `amount`/`price` in the checkout request — there is no such field, and if you
|
||||
add one to your own request body it is silently ignored (the price always comes from the
|
||||
offering).
|
||||
- Never build your own "payment succeeded" email — RootNode's activation/confirmation email
|
||||
fires automatically on webhook-confirmed conversion (same machinery `docs/
|
||||
WEBSITE_ENROLLMENT_INTEGRATION.md` already documents). A second email from your side would
|
||||
duplicate or conflict with it.
|
||||
- Never poll the status endpoint indefinitely/aggressively — a reasonable few-second interval
|
||||
for a short window (a minute or two) covers real webhook latency; beyond that, tell the
|
||||
customer to check their email and stop polling.
|
||||
|
||||
## Accessibility and loading-state notes
|
||||
|
||||
- The redirect to `checkout_url` in step 4 is a full navigation, not a modal — treat it like any
|
||||
other outbound link (announce the navigation to assistive tech, don't trap focus expecting a
|
||||
return).
|
||||
- The success page in step 6 has three real states to design for (processing/confirmed/failed),
|
||||
not two — see step 6's own state list. Design a genuine "still processing" state; don't leave
|
||||
the customer looking at a page that appears frozen while you poll.
|
||||
- Every checkout/agreement error in the tables above has a distinct, human-meaningful cause —
|
||||
map each to a real, specific message rather than one generic "something went wrong."
|
||||
|
||||
## Test-mode flow (what to actually test against)
|
||||
|
||||
Local/staging integration testing uses `PAYMENT_PROVIDER=test` (RootNode's own deterministic
|
||||
provider, no Stripe account needed) — see `docs/STRIPE_INTEGRATION_RUNBOOK.md` for how to drive a
|
||||
full checkout → webhook → confirmation cycle against it, including simulating a failed payment
|
||||
and an abandoned checkout. Once RootNode has real Stripe credentials, staging can switch to
|
||||
`PAYMENT_PROVIDER=stripe` in Stripe's OWN test mode (fake test cards, e.g. `4242 4242 4242 4242`)
|
||||
for a true end-to-end rehearsal before anything goes live with real money.
|
||||
|
||||
## Privacy note
|
||||
|
||||
The only personal data this contract ever sees is what `docs/WEBSITE_ENROLLMENT_INTEGRATION.md`
|
||||
already documents for registration (names, emails, phone) — nothing payment-specific is added.
|
||||
Whatever billing/contact information Stripe's hosted Checkout collects from the customer (name,
|
||||
email, card) stays with Stripe; RootNode never receives or stores it beyond the transaction
|
||||
metadata `docs/PHASE9_COMMERCE_AND_LAUNCH.md` describes (amount, currency, status, timestamps,
|
||||
provider reference — never card data).
|
||||
@@ -1,24 +1,31 @@
|
||||
# Winter Break Camp Operations
|
||||
|
||||
Phase 8 §29 — how to actually run the Winter Break Linux & Systems Foundations Camp
|
||||
(`docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md` Option A) using the platform built in Phases
|
||||
7-8, end to end, entirely from `/admin`. No payment step exists yet — this document covers
|
||||
everything up to and including the point where a real payment/checkout would plug in.
|
||||
Phase 8 §29 (payment step updated in Phase 9) — how to actually run the Winter Break Linux &
|
||||
Systems Foundations Camp (`docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md` Option A) using the
|
||||
platform built in Phases 7-9, end to end, entirely from `/admin`. As of Phase 9, real
|
||||
checkout/payment (Stripe test mode today, live mode once real credentials exist) is part of this
|
||||
flow — see `docs/PHASE9_COMMERCE_AND_LAUNCH.md` and `docs/WINTER_BREAK_LAUNCH_CHECKLIST.md`.
|
||||
|
||||
## What already exists vs. what Chase still fills in
|
||||
|
||||
**Already representable, real, tested:**
|
||||
- The curriculum package (ROOT 01-02, referencing the real, unmodified courses).
|
||||
- The offering itself, in DRAFT, with delivery mode `camp`.
|
||||
- Registration → guardian/adult → agreement → (simulated) confirmation → enrollment → cohort →
|
||||
activation → classroom → real Lab work, proven end to end in
|
||||
`infra/scripts/drills/test-phase8-winter-camp-registration.js` (12/12 passing).
|
||||
- The offering itself, in DRAFT, with delivery mode `camp`, and a real price.
|
||||
- Registration → guardian/adult → agreement → real checkout → verified webhook → confirmation →
|
||||
enrollment → cohort → activation → classroom → real Lab work, proven end to end in
|
||||
`infra/scripts/drills/test-phase9-winter-camp-commerce.js` (13/13 passing, test payment
|
||||
provider, no live money) — plus failed-payment retry, abandoned-checkout seat release, and a
|
||||
last-seat checkout race, none of which existed in Phase 8's own rehearsal.
|
||||
|
||||
**Still genuinely unknown, deliberately left editable, never invented by this phase:**
|
||||
- Exact dates, venue, real capacity, and price.
|
||||
- Exact dates, venue, real capacity, and price (the price FIELD is real and editable; the actual
|
||||
dollar amount to charge is still Chase's call).
|
||||
- The actual agreement text (a plain-language placeholder only — see `docs/
|
||||
STUDENT_GUARDIAN_DATA_MODEL.md`'s legal-review list).
|
||||
- Whether registration opens before or after a specific marketing push.
|
||||
- Real Stripe credentials — until they exist, the platform runs on the deterministic test
|
||||
provider, functionally complete but not yet validated against Stripe's own live systems. See
|
||||
`docs/STRIPE_INTEGRATION_RUNBOOK.md`.
|
||||
|
||||
## The operational sequence
|
||||
|
||||
@@ -35,40 +42,48 @@ everything up to and including the point where a real payment/checkout would plu
|
||||
4. **Assign the lead instructor** — both at the offering level (`instructor_offerings`, Phase 7)
|
||||
and the cohort level (`cohort_instructors`, unchanged since Phase 2A) — the offering detail
|
||||
page's own instructor step, or the two admin routes directly.
|
||||
5. **Configure the required agreement(s)** — create the agreement definition (title + plain-
|
||||
5. **Set the price** — `PATCH /api/admin/offerings/:id` with `price_amount`/`price_currency`
|
||||
(or leave `price_amount` unset for a free offering). Confirm `PAYMENT_PROVIDER` is configured
|
||||
(`test` for rehearsal, `stripe` with real credentials for a live launch — see `docs/
|
||||
STRIPE_INTEGRATION_RUNBOOK.md`).
|
||||
6. **Configure the required agreement(s)** — create the agreement definition (title + plain-
|
||||
language summary) and link it to the offering. **The actual legal text still needs
|
||||
professional review before this camp is opened to real families** — see below.
|
||||
6. **Check launch readiness** — the offering detail page's readiness panel should show every
|
||||
check green before publishing. If instructor/curriculum/agreements are configured but dates/
|
||||
location/capacity aren't final yet, readiness will honestly show that.
|
||||
7. **Publish, then open registration** — `draft → published → enrollment_open`. Only once
|
||||
7. **Check launch readiness** — the offering detail page's readiness panel should show every
|
||||
check green before publishing, including the new `price` and `payment_provider` checks. If
|
||||
instructor/curriculum/agreements/price are configured but dates/location/capacity aren't
|
||||
final yet, readiness will honestly show that.
|
||||
8. **Publish, then open registration** — `draft → published → enrollment_open`. Only once
|
||||
`enrollment_open` does the offering appear in the public offerings API and accept real
|
||||
registrations.
|
||||
8. **Registrations arrive** — via the website integration (once wired — see
|
||||
`docs/WEBSITE_ENROLLMENT_INTEGRATION.md`'s documented future flow) or entered directly by an
|
||||
admin for a phone/in-person sign-up.
|
||||
9. **Guardian/adult completes the agreement** — recorded via the registration's own agreement
|
||||
acceptance action.
|
||||
10. **Payment step (future, not built)** — this is the one step this phase does not implement.
|
||||
Today, an admin manually transitions a registration to `confirmed` once payment is verified
|
||||
by whatever process exists outside the platform; a future phase's real payment integration
|
||||
would do this automatically on a verified commerce event, the same shape
|
||||
`/api/integrations/enrollments` already uses for direct program enrollment.
|
||||
11. **Convert to enrollment** — creates the real student account (or matches an existing one),
|
||||
the real academic enrollment, and real cohort membership.
|
||||
12. **Account activation** — the student's (or, for a minor, the guardian's) real one-time
|
||||
9. **Registrations arrive** — via the website integration (contract ready, see
|
||||
`docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md`) or entered directly by an admin for a phone/
|
||||
in-person sign-up.
|
||||
10. **Guardian/adult completes the agreement, then checkout** — the agreement acceptance and
|
||||
checkout-creation endpoints are both live (Phase 9); checkout is blocked until the agreement
|
||||
is accepted.
|
||||
11. **Payment confirmation** — a verified, idempotent webhook (never a browser redirect) is what
|
||||
actually confirms payment and walks the registration to `confirmed`. A failed payment leaves
|
||||
the registration retriable without releasing the seat; an abandoned checkout or an expired,
|
||||
unpaid reservation releases it.
|
||||
12. **Convert to enrollment** — happens automatically on payment confirmation for a priced
|
||||
offering (or a manual admin transition for a free one) — creates the real student account
|
||||
(or matches an existing one), the real academic enrollment, and real cohort membership.
|
||||
13. **Account activation** — the student's (or, for a minor, the guardian's) real one-time
|
||||
activation token, delivered the same way every other RootNode activation email already is.
|
||||
13. **Classroom day** — Prepare Labs for the cohort, start the classroom session, students join
|
||||
14. **Classroom day** — Prepare Labs for the cohort, start the classroom session, students join
|
||||
and work through real ROOT 01-02 content and Labs.
|
||||
14. **Offering completion** — `in_progress → completed` once the camp concludes.
|
||||
15. **Offering completion** — `in_progress → completed` once the camp concludes.
|
||||
|
||||
## Known blockers before this can actually open to the public
|
||||
|
||||
- Real dates, venue, capacity, and price are not yet decided (this document does not invent
|
||||
them).
|
||||
- Real dates, venue, capacity, and the actual dollar price are not yet decided (this document
|
||||
does not invent them — the platform fields to hold them are real and ready).
|
||||
- The agreement text requires professional legal review (see `docs/
|
||||
STUDENT_GUARDIAN_DATA_MODEL.md`).
|
||||
- No payment processing exists — step 10 above is manual until a future phase builds it.
|
||||
- The website's own registration UI does not exist yet in this repository (rootnodeacademy.com
|
||||
is built separately) — the integration contract it needs is ready
|
||||
(`docs/WEBSITE_ENROLLMENT_INTEGRATION.md`), but nobody has built the website-side form yet.
|
||||
- Real Stripe credentials do not exist yet — the commerce pipeline is built and tested against
|
||||
the deterministic test provider only; see `docs/STRIPE_INTEGRATION_RUNBOOK.md` for going live.
|
||||
- The website's own registration/checkout UI does not exist yet in this repository
|
||||
(rootnodeacademy.com is built separately) — the integration contract it needs is ready
|
||||
(`docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md`), but nobody has built the website-side form yet.
|
||||
- See `docs/WINTER_BREAK_LAUNCH_CHECKLIST.md` for the full go/no-go list.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Winter Break Camp — Launch Checklist
|
||||
|
||||
The single go/no-go list for opening the Winter Break Linux & Systems Foundations Camp
|
||||
(`docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md` Option A) to real public registration. Combines
|
||||
`docs/WINTER_BREAK_CAMP_OPERATIONS.md`'s operational sequence with Phase 9's commerce
|
||||
requirements into one checklist Chase can walk top to bottom. Nothing on this list is invented by
|
||||
the platform — every unchecked item is a real decision or asset that has to come from Chase, the
|
||||
legal reviewer, or Stripe, not from more code.
|
||||
|
||||
## Platform readiness — done, verified, ready whenever the business inputs below land
|
||||
|
||||
- [x] Offering, capacity, registration lifecycle, agreements — built and tested (Phase 7/8).
|
||||
- [x] Offering pricing field, payment-provider abstraction, hosted checkout, verified webhook,
|
||||
refund recording, abandoned-reservation reaper — built and tested against the deterministic
|
||||
test provider (Phase 9).
|
||||
- [x] Website registration + checkout + agreement + status-lookup contract — documented and
|
||||
tested (`docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md`).
|
||||
- [x] Admin UI for offering pricing and per-registration payment/refund status.
|
||||
- [x] `GET /api/admin/offerings/:id/readiness` reflects real configuration state, including the
|
||||
new `price` and `payment_provider` checks.
|
||||
- [x] Full Winter Break commerce rehearsal green end to end against a real, freshly-built stack
|
||||
(`infra/scripts/drills/test-phase9-winter-camp-commerce.js`, 13/13): admin configuration,
|
||||
minor and adult registration, checkout, payment success, webhook, enrollment activation, a
|
||||
real classroom session, a real Lab task, failed payment, duplicate webhook, abandoned
|
||||
checkout, and a last-seat race.
|
||||
|
||||
## Business decisions — Chase's call, not invented by this phase
|
||||
|
||||
- [ ] Real camp dates and duration.
|
||||
- [ ] Real venue/location (or confirm fully online/remote).
|
||||
- [ ] Real capacity (seat count).
|
||||
- [ ] Real price and currency.
|
||||
- [ ] Whether registration opens ahead of, or alongside, a specific marketing push.
|
||||
- [ ] Refund policy in plain terms (the platform can RECORD any refund decision; it does not
|
||||
decide policy — see `docs/OFFERING_AND_REGISTRATION_MODEL.md`'s "Payment and the commerce
|
||||
boundary" section for what "record, never auto-cascade" means operationally).
|
||||
|
||||
## Legal review — required before any real family sees the agreement
|
||||
|
||||
- [ ] The actual participation/media/liability agreement text, reviewed by a real attorney
|
||||
(`agreement_definitions.body_summary` is currently a plain-language placeholder only — see
|
||||
`docs/STUDENT_GUARDIAN_DATA_MODEL.md`'s legal-review list).
|
||||
- [ ] Jurisdiction-specific minor-consent requirements confirmed.
|
||||
- [ ] Data-retention/deletion policy decided and, if needed, reflected in the agreement text.
|
||||
- [ ] A decision on whether emergency-contact fields need additional encryption at rest.
|
||||
|
||||
## Payments — pending real Stripe credentials
|
||||
|
||||
- [ ] Real Stripe account created/available (live mode).
|
||||
- [ ] `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (live values) provisioned into production
|
||||
configuration only — see `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Going live" section.
|
||||
- [ ] At least one real, small, refundable test transaction run end to end in production before
|
||||
opening registration publicly, then refunded.
|
||||
- [ ] Recommended (not required — the test provider already validates the application logic):
|
||||
one full rehearsal against Stripe's own TEST mode (fake cards, no real money) before
|
||||
flipping to live credentials, per `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s test-mode section.
|
||||
|
||||
## Website (separate repo, not RootNode-owned)
|
||||
|
||||
- [ ] rootnodeacademy.com's own registration/checkout form built against
|
||||
`docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md`. Not tracked or built in this repository — if
|
||||
that repo becomes available to RootNode's own engineering effort, say so explicitly before
|
||||
any code changes are made against it.
|
||||
- [ ] The website's `INTEGRATION_SHARED_SECRET` provisioned into its OWN backend configuration
|
||||
(never browser-reachable) — see `docs/WEBSITE_ENROLLMENT_INTEGRATION.md`'s "Production
|
||||
secret handling."
|
||||
- [ ] The website's success page built against the status-lookup polling pattern in
|
||||
`docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` §6 — never derived from the redirect URL alone.
|
||||
|
||||
## Final go/no-go
|
||||
|
||||
Registration should not open to the public until every box above is checked. The platform side
|
||||
(first section) has been true since this phase shipped; everything else is a business, legal, or
|
||||
external-team dependency this phase deliberately did not — and should not — invent on Chase's
|
||||
behalf.
|
||||
Reference in New Issue
Block a user