From fc96e1218a83df452df51fd38b06d2fb752bea37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=8A?= Date: Wed, 4 Mar 2026 20:41:47 +0530 Subject: [PATCH] Move Stripe plan price mapping to env with startup validation --- backend/README.md | 20 +++++ .../services/subscription/stripe_service.py | 80 ++++++++++++++++++- docs/Billing_Subscription/stripe-dev-guide.md | 22 +++-- .../stripe-go-live-checklist.md | 10 ++- 4 files changed, 120 insertions(+), 12 deletions(-) diff --git a/backend/README.md b/backend/README.md index e9939ab5..2149a9ae 100644 --- a/backend/README.md +++ b/backend/README.md @@ -199,6 +199,26 @@ You can customize the server behavior with these environment variables: - `PORT`: Server port (default: 8000) - `RELOAD`: Enable auto-reload (default: true) +Subscription billing (Stripe) variables used in deployment: + +- `STRIPE_SECRET_KEY`: Stripe API secret key (`sk_test_...` for test, `sk_live_...` for live). +- `STRIPE_WEBHOOK_SECRET`: Stripe webhook signing secret for `/api/subscription/webhook`. +- `STRIPE_MODE`: Stripe mode selector (`test` or `live`). Recommended to set explicitly in each environment. +- `STRIPE_PLAN_PRICE_MAPPING_TEST`: JSON mapping for test mode price IDs. +- `STRIPE_PLAN_PRICE_MAPPING_LIVE`: JSON mapping for live mode price IDs. +- `STRIPE_PLAN_PRICE_MAPPING`: Optional fallback JSON mapping used when mode-specific variable is not provided. + +Required mapping keys validated at startup: + +- `basic.monthly` +- `pro.monthly` + +Example mapping value: + +```json +{"basic":{"monthly":"price_123"},"pro":{"monthly":"price_456"}} +``` + Example: ```bash HOST=127.0.0.1 PORT=8080 python start_alwrity_backend.py diff --git a/backend/services/subscription/stripe_service.py b/backend/services/subscription/stripe_service.py index afd0e133..6e543d56 100644 --- a/backend/services/subscription/stripe_service.py +++ b/backend/services/subscription/stripe_service.py @@ -1,3 +1,4 @@ +import json import os import stripe from typing import Optional, Dict, Any @@ -8,11 +9,84 @@ from services.subscription.pricing_service import PricingService from datetime import datetime -STRIPE_PLAN_PRICE_MAPPING = { - (SubscriptionTier.BASIC.value, BillingCycle.MONTHLY.value): "price_1T2lWHR2EuR7zQJepLIVQ1EJ", - (SubscriptionTier.PRO.value, BillingCycle.MONTHLY.value): "price_1T2ljDR2EuR7zQJeuS317KCj", +REQUIRED_STRIPE_PLAN_KEYS = { + (SubscriptionTier.BASIC.value, BillingCycle.MONTHLY.value), + (SubscriptionTier.PRO.value, BillingCycle.MONTHLY.value), } + +def _detect_stripe_mode() -> str: + configured_mode = os.getenv("STRIPE_MODE", "").strip().lower() + if configured_mode in {"test", "live"}: + return configured_mode + + secret_key = os.getenv("STRIPE_SECRET_KEY", "").strip() + if secret_key.startswith("sk_live_"): + return "live" + if secret_key.startswith("sk_test_"): + return "test" + + # Default to test when mode cannot be derived. + return "test" + + +def _normalize_stripe_plan_price_mapping(raw_mapping: Dict[str, Any]) -> Dict[tuple[str, str], str]: + normalized_mapping: Dict[tuple[str, str], str] = {} + + for tier, billing_cycle_map in raw_mapping.items(): + if not isinstance(billing_cycle_map, dict): + raise RuntimeError( + "Stripe plan mapping must be nested JSON in the form " + '{"basic": {"monthly": "price_..."}}.' + ) + + for billing_cycle, price_id in billing_cycle_map.items(): + if not isinstance(price_id, str) or not price_id.strip(): + raise RuntimeError( + f"Invalid Stripe price id for tier={tier}, billing_cycle={billing_cycle}." + ) + normalized_mapping[(tier, billing_cycle)] = price_id.strip() + + return normalized_mapping + + +def _load_stripe_plan_price_mapping() -> Dict[tuple[str, str], str]: + stripe_mode = _detect_stripe_mode() + mode_var_name = f"STRIPE_PLAN_PRICE_MAPPING_{stripe_mode.upper()}" + raw_mapping_json = os.getenv(mode_var_name) or os.getenv("STRIPE_PLAN_PRICE_MAPPING") + + if not raw_mapping_json: + raise RuntimeError( + "Missing Stripe plan mapping configuration. Set " + f"{mode_var_name} (recommended) or STRIPE_PLAN_PRICE_MAPPING." + ) + + try: + parsed_mapping = json.loads(raw_mapping_json) + except json.JSONDecodeError as exc: + raise RuntimeError( + f"Invalid JSON in {mode_var_name}/STRIPE_PLAN_PRICE_MAPPING: {exc.msg}" + ) from exc + + if not isinstance(parsed_mapping, dict): + raise RuntimeError("Stripe plan mapping must decode to a JSON object.") + + mapping = _normalize_stripe_plan_price_mapping(parsed_mapping) + missing_keys = REQUIRED_STRIPE_PLAN_KEYS - set(mapping.keys()) + if missing_keys: + missing = ", ".join( + sorted([f"{tier}:{billing_cycle}" for tier, billing_cycle in missing_keys]) + ) + raise RuntimeError( + "Stripe plan mapping is missing required tier/cycle combinations: " + f"{missing}." + ) + + return mapping + + +STRIPE_PLAN_PRICE_MAPPING = _load_stripe_plan_price_mapping() + STRIPE_PRICE_TO_PLAN = { price_id: {"tier": SubscriptionTier(tier), "billing_cycle": BillingCycle(billing_cycle)} for (tier, billing_cycle), price_id in STRIPE_PLAN_PRICE_MAPPING.items() diff --git a/docs/Billing_Subscription/stripe-dev-guide.md b/docs/Billing_Subscription/stripe-dev-guide.md index ede5fbbe..0ff7d4f7 100644 --- a/docs/Billing_Subscription/stripe-dev-guide.md +++ b/docs/Billing_Subscription/stripe-dev-guide.md @@ -41,6 +41,14 @@ Required environment variables (backend): - 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) @@ -50,7 +58,9 @@ Required environment variables (backend): Stripe configuration: -- Price IDs are mapped in code (see below) and must exist in the configured Stripe account. +- 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`). @@ -59,14 +69,14 @@ Stripe configuration: ## 3. Plans, Prices and Mapping -Stripe price mapping lives in `StripeService`: +Stripe price mapping is loaded in `StripeService` from env JSON: - File: `backend/services/subscription/stripe_service.py` Key structures: - `STRIPE_PLAN_PRICE_MAPPING` - - Maps `(SubscriptionTier, BillingCycle)` → Stripe `price_id`. + - Runtime map of `(SubscriptionTier, BillingCycle)` → Stripe `price_id` parsed from env vars. - `STRIPE_PRICE_TO_PLAN` - Reverse map: `price_id` → `{ tier, billing_cycle }`. @@ -80,9 +90,9 @@ Helper methods: ### Adding or updating plans 1. Create prices in Stripe (with correct recurring configuration). -2. Update `STRIPE_PLAN_PRICE_MAPPING` with new price IDs. +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 with updated mapping. +4. Redeploy backend. Startup validation will fail if required keys are missing or mapping JSON is malformed. --- @@ -291,7 +301,7 @@ Considerations: ### Adding new subscription tiers or prices 1. Create or update prices in Stripe. -2. Update `STRIPE_PLAN_PRICE_MAPPING` in `StripeService`. +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). diff --git a/docs/Billing_Subscription/stripe-go-live-checklist.md b/docs/Billing_Subscription/stripe-go-live-checklist.md index 30ca36a8..012e74a8 100644 --- a/docs/Billing_Subscription/stripe-go-live-checklist.md +++ b/docs/Billing_Subscription/stripe-go-live-checklist.md @@ -14,6 +14,9 @@ Tick each item as you complete it. - [ ] **Environment variables configured for production** - [ ] `STRIPE_SECRET_KEY` set to **live** secret key. - [ ] `STRIPE_WEBHOOK_SECRET` set to **live** webhook signing secret. + - [ ] `STRIPE_MODE=live` is set (recommended for explicit mode selection). + - [ ] `STRIPE_PLAN_PRICE_MAPPING_LIVE` is set to JSON mapping live price IDs. + - [ ] (Optional fallback) `STRIPE_PLAN_PRICE_MAPPING` is set only if you intentionally use one shared mapping across environments. - [ ] `ADMIN_EMAILS` configured with correct admin emails (comma-separated). - [ ] `ADMIN_EMAIL_DOMAIN` configured if using domain-based admin access. - [ ] `DISABLE_AUTH` is **not** set to `"true"` in production. @@ -30,8 +33,10 @@ Tick each item as you complete it. - [ ] PRO monthly price created (if used). - [ ] Yearly prices created if you plan to sell yearly plans. - [ ] **Price mapping in backend updated** - - [ ] `STRIPE_PLAN_PRICE_MAPPING` uses **live** price IDs (not test IDs). - - [ ] Mapping covers all tiers and billing cycles you intend to offer. + - [ ] `STRIPE_PLAN_PRICE_MAPPING_LIVE` uses **live** price IDs (not test IDs). + - [ ] `STRIPE_PLAN_PRICE_MAPPING_TEST` is configured separately for test deployments. + - [ ] Mapping includes required keys: `basic.monthly` and `pro.monthly`. + - [ ] Mapping covers any additional tiers and billing cycles you intend to offer. - [ ] **SubscriptionPlan data is consistent** - [ ] DB has `SubscriptionPlan` rows for each tier (BASIC/PRO/etc.). - [ ] `is_active` is set to true for sellable plans. @@ -199,4 +204,3 @@ Perform these in **test** environment first, then in live with small amounts. - [ ] Ops team confirms they can use Disputes and Fraud Warnings tools comfortably. Once all items are checked, you can consider the Stripe integration ready for production traffic. -