From 0f8aa47f3a54ad04a282ffe384de071208507d43 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 7 Aug 2026 20:55:27 +0000 Subject: [PATCH] Terminal --- ...0807002100_billing_views_student_names.sql | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 supabase/migrations/20260807002100_billing_views_student_names.sql diff --git a/supabase/migrations/20260807002100_billing_views_student_names.sql b/supabase/migrations/20260807002100_billing_views_student_names.sql new file mode 100644 index 0000000..15d5a7f --- /dev/null +++ b/supabase/migrations/20260807002100_billing_views_student_names.sql @@ -0,0 +1,173 @@ +-- The rest of the billing views had the same fault as v_billing_detail. +-- +-- 20260807002000 fixed v_billing_detail, but three sibling views still inner +-- joined public.students purely to build a display name. A billing administrator +-- can read every invoice, payment and ledger entry in the organisation and no +-- student rows at all, so that join silently emptied each of them: +-- +-- v_outstanding_invoices 0 rows, while public.invoices showed 5 +-- v_student_ledger 0 rows, while public.payments showed 1 +-- +-- Both now resolve the name through student_display_name(), which re-checks +-- entitlement inside its SECURITY DEFINER body. Student row access is unchanged; +-- only the name lookup moved. v_payment_detail is added for the same reason — +-- the receivables screen was reaching for payments.students(...) directly and +-- rendering "—" for every payer. + +-- ============================================================================ +-- 1. Outstanding invoices +-- ============================================================================ +CREATE OR REPLACE VIEW public.v_outstanding_invoices +WITH (security_invoker = true) AS +SELECT + i.id AS invoice_id, + i.invoice_number, + i.student_id, + public.student_display_name(i.student_id) AS student_name, + i.household_id, + i.campus_id, + c.name AS campus_name, + i.billing_period_start, + i.billing_period_end, + i.invoice_date, + i.due_date, + i.total_cents, + i.amount_paid_cents, + i.balance_due_cents, + i.status, + CURRENT_DATE - i.due_date AS days_overdue +FROM public.invoices i +LEFT JOIN public.campuses c ON c.id = i.campus_id +WHERE i.status = ANY (ARRAY['issued'::text, 'partially_paid'::text]) + AND i.balance_due_cents > 0; + +GRANT SELECT ON public.v_outstanding_invoices TO authenticated; + +-- ============================================================================ +-- 2. Payments, with the payer named +-- ============================================================================ +CREATE OR REPLACE VIEW public.v_payment_detail +WITH (security_invoker = true) AS +SELECT + p.id AS payment_id, + p.student_id, + public.student_display_name(p.student_id) AS student_name, + p.household_id, + p.campus_id, + c.name AS campus_name, + p.kind, + p.method, + p.amount_cents, + p.received_at, + p.effective_date, + p.reference_number, + p.payer_name, + p.third_party_name, + p.scholarship_id, + p.status, + p.reverses_payment_id, + p.void_reason, + p.notes, + p.received_by +FROM public.payments p +LEFT JOIN public.campuses c ON c.id = p.campus_id; + +GRANT SELECT ON public.v_payment_detail TO authenticated; + +-- ============================================================================ +-- 3. Student ledger +-- ============================================================================ +-- Reproduced from the deployed definition with only the trailing students join +-- removed, so the running-balance window and the four-way UNION behind it are +-- untouched. +CREATE OR REPLACE VIEW public.v_student_ledger +WITH (security_invoker = true) AS + WITH events AS ( + SELECT i.student_id, + i.campus_id, + i.invoice_date AS txn_date, + i.billing_period_start AS effective_date, + i.created_at AS sort_ts, + 'invoice'::text AS source, + 'charge'::text AS direction, + i.total_cents AS amount_cents, + i.invoice_number AS reference, + NULL::text AS method, + i.issued_by AS entered_by, + i.id AS ref_id + FROM invoices i + WHERE i.status <> 'void'::text + UNION ALL + SELECT p.student_id, + p.campus_id, + p.received_at::date AS received_at, + p.effective_date, + p.created_at, + 'payment'::text, + CASE + WHEN p.kind = 'payment'::text THEN 'credit'::text + ELSE 'charge'::text + END AS "case", + CASE + WHEN p.kind = 'payment'::text THEN - p.amount_cents + ELSE p.amount_cents + END AS amount_cents, + p.reference_number, + p.method, + p.received_by, + p.id + FROM payments p + WHERE p.status <> 'voided'::text AND p.student_id IS NOT NULL + UNION ALL + SELECT c.student_id, + NULL::uuid AS uuid, + c.issued_at::date AS issued_at, + c.issued_at::date AS issued_at, + c.issued_at, + 'credit'::text, + 'credit'::text, + - c.amount_cents, + NULL::text, + NULL::text, + c.issued_by, + c.id + FROM student_credits c + WHERE NOT c.is_void + UNION ALL + SELECT l.student_id, + NULL::uuid AS uuid, + l.date, + l.date, + l.created_at, + 'ledger_entry'::text, + CASE + WHEN l.kind = 'charge'::ledger_kind THEN 'charge'::text + ELSE 'credit'::text + END AS "case", + CASE + WHEN l.kind = 'charge'::ledger_kind THEN l.amount_cents + ELSE - l.amount_cents + END AS "case", + NULL::text, + l.category::text AS category, + l.created_by, + l.id + FROM ledger_entries l + ) + SELECT e.student_id, + e.campus_id, + e.txn_date, + e.effective_date, + e.sort_ts, + e.source, + e.direction, + e.amount_cents, + e.reference, + e.method, + e.entered_by, + e.ref_id, + public.student_display_name(e.student_id) AS student_name, + sum(e.amount_cents) OVER (PARTITION BY e.student_id ORDER BY e.effective_date, e.sort_ts, e.ref_id ROWS UNBOUNDED PRECEDING) AS running_balance_cents + FROM events e; + +GRANT SELECT ON public.v_student_ledger TO authenticated;