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

9.7 KiB
Raw Blame History

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.