feat(offering): launch readiness, agreement requirements, guardian communications, website registration API
- 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]>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Phase 8 — self-caught correction to 035_phase8_enrollment_operations.sql, found via this
|
||||
-- phase's own smoke testing before any commit: notification_events.notification_type carries a
|
||||
-- CHECK constraint (Phase 5C) that never anticipated registration-stage notification types.
|
||||
-- Every offerings.js call to notifications.enqueueForRegistration() was silently failing (caught
|
||||
-- and swallowed by the deliberate "a notification failure must never block the real action"
|
||||
-- try/catch) until this was found — no registration-stage notification had ever actually been
|
||||
-- queued. Pure widening: every existing notification_type value stays valid, nothing about
|
||||
-- Phase 5C's own four types changes.
|
||||
ALTER TABLE notification_events DROP CONSTRAINT IF EXISTS notification_events_notification_type_check;
|
||||
ALTER TABLE notification_events ADD CONSTRAINT notification_events_notification_type_check
|
||||
CHECK (notification_type IN (
|
||||
'activation', 'enrollment_confirmation', 'password_reset', 'password_changed',
|
||||
'registration_received', 'registration_confirmed', 'registration_needs_action',
|
||||
'agreement_required', 'camp_operational_update'
|
||||
));
|
||||
Reference in New Issue
Block a user