Billing & Payment Discovery

Workstream 2: Billing & Payment Discovery

Purpose: Comprehensive technical and operational discovery of QuranFlow's billing landscape — what exists today, what the admin needs, what Stripe can provide, and what must be custom-built. Scope: Steps 2.1 through 2.5 of the QuranFlow Admin Backend Redesign Plan. Date: March 2026 Sources: Code audit of 5 GitHub repos, Admin Navigation Interview Report, Admin Capability Map (including Appendix A schema)


Table of Contents

  1. Step 2.1 — Checkout Repo Audit
  2. Step 2.2 — Current Billing Workarounds
  3. Step 2.3 — Stripe Data Available via API
  4. Step 2.4 — Billing Screen Requirements
  5. Step 2.5 — Billing Integration Brief

Step 2.1 — Checkout Repo Audit

Architecture Overview

QuranFlow's billing is distributed across five repositories that form a multi-layered checkout and payment system. The architecture is split between the legacy QuranFlow-specific backend (Yii2/PHP) and a newer, shared checkout platform (Laravel) that serves multiple AlMaghrib products.

                    Student-Facing                          Backend Services
                    ─────────────                          ────────────────

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────────┐
│  fe-checkout     │────▶│  fe-checkout-     │────▶│  quranflow-backend   │
│  (QF checkout    │     │  backend          │     │  (Yii2 admin panel)  │
│  frontend+Stripe)│     │  (webhook handler,│     │  Subscriptions table │
│                  │     │  session tracking)│     │  Payment Plans       │
│  Laravel/Vue     │     │  Laravel          │     │  Coupons             │
└──────────────────┘     └──────────────────┘     │  StripeIntegration   │
                                                   └──────────────────────┘
┌──────────────────┐
│  alm-checkout    │     (New multi-product checkout — not yet primary for QF)
│  Laravel/React   │
│  Multi-account   │
│  Stripe          │
└──────────────────┘

┌──────────────────┐
│  wp-plugin-      │     (WordPress shortcode that embeds checkout lightbox)
│  checkout-alm    │
│  React lightbox  │
└──────────────────┘

Repo 1: fe-checkout — QuranFlow Checkout Frontend

Location: almaghrib-engineering/fe-checkout Technology: Laravel 11 + Inertia.js + Vue 3 + Tailwind CSS Primary language: PHP (backend), Vue (frontend) Stripe library: stripe/stripe-php (server-side SDK) Deployment: Docker (Caddy web server), GitHub Actions CI/CD

Stripe Integration Points

Stripe API Method File Purpose
Checkout\Session::create() Hosted checkout StripeController.php Creates hosted Stripe Checkout sessions (subscription mode)
Customer::create() Direct API StripeController.php Creates Stripe customers for card/Google Pay payments
Subscription::create() Direct API StripeController.php Creates subscriptions directly (non-hosted flow)
PaymentIntent::retrieve/confirm() Direct API StripeController.php Confirms payment intents for card payments
Coupon::retrieve() Direct API StripeController.php Validates scholarship coupon before applying
Checkout\Session::retrieve() Verification routes/web.php Verifies payment on success page redirect
Webhook::constructEvent() Webhook WebhookController.php Validates incoming Stripe webhooks

Payment Plans Supported

The checkout supports exactly two plans, configured via environment variables:

Plan Default Price ID Amount Interval
Monthly price_1RjJWe2UpsimWJDwYFubMeVd $15/month Month
Annual price_1RjJZg2UpsimWJDwzrRCTtjl $120/year Year

Source: codebase/config/services.php — prices are mapped by slug (monthly, annual) to Stripe Price IDs.

Coupon/Scholarship Handling

Source: StripeController.php lines involving $hasScholarship, Scholarship.vue

Webhook Handling (fe-checkout)

The WebhookController.php in fe-checkout handles only two events:

Event Action
checkout.session.completed Logs payment success, marks local session as completed
invoice.payment_succeeded Logs subscription payment success

Critical gap: This webhook handler does not process subscription lifecycle events (cancellation, failure, past_due). These events are only handled in fe-checkout-backend.

Subscription Duplicate Prevention

Before proceeding to payment, the checkout calls /check-user-subscription which routes through the Laravel backend to fe-checkout-backend's /api/checkout/fe/user-active-subscription endpoint. If an active subscription is found for the email, the checkout displays an alert and prevents duplicate purchase. This is documented in docs/SUBSCRIPTION_CHECK_IMPLEMENTATION.md.

Source: codebase/routes/web.php, docs/SUBSCRIPTION_CHECK_IMPLEMENTATION.md

Data Model (fe-checkout)

fe-checkout stores no local billing data. It uses Laravel sessions for checkout state and relies entirely on:

Key Files Inventory

File Purpose
codebase/app/Http/Controllers/StripeController.php All Stripe API interactions: hosted checkout, direct subscriptions, payment intents, express checkout
codebase/app/Http/Controllers/WebhookController.php Stripe webhook receiver (minimal — only logs)
codebase/routes/web.php Checkout flow orchestration: session management, Stripe session creation, success verification
codebase/app/Services/AlMaghribApiService.php API client to fe-checkout-backend for session tracking and subscription checks
codebase/config/services.php Stripe keys, price IDs, scholarship coupon ID
codebase/resources/js/Pages/Checkout/Checkout.vue Checkout UI (plan selection, email, payment)
codebase/resources/js/Pages/Checkout/Scholarship.vue Scholarship discount reveal page
docs/SUBSCRIPTION_CHECK_IMPLEMENTATION.md Documents the pre-checkout subscription check flow

Repo 2: fe-checkout-backend — Checkout Backend Services

Location: almaghrib-engineering/fe-checkout-backend Technology: Laravel 11 (modular architecture) Primary language: PHP (Blade templates for emails) Databases: PostgreSQL (own DB), plus connections to alm_db_core (users) and alm_db_fe (subscriptions)

Role in the System

This is the central webhook processor and session tracker for the QuranFlow checkout. It:

  1. Tracks checkout sessions (IP, email, referral URL, status)
  2. Receives the invoice.paid webhook from Stripe
  3. Creates or updates user accounts in the core database
  4. Creates subscription records in the FE database
  5. Sends welcome/onboarding emails

Stripe Webhook Handling (THE critical handler)

The StripeService.php in this repo is the primary webhook handler. It processes:

Event Action
invoice.paid The ONLY event actively processed. Triggers user creation (if new), subscription creation, welcome email, checkout status update to "paid"

All other Stripe events are ignored (returns { "ignored": true }).

Subscription cancellation handling: There is commented-out code for customer.subscription.deleted that would handle unsubscription with refund logic. This code handles: subscription status update, refund processing for annual plans within 30 days, cancellation/refund emails. However, this is entirely commented out and not active.

Webhook Flow Detail (invoice.paid)

  1. Extract invoice data and line item metadata (plan type, email, checkout_session_id)
  2. Determine internal plan_id:
    • Annual + 50% coupon = SCHOLARSHIP_PLAN_ID (69)
    • Annual without coupon = ANNUAL_PLAN_ID (21)
    • Monthly = MONTHLY_PLAN_ID (29)
  3. Check if user exists in alm_db_core.users by email
    • If not: create user with generated password, send welcome email
    • If exists: check for active subscription — if already active, ignore (dedup)
  4. Check/create subscription in alm_db_fe.subscriptions (status = active)
  5. Create subscription_signups record with Stripe IDs
  6. Create fe_onboarding_emails tracking record
  7. Update checkout session status from "draft" to "paid"

Data Model (fe-checkout-backend — own database)

Table Fields Purpose
checkouts id, email, status (draft/paid), ip_info_id, referral_url Tracks checkout sessions
checkout_ip_info id, ip, geo_city, geo_country, geo_a2 GeoIP data for analytics
checkout_items id, checkout_id, relatable_id, relatable_type (fe/event/card), currency_id, type (oneoff/recurring), is_primary Items in a checkout
checkout_recurring_fe id, checkout_item_id, email, amount FE subscription signup details per checkout item
checkout_oneoff_events id, checkout_item_id, email, gender, dob, phone, location_host_id Event registration details

Data Model (fe-checkout-backend writes to external databases)

alm_db_core.users — Creates new user accounts:

