Modified by www.SourceFiles.app
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
-- Auto-charge tuition when a student is marked present or late.
|
||||
--
|
||||
-- Idempotency is the whole problem here. The attendance UI upserts on
|
||||
-- (student_id, date) and teachers freely toggle a status — present, then absent,
|
||||
-- then present again. A naive "insert a charge on attendance" trigger bills the
|
||||
-- family once per click. So each auto-charge is bound to the attendance row that
|
||||
-- caused it via ledger_entries.attendance_id (UNIQUE), which makes the write an
|
||||
-- upsert and lets a status change back to absent *remove* the charge.
|
||||
--
|
||||
-- Manual ledger entries keep attendance_id NULL. Postgres allows unlimited NULLs
|
||||
-- in a unique index, so hand-entered charges are unaffected, and the UI can tell
|
||||
-- auto from manual by whether attendance_id is set.
|
||||
|
||||
-- Per-student daily rate. NULL (the default) means "never auto-charge", so
|
||||
-- nothing starts billing until a rate is deliberately set on a student.
|
||||
ALTER TABLE public.students
|
||||
ADD COLUMN IF NOT EXISTS daily_tuition_cents INTEGER;
|
||||
|
||||
ALTER TABLE public.ledger_entries
|
||||
ADD COLUMN IF NOT EXISTS attendance_id UUID REFERENCES public.attendance(id) ON DELETE CASCADE;
|
||||
|
||||
-- One auto-charge per attendance record, and deleting the attendance record
|
||||
-- takes its charge with it (ON DELETE CASCADE above).
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE public.ledger_entries ADD CONSTRAINT ledger_entries_attendance_id_key UNIQUE (attendance_id);
|
||||
EXCEPTION WHEN duplicate_table OR duplicate_object THEN NULL; END $$;
|
||||
|
||||
-- SECURITY DEFINER because the writer is a *teacher* marking attendance, and
|
||||
-- ledger_entries is admin-write under RLS. The teacher never gains general
|
||||
-- ledger access — only this function's fixed, attendance-derived write.
|
||||
CREATE OR REPLACE FUNCTION public.sync_attendance_tuition()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
|
||||
DECLARE
|
||||
rate INTEGER;
|
||||
BEGIN
|
||||
SELECT daily_tuition_cents INTO rate FROM public.students WHERE id = NEW.student_id;
|
||||
|
||||
-- 'present' and 'late' are billable: the student attended either way.
|
||||
-- 'absent' and 'excused' are not.
|
||||
IF NEW.status IN ('present', 'late') AND rate IS NOT NULL AND rate > 0 THEN
|
||||
INSERT INTO public.ledger_entries
|
||||
(student_id, date, kind, category, amount_cents, note, created_by, attendance_id)
|
||||
VALUES
|
||||
(NEW.student_id, NEW.date, 'charge', 'tuition', rate,
|
||||
'Auto-charged from attendance (' || NEW.status || ')', NEW.recorded_by, NEW.id)
|
||||
ON CONFLICT (attendance_id) DO UPDATE
|
||||
SET amount_cents = EXCLUDED.amount_cents,
|
||||
date = EXCLUDED.date,
|
||||
note = EXCLUDED.note;
|
||||
ELSE
|
||||
-- Status moved to absent/excused, or the rate was cleared: undo the charge.
|
||||
DELETE FROM public.ledger_entries WHERE attendance_id = NEW.id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_attendance_tuition ON public.attendance;
|
||||
CREATE TRIGGER trg_attendance_tuition
|
||||
AFTER INSERT OR UPDATE ON public.attendance
|
||||
FOR EACH ROW EXECUTE FUNCTION public.sync_attendance_tuition();
|
||||
|
||||
-- Deliberately NOT backfilled. Existing attendance records stay unbilled —
|
||||
-- retroactively generating charges against families' balances should be an
|
||||
-- explicit decision, not a side effect of deploying this migration. Re-saving an
|
||||
-- attendance record re-fires the trigger and bills that day at the current rate.
|
||||
Reference in New Issue
Block a user