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