Setpoint Academy proposal · DocumentAll deliverables · Scope · Mockups

03 · Phased plan: PR-sized steps, read-only first

Extends the Academy Product Plan's four steps (Academy Health → makeups + waitlist → proration in billing → player progress) into the thirteen features A–M (M, the operations view, added 17 September). Every row is one branch, one PR, one Nestor review. Effort = focused agent hours + review hours. Nothing below starts before Nestor has approved this package (Gate N0). Assumes the defaults in 04-decisions.md.

Ordering principle

  1. Read-only pages and repairs first (they prove the numbers and cost nothing if wrong).
  2. Infrastructure the write features depend on (roles helper, audit log, settings).
  3. The ledger change, behind a facility flag that defaults to today's behaviour, switched for Pad Thai Padel on 1 November after a reviewed dry run.
  4. Money movement last, gated on Stripe live keys.
  5. Anything that needs a third party's approval (Meta) or a person's decision (Jose) sits behind that gate, not in the critical path.

Gates

Gate Owner What unlocks
N0 · Proposal approved Nestor any PR
N1 · Each PR reviewed Nestor merge + prod migration
S1 · Stripe live keys for Pad Thai Padel in Setpoint Nestor + Alvaro PR 12, 13
M1 · Meta Business verified, number, templates approved Alvaro PR 16
J1 · Jose confirms group slots, override rule, no-show rule (D7, D8, D9) Jose PR 8, PR 7 switch
J2 · Jose confirms what players see (D10) Jose PR 19
E1 · Alvaro confirms where events are created and who adds players (D19, D20) Alvaro PR 21, 22
A1 · Alvaro confirms student-cancel, grace, pause (D1–D4) Alvaro PR 10
C1 · One real billing cycle (1 Oct) checked against Step 1's worklist Alvaro + Abby PR 11
L1 · Ledger dry-run report reviewed Jose + Alvaro PR 7 switch on 1 Nov

The steps

