-- 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;