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
- Step 2.1 — Checkout Repo Audit
- Step 2.2 — Current Billing Workarounds
- Step 2.3 — Stripe Data Available via API
- Step 2.4 — Billing Screen Requirements
- 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
- A single scholarship coupon is used: ID
7IoeEtUb(configurable viaSTRIPE_SCHOLARSHIP_COUPON_ID) - The scholarship is a 50% discount on the annual plan only (reducing $120 to $60)
- The coupon is applied at the Stripe level using
discountson Checkout Sessions or Subscriptions - When scholarship is active, the plan ID sent to the backend webhook is
SCHOLARSHIP_PLAN_ID(default: 69) rather thanANNUAL_PLAN_ID(21) - There is a dedicated
/scholarshippage that reveals the discount and redirects back to checkout with the scholarship flag
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:
- Stripe for payment processing
fe-checkout-backendfor session tracking and user provisioningquranflow-backendfor the actual user account and subscription records
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:
- Tracks checkout sessions (IP, email, referral URL, status)
- Receives the
invoice.paidwebhook from Stripe - Creates or updates user accounts in the core database
- Creates subscription records in the FE database
- 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)
- Extract invoice data and line item metadata (plan type, email, checkout_session_id)
- Determine internal
plan_id:- Annual + 50% coupon =
SCHOLARSHIP_PLAN_ID(69) - Annual without coupon =
ANNUAL_PLAN_ID(21) - Monthly =
MONTHLY_PLAN_ID(29)
- Annual + 50% coupon =
- Check if user exists in
alm_db_core.usersby email- If not: create user with generated password, send welcome email
- If exists: check for active subscription — if already active, ignore (dedup)
- Check/create subscription in
alm_db_fe.subscriptions(status = active) - Create
subscription_signupsrecord with Stripe IDs - Create
fe_onboarding_emailstracking record - 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:
- email, name_full, password (hashed), created_at, updated_at
alm_db_fe.subscriptions — Main subscription record:
- user_id, status (active/cancelled), created_at, updated_at, last_active
alm_db_fe.subscription_signups — Individual signup records:
- user_id, plan_id, pp_customer_id (Stripe customer ID), pp_subscription_id (Stripe subscription ID), pp_subscription_status, signup_total, signup_used, has_loggedin, date_end, created_at
alm_db_fe.fe_onboarding_emails — Email sequence tracking:
- subscription_id, email_1 through email_6 (boolean flags), created_at
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:
- Yearly One-time, Yearly Monthly, Semester One-time, Semester Monthly, Semester Family, Number of Dependents
- Both Stripe and Local Database counts are displayed side by side
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:
- Lists active subscriptions with plan name (looked up from
payment_plansbypayment_plan_id) and date range - Each subscription has a delete/cancel button
- "Renew" and "Cancel" action buttons at the bottom
- JavaScript handles subscription type selection and add/delete operations
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:
- Handles Google Play subscription lifecycle notifications via Pub/Sub webhooks
- Processes 11 notification types: PURCHASED, RENEWED, CANCELED, ON_HOLD, IN_GRACE_PERIOD, RESTARTED, DEFERRED, PAUSED, REVOKED, EXPIRED, RECOVERED
- Links purchases to users via
externalAccountIdentifiers.obfuscatedExternalAccountId - Updates
userstable fields:google_in_app_purchase_subscription_state,google_in_app_purchase_subscription_expiry_time - Package name:
com.almaghrib.quranrevdevelopment(legacy branding)
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:
- Full PayPal product and plan management via REST API
- Uses sandbox credentials (not production) — suggesting this was developed but never deployed to production
- Supports: create/update products, create plans, create subscriptions, retrieve products
- Admin views exist at
backend/views/paypal/for product/plan management
3. InfusionSoft Migration
Located at common/models/InfusionSoftSubscriptions.php:
- Model for
InfusionSoftSubscriptionstable tracking migrated subscription data - Fields: contact_id, product_id, subscription_plan_id, product_name, billing_cycle, start_date, end_date, last_billed_date, status, emailatInfusionSoft, charges
- Legacy webhook handler in
Ex_Subscriptions/MisGT.phpcontainsprocessStripeWebhooks()with InfusionSoft migration logic and hardcoded user_ids in TODO comments
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:
- Processes
customer.created,charge.succeeded,invoice.payment_succeeded,payment_intent.succeeded - Uses hardcoded legacy Stripe Plan IDs (e.g.,
plan_FXeRd8nuOaELrK) - Contains
TODOcomments with hardcoded user IDs for testing - Includes
createGoodWillSubscription()for free subscriptions andcreateStripeSubscription()for legacy Stripe subscriptions
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
Two subscription systems exist in parallel: The legacy QuranFlow
subscriptionstable (linked touser_link_level_tag_idwith cycles, billing dates, plan IDs) and the newer FEsubscription_signupstable (linked touser_idwith Stripe IDs, plan IDs, status). These are in different databases and serve different purposes.stripe_customer_idin the users table is inconsistently populated: The checkout flow stores the Stripe customer ID insubscription_signups.pp_customer_id, not inusers.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.No automated subscription lifecycle handling: The only webhook event processed is
invoice.paid. There is no handling ofcustomer.subscription.deleted,invoice.payment_failed, orcustomer.subscription.updated. A legacy handler inMisGT.phpprocesses more events but uses hardcoded user IDs and legacy plan IDs — it is not in active use.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).
Family plans and instalment plans have partial schema representation: The
payment_plans.typefield supportsfamilyanddiscountas values, andsub_typesupportsInstallment. 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 to4).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.
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-subscriptionto prevent duplicate purchases. This is documented indocs/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.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 (
couponsTokentable). The checkout only uses a single hardcoded coupon ID. The coupon management screens already show usage count per coupon (viaEx_Coupons.getJsonData()which joinssubscriptionsbycoupon_id).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.
Three payment platform transitions have occurred: InfusionSoft (migrated from) → PayPal (sandbox, never deployed) → Stripe (current). Historical data from InfusionSoft exists in the
InfusionSoftSubscriptionstable. 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:
- Real-time subscription status (active, past_due, canceled)
- Payment method details
- Upcoming renewal dates
- Failed payment attempts
- Refund history
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:
- Partial scholarships (25%, 75%)
- Scholarship with conditions (maintain submission count, attend sessions)
- Scholarship renewal tracking
- Different scholarship sources or programs
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:
- They have no access to Stripe
- The admin backend shows no billing data
- 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:
- Admin deactivates the student account in the backend (changes
user_activation_statusto 0) - Admin separately navigates to Stripe Dashboard
- Admin finds the customer by email
- 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:
- Deactivates the backend account
- Cancels the Stripe subscription (via API)
- Logs both actions
- Optionally processes a refund
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:
GET /v1/customers/{id}— Retrieve a customerGET /v1/customers?email={email}— Search by emailGET /v1/customers/search?query=email:'{email}'— Search API
Rate Limits: 100 read requests/second in live mode. Customer search is more expensive.
Admin Actions Available:
- Update customer metadata
- Update default payment method
- Apply credit/debit balance
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:
GET /v1/subscriptions/{id}— Retrieve a subscriptionGET /v1/subscriptions?customer={customer_id}— List customer's subscriptionsPOST /v1/subscriptions/{id}— Update (cancel, modify, pause)DELETE /v1/subscriptions/{id}— Cancel immediately
Rate Limits: 100 read requests/second. Listing with expand can be expensive.
Admin Actions Available:
- Cancel subscription (immediately or at period end)
- Pause collection (for deferments)
- Resume paused collection
- Update subscription (change plan, apply coupon)
- Schedule cancellation at a future date
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:
GET /v1/invoices?customer={customer_id}— List customer's invoices (full payment history)GET /v1/invoices/{id}— Retrieve a single invoiceGET /v1/invoices/upcoming?customer={customer_id}— Preview next invoice
Rate Limits: 100 read requests/second. Listing all invoices for a customer is a single API call with pagination.
Admin Actions Available:
- Void an open invoice
- Mark uncollectible
- Send invoice (email to customer)
- Create manual invoice
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:
GET /v1/payment_intents/{id}— Retrieve payment details- Not typically listed directly — accessed via Invoice
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:
GET /v1/coupons— List all couponsGET /v1/coupons/{id}— Retrieve a couponPOST /v1/coupons— Create a coupon
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:
GET /v1/prices— List all pricesGET /v1/products— List all products
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:
users.stripe_customer_id (QuranFlow backend): Populated only when subscriptions are created through the admin panel's
StripeIntegrationComponent. Students who signed up through the checkout (most students) have their Stripe customer ID stored insubscription_signups.pp_customer_idin the FE database instead.subscription_signups.pp_customer_id (FE database): Populated by the webhook handler in
fe-checkout-backendfor all checkout-originated subscriptions.
Implication: To look up a student's Stripe data, the system needs to:
- Check
users.stripe_customer_idfirst - If empty, look up
subscription_signups.pp_customer_idin the FE database by user_id - 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:
- Subscription status, plan, payment history, special flags
- NO action buttons except "View in Stripe" (which they won't have access to unless given Stripe viewer role)
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)
- Admin promotes students (or automation does)
- Admin goes to Stripe to create subscriptions for continuing students
- But promotions may change (teacher overrides, student opt-outs)
- 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
- Step 3 uses Stripe's
Subscription::create()orSubscription Scheduleto create subscriptions that start on the new semester date - Step 4 uses the linked deactivation action (Stripe cancel + backend deactivate)
- Step 5 is a read-only verification view
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
| 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:
- Billing data on student profiles — regardless of where billing lives as a section, the student profile must have a payment tab
- Billing alerts on the dashboard — failed payments and past-due subscriptions must surface on whatever serves as the operational dashboard
- A billing configuration area — plans, coupons, scholarships, deferments, family plans need a management home
- A semester-close billing step — the semester close workflow must include billing operations
- 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)
codebase/app/Http/Controllers/StripeController.php— Stripe API interactionscodebase/app/Http/Controllers/WebhookController.php— Webhook receivercodebase/routes/web.php— Checkout flow orchestrationcodebase/app/Services/AlMaghribApiService.php— API client to backendcodebase/config/services.php— Stripe configurationcodebase/resources/js/Pages/Checkout/Checkout.vue— Checkout UIcodebase/resources/js/Pages/Checkout/Scholarship.vue— Scholarship page
fe-checkout-backend (almaghrib-engineering/fe-checkout-backend)
codebase/app/Modules/Checkout/Services/StripeService.php— Critical webhook handlercodebase/app/Modules/Checkout/Services/CheckoutService.php— Session CRUDcodebase/app/Modules/Checkout/Services/CheckoutItemService.php— Item and subscription managementcodebase/app/Modules/Checkout/Routes/api.php— API routescodebase/app/Modules/Checkout/Models/Checkout.php— Checkout modelcodebase/app/Modules/Checkout/Models/CheckoutItem.php— Item modelcodebase/app/Modules/Checkout/Models/CheckoutRecurringFe.php— FE subscription signup modelcodebase/app/Modules/Checkout/Database/Migrations/2025_02_10_135429_checkout.php— Checkout tablescodebase/app/Modules/Checkout/Database/Migrations/2025_02_12_135429_checkout_items.php— Item tablescodebase/app/Modules/Checkout/Database/Migrations/2025_07_12_061223_checkout_recurring_fe.php— Recurring FE table
quranflow-backend (almaghrib-engineering/quranflow-backend)
Core billing components:
common/components/StripeIntegrationComponent.php— Stripe API wrapper (Customer, Subscription, Charge, Coupon, Product operations)common/components/PaypalComponent.php— PayPal REST API wrapper (sandbox credentials; product/plan/subscription management)common/helpers/GoogleInAppPurchase.php— Google Play subscription lifecycle (11 notification types, user state updates)common/helpers/InAppPurchase.php— Base IAP logging class
Data models:
common/models/Subscriptions.php— Subscription data model (linked to user_link_level_tag_id)common/models/Payments.php— Payment transactions (marchant: 1=Stripe, 2=PayPal; status, coupon_id, subscription_id, user_id)common/models/PaymentPlans.php— Plan definitions (type: semester/Year/family/discount; sub_type: OneTime/Installment; product_id for Stripe lookup)common/models/Coupons.php— Coupons (semester-scoped, percentage_off, expiry_date)common/models/CouponsToken.php— Single-use coupon tokens (token, coupon_id, redeemed_by)common/models/StripeProducts.php— Stripe product/plan records (name, type, currency, interval, amount)common/models/InfusionSoftSubscriptions.php— Legacy InfusionSoft migration datacommon/models/User.php— User model (stripe_customer_id + undeclared google_in_app_purchase_* fields)
Extended models:
common/models/extended/generic/Ex_Subscriptions.php— Subscription constants, type/pricing definitionscommon/models/extended/generic/Ex_Subscriptions/AddGT.php— Subscription creation (cycle-based billing dates)common/models/extended/generic/Ex_Subscriptions/GetGT.php— Query methods (goodwill, stripe, by user)common/models/extended/generic/Ex_Subscriptions/MisGT.php— Legacy: goodwill creation, legacy Stripe subscriptions, processStripeWebhooks (hardcoded plan IDs/user IDs)common/models/extended/payments/Ex_Payments.php— recordTransaction(), getPaymentsHistory() (raw SQL)common/models/extended/payments/Ex_Coupons.php— Coupon listing with usage count (joins subscriptions by coupon_id)common/models/extended/payments/Ex_PaymentPlans.php— Plan lookup by product_id
Controllers:
backend/controllers/PaymentsController.php— Payment history view (create action has die('test') — dead code)backend/controllers/CouponsController.php— Coupon CRUD + Stripe sync via createCoupon()backend/controllers/PaymentPlansController.php— Plan CRUDbackend/controllers/StripeProductsController.php— Stripe product management (not in main nav)backend/controllers/PaypalController.php— PayPal product/plan management (sandbox)backend/controllers/InAppPurchaseController.php— Google IAP webhook receiver
Views:
backend/views/users/_student_subscription_details.php— Student profile subscription card (local DB subscriptions, Renew/Cancel buttons)backend/views/users/_subscription_modal.php— Manual subscription creation (Goodwill/Stripe choice, Stripe card element)backend/views/users/_refund.php— Refund modal (subscription dropdown, confirm action)backend/views/site/_subscriptions_stats_bar.php— Dashboard subscription stats (hardcoded family/dependent values)backend/views/payment-plans/_form.php— Plan form (type: semester/Year/family/discount; sub_type: OneTime/Installment)backend/views/coupons/_form.php— Coupon form (name, semester, short_code, percentage_off, expiry)backend/assets_n/js/custom/stripe-components.js— Legacy Stripe.js card element with test API key
alm-checkout (almaghrib-engineering/alm-checkout)
codebase/app/Services/StripeService.php— Multi-account Stripe service (6 accounts: US, CA, GB, GB_FD, AU, FE)codebase/app/Http/Controllers/CheckoutController.php— Template-based checkout flow handlercodebase/config/services.php— Multi-account Stripe config + QuranFlow API configcodebase/config/checkout.php— Checkout API configSTRIPE_IMPLEMENTATION_GUIDE.md— Comprehensive implementation guide (Phase 1/2 scenarios)codebase/database/migrations/2025_10_09_140105_create_customer_columns.php— Customer columns (stripe_id, pm_type, pm_last_four)codebase/database/migrations/2025_10_09_140106_create_subscriptions_table.php— Subscriptions table (Laravel Cashier-compatible)codebase/database/migrations/2025_10_09_140107_create_subscription_items_table.php— Subscription items tablecodebase/resources/js/components/Coupon.tsx— Coupon component (React)codebase/resources/js/components/Payment.tsx— Payment component (React)codebase/resources/js/components/stripe/StripeCheckoutEmbed.tsx— Embedded Stripe Checkout (React)codebase/resources/js/components/stripe/StripePaymentForm.tsx— Stripe payment form (React)codebase/resources/js/hooks/useStripeCheckout.ts— Stripe checkout hookcodebase/resources/js/types/stripe.ts— Stripe TypeScript type definitions
wp-plugin-checkout-alm (almaghrib-engineering/wp-plugin-checkout-alm)
codebase/plugin.php— WordPress plugin (UI embedding only, no billing logic)