174 lines
5.8 KiB
SQL
174 lines
5.8 KiB
SQL
-- 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;
|