Jun 28 - Implement payment functions (Stripe)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user