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

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


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.

4.2 Entry Points

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


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


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 Semester Hub > Close Workflow Step 3 (04-semester-management.md)

Semester close billing step:

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