v2 Gap Analysis — Billing & Payments
Billing & Payments — V2 Review Gap Analysis
Source: BACKEND-RAMBLINGS.md lines 288-367
Spec: docs/admin-spec/08-billing-payments.md
Implementation: src/pages/billing/, src/data/billing.ts, src/data/scholarships.ts, src/data/families.ts
Screens Covered
- Payment Overview / Dashboard
- Payments list (transaction history)
- Payment Plans
- Family Plans
- Coupons
- Scholarships & Deferments (two tabs)
Payment Overview / Dashboard
Matches
- Six alert sections in correct severity order: Failed Payments > Past Due > Missing Subscriptions > Expiring Scholarships > Deferment Resume Due > Recently Cancelled
- Metric cards: Active Subscriptions, Monthly Recurring Revenue, Failed Payments (30d), New Subscriptions (30d), Churn (30d)
- Actions per section: "View Student", "Send Reminder" (failed payments), "Retry Payment" (past due), "Cancel" (both), "Extend" (scholarships & deferments), "Confirm Resume" (deferments), "Contact" (recently cancelled)
- Student name is clickable → Student Detail > Payments tab
- Export Alerts and Refresh Data buttons present
Expands
- Spec includes "View in Stripe ↗" link for failed payments (not in implementation)
- Spec defines precise failure reasons from Stripe decline codes ("Card declined", "Insufficient funds", "Card expired", "Processing error"); implementation randomizes them by studentId % failureReasons.length
Deviates
- None significant
Adds
- None (implementation matches spec fully structurally)
Ambiguous
- Spec mentions "billing_alerts table" and webhook processing; implementation derives all alerts from mock data (paymentRecords, subscriptions, studentScholarships, studentDeferments) without persistent alerts table
- Payment Overview data all computed client-side from existing mocks — no webhook simulation
Payments List
Matches
- Five columns: Date, Student, Amount, Plan, Status, Method (card brand + last 4)
- Four filter types: Search (student name/email), Plan (dropdown), Status (Succeeded/Failed/Refunded/Pending), Date range (from/to)
- Sortable: Date (default descending), Student, Amount, Plan, Status
- "Export" button (top-right)
- Student name click → Student Detail > Payments tab
- Pagination pattern (25 per page standard, though mock shows all 30 records)
Expands
- None
Deviates
- None
Adds
- None
Ambiguous
- None
Payment Plans
Matches
- Eight columns: Name, Type, Sub-Type, Amount (formatted with currency), Cycles, Currency, Stripe Synced (badge), Active Subscribers
- Types presented as dropdown options in form: semester, year, family, discount
- Sub-types: OneTime, Installment
- Currencies: USD, CAD, GBP, EUR
- Actions: Create Plan, Edit Plan (with warning if active subscribers > 0), Delete Plan (disabled if active subscribers > 0)
- "Verify Stripe Sync" button calls out sync status
Expands
- None
Deviates
- Spec mentions grouping payment plans as "Standard / Family / Scholarships / Discounts" on the overview; implementation shows them flat in a single table with a "type" column (semester/year/family/discount)
- Interpretation: Current implementation does not visually group plans by category, but the type field allows filtering by group. Not a critical gap but a structural difference.
Adds
- None
Ambiguous
- "Verify Stripe Sync" action in impl is a toast ("all plans checked"), not a modal with detailed sync status per spec (which should show "Synced / Mismatch / Not Found" with details)
Family Plans
Matches
- Two views: List (all families, sorted alphabetically) and Detail (single family with member table)
- List columns: Family Name, Primary Contact (clickable → Student Detail), Members count, Discount (%), All Active? (badge)
- Detail header: Family Name, Primary Contact, Discount Applied, Created date, Member Count
- Status bar: green if all active, orange if issues (X of Y have billing issues)
- Member table columns: Student Name (clickable), Level (badge), Subscription Status (badge), Plan, Joined Family (date), Actions (View Student link, Remove button)
- Actions: Create Family, Edit Family, Delete Family, Add Member, Remove Member, Change Discount
- All family-level and member-level dialogs/confirmations match spec
Expands
- None
Deviates
- None
Adds
- None
Ambiguous
- Spec requires per-member discount percentage and per-member semester status (repeat/regular/mastery); implementation shows only Level (badge, e.g., "Intermediate") and subscription status
- Gap: Missing "semester status" field (repeat vs regular vs mastery student). Spec line 342: "if they signed up as a repeat student, if they signed up as a regular student that's just continuing, if they signed up as a mastery course student". Currently not surfaced in family member detail.
Coupons
Matches
- Seven columns: Name, Code, Discount (%), Semester, Expiry (red text if expired), Redemptions, Students Using
- Actions: Create Coupon, Edit Coupon (code read-only, warning if students > 0), Delete Coupon (disabled if students > 0)
- Form fields: Name, Short Code, Percentage Off (1-100), Semester (dropdown, optional "All semesters"), Expiry Date (optional)
- Redemptions count from Stripe (spec); implementation shows hardcoded counts
- "Students Using" is a number; spec allows clicking to filter Student Management by that coupon
Expands
- None
Deviates
- None
Adds
- None
Ambiguous
- Spec says create coupon should "create matching coupon in Stripe via API"; implementation just toasts "created successfully" without showing Stripe sync outcome
Scholarships & Deferments
Scholarships Tab
Programs View
- Columns: Program Name, Discount (%), Eligible Plans (plan types comma-separated), Students (count, clickable), Total Discount Value (computed), Renewal Policy, Description (truncated), Actions (Edit, Delete)
- Actions: Create Program, Edit Program (warning if students > 0), Delete Program (disabled if students > 0)
- Form fields match spec: Name, Discount Percentage, Eligible Plans (multi-select), Description, Renewal Policy (Auto-renew / Manual / One-time)
Students View
- Columns: Student Name (clickable), Program, Discount (%), Start Date, End Date (red if past), Renewal Status (badge: Active green, Expiring orange, Expired gray), Stripe Coupon Applied? (badge: Yes green, No red), Actions (Extend, Remove, View Student)
- Filter bar: Program (dropdown), Status (Active/Expiring/Expired), Search (name/email)
- Actions: Assign Student, Remove Student, Extend Scholarship, Renew All Expiring
Deferments Tab
Active Deferments
- Columns: Student Name (clickable), Pause Start, Resume Date (orange if within 14 days), Reason (truncated), Duration (N days or N months), Status (blue "Active" or orange "Resuming Soon"), Stripe Paused? (badge), Created By, Actions (Extend, Resume Now, View Student)
- Actions: Create Deferment, Extend Deferment, End Deferment Early (Resume Now)
- Form fields: Student (autocomplete, active subscriptions only), Pause Start Date, Resume Date, Reason
Deferment History
- Collapsible by default
- Columns: Student Name, Pause Period (date range), Reason, Duration, Outcome (Resumed/Cancelled/Extended)
- No actions on history rows
Matches
- All screens, columns, filters, and actions structurally present
- Tabs for Scholarships and Deferments present
- Programs/Students view toggle for Scholarships present
Expands
- None
Deviates
- None
Adds
- None
Ambiguous
- "Stripe Coupon Applied?" and "Stripe Paused?" badges show verification status; implementation hard-codes
stripeCouponAppliedandstripePausedper record in mock data- Gap: Spec suggests real-time verification against Stripe state; implementation shows static mock boolean
Cross-cutting: Subscription Cancel/Pause Actions
Stakeholder specification (lines 310-311): She does NOT explicitly name all four variants in this section, but the ramblings cover:
- Failed payments need reminder emails with invoice
- Payment plans support statuses: succeeded, failed, deferred
Spec detail (implicit across overview and deferments):
- Deferment actions: "Create Deferment" → calls Stripe
subscription.pause_collection = { behavior: 'void', resumes_at } - Deferment "End Deferment Early" → clears pause_collection
- Overview: "Cancel" action on failed payments and past due (not detailed, likely hard-cancel)
Implementation:
- Deferment "Create" and "Extend" → toasts only
- Deferment "Resume Now" → toasts "Billing resumed"
- Overview "Cancel" on failed/past due → toasts "Subscription cancelled"
- No support for "pause" vs "cancel-now" vs "cancel-at-date" differentiation in mock
Gap: Stakeholder said nothing about these fine-grained cancel variants in lines 288-367, so this is not strictly a gap against her narration. However, spec Section 6.4 mentions cancel/pause actions are in "Student Detail > Payments tab" (not fully specified here), which is out of scope for this review.
Summary
Key Themes
- Implementation is complete and spec-aligned structurally. All six screens present with correct layout, columns, filters, and action flows.
- Grouping of payment plans: Spec envisions them grouped (Standard / Family / Scholarships / Discounts); implementation shows flat list with type column. Not a gap against stakeholder, but a structural choice.
- Mock vs. real Stripe integration: All Stripe-dependent features (coupon sync, subscription pause, decline codes, redemptions) are mocked with static data. No webhook simulation.
Biggest Deviations
- Payment Plans: "Verify Stripe Sync" button shows a toast instead of a modal with per-plan sync status (Synced/Mismatch/Not Found).
- Family member semester status: Spec (line 342) mentions per-member semester status (repeat/regular/mastery). Implementation shows only Level and subscription status, not semester enrollment type.
- Failed payment failure reasons: Spec expects Stripe decline codes (e.g., "Card declined", "Insufficient funds"); implementation derives them pseudo-randomly per studentId.
Cancel-Action-Variant Status
- Deferment pause/resume fully modeled (Create, Extend, Resume Now).
- General subscription cancellation present but not differentiated (no "pause vs. cancel-now vs. cancel-at-date" variants in Payments/Overview).
- Spec assumes actions are on Student Detail > Payments tab (not in this domain), so not a gap per se.
Top Ambiguities
- Family member semester status: Missing "repeat/regular/mastery" tracking in member detail view.
- Stripe sync verification UI: Implementation shows toast; spec shows modal with details.
- Per-member discount percentage: Spec line 336-337 mentions "each one of them has a payment associated with their name with a percentage of discount... different for essentials and mastery students." Implementation does not surface per-member discounts; assumes all family members share the same family discount percentage.
Validation Questions
- Should payment plans be grouped visually (Standard / Family / Scholarships / Discounts) or is flat list with type column sufficient?
- Should family members track per-member discount % (vs. family-wide discount)?
- Should family members explicitly show semester status (repeat/regular/mastery)?
- Should "Verify Stripe Sync" show detailed mismatch info in a modal, or is a toast notification acceptable for a mockup?