- 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]>
54 lines
5.7 KiB
SQL
54 lines
5.7 KiB
SQL
-- Phase 8 — enrollment operations foundation: registration-stage notifications, a clean
|
|
-- pre-account agreement-acceptance type, offering agreement requirements, and idempotent
|
|
-- website registration submission. All additive/widening — no existing row's meaning changes,
|
|
-- no existing constraint gets stricter. See docs/PHASE8_ENROLLMENT_OPERATIONS.md.
|
|
|
|
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
-- notification_events — a registration may need a notification (Registration Received, Agreement
|
|
-- Required) before any student row exists at all (a brand-new guardian-submitted registration).
|
|
-- student_id is widened to nullable and a registration_id column added; a CHECK guarantees every
|
|
-- row still has SOME real subject. Every existing row already has student_id set, so this is a
|
|
-- pure widening — nothing about student-scoped notification behavior changes.
|
|
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
ALTER TABLE notification_events ALTER COLUMN student_id DROP NOT NULL;
|
|
ALTER TABLE notification_events ADD COLUMN IF NOT EXISTS registration_id UUID REFERENCES offering_registrations(registration_id);
|
|
ALTER TABLE notification_events DROP CONSTRAINT IF EXISTS notification_events_subject_check;
|
|
ALTER TABLE notification_events ADD CONSTRAINT notification_events_subject_check CHECK (student_id IS NOT NULL OR registration_id IS NOT NULL);
|
|
CREATE INDEX IF NOT EXISTS idx_notification_events_registration ON notification_events(registration_id);
|
|
|
|
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
-- agreement_acceptances — a 4th accepted_by_type, 'registrant', for the one real gap Phase 7's
|
|
-- own limitations list named: an adult accepting an agreement AT REGISTRATION TIME, before their
|
|
-- eventual student_id exists. Tied to registration_id only (no student/guardian id required) —
|
|
-- the CHECK is widened to accept that shape too. 'student'/'guardian'/'instructor_on_behalf'
|
|
-- (admin-recorded on someone's behalf) are unchanged and already covered Step 7's "student /
|
|
-- guardian / admin-instructor" minimum bar; 'registrant' closes the actual documented gap.
|
|
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
ALTER TABLE agreement_acceptances DROP CONSTRAINT IF EXISTS agreement_acceptances_accepted_by_type_check;
|
|
ALTER TABLE agreement_acceptances ADD CONSTRAINT agreement_acceptances_accepted_by_type_check
|
|
CHECK (accepted_by_type IN ('student', 'guardian', 'instructor_on_behalf', 'registrant'));
|
|
ALTER TABLE agreement_acceptances DROP CONSTRAINT IF EXISTS agreement_acceptances_check;
|
|
ALTER TABLE agreement_acceptances ADD CONSTRAINT agreement_acceptances_check
|
|
CHECK (accepted_by_student_id IS NOT NULL OR accepted_by_guardian_id IS NOT NULL OR (accepted_by_type = 'registrant' AND registration_id IS NOT NULL));
|
|
|
|
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
-- offering_agreement_requirements — which agreements a given offering actually requires before
|
|
-- its registrations may convert to enrollment (Step 18). A plain join table, not a new status
|
|
-- machine — offerings.js checks it at conversion time.
|
|
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS offering_agreement_requirements (
|
|
offering_id TEXT NOT NULL REFERENCES offerings(offering_id),
|
|
agreement_id TEXT NOT NULL REFERENCES agreement_definitions(agreement_id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (offering_id, agreement_id)
|
|
);
|
|
|
|
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
-- offering_registrations — a real unique index on external_reference (nulls excluded) is what
|
|
-- actually makes website registration submission idempotent under concurrent retries, the exact
|
|
-- same pattern enrollments.external_event_id already established in Phase 5B — a DB-level
|
|
-- constraint, not a SELECT-then-INSERT check.
|
|
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_offering_registrations_external_reference
|
|
ON offering_registrations(external_reference) WHERE external_reference IS NOT NULL;
|