From 827f4669d3f8a8568c1afa3d99ac31735f970240 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 22 Aug 2026 23:10:58 +0000 Subject: [PATCH] Terminal --- .../20260807000500_tuition_engine.sql | 483 ------------------ 1 file changed, 483 deletions(-) delete mode 100644 supabase/migrations/20260807000500_tuition_engine.sql diff --git a/supabase/migrations/20260807000500_tuition_engine.sql b/supabase/migrations/20260807000500_tuition_engine.sql deleted file mode 100644 index 26d9d74..0000000 --- a/supabase/migrations/20260807000500_tuition_engine.sql +++ /dev/null @@ -1,483 +0,0 @@ --- Tuition engine — spec section 8. --- --- Tuition must be assignable by campus, student, enrollment type, scheduled --- days, week, tier, support status, scholarship status and date range. That is --- too many axes for columns on students, so rates live in their own table and --- are *matched* against a student for a billing period. --- --- Rate resolution, most specific wins: --- 1. student_tuition_assignments — an explicit per-student override --- 2. tuition_rates — tier + campus + basis + day-count + dates --- 3. students.daily_tuition_cents — legacy fallback, see the note below --- --- IMPORTANT — double-billing. 20260726120000 added a trigger that posts a --- ledger charge every time a student is marked present. Once invoices are --- generated from this engine, that trigger would bill the same tuition twice. --- It is therefore made switchable here and left ON, so behaviour is unchanged --- until an administrator deliberately turns it off. Turn it off before issuing --- the first real invoice: --- UPDATE public.billing_settings SET legacy_attendance_autocharge = FALSE; - --- ============================================================================ --- 1. BILLING SETTINGS (singleton) --- ============================================================================ - -CREATE TABLE IF NOT EXISTS public.billing_settings ( - id BOOLEAN PRIMARY KEY DEFAULT TRUE, - legacy_attendance_autocharge BOOLEAN NOT NULL DEFAULT TRUE, - invoice_number_prefix TEXT NOT NULL DEFAULT 'INV', - default_due_days INTEGER NOT NULL DEFAULT 7, - late_pickup_fee_cents INTEGER NOT NULL DEFAULT 0, - early_dropoff_fee_cents INTEGER NOT NULL DEFAULT 0, - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - -- Enforces a single row: the only permitted primary key value is TRUE. - CONSTRAINT billing_settings_singleton CHECK (id) -); - -GRANT SELECT, INSERT, UPDATE ON public.billing_settings TO authenticated; -GRANT ALL ON public.billing_settings TO service_role; -ALTER TABLE public.billing_settings ENABLE ROW LEVEL SECURITY; - -INSERT INTO public.billing_settings (id) VALUES (TRUE) ON CONFLICT (id) DO NOTHING; - -DROP POLICY IF EXISTS "billing_settings read" ON public.billing_settings; -CREATE POLICY "billing_settings read" ON public.billing_settings FOR SELECT TO authenticated - USING (public.is_billing_admin() OR public.is_org_admin() OR public.is_auditor()); -DROP POLICY IF EXISTS "billing_settings manage" ON public.billing_settings; -CREATE POLICY "billing_settings manage" ON public.billing_settings FOR ALL TO authenticated - USING (public.is_org_admin()) WITH CHECK (public.is_org_admin()); - --- Make the legacy autocharge respect the switch. Same logic as before, --- with one guard added at the top. -CREATE OR REPLACE FUNCTION public.sync_attendance_tuition() -RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ -DECLARE - rate INTEGER; - enabled BOOLEAN; -BEGIN - SELECT legacy_attendance_autocharge INTO enabled FROM public.billing_settings WHERE id; - IF NOT COALESCE(enabled, TRUE) THEN - RETURN NEW; - END IF; - - IF NEW.status IN ('present', 'late') THEN - SELECT daily_tuition_cents INTO rate FROM public.students WHERE id = NEW.student_id; - IF rate IS NOT NULL AND rate > 0 THEN - INSERT INTO public.ledger_entries (student_id, date, kind, category, amount_cents, note) - VALUES (NEW.student_id, NEW.date, 'charge', 'tuition', rate, 'Auto-charged from attendance') - ON CONFLICT DO NOTHING; - END IF; - END IF; - RETURN NEW; -END; -$$; - --- ============================================================================ --- 2. TUITION TIERS --- ============================================================================ - -CREATE TABLE IF NOT EXISTS public.tuition_tiers ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL UNIQUE, - slug TEXT NOT NULL UNIQUE, - description TEXT, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - sort_order INTEGER NOT NULL DEFAULT 100, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.tuition_tiers TO authenticated; -GRANT ALL ON public.tuition_tiers TO service_role; -ALTER TABLE public.tuition_tiers ENABLE ROW LEVEL SECURITY; - -INSERT INTO public.tuition_tiers (name, slug, description, sort_order) VALUES - ('Standard Full-Time', 'standard_full_time', 'Billed per week.', 10), - ('Standard Part-Time', 'standard_part_time', 'Billed per scheduled day.', 20), - ('Support Program', 'support_program', 'Support students; may carry an additional charge.', 30), - ('Scholarship', 'scholarship', 'Students funded wholly or partly by scholarship.', 40) -ON CONFLICT (slug) DO NOTHING; - -ALTER TABLE public.students - ADD COLUMN IF NOT EXISTS tuition_tier_id UUID REFERENCES public.tuition_tiers(id) ON DELETE SET NULL; -CREATE INDEX IF NOT EXISTS students_tuition_tier_idx ON public.students (tuition_tier_id); - --- ============================================================================ --- 3. TUITION RATES --- ============================================================================ --- NULL means "any" on every matching column, so one row can cover all campuses --- or all tiers. min_days/max_days express the "by number of scheduled days" --- axis — e.g. a discounted per-day rate once a student is scheduled 4+ days. - -CREATE TABLE IF NOT EXISTS public.tuition_rates ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tier_id UUID REFERENCES public.tuition_tiers(id) ON DELETE CASCADE, - campus_id UUID REFERENCES public.campuses(id) ON DELETE CASCADE, - program_id UUID REFERENCES public.campus_programs(id) ON DELETE SET NULL, - - rate_basis TEXT NOT NULL, - amount_cents INTEGER NOT NULL CHECK (amount_cents >= 0), - - attendance_basis TEXT, - min_days SMALLINT, - max_days SMALLINT, - requires_support_student BOOLEAN, - requires_scholarship BOOLEAN, - - effective_start DATE NOT NULL DEFAULT CURRENT_DATE, - effective_end DATE, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - -- Higher wins when several rates match. Lets a campus-specific rate beat a - -- global one without relying on column-precedence guesswork. - priority INTEGER NOT NULL DEFAULT 100, - - notes TEXT, - created_by UUID REFERENCES auth.users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - CONSTRAINT tr_basis_valid CHECK (rate_basis IN ('per_day','per_week','per_month','flat')), - CONSTRAINT tr_attendance_valid - CHECK (attendance_basis IS NULL OR attendance_basis IN ('full_time','part_time')), - CONSTRAINT tr_days_ordered CHECK (min_days IS NULL OR max_days IS NULL OR min_days <= max_days), - CONSTRAINT tr_dates_ordered CHECK (effective_end IS NULL OR effective_start <= effective_end) -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.tuition_rates TO authenticated; -GRANT ALL ON public.tuition_rates TO service_role; -ALTER TABLE public.tuition_rates ENABLE ROW LEVEL SECURITY; - -CREATE INDEX IF NOT EXISTS tr_tier_idx ON public.tuition_rates (tier_id); -CREATE INDEX IF NOT EXISTS tr_campus_idx ON public.tuition_rates (campus_id); -CREATE INDEX IF NOT EXISTS tr_program_idx ON public.tuition_rates (program_id); -CREATE INDEX IF NOT EXISTS tr_lookup_idx ON public.tuition_rates (effective_start, effective_end, priority DESC) - WHERE is_active; - -DROP TRIGGER IF EXISTS trg_tr_upd ON public.tuition_rates; -CREATE TRIGGER trg_tr_upd BEFORE UPDATE ON public.tuition_rates - FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); - --- ============================================================================ --- 4. ADJUSTMENT RULES --- ============================================================================ --- Support charges, drop-off/pick-up penalties and discounts. The spec's --- Enterprise/Palm Bay support rule is seeded here as data, not code, because --- the directive says the method may change. - -CREATE TABLE IF NOT EXISTS public.tuition_adjustment_rules ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - rule_type TEXT NOT NULL, - - campus_id UUID REFERENCES public.campuses(id) ON DELETE CASCADE, - tier_id UUID REFERENCES public.tuition_tiers(id) ON DELETE CASCADE, - requires_support_student BOOLEAN, - requires_tag_slug TEXT, - - amount_basis TEXT NOT NULL, - amount_cents INTEGER, - percent NUMERIC(5,2), - - effective_start DATE NOT NULL DEFAULT CURRENT_DATE, - effective_end DATE, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - priority INTEGER NOT NULL DEFAULT 100, - notes TEXT, - - created_by UUID REFERENCES auth.users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - CONSTRAINT tar_type_valid CHECK (rule_type IN - ('support_charge','early_dropoff','late_pickup','discount','surcharge')), - CONSTRAINT tar_basis_valid CHECK (amount_basis IN - ('flat','per_day','per_week','percent','additional_week')), - -- A percent rule needs a percent; every other basis needs an amount. - CONSTRAINT tar_amount_present CHECK ( - (amount_basis = 'percent' AND percent IS NOT NULL) - OR (amount_basis <> 'percent' AND amount_cents IS NOT NULL) - ), - CONSTRAINT tar_dates_ordered CHECK (effective_end IS NULL OR effective_start <= effective_end) -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.tuition_adjustment_rules TO authenticated; -GRANT ALL ON public.tuition_adjustment_rules TO service_role; -ALTER TABLE public.tuition_adjustment_rules ENABLE ROW LEVEL SECURITY; -CREATE INDEX IF NOT EXISTS tar_campus_idx ON public.tuition_adjustment_rules (campus_id); -CREATE INDEX IF NOT EXISTS tar_tier_idx ON public.tuition_adjustment_rules (tier_id); -CREATE INDEX IF NOT EXISTS tar_active_idx ON public.tuition_adjustment_rules (rule_type, priority DESC) - WHERE is_active; - -DROP TRIGGER IF EXISTS trg_tar_upd ON public.tuition_adjustment_rules; -CREATE TRIGGER trg_tar_upd BEFORE UPDATE ON public.tuition_adjustment_rules - FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); - --- The spec's initial support rule: Enterprise campus, support students, an --- additional week of tuition. amount_cents is 0 because the school has not --- stated the figure — an administrator sets it before the rule has any effect. -INSERT INTO public.tuition_adjustment_rules - (name, rule_type, campus_id, requires_support_student, amount_basis, amount_cents, notes) -SELECT - 'Enterprise support-student additional week', - 'support_charge', - c.id, - TRUE, - 'additional_week', - 0, - 'Spec section 8: Enterprise campus support students carry an additional week ' - 'of tuition. Set amount_cents, or leave 0 to bill one extra week at the ' - 'student''s resolved weekly rate.' -FROM public.campuses c -WHERE c.name = 'Enterprise' - AND NOT EXISTS ( - SELECT 1 FROM public.tuition_adjustment_rules r - WHERE r.name = 'Enterprise support-student additional week' - ); - --- ============================================================================ --- 5. INDIVIDUAL TUITION ASSIGNMENT --- ============================================================================ --- The per-student override, with the approval and provenance trail the spec --- lists field by field. - -CREATE TABLE IF NOT EXISTS public.student_tuition_assignments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE, - campus_id UUID REFERENCES public.campuses(id) ON DELETE SET NULL, - - amount_cents INTEGER NOT NULL CHECK (amount_cents >= 0), - frequency TEXT NOT NULL DEFAULT 'per_week', - - start_date DATE NOT NULL DEFAULT CURRENT_DATE, - end_date DATE, - - reason TEXT NOT NULL, - document_path TEXT, - - approved_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - approved_at TIMESTAMPTZ, - - created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - last_modified_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - CONSTRAINT sta_frequency_valid - CHECK (frequency IN ('per_day','per_week','per_month','flat')), - CONSTRAINT sta_dates_ordered CHECK (end_date IS NULL OR start_date <= end_date) -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.student_tuition_assignments TO authenticated; -GRANT ALL ON public.student_tuition_assignments TO service_role; -ALTER TABLE public.student_tuition_assignments ENABLE ROW LEVEL SECURITY; -CREATE INDEX IF NOT EXISTS stat_student_idx ON public.student_tuition_assignments (student_id, start_date DESC); -CREATE INDEX IF NOT EXISTS stat_campus_idx ON public.student_tuition_assignments (campus_id); - -DROP TRIGGER IF EXISTS trg_stat_upd ON public.student_tuition_assignments; -CREATE TRIGGER trg_stat_upd BEFORE UPDATE ON public.student_tuition_assignments - FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); - --- ============================================================================ --- 6. SCHOLARSHIPS (Step Up) --- ============================================================================ - -CREATE TABLE IF NOT EXISTS public.scholarships ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE, - program_name TEXT NOT NULL DEFAULT 'Step Up For Students', - external_reference TEXT, - - award_amount_cents INTEGER NOT NULL CHECK (award_amount_cents >= 0), - frequency TEXT NOT NULL DEFAULT 'per_week', - - funding_period_start DATE NOT NULL DEFAULT CURRENT_DATE, - funding_period_end DATE, - - status TEXT NOT NULL DEFAULT 'active', - document_path TEXT, - notes TEXT, - - created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - CONSTRAINT sch_frequency_valid CHECK (frequency IN ('per_day','per_week','per_month','flat')), - CONSTRAINT sch_status_valid CHECK (status IN ('pending','active','suspended','ended')), - CONSTRAINT sch_dates_ordered - CHECK (funding_period_end IS NULL OR funding_period_start <= funding_period_end) -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.scholarships TO authenticated; -GRANT ALL ON public.scholarships TO service_role; -ALTER TABLE public.scholarships ENABLE ROW LEVEL SECURITY; -CREATE INDEX IF NOT EXISTS sch_student_idx ON public.scholarships (student_id); -CREATE INDEX IF NOT EXISTS sch_active_idx ON public.scholarships (student_id, funding_period_start) - WHERE status = 'active'; - -DROP TRIGGER IF EXISTS trg_sch_upd ON public.scholarships; -CREATE TRIGGER trg_sch_upd BEFORE UPDATE ON public.scholarships - FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); - --- ============================================================================ --- 7. PAYMENT PLANS --- ============================================================================ - -CREATE TABLE IF NOT EXISTS public.payment_plans ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - household_id UUID REFERENCES public.households(id) ON DELETE CASCADE, - student_id UUID REFERENCES public.students(id) ON DELETE CASCADE, - - name TEXT NOT NULL, - installment_amount_cents INTEGER NOT NULL CHECK (installment_amount_cents >= 0), - frequency TEXT NOT NULL DEFAULT 'weekly', - day_of_period SMALLINT, - - start_date DATE NOT NULL DEFAULT CURRENT_DATE, - end_date DATE, - status TEXT NOT NULL DEFAULT 'active', - notes TEXT, - - created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - CONSTRAINT pp_frequency_valid CHECK (frequency IN ('weekly','biweekly','monthly')), - CONSTRAINT pp_status_valid CHECK (status IN ('active','paused','completed','cancelled')), - CONSTRAINT pp_dates_ordered CHECK (end_date IS NULL OR start_date <= end_date), - -- Must attach to something. - CONSTRAINT pp_scope_present CHECK (household_id IS NOT NULL OR student_id IS NOT NULL) -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.payment_plans TO authenticated; -GRANT ALL ON public.payment_plans TO service_role; -ALTER TABLE public.payment_plans ENABLE ROW LEVEL SECURITY; -CREATE INDEX IF NOT EXISTS pplan_household_idx ON public.payment_plans (household_id); -CREATE INDEX IF NOT EXISTS pplan_student_idx ON public.payment_plans (student_id); - -DROP TRIGGER IF EXISTS trg_pplan_upd ON public.payment_plans; -CREATE TRIGGER trg_pplan_upd BEFORE UPDATE ON public.payment_plans - FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); - -ALTER TABLE public.students - ADD COLUMN IF NOT EXISTS payment_plan_id UUID REFERENCES public.payment_plans(id) ON DELETE SET NULL; -CREATE INDEX IF NOT EXISTS students_payment_plan_idx ON public.students (payment_plan_id); - --- ============================================================================ --- 8. RATE RESOLUTION --- ============================================================================ - --- Scheduled weekday count for a student across a period, campus by campus. --- The billing engine multiplies this by the resolved per-day rate. -CREATE OR REPLACE FUNCTION public.student_scheduled_days(_student UUID, _from DATE, _to DATE) -RETURNS TABLE (campus_id UUID, days_per_week SMALLINT, weeks NUMERIC, total_days NUMERIC) -LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public -AS $$ - SELECT - sc.campus_id, - sc.scheduled_days_per_week, - -- Fractional weeks, so a partial billing period bills proportionally. - ROUND(((LEAST(_to, COALESCE(sc.effective_end, _to)) - - GREATEST(_from, sc.effective_start) + 1)::numeric / 7.0), 4) AS weeks, - ROUND(((LEAST(_to, COALESCE(sc.effective_end, _to)) - - GREATEST(_from, sc.effective_start) + 1)::numeric / 7.0) - * sc.scheduled_days_per_week, 4) AS total_days - FROM public.student_campus_schedules sc - WHERE sc.student_id = _student - AND sc.effective_start <= _to - AND (sc.effective_end IS NULL OR sc.effective_end >= _from) - AND sc.scheduled_days_per_week > 0 -$$; - --- Best-matching rate for a student at a campus on a date. NULL columns on the --- rate mean "any", so specificity is expressed by priority, then by how --- recently the rate took effect. -CREATE OR REPLACE FUNCTION public.resolve_tuition_rate( - _student UUID, _campus UUID, _on DATE, _days SMALLINT DEFAULT NULL -) -RETURNS public.tuition_rates -LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public -AS $$ - SELECT r.* - FROM public.tuition_rates r - JOIN public.students s ON s.id = _student - WHERE r.is_active - AND r.effective_start <= _on - AND (r.effective_end IS NULL OR r.effective_end >= _on) - AND (r.tier_id IS NULL OR r.tier_id = s.tuition_tier_id) - AND (r.campus_id IS NULL OR r.campus_id = _campus) - AND (r.attendance_basis IS NULL OR r.attendance_basis = s.attendance_basis) - AND (r.requires_support_student IS NULL OR r.requires_support_student = s.is_support_student) - AND (r.requires_scholarship IS NULL OR r.requires_scholarship = EXISTS ( - SELECT 1 FROM public.scholarships sh - WHERE sh.student_id = _student AND sh.status = 'active' - AND sh.funding_period_start <= _on - AND (sh.funding_period_end IS NULL OR sh.funding_period_end >= _on))) - AND (_days IS NULL OR r.min_days IS NULL OR _days >= r.min_days) - AND (_days IS NULL OR r.max_days IS NULL OR _days <= r.max_days) - ORDER BY r.priority DESC, r.effective_start DESC - LIMIT 1 -$$; - --- Active per-student override for a date, if any. -CREATE OR REPLACE FUNCTION public.resolve_tuition_override(_student UUID, _on DATE) -RETURNS public.student_tuition_assignments -LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public -AS $$ - SELECT a.* FROM public.student_tuition_assignments a - WHERE a.student_id = _student - AND a.start_date <= _on - AND (a.end_date IS NULL OR a.end_date >= _on) - ORDER BY a.start_date DESC - LIMIT 1 -$$; - --- ============================================================================ --- 9. POLICIES --- ============================================================================ --- Rate cards are readable by staff who bill or administrate; per-student money --- is additionally readable by that student's guardians. - -DROP POLICY IF EXISTS "tiers read" ON public.tuition_tiers; -CREATE POLICY "tiers read" ON public.tuition_tiers FOR SELECT TO authenticated USING (TRUE); -DROP POLICY IF EXISTS "tiers manage" ON public.tuition_tiers; -CREATE POLICY "tiers manage" ON public.tuition_tiers FOR ALL TO authenticated - USING (public.is_billing_admin()) WITH CHECK (public.is_billing_admin()); - -DROP POLICY IF EXISTS "rates read" ON public.tuition_rates; -CREATE POLICY "rates read" ON public.tuition_rates FOR SELECT TO authenticated - USING (public.is_billing_admin() OR public.is_management() OR public.is_auditor() - OR public.has_campus_access(campus_id)); -DROP POLICY IF EXISTS "rates manage" ON public.tuition_rates; -CREATE POLICY "rates manage" ON public.tuition_rates FOR ALL TO authenticated - USING (public.is_billing_admin()) WITH CHECK (public.is_billing_admin()); - -DROP POLICY IF EXISTS "adj rules read" ON public.tuition_adjustment_rules; -CREATE POLICY "adj rules read" ON public.tuition_adjustment_rules FOR SELECT TO authenticated - USING (public.is_billing_admin() OR public.is_management() OR public.is_auditor()); -DROP POLICY IF EXISTS "adj rules manage" ON public.tuition_adjustment_rules; -CREATE POLICY "adj rules manage" ON public.tuition_adjustment_rules FOR ALL TO authenticated - USING (public.is_billing_admin()) WITH CHECK (public.is_billing_admin()); - -DROP POLICY IF EXISTS "student tuition read" ON public.student_tuition_assignments; -CREATE POLICY "student tuition read" ON public.student_tuition_assignments FOR SELECT TO authenticated - USING (public.is_billing_admin() OR public.is_auditor() OR public.is_parent_of(student_id)); -DROP POLICY IF EXISTS "student tuition manage" ON public.student_tuition_assignments; -CREATE POLICY "student tuition manage" ON public.student_tuition_assignments FOR ALL TO authenticated - USING (public.is_billing_admin()) WITH CHECK (public.is_billing_admin()); - -DROP POLICY IF EXISTS "scholarships read" ON public.scholarships; -CREATE POLICY "scholarships read" ON public.scholarships FOR SELECT TO authenticated - USING (public.is_billing_admin() OR public.is_auditor() OR public.is_parent_of(student_id)); -DROP POLICY IF EXISTS "scholarships manage" ON public.scholarships; -CREATE POLICY "scholarships manage" ON public.scholarships FOR ALL TO authenticated - USING (public.is_billing_admin()) WITH CHECK (public.is_billing_admin()); - -DROP POLICY IF EXISTS "payment plans read" ON public.payment_plans; -CREATE POLICY "payment plans read" ON public.payment_plans FOR SELECT TO authenticated - USING ( - public.is_billing_admin() OR public.is_auditor() - OR (student_id IS NOT NULL AND public.is_parent_of(student_id)) - OR (household_id IS NOT NULL AND household_id IN (SELECT public.user_household_ids())) - ); -DROP POLICY IF EXISTS "payment plans manage" ON public.payment_plans; -CREATE POLICY "payment plans manage" ON public.payment_plans FOR ALL TO authenticated - USING (public.is_billing_admin()) WITH CHECK (public.is_billing_admin());