From 788fb197e0a270423c1ff55829dc1d5dc380591b Mon Sep 17 00:00:00 2001 From: ced4568 Date: Wed, 26 Aug 2026 23:59:42 -0500 Subject: [PATCH] docs(phase8): document enrollment operations 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 --- docs/OFFERING_ADMIN_GUIDE.md | 69 +++++++ docs/OFFERING_AND_REGISTRATION_MODEL.md | 41 +++- docs/PHASE8_ENROLLMENT_OPERATIONS.md | 189 ++++++++++++++++++ ...EGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md | 62 ++++++ ...ADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md | 7 +- docs/STUDENT_GUARDIAN_DATA_MODEL.md | 6 +- docs/WEBSITE_ENROLLMENT_INTEGRATION.md | 149 ++++++++++++-- docs/WINTER_BREAK_CAMP_OPERATIONS.md | 74 +++++++ platform/README.md | 11 +- 9 files changed, 573 insertions(+), 35 deletions(-) create mode 100644 docs/OFFERING_ADMIN_GUIDE.md create mode 100644 docs/PHASE8_ENROLLMENT_OPERATIONS.md create mode 100644 docs/REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md create mode 100644 docs/WINTER_BREAK_CAMP_OPERATIONS.md diff --git a/docs/OFFERING_ADMIN_GUIDE.md b/docs/OFFERING_ADMIN_GUIDE.md new file mode 100644 index 0000000..110a86d --- /dev/null +++ b/docs/OFFERING_ADMIN_GUIDE.md @@ -0,0 +1,69 @@ +# Offering Admin Guide + +Phase 8 — how to actually run an offering from `/admin` → Offerings, end to end. Pairs with +`docs/OFFERING_AND_REGISTRATION_MODEL.md` (the underlying data model) and `docs/ +WINTER_BREAK_CAMP_OPERATIONS.md` (the specific Winter Break Camp walkthrough). + +## Creating an offering + +**Offerings → Create offering.** Required: offering ID, URL slug, name, program, delivery mode. +Optional at creation: capacity (blank = unlimited), a location. Dates, the registration window, +organization, cohort, curriculum package, and agreement requirements are all set from the +offering's own detail page after creation — a new offering always starts `draft`, invisible to +the public offerings API and closed to registration, so there's no rush to fill in everything +before saving. + +## The detail page + +Opening an offering shows: + +- **Overview** — status, seats, and the only status-transition buttons the offering can + actually take next (the ones the UI doesn't show are ones the server would reject anyway — + see `docs/OFFERING_AND_REGISTRATION_MODEL.md`'s transition diagram). +- **Launch readiness** — a live, server-computed checklist (curriculum, instructor, dates, + location, capacity, registration window, agreements configured). This is the same check the + server itself could use to gate opening registration — it is never just a frontend hint. +- **Required agreements** — which agreement definitions this offering requires before a + registration can convert to enrollment, with a form to add another. +- **Registrations / roster** — every registration against this offering, with a form to add one + directly (useful for phone/in-person sign-ups an instructor is entering by hand). + +## Moving an offering through its lifecycle + +`draft → published → enrollment_open → full/in_progress → completed`, or `→ cancelled` from +anywhere non-terminal. Only real, server-allowed transitions ever appear as buttons. `full` is +not automatic — an offering set to `enrollment_open` that happens to be at capacity will reject +new registrations with a clear error, but only moves to the visible `full` status when someone +explicitly sets it (or the very next registration attempt trips it) — check the readiness +panel/registration list to know why registration is currently unavailable. + +## Working a registration + +Open a registration from the offering's roster to see its own detail: registrant/guardian info, +agreement status (required vs. accepted vs. missing), lifecycle transition buttons, and its +communication history (every notification queued for it, with recipient always masked). Once a +registration is `confirmed` and every required agreement is accepted, **Convert to enrollment** +appears — this is the one action that creates the real student account (or matches an existing +one), the real academic enrollment, and real cohort membership if the offering has a cohort. + +## Agreements + +**Agreements** (reachable from an offering's "Required agreements" section) lets you define a +new agreement (title + a plain-language summary — never legal text, see `docs/ +STUDENT_GUARDIAN_DATA_MODEL.md`) and associate it with any offering that needs it. A registration +cannot convert to enrollment while any required agreement is unaccepted. + +## Guardians + +A minor's registration always carries a linked guardian — create one from the registration form +when registering a minor by hand, or let the website integration create/match one automatically +from a real submission (see `docs/WEBSITE_ENROLLMENT_INTEGRATION.md`). Guardian records are +admin/owner-only — a plain instructor's cohort access to a student's academic progress is +unchanged, but guardian contact detail is not part of that. + +## What the admin UI deliberately does not do + +No pricing/payment fields, no legal-document authoring, no bulk email composer — every +communication is a templated, transactional notification triggered by a real lifecycle event +(see `docs/REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md`), never a free-form message an admin +types and blasts out. diff --git a/docs/OFFERING_AND_REGISTRATION_MODEL.md b/docs/OFFERING_AND_REGISTRATION_MODEL.md index 3e11de8..bc03973 100644 --- a/docs/OFFERING_AND_REGISTRATION_MODEL.md +++ b/docs/OFFERING_AND_REGISTRATION_MODEL.md @@ -1,9 +1,18 @@ # Offering & Registration Model -Phase 7 Steps 3/6/12-14/18-20 — the schema and lifecycle for offerings, capacity, registrations, -agreements, and curriculum packages. See `platform/api/migrations/034_phase7_offerings_ -registrations_agreements.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) — 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. + +**Updated for Phase 8**: registration is now server-authoritative (draft/cancelled/completed/ +full/window-closed offerings reject registration attempts with distinct error codes — see +`assertRegistrationOpen()`); offerings can require specific agreements before a registration may +convert to enrollment (`offering_agreement_requirements`); `accepted_by_type` gained a +`'registrant'` value for an adult self-accepting before their student_id exists; the website +registration integration (`POST /api/integrations/registrations`) is live — see `docs/ +WEBSITE_ENROLLMENT_INTEGRATION.md`; and launch readiness is now server-computed +(`computeOfferingReadiness()`), exposed at `GET /api/admin/offerings/:id/readiness`. ## Offering lifecycle (server-defined transitions only) @@ -37,13 +46,23 @@ If zero rows come back, the offering is genuinely full — `409 OFFERING_FULL`. row-level lock Postgres takes for the UPDATE's own duration makes two concurrent registration attempts for the last seat serialize correctly, with exactly one succeeding, never both. -**A real ordering bug found and fixed during this phase's own testing**: the registration route -originally reserved the seat *before* validating the registration's own fields (e.g. a minor -registration missing a `guardianId`). Under a full offering, that let a plain validation error -get masked behind a false `OFFERING_FULL`, and even when the check passed, a request that would -later fail validation had already consumed a real seat. Fixed by creating (and validating) the -registration row first, then reserving the seat, rolling back (deleting) the registration row if -the seat reservation itself then fails — see `admin-offerings-routes.js`'s own comment. +**Two real ordering bugs found and fixed across Phase 7 and Phase 8, in opposite directions — +the current, final order is: validate fields → reserve the seat → insert the row → notify.** + +Phase 7's own testing found the route originally reserved the seat *before* validating the +registration's fields (e.g. a minor missing `guardianId`), letting a plain validation error get +masked behind a false `OFFERING_FULL` under a full offering. Fixed by validating first. + +Phase 8 then added a `registration_received` notification fired right after the registration row +was inserted — which briefly made the ORIGINAL Phase 7 fix's remaining shape (insert, then +reserve, then roll back the row by deleting it if the seat turned out to be unavailable) newly +unsafe: the notification row, created after insert but before the seat check, held a real +foreign key to the registration — so when reservation failed and the code tried to delete the +just-inserted registration row, that DELETE itself failed on the very foreign key the +notification had just created. Found via this phase's own regression testing, fixed by moving +seat reservation to happen BEFORE the insert (rolling the seat back, not a row, if the insert +itself then somehow fails) and only ever queuing the notification once the registration is fully, +durably committed — see `offerings.js`'s `createRegistration()` and its own header comment. ## Registration lifecycle diff --git a/docs/PHASE8_ENROLLMENT_OPERATIONS.md b/docs/PHASE8_ENROLLMENT_OPERATIONS.md new file mode 100644 index 0000000..295e6ea --- /dev/null +++ b/docs/PHASE8_ENROLLMENT_OPERATIONS.md @@ -0,0 +1,189 @@ +# 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. diff --git a/docs/REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md b/docs/REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md new file mode 100644 index 0000000..29aa4f2 --- /dev/null +++ b/docs/REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md @@ -0,0 +1,62 @@ +# Registration & Guardian Communications + +Phase 8 §8-§11 — how registration-stage notifications extend the existing Phase 5C notification +system. No second email subsystem was built: same `notification_events` table, same durable +worker (`notification-worker.js`), same Google Workspace-backed provider adapter +(`email/provider.js`), same masked-recipient admin API. + +## What's new vs. Phase 5C + +`notification_events.student_id` is now nullable, and a `registration_id` column was added (a +CHECK constraint guarantees every row still has a real subject) — because a registration-stage +notification (Registration Received, Agreement Required) may need to go out before any student +account exists at all, for a brand-new guardian-submitted registration. `notification_type`'s +CHECK constraint was widened to include five new types. See `033_phase8_...sql` and +`036_phase8_notification_types.sql`. + +**A real bug found and fixed during this phase's own testing**: the new notification types were +added to `notifications.js`/`notification-worker.js` before the database CHECK constraint was +widened to accept them — every registration-stage `enqueue` call was silently failing (caught by +the deliberate "a notification failure never blocks the real action" try/catch) until this was +caught and fixed. See `docs/PHASE8_ENROLLMENT_OPERATIONS.md`. + +## The five new templates + +1. **Registration Received** — queued the moment a registration is created (website or admin). +2. **Registration Confirmed** — queued when a registration moves to `confirmed`. +3. **Registration Needs Action** — available for an admin/future-flow to queue with a reason + when a registration needs something before it can proceed (not automatically triggered by + any single lifecycle transition this phase — a deliberate choice, since "needs action" covers + many different real reasons an admin should state explicitly). +4. **Agreement Required** — queued once per missing required agreement when a registration + enters `agreements_pending`. +5. **Camp/Offering Operational Update** — a deliberately generic operational notice (e.g. "camp + starts Monday, here's what to bring"), NOT a marketing template — no promotional content, no + discount codes, no unrelated announcements. Available for an admin to trigger manually; + nothing queues it automatically. + +All five are HTML + plain-text, RootNode-branded, no tracking pixels, no external images, no +passwords or sensitive data — same `templates.js` conventions Phase 5C established. + +## Recipient resolution — extended, never overwritten + +`resolveRecipient()`'s existing order (student's own email → the enrollment's +`metadata.contact_email`) now has a third, final fallback: a real, communications-enabled +primary guardian's email (`student_guardians`/`guardians`). This is purely additive — a student +who already resolves correctly today is completely unaffected, since the new fallback is only +ever reached when the first two return nothing. `enrollmentContactEmail` (Phase 5C, unchanged) +and this new guardian fallback deliberately coexist: `contact_email` remains a per-enrollment +override for the live website integration's existing contract, while the guardian relationship +is the durable, cross-enrollment identity — prefer the guardian model going forward, but the +transitional field is never broken. + +For registration-stage notifications (no student yet, possibly), a separate +`resolveRegistrationRecipient()` tries: the registration's own student (if already converted) → +its linked guardian → the registrant's own supplied email. + +## What was deliberately NOT built + +No bulk/marketing email composer, no free-form admin-authored message sent to an arbitrary +recipient — every notification is one of the fixed templates above, triggered by a real +lifecycle event or an explicit, scoped admin action (never a raw "compose email" form). No SMS. +No third notification channel beyond email. diff --git a/docs/ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md b/docs/ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md index 671c9aa..66ac58d 100644 --- a/docs/ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md +++ b/docs/ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md @@ -137,7 +137,10 @@ planned this phase. | Registrations, registration lifecycle | PHASE 7 BUILT | | Agreement definitions/acceptances (record-only, no legal text) | PHASE 7 BUILT | | Admin routes for all of the above | PHASE 7 BUILT | -| Admin UI (Offerings/Registrations screens) | **NOT BUILT this phase** — see known limitations | -| Public registration flow, payments, subscriptions | FUTURE | +| Admin UI (Offerings/Registrations screens) | **PHASE 8 BUILT** — `/admin` → Offerings, with launch readiness, roster, agreements, and per-registration detail (communications, agreement status). | +| 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 | | INIT_/COMPILE_ curriculum | FUTURE | | School/library SIS integrations, parent portal | FUTURE, explicitly out of scope | diff --git a/docs/STUDENT_GUARDIAN_DATA_MODEL.md b/docs/STUDENT_GUARDIAN_DATA_MODEL.md index f7e1373..e58db60 100644 --- a/docs/STUDENT_GUARDIAN_DATA_MODEL.md +++ b/docs/STUDENT_GUARDIAN_DATA_MODEL.md @@ -23,8 +23,10 @@ guardian is a real relationship, not a workaround field. detailed learning analytics by default. A guardian relationship is a contact/authorization record, not a monitoring grant. - **No automatic broad access.** `student_guardians.can_receive_communications` and - `is_primary_contact` govern notification targeting only (a future concern — this phase writes - the rows but does not wire them into `notifications.js`'s resolveRecipient yet), never + `is_primary_contact` govern notification targeting only — wired into `notifications.js`'s + `resolveRecipient()` as of Phase 8 (a communications-enabled primary guardian is the LAST + fallback, tried only after the student's own email and any enrollment-level `contact_email` — + see `docs/REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md`), never authorization to student data. ## Data minimization diff --git a/docs/WEBSITE_ENROLLMENT_INTEGRATION.md b/docs/WEBSITE_ENROLLMENT_INTEGRATION.md index fe19654..ac923d6 100644 --- a/docs/WEBSITE_ENROLLMENT_INTEGRATION.md +++ b/docs/WEBSITE_ENROLLMENT_INTEGRATION.md @@ -179,29 +179,142 @@ a first-class concept in this endpoint, talk to the RootNode team before buildin workaround; the schema now has the real tables for it, they're just not wired into this endpoint yet. -## Future flow: offerings and registrations (Phase 7, not yet live on this endpoint) +## Offerings and registrations (Phase 8 — live) -RootNode Academy is now (rootnodeacademy.com, live separately from this repository) building -toward representing specific purchasable deliveries — camps, cohorts, self-paced access — as -first-class **offerings**, with a **registration** step before final enrollment (see -`docs/OFFERING_AND_REGISTRATION_MODEL.md`). The eventual intended flow: +As of Phase 8, RootNode represents specific purchasable deliveries — camps, cohorts, self-paced +access — as first-class **offerings**, with a **registration** step before final academic +enrollment (see `docs/OFFERING_AND_REGISTRATION_MODEL.md`). This section covers the real, live +contract; the endpoint documented above (`/api/integrations/enrollments`, direct program +enrollment) is completely unaffected and still works exactly as described. + +### 1. List/view offerings (public, no auth) ``` -Website: learner selects an offering (e.g. "Winter Break Linux & Systems Camp") - -> Website collects registration info (student/guardian, readiness, agreements) - -> Website checkout/payment (future — not built yet) - -> Verified commerce event -> RootNode confirms the registration - -> RootNode activates the enrollment (reusing this exact endpoint's machinery) - -> Student receives their existing activation email, unchanged - -> Student/camp access begins +GET /api/public/offerings +GET /api/public/offerings/:slug ``` -**This is documentation of an intended future shape, not a live contract change.** The endpoint -below is unaffected — it continues to work exactly as documented, program-slug-based, with no -concept of an offering. A future phase will either extend this endpoint with an optional -`offering_slug` field or add a parallel endpoint once the website's actual checkout flow is -designed; that decision has not been made. Idempotency (`event_id`) will remain the mechanism -either way. +No `X-RootNode-Integration-Key` needed — this is a public, read-only endpoint, safe to call +directly from a browser. Only `published`/`enrollment_open`/`full`/`in_progress` offerings ever +appear; `draft` and `cancelled` never do. Example response: + +```json +{ + "slug": "winter-linux-systems-camp-2026", + "name": "Winter Break Linux & Systems Foundations Camp", + "description": "A hands-on introduction to Linux administration and networking.", + "program_id": "root-kubernetes-foundations", + "delivery_mode": "camp", + "recommended_min_age": 13, + "recommended_max_age": null, + "starts_at": "2026-12-15T14:00:00.000Z", + "ends_at": "2026-12-19T18:00:00.000Z", + "registration_open": true, + "availability": "limited", + "location_name": "Community Room B", + "location_type": "community_center" +} +``` + +`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. + +### 2. Select an offering, then submit a registration (server-to-server) + +``` +POST /api/integrations/registrations +Header: X-RootNode-Integration-Key: +``` + +Same trust boundary as the existing enrollment endpoint (Step 15) — server-to-server only, never +called from browser JS, the secret never leaves your backend's own configuration. + +**Adult example request:** + +```json +{ + "event_id": "reg_9f8e7d6c5b4a", + "source": "website", + "offering_slug": "winter-linux-systems-camp-2026", + "registrant": { "display_name": "Jamie Rivera", "email": "jamie@example.com", "is_minor": false } +} +``` + +**Minor/guardian example request:** + +```json +{ + "event_id": "reg_1a2b3c4d5e6f", + "source": "website", + "offering_slug": "winter-linux-systems-camp-2026", + "registrant": { "display_name": "Riley Rivera", "is_minor": true }, + "guardian": { "display_name": "Jamie Rivera", "email": "jamie@example.com", "phone": "555-0100" } +} +``` + +**Success response** (`201` new, `200` idempotent replay): + +```json +{ "registration_id": "6dd6d898-f895-477f-97d8-1e5ce89c27cd", "offering_id": "winter-linux-systems-camp-2026", "status": "started", "created": true } +``` + +**Errors**: `401 unauthorized` (bad/missing key), `400` (`offering_slug`/`event_id`/registrant +fields missing, or a minor with no guardian email), `404 OFFERING_NOT_FOUND`, `409` for +`OFFERING_NOT_OPEN` / `OFFERING_CANCELLED` / `OFFERING_COMPLETED` / `OFFERING_FULL` / +`REGISTRATION_NOT_YET_OPEN` / `REGISTRATION_CLOSED`. + +### 3. Duplicate/retry semantics (idempotent, DB-enforced) + +Identical to `/api/integrations/enrollments`: send the exact same `event_id` again (a retried +webhook, a timed-out request retried by your own backend) and you get back the **same +registration**, `created: false`, never a duplicate, never a second seat reserved — enforced by +a real unique index on `event_id`, safe under concurrent/parallel retries too. Never generate a +new `event_id` for a retry of the same real-world submission. + +### 4. Adult flow + +Send `registrant.email`, omit `guardian` entirely, `is_minor: false` (or omit it). The adult IS +both the eventual student and the registration contact — no guardian record is created. + +### 5. Minor/guardian flow + +Send `registrant.is_minor: true` and a real `guardian` object (`display_name` + `email` +required; `phone` optional). RootNode matches an existing guardian by email first (a returning +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 + +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. + +### 7. Future payment step + +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. + +### 8. Confirmation + +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. + +### 9. Enrollment activation + +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. + +### 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 +reuse these values against a real deployment. ## What the student experiences after you call this (as of Phase 5C) diff --git a/docs/WINTER_BREAK_CAMP_OPERATIONS.md b/docs/WINTER_BREAK_CAMP_OPERATIONS.md new file mode 100644 index 0000000..7ef0de3 --- /dev/null +++ b/docs/WINTER_BREAK_CAMP_OPERATIONS.md @@ -0,0 +1,74 @@ +# 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. + +## 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). + +**Still genuinely unknown, deliberately left editable, never invented by this phase:** +- Exact dates, venue, real capacity, and price. +- 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. + +## The operational sequence + +1. **Create the location** — `/admin` → Offerings → (via the offering create form's location + dropdown) or directly if a location admin surface is added later; today, an admin/owner uses + `POST /api/admin/locations` (documented in `docs/OFFERING_ADMIN_GUIDE.md`) once the real venue + is known. +2. **Create the curriculum package** — `winter-linux-systems-v1`, referencing + `root-01-computing-linux-foundations` and `root-02-linux-administration-networking` in + sequence. This step is a one-time setup — the same package can back every future Winter Break + Camp offering. +3. **Create the cohort** and **the offering** — delivery mode `camp`, referencing the program, + the curriculum package, and (once known) the location and capacity. +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- + 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 + `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 + 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 + and work through real ROOT 01-02 content and Labs. +14. **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). +- 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. diff --git a/platform/README.md b/platform/README.md index 6992021..03cfc86 100644 --- a/platform/README.md +++ b/platform/README.md @@ -112,10 +112,17 @@ further instructor accounts from the Instructors tab). conversion to a real enrollment, registration/guardian RBAC, explicit age-guidance-is-never- an-authorization-check verification, and confirmation that existing ROOT_ V1/self-paced/ classroom flows are unaffected. +- `test/e2e-phase8.js` — offering launch readiness, server-authoritative registration open/ + closed enforcement, capacity concurrency through the real registration endpoints, the public + offering serializer (no internal/PII leakage), the website registration integration contract + (sequential and concurrent idempotency), required-agreement conversion blocking with version + snapshotting, guardian notification recipient resolution, and confirmation that ROOT_ V1/self- + paced/classroom flows remain unaffected. `infra/scripts/drills/` also holds real-Lab dress rehearsals per phase (Phase 6D/6E foundations, -Phase 6F's multi-student pilot and full end-to-end V1 journey) — see each script's own header -and `docs/PHASE6F_ROOT_V1_PILOT_READINESS.md` for what they prove beyond what the e2e suites +Phase 6F's multi-student pilot and full end-to-end V1 journey, Phase 7/8's Winter Break Camp +rehearsals) — see each script's own header and `docs/PHASE6F_ROOT_V1_PILOT_READINESS.md`/`docs/ +PHASE8_ENROLLMENT_OPERATIONS.md` for what they prove beyond what the e2e suites above already cover via direct API. All e2e suites need `OWNER_EMAIL` / `OWNER_PASSWORD` set to a real bootstrapped instructor