New: PHASE8_ENROLLMENT_OPERATIONS.md (phase summary), WINTER_BREAK_CAMP_OPERATIONS.md (the real operational walkthrough), REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md, OFFERING_ADMIN_GUIDE.md. Updated: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (BUILT NOW/PHASE 8 BUILT table), OFFERING_AND_REGISTRATION_MODEL.md (both ordering bugs, launch readiness, registration enforcement), STUDENT_GUARDIAN_DATA_MODEL.md (guardian communications now wired), WEBSITE_ENROLLMENT_INTEGRATION.md (the real, live registration contract with worked examples, replacing the old speculative future-flow section), platform/README.md (test list). Co-Authored-By: Claude Sonnet 5 <[email protected]>
12 KiB
Phase 8 — Enrollment Operations, Guardian Communications & Website Integration
Phase 8's mission: turn Phase 7's business/product backend into an OPERABLE enrollment system — Offering → Registration → Guardian/Adult Contact → Agreements → Communications → Review → Confirmation → Enrollment → Cohort → Account Activation → Classroom/Lab Access — without direct database manipulation, ahead of RootNode's first commercial launch (Winter Break Camp). Not a payment phase — everything is built around where a future payment step will plug in.
Baseline
Confirmed at start: branch main, clean tree, HEAD = origin/main = Phase 7's
df5e39f767a6af3ba50daabde5de97fa1b1a5f3b.
What was built
- Migrations 035/036 — registration-stage notifications (nullable
student_id+registration_idonnotification_events), a'registrant'agreement-acceptance type,offering_agreement_requirements, a real unique index onexternal_reference(website registration idempotency), and the notification_type CHECK widening (see "bugs found" below). offerings.jsextended:assertRegistrationOpen()(server-authoritative registration windows),addAgreementRequirement/getRegistrationAgreementStatus(required vs. accepted vs. missing), a conversion-time block on missing required agreements,computeOfferingReadiness()(server-derived launch checklist), andcreateRegistrationFromIntegrationEvent()(the website's idempotent submission path, mirroringenrollment.js's established pattern exactly).notifications.jsextended: a guardian-email fallback inresolveRecipient(), and a parallelenqueueForRegistration()/resolveRegistrationRecipient()pair for the pre-account registration stage — same table, same worker, same masked admin view, no second system.- Five new templates (
registrationReceived,registrationConfirmed,registrationNeedsAction,agreementRequired,campOperationalUpdate) — HTML + text, RootNode-branded, no tracking, no marketing content. public-offerings-routes.js(new) — the unauthenticated, explicitly-allowlisted offering read API the website will consume.POST /api/integrations/registrations(new, in the existingintegration-routes.js) — the website's registration submission contract, same shared-secret boundary as/api/integrations/enrollments, fully idempotent.- Admin UI: a real
/admin→ Offerings section (OfferingsView.jsx) — list, create, detail (overview with valid-transitions-only buttons, launch readiness, required agreements, registrations/roster) and registration detail (lifecycle, agreement status, communication history, convert-to-enrollment). Reuses the existingAdmin.module.css/adminApiconventions exactly — no new UI framework or pattern introduced.
Two real bugs found and fixed during this phase's own testing (before any commit)
notification_typeCHECK constraint mismatch — the five new registration-stage notification types were wired intonotifications.js/notification-worker.jsbefore the database's own CHECK constraint was widened to accept them. EveryenqueueForRegistration()call was silently failing (caught by the deliberate "a notification failure never blocks the real action" try/catch) until a comprehensive smoke test surfaced it directly. Fixed in036_phase8_notification_types.sql. Also hardened: every previously-silent.catch(() => {})around a notification call now at least logs the error, so a future regression like this surfaces immediately instead of silently vanishing.- A registration-insert/seat-reservation/notification ordering bug — adding a
registration_receivednotification right after the registration row's insert (but before the seat reservation that could still fail and roll the row back) meant a failed reservation's cleanup DELETE could hit a foreign-key violation against the notification row it had just created. Found viae2e-phase7.js's own regression rerun. Fixed by reorderingcreateRegistration()to validate → reserve the seat → insert → notify, strictly in that order, and simplifying the admin route accordingly (seedocs/OFFERING_AND_REGISTRATION_ MODEL.mdfor the full before/after).
Cascading regression fixes (stale test assumptions, not new bugs)
Enforcing real registration-window checks (Step 21) broke e2e-phase7.js's own offeringB
(created in Phase 7, never transitioned out of draft, registered against successfully only
because no such enforcement existed yet) and a new age-guidance offering created the same way.
Fixed by publishing/opening both before registering, matching the new, correct requirement. A
third e2e-phase7.js test's own file-allowlist check needed widening to permit the new, entirely
legitimate public-offerings-routes.js reference to age-guidance fields (display-only, never
authorization).
Testing
platform/test/e2e-phase8.js — 16/16 passing (fresh stack): offering CRUD/transitions/
readiness, registration open/closed enforcement, capacity + concurrent final-seat race through
the real registration endpoints, the public serializer's safety (no internal/PII fields), website
integration idempotency (sequential replay + concurrent duplicate), adult/minor registration
paths, student-identity preservation on conversion, required-agreement conversion blocking with
version snapshotting even after the live definition's version later changes, offering→cohort
membership, a real activation token + queued notification on conversion, guardian recipient
resolution, RBAC (offering/location visible to any instructor, guardian/registration detail
admin-only), organization/location access_notes stripping, and confirmation ROOT_ V1 self-paced
and classroom flows are unaffected.
Winter Break commercial-path rehearsal
infra/scripts/drills/test-phase8-winter-camp-registration.js — 12/12 passing against a real,
freshly-built stack, with no payment code anywhere: admin configures a real offering (location,
capacity, instructor, required agreement), the website integration submits a REAL minor
registration (a real guardian created/matched, a confirmation notification queued), guardian
acceptance is recorded through the supported application flow, a future payment confirmation is
SIMULATED via a plain admin transition, the registration converts to a real enrollment and real
cohort membership, the account activates with a real one-time token, an adult registers and
converts independently, two students genuinely compete for the final seat (both correctly
rejected once capacity was already exhausted — no overbooking), the instructor runs a real
classroom session and Prepares Labs, and the minor student completes real ROOT 01 orientation
plus a REAL Lab task (linux-inspect-hardware) inside a real disposable Lab container, with the
instructor then seeing the real progress.
Regression
Fresh-stack reruns after every fix: e2e-phase7.js 17/17, e2e-phase6f.js 17/17, e2e-phase5b.js
32/32 (enrollment.js's resolveOrCreateStudent gained an optional, default-off returnToken
param in Phase 7, re-verified unaffected here), e2e-phase5c.js 29/29 (notifications.js was
directly extended this phase — every existing Phase 5C notification type/flow reconfirmed
byte-for-byte unaffected). e2e-phase5a.js not independently rerun: its own checks are embedded
in and reconfirmed by both 5B's and 5C's "Phase 5A ... still works unmodified" assertions, and no
code Phase 5A itself owns was touched.
Frontend build
npm run build clean — the new OfferingsView.jsx compiles 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
Both rehearsals (Phase 7's from the prior phase, re-verified here, and this phase's own) drive a
real disposable Lab container through a genuine linux-inspect-hardware scenario setup/validate
cycle — confirmed passing for real, not simulated, in the Winter Break commercial-path rehearsal.
Security/privacy findings
The public offerings API was built with an explicit field allowlist from the start (never
SELECT *) — e2e-phase8.js asserts directly that access_notes, organization_id,
location_id, created_by, reserved_seats, capacity, and metadata never appear in a
public response, and that a fully-booked offering reports "full" rather than a raw seat count
(avoiding a scraping/timing vector against a public, unauthenticated endpoint). No new security
weakening was introduced; the two bugs found above were both silent-failure/ordering bugs, not
authorization gaps.
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. agreement_definitions.body_summary
remains a plain-language placeholder, never legal text.
Documentation
New: PHASE8_ENROLLMENT_OPERATIONS.md (this document), WINTER_BREAK_CAMP_OPERATIONS.md,
REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md, OFFERING_ADMIN_GUIDE.md. Updated:
ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (BUILT NOW/PHASE 8 BUILT/FUTURE table),
OFFERING_AND_REGISTRATION_MODEL.md (the two ordering bugs, launch readiness, registration
enforcement), STUDENT_GUARDIAN_DATA_MODEL.md (guardian communications now wired),
WEBSITE_ENROLLMENT_INTEGRATION.md (the real, live registration contract with worked examples),
platform/README.md (test list). Reviewed, no change needed: docs/ENGINEERING_SOP.md,
docs/LAB_ARCHITECTURE.md, docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md,
docs/RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md (the existing provider adapter needed no changes — the
new templates render through the exact same delivery path).
Known limitations
- The website's own registration UI does not exist yet in this repository (rootnodeacademy.com is built separately) — the contract is ready and tested, nobody has built the website-side form against it yet.
- Agreement acceptance is not exposed on the website registration endpoint itself — currently a separate admin-recorded (or future self-service) step after submission.
registration_needs_actionhas no automatic trigger — an admin action, deliberately, since "needs action" covers many distinct real reasons an admin should state explicitly rather than the platform guessing one.- No waitlist was built — a
waitlistedregistration status was considered and deliberately deferred; the existingcancelled-releases-a-seat mechanism plus manual re-registration covers the real, near-term operational need without adding a new status machine branch this phase didn't have concrete evidence would be used. - No payment processing — every "payment confirmed" step in both rehearsals is a plain admin transition, exactly as authorized.
Remaining blockers before opening Winter Break registration to the public
- Real dates, venue, capacity, and price are still undecided (not invented by this phase).
- The agreement's actual legal text requires professional review.
- No payment processing exists yet.
- The website's own registration form doesn't exist yet.
None of these are platform readiness gaps — the platform is ready to operate the flow the moment those four external decisions/assets exist.
Commits
See git log for this phase's exact commit sequence (migrations/backend, admin UI, tests +
rehearsal, docs) — grouped per docs/ENGINEERING_SOP.md's existing convention.
Recommended Phase 9
Real payment integration (Stripe or equivalent) wired into the registration-confirmation step; the actual website-side registration form; agreement-acceptance exposed on the public registration contract once the legal text is finalized; and, informed by real Winter Break Camp operational experience, whether a waitlist is actually worth building.