This commit is contained in:
2026-08-22 23:11:07 +00:00
parent c46b45cc10
commit 0e2291e3ff
@@ -1,173 +0,0 @@
-- 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;