From b07777befd4f1fbf9d8088edbbf9b59804960959 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 7 Aug 2026 14:03:42 +0000 Subject: [PATCH] Modified by www.SourceFiles.app --- ...7001800_billing_and_attendance_reports.sql | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 supabase/migrations/20260807001800_billing_and_attendance_reports.sql diff --git a/supabase/migrations/20260807001800_billing_and_attendance_reports.sql b/supabase/migrations/20260807001800_billing_and_attendance_reports.sql new file mode 100644 index 0000000..639532f --- /dev/null +++ b/supabase/migrations/20260807001800_billing_and_attendance_reports.sql @@ -0,0 +1,224 @@ +-- Paid/unpaid by campus and attendance reporting — spec sections 14 and 19. +-- +-- Both sections ask for the same shape: a total you can drill into, filtered by +-- campus, date range, tier, full/part-time, scholarship and plan status. These +-- views therefore expose the raw filter columns rather than pre-aggregating, +-- so one view serves the dashboard tile and the drill-down behind it. +-- +-- security_invoker throughout: a campus administrator running the same query +-- sees only their campus, because the underlying policies still apply. + +-- ============================================================================ +-- 1. SECTION 14 — receivables +-- ============================================================================ + +-- Row per invoice with every documented filter attached. The dashboard totals +-- below are sums over this; clicking a total means selecting from it. +CREATE OR REPLACE VIEW public.v_billing_detail +WITH (security_invoker = true) AS +SELECT + i.id AS invoice_id, + i.invoice_number, + i.student_id, + s.first_name || ' ' || s.last_name AS student_name, + i.household_id, + h.name AS household_name, + i.campus_id, + c.name AS campus_name, + i.billing_period_start, + i.billing_period_end, + -- ISO week, for the spec's "billing week" filter. + to_char(i.billing_period_start, 'IYYY-"W"IW') AS billing_week, + i.invoice_date, + i.due_date, + i.tuition_tier_id, + t.name AS tuition_tier, + i.attendance_basis, + i.status, + i.total_cents, + i.amount_paid_cents, + i.balance_due_cents, + i.scholarship_cents, + i.penalties_cents, + i.payment_plan_id, + (i.payment_plan_id IS NOT NULL) AS on_payment_plan, + (i.scholarship_cents > 0) AS has_scholarship, + (i.status IN ('issued','partially_paid') AND i.balance_due_cents > 0 + AND i.due_date < CURRENT_DATE) AS is_past_due, + GREATEST(CURRENT_DATE - i.due_date, 0) AS days_overdue +FROM public.invoices i +JOIN public.students s ON s.id = i.student_id +LEFT JOIN public.households h ON h.id = i.household_id +LEFT JOIN public.campuses c ON c.id = i.campus_id +LEFT JOIN public.tuition_tiers t ON t.id = i.tuition_tier_id +WHERE i.status <> 'void'; + +GRANT SELECT ON public.v_billing_detail TO authenticated; + +-- The campus dashboard tiles from section 14. +CREATE OR REPLACE VIEW public.v_campus_billing_summary +WITH (security_invoker = true) AS +SELECT + c.id AS campus_id, + c.name AS campus_name, + COUNT(d.invoice_id) AS invoice_count, + COALESCE(SUM(d.total_cents), 0) AS total_invoiced_cents, + COALESCE(SUM(d.amount_paid_cents), 0) AS total_collected_cents, + COALESCE(SUM(d.balance_due_cents), 0) AS total_unpaid_cents, + COALESCE(SUM(d.balance_due_cents) FILTER (WHERE d.is_past_due), 0) AS past_due_cents, + COALESCE(SUM(d.scholarship_cents), 0) AS scholarship_applied_cents, + COALESCE(SUM(d.balance_due_cents) FILTER (WHERE d.on_payment_plan), 0) + AS payment_plan_balance_cents, + COUNT(DISTINCT d.student_id) FILTER (WHERE d.is_past_due) AS delinquent_accounts, + -- Awarded scholarship funding that has not yet been received as a payment. + COALESCE(( + SELECT SUM(sc.award_amount_cents) + FROM public.scholarships sc + JOIN public.students st ON st.id = sc.student_id + WHERE st.primary_campus_id = c.id + AND sc.status = 'active' + AND NOT EXISTS ( + SELECT 1 FROM public.payments p + WHERE p.scholarship_id = sc.id AND p.status = 'posted') + ), 0) AS scholarship_pending_cents +FROM public.campuses c +LEFT JOIN public.v_billing_detail d ON d.campus_id = c.id +GROUP BY c.id, c.name; + +GRANT SELECT ON public.v_campus_billing_summary TO authenticated; + +-- Household-level position, since a family pays as one even when billed per child. +CREATE OR REPLACE VIEW public.v_household_receivables +WITH (security_invoker = true) AS +SELECT + d.household_id, + d.household_name, + COUNT(DISTINCT d.student_id) AS students, + SUM(d.total_cents) AS invoiced_cents, + SUM(d.amount_paid_cents) AS paid_cents, + SUM(d.balance_due_cents) AS balance_cents, + SUM(d.balance_due_cents) FILTER (WHERE d.is_past_due) AS past_due_cents, + MAX(d.days_overdue) FILTER (WHERE d.is_past_due) AS worst_days_overdue +FROM public.v_billing_detail d +WHERE d.household_id IS NOT NULL +GROUP BY d.household_id, d.household_name; + +GRANT SELECT ON public.v_household_receivables TO authenticated; + +-- ============================================================================ +-- 2. SECTION 19 — attendance reporting +-- ============================================================================ + +-- One row per attendance record with everything section 19 asks to report on, +-- including whether the day was a campus transfer against the schedule. +CREATE OR REPLACE VIEW public.v_attendance_detail +WITH (security_invoker = true) AS +SELECT + a.id AS attendance_id, + a.student_id, + s.first_name || ' ' || s.last_name AS student_name, + s.attendance_basis, + s.calendar_basis, + a.date, + to_char(a.date, 'IYYY-"W"IW') AS iso_week, + date_trunc('month', a.date)::date AS month, + a.campus_id AS actual_campus_id, + c.name AS actual_campus, + s.primary_campus_id, + pc.name AS primary_campus, + -- A day where the child was somewhere other than their usual campus. + (a.campus_id IS DISTINCT FROM s.primary_campus_id) AS campus_transfer, + s.class_id, + cl.name AS class_name, + cl.teacher_id, + a.status, + a.was_scheduled, + a.check_in_at, + a.check_out_at, + a.early_arrival, + a.late_arrival, + a.early_pickup, + a.late_pickup, + a.is_manual_override, + a.absence_reason, + -- Fees raised for that day, so charges reconcile against the day that caused them. + COALESCE(( + SELECT SUM(e.final_fee_cents) FROM public.penalty_events e + WHERE e.attendance_id = a.id AND NOT e.is_waived + ), 0) AS fee_cents +FROM public.attendance a +JOIN public.students s ON s.id = a.student_id +LEFT JOIN public.campuses c ON c.id = a.campus_id +LEFT JOIN public.campuses pc ON pc.id = s.primary_campus_id +LEFT JOIN public.classes cl ON cl.id = s.class_id; + +GRANT SELECT ON public.v_attendance_detail TO authenticated; + +-- Daily campus roll-up: the "attendance today" tile and the daily report. +CREATE OR REPLACE VIEW public.v_attendance_by_campus_day +WITH (security_invoker = true) AS +SELECT + d.actual_campus_id AS campus_id, + d.actual_campus AS campus_name, + d.date, + COUNT(*) FILTER (WHERE d.was_scheduled) AS scheduled, + COUNT(*) FILTER (WHERE d.status IN ('present','late')) AS present, + COUNT(*) FILTER (WHERE d.status = 'absent') AS absent, + COUNT(*) FILTER (WHERE d.status = 'vacation') AS on_vacation, + COUNT(*) FILTER (WHERE d.status = 'excused') AS excused, + COUNT(*) FILTER (WHERE d.late_arrival) AS late_arrivals, + COUNT(*) FILTER (WHERE d.early_arrival) AS early_arrivals, + COUNT(*) FILTER (WHERE d.early_pickup) AS early_pickups, + COUNT(*) FILTER (WHERE d.late_pickup) AS late_pickups, + COUNT(*) FILTER (WHERE d.campus_transfer) AS campus_transfers, + COUNT(*) FILTER (WHERE d.is_manual_override) AS manual_overrides, + COALESCE(SUM(d.fee_cents), 0) AS fee_cents, + ROUND(COUNT(*) FILTER (WHERE d.status IN ('present','late'))::numeric * 100.0 + / NULLIF(COUNT(*) FILTER (WHERE d.was_scheduled), 0), 1) AS attendance_rate_percent +FROM public.v_attendance_detail d +GROUP BY d.actual_campus_id, d.actual_campus, d.date; + +GRANT SELECT ON public.v_attendance_by_campus_day TO authenticated; + +-- Scheduled versus actual, which section 19 calls out specifically. +CREATE OR REPLACE VIEW public.v_scheduled_vs_actual +WITH (security_invoker = true) AS +SELECT + d.student_id, + d.student_name, + d.attendance_basis, + d.primary_campus_id, + d.month, + COUNT(*) FILTER (WHERE d.was_scheduled) AS days_scheduled, + COUNT(*) FILTER (WHERE d.was_scheduled AND d.status IN ('present','late')) + AS scheduled_and_present, + COUNT(*) FILTER (WHERE NOT d.was_scheduled AND d.status IN ('present','late')) + AS unscheduled_but_present, + COUNT(*) FILTER (WHERE d.was_scheduled AND d.status = 'absent') AS scheduled_but_absent, + COUNT(*) FILTER (WHERE d.status = 'vacation') AS vacation_days +FROM public.v_attendance_detail d +GROUP BY d.student_id, d.student_name, d.attendance_basis, d.primary_campus_id, d.month; + +GRANT SELECT ON public.v_scheduled_vs_actual TO authenticated; + +-- Vacation position per student, for the balance report. +CREATE OR REPLACE VIEW public.v_vacation_status +WITH (security_invoker = true) AS +SELECT + b.student_id, + s.first_name || ' ' || s.last_name AS student_name, + s.primary_campus_id, + c.name AS campus_name, + b.policy_year, + b.weeks_allotted, + b.weeks_used, + b.weeks_remaining, + b.override_weeks IS NOT NULL AS is_overridden, + (SELECT COUNT(*) FROM public.vacation_requests r + WHERE r.student_id = b.student_id AND r.policy_year = b.policy_year + AND r.status = 'pending') AS pending_requests +FROM public.vacation_balances b +JOIN public.students s ON s.id = b.student_id +LEFT JOIN public.campuses c ON c.id = s.primary_campus_id; + +GRANT SELECT ON public.v_vacation_status TO authenticated;