""" app/billing/emails.py --------------------- Dunning / billing lifecycle emails sent via Flask-Mail in a background thread. Sends multipart HTML + plain-text messages using Jinja2 templates in app/templates/billing/email/. """ import logging import threading from flask import current_app, render_template from flask_mail import Message from app import mail logger = logging.getLogger(__name__) _SUBJECTS = { 'payment_failed': 'Action Required: Payment failed for your JQC subscription', 'trial_ending': 'Your JQC free trial ends in 3 days', 'subscription_cancelled': 'Your JQC subscription has been cancelled', } _HTML_TEMPLATES = { 'payment_failed': 'billing/email/payment_failed.html', 'trial_ending': 'billing/email/trial_ending.html', 'subscription_cancelled': 'billing/email/subscription_cancelled.html', } _PLAIN_BODIES = { 'payment_failed': ( "We were unable to process your most recent payment for your JQC subscription.\n\n" "Please update your payment method to avoid interruption to your service:\n" " {portal_url}\n\n— The JQC Team" ), 'trial_ending': ( "Your JQC free trial will end on {trial_ends_at}.\n\n" "Subscribe now to keep your workspace active:\n" " {subscribe_url}\n\n— The JQC Team" ), 'subscription_cancelled': ( "Your JQC subscription has been cancelled and your workspace access has been suspended.\n\n" "You can reactivate at any time:\n" " {portal_url}\n\n— The JQC Team" ), } def send_billing_email(to_addr: str, event_type: str, context_dict: dict): """Send a billing lifecycle HTML+plain email in a background thread. Args: to_addr: Recipient email address. event_type: One of 'payment_failed', 'trial_ending', 'subscription_cancelled'. context_dict: Variables substituted into both the HTML template and plain body. """ if not to_addr: logger.warning('BILLING EMAIL | skipped | event=%s | reason=no_recipient', event_type) return subject = _SUBJECTS.get(event_type, 'JQC Billing Notification') plain_tmpl = _PLAIN_BODIES.get(event_type, '') html_tmpl_path = _HTML_TEMPLATES.get(event_type) try: plain_body = plain_tmpl.format(**context_dict) except KeyError as exc: logger.error('BILLING EMAIL | plain_template_error | event=%s | key=%s', event_type, exc) plain_body = plain_tmpl app = current_app._get_current_object() def _send(): try: with app.app_context(): html_body = None if html_tmpl_path: try: html_body = render_template(html_tmpl_path, **context_dict) except Exception as exc: logger.error('BILLING EMAIL | html_render_failed | tmpl=%s | err=%s', html_tmpl_path, exc) msg = Message( subject=subject, recipients=[to_addr], body=plain_body, html=html_body, ) mail.send(msg) logger.info('BILLING EMAIL | sent | event=%s | to=%s', event_type, to_addr) except Exception as exc: logger.error('BILLING EMAIL | send_failed | event=%s | to=%s | err=%s', event_type, to_addr, exc) threading.Thread(target=_send, daemon=True).start()