# 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_id` on `notification_events`), a `'registrant'` agreement-acceptance type, `offering_agreement_requirements`, a real unique index on `external_reference` (website registration idempotency), and the notification_type CHECK widening (see "bugs found" below). - **`offerings.js` extended**: `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), and `createRegistrationFromIntegrationEvent()` (the website's idempotent submission path, mirroring `enrollment.js`'s established pattern exactly). - **`notifications.js` extended**: a guardian-email fallback in `resolveRecipient()`, and a parallel `enqueueForRegistration()`/`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 existing `integration-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 existing `Admin.module.css`/`adminApi` conventions exactly — no new UI framework or pattern introduced. ## Two real bugs found and fixed during this phase's own testing (before any commit) 1. **`notification_type` CHECK constraint mismatch** — the five new registration-stage notification types were wired into `notifications.js`/`notification-worker.js` before the database's own CHECK constraint was widened to accept them. Every `enqueueForRegistration()` 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 in `036_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. 2. **A registration-insert/seat-reservation/notification ordering bug** — adding a `registration_received` notification 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 via `e2e-phase7.js`'s own regression rerun. Fixed by reordering `createRegistration()` to validate → reserve the seat → insert → notify, strictly in that order, and simplifying the admin route accordingly (see `docs/OFFERING_AND_REGISTRATION_ MODEL.md` for 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_action` has 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 `waitlisted` registration status was considered and deliberately deferred; the existing `cancelled`-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 1. Real dates, venue, capacity, and price are still undecided (not invented by this phase). 2. The agreement's actual legal text requires professional review. 3. No payment processing exists yet. 4. 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.