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

- 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:
2026-09-05 12:51:49 +02:00
parent 4e6850d6b1
commit 6c26712687
76 changed files with 5289 additions and 271 deletions

View File

@@ -56,8 +56,18 @@ def is_smtp_configured() -> bool:
return bool(cfg.get("host") and cfg.get("user"))
def send_email(to: str, subject: str, body: str) -> bool:
"""Send an email via SMTP (sync). Returns True if sent successfully."""
def send_email(
to: str,
subject: str,
body: str,
extra_headers: Optional[dict] = None,
) -> bool:
"""Send an email via SMTP (sync). Returns True if sent successfully.
``extra_headers`` is merged into the message headers (e.g. the
``List-Unsubscribe`` / ``List-Unsubscribe-Post`` marketing headers);
existing call sites are unaffected.
"""
cfg = _get_smtp_config()
if not cfg.get("host"):
logger.warning("SMTP not configured, email not sent")
@@ -69,6 +79,9 @@ def send_email(to: str, subject: str, body: str) -> bool:
msg["From"] = cfg["from_email"]
msg["To"] = to
msg.attach(MIMEText(body, "html"))
for header_name, header_value in (extra_headers or {}).items():
if header_name and header_value:
msg[header_name] = header_value
port = int(cfg.get("port", 587))
use_tls = cfg.get("use_tls", True)
@@ -91,7 +104,12 @@ def send_email(to: str, subject: str, body: str) -> bool:
return False
async def send_email_async(to: str, subject: str, body: str) -> bool:
async def send_email_async(
to: str,
subject: str,
body: str,
extra_headers: Optional[dict] = None,
) -> bool:
"""Send an email via SMTP (async). Returns True if sent successfully."""
cfg = _get_smtp_config()
if not cfg.get("host"):
@@ -112,8 +130,11 @@ async def send_email_async(to: str, subject: str, body: str) -> bool:
# Use sync smtplib inside a thread for reliability (aiosmtplib has quirks with raw messages)
import asyncio
import functools
return await asyncio.get_event_loop().run_in_executor(
None, send_email, to, subject, body
None,
functools.partial(send_email, to, subject, body, extra_headers),
)
except Exception as e:
logger.error(f"Failed to send email (async) to {to}: {e}")