315 lines
9.7 KiB
Markdown
315 lines
9.7 KiB
Markdown
# Stripe Billing & Subscriptions – Developer Guide
|
||
|
||
This document explains how Stripe is integrated into ALwrity for subscriptions, billing, disputes, and fraud handling. It is aimed at developers working on the backend and frontend.
|
||
|
||
---
|
||
|
||
## 1. High-Level Architecture
|
||
|
||
- **Backend**
|
||
- Core service: `StripeService`
|
||
- File: `backend/services/subscription/stripe_service.py`
|
||
- Subscription/payment API routes:
|
||
- `backend/api/subscription/routes/payment.py`
|
||
- `backend/api/subscription/routes/disputes.py`
|
||
- `backend/api/subscription/routes/fraud_warnings.py`
|
||
- Models:
|
||
- `UserSubscription`, `SubscriptionPlan`, `BillingCycle`, `UsageStatus`, `FraudWarning`
|
||
- File: `backend/models/subscription_models.py`
|
||
- **Frontend**
|
||
- Pricing and checkout UI:
|
||
- `frontend/src/components/Pricing/PricingPage.tsx`
|
||
- Internal admin dashboards:
|
||
- `frontend/src/pages/StripeDisputesDashboard.tsx`
|
||
- Routing:
|
||
- `frontend/src/App.tsx` (route at `/stripe-disputes`)
|
||
|
||
Data flows:
|
||
|
||
- Public users:
|
||
- Browse pricing → select plan → start Stripe Checkout → complete subscription.
|
||
- Admin/internal users:
|
||
- Use `/stripe-disputes` dashboard to manage disputes and early fraud warnings.
|
||
|
||
---
|
||
|
||
## 2. Configuration & Environment
|
||
|
||
Required environment variables (backend):
|
||
|
||
- `STRIPE_SECRET_KEY`
|
||
- Stripe API key (test or live).
|
||
- `STRIPE_WEBHOOK_SECRET`
|
||
- Webhook signing secret for subscription webhooks.
|
||
- `ADMIN_EMAILS` (optional)
|
||
- Comma-separated list of admin emails allowed to access dispute/fraud endpoints.
|
||
- `ADMIN_EMAIL_DOMAIN` (optional)
|
||
- Domain considered admin (e.g. `example.com`).
|
||
- `DISABLE_AUTH` (optional)
|
||
- If `"true"`, bypasses admin checks for local/testing use only.
|
||
|
||
Stripe configuration:
|
||
|
||
- Price IDs are mapped in code (see below) and must exist in the configured Stripe account.
|
||
- Webhook endpoint must be configured in Stripe Dashboard:
|
||
- Path: `/api/subscription/webhook`
|
||
- Events: `checkout.session.completed`, `invoice.payment_succeeded`, `invoice.payment_failed`, `customer.subscription.updated`, `customer.subscription.deleted`, `radar.early_fraud_warning.created` (and optionally `radar.early_fraud_warning.updated`).
|
||
|
||
---
|
||
|
||
## 3. Plans, Prices and Mapping
|
||
|
||
Stripe price mapping lives in `StripeService`:
|
||
|
||
- File: `backend/services/subscription/stripe_service.py`
|
||
|
||
Key structures:
|
||
|
||
- `STRIPE_PLAN_PRICE_MAPPING`
|
||
- Maps `(SubscriptionTier, BillingCycle)` → Stripe `price_id`.
|
||
- `STRIPE_PRICE_TO_PLAN`
|
||
- Reverse map: `price_id` → `{ tier, billing_cycle }`.
|
||
|
||
Helper methods:
|
||
|
||
- `_get_price_id_for_plan(tier, billing_cycle) -> str`
|
||
- Used when creating Checkout sessions.
|
||
- `_get_plan_for_price_id(price_id) -> (SubscriptionPlan, BillingCycle)`
|
||
- Used when mapping Stripe subscription items back into our internal `SubscriptionPlan`.
|
||
|
||
### Adding or updating plans
|
||
|
||
1. Create prices in Stripe (with correct recurring configuration).
|
||
2. Update `STRIPE_PLAN_PRICE_MAPPING` with new price IDs.
|
||
3. Ensure a `SubscriptionPlan` row exists in the DB for the tier being mapped.
|
||
4. Redeploy backend with updated mapping.
|
||
|
||
---
|
||
|
||
## 4. Checkout and Subscription Lifecycle
|
||
|
||
### 4.1 Create Checkout Session
|
||
|
||
Endpoint:
|
||
|
||
- `POST /api/subscription/create-checkout-session`
|
||
- File: `backend/api/subscription/routes/payment.py`
|
||
|
||
Request body:
|
||
|
||
- `tier: SubscriptionTier` (e.g. `"basic"`, `"pro"`)
|
||
- `billing_cycle: BillingCycle` (e.g. `"monthly"`)
|
||
- `success_url: str`
|
||
- `cancel_url: str`
|
||
|
||
Flow:
|
||
|
||
1. Auth middleware resolves `current_user` and `user_id`.
|
||
2. `StripeService.create_checkout_session`:
|
||
- Fetches `price_id` via `_get_price_id_for_plan`.
|
||
- Finds or creates Stripe Customer (with `user_id` in metadata).
|
||
- Creates a Stripe Checkout Session:
|
||
- Mode: `subscription`.
|
||
- Metadata: includes `user_id` and `price_id`.
|
||
3. Returns `checkout_session.url` to the frontend.
|
||
|
||
Special handling:
|
||
|
||
- Metered prices:
|
||
- For metered prices, `quantity` is omitted to comply with Stripe rules.
|
||
- For non-metered prices, `quantity` is set to `1`.
|
||
|
||
### 4.2 Customer Portal Session
|
||
|
||
Endpoint:
|
||
|
||
- `POST /api/subscription/create-portal-session`
|
||
|
||
Flow:
|
||
|
||
1. Lookup `UserSubscription` and `stripe_customer_id`.
|
||
2. If missing, search Stripe by `metadata['user_id']`.
|
||
3. Create Stripe Billing Portal session and return URL.
|
||
|
||
### 4.3 Webhook Handling
|
||
|
||
Endpoint:
|
||
|
||
- `POST /api/subscription/webhook`
|
||
- File: `backend/api/subscription/routes/payment.py`
|
||
- Delegates to `StripeService.handle_webhook`.
|
||
|
||
Verification:
|
||
|
||
- `stripe.Webhook.construct_event(payload, sig_header, STRIPE_WEBHOOK_SECRET)` is used to validate signatures.
|
||
|
||
Handled events:
|
||
|
||
- `checkout.session.completed`
|
||
- Retrieves subscription and price.
|
||
- Updates `UserSubscription` to active and stores `stripe_customer_id` and `stripe_subscription_id`.
|
||
- `invoice.payment_succeeded`
|
||
- Sets `UserSubscription.status` to `ACTIVE`.
|
||
- Updates `current_period_end` from invoice period.
|
||
- `invoice.payment_failed`
|
||
- Sets status to `PAST_DUE`, `is_active` false.
|
||
- `customer.subscription.updated`
|
||
- Syncs status and `auto_renew`.
|
||
- `customer.subscription.deleted`
|
||
- Marks subscription as cancelled and disables auto renew.
|
||
|
||
Helper:
|
||
|
||
- `_update_user_subscription` centralizes updating/creating `UserSubscription` records based on Stripe data.
|
||
|
||
---
|
||
|
||
## 5. Disputes Integration
|
||
|
||
Backend routes:
|
||
|
||
- File: `backend/api/subscription/routes/disputes.py`
|
||
|
||
Endpoints:
|
||
|
||
- `GET /api/subscription/disputes`
|
||
- Proxies `stripe.Dispute.list`.
|
||
- `GET /api/subscription/disputes/{dispute_id}`
|
||
- Proxies `stripe.Dispute.retrieve`.
|
||
- `POST /api/subscription/disputes/{dispute_id}`
|
||
- Proxies `stripe.Dispute.modify` with `evidence`.
|
||
- `POST /api/subscription/disputes/{dispute_id}/close`
|
||
- Proxies `stripe.Dispute.close`.
|
||
|
||
Admin guard:
|
||
|
||
- `_ensure_admin(current_user)` ensures:
|
||
- Admin by email, domain, or role `"admin"`.
|
||
- Can be bypassed only when `DISABLE_AUTH=true` (local use).
|
||
|
||
Frontend UI:
|
||
|
||
- File: `frontend/src/pages/StripeDisputesDashboard.tsx`
|
||
- Route: `/stripe-disputes`
|
||
- Disputes tab:
|
||
- Lists disputes and allows:
|
||
- Viewing details.
|
||
- Submitting evidence fields:
|
||
- `customer_email_address`, `customer_name`, `customer_purchase_ip`, `access_activity_log`, `uncategorized_text`.
|
||
- Tagging a high-level fraud type, which is encoded into `uncategorized_text`.
|
||
- Closing the dispute.
|
||
|
||
---
|
||
|
||
## 6. Early Fraud Warnings (EFW) and Proactive Refunds
|
||
|
||
### 6.1 Ingestion
|
||
|
||
Model:
|
||
|
||
- `FraudWarning` in `backend/models/subscription_models.py`
|
||
- Columns: `id`, `charge_id`, `payment_intent_id`, `user_id`, `amount`, `currency`, `status`, `action`, `action_at`, `reason_notes`, `metadata`, `created_at`.
|
||
|
||
Ingestion logic:
|
||
|
||
- `StripeService._handle_early_fraud_warning`:
|
||
- Triggered for event types starting with `radar.early_fraud_warning.`.
|
||
- Retrieves the associated `Charge` to populate amount, currency, and metadata.
|
||
- Infers `user_id` from `charge.metadata.user_id` when available.
|
||
- Upserts a `FraudWarning` row with status `"open"` and action `"none"`.
|
||
- Stores raw EFW and Charge data in `metadata`.
|
||
|
||
### 6.2 Fraud Warnings API
|
||
|
||
File: `backend/api/subscription/routes/fraud_warnings.py`
|
||
|
||
Endpoints:
|
||
|
||
- `GET /api/subscription/fraud-warnings`
|
||
- Query params:
|
||
- `status` (default `"open"`)
|
||
- `limit`, `offset`
|
||
- Returns a list of warnings with core fields.
|
||
- `GET /api/subscription/fraud-warnings/{id}`
|
||
- Returns full details including `metadata`.
|
||
- `POST /api/subscription/fraud-warnings/{id}/refund`
|
||
- Performs a **full refund** via `stripe.Refund.create(charge=...)`.
|
||
- Updates `status="refunded"`, `action="refund_full"`, `action_at` and `reason_notes`.
|
||
- `POST /api/subscription/fraud-warnings/{id}/ignore`
|
||
- Sets `status="ignored"`, `action="ignored"`, updates notes.
|
||
|
||
All endpoints apply the same admin guard used for disputes.
|
||
|
||
### 6.3 Frontend Fraud Warnings Tab
|
||
|
||
- File: `frontend/src/pages/StripeDisputesDashboard.tsx`
|
||
|
||
Behavior:
|
||
|
||
- Adds a tabbed view:
|
||
- Tab 1: Disputes.
|
||
- Tab 2: Fraud Warnings.
|
||
- Fraud Warnings tab:
|
||
- Lists EFWs (from `/fraud-warnings`).
|
||
- Shows details including:
|
||
- Stripe EFW `fraud_type`, `actionable` flag.
|
||
- Amount, created time, internal status/action.
|
||
- Allows:
|
||
- Proactive full refund (calls `/fraud-warnings/{id}/refund`).
|
||
- Mark as ignored (calls `/fraud-warnings/{id}/ignore`).
|
||
- Add/update internal notes.
|
||
|
||
---
|
||
|
||
## 7. Rate Limiting for Checkout
|
||
|
||
Endpoint: `POST /api/subscription/create-checkout-session`
|
||
|
||
File: `backend/api/subscription/routes/payment.py`
|
||
|
||
Logic:
|
||
|
||
- Per-user in-memory rate limiting:
|
||
- Window: 60 seconds.
|
||
- Max requests: 10 within the window.
|
||
- On exceed:
|
||
- Logs a warning with `user_id`, IP, attempts count.
|
||
- Returns HTTP 429 with a friendly error message.
|
||
|
||
Purpose:
|
||
|
||
- Protects against card testing and abuse by limiting how often a user can create Checkout sessions.
|
||
|
||
Considerations:
|
||
|
||
- For multi-instance deployments, a shared store (e.g. Redis) is recommended to make rate limiting consistent across instances.
|
||
|
||
---
|
||
|
||
## 8. Extending and Maintaining the Integration
|
||
|
||
### Adding new subscription tiers or prices
|
||
|
||
1. Create or update prices in Stripe.
|
||
2. Update `STRIPE_PLAN_PRICE_MAPPING` in `StripeService`.
|
||
3. Ensure corresponding rows in `SubscriptionPlan`.
|
||
4. Add any needed frontend logic (e.g. additional tiers in pricing UI).
|
||
|
||
### Supporting additional Stripe events
|
||
|
||
- Extend `StripeService.handle_webhook` with new event types.
|
||
- Implement corresponding handlers (`_handle_*`) that:
|
||
- Parse event data.
|
||
- Update your DB models.
|
||
- Log with enough context.
|
||
|
||
### Making the system more robust
|
||
|
||
- Reintroduce idempotency keys for write operations (Checkout creation, refunds) using stable dedupe keys.
|
||
- Replace in-memory rate limiting with shared store-based limiting when scaling horizontally.
|
||
- Add more detailed logs/metrics around:
|
||
- New subscriptions.
|
||
- Failed payments.
|
||
- Disputes and early fraud warnings.
|
||
|