# Stripe Subscription Flow — Complete Documentation ## Architecture Overview The Stripe integration runs as **two AWS Lambda functions** triggered via **AWS EventBridge**: 1. **`stripeEvent`** (`lambda/src/stripeEvent.ts`) — handles Stripe webhook events 2. **`billingChange`** (`lambda/src/billingChange.ts`) — handles internal billing timeout/revert events ### Supporting Modules - `eventHandlers/checkout.ts` — checkout session completed logic - `eventHandlers/customer.ts` — subscription updated/deleted logic - `eventHandlers/invoice.ts` — invoice paid/failed logic - `utils/email.ts` — SES email builder and sender - `utils/superAdminLog.ts` — audit log writer - `utils/db.ts` — MongoDB connection --- ## Pricing Model - **Per-office, monthly subscription** using a single Stripe Price ID - `quantity` = number of active offices in the organization - Configured via `STRIPE_PRICE_ID` and `STRIPE_SECRET_KEY` environment variables - The frontend shows: `quantity x price_per_office = total/month` --- ## Stripe Webhook Events Handled | # | Event | Handler | Purpose | |---|-------|---------|---------| | 1 | `checkout.session.completed` | `checkout.sessionCompleted` | New subscription purchased or trial started | | 2 | `invoice.paid` | `invoice.invoicePaid` | Recurring payment succeeded | | 3 | `invoice.payment_failed` | `invoice.invoicePaymentFailed` | Recurring payment failed | | 4 | `customer.subscription.updated` | `customer.subscriptionUpdated` | Subscription status changed or cancel scheduled | | 5 | `customer.subscription.deleted` | `customer.subscriptionDeleted` | Subscription fully canceled/ended | --- ## Complete Lifecycle ### Phase 1: Setup (Super Admin) A super admin switches an organization's payment method to "subscription": - `paymentMethod` set to `"subscription"` - `subscriptionDeadline` set to `now + 24 hours` - Account Manager(s) assigned to the organization - Account Manager receives an email with a link to the billing page ### Phase 2: Checkout (Account Manager) When the Account Manager clicks "Subscribe" on the Billing page: 1. **Validates prerequisites:** - Organization's payment method must be "subscription" - No existing active/trialing subscription - At least one active office exists 2. **Reuses or expires existing checkout session:** - If an open session exists with matching office count → reuses it - If office count changed since last session → expires old session, creates new one 3. **Creates/validates Stripe Customer:** - If `customerId` exists and is not deleted → reuses it - Otherwise creates a new Stripe Customer with org name and AM email 4. **Creates Stripe Checkout Session:** - `mode: "subscription"` - `quantity: activeOfficeCount` - If self-signup org and trial not used → adds `trial_period_days: 30` - Stores `checkoutSessionId` on the organization for session sharing among AMs 5. **Redirects to Stripe-hosted Checkout page** ### Phase 3: Subscription Active (Webhook) When `checkout.session.completed` fires: - Saves `customerId` and `subscriptionId` on the organization - Sets `stripeSubscription.status` to `"active"` or `"trialing"` - Clears `checkoutSessionId` and `subscriptionDeadline` - If trial: marks `trialUsed = true` (prevents re-use) - Creates audit log: `SUBSCRIPTION_PURCHASED` ### Phase 4: Ongoing Operations #### Adding an Office 1. App checks `maxOfficeLimit` — rejects if limit reached 2. Office is created 3. `syncSubscriptionQuantity()` is called: - Counts active offices - Updates Stripe subscription quantity via `stripe.subscriptions.update()` - Stripe creates a **prorated charge** for the remaining days in the billing cycle #### Removing an Office 1. Office is soft-deleted (`isDeleted: true`) 2. `syncSubscriptionQuantity()` is called: - Counts active offices - Updates Stripe subscription quantity - Stripe creates a **prorated credit** for unused days #### Monthly Renewal (invoice.paid) - Sends "Invoice Paid" email to all account managers with: - Invoice number, amount, date, PDF download link - Creates audit log: `INVOICE_PAID` #### Payment Failure (invoice.payment_failed) - Sends "Payment Failed" email to all account managers with: - Invoice details - Warning about deactivation - Link to update payment method (`/login?redirectToBilling=true`) - Creates audit log: `INVOICE_PAYMENT_FAILED` #### Subscription Updated (customer.subscription.updated) - Syncs subscription `status` to the organization record - If `cancel_at_period_end` is true: - Creates audit log: `SUBSCRIPTION_CANCEL_SCHEDULED` ### Phase 5: Cancellation #### User-Initiated Cancel 1. User confirms by typing organization name 2. App calls `stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true })` 3. Subscription continues until end of current billing period 4. Creates audit log: `SUBSCRIPTION_CANCEL_SCHEDULED` #### Undo Cancellation 1. User clicks "Undo Cancellation" 2. App calls `stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: false })` 3. Subscription resumes normally 4. Creates audit log: `SUBSCRIPTION_CANCEL_REVERTED` #### Subscription Deleted (at period end) When `customer.subscription.deleted` fires: 1. Clears `subscriptionId`, sets status to `"canceled"` 2. If `paymentMethod !== "invoice"`: - Sets `isActive = false` (deactivates organization) - Sends "Organization Deactivated" email to all account managers 3. Creates audit log: `SUBSCRIPTION_CANCELED` #### Super Admin Deactivation When a super admin suspends an org that has an active subscription: - Calls `stripe.subscriptions.cancel(subscriptionId)` — immediate cancellation - This triggers the `customer.subscription.deleted` webhook --- ## Proration — How It Works ### Stripe's Default Behavior The `syncSubscriptionQuantity()` function does NOT explicitly set `proration_behavior`. This means Stripe applies its default: **`create_prorations`**. ### What Happens When Quantity Changes Mid-Cycle **Office ADDED mid-cycle:** - Stripe calculates remaining days in the current billing period - Creates a prorated line item (charge) on the **next** invoice - Example: $100/office/month, office added on day 15 of 30-day cycle → ~$50 proration charge **Office REMOVED mid-cycle:** - Stripe creates a prorated line item (credit) on the **next** invoice - Example: $100/office/month, office removed on day 10 → ~$67 credit **On the next regular invoice:** - Proration line items (charges and credits) appear alongside the new regular charge at the updated quantity - After this one-time adjustment, billing continues at the standard monthly rate ### Proration Scenarios Table | Scenario | Stripe Behavior | When Charged/Credited | |----------|----------------|----------------------| | Office added mid-cycle | Prorated charge for remaining days | Next invoice | | Office removed mid-cycle | Prorated credit for unused days | Next invoice | | Multiple offices added/removed | Multiple proration line items | Next invoice (net sum) | | Trial → Active (trial ends) | Full charge at current quantity | First real invoice | | Cancel at period end | No proration — runs until period end | Nothing extra | | Quantity unchanged at renewal | Standard charge: qty x price | Regular invoice | ### Important Notes on Prorations 1. **No immediate invoicing** — prorations accumulate to the next scheduled invoice (Stripe is NOT configured for `always_invoice` mode) 2. **No proration preview** — the system does not show users what the proration will be before adding/removing an office 3. **Fire-and-forget sync** — `syncSubscriptionQuantity()` is called with `.catch(() => {})`, meaning office creation/deletion succeeds even if the Stripe sync fails --- ## 24-Hour Deadline Enforcement If an organization is set to `paymentMethod: "subscription"` but doesn't complete Stripe Checkout within 24 hours, a scheduled EventBridge event triggers `billingChange`: ### Reason: `billing_revert` - Reverts `paymentMethod` to `"invoice"` - Clears `subscriptionDeadline` - Removes all account managers (`isAccountManager = false`) - Creates audit log: `BILLING_REVERTED` ### Reason: `activation_timeout` - Deactivates organization (`isActive = false`) - Clears `subscriptionDeadline` - Removes all account managers - Sends "Activation Timeout Deactivated" email to account managers - Creates audit log: `ORG_DEACTIVATED` Both scenarios skip processing if the org already has an active/trialing subscription. --- ## Manage Subscription (Stripe Customer Portal) Account Managers can access the Stripe Billing Portal to: - Update payment method (card) - View invoice history - Download receipts The portal is created via `stripe.billingPortal.sessions.create()` with a return URL to `/billing`. --- ## Maximum Office Limit - `stripeSubscription.maxOfficeLimit` is an **application-level cap only** - Stripe does NOT enforce this limit — it's checked in the office creation controller - Account Managers can update this limit from the Billing page - The limit cannot be set below the current active office count --- ## Emails Sent | Email | Recipients | Trigger | |-------|-----------|---------| | Invoice Paid | All active account managers | `invoice.paid` webhook | | Payment Failed | All active account managers | `invoice.payment_failed` webhook | | Organization Deactivated | All active account managers | `customer.subscription.deleted` (non-invoice orgs) | | Activation Timeout Deactivated | All active account managers | `billingChange` with `activation_timeout` | | Account Manager Added | Newly assigned AM | Super admin assigns AM during setup | | Setup Credit Card | Account Manager | Organization switched to subscription | --- ## Super Admin Audit Log Action Types | Action Type | When | |-------------|------| | `SUBSCRIPTION_PURCHASED` | Checkout completed | | `SUBSCRIPTION` | Checkout session created | | `INVOICE_PAID` | Invoice paid | | `INVOICE_PAYMENT_FAILED` | Invoice payment failed | | `SUBSCRIPTION_CANCEL_SCHEDULED` | Cancel scheduled (user or webhook) | | `SUBSCRIPTION_CANCEL_REVERTED` | Cancel undone by user | | `SUBSCRIPTION_CANCELED` | Subscription deleted | | `OFFICE_QTY_UPDATED` | Max office limit changed | | `ORG_DEACTIVATED` | Activation timeout | | `BILLING_REVERTED` | 24hr deadline expired, reverted to invoice | --- ## API Endpoints | Method | Path | Purpose | |--------|------|---------| | GET | `/payment/api/billing-details` | Get org, office count, and live Stripe subscription | | GET | `/payment/api/price` | Get unit price per office | | PUT | `/payment/api/max-office-limit` | Update maximum office limit | | POST | `/payment/api/create-checkout-session` | Create/reuse Stripe Checkout session | | POST | `/payment/api/cancel-subscription` | Schedule cancellation at period end | | POST | `/payment/api/revert-cancel-subscription` | Undo scheduled cancellation | | POST | `/payment/api/customer-portal` | Create Stripe Billing Portal session | | GET | `/payment/api/payment-history` | List all invoices for the org | --- ## Data Model (Organization.stripeSubscription) ```typescript interface StripeSubscription { customerId?: string | null; // Stripe Customer ID subscriptionId?: string | null; // Stripe Subscription ID status?: "active" | "past_due" | "unpaid" | "canceled" | "incomplete" | "trialing" | null; maxOfficeLimit?: number | null; // App-level cap on offices quantity?: number | null; // Stored quantity (informational) priceId?: string | null; // Stripe Price ID checkoutSessionId?: string | null; // Active checkout session for sharing } ``` Related Organization fields: - `paymentMethod: "invoice" | "subscription"` — billing mode - `subscriptionDeadline: Date | null` — 24hr checkout deadline - `selfSignup: boolean` — whether org was self-created - `trialUsed: boolean` — prevents repeated trials --- ## Flow Diagram ``` Super Admin switches to "subscription" │ ▼ ┌─────────────────────────────────┐ │ 24-hour countdown starts │ │ Account Manager(s) assigned │ │ AM receives email │ └────────────┬────────────────────┘ │ ┌────────┴────────┐ │ │ ▼ ▼ Completes Misses 24hr Checkout deadline │ │ │ ┌──────┴──────┐ │ │ │ │ ▼ ▼ │ billing_revert activation_timeout │ (→ invoice) (→ deactivate) │ ▼ ┌─────────────────────────────────┐ │ SUBSCRIPTION ACTIVE │ │ status: active/trialing │ └────────────┬────────────────────┘ │ ┌─────────┼─────────────┐ │ │ │ ▼ ▼ ▼ Add Office Remove Office Monthly Invoice │ │ │ ▼ ▼ │ qty + 1 qty - 1 ▼ │ │ ┌─────────┐ │ │ │ Paid? │ ▼ ▼ ├─Yes─────┼──→ Email "Invoice Paid" Proration Proration └─No──────┘──→ Email "Payment Failed" charge credit (next inv) (next inv) │ ▼ ┌─────────────────────────────────┐ │ CANCELLATION │ │ │ │ User cancels: │ │ → cancel_at_period_end=true │ │ → runs until period end │ │ → can undo before period end │ │ │ │ Super Admin suspends: │ │ → immediate cancel │ │ │ │ At period end: │ │ → subscription.deleted │ │ → org deactivated │ │ → email sent │ └─────────────────────────────────┘ ``` create pdf for this

Reading File
Reading File
Running Command
Writing File
Running Command
Running Command
Your PDF is ready:

Stripe Subscription Flow

PDF Document · PDF

Here's what's included across ~13 sections:
  • Cover page - dark branded cover with architecture badge row (Lambda, EventBridge, Stripe, MongoDB, SES)
  • Table of Contents - all 13 sections with page numbers
  • Architecture Overview - Lambda functions table + supporting modules
  • Pricing Model - env vars, quantity model
  • Webhook Events - all 5 events in a formatted table
  • Complete Lifecycle - all 5 phases rendered as colored phase blocks with step-by-step bullets
  • Proration - side-by-side add/remove boxes, scenarios table, and 3 important notes callouts
  • 24-Hour Deadline - two-column billing_revert vs activation_timeout breakdown
  • Customer Portal, Max Office Limit, Emails, Audit Log, API Endpoints - all as styled tables
  • Data Model - TypeScript interface in a code block + related fields table
  • Flow Diagram - visual block diagram with colored boxes showing the full lifecycle flow
This is a shared conversation. Sign in to Orris to start your own chat.