225 lines
8.7 KiB
SQL
225 lines
8.7 KiB
SQL
-- 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;
|