- Registration is now server-authoritative: draft/cancelled/completed/full/window-closed
offerings reject registration attempts with distinct error codes (assertRegistrationOpen).
- offering_agreement_requirements + conversion-time blocking on missing required
agreements; accepted_by_type gained 'registrant' for an adult self-accepting before
their student_id exists.
- computeOfferingReadiness() — a server-derived launch checklist (curriculum, instructor,
schedule, location, capacity, registration window, agreements, curriculum package).
- Guardian communications wired into the existing Phase 5C notification system: a guardian
email fallback in resolveRecipient(), a parallel enqueueForRegistration()/
resolveRegistrationRecipient() pair for pre-account registration-stage notifications
(registration_received/confirmed/needs_action, agreement_required, camp_operational_update),
five new RootNode-branded templates. Same table, same worker, same masked admin view —
no second notification system.
- public-offerings-routes.js: the unauthenticated, explicitly-allowlisted offering read API.
- POST /api/integrations/registrations: the website's idempotent registration submission
contract, same shared-secret boundary as the existing enrollment integration endpoint.
Two real bugs found and fixed during this phase's own testing before any commit:
(1) the five new notification_type values were wired into the worker before the DB's own
CHECK constraint was widened to accept them — every registration-stage notification was
silently failing until a comprehensive smoke test caught it (036 fixes the constraint;
every previously-silent notification catch now at least logs).
(2) firing a notification right after a registration's insert, before its seat reservation
could still fail and roll the row back, created a real foreign-key violation on rollback —
fixed by reordering createRegistration() to validate -> reserve -> insert -> notify.
See docs/OFFERING_AND_REGISTRATION_MODEL.md and docs/PHASE8_ENROLLMENT_OPERATIONS.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- email/provider.js: the seam. EMAIL_PROVIDER unset always defaults to the dev provider,
regardless of any other setting — misconfiguration means email silently doesn't send for
real, never that a dev/test run accidentally reaches a real student.
- email/gws-provider.js: Gmail API users.messages.send via a service account with Workspace
domain-wide delegation, impersonating one dedicated mailbox. No human password, no SMTP
basic auth, no googleapis/google-auth-library dependency — a hand-rolled RS256 JWT-bearer
OAuth exchange + one REST call (~120 lines), matching this codebase's established
hand-roll-before-adding-a-dependency convention.
- email/dev-provider.js: never makes a network call, returns the rendered message back to the
caller for dev/test inspection. Supports magic +forcetempfail/+forcepermfail recipient
markers so the permanent test suite can deterministically exercise retry/failure
classification without needing a real flaky provider.
- email/templates.js: activation/enrollment-confirmation/password-reset/password-changed,
HTML + plain-text, no external images/tracking/JS. Every interpolated value is HTML-escaped
in the HTML body (a real injection finding — display_name can come straight from a website
integration event).
- notification-worker.js: a setInterval poller (same pattern as sessions.js's sweeper), claims
via notifications.js, renders via templates.js, builds activation/reset URLs from
PUBLIC_APP_ORIGIN, stores a dev-only preview (never in production) so tests can extract the
real URL without a mailbox.