Merge branch 'pr-374'

This commit is contained in:
ajaysi
2026-03-05 10:34:36 +05:30
4 changed files with 120 additions and 12 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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).

View File

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