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
- Read-only pages and repairs first (they prove the numbers and cost nothing if wrong).
- Infrastructure the write features depend on (roles helper, audit log, settings).
- 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.
- Money movement last, gated on Stripe live keys.
- 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)
- Push notifications (no infrastructure anywhere; D14). Email + WhatsApp cover every message in scope.
- Stripe Subscriptions object (D5): its proration and anchor rules fight ours. Off-session auto-charging is deferred to a per-student opt-in: Setpoint collects no card mandate today and most academy money is cash, transfer or PromptPay.
- AI CRM, SEPA, white-label app, native student app: Dashboll features that do not pay back at 60 students; the native app is the TPS app's job through OpenRally.
- Makeup credits: the fixed-slot rule stands; a missed group session is lost. Waitlist (PR 9) is the only "recovery" mechanism.
- A general rewrite of reports: PR 14 adds one badge and one view;
academy-reportskeeps its shape until the ledger is the source (a later chore).
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.