feat(admin): canaux IA par palier (z.ai/DeepSeek directs), emails generes par IA, reductions Stripe
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m40s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m40s
- Chaque modele d'un palier devient une route {canal, modele} : openrouter,
deepseek direct, zhipu (z.ai, nouveau canal), minimax, openai, xai ;
repli automatique sur la route suivante si la cle manque, compatibilite
ascendante avec les anciens reglages a chaines
- Page Fournisseurs refondue en sections ; interrupteurs et chaine de
secours retablis ; tests de connexion avec delai d'attente
- Emails de relance generes par IA (canal au choix, prompt construit depuis
le plan marketing, HTML sane, RTL arabe/persan, jamais d'envoi automatique)
- Codes promo : validation locale avant Stripe, Coupon+PromotionCode avec
double plafond, comptage par session (webhook+sync=1 fois), ecriture
atomique, relier-a-Stripe, suppression, validation publique au checkout
- Page Tarifs : champ code promo (?promo=) avec verification traduite
- Tests : 1357 verts (facturation par palier testee dans le worker,
point de contact providers/available, promos, generation)
This commit is contained in:
@@ -71,3 +71,10 @@ Reportés de la revue marketing du 2026-09-05 (spec-hermes-marketing-execution.m
|
||||
- source_spec: `_bmad-output/implementation-artifacts/spec-strategie-llm-abonnements-admin.md`
|
||||
summary: Les cartes de revenus ne distinguent pas « aucune donnée » de « base injoignable » (affichage silencieux de zéro).
|
||||
evidence: Relecture de contenu ; amélioration d'affichage à prévoir sur RevenueOverview.tsx.
|
||||
|
||||
- source_spec: `_bmad-output/implementation-artifacts/spec-admin-fournisseurs-ia-emails-promos.md`
|
||||
summary: Le verrou des fichiers JSON de codes promo ne couvre qu'un processus ; avec plusieurs workers, deux paiements simultanés peuvent dépasser le plafond local.
|
||||
evidence: Relectures ; le plafond côté Stripe (max_redemptions sur le Promotion Code) reste le garde-fou réel. Un verrou inter-processus (fichier ou base) est un chantier à part, commun à tous les fichiers data/*.json.
|
||||
- source_spec: `_bmad-output/implementation-artifacts/spec-admin-fournisseurs-ia-emails-promos.md`
|
||||
summary: Environ 160 clés de traduction antérieures à ces chantiers n'existent que dans une partie des 13 langues (glossaires, traduction, connexion…).
|
||||
evidence: Constat du relecteur et de l'implémentation ; rapprochement complet à faire dans un passage dédié.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: 'Fournisseur par palier IA (DeepSeek/z.ai directs), page Fournisseurs, emails générés par IA, réductions Stripe'
|
||||
type: 'feature'
|
||||
created: '2026-09-05'
|
||||
status: 'done'
|
||||
baseline_commit: '6c26712'
|
||||
review_loop_iteration: 0
|
||||
context: []
|
||||
---
|
||||
|
||||
<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">
|
||||
|
||||
## Intent
|
||||
|
||||
**Problem :** Le routage par palier n'accepte que des modèles OpenRouter : Sepehr paie des accès directs DeepSeek et z.ai (Zhipu) qu'il ne peut pas utiliser ; la page Fournisseurs est confuse ; les emails de relance s'écrivent à la main sans s'appuyer sur l'IA ni sur le plan marketing ; aucune réduction ne peut être proposée dans les relances de façon fiable.
|
||||
|
||||
**Approach :** (1) Chaque modèle d'un palier devient une route `{provider, model}` : le palier choisit le canal (openrouter, deepseek direct, **zhipu** nouveau = z.ai, minimax, openai, zai) ; les chaînes simples restent acceptées (= openrouter, compatibilité). (2) Nouveau fournisseur `zhipu` (base `https://api.z.ai/api/paas/v4`, OpenAI-compatible) + refonte de la page Fournisseurs par sections. (3) Génération d'email par IA dans la page Marketing : fournisseur/modèle au choix, prompt système construit depuis `MARKETING_PLAN.md` (positionnement, tarifs, chiffres autorisés) ; rendu `{subject, html}` analysé avec robustesse. (4) Réductions : codes promo dans `data/promo_codes.json`, création Stripe Coupon+Promotion Code (double plafond), application au checkout et comptage des utilisations, sélecteur de promo dans l'email.
|
||||
|
||||
## Boundaries & Constraints
|
||||
|
||||
**Always :** Compatibilité ascendante : `models` à chaînes et `default_model` actuels continuent de fonctionner (testé) ; une route dont la clé manque est sautée au profit de la suivante de la liste ; tout appel LLM/Stripe a un délai d'attente, une erreur propagée claire et jamais d'HTTP 500 nu ; clés API jamais renvoyées en clair par de nouveaux endpoints ; interface en français correct, nouveaux textes dans les 13 langues ; facteur de coût : palier Premium = 5, Essentielle = 1 quel que soit le canal.
|
||||
|
||||
**Ask First :** Supprimer le fournisseur existant `zai` (xAI/Grok) — il reste ; changer les prix des forfaits ; créer une table de BDD (rester sur JSON dans `data/`).
|
||||
|
||||
**Never :** Passer Stripe en live ; appliquer une promo expirée/épuisée/hors forfait ; injecter le HTML généré sans longueur maximale et nettoyage basique ; bloquer un envoi de masse si la génération IA échoue ; toucher à `office-translator-landing-page/` ni aux dossiers d'outillage.
|
||||
|
||||
## I/O & Edge-Case Matrix
|
||||
|
||||
| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|
||||
|----------|--------------|---------------------------|----------------|
|
||||
| Palier avec routes multi-canaux | essential.routes = [zhipu/glm-5.3-flash, openrouter/deepseek-v4-flash] | Traduction via z.ai direct si clé présente, sinon openrouter ; facteur coût 1 | Clé zhipu absente → route suivante, journal d'avertissement |
|
||||
| Ancien réglage (chaînes) | ai_tiers.essential.models = ["deepseek/deepseek-v4-flash"] | Routé via openrouter comme aujourd'hui | N/A |
|
||||
| Code promo valide au checkout | promo actif, forfait pro, 3/10 utilisations | Session Stripe avec remise ; compteur incrémenté à la complétion seulement | N/A |
|
||||
| Code promo expiré/épuisé/mauvais forfait | n'importe lequel | Rejet explicite AVANT Stripe (message par cause) | Aucune session créée |
|
||||
| Génération IA d'email | fournisseur choisi, brief + promo optionnelle | {subject, html} conforme au plan marketing, pieds de page serveur inchangés | Sortie non analysable → erreur claire, aucun envoi automatique |
|
||||
|
||||
</frozen-after-approval>
|
||||
|
||||
## Code Map
|
||||
|
||||
- `routes/admin_routes.py` -- `AiTierSettings` l.1055 (évoluer vers routes `{provider, model, label}` + compat chaînes), `normalize_ai_tiers` l.1089 (validation des routes : fournisseur connu, dédoublonnage, anti-croisement de palier inchangé), endpoints marketing l.2388-2660 (y ajouter `POST /marketing/email/generate`, `GET/POST /marketing/promos`, `POST /marketing/promos/{code}/deactivate`), `_execute_bulk_send`.
|
||||
- `routes/translate_routes.py` -- `_resolve_tier_model` l.220 (renvoyer la route complète : `(provider, model)`), bloc d'instanciation l.1390-1460 (dispatch par canal : openrouter/openrouter_premium déjà OpenAI-compat ; deepseek → `DeepSeekTranslationProvider` ; zhipu/minimax/zai/openai → `OpenAITranslationProvider` avec leur base_url), `_compute_cost_factor` l.608 (basé sur le palier, pas le nom du canal).
|
||||
- `services/providers/config.py` -- motif env par fournisseur : ajouter `ZHIPU_ENABLED/ZHIPU_API_KEY/ZHIPU_MODEL/ZHIPU_BASE_URL` (défaut `https://api.z.ai/api/paas/v4`, modèle `glm-5.3`).
|
||||
- `services/payment_service.py` -- `create_checkout_session` l.58 (paramètre `promo_code`, `discounts=[{"promotion_code": …}]`), `handle_checkout_completed` (incrément du compteur via `metadata.promo_code`), `handle_webhook` l.235 ; credits sans promo.
|
||||
- `routes/auth_routes.py` -- `CheckoutRequest` l.72 (champ `promo_code` optionnel) + endpoint l.838.
|
||||
- `services/pricing_config.py` -- motifs Stripe (clés, mode live/test) à réutiliser ; `data/pricing_overrides.json` = précédent de fichier JSON géré admin.
|
||||
- `MARKETING_PLAN.md` (racine) -- source du prompt : sections 1 (positionnement/USP) et 2 (forfaits et chiffres autorisés) ; extraire côté serveur, tronquer (~4000 caractères).
|
||||
- `frontend/src/app/admin/settings/page.tsx` -- refonte : sections « Traduction classique » (google, google_cloud), « Fournisseurs IA directs » (deepseek, zhipu nouveau, minimax, openai, zai), « Passerelles multi-modèles » (openrouter, openrouter_premium : clé seule, rappel « modèles gérés dans Modèles & abonnements »), OCR, Emails ; boutons de test existants conservés.
|
||||
- `frontend/src/app/admin/models/page.tsx` -- chaque modèle gagne un sélecteur de fournisseur ; combobox libre (identifiants différents par canal) + catalogue OpenRouter pour ce canal.
|
||||
- `frontend/src/app/admin/marketing/page.tsx` -- bloc « Générer avec l'IA » (fournisseur+modèle, brief, langue, ton, promo optionnelle → remplit sujet/HTML) + section « Réductions » (liste, création : type %/montant, forfaits, plafond, expiration ; désactivation).
|
||||
- `frontend/src/app/pricing/page.tsx` -- champ code promo au checkout + lecture `?promo=` ; l'envoyer à create-checkout.
|
||||
- `.env.example` -- bloc `ZHIPU_*` documenté (et rappel `DEEPSEEK_*`) comme les autres fournisseurs.
|
||||
- i18n `admin.json`/`pricing.json` (13 langues) -- clés nouvelles (fournisseurs, génération IA, promos, champ code promo) ; `admin.nav.*` inchangé.
|
||||
- `tests/` -- motifs `test_ai_tier_routing.py`, `test_admin_marketing.py`, `test_worker_tier_routing.py`, mocks Stripe éventuels dans `tests/`.
|
||||
|
||||
## Tasks & Acceptance
|
||||
|
||||
**Execution :**
|
||||
- [ ] `routes/admin_routes.py` -- routes multi-canaux dans `AiTierSettings` (compat chaînes), validation dans `normalize_ai_tiers`, endpoints `/marketing/email/generate` (prompt = plan marketing + brief ; appel LLM du canal choisi ; analyse robuste JSON avec repli texte ; longueur max) et `/marketing/promos` (création Stripe Coupon+Promotion Code si Stripe configuré, sinon `local_only` refusé au checkout avec message clair ; désactivation ; plafonds côté local ET Stripe).
|
||||
- [ ] `routes/translate_routes.py` -- résolution `(provider, model)` par route de palier avec repli sur la route suivante si la clé manque, dispatch d'instanciation par canal (zhipu inclus), facteur de coût par palier.
|
||||
- [ ] `services/providers/config.py` + `.env.example` -- fournisseur `zhipu` (env documentées).
|
||||
- [ ] `services/payment_service.py` + `routes/auth_routes.py` -- `promo_code` au checkout : validation locale (actif, forfait, expiration, plafond) AVANT Stripe, remise Stripe, métadonnée `promo_code`, incrément à la complétion (webhook ET sync).
|
||||
- [ ] `frontend/src/app/admin/settings/page.tsx` -- refonte par sections + fournisseur zhipu.
|
||||
- [ ] `frontend/src/app/admin/models/page.tsx` -- sélecteur de fournisseur par modèle.
|
||||
- [ ] `frontend/src/app/admin/marketing/page.tsx` -- génération IA (choix du canal) + gestion des réductions.
|
||||
- [ ] `frontend/src/app/pricing/page.tsx` -- champ/URL du code promo au checkout.
|
||||
- [ ] i18n 13 langues -- toutes les nouvelles clés.
|
||||
- [ ] `tests/` -- compat chaînes, multi-canaux avec repli de clé, zhipu, coût par palier ; promos : validation, checkout mocké, incrément à la complétion, rejets par cause ; génération IA : prompt contient le plan, repli d'analyse.
|
||||
- [ ] `MARKETING_PLAN.md` -- une ligne « relances et réductions gérées depuis l'admin ».
|
||||
|
||||
**Acceptance Criteria :**
|
||||
- Given des clés DeepSeek et z.ai renseignées, when l'admin choisit ces canaux pour un palier, then les traductions du palier partent par ces canaux (repli automatique sur la route suivante en cas de clé absente) et la facturation (1×/5×) suit le palier.
|
||||
- Given le plan marketing, when l'admin génère un email par IA, then le texte respecte le positionnement et les chiffres autorisés, sans aucun envoi automatique.
|
||||
- Given un code promo à 3/10 utilisations, when trois souscriptions se complètent, then le compteur passe à 6 via webhook ou sync, et un 11ᵉ essai est refusé avant tout appel Stripe.
|
||||
- Given `pytest`, `npm run build:local`, `npm run test:run` et le contrôle i18n, when exécutés, then tout est vert.
|
||||
|
||||
## Design Notes
|
||||
|
||||
Route de palier : `{"provider": "zhipu", "model": "glm-5.3-flash", "label": "GLM-5.3 Flash (z.ai)"}` ; chaîne `"z-ai/glm-5.3-flash"` = `{"provider": "openrouter", …}`. Génération d'email : le serveur construit le message système (plan marketing tronqué + règles : jamais de chiffre hors plan, HTML simple à styles en ligne, langue imposée) puis l'appel LLM avec `response` attendu en JSON `{"subject": …, "html": …}` ; en cas d'échec d'analyse, extraire `<title>`/corps, sinon erreur 502 claire `AI_GENERATION_FAILED`. Promos : code `[A-Z0-9-]{3,24}` ; Stripe `coupon` (percent/amount EUR) + `promotion_code` (code lisible, `max_redemptions`, `expires_at`) ; `local_only` jamais appliqué au checkout ; compteur local incrémenté uniquement dans `handle_checkout_completed` via `metadata.promo_code`.
|
||||
|
||||
## Verification
|
||||
|
||||
**Commands :**
|
||||
- `./venv/Scripts/python.exe -m pytest -q --no-cov` -- expected: nouveaux tests verts, pas de nouvelle régression
|
||||
- `cd frontend && npm run build:local` -- expected: succès
|
||||
- `cd frontend && npm run test:run` -- expected: verts
|
||||
- script de comparaison i18n (clés identiques 13 langues) -- expected: OK
|
||||
|
||||
**Manual checks (if no CLI):**
|
||||
- Page Fournisseurs : sections lisibles, zhipu présent, tests de fournisseur fonctionnels.
|
||||
- Page Marketing : génération IA produit un sujet + HTML ; création d'un code promo en mode test Stripe.
|
||||
|
||||
## Spec Change Log
|
||||
|
||||
## Suggested Review Order
|
||||
|
||||
**Routage multi-canaux par palier**
|
||||
|
||||
- Routes `{canal, modele}` d'un palier avec compatibilite des anciennes chaines.
|
||||
[`admin_routes.py:1064`](../../routes/admin_routes.py#L1064)
|
||||
|
||||
- Resolution unique consciente des cles (une route sans cle est sautee), utilisee par le worker ET l'affichage.
|
||||
[`translate_routes.py:250`](../../routes/translate_routes.py#L250)
|
||||
|
||||
- Catalogue des canaux (zhipu = z.ai) et variables d'environnement.
|
||||
[`config.py:264`](../../services/providers/config.py#L264)
|
||||
|
||||
**Reductions Stripe**
|
||||
|
||||
- Validation locale avant tout appel Stripe (actif, forfait, expiration, plafond).
|
||||
[`payment_service.py:129`](../../services/payment_service.py#L129)
|
||||
|
||||
- Comptage par session (webhook + sync = une seule fois, meme en JSON).
|
||||
[`payment_service.py:212`](../../services/payment_service.py#L212)
|
||||
|
||||
- Creation Coupon+PromotionCode avec suppression du coupon orphelin en cas d'echec.
|
||||
[`payment_service.py:279`](../../services/payment_service.py#L279)
|
||||
|
||||
**Emails generes par IA**
|
||||
|
||||
- Nettoyage durci du HTML renvoye par le modele.
|
||||
[`admin_routes.py:3010`](../../routes/admin_routes.py#L3010)
|
||||
|
||||
- Generation : prompt = plan marketing + brief ; plan absent refuse, promo inconnue refusee.
|
||||
[`admin_routes.py:3061`](../../routes/admin_routes.py#L3061)
|
||||
|
||||
- Creation de code promo cote admin.
|
||||
[`admin_routes.py:3251`](../../routes/admin_routes.py#L3251)
|
||||
|
||||
**Points de contact paiement**
|
||||
|
||||
- `promo_code` accepte au checkout (validation AVANT Stripe).
|
||||
[`auth_routes.py:72`](../../routes/auth_routes.py#L72)
|
||||
|
||||
**Interfaces**
|
||||
|
||||
- Constante unique des canaux cote client.
|
||||
[`constants.ts:25`](../../frontend/src/app/admin/constants.ts#L25)
|
||||
|
||||
- Page Marketing : generation IA + reductions.
|
||||
[`page.tsx:112`](../../frontend/src/app/admin/marketing/page.tsx#L112)
|
||||
|
||||
- Page Tarifs : champ code promo, lecture `?promo=`, validation avant paiement.
|
||||
[`page.tsx:280`](../../frontend/src/app/pricing/page.tsx#L280)
|
||||
|
||||
**Peripheriques**
|
||||
|
||||
- Tests : routage, promos, generation.
|
||||
[`test_admin_promos.py:1`](../../tests/test_admin_promos.py#L1)
|
||||
Reference in New Issue
Block a user