Charge tuition automatically from attendance
Adds a per-student daily rate (students.daily_tuition_cents) and a trigger that writes a tuition charge when a student is marked present or late. Idempotency is the crux. The attendance UI upserts on (student_id, date) and teachers toggle a status freely — present, absent, 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 turns the write into an upsert, lets a change back to absent delete the charge, and cascades the charge away if the attendance record is deleted. Manual entries keep attendance_id NULL — Postgres allows unlimited NULLs in a unique index — so hand-entered charges are untouched and the UI can tell auto from manual. A NULL rate (the default) means never auto-charge, so nothing begins billing until a rate is deliberately set on a student. Existing attendance is not backfilled: retroactively generating charges against families' balances should be an explicit decision, not a side effect of deploying a migration. The trigger is SECURITY DEFINER because the writer is a teacher marking attendance while ledger_entries is admin-write under RLS; teachers gain no general ledger access, only this fixed attendance-derived write. Verified against the live schema across all eight paths: charge on present, reversal on absent, no duplicate on re-mark, late billable, excused free, no rate means no charge, rate change on re-save, and cascade on attendance delete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -480,6 +480,7 @@ export type Database = {
|
||||
ledger_entries: {
|
||||
Row: {
|
||||
amount_cents: number
|
||||
attendance_id: string | null
|
||||
category: Database["public"]["Enums"]["ledger_category"]
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
@@ -491,6 +492,7 @@ export type Database = {
|
||||
}
|
||||
Insert: {
|
||||
amount_cents: number
|
||||
attendance_id?: string | null
|
||||
category?: Database["public"]["Enums"]["ledger_category"]
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
@@ -502,6 +504,7 @@ export type Database = {
|
||||
}
|
||||
Update: {
|
||||
amount_cents?: number
|
||||
attendance_id?: string | null
|
||||
category?: Database["public"]["Enums"]["ledger_category"]
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
@@ -512,6 +515,13 @@ export type Database = {
|
||||
student_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "ledger_entries_attendance_id_fkey"
|
||||
columns: ["attendance_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "attendance"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "ledger_entries_student_id_fkey"
|
||||
columns: ["student_id"]
|
||||
@@ -1159,6 +1169,7 @@ export type Database = {
|
||||
class_id: string | null
|
||||
created_at: string
|
||||
custody_agreement: string | null
|
||||
daily_tuition_cents: number | null
|
||||
disciplinary_history: string | null
|
||||
dismissal_methods: string[]
|
||||
dismissal_other: string | null
|
||||
@@ -1192,6 +1203,7 @@ export type Database = {
|
||||
class_id?: string | null
|
||||
created_at?: string
|
||||
custody_agreement?: string | null
|
||||
daily_tuition_cents?: number | null
|
||||
disciplinary_history?: string | null
|
||||
dismissal_methods?: string[]
|
||||
dismissal_other?: string | null
|
||||
@@ -1225,6 +1237,7 @@ export type Database = {
|
||||
class_id?: string | null
|
||||
created_at?: string
|
||||
custody_agreement?: string | null
|
||||
daily_tuition_cents?: number | null
|
||||
disciplinary_history?: string | null
|
||||
dismissal_methods?: string[]
|
||||
dismissal_other?: string | null
|
||||
|
||||
Reference in New Issue
Block a user