# Branch Feature What ships Data changes Depends on Gate Effort (agent + review)
0 chore/academy-subscription-proposal — This package added to apps/docs plan pages; ac-*.ts stubs for the PRs below none — N0 2 h + 1 h
1 feature/academy-health H, G (read-only), E (worklist) Plan Step 1 as specified in PHASE-1-SPEC.md: three tabs, CSV per tab, tc-284 none 0 N1 8 h + 2 h
2 fix/lesson-patch-availability-override A (prereq) editableFields accepts availability_override; tc-285 covers the edit path none — N1 1 h + 0.5 h
3 chore/require-role-helper roles (prereq) requireRole(facility, [..]) helper; team routes gated to owner/admin; the ~20 ad-hoc constants replaced none — N1 4 h + 1 h
4 feature/admin-audit-log L admin_audit_log table + transactional write helper; wired into academy routes that already exist (groups, packages, lesson PATCH, payments revert); read-only Audit tab under Academy › Setup M-L1 3 N1 5 h + 1.5 h
5 feature/academy-csv-export K Export endpoint + button on Bookings, Payments, Lessons; filters; per-coach hours summary; streaming none — N1 4 h + 1 h
6 feature/attendance-state C (part 1) lesson_players.attendance_state + mark done / no-show (admin roster panel + coach PWA), student page shows balance · reserved · consumed · history; ledger_mode='on_add' default so nothing changes for any club M-C1, M-C2 4 N1 8 h + 2 h
7 feature/ledger-on-done C (part 2) academies.ledger_mode='on_done': reservation is a locked, package-attributed seat (reservation-aware get_player_package_remaining_sessions first), mark_attendance RPC writes the ledger, done-time exhaustion; backfill dry-run + one-transaction commit keyed to the report snapshot, inverse migration tested; past lessons default attended; Growth (monthly) new template + the 9 multi-package and 62 NULL-expiry packages resolved by hand M-C3, M-C4, M-C5 6 L1, J1 12 h + 3 h, then the 1 Nov switch
8 feature/groups-schedule B (part 1) academy_group_slots, class link, "Generate lessons" from a group, roster-by-package with persisted package selection, seats spread across the month for two-slot groups, "needs package" + override with reason code (comp / bill later / trial → audit); paid non-package seats never touched M-B1, M-B2 4, 6 J1 9 h + 2 h
9 feature/lesson-waitlist B (part 2), Plan Step 2 lesson_waitlist with timed claim on a released seat; email transport + wa.me lane M-B3 8 N1 6 h + 1.5 h
10 feature/subscription-states D (part 1) past_due (still billable), paused (re-anchors on resume); pause flow before day 26, slots lost; academy_settings (grace from lapse day, freeze, past-due); day-26 dispatch_renewal_notices; roster lapse job dispatch_roster_sync with notices and a 24 h cutoff M-D1, M-D2, M-D3 6, 8 A1 7 h + 2 h
11 feature/proration-bridge E, Plan Step 3 compute_bridge in process-package-billing; payment_requests.bridge_tier/bridge_days; worklist shows the same number the function will charge; proration_mode flag M-E1 1, 10 C1 6 h + 1.5 h
12 feature/billing-issue-on-first D (part 2) process-package-billing issues the request on the 1st: Stripe payment link (card + PromptPay) minted per request, payment_requests.checkout_url, webhook checkout.session.completed → payments (POS bridge fires); unpaid by day 3 → past_due; saved-card off-session charge as a per-student opt-in flag, off by default (D5) M-D4 10, 11 S1 10 h + 3 h
13 feature/payment-reconciliation G (persistent) Nightly report table: payments ↔︎ package/lesson ↔︎ POS; unmatched, duplicates, variance; replaces Step 1's tab M-G1 12 S1 4 h + 1 h
14 feature/attendance-vs-payment H (persistent) Per student per month: training-without-paying, paid-not-training; badge on student page; replaces Step 1's tab M-H1 (view) 7 N1 3 h + 1 h
15 feature/planning-drag-drop A dnd-kit on the shared week grid (Calendar + Planning): move to free court/time, swap when occupied, 409 conflict toast, keyboard fallback, undo, audit row none (audit exists) 2, 4 N1 12 h + 3 h
16 feature/whatsapp-cloud F whatsapp-cloud.ts provider (Meta Cloud API), template registry, opt-in/out, reminder schedule tied to billing states; wa.me lane kept as fallback M-F1 10 M1 8 h + 2 h
17 feature/crm-leads I leads + lead_events; lead → trial → student; source, owner, next action; converts into the existing customer/user identity; Sheet importer M-I1, M-I2 3 N1 8 h + 2 h
18 feature/academy-messaging J Templates for reminders / schedule change / cancellation; 1:1 and group messages from a lesson or group (email + WhatsApp when 16 is live); no push (D14) M-J1 16 N1 8 h + 2 h
19 feature/player-progress Plan Step 4 me/progress, level history, web page M-P1 7 J2 6 h + 1.5 h
20 feature/ops-card-contract M (part 1) Read-only: the five card fields (type, coach or Unassigned, confirmed / capacity, waitlist, source + payment) on the Staff PWA calendar, the admin calendar and the admin lists; coach filter with Me and Unassigned in both apps; type filter gains Group coaching and Events (phone screens 1 and 6 in Mockup N); bookings.source written by the OpenRally confirm RPC; unassigned worklist on Academy Health; fix-first: strip the [recurring:<uuid>] notes tag in the PWA detail sheet M-M2 1 N1 9 h + 2 h
21 feature/events-as-programs M (part 2) "What is this?" at creation in both apps; Event = program occurrence (americano, first_time types), roster + waitlist from program_enrollments; instructor in program_session_payload → TPS co-organiser; inbound co-organiser email → instructor_id; TPS-created events imported once as source='tps', read-only until D19 switches; the guest_booking_category='event' label retired; inbound coach match is a suggestion until confirmed once (coach_external_links); change guard on emit_program_event; echo guard on external_event_id M-M1, M-M3 20, 4 N1, D19 11 h + 3 h
22 feature/pwa-add-player M (part 3) Add player from any card in the PWA (Mockup N screens 3–5; search or guest); lesson_players for lessons and groups, enrollment RPC for events with waitlist position when full (never overbooks from the phone), guest fields for court; three payment chips (Paid on TPS · Due at desk · Unpaid); coach limited to own lessons and never events (D20); RLS for manager and staff on program_enrollments; audit row per add M-M4 20, 21 N1 6 h + 1.5 h

Migration ids M-* are defined in 02-technical-spec.md §2 (M-M1–M-M3 in §15). Total: about 151 agent hours and 40 review hours.

30 / 60 / 90 days

By Ships Why this cut
16 October (30 days) PRs 0–6 and 20: proposal in docs, Academy Health live, the override defect fixed, roles helper, audit log, CSV export, attendance state (flag off), the card contract and coach filter in both apps All read-only or flag-off. Abby gets the worklist for the 1 Nov cycle; Jose starts marking attendance so the dry run has real data; coaches and staff see who is on every card from day one.
15 November (60 days) PRs 7–11 and 21–22: ledger switched for Pad Thai Padel on 1 Nov after the reviewed dry run, groups linked to schedule, waitlist, subscription states with pause and grace, the bridge inside billing, events as programs with the coach on both sides, add-player from the phone The operational core. Every 1 Nov charge is still a payment link, but the amount, the state and the roster are now computed by Setpoint.
15 December (90 days) PRs 12–16: payment requests issued by Setpoint on the 1st, persistent reconciliation and attendance-vs-payment, drag-and-drop planning, WhatsApp Cloud API Money movement and the two third-party gates. If S1 or M1 slips, 15 (drag-and-drop) still ships.
After PRs 17–19: leads, messaging, player progress Valuable, not on the critical path for "clean from the 1st".

Cut (not in this scope)

Rules that stay in force (from the Plan)

Proposal before code · one feature, one branch, one PR · migrations only where listed, created with supabase migration new, applied once to production by Nestor · Playwright in the foreground, data-testid, no skips · docs-sync on every PR · self-review + demo GIF · local Supabase only, never .env.production · AUTO / STANDING RULE / DECISION / NEVER: the software computes and flags; Abby executes what a list shows; Alvaro decides money, Jose decides sport; the software never takes or refunds money on its own outside the scheduled charge the student agreed to, never deletes, never messages an opt-out.