alm_db_fe.subscriptions — Main subscription record:

alm_db_fe.subscription_signups — Individual signup records:

alm_db_fe.fe_onboarding_emails — Email sequence tracking:

API Endpoints

Endpoint Method Purpose
POST /api/checkout Create checkout session
PUT /api/checkout/{id} Update checkout session (add email)
POST /api/checkout/{id}/items Add items to checkout session
POST /api/checkout/fe/subscription Create FE subscription directly
POST /api/checkout/fe/user-active-subscription Check if user has active subscription
POST /api/checkout/stripe/webhook Stripe webhook handler
POST /api/checkout/stripe/webhook/unsubscribe Unsubscription webhook (handler exists but logic is commented out)

Key Files Inventory

File Purpose
Modules/Checkout/Services/StripeService.php Critical: Stripe webhook handler, user provisioning, subscription creation
Modules/Checkout/Services/CheckoutService.php Checkout session CRUD
Modules/Checkout/Services/CheckoutItemService.php Checkout item management, subscription check
Modules/Checkout/Http/Controllers/StripeController.php Webhook endpoint controller
Modules/Checkout/Routes/api.php API route definitions
Modules/Mail/Services/FeOnboardingEmailService.php Welcome and onboarding email logic

Repo 3: quranflow-backend — QuranFlow Admin Panel

Location: almaghrib-engineering/quranflow-backend Technology: Yii2 (PHP framework) Primary language: PHP Stripe library: Stripe PHP SDK (vendored in backend/components/stripe/)

Role in the System

This is the admin panel that the QuranFlow admin uses daily. It has its own Stripe integration layer and local subscription management, but these are legacy systems primarily used for manual subscription operations rather than automated checkout.

Stripe Integration (StripeIntegrationComponent)

Located at common/components/StripeIntegrationComponent.php, this component provides direct Stripe API calls:

Method Stripe API Purpose
createCustomer() Customer::create() Create Stripe customer with token
createCustomerNew() Customer::create() Create customer with email only
createCharge() Charge::create() One-time charge
createSubscription() Subscription::create() Create subscription with coupon
createSubscriptionWithoutCoupon() Subscription::create() Create subscription without coupon
createCoupon() Coupon::create() Create Stripe coupon (synced from admin Coupons section)
createStripeProductUsingSdk() Product::create() Create Stripe product
createStripeProduct_PlanUsingSdk() Plan::create() Create Stripe plan
retrieveCustomerByEmail() Customer::all() Look up customer by email
getAllProducts() Product::all() List all products

Subscription Data Model (subscriptions table)

Field Type Notes
id int Primary key
stripeSubscriptionId string Stripe subscription ID
user_link_level_tag_id int FK to enrollment record (ties subscription to a specific student+semester+level)
payment_plan_id int FK to payment_plans table
coupon_id int FK to coupons table
status int Subscription status (1 = active)
start_date date Subscription start
end_date date Subscription end
next_billing_date date Next billing cycle
cycles int Total billing cycles
cycles_remaining int Remaining cycles
membership int Membership type
created_at datetime
updated_at datetime

Important: The subscription is linked to user_link_level_tag_id, NOT directly to user_id. This means subscriptions are per-enrollment (student + semester + level), not per-student. This has implications: if a student is promoted to a new level or enrolls in a new semester, their subscription link changes.

Users Table (billing-relevant fields)

The users table contains several billing-relevant fields:

Field Type Notes
stripe_customer_id string Stripe customer ID — inconsistently populated (see below)
google_in_app_purchase_subscription_state string Google Play subscription state (e.g., SUBSCRIPTION_STATE_ACTIVE) — not declared in model docblock
google_in_app_purchase_subscription_expiry_time string Google Play subscription expiry — not declared in model docblock
user_activation_status int 0=blocked, 1=active — used for deactivation

stripe_customer_id population gap: The checkout flow in fe-checkout creates Stripe customers directly and passes the customer ID to fe-checkout-backend, which stores it in subscription_signups.pp_customer_id, NOT in users.stripe_customer_id. Only manual admin-created subscriptions (via the subscription modal) populate the users table field. This means most students who enrolled through the checkout have their Stripe customer ID stored in a different database.

Google IAP fields: The GoogleInAppPurchase.php helper writes to google_in_app_purchase_subscription_state and google_in_app_purchase_subscription_expiry_time via Yii2's dynamic property access, but these fields are not formally declared in the User.php model class. They exist as database columns only.

Source: common/models/User.php (line 30: @property string $stripe_customer_id), common/helpers/GoogleInAppPurchase.php

Subscription Types and Pricing

The Ex_Subscriptions model defines subscription constants:

Constant Value Amount (cents)
ONE_MONTH_SUBSCRIPTION 1 $67 (6700)
FOUR_MONTH_SUBSCRIPTION 2 $197 (19700)
FOUR_MONTH_SUBSCRIPTION_UK 3 $166.67 (16667)
ONE_YEAR_SUBSCRIPTION 4 $497 (49700)
LIFE_TIME_SUBSCRIPTION 5 N/A

Note: These are the legacy QuranFlow pricing tiers. The current checkout (fe-checkout) uses the Faith Essentials pricing: $15/month or $120/year. This indicates a pricing model transition has occurred, and the admin backend's subscription constants are outdated.

Payment Types

Type Code Notes
Goodwill 1 Free subscription (scholarship/courtesy)
Stripe 2 Paid via Stripe

The admin can create subscriptions manually via a modal on the student profile page, choosing either "Goodwill" (free) or "Stripe" (paid with card entry).

Payment Plans Data Model

The payment_plans table defines pricing options:

Field Type Notes
id int Primary key
name string Plan name
type string Options: semester, Year, family, discount (from admin form dropdown)
currency string USD or CAD
cycles string 1, 4, or 12
product_id string Stripe Product ID used for lookup
nickname string Human-readable identifier
amount float Price amount
description string Plan description
status int Active/inactive
sub_type string OneTime or Installment (from admin form dropdown)
plan_order int Display ordering

Source: common/models/PaymentPlans.php, backend/views/payment-plans/_form.php

Key finding: The plan type dropdown includes family and discount as options, and sub_type includes Installment. This means family plans and instalment payment structures have partial representation in the database schema, even though no automated logic exists to process them. They are defined as plan records but all management happens in Google Sheets.

Coupons Data Model

The coupons table tracks discount codes:

Field Type Notes
coupon_id int Primary key
name string Coupon name
short_code string Code entered at checkout
percentage_off number Discount percentage
expiry_date date When coupon expires
semester_id int FK to semester — coupons are scoped to semesters
creation_date date
date_modified date

Source: common/models/Coupons.php

Additionally, a couponsToken table provides a token-based redemption system:

Field Type Notes
ct_id int Primary key
token string Unique token string
coupon_id int FK to coupons
timestamp int Creation timestamp
redeemed_by int User ID who redeemed (nullable)

Source: common/models/CouponsToken.php

This means the admin backend has infrastructure for unique, single-use coupon tokens (each token can only be redeemed once by one user). This is more sophisticated than the checkout's single shared scholarship coupon ID.

Stripe Products Table

A stripeProducts table exists in the database:

Field Type Notes
id int Primary key
name string Product name
type string Product type
currency string Payment currency
interval string Billing interval
product_id string Stripe Product ID
nickname string
amount int Price amount

Source: common/models/StripeProducts.php, backend/controllers/StripeProductsController.php

The StripeProductsController provides CRUD with Stripe sync via StripeIntegrationComponent::createStripeProductUsingSdk() — but this controller is not listed in the main navigation (not in Appendix B of the capability map).

Payments Table Schema

The payments table tracks historical transaction records:

Field Type Notes
payment_id int Primary key
package_id int FK to payment_plans
marchant int 1 = Stripe, 2 = PayPal
status int 1 = Success, other = Failed
failed_cause int Why payment failed (nullable)
creation_date int Unix timestamp
date_modified int Unix timestamp
coupon_id int FK to coupons (nullable)
subscription_id int FK to subscriptions
user_id int FK to users

Source: common/models/Payments.php, common/models/extended/payments/Ex_Payments.php

