Move Stripe plan price mapping to env with startup validation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
import stripe
|
||||
from typing import Optional, Dict, Any
|
||||
@@ -8,11 +9,84 @@ from models.subscription_models import UserSubscription, SubscriptionPlan, Subsc
|
||||
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()
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user