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:
152
tests/test_worker_tier_routing.py
Normal file
152
tests/test_worker_tier_routing.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Intégration : ``_run_translation_job`` route bien le modèle par palier.
|
||||
|
||||
Un faux fournisseur OpenAI-compatible capture le paramètre ``model`` reçu du
|
||||
worker ; ``load_settings`` (réglages admin) est substitué. On vérifie que :
|
||||
- provider="openrouter" → modèle Essentielle (réglages > plan) ;
|
||||
- provider="openrouter_premium" → modèle Premium ;
|
||||
- un ``ai_tiers.essential`` personnalisé est honoré sans redéploiement ;
|
||||
- un ``ai_tiers.essential`` contenant un modèle Premium est corrigé à l'exécution.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
import routes.translate_routes as tr
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def minimal_xlsx(tmp_path):
|
||||
try:
|
||||
import openpyxl
|
||||
|
||||
wb = openpyxl.Workbook()
|
||||
wb.active["A1"] = "Hello"
|
||||
p = tmp_path / "minimal.xlsx"
|
||||
wb.save(p)
|
||||
return p
|
||||
except ImportError:
|
||||
pytest.skip("openpyxl required")
|
||||
|
||||
|
||||
def _settings(ai_tiers: dict, api_key: str = "sk-or-test"):
|
||||
from routes.admin_routes import SettingsConfig
|
||||
|
||||
return SettingsConfig.model_validate(
|
||||
{
|
||||
"openrouter": {"api_key": api_key},
|
||||
"openrouter_premium": {"api_key": api_key},
|
||||
"ai_tiers": ai_tiers,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _register_job(job_id: str) -> None:
|
||||
tr._translation_jobs[job_id] = {
|
||||
"id": job_id,
|
||||
"status": "processing",
|
||||
"progress_percent": 0,
|
||||
"current_step": "",
|
||||
"provider": "openrouter",
|
||||
"created_at": "2026-09-05T00:00:00+00:00",
|
||||
}
|
||||
|
||||
|
||||
def _run_job(monkeypatch, tmp_path, minimal_xlsx, admin_settings, provider: str,
|
||||
user_plan: str) -> list:
|
||||
"""Lance _run_translation_job avec des faux traducteurs/fournisseurs et
|
||||
renvoie la liste des modèles capturés par le faux fournisseur."""
|
||||
import routes.admin_routes as admin_routes_mod
|
||||
import services.providers.openai_provider as openai_provider_mod
|
||||
|
||||
captured: list = []
|
||||
|
||||
class FakeProvider:
|
||||
def __init__(self, api_key=None, model=None, base_url=None, timeout=60, **kwargs):
|
||||
self.model = model
|
||||
self._model = model
|
||||
captured.append(model)
|
||||
|
||||
class FakeExcelTranslator:
|
||||
def __init__(self, provider=None):
|
||||
self.provider = provider
|
||||
|
||||
def translate_file(self, input_path, output_path, target_lang,
|
||||
source_lang="auto", progress_callback=None,
|
||||
translate_images=False, **kwargs):
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_bytes(minimal_xlsx.read_bytes())
|
||||
|
||||
def get_translation_stats(self):
|
||||
return {"attempted": 1, "changed": 1}
|
||||
|
||||
monkeypatch.setattr(openai_provider_mod, "OpenAITranslationProvider", FakeProvider)
|
||||
monkeypatch.setattr(tr, "ExcelTranslator", FakeExcelTranslator)
|
||||
monkeypatch.setattr(
|
||||
admin_routes_mod, "load_settings", lambda: admin_settings, raising=True
|
||||
)
|
||||
|
||||
_register_job("job-test")
|
||||
try:
|
||||
asyncio.run(
|
||||
tr._run_translation_job(
|
||||
job_id="job-test",
|
||||
input_path=Path(minimal_xlsx),
|
||||
file_extension=".xlsx",
|
||||
target_lang="fr",
|
||||
source_lang="en",
|
||||
provider=provider,
|
||||
user_id=None,
|
||||
custom_prompt=None,
|
||||
glossary_id=None,
|
||||
prompt_id=None,
|
||||
webhook_url=None,
|
||||
user_plan=user_plan,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
tr._translation_jobs.pop("job-test", None)
|
||||
return captured
|
||||
|
||||
|
||||
def test_openrouter_pro_uses_essential_tier_model(monkeypatch, tmp_path, minimal_xlsx):
|
||||
settings = _settings({"essential": {"models": [], "default_model": ""},
|
||||
"premium": {"models": [], "default_model": ""}})
|
||||
captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
|
||||
provider="openrouter", user_plan="PlanType.PRO")
|
||||
assert captured, "le faux fournisseur n'a pas été instancié"
|
||||
assert captured[0] == "deepseek/deepseek-v4-flash"
|
||||
assert captured[0] not in (
|
||||
"anthropic/claude-sonnet-5",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"z-ai/glm-5.3",
|
||||
)
|
||||
|
||||
|
||||
def test_openrouter_premium_uses_premium_tier_model(monkeypatch, tmp_path, minimal_xlsx):
|
||||
settings = _settings({"essential": {"models": [], "default_model": ""},
|
||||
"premium": {"models": [], "default_model": ""}})
|
||||
captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
|
||||
provider="openrouter_premium", user_plan="PlanType.BUSINESS")
|
||||
assert captured, "le faux fournisseur n'a pas été instancié"
|
||||
assert captured[0] == "anthropic/claude-sonnet-5"
|
||||
|
||||
|
||||
def test_admin_tier_default_applied_without_redeployment(monkeypatch, tmp_path, minimal_xlsx):
|
||||
settings = _settings({"essential": {"models": [], "default_model": "z-ai/glm-5.3-flash"},
|
||||
"premium": {"models": [], "default_model": ""}})
|
||||
captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
|
||||
provider="openrouter", user_plan="PlanType.PRO")
|
||||
assert captured[0] == "z-ai/glm-5.3-flash"
|
||||
|
||||
|
||||
def test_essential_tier_resolving_premium_model_is_corrected(monkeypatch, tmp_path, minimal_xlsx):
|
||||
"""Garde d'exécution : un ai_tiers essential contaminé par un modèle
|
||||
Premium ne doit pas partir en production — repli sur le défaut du plan."""
|
||||
settings = _settings({"essential": {"models": [], "default_model": "anthropic/claude-sonnet-5"},
|
||||
"premium": {"models": [], "default_model": ""}})
|
||||
captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
|
||||
provider="openrouter", user_plan="PlanType.PRO")
|
||||
assert captured[0] == "deepseek/deepseek-v4-flash"
|
||||
Reference in New Issue
Block a user