The PaymentsController.actionIndex() retrieves payment history via raw SQL joining payments with payment_plans and users. The actionCreate() method contains a dead die('test') call, indicating it was disabled during development and never re-enabled — payments cannot be created through the admin UI.

Source: backend/controllers/PaymentsController.php

Subscription Stats Bar (Dashboard)

The admin dashboard includes a subscription statistics view (_subscriptions_stats_bar.php) showing:

Critical finding: The Semester Family count and Number of Dependents are hardcoded to 4 in the view. The Local Database section also uses hardcoded values (30 for yearly/semester counts). This confirms these views are mockups/placeholders that were never connected to live data.

Source: backend/views/site/_subscriptions_stats_bar.php

Refund View (Existing)

A refund modal exists at backend/views/users/_refund.php. It presents a dropdown of subscriptions and a "Confirm" button that POSTs to users/refund. This shows refund capability was designed but may not be fully implemented — the form exists but the underlying UsersController::actionRefund() needs verification.

Source: backend/views/users/_refund.php

Student Subscription Details View

The student profile shows subscription details via _student_subscription_details.php:

This is the existing billing surface within the admin — it shows locally-stored subscription records but not real-time Stripe data.

Source: backend/views/users/_student_subscription_details.php

Legacy Payment Systems

The quranflow-backend contains three legacy payment integrations beyond the current Stripe checkout:

1. Google In-App Purchase (Android)

Located at common/helpers/GoogleInAppPurchase.php and backend/controllers/InAppPurchaseController.php:

Important: These user fields (google_in_app_purchase_subscription_state, google_in_app_purchase_subscription_expiry_time) exist in the database but are NOT declared in the User model's property docblock — they're accessed dynamically via Yii2's magic properties.

Implication for billing integration: Some students may have active subscriptions through Google Play rather than Stripe. The billing dashboard must account for this payment channel.

2. PayPal (Sandbox)

Located at common/components/PaypalComponent.php, backend/controllers/PaypalController.php, backend/models/PaypalProduct.php, backend/models/PaypalPlan.php:

3. InfusionSoft Migration

Located at common/models/InfusionSoftSubscriptions.php:

Implication: The system has been through at least three payment platform transitions: InfusionSoft → PayPal (attempted) → Stripe. Historical data from earlier platforms may exist in the database.

Legacy Webhook Handler (MisGT.php)

The Ex_Subscriptions/MisGT.php file contains a separate, legacy webhook handler (processStripeWebhooks()) that:

This is distinct from the fe-checkout-backend webhook handler and appears to be the original webhook processing before the checkout was rebuilt. It confirms the legacy pricing model was active at one point.

Source: common/models/extended/generic/Ex_Subscriptions/MisGT.php

Admin Panel Controllers Related to Billing

Controller Path What it does
PaymentsController /payments View payment transaction history (read-only; create action is dead code)
PaymentPlansController /payment-plans CRUD for payment plan definitions (type/sub_type/cycles/currency)
CouponsController /coupons CRUD for coupons (syncs to Stripe via createCoupon(); scoped to semesters)
StripeProductsController /stripe-products CRUD for Stripe products (not in main nav; syncs via createStripeProductUsingSdk())
PaypalController /paypal PayPal product/plan management (sandbox only; likely unused)
InAppPurchaseController /in-app-purchase Google Play subscription lifecycle handler
UsersController /users Student management including manual subscription creation, delete-subscription, refund actions

Key Files Inventory

File Purpose
common/components/StripeIntegrationComponent.php Direct Stripe API wrapper for admin operations
common/components/PaypalComponent.php PayPal REST API wrapper (sandbox)
common/helpers/GoogleInAppPurchase.php Google Play subscription lifecycle handler
common/helpers/InAppPurchase.php Base class for IAP logging
common/models/Subscriptions.php Subscription data model
common/models/Payments.php Payment transaction records (marchant field: 1=Stripe, 2=PayPal)
common/models/PaymentPlans.php Payment plan definitions with type/sub_type
common/models/Coupons.php Coupon definitions (semester-scoped)
common/models/CouponsToken.php Single-use coupon token redemption
common/models/StripeProducts.php Stripe product/plan records
common/models/InfusionSoftSubscriptions.php Legacy InfusionSoft migration data
common/models/extended/generic/Ex_Subscriptions.php Extended subscription model with business logic
common/models/extended/generic/Ex_Subscriptions/AddGT.php Subscription creation logic
common/models/extended/generic/Ex_Subscriptions/GetGT.php Subscription query methods (goodwill, stripe, by user)
common/models/extended/generic/Ex_Subscriptions/MisGT.php Legacy webhook handler, goodwill creation, legacy Stripe subscriptions
common/models/extended/payments/Ex_Payments.php Payment recording and history retrieval
common/models/extended/payments/Ex_Coupons.php Coupon listing with usage count from subscriptions
common/models/extended/payments/Ex_PaymentPlans.php Plan lookup by product_id
backend/controllers/PaymentsController.php Payment history view (create action disabled)
backend/controllers/CouponsController.php Coupon management with Stripe sync
backend/controllers/PaymentPlansController.php Payment plan management
backend/controllers/StripeProductsController.php Stripe product CRUD (hidden from nav)
backend/controllers/PaypalController.php PayPal product/plan management
backend/controllers/InAppPurchaseController.php Google IAP webhook receiver
backend/views/users/_student_subscription_details.php Student profile subscription display
backend/views/users/_subscription_modal.php Manual subscription creation modal
backend/views/users/_refund.php Refund modal (subscription dropdown + confirm)
backend/views/site/_subscriptions_stats_bar.php Dashboard stats (hardcoded values)
backend/views/payment-plans/_form.php Plan form revealing type/sub_type options
backend/assets_n/js/custom/stripe-components.js Legacy Stripe.js card element + AJAX payment (test API key)

Repo 4: alm-checkout — Multi-Product Checkout Platform

Location: almaghrib-engineering/alm-checkout Technology: Laravel 11 + Inertia.js + React + TypeScript + Tailwind CSS Stripe library: stripe/stripe-php (uses Laravel Cashier migrations)

Role in the System

This is the newer, centralized checkout platform designed to handle multiple AlMaghrib products (events, Faith Essentials subscriptions, donations) across multiple Stripe accounts and regional entities. It uses a template-based system where checkout flows are defined by an API and rendered dynamically.

Multi-Account Stripe Architecture

Unlike fe-checkout (single Stripe account), alm-checkout supports six Stripe accounts:

Account Config Key Purpose
STRIPE_US stripe_us_secret US operations
STRIPE_CA stripe_ca_secret Canada operations
STRIPE_GB stripe_gb_secret UK operations
STRIPE_GB_FD stripe_gb_fd_secret UK Foundation (non-profit/donations)
STRIPE_AU stripe_au_secret Australia operations
STRIPE_FE stripe_fe_secret Faith Essentials (QuranFlow rebrand)

Source: codebase/config/services.php, codebase/app/Services/StripeService.php

Stripe Integration Points

Method Mode Purpose
createSessionByMode() payment/subscription/setup Unified Stripe Checkout Session creation (embedded UI mode)
createSetupIntent() setup Save payment method without charging
cancelSubscriptionAt() management Schedule future cancellation
cancelSubscriptionImmediately() management Immediate cancellation
getOrCreateCustomer() utility Find or create Stripe customer by email
createCustomerSession() utility Customer session for saved payment method display
attachCustomerToSetupIntent() utility Two-phase setup: attach customer after email entry
retrieveSession() utility Retrieve checkout session details
retrieveSubscription() utility Retrieve subscription details

Data Model (uses Laravel Cashier schema)

The alm-checkout repo includes Laravel Cashier-compatible migrations:

Users table additions: stripe_id, pm_type, pm_last_four, trial_ends_at

Subscriptions table: user_id, type, stripe_id (unique), stripe_status, stripe_price, quantity, trial_ends_at, ends_at

Subscription Items table: standard Cashier schema with stripe_id, stripe_product, stripe_price, quantity, meter_id, meter_event_name

QuranFlow Connection

The config/services.php includes explicit QuranFlow API configuration:

'quranflow' => [
    'base_url' => env('QURANFLOW_API_URL', 'https://dev.api.quranflow.org'),
    'api_version' => env('QURANFLOW_API_VERSION', 'v8'),
    'platform' => env('QURANFLOW_PLATFORM', 'web'),
]

