feat: implement email template system with vendor overrides

Add comprehensive email template management for both admin and vendors:

Admin Features:
- Email templates management page at /admin/email-templates
- Edit platform templates with language support (en, fr, de, lb)
- Preview templates with sample variables
- Send test emails
- View email logs per template

Vendor Features:
- Email templates customization page at /vendor/{code}/email-templates
- Override platform templates with vendor-specific versions
- Preview and test customized templates
- Revert to platform defaults

Technical Changes:
- Migration for vendor_email_templates table
- VendorEmailTemplate model with override management
- Enhanced EmailService with language resolution chain
  (customer preferred -> vendor preferred -> platform default)
- Branding resolution (Wizamart default, removed for whitelabel)
- Platform-only template protection (billing templates)
- Admin and vendor API endpoints with full CRUD
- Updated seed script with billing and team templates

Files: 22 changed, ~3,900 lines added

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 18:29:26 +01:00
parent 2e1a2fc9fc
commit c52af2a155
22 changed files with 3882 additions and 119 deletions

View File

@@ -896,6 +896,363 @@ Dëse Link leeft an {{ expiry_hours }} Stonn(en) of. Wann Dir dës Passwuertzrec
Mat beschte Gréiss,
D'Team
""",
},
# -------------------------------------------------------------------------
# PLATFORM-ONLY BILLING TEMPLATES
# -------------------------------------------------------------------------
{
"code": "subscription_welcome",
"language": "en",
"name": "Subscription Welcome",
"description": "Sent to vendors when they subscribe to a paid plan",
"category": EmailCategory.BILLING.value,
"is_platform_only": True,
"required_variables": json.dumps(["vendor_name", "tier_name", "billing_cycle", "amount"]),
"variables": json.dumps([
"vendor_name", "tier_name", "billing_cycle", "amount",
"next_billing_date", "dashboard_url"
]),
"subject": "Welcome to {{ tier_name }} - Subscription Confirmed",
"body_html": """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Subscription Confirmed!</h1>
</div>
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">Hi {{ vendor_name }},</p>
<p>Thank you for subscribing to Wizamart! Your {{ tier_name }} subscription is now active.</p>
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981;">
<h3 style="margin-top: 0; color: #10b981;">Subscription Details</h3>
<p style="margin: 5px 0;"><strong>Plan:</strong> {{ tier_name }}</p>
<p style="margin: 5px 0;"><strong>Billing Cycle:</strong> {{ billing_cycle }}</p>
<p style="margin: 5px 0;"><strong>Amount:</strong> {{ amount }}</p>
<p style="margin: 5px 0;"><strong>Next Billing Date:</strong> {{ next_billing_date }}</p>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ dashboard_url }}" style="background: #10b981; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">
Go to Dashboard
</a>
</div>
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
If you have any questions about your subscription, please contact our support team.
</p>
<p>Best regards,<br><strong>The Wizamart Team</strong></p>
</div>
<div style="text-align: center; padding: 20px; color: #9ca3af; font-size: 12px;">
<p>&copy; 2024 Wizamart. All rights reserved.</p>
</div>
</body>
</html>""",
"body_text": """Subscription Confirmed!
Hi {{ vendor_name }},
Thank you for subscribing to Wizamart! Your {{ tier_name }} subscription is now active.
Subscription Details:
- Plan: {{ tier_name }}
- Billing Cycle: {{ billing_cycle }}
- Amount: {{ amount }}
- Next Billing Date: {{ next_billing_date }}
Go to Dashboard: {{ dashboard_url }}
If you have any questions about your subscription, please contact our support team.
Best regards,
The Wizamart Team
""",
},
{
"code": "payment_failed",
"language": "en",
"name": "Payment Failed",
"description": "Sent when a subscription payment fails",
"category": EmailCategory.BILLING.value,
"is_platform_only": True,
"required_variables": json.dumps(["vendor_name", "tier_name", "amount"]),
"variables": json.dumps([
"vendor_name", "tier_name", "amount", "retry_date",
"update_payment_url", "support_email"
]),
"subject": "Action Required: Payment Failed for Your Subscription",
"body_html": """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Payment Failed</h1>
</div>
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">Hi {{ vendor_name }},</p>
<p>We were unable to process your payment of <strong>{{ amount }}</strong> for your {{ tier_name }} subscription.</p>
<div style="background: #fef2f2; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #ef4444;">
<h3 style="margin-top: 0; color: #dc2626;">What happens next?</h3>
<p style="margin: 5px 0;">We'll automatically retry the payment on {{ retry_date }}.</p>
<p style="margin: 5px 0;">To avoid any service interruption, please update your payment method.</p>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ update_payment_url }}" style="background: #ef4444; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">
Update Payment Method
</a>
</div>
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
If you need assistance, please contact us at {{ support_email }}.
</p>
<p>Best regards,<br><strong>The Wizamart Team</strong></p>
</div>
</body>
</html>""",
"body_text": """Payment Failed
Hi {{ vendor_name }},
We were unable to process your payment of {{ amount }} for your {{ tier_name }} subscription.
What happens next?
- We'll automatically retry the payment on {{ retry_date }}.
- To avoid any service interruption, please update your payment method.
Update Payment Method: {{ update_payment_url }}
If you need assistance, please contact us at {{ support_email }}.
Best regards,
The Wizamart Team
""",
},
{
"code": "subscription_cancelled",
"language": "en",
"name": "Subscription Cancelled",
"description": "Sent when a subscription is cancelled",
"category": EmailCategory.BILLING.value,
"is_platform_only": True,
"required_variables": json.dumps(["vendor_name", "tier_name"]),
"variables": json.dumps([
"vendor_name", "tier_name", "end_date", "reactivate_url"
]),
"subject": "Your Wizamart Subscription Has Been Cancelled",
"body_html": """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Subscription Cancelled</h1>
</div>
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">Hi {{ vendor_name }},</p>
<p>Your {{ tier_name }} subscription has been cancelled as requested.</p>
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #6b7280;">
<h3 style="margin-top: 0; color: #4b5563;">What happens now?</h3>
<p style="margin: 5px 0;">You'll continue to have access to your {{ tier_name }} features until <strong>{{ end_date }}</strong>.</p>
<p style="margin: 5px 0;">After that date, your account will be downgraded to the Free tier.</p>
</div>
<p>Changed your mind? You can reactivate your subscription at any time:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ reactivate_url }}" style="background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">
Reactivate Subscription
</a>
</div>
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
We're sorry to see you go. If there's anything we could have done better, please let us know.
</p>
<p>Best regards,<br><strong>The Wizamart Team</strong></p>
</div>
</body>
</html>""",
"body_text": """Subscription Cancelled
Hi {{ vendor_name }},
Your {{ tier_name }} subscription has been cancelled as requested.
What happens now?
- You'll continue to have access to your {{ tier_name }} features until {{ end_date }}.
- After that date, your account will be downgraded to the Free tier.
Changed your mind? You can reactivate your subscription at any time:
{{ reactivate_url }}
We're sorry to see you go. If there's anything we could have done better, please let us know.
Best regards,
The Wizamart Team
""",
},
{
"code": "trial_ending",
"language": "en",
"name": "Trial Ending Soon",
"description": "Sent when a trial is about to end",
"category": EmailCategory.BILLING.value,
"is_platform_only": True,
"required_variables": json.dumps(["vendor_name", "days_remaining"]),
"variables": json.dumps([
"vendor_name", "tier_name", "days_remaining", "trial_end_date",
"upgrade_url", "features_list"
]),
"subject": "Your Trial Ends in {{ days_remaining }} Days",
"body_html": """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Your Trial is Ending Soon</h1>
</div>
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">Hi {{ vendor_name }},</p>
<p>Your {{ tier_name }} trial ends in <strong>{{ days_remaining }} days</strong> ({{ trial_end_date }}).</p>
<div style="background: #fffbeb; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #f59e0b;">
<h3 style="margin-top: 0; color: #d97706;">Don't lose these features:</h3>
<p style="margin: 5px 0;">{{ features_list }}</p>
</div>
<p>Subscribe now to continue using all {{ tier_name }} features without interruption:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ upgrade_url }}" style="background: #f59e0b; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">
Subscribe Now
</a>
</div>
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
Have questions? Reply to this email and we'll help you choose the right plan.
</p>
<p>Best regards,<br><strong>The Wizamart Team</strong></p>
</div>
</body>
</html>""",
"body_text": """Your Trial is Ending Soon
Hi {{ vendor_name }},
Your {{ tier_name }} trial ends in {{ days_remaining }} days ({{ trial_end_date }}).
Don't lose these features:
{{ features_list }}
Subscribe now to continue using all {{ tier_name }} features without interruption:
{{ upgrade_url }}
Have questions? Reply to this email and we'll help you choose the right plan.
Best regards,
The Wizamart Team
""",
},
{
"code": "team_invite",
"language": "en",
"name": "Team Member Invitation",
"description": "Sent when a vendor invites a team member",
"category": EmailCategory.SYSTEM.value,
"is_platform_only": False,
"required_variables": json.dumps(["invitee_name", "inviter_name", "vendor_name", "accept_url"]),
"variables": json.dumps([
"invitee_name", "inviter_name", "vendor_name", "role",
"accept_url", "expires_in_days"
]),
"subject": "{{ inviter_name }} invited you to join {{ vendor_name }} on Wizamart",
"body_html": """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">You've Been Invited!</h1>
</div>
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">Hi {{ invitee_name }},</p>
<p><strong>{{ inviter_name }}</strong> has invited you to join <strong>{{ vendor_name }}</strong> as a team member on Wizamart.</p>
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #6366f1;">
<h3 style="margin-top: 0; color: #6366f1;">Invitation Details</h3>
<p style="margin: 5px 0;"><strong>Vendor:</strong> {{ vendor_name }}</p>
<p style="margin: 5px 0;"><strong>Role:</strong> {{ role }}</p>
<p style="margin: 5px 0;"><strong>Invited by:</strong> {{ inviter_name }}</p>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ accept_url }}" style="background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">
Accept Invitation
</a>
</div>
<p style="color: #6b7280; font-size: 14px;">
This invitation will expire in {{ expires_in_days }} days.
</p>
<p style="color: #6b7280; font-size: 14px; margin-top: 20px;">
If you weren't expecting this invitation, you can safely ignore this email.
</p>
<p>Best regards,<br><strong>The Wizamart Team</strong></p>
</div>
</body>
</html>""",
"body_text": """You've Been Invited!
Hi {{ invitee_name }},
{{ inviter_name }} has invited you to join {{ vendor_name }} as a team member on Wizamart.
Invitation Details:
- Vendor: {{ vendor_name }}
- Role: {{ role }}
- Invited by: {{ inviter_name }}
Accept Invitation: {{ accept_url }}
This invitation will expire in {{ expires_in_days }} days.
If you weren't expecting this invitation, you can safely ignore this email.
Best regards,
The Wizamart Team
""",
},
]
@@ -910,6 +1267,10 @@ def seed_templates():
updated = 0
for template_data in TEMPLATES:
# Set defaults for new fields
template_data.setdefault("is_platform_only", False)
template_data.setdefault("required_variables", None)
# Check if template already exists
existing = (
db.query(EmailTemplate)
@@ -925,13 +1286,15 @@ def seed_templates():
for key, value in template_data.items():
setattr(existing, key, value)
updated += 1
print(f"Updated: {template_data['code']} ({template_data['language']})")
platform_only_tag = " [platform-only]" if template_data.get("is_platform_only") else ""
print(f"Updated: {template_data['code']} ({template_data['language']}){platform_only_tag}")
else:
# Create new template
template = EmailTemplate(**template_data)
db.add(template)
created += 1
print(f"Created: {template_data['code']} ({template_data['language']})")
platform_only_tag = " [platform-only]" if template_data.get("is_platform_only") else ""
print(f"Created: {template_data['code']} ({template_data['language']}){platform_only_tag}")
db.commit()
print(f"\nDone! Created: {created}, Updated: {updated}")