From f888ca67b9de34eff1fab06bfe5ce929e8dd46c4 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 7 Aug 2026 04:43:28 +0000 Subject: [PATCH] Modified by www.SourceFiles.app --- .../migrations/20260807000700_compliance.sql | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 supabase/migrations/20260807000700_compliance.sql diff --git a/supabase/migrations/20260807000700_compliance.sql b/supabase/migrations/20260807000700_compliance.sql new file mode 100644 index 0000000..892b04e --- /dev/null +++ b/supabase/migrations/20260807000700_compliance.sql @@ -0,0 +1,194 @@ +-- Staff compliance and certification tracking — spec section 11. +-- +-- Certification *types* are org-level reference data; a requirement binds a +-- type to a role and optionally to a campus, so a campus with an infant room +-- can demand credentials the others do not. +-- +-- Employee records are explicitly named in section 3 as something teachers must +-- not have access to, so everything here is readable only by the person it +-- concerns plus management-level roles. + +CREATE TABLE IF NOT EXISTS public.certification_types ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE, + slug TEXT NOT NULL UNIQUE, + issuing_body TEXT, + description TEXT, + -- NULL means the credential does not expire. + validity_months INTEGER CHECK (validity_months IS NULL OR validity_months > 0), + renewal_reminder_days INTEGER NOT NULL DEFAULT 60, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +GRANT SELECT, INSERT, UPDATE, DELETE ON public.certification_types TO authenticated; +GRANT ALL ON public.certification_types TO service_role; +ALTER TABLE public.certification_types ENABLE ROW LEVEL SECURITY; + +CREATE TABLE IF NOT EXISTS public.compliance_requirements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + certification_type_id UUID NOT NULL REFERENCES public.certification_types(id) ON DELETE CASCADE, + campus_id UUID REFERENCES public.campuses(id) ON DELETE CASCADE, + applies_to_roles app_role[] NOT NULL DEFAULT ARRAY['teacher','staff']::app_role[], + is_mandatory BOOLEAN NOT NULL DEFAULT TRUE, + grace_period_days INTEGER NOT NULL DEFAULT 0, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (certification_type_id, campus_id) +); + +GRANT SELECT, INSERT, UPDATE, DELETE ON public.compliance_requirements TO authenticated; +GRANT ALL ON public.compliance_requirements TO service_role; +ALTER TABLE public.compliance_requirements ENABLE ROW LEVEL SECURITY; +CREATE INDEX IF NOT EXISTS creq_type_idx ON public.compliance_requirements (certification_type_id); +CREATE INDEX IF NOT EXISTS creq_campus_idx ON public.compliance_requirements (campus_id); + +CREATE TABLE IF NOT EXISTS public.staff_certifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + certification_type_id UUID NOT NULL REFERENCES public.certification_types(id) ON DELETE RESTRICT, + + certificate_number TEXT, + issued_on DATE, + expires_on DATE, + document_path TEXT, + + status TEXT NOT NULL DEFAULT 'active', + verified_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, + verified_at TIMESTAMPTZ, + notes TEXT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT stc_status_valid CHECK (status IN ('pending','active','expired','revoked')), + CONSTRAINT stc_dates_ordered CHECK (issued_on IS NULL OR expires_on IS NULL OR issued_on <= expires_on) +); + +GRANT SELECT, INSERT, UPDATE, DELETE ON public.staff_certifications TO authenticated; +GRANT ALL ON public.staff_certifications TO service_role; +ALTER TABLE public.staff_certifications ENABLE ROW LEVEL SECURITY; +CREATE INDEX IF NOT EXISTS stc_user_idx ON public.staff_certifications (user_id); +CREATE INDEX IF NOT EXISTS stc_type_idx ON public.staff_certifications (certification_type_id); +-- The compliance dashboard's central query: what lapses soon. +CREATE INDEX IF NOT EXISTS stc_expiring_idx ON public.staff_certifications (expires_on) + WHERE status = 'active' AND expires_on IS NOT NULL; + +DROP TRIGGER IF EXISTS trg_stc_upd ON public.staff_certifications; +CREATE TRIGGER trg_stc_upd BEFORE UPDATE ON public.staff_certifications + FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); + +-- Derive expiry from the certification type when the user did not supply one. +CREATE OR REPLACE FUNCTION public.derive_certification_expiry() +RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ +DECLARE + months INTEGER; +BEGIN + IF NEW.expires_on IS NULL AND NEW.issued_on IS NOT NULL THEN + SELECT validity_months INTO months + FROM public.certification_types WHERE id = NEW.certification_type_id; + IF months IS NOT NULL THEN + NEW.expires_on := NEW.issued_on + (months || ' months')::interval; + END IF; + END IF; + + -- Keep status honest without waiting for a nightly job. + IF NEW.expires_on IS NOT NULL AND NEW.expires_on < CURRENT_DATE + AND NEW.status = 'active' THEN + NEW.status := 'expired'; + END IF; + + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS trg_stc_expiry ON public.staff_certifications; +CREATE TRIGGER trg_stc_expiry BEFORE INSERT OR UPDATE ON public.staff_certifications + FOR EACH ROW EXECUTE FUNCTION public.derive_certification_expiry(); + +-- Requirements a user has not satisfied: missing entirely, expired, or lapsing +-- inside the reminder window. +CREATE OR REPLACE FUNCTION public.staff_compliance_gaps(_user UUID) +RETURNS TABLE ( + certification_type_id UUID, certification_name TEXT, campus_id UUID, + gap_kind TEXT, expires_on DATE +) +LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$ + SELECT + t.id, t.name, req.campus_id, + CASE + WHEN c.id IS NULL THEN 'missing' + WHEN c.status = 'revoked' THEN 'revoked' + WHEN c.expires_on IS NOT NULL AND c.expires_on < CURRENT_DATE THEN 'expired' + ELSE 'expiring_soon' + END, + c.expires_on + FROM public.compliance_requirements req + JOIN public.certification_types t ON t.id = req.certification_type_id + -- The requirement applies only if the user holds one of its roles, and (when + -- campus-specific) is assigned to that campus. + JOIN public.user_roles ur ON ur.user_id = _user AND ur.role = ANY(req.applies_to_roles) + LEFT JOIN public.staff_campus_assignments sca + ON sca.user_id = _user AND sca.campus_id = req.campus_id + LEFT JOIN LATERAL ( + SELECT sc.* FROM public.staff_certifications sc + WHERE sc.user_id = _user AND sc.certification_type_id = t.id + AND sc.status IN ('active','pending') + ORDER BY sc.expires_on DESC NULLS FIRST + LIMIT 1 + ) c ON TRUE + WHERE req.is_mandatory + AND (req.campus_id IS NULL OR sca.id IS NOT NULL) + AND ( + c.id IS NULL + OR c.status = 'revoked' + OR (c.expires_on IS NOT NULL + AND c.expires_on < CURRENT_DATE + make_interval(days => t.renewal_reminder_days)) + ) +$$; + +-- ============================================================================ +-- POLICIES +-- ============================================================================ + +DROP POLICY IF EXISTS "cert types read" ON public.certification_types; +CREATE POLICY "cert types read" ON public.certification_types FOR SELECT TO authenticated + USING (public.is_management() OR public.is_auditor() + OR public.current_user_has_any_role(ARRAY['teacher','staff','campus_admin']::app_role[])); +DROP POLICY IF EXISTS "cert types manage" ON public.certification_types; +CREATE POLICY "cert types manage" ON public.certification_types FOR ALL TO authenticated + USING (public.is_org_admin()) WITH CHECK (public.is_org_admin()); + +DROP POLICY IF EXISTS "compliance req read" ON public.compliance_requirements; +CREATE POLICY "compliance req read" ON public.compliance_requirements FOR SELECT TO authenticated + USING (public.is_management() OR public.is_auditor() + OR public.current_user_has_any_role(ARRAY['teacher','staff','campus_admin']::app_role[])); +DROP POLICY IF EXISTS "compliance req manage" ON public.compliance_requirements; +CREATE POLICY "compliance req manage" ON public.compliance_requirements FOR ALL TO authenticated + USING (public.is_org_admin()) WITH CHECK (public.is_org_admin()); + +-- Own record, management, or the campus admin of a campus this person works at. +DROP POLICY IF EXISTS "staff certs read" ON public.staff_certifications; +CREATE POLICY "staff certs read" ON public.staff_certifications FOR SELECT TO authenticated + USING ( + user_id = (SELECT auth.uid()) + OR public.is_management() OR public.is_auditor() + OR (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[]) + AND EXISTS (SELECT 1 FROM public.staff_campus_assignments a + WHERE a.user_id = staff_certifications.user_id + AND a.campus_id IN (SELECT public.user_campus_ids()))) + ); + +-- Staff may upload their own credentials; only management may verify them, +-- which is why verified_by/verified_at are management-writable in practice. +DROP POLICY IF EXISTS "staff certs self insert" ON public.staff_certifications; +CREATE POLICY "staff certs self insert" ON public.staff_certifications FOR INSERT TO authenticated + WITH CHECK (user_id = (SELECT auth.uid()) OR public.is_management()); + +DROP POLICY IF EXISTS "staff certs manage" ON public.staff_certifications; +CREATE POLICY "staff certs manage" ON public.staff_certifications FOR UPDATE TO authenticated + USING (public.is_management()) WITH CHECK (public.is_management()); + +DROP POLICY IF EXISTS "staff certs delete" ON public.staff_certifications; +CREATE POLICY "staff certs delete" ON public.staff_certifications FOR DELETE TO authenticated + USING (public.is_org_admin());