From 3c0f8c21ea0270da80f0c194109c4bf9131521cc Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 22 Aug 2026 23:10:54 +0000 Subject: [PATCH] Terminal --- ...26120000_attendance_tuition_autocharge.sql | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 supabase/migrations/20260726120000_attendance_tuition_autocharge.sql diff --git a/supabase/migrations/20260726120000_attendance_tuition_autocharge.sql b/supabase/migrations/20260726120000_attendance_tuition_autocharge.sql deleted file mode 100644 index 85413ed..0000000 --- a/supabase/migrations/20260726120000_attendance_tuition_autocharge.sql +++ /dev/null @@ -1,66 +0,0 @@ --- 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.