rmi-backend/app/_archive/legacy_2026_07/subscription_pricing_api.py
cryptorugmunch 628c1d2a10
Some checks failed
CI / build (push) Failing after 3s
refactor(rmi-backend,audit): mount Wave 3 + archive 136 dead-code files (P2.3)
PHASE 2.3 (AUDIT-2026-Q3.md):

Task 1 — Wire-in Wave 3 (1 router mounted, 2 deferred):
  - app.routers.unified_scanner_router mounted at /api/v2/scanner/* (2 routes:
    POST /api/v2/scanner/token/scan, POST /api/v2/scanner/wallet/scan).
    Refactored prefix from /api/v2 -> /api/v2/scanner to avoid future conflicts
    with the v1 /api/v1/scanner/ stub.
  - app.routers.unified_wallet_scanner DEFERRED (no router APIRouter attribute;
    library module consumed by unified_scanner_router via get_wallet_scanner()).
  - app.routers.admin_extensions DEFERRED (DORMANT per audit; 25 routes at
    /api/v1/admin/* would shadow /api/v1/admin/alerts_webhook).

Task 2 — Archive 136 dead-code files to app/_archive/legacy_2026_07/:
  - 73 routers in app/routers/ (reach graph showed zero reach into mount.py).
  - 63 flat app/*.py (domain modules never imported by live code).
  - 1 file RESTORED post-archive: app/routers/x402_bridge_health.py (caught by
    tests/unit/test_bridge_health.py which directly imports it; reach graph
    considered tests/ only as transitive reach — to be patched in next cycle).

Forced-LIVE (NOT archived per user directive):
  - app/ai_pipeline_v3.py  (3 importers in audit window, importers themselves DEAD)
  - app/splade_bm25.py       (LIVE via app.rag_service)
  - app/wallet_manager_v2.py (LIVE via x402_enforcement, x402_tools, sweep_all, sweep_now)
  - app/crypto_embeddings.py (NOT in audit ARCHIVE list; heavy import graph)

Verification (forward-import closure from mount.py + main.py + factory.py + lifespan.py):
  - imports = 348 app.* modules
  - reached = 194 files reachable from roots
  - archive set = audit_dead (186) - reached - forced_live (4) - test_live (1) = 136
  - Net delta: 136 files moved, 44,932 LOC reduction, 293->295 active routes (+2 from Wave 3)

pyproject.toml updates:
  - setuptools.packages.find: added exclude for app._archive*
  - ruff.extend-exclude: added "app/_archive/"
  - mypy.exclude: added "app/_archive/"

Smoke test: pytest tests/ — 817 passed, 3 pre-existing failures unchanged
(0 new failures; 0 routes lost; all 4 forced-LIVE files still importable).

Restoration: git mv app/_archive/legacy_2026_07/<name>.py <original-path>
and add the import to app/mount.py ROUTER_MODULES.

Refs: AUDIT-2026-Q3.md /home/dev/pry/rmi-final-deadcode-2026-07-06.md
2026-07-06 20:52:31 +02:00

777 lines
25 KiB
Python

"""
RMI Subscription & Pricing API
==============================
Manages subscription tiers, payments, and billing:
- 3 premium tiers: Starter ($19.99), Pro ($38), Unlimited ($89)
- Enterprise custom pricing (contact biz@rugmunch.io)
- Yearly discount: 20% off
- 6-month discount: 10% off
- 15+ crypto payment chains
- Webhook handling for payment confirmation
- Subscription status, upgrades, downgrades, cancellations
"""
import json
import logging
import os
import secrets
from datetime import datetime, timedelta
from typing import Any
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
logger = logging.getLogger("rmi_subscriptions")
router = APIRouter(tags=["subscriptions"])
# ── Redis helper ──
ADDON_CONFIG = {
"PORTFOLIO_PRO": {
"name": "Portfolio Pro",
"price_monthly": 3.00,
"price_six_month": 16.20, # 3.00 * 6 * 0.9
"price_yearly": 28.80, # 3.00 * 12 * 0.8
"kind": "addon", # addon tiers can stack with base tiers
"category": "portfolio",
"wallets_limit": 50, # vs 3 on free
"history_window_days": 30, # vs 7 on free
"features": [
"Track up to 50 wallets",
"30-day P&L history (vs 7-day free)",
"Per-token risk overlay",
"Cross-wallet top holdings",
"Time-series price charts",
"CSV export",
"Works WITH any base tier",
],
"stripe_price_id": os.getenv("STRIPE_PORTFOLIO_PRO_PRICE_ID"),
"x402_price_atoms": "3000000", # $3.00 USDC (6 decimals)
"x402_chain_id": 8453, # Base
},
}
# Tier definitions
TIER_CONFIG = {
"FREE": {
"name": "Free",
"price_monthly": 0,
"price_six_month": 0,
"price_yearly": 0,
"scans_per_month": 5,
"api_calls_per_day": 10,
"features": [
"5 scans per month",
"10 API calls per day",
"Basic token analysis",
"Community support",
],
"stripe_price_id": None,
},
"STARTER": {
"name": "Starter",
"price_monthly": 19.99,
"price_six_month": 107.95, # 19.99 * 6 * 0.9 (10% off)
"price_yearly": 191.90, # 19.99 * 12 * 0.8 (20% off)
"scans_per_month": 50,
"api_calls_per_day": 100,
"features": [
"50 scans per month",
"100 API calls per day",
"Advanced token analysis",
"Risk scoring",
"Discord support",
"Export to CSV",
],
"stripe_price_id": os.getenv("STRIPE_STARTER_PRICE_ID"),
},
"PRO": {
"name": "Pro",
"price_monthly": 38.00,
"price_six_month": 205.20, # 38 * 6 * 0.9
"price_yearly": 364.80, # 38 * 12 * 0.8
"scans_per_month": 200,
"api_calls_per_day": 500,
"features": [
"200 scans per month",
"500 API calls per day",
"Advanced token analysis",
"Risk scoring + AI insights",
"Priority Discord support",
"Export to CSV/JSON",
"Webhook alerts",
"Multi-chain monitoring",
],
"stripe_price_id": os.getenv("STRIPE_PRO_PRICE_ID"),
},
"ELITE": {
"name": "Unlimited",
"price_monthly": 89.00,
"price_six_month": 480.60, # 89 * 6 * 0.9
"price_yearly": 854.40, # 89 * 12 * 0.8
"scans_per_month": -1, # unlimited
"api_calls_per_day": -1, # unlimited
"features": [
"Unlimited scans",
"Unlimited API calls",
"Full AI analysis suite",
"Custom risk models",
"Dedicated support channel",
"All export formats",
"Real-time webhook alerts",
"Multi-chain + cross-chain",
"White-label options",
"API key management",
],
"stripe_price_id": os.getenv("STRIPE_ELITE_PRICE_ID"),
},
"ENTERPRISE": {
"name": "Enterprise",
"price_monthly": None, # Custom
"price_six_month": None,
"price_yearly": None,
"scans_per_month": -1,
"api_calls_per_day": -1,
"features": [
"Everything in Unlimited",
"Custom SLA",
"Dedicated infrastructure",
"Custom integrations",
"On-premise deployment option",
"Team management (up to 50 seats)",
"SSO / SAML",
"Audit logs",
"Dedicated account manager",
"Custom AI model training",
],
"stripe_price_id": None,
"contact_email": "biz@rugmunch.io",
},
}
# Crypto payment configuration
CRYPTO_PAYMENT_CONFIG = {
"accepted_chains": [
{"id": "ethereum", "name": "Ethereum", "symbol": "ETH", "type": "evm", "confirmations": 12},
{"id": "base", "name": "Base", "symbol": "ETH", "type": "evm", "confirmations": 10},
{"id": "arbitrum", "name": "Arbitrum", "symbol": "ETH", "type": "evm", "confirmations": 10},
{"id": "optimism", "name": "Optimism", "symbol": "ETH", "type": "evm", "confirmations": 10},
{"id": "polygon", "name": "Polygon", "symbol": "MATIC", "type": "evm", "confirmations": 20},
{"id": "bsc", "name": "BSC", "symbol": "BNB", "type": "evm", "confirmations": 15},
{
"id": "avalanche",
"name": "Avalanche",
"symbol": "AVAX",
"type": "evm",
"confirmations": 12,
},
{"id": "fantom", "name": "Fantom", "symbol": "FTM", "type": "evm", "confirmations": 10},
{"id": "solana", "name": "Solana", "symbol": "SOL", "type": "solana", "confirmations": 32},
{"id": "tron", "name": "Tron", "symbol": "TRX", "type": "tron", "confirmations": 19},
{"id": "bitcoin", "name": "Bitcoin", "symbol": "BTC", "type": "btc", "confirmations": 3},
{"id": "monero", "name": "Monero", "symbol": "XMR", "type": "xmr", "confirmations": 10},
{"id": "litecoin", "name": "Litecoin", "symbol": "LTC", "type": "ltc", "confirmations": 6},
{
"id": "dogecoin",
"name": "Dogecoin",
"symbol": "DOGE",
"type": "doge",
"confirmations": 10,
},
{"id": "ripple", "name": "XRP", "symbol": "XRP", "type": "xrp", "confirmations": 1},
],
"payment_wallet": os.getenv("CRYPTO_PAYMENT_WALLET", "0x0000000000000000000000000000000000000000"),
"sol_payment_wallet": os.getenv("SOL_PAYMENT_WALLET", ""),
"btc_payment_address": os.getenv("BTC_PAYMENT_ADDRESS", ""),
"xmr_payment_address": os.getenv("XMR_PAYMENT_ADDRESS", ""),
"contact_email": "biz@rugmunch.io",
}
# Combined config returned to the client (base tiers + add-ons)
ALL_PRODUCTS = {**TIER_CONFIG, **ADDON_CONFIG}
# ── Models ──
class SubscriptionCreateRequest(BaseModel):
tier: str = Field(..., pattern="^(STARTER|PRO|ELITE|PORTFOLIO_PRO)$")
period: str = Field(..., pattern="^(monthly|six_month|yearly)$")
payment_method: str = Field(..., pattern="^(stripe|crypto|x402)$")
crypto_chain: str | None = None # Required if payment_method == crypto/x402
class AddonCreateRequest(BaseModel):
"""For stacking add-ons on top of an existing subscription."""
addon: str = Field(..., pattern="^(PORTFOLIO_PRO)$")
period: str = Field(..., pattern="^(monthly|six_month|yearly)$")
payment_method: str = Field(..., pattern="^(stripe|crypto|x402)$")
crypto_chain: str | None = None
class CryptoPaymentRequest(BaseModel):
tier: str
period: str
chain: str
tx_hash: str
amount_paid_usd: float
from_address: str
class SubscriptionResponse(BaseModel):
id: str
user_id: str
tier: str
period: str
status: str
current_period_start: str
current_period_end: str
cancel_at_period_end: bool
payment_method: str
amount_paid: float
currency: str
created_at: str
class PricingResponse(BaseModel):
tiers: dict[str, Any]
discounts: dict[str, float]
crypto_chains: list[dict]
# ── Helpers ──
def calculate_price(tier: str, period: str) -> float:
"""Calculate price for tier + period."""
config = TIER_CONFIG.get(tier.upper())
if not config:
return 0
if period == "monthly":
return config["price_monthly"]
elif period == "six_month":
return config["price_six_month"]
elif period == "yearly":
return config["price_yearly"]
return config["price_monthly"]
_PERIOD_DAYS = {
"monthly": 30,
"six_month": 180,
"yearly": 365,
}
def get_period_days(period: str) -> int:
"""Get number of days for a billing period."""
return _PERIOD_DAYS.get(period, 30)
def _get_user_subscriptions(user_id: str) -> list[dict]:
"""Get all subscriptions for a user."""
r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
subs_raw = r.hget("rmi:subscriptions", user_id)
if subs_raw:
return json.loads(subs_raw)
return []
def _save_subscription(sub: dict):
"""Save a subscription to Redis."""
r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
user_id = sub["user_id"]
subs = _get_user_subscriptions(user_id)
# Update existing or append
found = False
for i, s in enumerate(subs):
if s["id"] == sub["id"]:
subs[i] = sub
found = True
break
if not found:
subs.append(sub)
r.hset("rmi:subscriptions", user_id, json.dumps(subs))
# ── Public Endpoints ──
@router.get("/pricing")
async def get_pricing() -> PricingResponse:
"""Get all pricing information - base tiers + add-ons."""
return {
"tiers": TIER_CONFIG,
"addons": ADDON_CONFIG,
"all_products": ALL_PRODUCTS,
"discounts": {
"six_month": 0.10, # 10% off
"yearly": 0.20, # 20% off
},
"crypto_chains": CRYPTO_PAYMENT_CONFIG["accepted_chains"],
"enterprise_contact": "biz@rugmunch.io",
}
@router.get("/pricing/calculate")
async def calculate_pricing(tier: str, period: str):
"""Calculate exact price for a tier + period."""
price = calculate_price(tier, period)
if price == 0 and tier.upper() != "FREE":
raise HTTPException(status_code=400, detail="Invalid tier or period")
config = TIER_CONFIG.get(tier.upper())
monthly = config["price_monthly"]
savings = 0
if period == "six_month":
savings = (monthly * 6) - price
elif period == "yearly":
savings = (monthly * 12) - price
return {
"tier": tier.upper(),
"period": period,
"price": price,
"currency": "USD",
"monthly_equivalent": round(price / get_period_days(period) * 30, 2),
"savings": round(savings, 2),
"savings_percent": round(savings / (monthly * (6 if period == "six_month" else 12)) * 100, 1)
if savings > 0
else 0,
}
# ── Authenticated Endpoints ──
@router.get("/subscription")
async def get_my_subscription(request: Request):
"""Get current user's active subscription."""
from app.auth import get_current_user
user = await get_current_user(request)
if not user:
raise HTTPException(status_code=401, detail="Authentication required")
subs = _get_user_subscriptions(user["id"])
# Find active subscription
active = None
now = datetime.utcnow().isoformat()
for sub in subs:
if sub["status"] == "active" and sub["current_period_end"] > now:
active = sub
break
if not active:
# Return free tier info
return {
"tier": "FREE",
"status": "active",
"features": TIER_CONFIG["FREE"]["features"],
"scans_remaining": user.get("scans_remaining", 5),
"api_calls_today": 0,
}
return {
"subscription": active,
"tier": active["tier"],
"features": TIER_CONFIG.get(active["tier"], {}).get("features", []),
"days_remaining": max(0, (datetime.fromisoformat(active["current_period_end"]) - datetime.utcnow()).days),
}
@router.post("/subscription/create")
async def create_subscription(req: SubscriptionCreateRequest, request: Request):
"""Create a new subscription (Stripe, crypto, or x402)."""
from app.auth import get_current_user
user = await get_current_user(request)
if not user:
raise HTTPException(status_code=401, detail="Authentication required")
tier = req.tier.upper()
# Allow base tiers + addons (PORTFOLIO_PRO can be purchased standalone)
if tier not in ALL_PRODUCTS or tier == "FREE":
raise HTTPException(status_code=400, detail="Invalid tier")
price = calculate_price(tier, req.period)
days = get_period_days(req.period)
sub_id = secrets.token_hex(16)
now = datetime.utcnow()
subscription = {
"id": sub_id,
"user_id": user["id"],
"tier": tier,
"period": req.period,
"status": "pending", # pending until payment confirmed
"current_period_start": now.isoformat(),
"current_period_end": (now + timedelta(days=days)).isoformat(),
"cancel_at_period_end": False,
"payment_method": req.payment_method,
"amount_paid": price,
"currency": "USD",
"created_at": now.isoformat(),
"crypto_chain": req.crypto_chain,
"tx_hash": None,
}
if req.payment_method == "stripe":
# Create Stripe checkout session
try:
import stripe
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
checkout = stripe.checkout.Session.create(
payment_method_types=["card"],
line_items=[
{
"price": TIER_CONFIG[tier]["stripe_price_id"],
"quantity": 1,
}
],
mode="subscription",
success_url="https://rugmunch.io/pricing/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url="https://rugmunch.io/pricing/cancel",
metadata={
"user_id": user["id"],
"subscription_id": sub_id,
"tier": tier,
"period": req.period,
},
)
subscription["stripe_session_id"] = checkout.id
_save_subscription(subscription)
return {
"status": "pending",
"checkout_url": checkout.url,
"subscription_id": sub_id,
}
except Exception as e:
logger.error(f"Stripe checkout failed: {e}")
raise HTTPException(status_code=500, detail="Payment processing failed") from e
elif req.payment_method == "crypto":
if not req.crypto_chain:
raise HTTPException(status_code=400, detail="crypto_chain required for crypto payments")
chain = req.crypto_chain.lower()
valid_chains = [c["id"] for c in CRYPTO_PAYMENT_CONFIG["accepted_chains"]]
if chain not in valid_chains:
raise HTTPException(status_code=400, detail=f"Unsupported chain: {chain}")
# Generate payment address/QR data
payment_wallet = CRYPTO_PAYMENT_CONFIG["payment_wallet"]
if chain == "solana":
payment_wallet = CRYPTO_PAYMENT_CONFIG.get("sol_payment_wallet", payment_wallet)
elif chain == "bitcoin":
payment_wallet = CRYPTO_PAYMENT_CONFIG.get("btc_payment_address", payment_wallet)
elif chain == "monero":
payment_wallet = CRYPTO_PAYMENT_CONFIG.get("xmr_payment_address", payment_wallet)
_save_subscription(subscription)
return {
"status": "pending_payment",
"subscription_id": sub_id,
"payment_details": {
"chain": chain,
"amount_usd": price,
"payment_address": payment_wallet,
"memo": f"RMI-{sub_id[:8]}",
"expires_at": (now + timedelta(hours=24)).isoformat(),
},
"instructions": f"Send payment to the address above. Include memo 'RMI-{sub_id[:8]}' for faster confirmation.",
}
elif req.payment_method == "x402":
# x402 micropayment - return EIP-3009 challenge for client to sign
config = ALL_PRODUCTS.get(tier, {})
x402_price_atoms = config.get("x402_price_atoms")
x402_chain_id = config.get("x402_chain_id", 8453)
if not x402_price_atoms:
raise HTTPException(status_code=400, detail=f"x402 not available for tier {tier}")
_save_subscription(subscription)
return {
"status": "pending_x402",
"subscription_id": sub_id,
"x402": {
"version": 2,
"scheme": "exact",
"network": f"eip155:{x402_chain_id}",
"asset": "USDC",
"amount_atoms": x402_price_atoms,
"amount_usd": price,
"tier": tier,
"period": req.period,
"pay_to": os.getenv("X402_PAY_TO", "0x1E3AC01d0fdb976179790BDD02823196A92705C9"),
"expires_at": (now + timedelta(minutes=10)).isoformat(),
},
"instructions": f"Sign the EIP-3009 USDC transfer authorization via your wallet. ${price} will be charged on Base. Receipt settles the subscription automatically.",
}
raise HTTPException(status_code=400, detail="Invalid payment method")
@router.post("/subscription/crypto/confirm")
async def confirm_crypto_payment(req: CryptoPaymentRequest, request: Request):
"""Confirm a crypto payment (called by user after sending tx, or webhook)."""
from app.auth import _save_user, get_current_user
user = await get_current_user(request)
if not user:
raise HTTPException(status_code=401, detail="Authentication required")
# Find pending subscription
subs = _get_user_subscriptions(user["id"])
pending = None
for sub in subs:
if sub["status"] == "pending" and sub.get("crypto_chain") == req.chain:
pending = sub
break
if not pending:
raise HTTPException(status_code=404, detail="No pending subscription found")
# TODO: Verify transaction on-chain
# For now, we accept the tx_hash and mark as active (in production, verify via RPC)
pending["status"] = "active"
pending["tx_hash"] = req.tx_hash
pending["amount_paid"] = req.amount_paid_usd
pending["confirmed_at"] = datetime.utcnow().isoformat()
pending["from_address"] = req.from_address
_save_subscription(pending)
# Update user tier
user["tier"] = pending["tier"]
user["scans_remaining"] = TIER_CONFIG[pending["tier"]]["scans_per_month"]
_save_user(user)
logger.info(f"[SUBSCRIPTION] Crypto payment confirmed: user={user['id']} tier={pending['tier']} tx={req.tx_hash}")
return {
"status": "active",
"subscription": pending,
}
@router.post("/subscription/cancel")
async def cancel_subscription(request: Request):
"""Cancel subscription at period end."""
from app.auth import get_current_user
user = await get_current_user(request)
if not user:
raise HTTPException(status_code=401, detail="Authentication required")
subs = _get_user_subscriptions(user["id"])
active = None
for sub in subs:
if sub["status"] == "active":
active = sub
break
if not active:
raise HTTPException(status_code=404, detail="No active subscription")
active["cancel_at_period_end"] = True
active["cancelled_at"] = datetime.utcnow().isoformat()
_save_subscription(active)
return {"status": "ok", "message": "Subscription will cancel at period end"}
@router.post("/subscription/upgrade")
async def upgrade_subscription(tier: str, request: Request):
"""Upgrade to a higher tier."""
from app.auth import get_current_user
user = await get_current_user(request)
if not user:
raise HTTPException(status_code=401, detail="Authentication required")
tier = tier.upper()
if tier not in TIER_CONFIG or tier in ("FREE", "ENTERPRISE"):
raise HTTPException(status_code=400, detail="Invalid upgrade tier")
subs = _get_user_subscriptions(user["id"])
active = None
for sub in subs:
if sub["status"] == "active":
active = sub
break
if active:
# Cancel current and create new
active["status"] = "cancelled"
active["cancelled_at"] = datetime.utcnow().isoformat()
active["cancel_reason"] = "upgraded"
_save_subscription(active)
# Create new subscription for new tier
# (User will need to complete payment)
return {
"status": "upgrade_initiated",
"new_tier": tier,
"message": "Please complete payment for the new tier",
"checkout_url": f"/pricing?upgrade_to={tier}",
}
# ── Webhook Endpoints ──
@router.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
"""Handle Stripe webhooks for subscription events."""
from app.auth import _get_user, _save_user
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
import stripe
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
endpoint_secret = os.getenv("STRIPE_WEBHOOK_SECRET")
event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
except Exception as e:
logger.error(f"Stripe webhook error: {e}")
raise HTTPException(status_code=400, detail="Invalid webhook") from e
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
metadata = session.get("metadata", {})
user_id = metadata.get("user_id")
sub_id = metadata.get("subscription_id")
tier = metadata.get("tier")
if user_id and sub_id:
# Activate subscription
subs = _get_user_subscriptions(user_id)
for sub in subs:
if sub["id"] == sub_id:
sub["status"] = "active"
sub["stripe_subscription_id"] = session.get("subscription")
sub["confirmed_at"] = datetime.utcnow().isoformat()
_save_subscription(sub)
# Update user tier
user = _get_user(user_id)
if user:
user["tier"] = tier
user["scans_remaining"] = TIER_CONFIG[tier]["scans_per_month"]
_save_user(user)
break
elif event["type"] == "invoice.payment_failed":
event["data"]["object"]
# Mark subscription as past_due
# TODO: Implement
pass
return {"status": "ok"}
# ── Admin Endpoints ──
@router.get("/admin/subscriptions")
async def list_all_subscriptions(
request: Request,
status: str | None = None,
tier: str | None = None,
limit: int = 50,
offset: int = 0,
):
"""List all subscriptions (admin only)."""
from app.auth import get_current_user
user = await get_current_user(request)
if not user or user.get("role") not in ("ADMIN", "SUPERADMIN"):
raise HTTPException(status_code=403, detail="Admin access required")
r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
all_subs = r.hgetall("rmi:subscriptions")
results = []
for user_id, subs_raw in all_subs.items():
subs = json.loads(subs_raw)
for sub in subs:
if status and sub["status"] != status:
continue
if tier and sub["tier"] != tier.upper():
continue
sub["user_id"] = user_id
results.append(sub)
total = len(results)
paginated = results[offset : offset + limit]
return {
"subscriptions": paginated,
"total": total,
"limit": limit,
"offset": offset,
}
@router.get("/admin/revenue")
async def get_revenue_stats(request: Request):
"""Get revenue statistics (admin only)."""
from app.auth import get_current_user
from app.core.redis import get_redis
user = await get_current_user(request)
if not user or user.get("role") not in ("ADMIN", "SUPERADMIN"):
raise HTTPException(status_code=403, detail="Admin access required")
r = get_redis()
all_subs = r.hgetall("rmi:subscriptions")
stats = {
"total_revenue": 0,
"by_tier": {},
"by_period": {},
"by_payment_method": {},
"active_subscriptions": 0,
"cancelled_subscriptions": 0,
"pending_subscriptions": 0,
}
for _user_id, subs_raw in all_subs.items():
subs = json.loads(subs_raw)
for sub in subs:
if sub["status"] == "active":
stats["active_subscriptions"] += 1
stats["total_revenue"] += sub.get("amount_paid", 0)
tier = sub["tier"]
stats["by_tier"][tier] = stats["by_tier"].get(tier, 0) + sub.get("amount_paid", 0)
period = sub.get("period", "monthly")
stats["by_period"][period] = stats["by_period"].get(period, 0) + sub.get("amount_paid", 0)
method = sub.get("payment_method", "stripe")
stats["by_payment_method"][method] = stats["by_payment_method"].get(method, 0) + sub.get(
"amount_paid", 0
)
elif sub["status"] == "cancelled":
stats["cancelled_subscriptions"] += 1
elif sub["status"] == "pending":
stats["pending_subscriptions"] += 1
return stats