# 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.

