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:
@@ -182,6 +182,89 @@ def _allowed_providers_for_plan(plan: PlanType) -> set:
|
||||
return set(plan_cfg.get("providers", []))
|
||||
|
||||
|
||||
def _plan_from_str(plan_str: Optional[str]) -> PlanType:
|
||||
"""Resolve a ``user_plan`` string ("PlanType.PRO" or "pro") to a PlanType."""
|
||||
name = (plan_str or "").strip().lower()
|
||||
if name.startswith("plantype."):
|
||||
name = name.split(".", 1)[1]
|
||||
try:
|
||||
return PlanType(name)
|
||||
except ValueError:
|
||||
if name:
|
||||
logger.warning(
|
||||
"Unrecognized user_plan %r — falling back to the Free plan "
|
||||
"(Essentielle tier).",
|
||||
plan_str,
|
||||
)
|
||||
return PlanType.FREE
|
||||
|
||||
|
||||
def _plan_ai_models(plan: PlanType, premium: bool) -> list:
|
||||
"""Official model range for the plan's AI tier (Essentielle or Premium).
|
||||
|
||||
Plans without their own list (Free, Starter, Enterprise custom) fall back
|
||||
to the global defaults: Pro's Essentielle range and Business's Premium range.
|
||||
"""
|
||||
from models.subscription import (
|
||||
DEFAULT_AI_MODELS_ESSENTIAL,
|
||||
DEFAULT_AI_MODELS_PREMIUM,
|
||||
)
|
||||
|
||||
key = "ai_models_premium" if premium else "ai_models_essential"
|
||||
models = [m for m in (PLANS.get(plan, {}).get(key) or []) if m]
|
||||
if not models:
|
||||
models = list(DEFAULT_AI_MODELS_PREMIUM if premium else DEFAULT_AI_MODELS_ESSENTIAL)
|
||||
return models
|
||||
|
||||
|
||||
def _resolve_tier_model(admin_cfg: Any, user_plan: Optional[str], premium: bool) -> str:
|
||||
"""Modèle réellement routé pour le palier IA du plan.
|
||||
|
||||
Ordre de résolution (spec « Routage réel par abonnement ») :
|
||||
1. réglages admin ``ai_tiers.<palier>.default_model`` (changement sans redéploiement) ;
|
||||
2. premier modèle de ``ai_tiers.<palier>.models`` (ordre = priorité de secours) ;
|
||||
3. défaut du plan (``PLANS[plan]["ai_models_essential"|"ai_models_premium"][0]``).
|
||||
Garde d'exécution : un modèle résolu dans la gamme de l'autre palier est
|
||||
rejeté et remplacé par le défaut du plan — un utilisateur Pro (moteur
|
||||
openrouter = Essentielle) ne peut pas déclencher un modèle Premium.
|
||||
"""
|
||||
from models.subscription import (
|
||||
DEFAULT_AI_MODELS_ESSENTIAL,
|
||||
DEFAULT_AI_MODELS_PREMIUM,
|
||||
)
|
||||
|
||||
tier_cfg = getattr(getattr(admin_cfg, "ai_tiers", None), "premium" if premium else "essential", None)
|
||||
resolved = ""
|
||||
if tier_cfg is not None:
|
||||
default = (getattr(tier_cfg, "default_model", "") or "").strip()
|
||||
if default:
|
||||
resolved = default
|
||||
else:
|
||||
tier_models = [m.strip() for m in (getattr(tier_cfg, "models", None) or []) if m and m.strip()]
|
||||
if tier_models:
|
||||
resolved = tier_models[0]
|
||||
if not resolved:
|
||||
resolved = _plan_ai_models(_plan_from_str(user_plan), premium)[0]
|
||||
|
||||
# Garde : le modèle résolu doit appartenir au palier demandé.
|
||||
if premium and resolved in DEFAULT_AI_MODELS_ESSENTIAL:
|
||||
logger.warning(
|
||||
"ai_tiers.premium resolved to an Essentielle model (%s) — using the "
|
||||
"plan Premium default instead.",
|
||||
resolved,
|
||||
)
|
||||
resolved = _plan_ai_models(_plan_from_str(user_plan), premium=True)[0]
|
||||
elif not premium and resolved in DEFAULT_AI_MODELS_PREMIUM:
|
||||
logger.warning(
|
||||
"ai_tiers.essential resolved to a Premium model (%s) — refusing: a "
|
||||
"Pro subscription must never trigger a Premium model. Using the "
|
||||
"plan Essentielle default instead.",
|
||||
resolved,
|
||||
)
|
||||
resolved = _plan_ai_models(_plan_from_str(user_plan), premium=False)[0]
|
||||
return resolved
|
||||
|
||||
|
||||
def _image_translation_allowed_for_plan(plan: PlanType) -> bool:
|
||||
"""Vision (text-in-images) is sold on Pro and above."""
|
||||
return plan in (PlanType.PRO, PlanType.BUSINESS, PlanType.ENTERPRISE)
|
||||
@@ -1275,9 +1358,9 @@ async def _run_translation_job(
|
||||
return (admin_val or "").strip() or os.getenv(env_var, default)
|
||||
|
||||
api_key = _cfg(_admin_cfg.openrouter.api_key, "OPENROUTER_API_KEY")
|
||||
model = _cfg(_admin_cfg.openrouter.model, "OPENROUTER_MODEL", "google/gemini-3.5-flash")
|
||||
if model in ("deepseek/deepseek-v3.2", "google/gemini-2.0-flash-001"):
|
||||
model = "google/gemini-3.5-flash"
|
||||
# Routage réel par abonnement : le modèle vient du palier IA du plan
|
||||
# (réglages admin « ai_tiers » > défaut du plan) — voir _resolve_tier_model.
|
||||
model = _resolve_tier_model(_admin_cfg, user_plan, premium=False)
|
||||
|
||||
# Story 3.10: Retrieve and format glossary terms for LLM prompt
|
||||
glossary_terms = None
|
||||
@@ -1363,7 +1446,8 @@ async def _run_translation_job(
|
||||
)
|
||||
elif _p == "openrouter_premium":
|
||||
premium_key = _cfg(_admin_cfg.openrouter_premium.api_key, "OPENROUTER_API_KEY")
|
||||
premium_model = _cfg(_admin_cfg.openrouter_premium.model, "OPENROUTER_PREMIUM_MODEL", "anthropic/claude-sonnet-4.6")
|
||||
# Palier Premium : résolu par abonnement (défaut anthropic/claude-sonnet-5).
|
||||
premium_model = _resolve_tier_model(_admin_cfg, user_plan, premium=True)
|
||||
if not premium_key:
|
||||
premium_key = api_key # fall back to main openrouter key
|
||||
if premium_key:
|
||||
|
||||
Reference in New Issue
Block a user