Admin Spec — Billing & Payments
Billing & Payments
Domain 7 of the Enhanced Candidate A navigation structure. Contains: Payment Overview (NEW), Payments (transaction history), Payment Plans, Coupons, Family Plans (NEW), Scholarships & Deferments (NEW). This domain is entirely new. No current admin backend section exists for billing. All billing operations currently require Stripe Dashboard + Google Sheets. Primary workflows served: WF2 (Student Support — billing questions), WF3 (Semester Close — payment setup), WF4 (Daily Monitoring — failed payment alerts)
1. Domain Overview
Billing & Payments is the highest-impact new domain. The interview identified payment/billing management as the single most impactful addition (Interview Q5, Q9). This domain provides two levels of billing integration:
- Aggregate level (this domain): Payment Overview dashboard, payment configuration (plans, coupons), and special arrangement management (families, scholarships, deferments)
- Individual level: Student Detail Page > Payments tab (specified in 03-student-management.md)
The Dashboard (02-dashboard.md) surfaces billing alerts as tiles that link to either Payment Overview or directly to affected student profiles.
Workarounds Eliminated
| ID | Workaround | Current Tool | Replaced By |
|---|---|---|---|
| W1 | Stripe Dashboard dependency for payment lookup | Stripe Dashboard | Payment Overview + Student Detail > Payments tab |
| W2 | Family payment plans tracked in spreadsheet | Google Sheets | Family Plans screen (Section 6) |
| W3 | Scholarship tracking in spreadsheet | Google Sheets | Scholarships & Deferments screen (Section 7) |
| W4 | Deferment tracking in spreadsheet | Google Sheets | Scholarships & Deferments screen (Section 7) |
| W5 | Support staff cannot view billing without Stripe access | Stripe Dashboard (admin only) | Read-only billing views for Support Staff role |
| W6 | Failed payment follow-up is manual | Email + Stripe Dashboard | Dashboard alert tiles + Payment Overview alerts |
| W7 | Semester-end payment setup requires Stripe Dashboard | Stripe Dashboard | Close Workflow Step 3 (04-semester-management.md) |
| W8 | Deactivating a student does not cancel billing | Manual Stripe cancellation | Linked deactivation (03-student-management.md) |
Data Architecture
Existing tables (already in QuranFlow database):
subscriptions: id, stripeSubscriptionId, user_link_level_tag_id, payment_plan_id, coupon_id, status, start_date, end_date, next_billing_date, cycles, cycles_remainingpayment_plans: name, type (semester/year/family/discount), sub_type (OneTime/Installment), amount, cycles, currency, product_idcoupons: name, semester_id, short_code, percentage_off, expiry_date (synced to Stripe)
New tables (assumptions from WS2 — to be validated):
family_groups: id, name, primary_contact_user_id, discount_coupon_id, created_atfamily_members: id, family_group_id, user_id, joined_atscholarship_programs: id, name, discount_percentage, eligible_plan_types, description, renewal_policystudent_scholarships: id, user_id, scholarship_program_id, start_date, end_date, renewal_status, coupon_idstudent_deferments: id, user_id, subscription_id, pause_start, resume_date, reason, status, created_bybilling_alerts: id, type, user_id, stripe_event_id, data, status (new/acknowledged/resolved), created_at
Stripe API endpoints used:
- Customer:
Customer::search(), customer by ID - Subscription:
GET /v1/subscriptions/{id},POST /v1/subscriptions/{id}(update/cancel) - Invoice:
GET /v1/invoices?customer={id},GET /v1/invoices/upcoming - Payment Method: via subscription expand (
expand[]=default_payment_method) - Pause:
subscription.pause_collectionwithresumes_at - Coupon: create/apply via subscription
Assumption (OQ6): Single Stripe account (fe-checkout).
Assumption (OQ4): QuranFlow subscriptions table is canonical after customer ID reconciliation.
Stripe Data Patterns (domain-specific)
This domain follows the Stripe data patterns defined in 01-global-patterns.md Section 5. Domain-specific notes:
| Screen | Fetch Strategy | Cache Table |
|---|---|---|
| Payment Overview — metrics bar | Webhook-cached + periodic sync (every 15 min) | billing_alerts + cached metrics |
| Payment Overview — alert lists | Webhook-driven, billing_alerts table |
billing_alerts |
| Payments (transaction history) | Webhook-cached invoice data | Local payments/invoices cache |
| Payment Plans — Stripe sync check | On-demand Stripe API call (admin-triggered) | None (real-time verification) |
| Coupons — redemption count | Stripe API on page load (coupon times_redeemed) |
Short-lived cache (5 min TTL) |
| Family Plans | Local tables only; Stripe coupon applied via API on create/edit | family_groups, family_members |
| Scholarships | Local tables + Stripe coupon verification | scholarship_programs, student_scholarships |
| Deferments | Local tables + Stripe pause_collection verification |
student_deferments |
2. Screen: Payment Overview (NEW)
2.1 Purpose
Aggregate billing health dashboard. Shows subscription metrics, surfaces at-risk accounts, and provides actionable alert lists. This is where the admin starts when investigating billing issues.
2.2 Entry Points
- Sidebar: Billing & Payments > Payment Overview
- Dashboard: Failed Payments tile click (navigates here, scrolled to Failed Payments alert section)
- Note: Past due subscriptions surface through the Failed Payments dashboard tile (there is no separate "Past Due" tile — past due is a subsection within Payment Overview, see Section 2.5)
2.3 Layout
Metrics bar (top) → Alert sections (scrollable, expandable) → At-risk accounts list
2.4 Metrics Bar
Five metric cards in a horizontal row (uses Metric Card pattern from 01-global-patterns.md Section 4.5):
| Metric | Source | Format | Trend |
|---|---|---|---|
| Active Subscriptions | Count of subscriptions with status=active in Stripe (cached via periodic sync) | "247 Active" | vs. previous month |
| Monthly Recurring Revenue | Sum of active subscription amounts (cached, periodic sync) | "$3,705/mo" | vs. previous month |
| Failed Payments (30 days) | Count from billing_alerts where type=invoice.payment_failed and created_at within 30 days |
"3 Failed" (red text if >0) | vs. previous 30-day window |
| New Subscriptions (30 days) | Count from billing_alerts where type=customer.subscription.created and created_at within 30 days |
"12 New" | vs. previous 30-day window |
| Churn (30 days) | Count from billing_alerts where type=customer.subscription.deleted and created_at within 30 days |
"5 Cancelled" | vs. previous 30-day window |
2.5 Alert Sections
Each section is an expandable card with a count badge and a list of affected students. Sections are sorted by severity: Failed Payments > Past Due > Missing Subscriptions > Expiring Scholarships > Deferment Resume Due > Recently Cancelled.
A. Failed Payments (source: billing_alerts table, webhook: invoice.payment_failed)
| Column | Source | Format |
|---|---|---|
| Student Name | users.name via billing_alerts.user_id |
Clickable → Student Detail > Payments tab |
| Amount | Invoice amount from billing_alerts.data JSON |
"$15.00" |
| Failure Reason | Stripe decline code from billing_alerts.data JSON |
"Card declined" / "Insufficient funds" |
| Attempts | Invoice attempt_count from billing_alerts.data JSON |
"2 of 3" |
| Last Attempt | billing_alerts.created_at |
"Mar 8, 2026" |
| Actions | — | "View Student" / "View in Stripe ↗" / "Send Reminder" / "Cancel" |
B. Past Due Subscriptions (source: Stripe subscription.status = past_due, periodic sync to billing_alerts)
| Column | Source | Format |
|---|---|---|
| Student Name | users.name via billing_alerts.user_id |
Clickable → Student Detail > Payments tab |
| Days Past Due | Calculated: today minus current_period_end from billing_alerts.data |
"12 days" |
| Amount Owed | Subscription amount from billing_alerts.data |
"$15.00" |
| Plan | payment_plans.name via subscription → payment_plan_id |
"Monthly" |
| Actions | — | "View Student" / "Retry Payment" / "Cancel" |
C. Missing Subscriptions (source: cross-reference: users with user_activation_status=1 and user_type=3 but no active Stripe subscription via subscriptions table)
| Column | Source | Format |
|---|---|---|
| Student Name | users.name |
Clickable → Student Detail > Payments tab |
| Level | Current enrollment level from user_link_level_tag |
Level badge |
| Semester | semesters.name via enrollment |
Text |
| Account Status | users.user_activation_status |
"Active" badge |
| Actions | — | "View Student" / "Investigate" / "Deactivate" |
D. Expiring Scholarships (source: student_scholarships where end_date within 30 days of today and renewal_status != 'Renewed')
| Column | Source | Format |
|---|---|---|
| Student Name | users.name via student_scholarships.user_id |
Clickable → Student Detail > Payments tab |
| Scholarship | scholarship_programs.name via student_scholarships.scholarship_program_id |
"50% Annual" |
| Expiry Date | student_scholarships.end_date |
"Apr 15, 2026" |
| Actions | — | "View Student" / "Extend" / "Remove" |
E. Deferment Resume Due (source: student_deferments where resume_date within 14 days of today and status = 'Active', cross-checked with Stripe subscription.pause_collection.resumes_at)
| Column | Source | Format |
|---|---|---|
| Student Name | users.name via student_deferments.user_id |
Clickable → Student Detail > Payments tab |
| Resume Date | student_deferments.resume_date |
"Mar 20, 2026" |
| Paused Since | student_deferments.pause_start |
"Jan 20, 2026" |
| Actions | — | "View Student" / "Confirm Resume" / "Extend" |
F. Recently Cancelled (source: billing_alerts where type=customer.subscription.deleted and created_at within last 14 days)
| Column | Source | Format |
|---|---|---|
| Student Name | users.name via billing_alerts.user_id |
Clickable → Student Detail > Payments tab |
| Cancellation Date | billing_alerts.created_at |
"Mar 5, 2026" |
| Last Payment | Last paid invoice amount from billing_alerts.data JSON |
"$15.00" |
| Was Active Student? | users.user_activation_status (1 = Yes, 0 = No) |
Yes/No badge |
| Actions | — | "View Student" / "Contact" |
2.6 Actions (page-level)
| Action | Trigger | Permission | Behavior |
|---|---|---|---|
| Refresh Data | "Refresh" button (top-right, next to metrics bar) | Admin | Re-fetches Stripe data, rebuilds alert lists. Shows loading spinner during refresh. |
| Export Alerts | "Export" button (top-right) | Admin | Downloads CSV of all current alert items grouped by section |
2.7 States
| State | Display |
|---|---|
| All healthy | All alert sections show "0 items" — collapsed. Metrics bar visible. Message below metrics: "All billing is healthy. No issues to address." |
| Has alerts | Alert sections with items expanded by default, sorted by severity (Failed > Past Due > Missing > Expiring > Resume Due > Cancelled). Sections with 0 items collapsed. |
| Loading | Skeleton metrics bar (5 gray rectangles) + "Loading alerts..." placeholder in alert area |
| Stripe error | Warning banner below metrics bar: "Some billing data could not be loaded. Showing cached data." Sections that loaded display normally; failed sections show inline error: "Unable to load [section name]. Retry" |
2.8 Role Visibility
- Admin: Full view + all actions (Refresh, Export, Send Reminder, Cancel, Retry Payment, Extend, etc.)
- View-Only / Support Staff: Full view of metrics and alert lists. No action buttons except "View Student" links (which navigate to Student Detail).
- TA: Hidden (sidebar item not visible)
3. Screen: Payments (Transaction History)
3.1 Purpose
View payment transaction records. Relocated from Reports section. Provides a filterable, sortable list of all payment events.
3.2 Entry Points
- Sidebar: Billing & Payments > Payments
3.3 Layout
Filter bar (top) + data table + pagination (bottom). Uses standard Data Table and Filter Bar patterns from 01-global-patterns.md Sections 4.1 and 4.2.
3.4 Filter Bar
| Filter | Type | Options |
|---|---|---|
| Search | Text input | Filters by student name or email |
| Date Range | Date range picker | Start date – End date |
| Plan | Dropdown | All plan names from payment_plans.name |
| Status | Dropdown | Succeeded / Failed / Refunded / Pending |
3.5 Data Displayed
| Column | Source | Sortable | Filterable |
|---|---|---|---|
| Date | Payment date (from invoice or local record) | Yes (default: descending) | Yes (date range in filter bar) |
| Student | users.name via subscription → user link |
Yes | Yes (search in filter bar) |
| Amount | Payment amount from invoice | Yes | No |
| Plan | payment_plans.name via subscriptions.payment_plan_id |
Yes | Yes (dropdown in filter bar) |
| Status | Payment status (Succeeded / Failed / Refunded / Pending) | Yes | Yes (dropdown in filter bar) |
| Method | Card brand + last 4 digits (e.g., "Visa •••• 4242") from Stripe payment method | No | No |
3.6 Actions
| Action | Trigger | Permission | Behavior |
|---|---|---|---|
| View Student | Student name click | All roles with access | Navigates to Student Detail > Payments tab |
| Export | "Export" button (top-right) | Admin | Downloads CSV of current filtered/sorted view |
3.7 Pagination
Standard pagination (01-global-patterns.md Section 4.1): 25 records per page, "1-25 of 342" format.
3.8 States
| State | Display |
|---|---|
| Loaded | Data table with records |
| Empty | "No payment records found." (should not occur in production) |
| Filtered - no results | "No payments match your filters." + "Clear filters" link |
| Loading | Skeleton data table |
| Error | "Unable to load payment records. Please try again." + "Retry" button |
3.9 Role Visibility
- Admin: Full view + Export button
- View-Only / Support Staff: Full view, no Export button
- TA: Hidden
4. Screen: Payment Plans
4.1 Purpose
Define pricing options available at checkout. Enhanced with Stripe sync visibility and active subscriber counts so admins can see plan usage before editing or deleting.
4.2 Entry Points
- Sidebar: Billing & Payments > Payment Plans
4.3 Layout
Data table (no pagination expected — small dataset, typically <15 plans). Uses standard Data Table pattern. No filter bar (dataset too small to require filtering).
4.4 Data Displayed
| Column | Source | Notes |
|---|---|---|
| Name | payment_plans.name |
Plan display name |
| Type | payment_plans.type |
semester / year / family / discount |
| Sub-Type | payment_plans.sub_type |
OneTime / Installment |
| Amount | payment_plans.amount |
Formatted as currency with payment_plans.currency (e.g., "$15.00 USD") |
| Cycles | payment_plans.cycles |
Number of billing cycles (e.g., "6" for 6-month installment) |
| Currency | payment_plans.currency |
ISO currency code |
| Stripe Price ID (NEW) | payment_plans.product_id → looked up against Stripe Prices |
Shows linked Stripe price ID. Warning icon (⚠) if local product_id does not match a valid Stripe Price, or if amounts/currencies differ. |
| Active Subscribers (NEW) | Count of subscriptions where payment_plan_id = this plan and status = 'active' |
Number. "0" if none. |
| Actions | — | Edit / Delete icons (Admin only) |
4.5 Actions
| Action | Trigger | Permission | Behavior |
|---|---|---|---|
| Create Plan | "Create Plan" button (top-right) | Admin | Opens modal with fields: Name, Type (dropdown), Sub-Type (dropdown), Amount (number), Cycles (number), Currency (dropdown). On save, creates local record. Note: does not auto-create Stripe Price — admin must link manually or use Sync. |
| Edit Plan | Edit icon on row | Admin | Opens modal pre-filled with plan data. All fields editable. Warning if Active Subscribers > 0: "This plan has [N] active subscribers. Changes will apply to new subscriptions only." |
| Delete Plan | Delete icon on row | Admin | Destructive confirmation (01-global-patterns.md Section 4.6). Blocked if Active Subscribers > 0: button disabled with tooltip "Cannot delete plan with active subscribers." If 0 subscribers: "Delete [plan name]? This cannot be undone." |
| Sync with Stripe (NEW) | "Verify Stripe Sync" button (top-right, secondary) | Admin | Iterates all plans, checks each product_id against Stripe Prices API. Displays results modal: list of plans with status (Synced / Mismatch / Not Found). Mismatches show details (e.g., "Local: $15.00, Stripe: $20.00"). |
4.6 States
| State | Display |
|---|---|
| Loaded | Data table with plans |
| Empty | "No payment plans configured." + "Create Plan" button |
| Loading | Skeleton data table |
| Error | "Unable to load payment plans. Please try again." + "Retry" button |
| After Stripe Sync | Results modal showing sync status per plan |
4.7 Role Visibility
- Admin: Full CRUD + Stripe sync verification
- View-Only / Support Staff: View table only (no Create, Edit, Delete, or Sync buttons)
- TA / Support Staff: Hidden (sidebar item not visible)
5. Screen: Coupons
5.1 Purpose
Manage discount codes. Enhanced with redemption tracking from Stripe, so admins can see coupon usage without opening Stripe Dashboard.
5.2 Entry Points
- Sidebar: Billing & Payments > Coupons
5.3 Layout
Data table. Uses standard Data Table pattern (01-global-patterns.md Section 4.1). No filter bar (typically <20 coupons).
5.4 Data Displayed
| Column | Source | Notes |
|---|---|---|
| Name | coupons.name |
Coupon display name |
| Code | coupons.short_code |
The code students enter at checkout |
| Discount | coupons.percentage_off |
Formatted as "50% off" |
| Semester | semesters.name via coupons.semester_id |
Scope. Shows semester name, or "All semesters" if semester_id is null |
| Expiry | coupons.expiry_date |
Formatted date, or "No expiry" if null. Red text if expired. |
| Redemptions (NEW) | From Stripe coupon times_redeemed (fetched on page load, cached 5 min) |
Count (e.g., "14") |
| Students Using (NEW) | Count of subscriptions where coupon_id = this coupon and status = 'active' |
Number. Clickable → Students list filtered by this coupon |
| Actions | — | Edit / Delete icons (Admin only) |
5.5 Actions
| Action | Trigger | Permission | Behavior |
|---|---|---|---|
| Create Coupon | "Create Coupon" button (top-right) | Admin | Opens modal with fields: Name, Short Code, Percentage Off (number, 1-100), Semester (dropdown, optional — "All semesters" default), Expiry Date (date picker, optional). On save: creates local record AND creates matching coupon in Stripe via API. Toast: "Coupon created and synced to Stripe." |
| Edit Coupon | Edit icon on row | Admin | Opens modal pre-filled. Editable fields: Name, Percentage Off, Semester, Expiry Date. Short Code is read-only after creation. Updates local record and Stripe coupon. Warning if Students Using > 0: "This coupon is applied to [N] active subscriptions. Changes to discount percentage will take effect at next billing cycle." |
| Delete Coupon | Delete icon on row | Admin | Blocked if Students Using > 0: button disabled with tooltip "Cannot delete coupon with active redemptions. Remove from all students first." If 0 active: destructive confirmation "Delete coupon [name]? This will also delete the coupon in Stripe." Deletes local record and Stripe coupon. |
| View Students | Students Using count click | Admin, View-Only, Support Staff | Navigates to Student Management > Students with filter applied: coupon = [coupon name] |
5.6 States
| State | Display |
|---|---|
| Loaded | Data table with coupons |
| Empty | "No coupons created." + "Create Coupon" button |
| Loading | Skeleton data table |
| Error | "Unable to load coupons. Please try again." + "Retry" button |
| Stripe sync issue | Warning icon next to Redemptions column for any coupon where Stripe lookup failed. Tooltip: "Could not fetch Stripe data. Showing last known value." |
5.7 Role Visibility
- Admin: Full CRUD
- View-Only / Support Staff: View table + click "Students Using" links. No Create, Edit, Delete.
- TA / Support Staff: Hidden (sidebar item not visible)
6. Screen: Family Plans (NEW)
6.1 Purpose
Manage multi-student family billing arrangements. Replaces Google Sheets tracking. Eliminates workaround W2.
Assumption (OQ1): ~10-20 family plans per semester, justifying full CRUD rather than a simple tagging approach.
6.2 Entry Points
- Sidebar: Billing & Payments > Family Plans
- Student Detail > Payments tab: family name link (e.g., "Ahmed Family" → Family Detail view)
6.3 Layout
Two views:
- Family List View (default): Data table of all family groups
- Family Detail View (click a family): Header with family info + members table
6.4 Data Displayed — Family List View
| Column | Source | Sortable |
|---|---|---|
| Family Name | family_groups.name |
Yes (default: alphabetical) |
| Primary Contact | users.name via family_groups.primary_contact_user_id |
Yes |
| Members | Count of family_members where family_group_id = this family |
Yes |
| Discount | coupons.percentage_off via family_groups.discount_coupon_id |
Yes |
| All Active? | Derived: whether all members have active subscriptions (check subscriptions.status for each member's user_id) |
Yes (badge: "All Active" green / "Issues" orange) |
| Actions | — | Edit / Delete icons (Admin only) |
6.5 Data Displayed — Family Detail View
Header section:
| Field | Source |
|---|---|
| Family Name | family_groups.name |
| Primary Contact | users.name via family_groups.primary_contact_user_id (clickable → Student Detail) |
| Discount Applied | coupons.percentage_off via family_groups.discount_coupon_id (e.g., "25% family discount") |
| Created | family_groups.created_at |
| Member Count | Count of family_members |
Status bar (below header):
- All active: Green bar: "All [N] members have active subscriptions."
- Issues: Orange bar: "[X] of [Y] members have billing issues." — affected members highlighted in table below.
Members table:
| Column | Source | Notes |
|---|---|---|
| Student Name | users.name via family_members.user_id |
Clickable → Student Detail Page |
| Level | Current enrollment level from user_link_level_tag |
Level badge |
| Subscription Status | subscriptions.status for this user (or Stripe status if no local record) |
Badge: Active (green) / Past Due (orange) / Cancelled (gray) / None (red) |
| Plan | payment_plans.name via subscriptions.payment_plan_id |
Plan name or "No subscription" |
| Joined Family | family_members.joined_at |
Formatted date |
| Actions | — | "View Student" link, "Remove" icon (Admin only) |
6.6 Actions
| Action | Trigger | Permission | Confirmation |
|---|---|---|---|
| Create Family | "Create Family" button (list view, top-right) | Admin | Modal with fields: Family Name (text), Primary Contact (student search autocomplete), Initial Members (multi-student search, minimum 2), Discount Coupon (dropdown from existing coupons or "Create new"). On save: creates family_groups record, creates family_members records, applies discount coupon to each member's Stripe subscription. |
| Add Member | "Add Member" button (detail view) | Admin | Student search autocomplete → select student → confirmation: "Add [name] to [family]? The family discount ([X]% off) will be applied to their subscription." On confirm: creates family_members record, applies family discount coupon to student's Stripe subscription. |
| Remove Member | Remove icon on member row (detail view) | Admin | Confirmation: "Remove [name] from [family name]? Their family discount will be removed." On confirm: deletes family_members record, removes coupon from student's Stripe subscription. |
| Edit Family | "Edit" button (detail view header) | Admin | Modal pre-filled: edit Family Name, Primary Contact, Discount Coupon. Does NOT edit member list (use Add/Remove for that). |
| Delete Family | "Delete" button (detail view header) | Admin | Destructive confirmation: "Delete [family name]? All [N] members will have their family discount removed. This cannot be undone." On confirm: removes coupon from all members' Stripe subscriptions, deletes family_members records, deletes family_groups record. |
| Change Discount | "Change Discount" link (detail view header, next to discount) | Admin | Coupon selector dropdown → select new coupon → confirmation: "Change family discount from [old]% to [new]%? This will update all [N] members' subscriptions." On confirm: updates family_groups.discount_coupon_id, removes old coupon and applies new coupon to all members' Stripe subscriptions. |
| View Member | Student name click (detail view) | All roles with access | Navigates to Student Detail Page |
6.7 States
| State | Display |
|---|---|
| Has families (list view) | Family list data table |
| No families (list view) | "No family plans configured." + "Create Family" button |
| Family detail — all active | Green status bar: "All [N] members have active subscriptions." |
| Family detail — issues | Orange banner: "[X] of [Y] members have billing issues." with affected member rows highlighted (orange left border) |
| Family detail — single member | Warning: "This family has only 1 member. Add another member or delete this family." |
| Loading | Skeleton data table (list) or skeleton header + table (detail) |
| Error | "Unable to load family plans. Please try again." + "Retry" button |
6.8 Role Visibility
- Admin: Full CRUD (Create, Edit, Delete families; Add, Remove members; Change Discount)
- View-Only / Support Staff: View family list and detail views. "View Student" links work. No Create, Edit, Delete, Add, Remove, or Change Discount actions.
- TA: Hidden (sidebar item not visible)
7. Screen: Scholarships & Deferments (NEW)
7.1 Purpose
Manage students on special billing arrangements. Replaces Google Sheets tracking. Eliminates workarounds W3 (scholarships) and W4 (deferments).
This screen has two sub-sections presented as tabs within the screen (horizontal tab bar below page header, using Tab pattern from 01-global-patterns.md Section 4.11).
7.2 Entry Points
- Sidebar: Billing & Payments > Scholarships & Deferments
- Student Detail > Payments tab: scholarship or deferment link
- Payment Overview: "Expiring Scholarships" section → "Extend" or "View Student"
- Payment Overview: "Deferment Resume Due" section → "Extend" or "Confirm Resume"
7.3 Sub-Tab: Scholarships
Assumption (OQ2): ~15-30 scholarship students, 3 tiers (Full, 50%, 25%).
Two views within the Scholarships tab:
View toggle: "Programs" | "Students" toggle at top of tab content (default: Programs).
7.3.1 Scholarship Programs View
Manages the program definitions themselves.
| Column | Source | Notes |
|---|---|---|
| Program Name | scholarship_programs.name |
e.g., "Full Scholarship", "50% Scholarship", "25% Scholarship" |
| Discount | scholarship_programs.discount_percentage |
Formatted as "100%", "50%", "25%" |
| Eligible Plans | scholarship_programs.eligible_plan_types |
Comma-separated list of plan types (e.g., "semester, year") |
| Students | Count of student_scholarships where scholarship_program_id = this program and renewal_status in ('Active', 'Expiring') |
Number (clickable → switches to Students view filtered by this program) |
| Total Discount Value (NEW) | Computed: sum of (original plan amount × discount percentage) for all active students in this program for the current semester | Formatted as currency (e.g., "$450.00 this semester") |
| Renewal Policy | scholarship_programs.renewal_policy |
"Auto-renew" / "Manual" / "One-time" |
| Description | scholarship_programs.description |
Truncated to 50 chars with tooltip for full text |
| Actions | — | Edit / Delete icons (Admin only) |
7.3.2 Scholarship Students View
Manages individual student scholarship assignments.
| Column | Source | Notes |
|---|---|---|
| Student Name | users.name via student_scholarships.user_id |
Clickable → Student Detail > Payments tab |
| Program | scholarship_programs.name via student_scholarships.scholarship_program_id |
Program name |
| Discount | scholarship_programs.discount_percentage |
e.g., "50%" |
| Start Date | student_scholarships.start_date |
Formatted date |
| End Date | student_scholarships.end_date |
Formatted date. Red text if past. |
| Renewal Status | student_scholarships.renewal_status |
Badge: "Active" (green) / "Expiring" (orange, when end_date within 30 days) / "Expired" (gray) |
| Stripe Coupon Applied? | Verify: student's active subscription in Stripe has a coupon matching student_scholarships.coupon_id |
Yes (green badge) / No (red badge with warning icon) |
| Actions | — | "Extend" / "Remove" / "View Student" |
Filter bar (Students view only):
| Filter | Type | Options |
|---|---|---|
| Program | Dropdown | All program names |
| Status | Dropdown | Active / Expiring / Expired |
| Search | Text input | Student name or email |
7.3.3 Scholarship Actions
| Action | Trigger | Permission | Behavior |
|---|---|---|---|
| Create Program | "Create Program" button (Programs view) | Admin | Modal with fields: Program Name (text), Discount Percentage (number, 1-100), Eligible Plan Types (multi-select: semester, year, family, discount), Description (text area), Renewal Policy (dropdown: Auto-renew / Manual / One-time). Creates scholarship_programs record. |
| Edit Program | Edit icon on program row | Admin | Modal pre-filled with program data. All fields editable. Warning if Students > 0: "This program has [N] active students. Discount changes will take effect at next billing cycle." |
| Delete Program | Delete icon on program row | Admin | Blocked if Students > 0: button disabled with tooltip "Cannot delete program with assigned students. Remove all students first." If 0 students: destructive confirmation "Delete program [name]? This cannot be undone." |
| Assign Student | "Assign Student" button (Students view) | Admin | Modal: Student search autocomplete → Select scholarship program (dropdown) → Set start date (default: today) → Set end date (required) → On save: creates student_scholarships record, creates Stripe coupon matching program discount if not exists, applies coupon to student's Stripe subscription via API. Toast: "[Student name] assigned to [program name]. Stripe coupon applied." |
| Remove Student | "Remove" on student row | Admin | Confirmation: "Remove [name] from [program name]? Their scholarship discount will be removed from Stripe." On confirm: sets renewal_status to 'Expired', removes coupon from student's Stripe subscription. |
| Extend Scholarship | "Extend" on student row | Admin | Date picker modal for new end date (must be after current end_date). Updates student_scholarships.end_date. Sets renewal_status back to 'Active' if was 'Expiring'. |
| Renew All Expiring | "Renew All" button (Students view, appears only when expiring scholarships exist) | Admin | Confirmation: "Renew [N] expiring scholarships? End dates will be extended by one semester." On confirm: batch updates all student_scholarships with renewal_status = 'Expiring': sets new end_date to current semester end + 1 semester, sets renewal_status to 'Active'. |
| View Student | "View Student" on student row or name click | All roles with access | Navigates to Student Detail > Payments tab |
7.4 Sub-Tab: Deferments
Assumption (OQ3): ~5-10 per semester, 1-semester typical. Uses Stripe pause_collection.
Two sections within the Deferments tab:
Active Deferments (always visible):
| Column | Source | Notes |
|---|---|---|
| Student Name | users.name via student_deferments.user_id |
Clickable → Student Detail > Payments tab |
| Pause Start | student_deferments.pause_start |
Formatted date |
| Resume Date | student_deferments.resume_date |
Formatted date. Orange text if within 14 days. |
| Reason | student_deferments.reason |
Free text (truncated to 40 chars, tooltip for full) |
| Duration | Calculated: resume_date minus pause_start |
"[N] days" or "[N] months" |
| Status | student_deferments.status |
Badge: "Active" (blue/purple) / "Resuming Soon" (orange, when resume_date within 14 days) |
| Stripe Paused? | Verify: student's subscription has pause_collection set and resumes_at matches resume_date |
Yes (green badge) / No (red badge with warning icon) |
| Created By | users.name via student_deferments.created_by |
Admin who created the deferment |
| Actions | — | "Extend" / "Resume Now" / "View Student" |
Deferment History (collapsed by default, expandable):
| Column | Source | Notes |
|---|---|---|
| Student Name | users.name via student_deferments.user_id |
Clickable → Student Detail > Payments tab |
| Pause Period | student_deferments.pause_start – actual end date |
Formatted date range (e.g., "Jan 20 – Mar 20, 2026") |
| Reason | student_deferments.reason |
Free text |
| Duration | Calculated: actual end minus pause_start |
"[N] days" |
| Outcome | Derived from student_deferments.status and subscription state |
"Resumed" / "Cancelled" / "Extended" |
7.4.1 Deferment Actions
| Action | Trigger | Permission | Behavior |
|---|---|---|---|
| Create Deferment | "New Deferment" button (top of deferments tab) | Admin | Modal with fields: Student (search autocomplete — only shows students with active subscriptions), Pause Start Date (date picker, default: today), Resume Date (date picker, required), Reason (text area, required). On save: creates student_deferments record with status='Active', calls Stripe API to set subscription.pause_collection = { behavior: 'void', resumes_at: [resume_date] }. Toast: "Deferment created for [name]. Subscription paused in Stripe." |
| Extend Deferment | "Extend" on active deferment row | Admin | Date picker modal for new resume date (must be after current resume_date). Updates student_deferments.resume_date, calls Stripe API to update subscription.pause_collection.resumes_at to new date. Toast: "Deferment extended to [new date]." |
| End Deferment Early | "Resume Now" on active deferment row | Admin | Confirmation: "Resume billing for [name] immediately? Their next invoice will be generated." On confirm: clears Stripe pause_collection (billing resumes immediately), updates student_deferments.status to 'Completed', sets actual end date to today. Toast: "Billing resumed for [name]." |
| View Student | Student name click or "View Student" link | All roles with access | Navigates to Student Detail > Payments tab |
7.5 States
| State | Display |
|---|---|
| Scholarships — has programs and students | Programs list + students list (togglable views) |
| Scholarships — has programs, no students | Programs list shown. Students view: "No students assigned to scholarship programs." + "Assign Student" button |
| Scholarships — empty | "No scholarship programs configured." + "Create Program" button |
| Deferments — has active | Active deferments table shown expanded. History section below (collapsed by default). |
| Deferments — none active, has history | Active section: "No active deferments." History section available (collapsed). |
| Deferments — none active, no history | "No deferment records." + "New Deferment" button |
| Loading | Skeleton tables for active tab |
| Error | "Unable to load [scholarships/deferments]. Please try again." + "Retry" button |
| Stripe sync mismatch | For any row where local record disagrees with Stripe state (e.g., coupon not applied, pause not set): red "No" badge in the Stripe verification column with warning icon. Tooltip: "Local record does not match Stripe. Click to investigate." → opens Student Detail > Payments tab. |
7.6 Role Visibility
- Admin: Full CRUD on both sub-tabs (Create/Edit/Delete programs; Assign/Remove/Extend/Renew students; Create/Extend/Resume deferments)
- View-Only / Support Staff: View all data on both sub-tabs. "View Student" links work. No action buttons for Create, Edit, Delete, Assign, Remove, Extend, Renew, or Resume.
- TA: Hidden (sidebar item not visible)
8. Cross-Domain Integration Points
This section documents how Billing & Payments connects to other domains. Each integration is specified in detail in the target domain's spec document; this section provides a summary for reference.
8.1 Dashboard (02-dashboard.md)
| Dashboard Element | Links To | Data Source |
|---|---|---|
| Failed Payments alert tile | Payment Overview, filtered to Failed Payments section | billing_alerts where type=invoice.payment_failed and status='new' |
| Past Due alert tile | Payment Overview, filtered to Past Due section | billing_alerts where type=customer.subscription.updated and data contains past_due status |
8.2 Student Detail > Payments Tab (03-student-management.md)
The individual-level billing view. Shows a single student's:
- Current subscription status (from Stripe, real-time)
- Payment history (invoices from Stripe)
- Active scholarship (from
student_scholarships) - Active deferment (from
student_deferments) - Family membership (from
family_members) - Actions: Cancel Subscription, Apply Coupon, Create Deferment, linked deactivation
8.3 Semester Hub > Close Workflow Step 3 (04-semester-management.md)
Semester close billing step:
- Creates/updates Stripe subscriptions for promoted students
- Handles plan changes for level transitions
- Uses
payment_plansto determine pricing - Applies existing coupons (scholarship, family) to new subscriptions
8.4 Student Detail > Actions Tab (03-student-management.md)
Linked deactivation (W8):
- "Deactivate Student" action checks for active Stripe subscription
- If found: combines account deactivation + Stripe subscription cancellation in one action
- Uses
subscriptions.stripeSubscriptionIdto call Stripe cancel API
9. Webhook Processing
Billing & Payments depends on Stripe webhooks to populate the billing_alerts table and keep cached data current. This section documents the webhook events consumed.
| Stripe Event | billing_alerts.type |
Processing |
|---|---|---|
invoice.payment_failed |
invoice.payment_failed |
Create alert with user_id (resolved from Stripe customer_id via customer ID resolution chain — 01-global-patterns.md Section 5.2), store invoice amount, decline code, attempt count in data JSON. |
customer.subscription.updated |
subscription.updated |
If status changed to past_due: create alert. If status changed to active from past_due: resolve existing past_due alert. |
customer.subscription.deleted |
customer.subscription.deleted |
Create alert with cancellation details. Used by Recently Cancelled section. |
customer.subscription.created |
customer.subscription.created |
Create alert (informational). Used by New Subscriptions metric. |
invoice.paid |
invoice.paid |
Update local payment records. Resolve any existing invoice.payment_failed alerts for this subscription. |
customer.subscription.paused |
subscription.paused |
Verify against student_deferments — if no matching local record, create alert for investigation. |
customer.subscription.resumed |
subscription.resumed |
Update student_deferments.status to 'Completed' if matching record exists. |
Alert lifecycle: new → acknowledged (admin has seen it) → resolved (issue addressed or auto-resolved by subsequent event). Alerts auto-resolve when a corrective event arrives (e.g., invoice.paid resolves prior invoice.payment_failed for the same subscription).
10. Assumptions and Open Questions
This domain relies on several assumptions documented in 00-spec-index.md. Summary of billing-specific assumptions:
| ID | Assumption | Impact on This Spec | Validation Needed |
|---|---|---|---|
| OQ1 | ~10-20 family plans per semester | Full CRUD for Family Plans (Section 6). If <5, could simplify to tagging. | Confirm with admin: actual family count. |
| OQ2 | ~15-30 scholarship students, 3 tiers | Program-based management (Section 7.3). If only 1-2 tiers, could simplify to a per-student flag. | Confirm with admin: scholarship tier count and student volume. |
| OQ3 | ~5-10 deferments per semester | Lightweight management with Stripe pause_collection (Section 7.4). If >20, may need bulk tools. |
Confirm with admin: typical deferment volume. |
| OQ4 | QuranFlow subscriptions table is canonical |
All local queries use subscriptions table. Stripe API used for real-time verification and actions only. |
Confirm: is customer ID reconciliation complete? |
| OQ6 | Single Stripe account (fe-checkout) | No multi-account routing needed. Single API key configuration. | Confirm: is there a second Stripe account for any purpose? |
New questions raised by this spec (to be logged in 00-spec-index.md if not already covered):
- BQ1: Should "Send Reminder" on failed payments use email or SMS? Assumed: email via existing Email Management infrastructure.
- BQ2: Should deferment creation require manager approval or is admin self-service sufficient? Assumed: admin self-service (the admin IS the decision-maker per interview).
- BQ3: When a family discount coupon is changed, should it take effect immediately or at next billing cycle? Assumed: next billing cycle (Stripe default behavior for coupon changes).