This suggests alm-checkout is designed to eventually replace fe-checkout as the QuranFlow checkout frontend, with the ability to handle multi-product checkout scenarios.

Key Files Inventory

File Purpose
codebase/app/Services/StripeService.php Multi-account Stripe service with full subscription lifecycle
codebase/app/Http/Controllers/CheckoutController.php Template-based checkout flow handler
codebase/app/Services/TemplateApiService.php API client for checkout flow configuration
codebase/config/checkout.php Central checkout API URL configuration
STRIPE_IMPLEMENTATION_GUIDE.md Comprehensive implementation guide for multi-scenario checkout

Repo 5: wp-plugin-checkout-alm — WordPress Checkout Plugin

Location: almaghrib-engineering/wp-plugin-checkout-alm Technology: WordPress plugin + React lightbox (via shortcode)

Role in the System

This is a lightweight WordPress plugin that embeds a checkout lightbox/button on WordPress pages. It renders a React component via a shortcode [react_button type="event"] that opens the alm-checkout system in a modal. It does not contain any billing logic itself — it's purely a UI embedding mechanism.

Relevance to billing discovery: Minimal. This plugin is the entry point for WordPress-hosted landing pages but delegates all billing to alm-checkout.


Cross-Repo Data Flow Summary

Student clicks "Subscribe"
         │
         ▼
┌─────────────────┐
│   fe-checkout    │  1. Collects email + plan choice
│   (or alm-       │  2. Calls fe-checkout-backend to create checkout session
│    checkout)     │  3. Creates Stripe Checkout Session or Subscription
│                  │  4. Redirects to Stripe hosted checkout (or processes inline)
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   Stripe         │  5. Processes payment
│                  │  6. Sends `invoice.paid` webhook
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   fe-checkout-   │  7. Receives webhook
│   backend        │  8. Looks up or creates user in quranflow-backend DB
│                  │  9. Creates subscription in FE DB
│                  │  10. Sends welcome email
│                  │  11. Updates checkout session to "paid"
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   quranflow-     │  Now has: user record, subscription_signup record
│   backend DB     │  Admin can see: student in list, payment in history
│                  │  Admin CANNOT see: Stripe subscription status,
│                  │    payment method, renewal dates (must go to Stripe)
└─────────────────┘

Critical Architectural Findings

  1. Two subscription systems exist in parallel: The legacy QuranFlow subscriptions table (linked to user_link_level_tag_id with cycles, billing dates, plan IDs) and the newer FE subscription_signups table (linked to user_id with Stripe IDs, plan IDs, status). These are in different databases and serve different purposes.

  2. stripe_customer_id in the users table is inconsistently populated: The checkout flow stores the Stripe customer ID in subscription_signups.pp_customer_id, not in users.stripe_customer_id. Only manual admin-created subscriptions (via the subscription modal) would populate the users table field. This is confirmed by code analysis — the User model declares the field but the fe-checkout-backend webhook handler never writes to it.

  3. No automated subscription lifecycle handling: The only webhook event processed is invoice.paid. There is no handling of customer.subscription.deleted, invoice.payment_failed, or customer.subscription.updated. A legacy handler in MisGT.php processes more events but uses hardcoded user IDs and legacy plan IDs — it is not in active use.

  4. Pricing divergence: The admin backend defines legacy pricing ($67/month, $197/4months, $497/year) while the checkout uses Faith Essentials pricing ($15/month, $120/year). The plan_id mapping bridges this (21=annual, 29=monthly, 69=scholarship).

  5. Family plans and instalment plans have partial schema representation: The payment_plans.type field supports family and discount as values, and sub_type supports Installment. This means the database can store these plan types, but no business logic processes them — all management happens in Google Sheets. The dashboard subscription stats bar has family/dependent counters but they display hardcoded values (all set to 4).

  6. Three payment channels exist, only one is actively maintained: (a) Stripe via fe-checkout (active), (b) Google Play In-App Purchase (functional — updates user state on subscription events), (c) PayPal (sandbox only, likely never deployed). The billing integration must account for students who may have Google Play subscriptions rather than Stripe.

  7. Duplicate/dead subscription check: fe-checkout implements a pre-checkout subscription check (/check-user-subscription) that calls fe-checkout-backend's /api/checkout/fe/user-active-subscription to prevent duplicate purchases. This is documented in docs/SUBSCRIPTION_CHECK_IMPLEMENTATION.md. However, the fe-checkout-backend also performs dedup during webhook processing (checks for existing active subscription before creating a new one). These are two independent layers of protection.

  8. Coupon system is more capable than currently used: The admin backend has a full coupon CRUD with semester scoping, Stripe sync, percentage-off configuration, expiry dates, and a token-based single-use redemption system (couponsToken table). The checkout only uses a single hardcoded coupon ID. The coupon management screens already show usage count per coupon (via Ex_Coupons.getJsonData() which joins subscriptions by coupon_id).

  9. Existing billing surfaces in the admin are partially built: The student profile has a subscription details card showing active subscriptions from the local DB with Renew/Cancel buttons. A refund modal exists. The dashboard has a subscription stats bar. The payments index shows transaction history. These are starting points — not greenfield.

  10. Three payment platform transitions have occurred: InfusionSoft (migrated from) → PayPal (sandbox, never deployed) → Stripe (current). Historical data from InfusionSoft exists in the InfusionSoftSubscriptions table. Migration code and references persist across the codebase.


Step 2.2 — Current Billing Workarounds

Workaround Inventory

# Workaround Tool Used What Data It Tracks Who Maintains Frequency Impact
W1 Payment & subscription management Stripe Dashboard All subscription status, payment history, customer details, renewal dates Admin Multiple times daily Highest — every student support interaction involving billing requires leaving the admin backend
W2 Family payment plans Google Sheets Which families share plans, number of members, payment amounts, special rates Admin Updated per enrollment cycle High — no system linkage between family members' billing
W3 Scholarship tracking Google Sheets Who has scholarships, scholarship type, terms, amount Admin Updated per enrollment cycle Medium — the 50% annual coupon handles checkout, but tracking ongoing scholarship students is manual
W4 Deferment tracking Google Sheets Who is deferring payment, deferment timeline, resumption date, status Admin Updated as needed Medium — no system concept of deferred payment
W5 Student payment status lookup Stripe + Sheets Whether a student is paid, which plan, whether scholarship/family Admin + Support Multiple times daily Highest — support staff cannot answer basic billing questions independently
W6 Failed payment follow-up Stripe alerts (if configured) + manual outreach Who missed a payment, how many attempts, resolution status — specifically "instalment plan students" (interview Q7) Admin As alerts arrive High — no dashboard visibility, admin must check Stripe proactively
W7 Semester-end payment setup Stripe Dashboard Creating new subscriptions for continuing students Admin Once per semester (bulk) High — error-prone, done before promotions finalized, requires corrections
W8 Student deactivation + payment cancel Backend + Stripe separately Account status in backend, subscription status in Stripe Admin As students leave Critical — two-step process leads to students retaining access after cancellation or being charged after deactivation
W9 EOC assessment + billing alignment Google Sheets + Stripe Which students passed, which are continuing, who needs new payment setup Admin + Teaching Staff Once per semester High — billing decisions depend on promotion decisions tracked in separate sheets

Workaround Detail Analysis

W1: Stripe Dashboard Dependency

What the admin does: Opens Stripe dashboard to look up customer by email, view subscription status, check payment history, see upcoming invoices, manage subscription (cancel, modify, refund).

Why it's a workaround: The admin backend has a "Payments" screen (/payments/index) but it only shows historical payment records stored locally. It does not show:

Ideal replacement: A billing tab on the student profile showing real-time Stripe data (subscription status, plan, payment history, payment method) with action buttons (cancel, modify, refund link).

W2: Family Payment Plans

What the admin does: Maintains a Google Sheet listing families where multiple members share a single payment arrangement (e.g., one payment covers two siblings).

Why it's a workaround: Stripe doesn't natively support "family plans." Each student is a separate Stripe customer with a separate subscription. The admin must manually track which students are connected and ensure their billing is coordinated.

