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 <[email protected]>
This commit is contained in:
2026-08-26 23:59:42 -05:00
co-authored by Claude Sonnet 5
parent aa63d09fb8
commit 788fb197e0
9 changed files with 573 additions and 35 deletions
+131 -18
View File
@@ -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: <the same secret /api/integrations/enrollments already uses>
```
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": "[email protected]", "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": "[email protected]", "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)