Files
ALwrity/docs/Billing_Subscription/stripe-dev-guide.md

325 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
- `STRIPE_MODE`
- Stripe mode selector (`test` or `live`). If unset, mode is inferred from `STRIPE_SECRET_KEY` prefix.
- `STRIPE_PLAN_PRICE_MAPPING_TEST`
- JSON map for test mode tier/cycle to Stripe price IDs.
- `STRIPE_PLAN_PRICE_MAPPING_LIVE`
- JSON map for live mode tier/cycle to Stripe price IDs.
- `STRIPE_PLAN_PRICE_MAPPING` (fallback)
- Optional shared JSON map used only if mode-specific mapping env vars are not set.
- `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 loaded from environment JSON and validated at backend startup.
- Required mapping keys (fail-fast): `basic.monthly`, `pro.monthly`.
- Mode-specific env vars allow separate test/live values with no code edits.
- 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 is loaded in `StripeService` from env JSON:
- File: `backend/services/subscription/stripe_service.py`
Key structures:
- `STRIPE_PLAN_PRICE_MAPPING`
- Runtime map of `(SubscriptionTier, BillingCycle)` → Stripe `price_id` parsed from env vars.
- `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 mapping env vars (`STRIPE_PLAN_PRICE_MAPPING_TEST` / `STRIPE_PLAN_PRICE_MAPPING_LIVE`) with new price IDs.
3. Ensure a `SubscriptionPlan` row exists in the DB for the tier being mapped.
4. Redeploy backend. Startup validation will fail if required keys are missing or mapping JSON is malformed.
---
## 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 the relevant mapping environment variable (`STRIPE_PLAN_PRICE_MAPPING_TEST` or `STRIPE_PLAN_PRICE_MAPPING_LIVE`).
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.