""" Statistiques admin enrichies (spec « Stratégie LLM par abonnement ») : revenus encaissés, MRR estimé, paliers IA utilisés, compteur liste d'attente. En mode JSON (sans base de données), les revenus restent à zéro sans erreur. """ import json import pytest from pathlib import Path from fastapi.testclient import TestClient ADMIN_LOGIN_URL = "/api/v1/admin/login" STATS_URL = "/api/v1/admin/stats" def _write_json(path: Path, data) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") @pytest.fixture def stats_env(tmp_path: Path, monkeypatch): import routes.waitlist_routes as waitlist_mod waitlist_file = tmp_path / "waitlist.json" monkeypatch.setattr(waitlist_mod, "WAITLIST_FILE", waitlist_file) return {"waitlist_file": waitlist_file} @pytest.fixture def client(stats_env, monkeypatch, tmp_path: Path): import services.auth_service as auth_svc from middleware.rate_limiting import RateLimitManager monkeypatch.setattr(auth_svc, "USERS_FILE", tmp_path / "users.json") monkeypatch.setattr(auth_svc, "USE_DATABASE", False) monkeypatch.setattr(auth_svc, "DATABASE_AVAILABLE", False) async def _check_request_allow(self, request): return True, "ok", "test" async def _check_translation_allow(self, request, file_size_mb=0): return True, "ok" monkeypatch.setattr(RateLimitManager, "check_request", _check_request_allow) monkeypatch.setattr(RateLimitManager, "check_translation", _check_translation_allow) from main import app return TestClient(app, raise_server_exceptions=True) @pytest.fixture def auth_headers(client, monkeypatch): import routes.admin_routes as admin_routes_mod monkeypatch.setattr(admin_routes_mod, "ADMIN_USERNAME", "admin") monkeypatch.setattr(admin_routes_mod, "ADMIN_PASSWORD", "admin-secret") monkeypatch.setattr(admin_routes_mod, "ADMIN_PASSWORD_HASH", None) r = client.post(ADMIN_LOGIN_URL, json={"password": "admin-secret"}) assert r.status_code == 200, r.text return {"Authorization": f"Bearer {r.json()['access_token']}"} def test_stats_include_revenue_mrr_waitlist_and_tiers(client, auth_headers, stats_env): _write_json(stats_env["waitlist_file"], [{"email": "a@example.com"}]) r = client.get(STATS_URL, headers=auth_headers) assert r.status_code == 200, r.text body = r.json() # Nouveaux blocs présents for key in ("revenue", "mrr_estimated", "ai_tier_usage", "waitlist_count"): assert key in body, f"clé manquante dans /admin/stats : {key}" # Sans base de données : revenus à zéro, sans erreur assert body["revenue"]["collected_total"] == 0.0 assert body["revenue"]["collected_30d"] == 0.0 assert body["revenue"]["credits_purchased"] == 0.0 # Compteur liste d'attente branché sur data/waitlist.json assert body["waitlist_count"] == 1 # Paliers IA : structure complète assert set(body["ai_tier_usage"].keys()) == { "essential", "premium", "classic", "other", } # Anciennes clés toujours présentes (non-régression) assert "users" in body and "translations" in body and "cache" in body def test_stats_mrr_estimated_from_plan_distribution(client, auth_headers, monkeypatch): """Le MRR estimé s'appuie sur la répartition des plans (prix mensuel effectif).""" import services.auth_service as auth_svc users_file = auth_svc.USERS_FILE users_file.parent.mkdir(parents=True, exist_ok=True) _write_json( users_file, { "u1": { "email": "p1@example.com", "name": "P1", "plan": "pro", "docs_translated_this_month": 0, }, "u2": { "email": "p2@example.com", "name": "P2", "plan": "pro", "docs_translated_this_month": 0, }, }, ) r = client.get(STATS_URL, headers=auth_headers) assert r.status_code == 200, r.text body = r.json() from services import pricing_config as pricing_cfg from models.subscription import PlanType monthly_pro, _ = pricing_cfg.get_effective_monthly_yearly(PlanType.PRO.value) assert body["users"]["by_plan"].get("pro") == 2 assert body["mrr_estimated"]["by_plan"].get("pro") == pytest.approx( round(monthly_pro * 2, 2) ) assert body["mrr_estimated"]["total"] == pytest.approx( round(monthly_pro * 2, 2) ) # --------------------------------------------------------------------------- # Revenus et paliers IA en mode base de données (SQLite de test, cf. conftest) # --------------------------------------------------------------------------- @pytest.fixture def db_client(monkeypatch, tmp_path: Path): """TestClient avec authentification admin simple et base de test active.""" import services.auth_service as auth_svc from middleware.rate_limiting import RateLimitManager monkeypatch.setattr(auth_svc, "USERS_FILE", tmp_path / "users.json") monkeypatch.setattr(auth_svc, "USE_DATABASE", True) monkeypatch.setattr(auth_svc, "DATABASE_AVAILABLE", True) async def _check_request_allow(self, request): return True, "ok", "test" async def _check_translation_allow(self, request, file_size_mb=0): return True, "ok" monkeypatch.setattr(RateLimitManager, "check_request", _check_request_allow) monkeypatch.setattr(RateLimitManager, "check_translation", _check_translation_allow) import routes.admin_routes as admin_routes_mod monkeypatch.setattr(admin_routes_mod, "ADMIN_USERNAME", "admin") monkeypatch.setattr(admin_routes_mod, "ADMIN_PASSWORD", "admin-secret") monkeypatch.setattr(admin_routes_mod, "ADMIN_PASSWORD_HASH", None) from main import app return TestClient(app, raise_server_exceptions=True) def test_stats_revenue_and_tiers_from_database(db_client): """Paiements réussis + traductions en base → revenus et paliers non nuls.""" from database.connection import get_sync_session from database.models import User as DBUser, PaymentHistory, Translation with get_sync_session() as session: user = DBUser( email="payer@example.com", name="Payer", password_hash="not-a-real-hash", ) session.add(user) session.flush() session.add_all( [ PaymentHistory( user_id=user.id, amount_cents=1900, currency="eur", payment_type="subscription", status="succeeded", ), PaymentHistory( user_id=user.id, amount_cents=900, currency="eur", payment_type="credits", status="succeeded", ), PaymentHistory( user_id=user.id, amount_cents=5000, currency="eur", payment_type="subscription", status="failed", # jamais comptabilisé ), Translation( user_id=user.id, original_filename="a.xlsx", file_type="xlsx", target_language="fr", provider="openrouter", status="completed", ), Translation( user_id=user.id, original_filename="b.xlsx", file_type="xlsx", target_language="fr", provider="openrouter_premium", status="completed", ), ] ) session.commit() r = db_client.get(STATS_URL, headers=_admin_headers(db_client)) assert r.status_code == 200, r.text body = r.json() assert body["revenue"]["currency"] == "EUR" assert body["revenue"]["collected_total"] >= 28.0 # 19 € + 9 € (l'échec ignoré) assert body["revenue"]["collected_30d"] >= 28.0 assert body["revenue"]["credits_purchased"] >= 9.0 assert body["revenue"]["payments_30d"] >= 2 assert body["ai_tier_usage"]["essential"] >= 1 assert body["ai_tier_usage"]["premium"] >= 1 def _admin_headers(client) -> dict: r = client.post(ADMIN_LOGIN_URL, json={"password": "admin-secret"}) assert r.status_code == 200, r.text return {"Authorization": f"Bearer {r.json()['access_token']}"}