-- Two honesty fixes to the section 14 reporting views. -- -- Both are about a number that reads as a fact when it is really an absence. -- Neither changes what anyone is permitted to see. -- ============================================================================ -- 1. A household with nothing past due owes zero, not "unknown" -- ============================================================================ -- -- SUM(...) FILTER over no matching rows returns NULL, so past_due_cents came -- back NULL for any household that is simply up to date. v_campus_billing_summary -- already wraps the identical expression in COALESCE; this brings the household -- view into line so callers can add the two together without special-casing. -- -- worst_days_overdue is deliberately left NULL: there is no meaningful "zero -- days overdue" for an invoice that was never overdue, and 0 would read as -- "due today". 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, COALESCE(SUM(d.balance_due_cents) FILTER (WHERE d.is_past_due), 0) 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. Unfunded scholarship money: absent is not the same as zero -- ============================================================================ -- -- scholarship_pending_cents reads public.scholarships and public.payments, -- whose RLS admits only billing admins, auditors and a student's own parents. -- Under security_invoker a campus administrator's subquery therefore matched -- nothing and the tile rendered $0 — indistinguishable from "all funding -- received", which is the opposite of the truth and the more reassuring of the -- two readings. -- -- The column now returns NULL for anyone who cannot see scholarship data, so -- the dashboard can render "—" and say nothing rather than something false. -- The gate mirrors the scholarships SELECT policy exactly; widening who may see -- this number is a policy decision, not a reporting one, and is not made here. 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, CASE WHEN public.is_billing_admin() OR public.is_auditor() THEN 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) ELSE NULL END 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;