Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 77 additions & 3 deletions backend/services/subscription/stripe_service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
import stripe
from typing import Optional, Dict, Any
Expand All @@ -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()
Expand Down
22 changes: 16 additions & 6 deletions docs/Billing_Subscription/stripe-dev-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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`).
Expand All @@ -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 }`.

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

---

Expand Down Expand Up @@ -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).

Expand Down
10 changes: 7 additions & 3 deletions docs/Billing_Subscription/stripe-go-live-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.