v2 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:

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 End Checklist Step 4: Set Up Automatic Payments (04-semester-management.md §3.8) + End Checklist Payment Setup Queue (§2.9 of this file)
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):

New tables (assumptions from WS2 — to be validated):

Stripe API endpoints used:

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

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

2.9 End Checklist Payment Setup Queue (NEW)

2.9.1 Purpose

Per-student payment setup surface for the currently-closing semester's End Checklist Step 4: Set Up Automatic Payments (see 04-semester-management.md §3.8). Shows each eligible passing L1–L4 student's payment_setup_status (Pending / Created / Failed / Exempted), lets the admin trigger Stripe subscription creation individually or in bulk, and auto-completes Step 4 once every eligible student is created (or exempted).

2.9.2 Scope

Students where user_link_level_tag.final_status = 'pass' AND level IN (1, 2, 3, 4) for the closing semester.

2.9.3 Entry Point

2.9.4 Data Displayed

Column Source Sortable Filterable Format
Student Name users.name via enrollment Yes (default: alphabetical) Yes (search) Clickable → Student Detail > Payments tab
Level level_tags.name via user_link_level_tag Yes Yes (dropdown: Level 1 / Level 2 / Level 3 / Level 4) Level badge
Final Status user_link_level_tag.final_status for closing semester Yes No (scope-locked to pass) Badge: Pass (green)
Payment Setup Status subscriptions.next_semester_setup_status (inferred from Stripe subscription state for the student's next-semester subscription) Yes Yes (dropdown: Pending / Created / Failed / Exempted) Badge: Pending (amber) / Created (green) / Failed (red) / Exempted (gray)
Last Attempt Timestamp of most recent setup attempt (success or failure) Yes No "Mar 15, 2026 at 11:02 AM" or "—" if never attempted
Action No No "Set Up Payment" button (Pending/Failed) / "View" link (Created) / "—" (Exempted)

2.9.5 Data Model Note

subscriptions.next_semester_setup_status enum values: 'pending' / 'created' / 'failed' / 'exempted'. This attribute is inferred from Stripe subscription state for the student's next-semester subscription:

2.9.6 Filter Bar

Filter Type Options
Search Text input Student name or email
Level Dropdown All / Level 1 / Level 2 / Level 3 / Level 4
Payment Setup Status Dropdown All / Pending / Created / Failed / Exempted (default: Pending + Failed)

2.9.7 Actions

Action Trigger Permission Behavior
Set Up Payment (per-student) "Set Up Payment" button on a Pending or Failed row Admin Triggers Stripe subscription creation for this student starting on the next semester's start_date, using the student's existing payment method and the plan inherited from their current subscription (adjusted for new level if applicable). Updates next_semester_setup_status to created on success or failed on error. Toast: "Payment setup created for [student name]." (or error toast with Stripe decline reason on failure).
Set Up Payments for Selected (bulk) Checkbox selection + "Set Up Payments for Selected" button in the bulk action bar Admin Queues Stripe subscription creation for all selected Pending/Failed students. Confirmation: "Set up payments for [N] passing students?" Progress indicator during batch. On completion, shows summary: "[X] created, [Y] failed." Failed rows remain in the queue with failed status for re-triage.
View (per-student) "View" link on a Created row Admin, View-Only Opens Student Detail > Payments tab, anchored to the next-semester subscription row.
Mark Exempted Row overflow menu → "Mark Exempted" Admin Modal: Reason (required text, e.g., "Full scholarship — no payment method needed"). On save: sets next_semester_setup_status to exempted. Reversible via "Clear Exemption" in the same overflow menu (which returns the row to pending).
Refresh Queue "Refresh" button (top-right of queue) Admin Re-queries Stripe for each in-flight subscription and re-computes statuses. Useful after failed attempts are corrected externally.
Return to End Checklist Persistent banner at top when ?from=end-checklist is set Admin "← Return to End Checklist for [Semester Name]." Clicking returns to 04-semester-management.md §3.8 with scroll position preserved.

2.9.8 States

State Display
All created Green status bar: "All [N] eligible passing L1–L4 students have payment setup complete for [Next Semester Name]." Step 4 in End Checklist auto-completes.
In progress Mix of Pending / Created / Failed rows. Counts summarized above the table: "[X] Pending · [Y] Created · [Z] Failed · [W] Exempted".
No eligible students "No passing L1–L4 students for [Semester Name]. Step 4 does not apply this semester." Covered when Step 2 hasn't produced any passes yet or an unusual semester has no L1–L4 enrollment.
Failed-attempts pending If any rows are failed, amber banner: "[N] students failed payment setup. Review the Last Attempt column and retry after correcting the underlying issue (e.g., expired card)."
Loading Skeleton data table.
Error "Unable to load the Payment Setup Queue. Please try again." + "Retry" button.

2.9.9 Role Visibility

2.10 Repeat Checkout Flow (cross-reference, NEW)

2.10.1 Context

The End Checklist Step 3 fail email (see 04-semester-management.md §3.8) includes an opt-in-to-repeat link for failing L1–L4 students. Clicking that link sends the student to a repeat-discount checkout (50% off the next semester's tuition) — this is an existing production flow preserved in v2.

2.10.2 Admin touchpoints

  1. Auto-eligibility: When Step 3 sends a fail email to a student, the system writes a semester_repeat_eligibility row with source='auto_from_fail' for the next semester. No admin action required.
  2. Manual override: Students who skip a semester and want to repeat a later one are NOT on the auto-generated list. Admin adds them via the per-student "Mark as Repeat-Eligible" action on Student Detail > Actions (see 03-student-management.md §3.10). This writes a semester_repeat_eligibility row with source='manual_override' and records added_by_admin_id.
  3. Checkout validation (production): The public repeat-checkout URL looks up semester_repeat_eligibility for the student + semester. If no row exists, checkout rejects with "You are not currently eligible for the repeat discount. Please contact support." If a row exists, the 50% discount is applied.
  4. Visibility: Students with enrollment_type='Repeat' are surfaced on the Students list via the Enrollment Type filter (see 03-student-management.md §2.4). The repeat-eligible list itself is not exposed as a standalone admin screen — it is a data projection of the enrollment records + the semester_repeat_eligibility table.
  5. Approval: No admin approval is required for the student-side opt-in. The student self-serves the checkout once on the eligibility list (confirmed STAKEHOLDER-ANSWERS-2026-04-22 §C Q2).

2.10.3 Mockup scope

The "Mark as Repeat-Eligible" modal on Student Detail is the primary interactive surface. The repeat-discount checkout is student-facing and out of scope for the admin mockup.


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

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


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. Plans are presented in grouped sections (Standard / Family / Scholarships / Discounts) so admins can see scholarships and family pricing inline with standard plans; coupons remain on their own screen (Section 5) as a different stakeholder category.

4.2 Entry Points

4.3 Layout

Grouped data tables, one section per category, rendered as stacked collapsible cards (all expanded by default). Each section uses the standard Data Table pattern (01-global-patterns.md Section 4.1). No filter bar (dataset too small — typically <15 plans total across all groups). Page-level actions (Create Plan, Verify Stripe Sync) live in the page header and apply across all groups.

Group sections (in order):

Group Source filter Description
Standard payment_plans.type in ('semester', 'year') Default pricing tiers offered at checkout
Family payment_plans.type = 'family' Multi-student family pricing; cross-linked to Family Plans (Section 6)
Scholarships payment_plans.type = 'discount' AND linked to a scholarship_programs record via the associated coupon Discount plans funded through scholarship programs; cross-linked to Scholarships & Deferments (Section 7)
Discounts payment_plans.type = 'discount' AND not linked to a scholarship program Generic discount plans (promotional, seasonal, etc.)

Each group section header shows the group name and a count (e.g., "Standard (4)"). Empty groups collapse to a one-line "No [group] plans configured." message.

4.4 Data Displayed

Columns are identical across all four group tables:

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 (page header, top-right) Admin Opens modal with fields: Name, Type (dropdown: semester/year/family/discount — determines group placement), 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." Changing Type moves the plan to the corresponding group section.
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."
Verify Stripe Sync (NEW) "Verify Stripe Sync" button (page header, secondary) Admin Iterates all plans across all groups, checks each product_id against Stripe Prices API. Opens a result modal titled "Stripe Sync Results" containing a table with columns: Plan Name, Group, Status (Synced / Mismatch / Not Found — color-coded badge), Details. Mismatches show details such as "Local: $15.00 USD, Stripe: $20.00 USD" or "Currency mismatch (Local: USD, Stripe: CAD)." Not Found rows show "No Stripe Price matches product_id [id]." Modal footer: "Close" button and "Export Report" (CSV download). A lightweight toast ("Sync check complete — [N] issues found") appears simultaneously so the admin can dismiss the modal and still see a summary.

4.6 States

State Display
Loaded Four grouped data tables, each showing plans in its category with a header count (e.g., "Standard (4)"). Groups with zero plans show "No [group] plans configured." inline.
Empty (no plans at all) All four group sections collapsed to "No plans configured." + single page-level "Create Plan" button
Loading Skeleton headers + skeleton data tables for each group
Error "Unable to load payment plans. Please try again." + "Retry" button
After Stripe Sync Result modal (see §4.5 Verify Stripe Sync) remains open until admin closes it. Rows with Mismatch / Not Found in the main tables gain a warning icon in the Stripe Price ID column.

4.7 Role Visibility


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

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


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

6.3 Layout

Two views:

  1. Family List View (default): Data table of all family groups
  2. 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") — this is the default discount inherited by members unless overridden per-member
Created family_groups.created_at
Member Count Count of family_members

Status bar (below header):

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
Semester Status (NEW) family_members.semester_status Badge: "Regular" (blue — continuing student), "Repeat" (orange — repeating previous level), "Mastery" (purple — mastery course enrollment). Editable inline via "Change status" action in row overflow menu.
Member Discount % (NEW) family_members.member_discount_percentage Percentage (e.g., "25%"). Shown with a small inherit indicator if it equals family_groups.discount_coupon_id.percentage_off (e.g., "25% (default)"). If overridden per-member, shown as "30% (custom)". Editable via "Change Discount" action in row overflow menu.
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), overflow menu with "Change Discount" and "Change Status" (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 — each row captures per-member Semester Status [Regular/Repeat/Mastery] and an optional Member Discount % override), Discount Coupon (dropdown from existing coupons or "Create new" — this is the family default). On save: creates family_groups record, creates family_members records with semester_status and member_discount_percentage (defaulting to family discount when no override given), applies appropriate discount coupon to each member's Stripe subscription.
Add Member "Add Member" button (detail view) Admin Modal: Student search autocomplete → select student → Semester Status dropdown (Regular/Repeat/Mastery, required) → Member Discount % (number input, pre-filled with family default; admin can override) → confirmation summary: "Add [name] to [family] as a [status] member with [X]% discount?" On confirm: creates family_members record with semester_status and member_discount_percentage, applies discount coupon matching the member-level discount 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, Default Discount Coupon. Does NOT edit member list or per-member overrides (use Add/Remove or the per-row actions 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 Family Default Discount "Change Discount" link (detail view header, next to discount) Admin Coupon selector dropdown → select new coupon → confirmation: "Change family default discount from [old]% to [new]%? This updates the family default and re-applies to any member whose discount is still inheriting the default. Members with custom per-member overrides are unchanged." On confirm: updates family_groups.discount_coupon_id, re-applies new coupon to members whose member_discount_percentage equals the old default; leaves custom-override members alone.
Change Member Discount (NEW) "Change Discount" in member row overflow menu Admin Modal: Member Discount % (number input, pre-filled with current value). Options: "Set custom %" (overrides inheritance) or "Reset to family default" (drops override, inherits again). On confirm: updates family_members.member_discount_percentage, applies corresponding coupon on that member's Stripe subscription. Toast: "[Name]'s discount updated to [X]%."
Change Member Semester Status (NEW) "Change Status" in member row overflow menu Admin Dropdown: Regular / Repeat / Mastery. On confirm: updates family_members.semester_status. No Stripe-side effect unless combined with a plan change (which is done via Student Detail). Toast: "[Name] marked as [status] for this semester."
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."
Family detail — mixed discounts Info line near header: "[X] member(s) use the family default discount; [Y] use a custom discount override." (shown only when at least one member has a custom override)
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


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

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


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:

8.3 End Checklist Step 4: Set Up Automatic Payments (04-semester-management.md)

End-of-semester billing handoff:

8.4 Student Detail > Actions Tab (03-student-management.md)

Linked deactivation (W8):


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):