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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user