""" app/billing/emails.py --------------------- Dunning / billing lifecycle emails sent via Flask-Mail in a background thread. Pattern mirrors app/utils/notifications.py: captures the app object before spawning the thread so the mail context is valid inside the worker. Public API: send_billing_email(to_addr, event_type, context_dict) event_type in ('payment_failed', 'trial_ending', 'subscription_cancelled') """ import logging import threading from flask import current_app from flask_mail import Message from app import mail logger = logging.getLogger(__name__) # ── Email templates ──────────────────────────────────────────────────────────── _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', } _BODIES = { 'payment_failed': """\ Hi, We were unable to process your most recent payment for your JQC subscription. Please update your payment method to avoid interruption to your service: {portal_url} If you have any questions, please contact support. — The JQC Team """, 'trial_ending': """\ Hi, Your JQC free trial will end on {trial_ends_at}. After that date, you will need an active subscription to continue using the service. Subscribe now to keep your workspace active: {subscribe_url} — The JQC Team """, 'subscription_cancelled': """\ Hi, Your JQC subscription has been cancelled and your workspace access has been suspended. You can reactivate your subscription at any time: {portal_url} — The JQC Team """, } def send_billing_email(to_addr: str, event_type: str, context_dict: dict): """Send a billing lifecycle email in a background thread. Args: to_addr: Recipient email address (tenant admin or billing contact). event_type: Key in _SUBJECTS / _BODIES. context_dict: Variables substituted into the body template. """ 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') body_template = _BODIES.get(event_type, '') try: body = body_template.format(**context_dict) except KeyError as exc: logger.error('BILLING EMAIL | template_error | event=%s | missing_key=%s', event_type, exc) body = body_template # send with unfilled placeholders rather than crashing app = current_app._get_current_object() def _send(): try: with app.app_context(): msg = Message( subject=subject, recipients=[to_addr], body=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) thread = threading.Thread(target=_send, daemon=True) thread.start()