Ideal replacement: A "family group" entity in the admin backend that links student accounts, with a shared billing view showing all family members' subscription status and a single point for managing family-wide billing changes.

Volume estimate: Unknown — flagged as open question. Interview suggests this is a recurring pattern but exact numbers per semester are not available.

W3: Scholarship Tracking

What the admin does: Tracks scholarship recipients in a Google Sheet. The checkout handles the 50% annual discount via a Stripe coupon, but ongoing scholarship management (renewals, eligibility changes, different scholarship levels) is manual.

Why it's a workaround: The only automated scholarship mechanism is a binary 50% coupon on annual plans. There's no concept of:

Ideal replacement: Scholarship status as a tag/flag on student profiles with configurable discount levels, visible in billing screens and reportable.

Volume estimate: Unknown — flagged as open question.

W4: Deferment Tracking

What the admin does: Tracks students who have been granted payment deferment (delayed start, paused billing) in a Google Sheet.

Why it's a workaround: There is no concept of deferment in any codebase. A deferred student's Stripe subscription is either active (being charged) or canceled (not being charged). There's no middle state.

Ideal replacement: A deferment status on student profiles that pauses billing (via Stripe's pause_collection or subscription schedules) and tracks the resumption date.

Volume estimate: Unknown — flagged as open question.

W5: Support Staff Billing Access

Current state: Customer support staff must escalate all billing questions to the admin because:

  1. They have no access to Stripe
  2. The admin backend shows no billing data
  3. Google Sheets with billing info may not be shared with support

Ideal replacement: A read-only billing view accessible to support staff (user type 5: Admin View Only) showing subscription status, payment history, and plan type — without the ability to modify.

W8: Linked Deactivation + Payment Cancellation

Current state: When a student leaves the program:

  1. Admin deactivates the student account in the backend (changes user_activation_status to 0)
  2. Admin separately navigates to Stripe Dashboard
  3. Admin finds the customer by email
  4. Admin cancels the subscription in Stripe

If step 2-4 is missed, the student continues to be charged. If step 1 is missed, the student retains app access.

Ideal replacement: A single "Deactivate Student" action that simultaneously:

Volume and Frequency Estimates (Open Questions)

Question Why It Matters Status
How many students are on family plans per semester? Determines if family plans need a full feature or lightweight tagging Unknown — needs PM input
How many scholarship students per semester? Determines scope of scholarship feature Unknown — needs PM input
How many deferment students per semester? Determines if deferment needs automation or just notes Unknown — needs PM input
How many times per week does the admin visit Stripe? Quantifies the cost of the current workaround Unknown — needs PM input
What billing questions does support handle independently vs. escalate? Shapes the support staff billing view Unknown — needs PM input
How many students continue semester-to-semester vs. new enrollments? Affects semester-end payment setup volume Unknown — needs PM input
How many payment failures occur per month? Determines urgency of failed payment alerts Unknown — could be extracted from Stripe

Step 2.3 — Stripe Data Available via API

Stripe Object Reference for QuranFlow Admin

Customer Object

What It Contains Admin Relevance Key Fields
Email, name, phone Student identification and contact id, email, name, phone
Default payment method Understanding payment capability invoice_settings.default_payment_method
Created date Account age tracking created
Metadata Custom data attached during checkout metadata (plan, scholarship flag, checkout_session_id)
Balance Credit/debit balance on account balance

API Endpoints:

Rate Limits: 100 read requests/second in live mode. Customer search is more expensive.

Admin Actions Available:

Subscription Object

What It Contains Admin Relevance Key Fields
Status Real-time subscription state status: active, past_due, canceled, incomplete, trialing, unpaid, paused
Current period Billing cycle dates current_period_start, current_period_end
Plan/Price Which plan the student is on items.data[0].price.id, items.data[0].price.unit_amount, items.data[0].price.recurring.interval
Cancel state Whether cancellation is scheduled cancel_at_period_end, cancel_at, canceled_at
Discount Applied coupon/promotion discount.coupon.id, discount.coupon.percent_off
Default payment method Card on file for this subscription default_payment_method
Latest invoice Most recent billing attempt latest_invoice (expandable)
Metadata Custom data from checkout metadata (plan type, scholarship flag)
Trial Trial period dates trial_start, trial_end
Collection paused Deferment-like state pause_collection (reason, resumes_at)

API Endpoints:

Rate Limits: 100 read requests/second. Listing with expand can be expensive.

Admin Actions Available:

Invoice Object

What It Contains Admin Relevance Key Fields
Status Payment outcome status: draft, open, paid, void, uncollectible
Amount What was charged amount_due, amount_paid, amount_remaining
Payment attempts How many times payment was tried attempt_count, next_payment_attempt
Billing reason Why this invoice exists billing_reason: subscription_create, subscription_cycle, manual
Customer email Who was charged customer_email
Line items What was charged for lines.data
Payment intent Underlying payment payment_intent (expandable)
Created/paid dates Timing created, status_transitions.paid_at
Hosted invoice URL Shareable link hosted_invoice_url

API Endpoints:

Rate Limits: 100 read requests/second. Listing all invoices for a customer is a single API call with pagination.

Admin Actions Available:

Payment Intent Object

What It Contains Admin Relevance Key Fields
Status Detailed payment state status: succeeded, requires_payment_method, requires_confirmation, requires_action, processing, canceled
Amount What was attempted amount, amount_received
Failure reason Why payment failed last_payment_error.message, last_payment_error.code, last_payment_error.decline_code
Payment method Card/bank details payment_method (expandable to get card brand, last4)
Charges Associated charge records charges.data

API Endpoints:

Admin Relevance: Most useful for troubleshooting failed payments — provides the exact decline code and failure reason.

Coupon / Promotion Code

What It Contains Admin Relevance Key Fields
Discount amount How much off percent_off or amount_off
Duration How long discount applies duration: once, repeating, forever
Redemption count How many times used times_redeemed
Valid status Whether still usable valid
Max redemptions Usage limit max_redemptions

API Endpoints:

Admin Relevance: Already partially integrated — CouponsController in quranflow-backend syncs coupons to Stripe. But the admin cannot see coupon usage or applied discounts on student subscriptions.

Product / Price

What It Contains Admin Relevance Key Fields
Product name Human-readable plan name product.name
Price amount What's charged unit_amount (in cents)
Billing interval How often recurring.interval, recurring.interval_count
Active status Whether price is current active
Currency Payment currency currency

API Endpoints:

Admin Relevance: Needed for the Payment Configuration screen to display and manage available plans.

Answering Key Questions

Q1: Real-time API vs. webhooks for subscription status?

Answer: Both approaches are viable, with different trade-offs.

Approach Pros Cons Best For
Real-time API calls Always current data, no infrastructure needed Rate limits (100/sec), latency, cost Student profile lookups, on-demand queries
Webhooks + local cache No rate limit concerns, faster reads, works offline Requires webhook infrastructure, can miss events, eventual consistency Dashboard alerts, bulk views, reports
Hybrid (recommended) Best of both worlds More complex to build Webhooks for alerts/status changes + API for on-demand detail

Recommendation: Use webhooks for proactive alerts (failed payments, cancellations) and real-time API for student profile payment tab (on-demand when admin views a student). This avoids building a full local Stripe data mirror while still enabling the dashboard alerts the admin requested.

Q2: Is stripe_customer_id populated?

Answer: Inconsistently. Based on code analysis:

Implication: To look up a student's Stripe data, the system needs to:

  1. Check users.stripe_customer_id first
  2. If empty, look up subscription_signups.pp_customer_id in the FE database by user_id
  3. If neither exists, search Stripe by email using Customer::search()

Recommendation: Backfill users.stripe_customer_id from subscription_signups.pp_customer_id and ensure future webhook processing updates it.

Q3: Full payment history with one API call?

Answer: Yes, with pagination. GET /v1/invoices?customer={customer_id}&limit=100 returns all invoices for a customer, ordered by creation date. Each invoice includes amount, status, date, and line items. For most QuranFlow students (1-3 years of monthly or annual payments), this would be 12-36 invoices — well within a single paginated response.

Stripe also provides hosted_invoice_url on each invoice, giving the admin a direct link to a Stripe-rendered invoice page without needing to build one.

Q4: What powers "failed payment alerts"?

Answer: Two Stripe webhook events:

Event When It Fires Data Available
invoice.payment_failed When a subscription payment attempt fails Customer ID, amount, failure reason, next attempt date, attempt count
customer.subscription.updated (with status change to past_due) When subscription moves to past_due after failed payment Customer ID, subscription ID, new status

Current state: Neither event is processed by any QuranFlow system. The fe-checkout-backend webhook handler only processes invoice.paid.

To implement: Add webhook handlers for invoice.payment_failed and customer.subscription.updated, store failed payment alerts in a local alerts table, and surface them on the admin dashboard.

Stripe's automatic retry schedule: By default, Stripe retries failed payments on a Smart Retries schedule (typically 3 attempts over ~3 weeks before marking the subscription as past_due or canceled). This is configurable in Stripe Dashboard > Settings > Billing > Subscriptions and emails.

Q5: Family plans in Stripe?

Answer: Stripe has no native family plan concept. Options for representation:

Approach How It Works Pros Cons
Shared customer One Stripe customer, multiple subscriptions (one per family member) Single billing point Doesn't match one-subscription-per-student model; complicates individual management
Coupon-based Each family member has own subscription but with a family discount coupon Works with existing architecture Need to track family membership separately; coupon management overhead
Metadata + custom layer Each student has own subscription; metadata tag marks family membership; custom logic tracks family group Most flexible, works with existing architecture Requires building the family management layer in admin backend

Recommendation: Use metadata + custom layer (option 3). Create a "family group" entity in the admin backend that links student IDs. Apply family-specific coupons to each member's subscription. The family group provides the administrative view while Stripe handles individual billing. This aligns with the admin's stated need (interview Q2): seeing family plan status on student profiles.

Improvement Opportunities: Stripe Features Not Currently Used

Feature What It Does Admin Benefit
Billing Portal (stripe.com/docs/billing/subscriptions/integrations/customer-portal) Self-service portal where customers can update payment method, cancel, view invoices Reduces admin burden for routine billing tasks; students can self-serve
Automatic Dunning (Settings > Billing > Subscriptions) Automated emails to customers with failed payments; configurable retry schedule Eliminates manual follow-up for failed payments; admin only handles escalations
Subscription Schedules Plan future subscription changes (upgrade, downgrade, cancel at date) Enables semester-end payment setup: schedule new subscriptions to start when current semester ends
Pause Collection Temporarily stop billing without canceling Perfect for deferments — pause billing and auto-resume at a set date
Customer Balance Apply credit to a customer's account Handle partial refunds, credits, account adjustments without full refund/resubscribe
Webhook Endpoints (expansion) Subscribe to more events Enable real-time alerts for: invoice.payment_failed, customer.subscription.deleted, customer.subscription.updated, customer.subscription.paused
Stripe Tax Automatic tax calculation Compliance for international students (currently automatic_tax.enabled = false)
Payment Links No-code shareable payment URLs Quick way to send payment links for special cases (scholarships, custom amounts) without going through full checkout
Invoicing Send one-off invoices Handle custom billing scenarios (catch-up payments, partial semester fees)

Step 2.4 — Billing Screen Requirements

A. Student Profile — Payment Tab

Purpose: Show all billing information for a single student in context, eliminating the need to visit Stripe.

Data Displayed

Section Data Source Technical Approach
Subscription Status Status (active/past_due/canceled/paused), plan name, billing interval, current period dates Stripe Subscription API Real-time API call when tab is opened, using pp_customer_id or stripe_customer_id
Current Plan Plan name, amount, interval, applied discount Stripe Subscription + Price Same API call as above (expand price)
Payment Method Card brand, last 4 digits, expiry Stripe Payment Method API Expand default_payment_method on subscription
Payment History Last 12 payments: date, amount, status (paid/failed/refunded), invoice link Stripe Invoice API GET /v1/invoices?customer={id}&limit=12
Special Status Flags Scholarship (yes/no, type), Family Plan (group members), Deferment (status, resume date) Local database (new) New billing_flags table or fields on student profile
Upcoming Next payment date, next amount Stripe Invoice API GET /v1/invoices/upcoming?customer={id}

Actions Available

Action What It Does Technical Approach Permission Level
Cancel Subscription Cancel at period end or immediately POST /v1/subscriptions/{id} with cancel_at_period_end=true or DELETE Admin only
Cancel + Deactivate Cancel subscription AND deactivate backend account Stripe API + local DB update in single transaction Admin only
Pause Billing (Deferment) Pause collection, set resume date POST /v1/subscriptions/{id} with pause_collection Admin only
Resume Billing Resume a paused subscription POST /v1/subscriptions/{id} clear pause_collection Admin only
Apply Discount Apply a coupon to active subscription POST /v1/subscriptions/{id} with coupon Admin only
Mark as Scholarship Flag student as scholarship recipient Local DB update + apply coupon Admin only
Mark as Family Plan Add student to family group Local DB update Admin only
View in Stripe Deep link to Stripe Dashboard for advanced operations URL: https://dashboard.stripe.com/customers/{customer_id} Admin only
Send Payment Link Generate and send a Stripe Payment Link for custom amounts Stripe Payment Links API Admin only

View-Only Access (Support Staff)

Support staff (user type 5) should see:

B. Billing Dashboard / Alerts

Purpose: Proactive operational view showing billing exceptions that need attention. This directly addresses interview Q7 — the admin's ideal dashboard.

Alert Types

Alert Threshold Data Source Actions
Failed Payments Any invoice.payment_failed event in last 30 days Webhook-populated local table View student, view in Stripe, send reminder, cancel
Past Due Subscriptions Subscriptions with status past_due Webhook-populated or periodic Stripe sync View student, retry payment (Stripe), cancel
Upcoming Renewals Subscriptions renewing in next 7 days Stripe Subscription API (current_period_end) or local cache View list, no action needed (informational)
Expiring Scholarships Scholarship students with subscription end date approaching Local DB query Review, extend, or remove scholarship
Deferment Resume Due Paused subscriptions with resumes_at in next 7 days Stripe Subscription API or local cache Review, confirm resume, extend deferment
Cancelled Subscriptions Recent cancellations (last 14 days) Webhook-populated local table Review, contact student, re-enroll
Students Without Active Subscription Active backend accounts with no active Stripe subscription Cross-reference local users + Stripe Investigate, deactivate, or resolve

Metrics (Summary Bar)

Metric Source
Total active subscriptions Stripe or local count
Monthly recurring revenue Stripe or calculated from active subscriptions
Failed payments this month Webhook count
New subscriptions this month Webhook count
Churn this month Webhook count (cancellations)

Technical Implementation

Recommended approach: Implement webhook handlers for key events and store them in a local billing_events table. The dashboard queries this table rather than making live Stripe API calls.

Webhook Event Local Action
invoice.payment_failed Insert into billing_alerts with type "failed_payment"
customer.subscription.deleted Insert into billing_alerts with type "cancellation"
customer.subscription.updated Update local subscription status cache
customer.subscription.paused Insert into billing_alerts with type "deferment_started"
invoice.paid Update local subscription status, clear related failed_payment alerts

C. Payment Configuration

Purpose: Manage payment plans, coupons, scholarships, deferment policies, and family plan settings.

Sub-screens

C1. Payment Plans (already exists at /payment-plans/index)

Current State Enhancement Needed
CRUD for payment plan records Display which Stripe Price IDs correspond to each plan
Local database only Show active subscriber count per plan
No connection to actual Stripe prices Sync/validate against Stripe Prices

C2. Coupons (already exists at /coupons/index)

Current State Enhancement Needed
CRUD with Stripe sync (creates coupon in Stripe) Show redemption count from Stripe
No visibility into applied coupons List students currently using each coupon
No expiration management Show coupon validity dates and auto-expiry

C3. Scholarship Management (NEW — replaces Google Sheets)

Feature Description
Scholarship programs Define named scholarship programs (e.g., "50% Annual", "Full Scholarship")
Student assignment Assign students to scholarship programs
Automatic coupon application When assigning, apply corresponding Stripe coupon
Renewal tracking Track when scholarship expires and whether it's renewed
Reporting Count of scholarship students per semester, total discount value

C4. Deferment Management (NEW — replaces Google Sheets)

Feature Description
Create deferment Select student, set pause start and resume dates
Automatic pause Pause Stripe subscription collection
Resume tracking Alert when deferment resume date approaches
Deferment history Track all past deferments per student

C5. Family Plan Management (NEW — replaces Google Sheets)

Feature Description
Create family group Name the group, add family members (student accounts)
Shared billing view See all members' subscription status in one view
Apply family discount Apply a family discount coupon to all members
Manage membership Add/remove family members

D. Semester Close — Payment Workflow

Purpose: Streamline the most error-prone billing workflow — setting up payments for continuing students after promotions.

Current Pain Point (from interview Q3, Q8)

  1. Admin promotes students (or automation does)
  2. Admin goes to Stripe to create subscriptions for continuing students
  3. But promotions may change (teacher overrides, student opt-outs)
  4. Admin must correct Stripe subscriptions that were set up too early

Proposed Workflow

Step Screen Actions Data Needed
1. Review Promotion Results Semester Close Hub View all students with promotion status (pass/fail/pending) Student Report data + EOC assessment data
2. Confirm Continuing Students Semester Close Hub Mark which promoted students will continue to next semester Student decisions (opt-in/opt-out)
3. Bulk Payment Setup Semester Close — Billing For confirmed continuing students: create/renew Stripe subscriptions Student email, plan selection, existing Stripe customer ID
4. Handle Leavers Semester Close — Billing For students not continuing: cancel Stripe subscriptions + deactivate accounts (linked action) List of non-continuing students
5. Verify Semester Close — Billing Dashboard showing: X students set up, Y cancelled, Z pending Summary view

Technical Approach

Improvement: Gate Payment Setup Behind Promotion Finalization

Implement a promotion gate: the billing setup step cannot begin until the admin marks promotions as "finalized." This prevents the current problem of setting up payments before promotion decisions are complete.

E. Additional Billing Surfaces Beyond the Plan

E1. Refund Management

Feature Description
View refund history Show all refunds processed per student
Process refund Initiate refund for a specific invoice/charge from within admin
Refund policy enforcement Display refund eligibility (e.g., within 30 days of annual plan)

Source: The commented-out unsubscription code in fe-checkout-backend already contains refund eligibility logic (30-day window for annual plans, amount verification).

E2. Revenue Reports

Report Description
Monthly revenue summary Total collected, by plan type, new vs. renewal
Scholarship impact Total discount value provided
Churn report Cancellations by month, reasons if available
Payment failure rate Failures vs. total attempts, by month
Family plan summary Revenue from family plans vs. individual

E3. Automated Billing Emails

Email Trigger Purpose
Payment failed — student notification invoice.payment_failed webhook Prompt student to update payment method
Subscription cancelled — confirmation customer.subscription.deleted webhook Confirm cancellation, provide re-enrollment link
Upcoming renewal — reminder 7 days before current_period_end Inform student of upcoming charge
Deferment resume — reminder 7 days before resumes_at Remind student billing will resume

Step 2.5 — Billing Integration Brief

1. What Billing Data Exists Today

In Stripe (source of truth for payment data)

Data Exists Accessible
Customer records Yes Via API using customer ID or email search
Subscription status (active/canceled/past_due) Yes Via API
Payment history (all invoices) Yes Via API
Payment method details Yes Via API
Coupon/discount applied Yes Via API (on subscription object)
Products and Prices (plan definitions) Yes Via API
Failed payment details Yes Via API + webhook events

In Local Databases

Data Table Database Notes
Subscription record (legacy) subscriptions QuranFlow DB Linked to enrollment (user_link_level_tag_id), has cycles, billing dates, plan ID, coupon ID
Subscription signup subscription_signups FE DB Has Stripe customer/subscription IDs, plan_id, status
Subscription status subscriptions FE DB Simple user_id + status (active/cancelled)
Payment plans payment_plans QuranFlow DB Plan definitions (name, amount, cycles, currency)
Coupons coupons QuranFlow DB Coupon definitions (synced to Stripe on create)
Stripe customer ID users.stripe_customer_id QuranFlow DB Inconsistently populated
Stripe customer ID subscription_signups.pp_customer_id FE DB Populated for checkout-originated subscriptions
Checkout sessions checkouts FE Checkout DB Session tracking with status (draft/paid)
Payment history payments QuranFlow DB Historical payment records (limited)

In Google Sheets Only (no system representation)

Data Gap Impact
Family plan groups and members Cannot see family billing context on student profiles. Note: payment_plans.type supports family as a value, but no grouping/membership logic exists
Scholarship recipients (beyond the binary coupon) Cannot track ongoing scholarships, different levels, renewals. Note: coupon infrastructure exists (semester-scoped, token-based) but only one hardcoded coupon is used in checkout
Deferment students and timelines Cannot programmatically pause/resume billing
EOC assessment outcomes Cannot gate payment setup behind promotion decisions
Semester-end student status overview Cannot automate continuing/leaving student billing

Legacy Payment Data (may exist in database)

Data Table Notes
InfusionSoft subscriptions InfusionSoftSubscriptions Migrated historical data: contact_id, product_name, billing_cycle, charges
Google Play subscription state users (undeclared columns) google_in_app_purchase_subscription_state, google_in_app_purchase_subscription_expiry_time
PayPal products/plans PaypalProduct, PaypalPlan Sandbox only — likely no real transaction data

2. What the Admin Needs to See and Do

Must-Have (eliminates highest-impact workarounds)

Need Source Workaround Priority
View student subscription status on profile W1, W5 P0
View student payment history on profile W1, W5 P0
Cancel subscription from student profile W1, W8 P0
Linked deactivation + payment cancellation W8 P0
Failed payment alerts on dashboard W6 P0
Scholarship status on student profile W3 P1
Family plan grouping and view W2 P1

Should-Have (significant efficiency gain)

Need Source Workaround Priority
Deferment management (pause/resume) W4 P1
Semester-end bulk payment setup W7 P1
Support staff read-only billing view W5 P1
Billing dashboard with metrics W6 P2
Revenue reports New P2

Nice-to-Have (future improvement)

Need Source Priority
Student self-service billing portal (Stripe Billing Portal) Improvement opportunity P3
Automated dunning emails Improvement opportunity P3
Refund management from admin Improvement opportunity P3
Advanced coupon analytics Improvement opportunity P3

3. What Stripe Can Provide

Need Stripe Capability API Complexity Notes
Real-time subscription status GET /v1/subscriptions/{id} Low Single API call
Payment history GET /v1/invoices?customer={id} Low Paginated, one call for typical student
Failed payment detection invoice.payment_failed webhook Medium Requires webhook infrastructure
Cancel subscription DELETE /v1/subscriptions/{id} or update Low Single API call
Pause billing (deferments) pause_collection on subscription Low Single API call
Apply coupon Update subscription with coupon Low Single API call
Customer search by email Customer::search() Low Needed for students without stored customer ID
Upcoming payment preview GET /v1/invoices/upcoming Low Single API call
Bulk subscription creation Subscription::create() in loop Medium Rate-limited at 100/sec; 50 students = 0.5sec
Self-service portal Stripe Billing Portal Low (config) Minimal dev effort — mostly Stripe config
Payment method details Expand on subscription or payment method Low Card brand, last 4, expiry

4. What Gaps Remain (Custom Development Needed)

Gap Why Stripe Alone Can't Solve It Custom Development Required
Family plan management Stripe has no family concept Build family group entity, family management UI, family discount application
Scholarship program management Stripe coupons are binary (applied or not) — no concept of scholarship programs, eligibility, renewals Build scholarship program entity, assignment UI, renewal tracking
Deferment tracking Stripe's pause_collection works but has no admin UI or tracking history Build deferment management UI with history and alerts
Linked deactivation Stripe and QuranFlow backend are separate systems Build single action that calls both Stripe API and local DB in a transaction
Billing alerts/dashboard Stripe webhooks provide events but not an admin dashboard Build webhook receivers, local alert storage, dashboard UI
Customer ID reconciliation stripe_customer_id is inconsistently populated Build backfill script; update webhook handler to populate users.stripe_customer_id
Semester-end billing workflow No concept of "semester close + billing" in Stripe Build workflow UI that combines promotion data with Stripe subscription creation
Support staff view Stripe Dashboard requires its own access control Build read-only billing views within admin backend
Cross-database subscription unification Two subscription systems (QuranFlow DB + FE DB) with different schemas Either migrate to single system or build adapter layer that queries both
Google IAP subscription visibility Google Play subscriptions update user state but are not visible in admin billing UI Build read-only display of Google IAP state on student profile; consider whether Google IAP students need the same billing tab
Legacy data cleanup InfusionSoft migration data, hardcoded dashboard values, dead PaymentsController create action, sandbox PayPal code Audit and either remove or document; prevents confusion during development

5. Billing as Nav Sections — Proposed Screens

The following screens must have a home in whatever navigation structure Workstream 1 selects:

Primary Billing Screens

Screen Type Where It Might Live Content
Student Payment Tab Entity sub-view Within Student Profile (any candidate) Subscription status, history, actions
Billing Dashboard Dashboard widget or dedicated section Dashboard (Candidate B) or Billing section (Candidate A) Alerts, metrics, action links
Payment Configuration Settings/admin screen Billing section or Settings Plans, coupons, scholarship programs
Semester Close — Billing Workflow step Semester Management or standalone workflow Bulk payment setup, linked deactivation

Supporting Billing Screens

Screen Type Where It Might Live Content
Family Plan Management CRUD + grouping Billing section or Student Management Family groups, member linking, shared billing view
Scholarship Management CRUD + assignment Billing section or Student Management Scholarship programs, student assignment
Deferment Management CRUD + tracking Billing section or Student Management Active deferments, upcoming resumes
Revenue Reports Reporting Reports section Revenue metrics, churn, scholarship impact

Navigation Accommodation Requirements

Any navigation candidate from Workstream 1 must accommodate:

  1. Billing data on student profiles — regardless of where billing lives as a section, the student profile must have a payment tab
  2. Billing alerts on the dashboard — failed payments and past-due subscriptions must surface on whatever serves as the operational dashboard
  3. A billing configuration area — plans, coupons, scholarships, deferments, family plans need a management home
  4. A semester-close billing step — the semester close workflow must include billing operations
  5. Role-based access — support staff see read-only billing; admin sees full controls

6. Implementation Phases

Phase 1: Foundation (Weeks 1-4)

Goal: Eliminate the highest-impact workaround (Stripe Dashboard dependency) and enable support staff independence.

Deliverable What It Does Technical Work
Customer ID reconciliation Backfill users.stripe_customer_id from subscription_signups.pp_customer_id Migration script + update webhook handler
Student Payment Tab (read-only) Show subscription status, plan, payment history on student profile Stripe API integration layer + frontend tab
Deep link to Stripe "View in Stripe" button on student profile URL construction using customer ID
Support staff access Read-only billing view for user type 5 Permission checks on billing tab

Note: Phase 1 should also verify the population of google_in_app_purchase_subscription_state on users and decide whether to display it on the billing tab. If only a handful of students use Google IAP, a simple text field may suffice.

Impact: Admin and support staff can see billing data without leaving the admin backend. Reduces Stripe visits by ~60%.

Phase 2: Actions + Alerts (Weeks 5-8)

Goal: Enable billing actions from within the admin backend and implement proactive alerts.

Deliverable What It Does Technical Work
Cancel subscription action Cancel from student profile Stripe API call + local status update
Linked deactivation Cancel + deactivate as single action Combined Stripe + local operation
Webhook expansion Handle invoice.payment_failed, customer.subscription.deleted, customer.subscription.updated New webhook handlers + alert storage
Billing alerts on dashboard Show failed payments, past-due subscriptions, recent cancellations Dashboard widget + alert query
Pause/resume billing Deferment management (basic) Stripe pause_collection API

Impact: Admin can manage most billing operations without Stripe. Failed payments are visible proactively. Linked deactivation eliminates the most critical process gap.

Phase 3: Bulk Operations + Tracking (Weeks 9-12)

Goal: Replace Google Sheets for scholarship, deferment, and family plan tracking. Streamline semester-end billing.

Deliverable What It Does Technical Work
Scholarship management Define programs, assign students, track renewals New DB tables + CRUD UI + Stripe coupon application
Family plan management Create family groups, link members, shared billing view New DB tables + CRUD UI
Deferment management Full lifecycle: create, pause, resume, history New DB tables + CRUD UI + Stripe pause integration
Semester-end billing workflow Promotion-gated bulk subscription setup + linked deactivation Workflow UI combining student data + Stripe operations

Impact: All Google Sheets eliminated. Semester-end billing is safer and faster. All billing data lives in one system.

Phase 4: Optimization (Weeks 13-16)

Goal: Add polish, reporting, and self-service capabilities.

Deliverable What It Does Technical Work
Revenue reports Monthly revenue, churn, scholarship impact Reporting UI + data aggregation
Billing portal Student self-service for payment method updates Stripe Billing Portal configuration
Automated dunning Automatic failed payment emails to students Stripe dunning configuration + email templates
Coupon analytics Usage tracking, active discount reporting Stripe API + reporting UI
Refund management Process refunds from admin Stripe Refund API + UI

Impact: Full billing management maturity. Admin is proactive rather than reactive. Students can self-serve for routine billing tasks.

7. Open Questions Requiring PM/Admin Input

# Question Why It Matters Impact If Not Resolved
OQ1 How many students are on family plans per semester? Determines if family plans need full CRUD or lightweight tagging Could over- or under-engineer the family plan feature
OQ2 How many scholarship students per semester, and how many distinct scholarship levels exist? Determines scope of scholarship management feature Might build unnecessary complexity or miss needed flexibility
OQ3 How many deferment students per semester, and what's the typical deferment period? Determines if deferments need full management or simple notes Could over-engineer for a rare scenario
OQ4 Is the FE database (alm_db_fe) the canonical source for active subscriptions, or is the QuranFlow subscriptions table? Determines which database the billing integration queries Wrong choice creates data inconsistency
OQ5 Should the billing integration read from subscription_signups (FE DB) or from subscriptions (QuranFlow DB), or both? The two tables have different schemas and different data Must be resolved before Phase 1
OQ6 What is the relationship between the legacy QuranFlow pricing ($67/$197/$497) and the current FE pricing ($15/$120)? Are legacy plans still active? Determines whether the billing UI needs to support both pricing models Could show incorrect pricing to admin
OQ7 Does the checkout repo need modification to populate users.stripe_customer_id, or should the admin backend reconcile it? Determines whether the fix goes in the checkout system or the admin system Affects Phase 1 implementation approach
OQ8 What Stripe account is used for QuranFlow/Faith Essentials? Is it STRIPE_FE from alm-checkout, or the single account in fe-checkout? Determines which API keys to use for the admin billing integration Wrong account = wrong data
OQ9 Should the semester-end billing workflow support subscription schedules (start billing on a future date), or should subscriptions be created immediately? Determines whether to use Stripe Subscription Schedules or simple create Affects workflow design and complexity
OQ10 What happens to the fe-checkout repo if billing is surfaced in the admin backend — does checkout remain separate? Determines long-term architecture: are checkout and admin billing one system or two? Affects maintenance and development planning
OQ11 How many students have active Google Play (IAP) subscriptions vs. Stripe subscriptions? Determines whether the billing integration needs to support Google Play or only Stripe If Google Play students are significant, the billing tab must show IAP state alongside Stripe data
OQ12 Are the legacy InfusionSoft subscription records still relevant, or can they be archived? Determines whether the billing dashboard needs to display historical InfusionSoft data Affects data migration and schema cleanup scope
OQ13 Is the coupon token system (couponsToken table) actively used, or only the shared scholarship coupon? Determines whether to build on existing token infrastructure or the simpler coupon approach Could reduce development effort if token system is already proven

Appendix: Complete Key Files Index

fe-checkout (almaghrib-engineering/fe-checkout)

fe-checkout-backend (almaghrib-engineering/fe-checkout-backend)

quranflow-backend (almaghrib-engineering/quranflow-backend)

Core billing components:

Data models:

Extended models:

Controllers:

Views:

alm-checkout (almaghrib-engineering/alm-checkout)

wp-plugin-checkout-alm (almaghrib-engineering/wp-plugin-checkout-alm)