feat(abonnements): paliers LLM Essentielle/Premium, admin modeles par forfait, statistiques revenus, emails marketing
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s
- Gamme officielle : Essentielle (Pro) = deepseek-v4-flash, glm-5.3-flash, minimax-m3 ; Premium (Business) = claude-sonnet-5, deepseek-v4-pro, glm-5.3 - Routage reel : le modele employe suit le palier IA du forfait (reglages admin > defaut du plan), garde anti-croisement de palier - Nouvelle page admin « Modeles & abonnements » (matrice forfaits/paliers, modele par defaut, catalogue OpenRouter) - Statistiques enrichies : revenus (30 j + total), MRR estime, credits, liste d'attente, paliers utilises - Nouvelle page admin « Marketing » : audiences avec compteurs, apercu, envoi test obligatoire, historique, desabonnement public + en-tetes List-Unsubscribe ; .gitignore pour les donnees d'execution - Interface (accueil + tarifs) alignee sur la gamme, 13 langues completes - Tests : routage par palier (fonction + tache), marketing, revenus en base
This commit is contained in:
157
tests/test_ai_tier_routing.py
Normal file
157
tests/test_ai_tier_routing.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Routage réel des modèles par palier IA (spec « Stratégie LLM par abonnement »).
|
||||
|
||||
- PLANS expose ai_models_essential / ai_models_premium (gamme officielle).
|
||||
- Un plan Pro (moteur openrouter) résout toujours un modèle Essentielle,
|
||||
jamais un modèle Premium.
|
||||
- Le défaut admin (ai_tiers.<palier>.default_model) prime sur le défaut du
|
||||
plan, sans redéploiement.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from models.subscription import (
|
||||
DEFAULT_AI_MODELS_ESSENTIAL,
|
||||
DEFAULT_AI_MODELS_PREMIUM,
|
||||
PLANS,
|
||||
PlanType,
|
||||
)
|
||||
from routes.translate_routes import (
|
||||
_allowed_providers_for_plan,
|
||||
_plan_ai_models,
|
||||
_resolve_tier_model,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(monkeypatch, tmp_path: Path):
|
||||
"""TestClient with JSON auth and rate limiting disabled."""
|
||||
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)
|
||||
|
||||
|
||||
def _settings(ai_tiers: dict | None = None):
|
||||
from routes.admin_routes import SettingsConfig
|
||||
|
||||
if ai_tiers is None:
|
||||
return SettingsConfig()
|
||||
return SettingsConfig.model_validate({"ai_tiers": ai_tiers})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gamme officielle dans PLANS
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_plans_expose_official_model_ranges():
|
||||
essential = PLANS[PlanType.PRO]["ai_models_essential"]
|
||||
assert essential == [
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"z-ai/glm-5.3-flash",
|
||||
"minimax/minimax-m3",
|
||||
]
|
||||
business = PLANS[PlanType.BUSINESS]
|
||||
assert business["ai_models_essential"] == essential
|
||||
assert business["ai_models_premium"] == [
|
||||
"anthropic/claude-sonnet-5",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"z-ai/glm-5.3",
|
||||
]
|
||||
|
||||
|
||||
def test_old_single_model_keys_removed():
|
||||
assert "ai_model_essential" not in PLANS[PlanType.PRO]
|
||||
assert "ai_model_essential" not in PLANS[PlanType.BUSINESS]
|
||||
assert "ai_model_premium" not in PLANS[PlanType.BUSINESS]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Résolution par palier
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_pro_resolves_essential_plan_default():
|
||||
assert _resolve_tier_model(_settings(), "pro", premium=False) == (
|
||||
"deepseek/deepseek-v4-flash"
|
||||
)
|
||||
|
||||
|
||||
def test_business_resolves_premium_plan_default():
|
||||
assert _resolve_tier_model(_settings(), "business", premium=True) == (
|
||||
"anthropic/claude-sonnet-5"
|
||||
)
|
||||
|
||||
|
||||
def test_admin_tier_default_overrides_plan_default():
|
||||
cfg = _settings(
|
||||
{"essential": {"models": [], "default_model": "z-ai/glm-5.3-flash"}}
|
||||
)
|
||||
assert _resolve_tier_model(cfg, "pro", premium=False) == "z-ai/glm-5.3-flash"
|
||||
|
||||
|
||||
def test_admin_tier_models_first_used_when_default_empty():
|
||||
cfg = _settings(
|
||||
{
|
||||
"essential": {
|
||||
"models": ["minimax/minimax-m3", "deepseek/deepseek-v4-flash"],
|
||||
"default_model": "",
|
||||
}
|
||||
}
|
||||
)
|
||||
assert _resolve_tier_model(cfg, "pro", premium=False) == "minimax/minimax-m3"
|
||||
|
||||
|
||||
def test_pro_never_resolves_premium_model():
|
||||
cfg = _settings(
|
||||
{
|
||||
"essential": {"models": [], "default_model": ""},
|
||||
"premium": {"models": [], "default_model": ""},
|
||||
}
|
||||
)
|
||||
for plan_str in ("pro", "PlanType.PRO"):
|
||||
model = _resolve_tier_model(cfg, plan_str, premium=False)
|
||||
assert model in DEFAULT_AI_MODELS_ESSENTIAL
|
||||
assert model not in DEFAULT_AI_MODELS_PREMIUM
|
||||
|
||||
|
||||
def test_pro_plan_does_not_allow_premium_provider():
|
||||
allowed = _allowed_providers_for_plan(PlanType.PRO)
|
||||
assert "openrouter" in allowed
|
||||
assert "openrouter_premium" not in allowed
|
||||
|
||||
|
||||
def test_plan_fallback_for_plans_without_range():
|
||||
# Free/Enterprise n'ont pas de liste : retombée sur la gamme officielle.
|
||||
assert _plan_ai_models(PlanType.FREE, premium=False) == DEFAULT_AI_MODELS_ESSENTIAL
|
||||
assert _plan_ai_models(PlanType.ENTERPRISE, premium=True) == DEFAULT_AI_MODELS_PREMIUM
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /plans expose les gammes (affichage public)
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_get_plans_exposes_model_lists(client):
|
||||
r = client.get("/api/v1/auth/plans")
|
||||
assert r.status_code == 200
|
||||
plans = {p["id"]: p for p in r.json()["data"]["plans"]}
|
||||
assert plans["pro"]["ai_models_essential"] == [
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"z-ai/glm-5.3-flash",
|
||||
"minimax/minimax-m3",
|
||||
]
|
||||
assert plans["business"]["ai_models_premium"][0] == "anthropic/claude-sonnet-5"
|
||||
assert "ai_model_essential" not in plans["pro"]
|
||||
Reference in New Issue
Block a user