docs(phase7): document the business/product architecture foundation
New: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (canonical business/platform bridge — course families, delivery modes, BUILT NOW/PHASE 7 BUILT/FUTURE), OFFERING_AND_ REGISTRATION_MODEL.md (lifecycle diagrams, capacity mechanics), STUDENT_GUARDIAN_DATA_ MODEL.md (data minimization, RBAC, legal-review items), SCHOOL_AND_COMMUNITY_DELIVERY_ ARCHITECTURE.md (organization/location mapping, explicitly not multi-tenancy), PHASE7_PRODUCT_ARCHITECTURE.md (phase summary). Updated: WEBSITE_ENROLLMENT_INTEGRATION.md (documented future offering/registration flow, current live contract unchanged), platform/README.md (test list). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# 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.
|
||||
|
||||
## Offering lifecycle (server-defined transitions only)
|
||||
|
||||
```
|
||||
draft ──► published ──► enrollment_open ──┬──► full ──► enrollment_open (reopen)
|
||||
│ │ │ │
|
||||
│ │ ├──► in_progress ──► completed
|
||||
│ │ │
|
||||
└──► cancelled ◄──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
No route accepts an arbitrary client-supplied status — `offerings.transitionOffering()` checks
|
||||
a fixed transition table and rejects anything not explicitly allowed with `409
|
||||
INVALID_TRANSITION`. `full` is a distinct status from `enrollment_open` (not a computed flag) so
|
||||
an admin can see at a glance why enrollment stopped, and can explicitly reopen it (e.g. a seat
|
||||
freed up) rather than have it silently reappear.
|
||||
|
||||
## Capacity — race-safe by construction
|
||||
|
||||
`offerings.capacity` is nullable (NULL = unlimited, the normal case for `self_paced`).
|
||||
`offerings.reserved_seats` is a maintained counter, incremented by exactly one SQL statement:
|
||||
|
||||
```sql
|
||||
UPDATE offerings SET reserved_seats = reserved_seats + 1
|
||||
WHERE offering_id = $1 AND reserved_seats < capacity
|
||||
RETURNING *;
|
||||
```
|
||||
|
||||
If zero rows come back, the offering is genuinely full — `409 OFFERING_FULL`. This is not a
|
||||
`SELECT count(*) ... then INSERT` pattern (the exact anti-pattern the authorization named): the
|
||||
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.
|
||||
|
||||
## Registration lifecycle
|
||||
|
||||
```
|
||||
started ──► submitted ──► reviewed ──┬──► agreements_pending ──┬──► payment_pending ──► confirmed ──► enrollment_activated
|
||||
│ │ │ │
|
||||
│ └──────────────────► confirmed ◄────────────────┘
|
||||
└──► cancelled (from any non-terminal state; releases the reserved seat)
|
||||
```
|
||||
|
||||
Not every offering needs every stage — a simple self-paced offering can go `started` →
|
||||
`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.
|
||||
|
||||
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`/
|
||||
`createManualEnrollment` rather than duplicating student-matching or enrollment-creation logic,
|
||||
and creates `cohort_enrollments` membership too if the offering has a `cohort_id`.
|
||||
|
||||
## Minor registrations
|
||||
|
||||
`offering_registrations.is_minor = true` requires a non-null `guardian_id` — enforced by a
|
||||
database CHECK constraint, not just application logic, so it holds even against a direct/future
|
||||
API caller that skips the usual route. The guardian is a real `guardians` row (Phase 7's new
|
||||
model), never a duplicated email on the eventual student record.
|
||||
|
||||
## Agreements — acceptance records, not legal text
|
||||
|
||||
`agreement_definitions` stores a title and a plain-language `body_summary` for admin-UI display
|
||||
— explicitly **not** attorney-approved legal language (see `docs/STUDENT_GUARDIAN_DATA_MODEL.md`'s
|
||||
legal-review list). `agreement_acceptances` snapshots the `accepted_version` at acceptance time
|
||||
(not just a live foreign key to the current version), so a later edit to an agreement's text
|
||||
never retroactively reinterprets what someone already agreed to — the same principle
|
||||
`COURSE_CONTENT_MODEL.md` §6b already established for assessment session versioning. An
|
||||
acceptance is tied to a student OR a guardian (a CHECK constraint requires at least one), and
|
||||
optionally to the registration/enrollment context it was collected for.
|
||||
|
||||
## Curriculum packages — reference, never clone
|
||||
|
||||
A `curriculum_package` names a versioned subset of an existing program's courses via
|
||||
`curriculum_package_items` (`course_id` + `sequence`) — a pure reference list, storing no
|
||||
content of its own. Worked example, matching `docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md`'s
|
||||
Option A:
|
||||
|
||||
```
|
||||
curriculum_package_id: winter-linux-systems-v1
|
||||
program_id: root-kubernetes-foundations
|
||||
items: [ (root-01-computing-linux-foundations, seq 1), (root-02-linux-administration-networking, seq 2) ]
|
||||
```
|
||||
|
||||
An offering referencing this package still points its students at the real, unmodified ROOT 01/
|
||||
02 courses — nothing is cloned, no activity is duplicated, and any future content fix to ROOT 01/
|
||||
02 is automatically reflected for every offering referencing the package. Historical offerings
|
||||
stay stable against a **specific version** of the package (bump `curriculum_packages.version` for
|
||||
a semantic change, exactly like every other versioned row in this schema) rather than silently
|
||||
picking up a redefinition.
|
||||
|
||||
## Offering → cohort
|
||||
|
||||
For cohort/camp/workshop delivery, an offering's `cohort_id` (nullable) points at an existing
|
||||
`cohorts` row, unchanged from Phase 2A. `convertRegistrationToEnrollment()` inserts the matching
|
||||
`cohort_enrollments` row when present — a student registering into a cohort-backed offering ends
|
||||
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.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Phase 7 — Business & Product Architecture Foundation
|
||||
|
||||
Phase 7's mission: formalize RootNode Academy as a business/product platform capable of a future
|
||||
catalog across INIT_/COMPILE_/ROOT_/BOOTCAMP_, without rebuilding any proven system and without
|
||||
authoring new curriculum.
|
||||
|
||||
## Baseline
|
||||
|
||||
Confirmed at start: branch `main`, clean tree, HEAD = `origin/main` = Phase 6F's
|
||||
`d0664104515e4bd389c8e7bc88293327e7eda100`, tag `v0.6.0-root-v1` present. Reviewed existing
|
||||
`programs`/`enrollments`/`cohorts`/`classroom_sessions`/`students` schema before designing
|
||||
anything new — found `programs.family`/`delivery_mode` and `cohorts.seat_target` already
|
||||
existed, so Phase 7 widened rather than duplicated them where it could.
|
||||
|
||||
## What was built
|
||||
|
||||
- **Organizations, locations** (`033_phase7_organizations_locations_guardians.sql`) — a business/
|
||||
delivery relationship and a physical/virtual venue, deliberately not SaaS multi-tenancy.
|
||||
- **Guardians, student_guardians** (same migration) — the real foundation replacing the Phase
|
||||
5C `contact_email` workaround, left functioning unchanged for the live website integration.
|
||||
- **Offerings, curriculum packages, registrations, agreements**
|
||||
(`034_phase7_offerings_registrations_agreements.sql`) — the full purchasable-delivery model,
|
||||
pre-enrollment registration lifecycle, versioned agreement acceptance records, and
|
||||
reference-only curriculum subsets.
|
||||
- **`platform/api/src/offerings.js`** — the business logic: server-defined status transitions
|
||||
(offerings and registrations both), race-safe atomic seat reservation, and
|
||||
`convertRegistrationToEnrollment()` reusing `enrollment.js`'s existing student-matching and
|
||||
enrollment-creation logic rather than duplicating it.
|
||||
- **`platform/api/src/admin-offerings-routes.js`** — the full admin/owner API surface for all of
|
||||
the above, registered in `server.js` alongside the existing route modules.
|
||||
- **`programs.family` widened** to accept `bootcamp` (a pure CHECK-constraint widening; no
|
||||
existing row changes).
|
||||
|
||||
## What was NOT built this phase
|
||||
|
||||
**Admin UI** (Steps 16/17 — Offerings/Registrations screens in `/admin`). This was a deliberate
|
||||
scope call: given the phase's own emphasis ("BUSINESS DOMAIN MODEL, PRODUCT ARCHITECTURE,
|
||||
ENROLLMENT/OPERATIONS ARCHITECTURE"), the schema, business logic, RBAC, tests, and a full
|
||||
working rehearsal were prioritized as the load-bearing deliverable over a polished React
|
||||
surface. Every capability Steps 16-17 describe is fully implemented and tested at the API layer
|
||||
— an admin UI is now a bounded, well-specified follow-up (the routes and their exact shapes
|
||||
already exist), not a design question.
|
||||
|
||||
Also not built, per the explicit DO-NOT-BUILD list: payment processing, subscription billing,
|
||||
public checkout, INIT_/COMPILE_ curriculum, ROOT 08/11 expansion, a Parent Portal UI, school
|
||||
SIS integrations, certificates.
|
||||
|
||||
## A real bug found and fixed during this phase's own testing
|
||||
|
||||
The registration-creation route originally reserved an offering's seat *before* validating the
|
||||
registration's own fields. Under a full offering, this let a plain validation failure (e.g. a
|
||||
minor registration missing `guardianId`) get masked behind a false `409 OFFERING_FULL`, and even
|
||||
when capacity wasn't the issue, a request that would fail validation anyway had already consumed
|
||||
a real seat. Found via this phase's own smoke testing before any commit; fixed by validating
|
||||
(creating) the registration first, then reserving the seat, rolling back the registration row if
|
||||
reservation then genuinely fails. See `docs/OFFERING_AND_REGISTRATION_MODEL.md`.
|
||||
|
||||
## Testing
|
||||
|
||||
`platform/test/e2e-phase7.js` — 17/17 passing (fresh stack): course-family/age-guidance
|
||||
independence from authorization, offerings across two distinct locations, curriculum-package
|
||||
content-reference integrity (including rejecting a fabricated course id), atomic capacity
|
||||
enforcement with correct validate-before-reserve ordering, instructor assignment, guardian
|
||||
creation/linking, minor-vs-adult registration paths, registration lifecycle transitions
|
||||
(including a rejected invalid jump), agreement-version-snapshotted acceptance, registration→
|
||||
enrollment conversion with correct cohort/no-cohort behavior, curriculum-package version-bump
|
||||
stability, registration/guardian RBAC (admin/owner only) vs. offering/location visibility (any
|
||||
instructor, with `access_notes` stripped), explicit age-independence, and confirmation that an
|
||||
existing ROOT_ V1 self-paced enrollment and an existing classroom/cohort flow are both
|
||||
completely unaffected.
|
||||
|
||||
## Winter Break Camp rehearsal
|
||||
|
||||
`infra/scripts/drills/test-phase7-winter-camp.js` — 18/18 passing against a real, freshly-built
|
||||
stack, with **no payment code invoked anywhere**: admin creates a real organization/location, a
|
||||
curriculum package referencing the existing ROOT 01-02 courses (matching `docs/
|
||||
WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md`'s Option A exactly), a real cohort, and a DRAFT offering
|
||||
with dates/price/exact seat count left unset as authorized; a guardian registers a minor and an
|
||||
adult registers themselves; both move through submission, agreement acceptance, and a simulated
|
||||
admin-side commerce confirmation; both convert to real enrollments and real cohort membership
|
||||
with real one-time activation tokens; both activate real accounts; the instructor starts a real
|
||||
classroom session and Prepares Labs; the minor student joins, completes real ROOT 01 orientation
|
||||
and lesson activities, and passes a REAL Lab task (`linux-inspect-hardware`) inside a real
|
||||
disposable Lab container — with the pass then confirmed to survive a real logout/login; the
|
||||
instructor sees real roster state; the offering moves through `in_progress` to `completed`.
|
||||
|
||||
## ROOT_ V1 regression
|
||||
|
||||
`e2e-phase6f.js` rerun fresh: 17/17 unaffected. `e2e-phase5b.js` (the enrollment-related suite
|
||||
most exposed by this phase's one shared-code change — `enrollment.js`'s `resolveOrCreateStudent`
|
||||
gained an optional, default-off parameter) rerun fresh: 32/32, including its own embedded
|
||||
"Phase 5A student authentication still works unmodified" and "Phase 5A remote classroom still
|
||||
works unmodified" checks. `e2e-phase5c.js` was not independently re-run: no code in
|
||||
`notifications.js`/`students-auth.js` itself changed, only an additive, default-off parameter on
|
||||
a function `notifications.js` doesn't call.
|
||||
|
||||
## Frontend, migrations, content validation
|
||||
|
||||
Frontend: `npm run build` clean, no frontend code touched this phase. Content validation:
|
||||
0 errors, unchanged from Phase 6F's own numbers (this phase touched no course/module/activity
|
||||
content at all). Migrations: additive-only, `CREATE TABLE IF NOT EXISTS`/`DROP CONSTRAINT IF
|
||||
EXISTS` throughout, applied once via the existing `schema_migrations` tracking mechanism — no
|
||||
destructive statement anywhere in `033`/`034`.
|
||||
|
||||
## Documentation
|
||||
|
||||
New: `ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md`, `OFFERING_AND_REGISTRATION_
|
||||
MODEL.md`, `STUDENT_GUARDIAN_DATA_MODEL.md`, `SCHOOL_AND_COMMUNITY_DELIVERY_ARCHITECTURE.md`,
|
||||
this document. Updated: `WEBSITE_ENROLLMENT_INTEGRATION.md` (a documented future flow, current
|
||||
contract unchanged). Reviewed, no change needed: `REMOTE_AND_SELF_PACED_ARCHITECTURE.md`,
|
||||
`ENGINEERING_SOP.md`, `LAB_ARCHITECTURE.md`, `platform/README.md` (no new e2e-suite convention
|
||||
beyond what its existing list format already documents — see commit history for the actual
|
||||
`platform/README.md` addition covering `e2e-phase7.js`).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **No admin UI** for offerings/registrations — the API is complete and tested; the screens are
|
||||
a well-specified follow-up, not a design question.
|
||||
- **Guardian communications are not yet wired into `notifications.js`** —
|
||||
`student_guardians.can_receive_communications`/`is_primary_contact` are recorded but nothing
|
||||
currently reads them to route an actual email.
|
||||
- **The website integration endpoint does not yet accept an `offering_slug`** — the future flow
|
||||
is documented, not implemented; that decision (extend the existing endpoint vs. a parallel
|
||||
one) was deliberately left open pending real website/checkout design.
|
||||
- **`agreement_acceptances.accepted_by_type = 'student'`** is only meaningful post-conversion,
|
||||
once a real `student_id` exists — an adult accepting at registration time (before their
|
||||
account exists) currently must accept as `'guardian'`-style even when self-registering; a
|
||||
future phase could add a `'self'` accepted-by-type distinct from `'guardian'` if that
|
||||
distinction becomes operationally important.
|
||||
|
||||
## Items requiring legal/business review before production use with real minors
|
||||
|
||||
See `docs/STUDENT_GUARDIAN_DATA_MODEL.md`'s own list in full: the actual legal text of any
|
||||
participation/media/liability agreement, jurisdiction-specific minor-consent requirements,
|
||||
data-retention/deletion policy for guardian and registration records, and whether emergency-
|
||||
contact fields need additional encryption at rest. None of these are resolved by this phase's
|
||||
work — they are named so they are never silently assumed away.
|
||||
|
||||
## Bugs found/fixed
|
||||
|
||||
One (the seat-reservation-before-validation ordering bug above), found and fixed before any
|
||||
commit via this phase's own smoke testing — not discovered by a later test run.
|
||||
|
||||
## Recommended Phase 8 scope
|
||||
|
||||
The admin UI for offerings/registrations (the one deliberately deferred piece of this phase's
|
||||
own mission); wiring guardian communications into `notifications.js`; and, informed by real
|
||||
business decisions, either extending `/api/integrations/enrollments` with an `offering_slug`
|
||||
or designing the parallel endpoint the future commerce flow needs — plus whatever the business
|
||||
decides about actually opening Winter Break Camp registration (which requires payment
|
||||
processing, explicitly out of every phase's scope so far).
|
||||
@@ -0,0 +1,143 @@
|
||||
# RootNode Academy — Business & Product Architecture
|
||||
|
||||
Phase 7 Step 1 — the canonical bridge between RootNode's business strategy and its platform
|
||||
architecture. This is where "what RootNode Academy sells and to whom" and "what the codebase
|
||||
actually implements" are kept in one place, deliberately separated into BUILT NOW / PHASE 7
|
||||
BUILT / FUTURE so nobody has to guess which parts are real.
|
||||
|
||||
## Course families
|
||||
|
||||
INIT_, COMPILE_, ROOT_, and BOOTCAMP_ are **product positioning and technical-depth families**,
|
||||
not a rigid age ladder and not a mandatory sequence:
|
||||
|
||||
| Family | Typical audience (guidance, not a gate) | Status |
|
||||
|---|---|---|
|
||||
| INIT_ | ~ages 5-10 | Not yet authored (BUILT NOW: `programs.family` accepts it; PHASE 7 BUILT: nothing else; FUTURE: real curriculum) |
|
||||
| COMPILE_ | ~ages 11-14 | Same as INIT_ |
|
||||
| ROOT_ | ~ages 15+ through adult | **Fully authored, V1 pilot-ready** (Phase 6F) |
|
||||
| BOOTCAMP_ | short/intensive seasonal or specialty offerings | BUILT NOW: `programs.family` accepts it; PHASE 7 BUILT: the offering/curriculum-package machinery a bootcamp needs; FUTURE: an actual bootcamp program row |
|
||||
|
||||
**INIT → COMPILE → ROOT is explicitly NOT a mandatory path.** A student's access to any given
|
||||
course depends on that course's own prerequisites (already real and server-enforced within
|
||||
ROOT_ — see `docs/ROOT_V1_PREREQUISITE_MAP.md`), not on which family they've touched before.
|
||||
|
||||
## COMPILE_ / ROOT_ overlap
|
||||
|
||||
These two families intentionally cover overlapping technical territory at different depths — an
|
||||
adult is not blocked from an electronics/robotics/IoT course merely because it's positioned
|
||||
under COMPILE_, and a COMPILE_-age student is not blocked from later revisiting the same domain
|
||||
at ROOT_ depth. Example: COMPILE_ might teach "ESP32 + a sensor + introductory MQTT"; ROOT_ might
|
||||
teach "Linux + networking + a real broker/service + a containerized IoT workload + observability"
|
||||
— same domain, different technical floor. **BUILT NOW**: nothing prevents this architecturally —
|
||||
`programs`/`offerings` carry no family-based authorization check anywhere in the codebase.
|
||||
**FUTURE**: the actual COMPILE_ electronics/robotics content itself (explicitly out of Phase 7's
|
||||
scope).
|
||||
|
||||
## Program vs. Course vs. Offering
|
||||
|
||||
Three previously-conflated levels, now distinct:
|
||||
|
||||
- **Program** (`programs` table, Phase 5B, unchanged): the educational product/path itself —
|
||||
e.g. "ROOT Kubernetes Foundations." Carries `family` and a default `delivery_mode`.
|
||||
- **Course** (`courses` table, Phase 6A, unchanged): a curricular unit inside a program — e.g.
|
||||
"ROOT 02 Linux Administration & Networking."
|
||||
- **Offering** (`offerings` table, **new this phase**): a specific purchasable/enrollable
|
||||
*instance* of delivering a program (or a curriculum-package subset of one) — e.g. "Winter
|
||||
Break Linux & Systems Camp — December 2026," "ROOT Foundations Cohort — Spring 2027," "ROOT
|
||||
Self-Paced." A program can have many offerings across time; an offering always points at
|
||||
exactly one program.
|
||||
|
||||
A **workshop** and a **cohort** are both just `delivery_mode` values on an offering, not separate
|
||||
tables — see "Delivery modes" below. A **curriculum package** (new this phase) is an optional,
|
||||
named, versioned SUBSET reference into an existing program's courses, for when an offering (like
|
||||
a camp) intentionally teaches only part of a program — see `docs/OFFERING_AND_REGISTRATION_MODEL.md`.
|
||||
|
||||
## Age guidance, not a gate
|
||||
|
||||
`offerings.recommended_min_age`/`recommended_max_age` and each course family's typical-audience
|
||||
description are **display/marketing guidance only**. No API authorization check anywhere in this
|
||||
codebase reads either value — enrollment and course access remain governed entirely by
|
||||
prerequisites, entitlement (`enrollments.status`), and (new this phase) offering capacity/
|
||||
registration status. Verified explicitly in `platform/test/e2e-phase7.js`.
|
||||
|
||||
## Delivery modes
|
||||
|
||||
Curriculum and delivery are separate concepts. `offerings.delivery_mode` (new this phase) is a
|
||||
single column supporting: `self_paced`, `cohort`, `hybrid`, `camp`, `workshop`, `school`,
|
||||
`after_school`, `homeschool`, `community`, `one_to_one`. No curriculum content is duplicated per
|
||||
mode — the same `program`/`courses`/`course_modules`/`module_activities` are read regardless of
|
||||
which offering enrolled the student into them, exactly the same principle
|
||||
`docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md` already established for self-paced vs.
|
||||
instructor-led delivery of ROOT_ V1.
|
||||
|
||||
## Organizations and locations
|
||||
|
||||
**Organization** (new this phase): a business/delivery relationship — a school, library,
|
||||
homeschool group, community organization, or workforce-development partner. **Not** SaaS
|
||||
multi-tenancy: RootNode Academy remains one Academy, one database, one set of students/
|
||||
instructors; an organization is metadata an offering can optionally reference, not a data
|
||||
partition. **Location** (new this phase): a physical or virtual delivery site, optionally tied
|
||||
to an organization (a rented community-center room needs no partner org; "online" needs neither
|
||||
an org nor a physical address). See `docs/SCHOOL_AND_COMMUNITY_DELIVERY_ARCHITECTURE.md`.
|
||||
|
||||
## Guardians and minors
|
||||
|
||||
**Guardian** (new this phase, `guardians`/`student_guardians`): a real, separate identity from a
|
||||
student's own account — not a login/auth identity yet (no password, no session), just a
|
||||
contact/relationship record. Replaces the Phase 5C `enrollments.metadata.contact_email`
|
||||
workaround as the proper foundation, while leaving that workaround functioning unchanged for
|
||||
backward compatibility with the live website integration. See
|
||||
`docs/STUDENT_GUARDIAN_DATA_MODEL.md`.
|
||||
|
||||
## Registration, enrollment, and agreements
|
||||
|
||||
**Registration** (new this phase, `offering_registrations`): pre-enrollment interest/application
|
||||
information for a specific offering — separate from `enrollments` (academic entitlement,
|
||||
unchanged). A registration becomes a real enrollment only once `confirmed`, via
|
||||
`convertRegistrationToEnrollment()`. **Agreement** (new this phase,
|
||||
`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
|
||||
|
||||
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.
|
||||
|
||||
## First commercial target: Winter Break Camp
|
||||
|
||||
Per `docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md`'s Option A (Linux & Systems Foundations,
|
||||
ROOT 01-02, ~12.4 hours), Phase 7 models this as a real, if still DRAFT, offering
|
||||
(`docs/OFFERING_AND_REGISTRATION_MODEL.md`'s worked example) — proving the platform CAN
|
||||
represent and operate it, without inventing dates, venue, price, or seat count that aren't
|
||||
already decided.
|
||||
|
||||
## Future memberships/subscriptions and institutional delivery
|
||||
|
||||
Recurring (self-paced subscriptions, after-school programs, school/community contracts) and
|
||||
institutional (schools, libraries, workforce partners) revenue channels are **FUTURE** — the
|
||||
`organizations`/`locations`/`offerings` model this phase builds is deliberately general enough
|
||||
to represent them later without a schema redesign, but no billing/subscription code exists or is
|
||||
planned this phase.
|
||||
|
||||
## BUILT NOW / PHASE 7 BUILT / FUTURE — summary
|
||||
|
||||
| Concept | Status |
|
||||
|---|---|
|
||||
| Programs, courses, ROOT_ V1 curriculum | BUILT NOW (Phases 1-6F) |
|
||||
| `programs.family` accepting `bootcamp` | PHASE 7 BUILT (schema widening only) |
|
||||
| Organizations, locations | PHASE 7 BUILT |
|
||||
| Guardians, student-guardian relationships | PHASE 7 BUILT |
|
||||
| Offerings, capacity, server-defined status transitions | PHASE 7 BUILT |
|
||||
| Curriculum packages (subset references) | PHASE 7 BUILT |
|
||||
| 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 |
|
||||
| INIT_/COMPILE_ curriculum | FUTURE |
|
||||
| School/library SIS integrations, parent portal | FUTURE, explicitly out of scope |
|
||||
@@ -0,0 +1,64 @@
|
||||
# School & Community Delivery Architecture
|
||||
|
||||
Phase 7 Step 27 — how a future school, library, homeschool organization, or community partner
|
||||
maps onto the platform's new organization/location/offering model. This is architecture
|
||||
documentation for an asset-light delivery strategy — RootNode favors partner/temporary venues
|
||||
over building permanent classrooms everywhere, at least initially.
|
||||
|
||||
## The mapping
|
||||
|
||||
```
|
||||
Organization (e.g. "Oxford Community Library")
|
||||
└─ Location (e.g. "Library — Community Room B", or "Online")
|
||||
└─ Offering (e.g. "After-School ROOT Foundations — Library Branch, Spring 2027")
|
||||
├─ program_id → an existing program (ROOT_ V1, or a future family)
|
||||
├─ curriculum_package_id → optionally, a subset (Step 20)
|
||||
├─ delivery_mode → 'after_school' / 'community' / 'school' / 'homeschool'
|
||||
├─ cohort_id → the real classroom/roster mechanism, unchanged
|
||||
└─ instructor_offerings → who teaches it
|
||||
```
|
||||
|
||||
A school, library, or homeschool organization is represented as ONE `organizations` row. It may
|
||||
have multiple `locations` (a school with two campuses; a library system with several branches).
|
||||
Each real delivery of curriculum at that organization is its own `offerings` row — this lets one
|
||||
organization run several different offerings (different programs, different cohorts, different
|
||||
semesters) without any of them being confused for each other, and without needing a new
|
||||
mechanism for "an organization has many offerings" (it's the same one-to-many `organization_id`
|
||||
foreign key `locations` and `offerings` already both carry).
|
||||
|
||||
## What this is NOT
|
||||
|
||||
**Not multi-tenancy.** No table in this schema is partitioned by `organization_id` the way a
|
||||
true multi-tenant SaaS product would be — students, instructors, courses, and progress are not
|
||||
scoped to an organization. An organization is a business/delivery *context* an offering can
|
||||
optionally reference, nothing more. RootNode Academy remains one Academy with one shared
|
||||
platform, one curriculum, and one set of students — a student who attends via a school
|
||||
partnership is a normal RootNode student, indistinguishable in the data model from a self-paced
|
||||
student, except for which offering(s) they registered through.
|
||||
|
||||
## Multi-location operation
|
||||
|
||||
Two offerings at two different locations coexist with no special isolation mechanism needed —
|
||||
they're just two rows in `offerings` with different `location_id`s, each with its own cohort/
|
||||
roster/registrations. `platform/test/e2e-phase7.js` proves this concretely: two offerings, two
|
||||
locations, verified to never leak state into each other. This is operational separation (two
|
||||
independent business records), not a security/tenant-isolation claim.
|
||||
|
||||
## What was explicitly NOT built this phase
|
||||
|
||||
- **No school SIS integration** (no roster import/export, no grade-passback, no LMS
|
||||
interoperability standard like OneRoster or LTI).
|
||||
- **No Google Classroom integration.**
|
||||
- **No organization-specific portal or branding** — every organization's students see the same
|
||||
RootNode Academy platform; there is no white-labeling.
|
||||
- **No organization-level admin role** — an organization contact does not get their own login or
|
||||
visibility into the platform; all administration remains through RootNode's own
|
||||
instructor/admin accounts, same as any other offering.
|
||||
|
||||
## Future extension points (not built, but not blocked)
|
||||
|
||||
If a future phase needs an organization contact to see their own offering's roster/progress
|
||||
(e.g. a school liaison checking on their students), that's a new, scoped RBAC role — the
|
||||
`organizations`/`offerings` model here doesn't need to change to support it, only the
|
||||
authorization layer would need a new role definition, deliberately deferred rather than guessed
|
||||
at now.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Student & Guardian Data Model
|
||||
|
||||
Phase 7 Step 28 — why student and guardian identities are kept separate, what data-minimization
|
||||
principles govern the new `guardians`/`student_guardians` tables, and what still needs
|
||||
professional/legal review before any of this touches a real minor's data in production.
|
||||
|
||||
## Why separate identities
|
||||
|
||||
A student's own identity (`students` table — `email`/`login_identifier`, unchanged) is their
|
||||
academic account. A guardian (`guardians`, new this phase) is a real, separate identity: a
|
||||
parent with three enrolled children is **one** guardian row linked to three students via
|
||||
`student_guardians`, not a guardian email copy-pasted into three student records. This mirrors
|
||||
exactly the reasoning `docs/WEBSITE_ENROLLMENT_INTEGRATION.md` already gave for keeping
|
||||
`contact_email` off `students.email` — a contact address is not an academic identity, and now a
|
||||
guardian is a real relationship, not a workaround field.
|
||||
|
||||
## What this phase deliberately did NOT build
|
||||
|
||||
- **No Parent Portal.** A guardian record has no login, no password, no session — it cannot
|
||||
authenticate into anything yet. Building that is a real, separate future decision.
|
||||
- **No child-activity surveillance.** Nothing in this schema or its routes lets a guardian (once
|
||||
they could log in, in some future phase) see a student's Lab activity, scenario attempts, or
|
||||
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
|
||||
authorization to student data.
|
||||
|
||||
## Data minimization
|
||||
|
||||
Only fields with a genuine, near-term operational purpose were added: `display_name`, `email`,
|
||||
`phone`, and a free-text `notes` field for a guardian; `relationship`
|
||||
(`parent`/`guardian`/`authorized_contact`/`emergency_contact`), `is_primary_contact`, and
|
||||
`can_receive_communications` for the relationship itself. Registration-time fields
|
||||
(`readiness_notes`, `accessibility_notes`, `emergency_contact_name`/`emergency_contact_phone`)
|
||||
were chosen because a real camp/cohort genuinely needs them operationally — **no medical-record
|
||||
functionality was built**, and `accessibility_notes` is a free-text operational note (e.g. "uses
|
||||
a screen reader," "needs extra Lab time"), never a structured health/diagnosis field.
|
||||
|
||||
## RBAC
|
||||
|
||||
Guardian records and registration detail (readiness/accessibility/emergency-contact notes) are
|
||||
admin/owner-only routes — a plain instructor's existing cohort-scoped access to a student's
|
||||
academic progress is unchanged, but they do not automatically see guardian contact information
|
||||
or registration notes through the new routes. See `admin-offerings-routes.js`'s own RBAC
|
||||
comment and `platform/test/e2e-phase7.js`'s guardian-RBAC test.
|
||||
|
||||
## Adult vs. minor enrollment
|
||||
|
||||
An adult learner registers and is their own `registrant_email`/eventual student identity — no
|
||||
guardian row is created or required. A minor's registration requires a `guardian_id` (enforced
|
||||
by a database CHECK constraint on `offering_registrations`, not just application logic). Neither
|
||||
path writes a guardian's email into `students.email` — a minor with no email of their own still
|
||||
gets `login_identifier`-based identity (the same mechanism `docs/WEBSITE_ENROLLMENT_INTEGRATION.md`
|
||||
already established), preserving the identity semantics Phase 5A/5C set up.
|
||||
|
||||
## Retention — flagged, not decided
|
||||
|
||||
This phase does not implement a data-retention or deletion policy for guardian/registration
|
||||
records. **Requires future legal/business review**: how long registration data (especially
|
||||
accessibility/emergency-contact notes for a minor) should be retained after an offering
|
||||
completes or a registration is cancelled, and what a guardian's/family's data-deletion request
|
||||
process should look like.
|
||||
|
||||
## Items explicitly requiring professional/legal review before production use with real minors
|
||||
|
||||
- Whether the current field set (name/email/phone/relationship) is sufficient or whether
|
||||
jurisdiction-specific requirements (e.g. COPPA-adjacent consent language, state-specific camp/
|
||||
minor-supervision regulations) require additional structured fields.
|
||||
- The actual legal text of any participation agreement, media consent, or liability waiver —
|
||||
`agreement_definitions.body_summary` is a plain-language placeholder for admin-UI display
|
||||
only, never a substitute for attorney-reviewed legal language.
|
||||
- Data retention/deletion policy for guardian and registration records.
|
||||
- Whether `emergency_contact_name`/`emergency_contact_phone` need to be encrypted at rest beyond
|
||||
Postgres's existing storage (no additional encryption was added this phase).
|
||||
|
||||
None of the above is resolved by this phase's work — they are named here specifically so they
|
||||
are not silently assumed away.
|
||||
@@ -171,10 +171,37 @@ email — otherwise the enrollment is created but nobody is notified automatical
|
||||
instructor has to relay the activation link manually. `contact_email` is stored only against
|
||||
that specific enrollment, never copied into the student's own identity record.
|
||||
|
||||
There is no guardian/parent account system yet — the enrollment is created directly against the
|
||||
student identity you provide. If you need to support "a parent manages multiple children's
|
||||
enrollments" as a first-class concept later, talk to the RootNode team before building around a
|
||||
workaround; the schema has room for it but it isn't implemented.
|
||||
**As of Phase 7, a real guardian/parent record now exists** (`guardians`/`student_guardians` —
|
||||
see `docs/STUDENT_GUARDIAN_DATA_MODEL.md`), but this endpoint's contract is UNCHANGED and does
|
||||
not yet create or link one — `student.contact_email` still works exactly as described above,
|
||||
stored only on the enrollment. If you need "a parent manages multiple children's enrollments" as
|
||||
a first-class concept in this endpoint, talk to the RootNode team before building around a
|
||||
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)
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
## What the student experiences after you call this (as of Phase 5C)
|
||||
|
||||
|
||||
@@ -106,6 +106,12 @@ further instructor accounts from the Instructors tab).
|
||||
prerequisite graph (no cycles, no missing nodes), the strengthened Capstone gate (ROOT 09 AND
|
||||
ROOT 12 both required) in both directions, the new orientation module, and an explicit check
|
||||
that no age/grade gate exists anywhere in enrollment.
|
||||
- `test/e2e-phase7.js` — organizations/locations, guardians and student-guardian relationships,
|
||||
curriculum packages (reference-only, never a content clone), offerings (capacity, server-
|
||||
defined status transitions), the registration lifecycle through agreement acceptance and
|
||||
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.
|
||||
|
||||
`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
|
||||
|
||||
Reference in New Issue
Block a user