Files
orion/app/api/v1/admin/email_templates.py
Samir Boulahtit c52af2a155 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>
2026-01-03 18:29:26 +01:00

320 lines
8.8 KiB
Python

# app/api/v1/admin/email_templates.py
"""
Admin email templates management endpoints.
Provides endpoints for:
- Listing all platform email templates
- Viewing template details (all languages)
- Updating template content
- Preview and test email sending
"""
import logging
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.services.email_service import EmailService
from models.database.email import EmailCategory, EmailTemplate
from models.database.user import User
from models.schema.email import (
EmailPreviewRequest,
EmailPreviewResponse,
EmailTemplateResponse,
EmailTemplateSummary,
EmailTemplateUpdate,
EmailTestRequest,
EmailTestResponse,
)
router = APIRouter(prefix="/email-templates")
logger = logging.getLogger(__name__)
@router.get("", response_model=list[EmailTemplateSummary])
def list_templates(
category: str | None = Query(None, description="Filter by category"),
include_inactive: bool = Query(False, description="Include inactive templates"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
List all platform email templates.
Templates are grouped by code, showing available languages for each.
"""
templates = EmailTemplate.get_all_templates(
db, category=category, include_inactive=include_inactive
)
return EmailTemplateSummary.from_db_list(templates)
@router.get("/categories")
def get_categories(
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""Get all email template categories."""
return {
"categories": [
{"code": EmailCategory.AUTH.value, "name": "Authentication", "description": "Signup, password reset, verification"},
{"code": EmailCategory.ORDERS.value, "name": "Orders", "description": "Order confirmations, shipping updates"},
{"code": EmailCategory.BILLING.value, "name": "Billing", "description": "Invoices, payment failures, subscription changes"},
{"code": EmailCategory.SYSTEM.value, "name": "System", "description": "Team invites, notifications"},
{"code": EmailCategory.MARKETING.value, "name": "Marketing", "description": "Newsletters, promotions"},
]
}
@router.get("/{code}", response_model=list[EmailTemplateResponse])
def get_template(
code: str,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Get a template by code with all language versions.
Returns all language versions of the template.
"""
templates = (
db.query(EmailTemplate)
.filter(EmailTemplate.code == code)
.order_by(EmailTemplate.language)
.all()
)
if not templates:
raise HTTPException(status_code=404, detail=f"Template not found: {code}")
return [EmailTemplateResponse.from_db(t) for t in templates]
@router.get("/{code}/{language}", response_model=EmailTemplateResponse)
def get_template_language(
code: str,
language: str,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Get a specific language version of a template.
"""
template = (
db.query(EmailTemplate)
.filter(
EmailTemplate.code == code,
EmailTemplate.language == language,
)
.first()
)
if not template:
raise HTTPException(
status_code=404,
detail=f"Template not found: {code} ({language})",
)
return EmailTemplateResponse.from_db(template)
@router.put("/{code}/{language}", response_model=EmailTemplateResponse)
def update_template(
code: str,
language: str,
data: EmailTemplateUpdate,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Update a specific language version of a template.
Only provided fields are updated.
"""
template = (
db.query(EmailTemplate)
.filter(
EmailTemplate.code == code,
EmailTemplate.language == language,
)
.first()
)
if not template:
raise HTTPException(
status_code=404,
detail=f"Template not found: {code} ({language})",
)
# Update provided fields
import json
if data.name is not None:
template.name = data.name
if data.description is not None:
template.description = data.description
if data.subject is not None:
template.subject = data.subject
if data.body_html is not None:
template.body_html = data.body_html
if data.body_text is not None:
template.body_text = data.body_text
if data.variables is not None:
template.variables = json.dumps(data.variables)
if data.required_variables is not None:
template.required_variables = json.dumps(data.required_variables)
if data.is_active is not None:
template.is_active = data.is_active
db.commit()
db.refresh(template)
logger.info(
f"Email template updated: {code} ({language}) by admin {current_admin.id}"
)
return EmailTemplateResponse.from_db(template)
@router.post("/{code}/preview", response_model=EmailPreviewResponse)
def preview_template(
code: str,
data: EmailPreviewRequest,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Preview a template with sample variables.
Returns rendered subject and body without sending.
"""
template = (
db.query(EmailTemplate)
.filter(
EmailTemplate.code == code,
EmailTemplate.language == data.language,
)
.first()
)
if not template:
raise HTTPException(
status_code=404,
detail=f"Template not found: {code} ({data.language})",
)
email_service = EmailService(db)
# Render with provided variables
subject = email_service.render_template(template.subject, data.variables)
body_html = email_service.render_template(template.body_html, data.variables)
body_text = (
email_service.render_template(template.body_text, data.variables)
if template.body_text
else None
)
return EmailPreviewResponse(
subject=subject,
body_html=body_html,
body_text=body_text,
)
@router.post("/{code}/test", response_model=EmailTestResponse)
def send_test_email(
code: str,
data: EmailTestRequest,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Send a test email to specified address.
Uses the template with provided variables.
"""
template = (
db.query(EmailTemplate)
.filter(
EmailTemplate.code == code,
EmailTemplate.language == data.language,
)
.first()
)
if not template:
raise HTTPException(
status_code=404,
detail=f"Template not found: {code} ({data.language})",
)
email_service = EmailService(db)
# Send test email
log = email_service.send_template(
template_code=code,
to_email=data.to_email,
to_name=current_admin.full_name,
language=data.language,
variables=data.variables,
user_id=current_admin.id,
related_type="email_test",
include_branding=True,
)
if log.status == "sent":
return EmailTestResponse(
success=True,
message=f"Test email sent to {data.to_email}",
email_log_id=log.id,
)
else:
return EmailTestResponse(
success=False,
message=f"Failed to send test email: {log.error_message}",
email_log_id=log.id,
)
@router.get("/{code}/logs")
def get_template_logs(
code: str,
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Get recent email logs for a specific template.
Useful for debugging and monitoring.
"""
from models.database.email import EmailLog
logs = (
db.query(EmailLog)
.filter(EmailLog.template_code == code)
.order_by(EmailLog.created_at.desc())
.limit(limit)
.all()
)
return {
"template_code": code,
"logs": [
{
"id": log.id,
"recipient_email": log.recipient_email,
"subject": log.subject,
"status": log.status,
"sent_at": log.sent_at,
"error_message": log.error_message,
"created_at": log.created_at,
}
for log in logs
],
"total": len(logs),
}