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:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user