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]>
190 lines
12 KiB
Markdown
190 lines
12 KiB
Markdown
# 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.
|