Setpoint Academy proposal · Working fileAll deliverables · Scope · Mockups

Target model (shared source for scope, spec, mockups, red team)

Written 16 Sep 2026. Everything here must map onto the real Setpoint schema confirmed read-only on local Supabase (177 tables) and the 10 Sep 2026 exports. Column names are the real ones.

1. Subscription

2. Session ledger: reservation separated from consumption

Today: create_lesson_atomic calls consume_package_session when a player is added (deduct on add). refund_package_session restores on cancellation. lesson_players.status = confirmed | held | cancelled. lesson_players.checked_in_at and attended exist but never touch the ledger. package_session_transactions.reason CHECK = purchase_grant | consume | refund | admin_adjust | backfill_balance | renewal_reset | expiry_forfeit | cancellation_forfeit; balance_after >= 0; source = live | backfill.

Target: a per-player attendance state on lesson_players (new column attendance_state, CHECK) and a reservation that does not deduct.

attendance_state Meaning Ledger effect Who sets it
scheduled on a future roster reserved (no consume row); shown as Reserved roster add (auto from group, or manual)
attended checked in or marked done consume row, delta −1 coach check-in (PWA QR), manager "mark done", or auto at lesson end + N hours if the facility opts in
no_show fixed slot missed consume row, delta −1, metadata.attendance='no_show' coach / manager "no-show", or auto when a lesson completes with no check-in (setting)
cancelled_by_academy lesson cancelled or seat released by staff released: no row, or refund if already consumed lesson cancel cascade, roster remove
cancelled_by_student student cancelled group: consume (slot lost); individual: released if > free_cancellation_hours (24) else consume student (web/app) or staff on their behalf

Balance = purchased − Σ consume rows (attended + no_show + student-cancel per rule) ± adjustments. Reserved = seats with attendance_state='scheduled' whose player_package_id points at the package: a locked fact written at insert (red team), capped so reserved + consumed ≤ purchased, and get_player_package_remaining_sessions becomes reservation-aware before any flag flips. Shown side by side: "6 · 2 used · 3 reserved · 1 free".

Ledger reasons: keep ONE consume reason and record the flavour in metadata.attendance (attended | no_show | student_cancel); no delta-0 rows (red team). Add ledger_mode_switch (with an inverse) for the one-time switch. Existing refund stays for staff reversals. Because package_session_transactions is append-only with balance_after, a reservation must never write a negative or provisional row.

Migration path from deduct-on-add:

  1. Stop the deduction at add: create_lesson_atomic and the group-seat confirm RPCs stop calling consume_package_session; they set attendance_state='scheduled' instead (behind an ACADEMY-level flag academies.ledger_mode = 'on_add' | 'on_done', default on_add so no live club changes; red team: a facility flag splits a student who trains at two sites).
  2. One-time reconciliation for Pad Thai Padel when the flag flips: for every consume row whose lesson is still in the future → insert a compensating refund row (source='backfill', note "ledger_mode switch") and set the seat to scheduled; for past lessons: lesson status='cancelled' → cancelled_by_academy (+ refund if consumed); otherwise → attended (never inferred no_show: only 52 of 698 seats were ever checked in, so absence of a check-in proves nothing); rows use reason ledger_mode_switch and keep the package link; dry-run report reviewed by Jose first.
  3. Exhaustion check (check_package_exhaustion) moves from add-time to done-time; over-reservation is allowed up to purchased sessions + 0 (setting allow_overbook_reserved default false): a student with 4 sessions can hold at most 4 reserved-or-consumed seats in the period.

3. Roster follows package

4. Month-end billing

5. Settings (facility-level, academy_payment_settings or a new academy_settings row)

ledger_mode (on_add | on_done), auto_mark_no_show_hours (null = manual), student_cancel_rule_group (deduct | release_if_24h), student_cancel_rule_individual (release_if_24h), roster_grace_days (7), pause_notice_day (26), freeze_day (7), past_due_day (3), reminder_day_before_1st (26), allow_overbook_reserved (false), proration_mode (none | weekly_quarters).