--- title: 'Stratégie LLM par abonnement + admin (modèles, statistiques, emails marketing)' type: 'feature' created: '2026-09-05' status: 'done' baseline_commit: '4e6850d6b12f2f97e9ec91a821b51a3b324d8f71' review_loop_iteration: 0 context: [] --- ## Intent **Problem :** Les modèles IA affichés sur la page d'accueil et la page tarifs ne correspondent plus à la gamme réelle (Claude Sonnet 4.6, « Claude Haiku », Gemini) ; aucun modèle n'est réellement routé par abonnement (le modèle employé vient des réglages fournisseur) ; l'admin ne peut ni gérer les modèles par type d'abonnement, ni voir de statistiques de revenus, ni envoyer d'emails de relance marketing. **Approach :** (1) Nouvelle gamme officielle — IA Essentielle (plan Pro) : `deepseek/deepseek-v4-flash`, `z-ai/glm-5.3-flash`, `minimax/minimax-m3` ; IA Premium (plan Business) : `anthropic/claude-sonnet-5` (remplace le 4.6, $2/$10 /1M), alternatives `deepseek/deepseek-v4-pro` et `z-ai/glm-5.3`. (2) Routage réel : le modèle utilisé par traduction est résolu par le palier IA du plan (réglages admin > défauts des plans). (3) Page admin dédiée « Modèles & abonnements » (matrice plan → palier → modèles actifs + modèle par défaut). (4) Statistiques enrichies : revenus (encaissé, MRR estimé, crédits), conversion, liste d'attente. (5) Page admin « Marketing » : envoi d'emails de relance par audience (liste d'attente, inactifs, par plan) avec aperçu, envoi test, historique et désabonnement — bonnes pratiques type Brevo/Mailchimp. ## Boundaries & Constraints **Always :** Prix/textes en français correct dans l'interface ; les noms de modèles restent en latin tel quel dans les 13 langues ; ne jamais exposer de clé API dans les réponses admin ; un utilisateur Pro ne doit jamais déclencher un modèle Premium ; SMTP existant (`services/email_service.py`) réutilisé, pas de nouveau fournisseur. **Ask First :** Changer les prix des plans (19 €/49 €) ; créer une table de BDD nouvelle (préférer les fichiers JSON du dossier `data/` comme le font waitlist et réglages) ; toucher à la chaîne de secours (`FALLBACK_CHAIN`). **Never :** Modifier le dossier `office-translator-landing-page/` (prototype hors production) ni `.claude/`/`.agent/` ; supprimer les anciennes clés `ai_model_essential`/`ai_model_premium` utilisées ailleurs ; Stripe en mode live ; envoyer un email en masse sans passer par l'envoi test ; citer DeepL comme moteur. ## I/O & Edge-Case Matrix | Scenario | Input / State | Expected Output / Behavior | Error Handling | |----------|--------------|---------------------------|----------------| | Traduction Pro, moteur openrouter | plan=pro, réglages admin essential.default=`z-ai/glm-5.3-flash` | Job exécuté avec ce modèle, facteur coût 1 | Si vide → défaut du plan (`deepseek/deepseek-v4-flash`) | | Traduction Business, premium | plan=business, openrouter_premium | Modèle = premium.default (`anthropic/claude-sonnet-5`) | Si clé absente → échec gracieux existant | | Envoi massif partiel | 200 destinataires, 3 échecs SMTP | Réponse : envoyés/échoués détaillés ; historique journalisé | Continuer après échec individuel, jamais d'HTTP 500 | | Désabonné | email présent dans `data/unsubscribes.json` | Exclu de tout envoi d'audience | N/A | ## Code Map - `models/subscription.py` -- PLANS (l.38-187) : clés `ai_model_essential`/`ai_model_premium` à remplacer par listes `ai_models_essential`/`ai_models_premium` (PRO l.100, BUSINESS l.130-131) ; commentaire d'en-tête l.34-37. - `routes/translate_routes.py` -- résolution modèle l.1277-1280 (openrouter, défaut `google/gemini-3.5-flash` + remap ancien IDs) et l.1364-1375 (openrouter_premium, défaut `anthropic/claude-sonnet-4.6`) ; `_compute_cost_factor` l.562 ; `_allowed_providers_for_plan` l.179. C'est ici que le routage par palier s'applique (via `load_settings` déjà importé l.1269). - `routes/admin_routes.py` -- `SettingsConfig` l.914-928 (ajouter section `ai_tiers`) ; GET/PUT `/settings` l.956-1054 ; `GET /stats` l.562 (usage seul, à enrichir) ; catalogue modèles OpenRouter `GET /providers/openrouter/models` existant (réutiliser pour la combobox admin) ; `POST /providers/smtp/test-send` l.1332 (exemple d'envoi). - `services/email_service.py` -- `send_email_async(to, subject, body)` l.94 ; `is_smtp_configured()` l.53. Réutiliser tel quel. - `routes/waitlist_routes.py` -- `data/waitlist.json` (email, interest, created_at) : audience marketing n°1, motif de fichier JSON à copier (verrou `_write_lock`) pour historique d'envois et désabonnements. - `database/models.py` -- `PaymentHistory` l.293 (amount_cents, payment_type, status, created_at) et `Translation` l.149 (created_at, provider) : sources des stats de revenus ; `repositories.py` a les accesseurs. - `services/pricing_config.py` -- `get_effective_monthly_yearly` pour le MRR estimé (prix effectifs, overrides inclus). - `routes/auth_routes.py` l.763-811 -- `GET /plans` : exposer les listes de modèles (`ai_models_essential`/`premium`) au frontend. - `frontend/src/components/landing/landing-page.tsx` l.440-582 -- PricingSection : textes via i18n uniquement. - `frontend/src/app/pricing/page.tsx` -- `STATIC_PLANS` l.62-198 (business.feat3 « Claude Haiku », enterprise.feat2 « Claude Opus 4.6 ») ; section aiModels textée via i18n. - `frontend/src/lib/i18n/messages//{landing,pricing,admin}.json` (13 langues : fr,en,de,es,it,pt,nl,ar,fa,ja,ko,ru,zh) -- clés à mettre à jour ; structure identique entre langues. - `frontend/src/app/admin/` -- `constants.ts` (nav, ajouter 2 entrées), `settings/page.tsx` (motif de page avec combobox modèles), `stats/page.tsx` + `useTranslationStats.ts` (motif graphiques), `types.ts`. Nouvelles pages : `models/page.tsx`, `marketing/page.tsx`. - `tests/` -- pytest, motif `test_admin_*.py`. ## Tasks & Acceptance **Execution :** - [x] `models/subscription.py` -- remplacer les deux clés modèle par les listes de gamme (Essentielle/Premium ci-dessus) -- source de vérité. - [x] `routes/auth_routes.py` -- exposer `ai_models_essential`/`ai_models_premium` dans `GET /plans` -- affichage public. - [x] `routes/translate_routes.py` -- résoudre le modèle par palier : réglages admin (`ai_tiers`) > défaut du plan ; défauts premium → `anthropic/claude-sonnet-5` ; nettoyer le remap l.1279 -- routage réel par abonnement. - [x] `routes/admin_routes.py` -- `SettingsConfig.ai_tiers` (essential/premium : models[], default_model) avec valeurs par défaut issues de PLANS ; enrichir `GET /stats` (revenus encaissés 30 j + total via PaymentHistory, MRR estimé par plan, crédits achetés, compteur liste d'attente, répartition paliers IA utilisés) ; nouveaux endpoints `GET/POST /marketing/audiences` (comptes par audience), `POST /marketing/email/send` (audience, sujet, html, test_mode ; exclude désabonnés ; historique dans `data/marketing_emails.json`), `GET /marketing/email/history`, et public `GET /api/v1/marketing/unsubscribe` -- gestion + stats + relances. - [x] `services/email_service.py` -- ne rien changer sauf si un helper pied-de-page désabonnement s'avère nécessaire. - [x] `frontend/src/app/admin/models/page.tsx` + `marketing/page.tsx` + `constants.ts` + `types.ts` -- deux pages admin (matrice modèles par plan/palier avec défaut radio + combobox du catalogue OpenRouter ; marketing : audience + compteur, sujet/HTML avec aperçu, envoi test, envoi réel, historique) -- l'exigence de Sepehr. - [x] `frontend/src/app/admin/stats/` -- cartes et graphiques des nouvelles métriques (revenus, MRR, liste d'attente) -- visibilité business. - [x] `frontend/src/app/pricing/page.tsx` + `components/landing/landing-page.tsx` -- textes statiques à jour (Claude 5, gamme) -- cohérence marketing. - [ ] i18n `landing.json`/`pricing.json`/`admin.json` (13 langues) -- clés modifiées/nouvelles traduites ; noms de modèles inchangés -- aucune chaîne manquante. - [x] `tests/` -- tests : routage palier (pro→essential, business→premium, défaut admin), refus modèle premium pour plan pro, endpoint marketing (envoi test, désabonné exclu, historique), stats revenus. **Acceptance Criteria :** - Given un utilisateur Pro, when il traduit avec le moteur openrouter, then le modèle employé est celui du palier Essentielle (jamais un modèle Premium) et la page tarifs affiche la gamme exacte. - Given l'admin, when il change le modèle par défaut Essentielle puis traduit, then le nouveau modèle est utilisé sans redéploiement. - Given un désabonné, when un envoi marketing part, then il ne reçoit rien et l'historique trace envoyés/échecs. - Given `npm run build` et `pytest`, when exécutés, then succès sans erreur ni chaîne i18n manquante. ## Design Notes Matrice admin (bonne pratique type LiteLLM/OpenRouter) : lignes = plans Pro/Business, colonnes = palier, contenu = modèles actifs (ordre = priorité de secours) + radio « par défaut ». Emails : audience = `waitlist` | `inactive_30` | `plan:pro` | `all_users` ; envoi séquentiel 0,2 s d'intervalle dans une tâche de fond ; pied de page avec lien `/api/v1/marketing/unsubscribe?email=`. Chiffres marketing autorisés : Essentielle « à partir de 0,09 $/M jetons (≈3× moins cher qu'annoncé auparavant) », contexte jusqu'à 1,3 M (GLM-5.3 Flash) ; Premium « Claude 5, 1M de contexte ». ## Verification **Commands :** - `cd frontend && npm run build` -- expected: compilation sans erreur - `cd frontend && npm run test:run` -- expected: tests vitest verts - `pytest -x -q` -- expected: nouveaux tests + régression verts - `node -e "…vérif clés i18n identiques fr/en/…"` -- expected: aucune clé manquante (script de comparaison) **Manual checks (if no CLI):** - Page d'accueil + /pricing : gamme affichée = deepseek-v4-flash, glm-5.3-flash, minimax-m3 / Claude 5, deepseek-v4-pro, glm-5.3 ; plus aucun « Claude Haiku » ni « Sonnet 4.6 ». - Admin : pages Modèles et Marketing fonctionnelles ; envoi test reçu. ## Spec Change Log ## Suggested Review Order **Gamme officielle et exposition publique** - La source de vérité : les deux gammes officielles (Essentielle Pro / Premium Business) et les défauts. [`subscription.py:41`](../../models/subscription.py#L41) - L'endpoint public des forfaits expose les listes de modèles aux pages tarifs. [`auth_routes.py:786`](../../routes/auth_routes.py#L786) **Routage réel par palier** - Résolution du modèle : réglages admin > premier de la liste > défaut du plan, avec garde anti-croisement de palier. [`translate_routes.py:220`](../../routes/translate_routes.py#L220) - Le point d'entrée historique affiche désormais le modèle réellement routé. [`legacy_routes.py:91`](../../routes/legacy_routes.py#L91) **Côté admin : réglages et protections** - Bloc `ai_tiers` : défauts, normalisation (dédoublonnage, refus de croisement de gamme). [`admin_routes.py:1055`](../../routes/admin_routes.py#L1055) - Garde « pristine » : une sauvegarde depuis la page Fournisseurs n'écrase plus les paliers personnalisés. [`admin_routes.py:1232`](../../routes/admin_routes.py#L1232) **Marketing : audiences, envoi, désabonnement** - En-têtes de délivrabilité et lien de désabonnement par destinataire. [`admin_routes.py:2388`](../../routes/admin_routes.py#L2388) - Audiences avec compteurs (liste d'attente, inactifs 30 jours, par forfait). [`admin_routes.py:2417`](../../routes/admin_routes.py#L2417) - Envoi : test obligatoire (empreinte), historique « running » avant la boucle, verrou anti-campagne simultanée. [`admin_routes.py:2499`](../../routes/admin_routes.py#L2499) **Interfaces** - Page « Modèles & abonnements » : matrice forfaits → paliers, défaut radio, catalogue OpenRouter. [`page.tsx:1`](../../frontend/src/app/admin/models/page.tsx#L1) - Page « Marketing » : audience, aperçu, envoi test, historique (textes 100 % i18n). [`page.tsx:1`](../../frontend/src/app/admin/marketing/page.tsx#L1) - Cartes de revenus / MRR / liste d'attente des statistiques. [`RevenueOverview.tsx:22`](../../frontend/src/app/admin/RevenueOverview.tsx#L22) - Listes de modèles côté client alignées sur la gamme. [`store.ts:10`](../../frontend/src/lib/store.ts#L10) **Périphériques** - En-têtes optionnels du service SMTP existant (aucun changement pour les appels en place). [`email_service.py:63`](../../services/email_service.py#L63) - Test d'intégration du routage dans la tâche de traduction (faux fournisseur capturant le modèle). [`test_worker_tier_routing.py:114`](../../tests/test_worker_tier_routing.py#L114) - Tests marketing (désabonnement en cours de campagne, campagne simultanée, échappement) et revenus en base. [`test_admin_marketing.py:1`](../../tests/test_admin_marketing.py#L1)