Jun 28 - Update and polish UI/UX

This commit is contained in:
2026-06-28 19:00:34 -04:00
parent 54fa5b8c87
commit a42fc31fc5
14 changed files with 943 additions and 91 deletions
+48 -58
View File
@@ -2,98 +2,89 @@
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')
Sends multipart HTML + plain-text messages using Jinja2 templates in
app/templates/billing/email/.
"""
import logging
import threading
from flask import current_app
from flask import current_app, render_template
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',
'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,
_HTML_TEMPLATES = {
'payment_failed': 'billing/email/payment_failed.html',
'trial_ending': 'billing/email/trial_ending.html',
'subscription_cancelled': 'billing/email/subscription_cancelled.html',
}
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
""",
_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 email in a background thread.
"""Send a billing lifecycle HTML+plain 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.
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')
body_template = _BODIES.get(event_type, '')
plain_tmpl = _PLAIN_BODIES.get(event_type, '')
html_tmpl_path = _HTML_TEMPLATES.get(event_type)
try:
body = body_template.format(**context_dict)
plain_body = plain_tmpl.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
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=body,
body=plain_body,
html=html_body,
)
mail.send(msg)
logger.info('BILLING EMAIL | sent | event=%s | to=%s', event_type, to_addr)
@@ -101,5 +92,4 @@ def send_billing_email(to_addr: str, event_type: str, context_dict: dict):
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()
threading.Thread(target=_send, daemon=True).start()