refactor: move email template logic to service layer
Create EmailTemplateService to follow layered architecture: - Move database queries from API endpoints to service - Move business logic (validation, resolution) to service - Reduce architecture violations from 62 to 29 errors Files: - app/services/email_template_service.py (new) - app/api/v1/admin/email_templates.py (refactored) - app/api/v1/vendor/email_templates.py (refactored) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
336
app/api/v1/vendor/email_templates.py
vendored
336
app/api/v1/vendor/email_templates.py
vendored
@@ -11,24 +11,24 @@ Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pa
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from jinja2 import Template
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.services.email_service import EmailService
|
||||
from app.services.email_template_service import EmailTemplateService
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.database.email import EmailTemplate
|
||||
from models.database.user import User
|
||||
from models.database.vendor_email_template import VendorEmailTemplate
|
||||
|
||||
router = APIRouter(prefix="/email-templates")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Supported languages
|
||||
SUPPORTED_LANGUAGES = ["en", "fr", "de", "lb"]
|
||||
|
||||
# =============================================================================
|
||||
# SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class VendorTemplateUpdate(BaseModel):
|
||||
@@ -55,6 +55,11 @@ class TemplateTestRequest(BaseModel):
|
||||
variables: dict[str, Any] = {}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ENDPOINTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_overridable_templates(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
@@ -67,43 +72,8 @@ def list_overridable_templates(
|
||||
Platform-only templates (billing, subscription) are excluded.
|
||||
"""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Get all overridable platform templates
|
||||
platform_templates = EmailTemplate.get_overridable_templates(db)
|
||||
|
||||
# Get all vendor overrides
|
||||
vendor_overrides = VendorEmailTemplate.get_all_overrides_for_vendor(db, vendor_id)
|
||||
|
||||
# Build override lookup
|
||||
override_lookup = {}
|
||||
for override in vendor_overrides:
|
||||
key = (override.template_code, override.language)
|
||||
override_lookup[key] = override
|
||||
|
||||
# Build response
|
||||
templates_response = []
|
||||
for template in platform_templates:
|
||||
# Check which languages have overrides
|
||||
override_languages = []
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
if (template.code, lang) in override_lookup:
|
||||
override_languages.append(lang)
|
||||
|
||||
templates_response.append({
|
||||
"code": template.code,
|
||||
"name": template.name,
|
||||
"category": template.category,
|
||||
"description": template.description,
|
||||
"available_languages": _get_available_languages(db, template.code),
|
||||
"override_languages": override_languages,
|
||||
"has_override": len(override_languages) > 0,
|
||||
"variables": template.required_variables.split(",") if template.required_variables else [],
|
||||
})
|
||||
|
||||
return {
|
||||
"templates": templates_response,
|
||||
"supported_languages": SUPPORTED_LANGUAGES,
|
||||
}
|
||||
service = EmailTemplateService(db)
|
||||
return service.list_overridable_templates(vendor_id)
|
||||
|
||||
|
||||
@router.get("/{code}")
|
||||
@@ -118,70 +88,8 @@ def get_template(
|
||||
Returns platform template details and vendor overrides for each language.
|
||||
"""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Get platform template
|
||||
platform_template = db.query(EmailTemplate).filter(
|
||||
EmailTemplate.code == code
|
||||
).first()
|
||||
|
||||
if not platform_template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
if platform_template.is_platform_only:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This is a platform-only template and cannot be customized"
|
||||
)
|
||||
|
||||
# Get all language versions of platform template
|
||||
platform_versions = db.query(EmailTemplate).filter(
|
||||
EmailTemplate.code == code
|
||||
).all()
|
||||
|
||||
# Get vendor overrides for all languages
|
||||
vendor_overrides = (
|
||||
db.query(VendorEmailTemplate)
|
||||
.filter(
|
||||
VendorEmailTemplate.vendor_id == vendor_id,
|
||||
VendorEmailTemplate.template_code == code,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
override_lookup = {v.language: v for v in vendor_overrides}
|
||||
platform_lookup = {t.language: t for t in platform_versions}
|
||||
|
||||
# Build language versions
|
||||
languages = {}
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
platform_ver = platform_lookup.get(lang)
|
||||
override_ver = override_lookup.get(lang)
|
||||
|
||||
languages[lang] = {
|
||||
"has_platform_template": platform_ver is not None,
|
||||
"has_vendor_override": override_ver is not None,
|
||||
"platform": {
|
||||
"subject": platform_ver.subject if platform_ver else None,
|
||||
"body_html": platform_ver.body_html if platform_ver else None,
|
||||
"body_text": platform_ver.body_text if platform_ver else None,
|
||||
} if platform_ver else None,
|
||||
"vendor_override": {
|
||||
"subject": override_ver.subject if override_ver else None,
|
||||
"body_html": override_ver.body_html if override_ver else None,
|
||||
"body_text": override_ver.body_text if override_ver else None,
|
||||
"name": override_ver.name if override_ver else None,
|
||||
"updated_at": override_ver.updated_at.isoformat() if override_ver else None,
|
||||
} if override_ver else None,
|
||||
}
|
||||
|
||||
return {
|
||||
"code": code,
|
||||
"name": platform_template.name,
|
||||
"category": platform_template.category,
|
||||
"description": platform_template.description,
|
||||
"variables": platform_template.required_variables.split(",") if platform_template.required_variables else [],
|
||||
"languages": languages,
|
||||
}
|
||||
service = EmailTemplateService(db)
|
||||
return service.get_vendor_template(vendor_id, code)
|
||||
|
||||
|
||||
@router.get("/{code}/{language}")
|
||||
@@ -196,63 +104,9 @@ def get_template_language(
|
||||
|
||||
Returns vendor override if exists, otherwise platform template.
|
||||
"""
|
||||
if language not in SUPPORTED_LANGUAGES:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported language: {language}")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Check if template is overridable
|
||||
platform_template = db.query(EmailTemplate).filter(
|
||||
EmailTemplate.code == code
|
||||
).first()
|
||||
|
||||
if not platform_template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
if platform_template.is_platform_only:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This is a platform-only template and cannot be customized"
|
||||
)
|
||||
|
||||
# Check for vendor override
|
||||
vendor_override = VendorEmailTemplate.get_override(db, vendor_id, code, language)
|
||||
|
||||
# Get platform version for this language
|
||||
platform_version = EmailTemplate.get_by_code_and_language(db, code, language)
|
||||
|
||||
if vendor_override:
|
||||
return {
|
||||
"code": code,
|
||||
"language": language,
|
||||
"source": "vendor_override",
|
||||
"subject": vendor_override.subject,
|
||||
"body_html": vendor_override.body_html,
|
||||
"body_text": vendor_override.body_text,
|
||||
"name": vendor_override.name,
|
||||
"variables": platform_template.required_variables.split(",") if platform_template.required_variables else [],
|
||||
"platform_template": {
|
||||
"subject": platform_version.subject if platform_version else None,
|
||||
"body_html": platform_version.body_html if platform_version else None,
|
||||
} if platform_version else None,
|
||||
}
|
||||
elif platform_version:
|
||||
return {
|
||||
"code": code,
|
||||
"language": language,
|
||||
"source": "platform",
|
||||
"subject": platform_version.subject,
|
||||
"body_html": platform_version.body_html,
|
||||
"body_text": platform_version.body_text,
|
||||
"name": platform_version.name,
|
||||
"variables": platform_template.required_variables.split(",") if platform_template.required_variables else [],
|
||||
"platform_template": None,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No template found for language: {language}"
|
||||
)
|
||||
service = EmailTemplateService(db)
|
||||
return service.get_vendor_template_language(vendor_id, code, language)
|
||||
|
||||
|
||||
@router.put("/{code}/{language}")
|
||||
@@ -269,42 +123,12 @@ def update_template_override(
|
||||
Creates a vendor-specific version of the email template.
|
||||
The platform template remains unchanged.
|
||||
"""
|
||||
if language not in SUPPORTED_LANGUAGES:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported language: {language}")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
service = EmailTemplateService(db)
|
||||
|
||||
# Check if template exists and is overridable
|
||||
platform_template = db.query(EmailTemplate).filter(
|
||||
EmailTemplate.code == code
|
||||
).first()
|
||||
|
||||
if not platform_template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
if platform_template.is_platform_only:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This is a platform-only template and cannot be customized"
|
||||
)
|
||||
|
||||
# Validate template content (try to render with dummy variables)
|
||||
try:
|
||||
Template(template_data.subject).render({})
|
||||
Template(template_data.body_html).render({})
|
||||
if template_data.body_text:
|
||||
Template(template_data.body_text).render({})
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid template syntax: {str(e)}"
|
||||
)
|
||||
|
||||
# Create or update override
|
||||
override = VendorEmailTemplate.create_or_update(
|
||||
db=db,
|
||||
return service.create_or_update_vendor_override(
|
||||
vendor_id=vendor_id,
|
||||
template_code=code,
|
||||
code=code,
|
||||
language=language,
|
||||
subject=template_data.subject,
|
||||
body_html=template_data.body_html,
|
||||
@@ -312,19 +136,6 @@ def update_template_override(
|
||||
name=template_data.name,
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"Vendor {vendor_id} updated email template override: {code}/{language}"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Template override saved",
|
||||
"code": code,
|
||||
"language": language,
|
||||
"is_new": override.created_at == override.updated_at,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{code}/{language}")
|
||||
def delete_template_override(
|
||||
@@ -338,24 +149,9 @@ def delete_template_override(
|
||||
|
||||
Reverts to using the platform default template for this language.
|
||||
"""
|
||||
if language not in SUPPORTED_LANGUAGES:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported language: {language}")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
deleted = VendorEmailTemplate.delete_override(db, vendor_id, code, language)
|
||||
|
||||
if not deleted:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="No override found for this template and language"
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"Vendor {vendor_id} deleted email template override: {code}/{language}"
|
||||
)
|
||||
service = EmailTemplateService(db)
|
||||
service.delete_vendor_override(vendor_id, code, language)
|
||||
|
||||
return {
|
||||
"message": "Template override deleted - reverted to platform default",
|
||||
@@ -378,65 +174,23 @@ def preview_template(
|
||||
"""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
|
||||
|
||||
# Check if template exists
|
||||
platform_template = db.query(EmailTemplate).filter(
|
||||
EmailTemplate.code == code
|
||||
).first()
|
||||
|
||||
if not platform_template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
# Get template content (vendor override or platform)
|
||||
vendor_override = VendorEmailTemplate.get_override(
|
||||
db, vendor_id, code, preview_data.language
|
||||
)
|
||||
platform_version = EmailTemplate.get_by_code_and_language(
|
||||
db, code, preview_data.language
|
||||
)
|
||||
|
||||
if vendor_override:
|
||||
subject = vendor_override.subject
|
||||
body_html = vendor_override.body_html
|
||||
body_text = vendor_override.body_text
|
||||
source = "vendor_override"
|
||||
elif platform_version:
|
||||
subject = platform_version.subject
|
||||
body_html = platform_version.body_html
|
||||
body_text = platform_version.body_text
|
||||
source = "platform"
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No template found for language: {preview_data.language}"
|
||||
)
|
||||
service = EmailTemplateService(db)
|
||||
|
||||
# Add branding variables
|
||||
variables = {
|
||||
**_get_sample_variables(code),
|
||||
**preview_data.variables,
|
||||
"platform_name": "Wizamart",
|
||||
"vendor_name": vendor.name,
|
||||
"support_email": vendor.contact_email or "support@wizamart.com",
|
||||
"vendor_name": vendor.name if vendor else "Your Store",
|
||||
"support_email": vendor.contact_email if vendor else "support@wizamart.com",
|
||||
}
|
||||
|
||||
# Render templates
|
||||
try:
|
||||
rendered_subject = Template(subject).render(variables)
|
||||
rendered_html = Template(body_html).render(variables)
|
||||
rendered_text = Template(body_text).render(variables) if body_text else None
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Template rendering error: {str(e)}"
|
||||
)
|
||||
|
||||
return {
|
||||
"source": source,
|
||||
"language": preview_data.language,
|
||||
"subject": rendered_subject,
|
||||
"body_html": rendered_html,
|
||||
"body_text": rendered_text,
|
||||
}
|
||||
return service.preview_vendor_template(
|
||||
vendor_id=vendor_id,
|
||||
code=code,
|
||||
language=preview_data.language,
|
||||
variables=variables,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{code}/test")
|
||||
@@ -454,25 +208,16 @@ def send_test_email(
|
||||
vendor_id = current_user.token_vendor_id
|
||||
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
|
||||
|
||||
# Check if template exists
|
||||
platform_template = db.query(EmailTemplate).filter(
|
||||
EmailTemplate.code == code
|
||||
).first()
|
||||
|
||||
if not platform_template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
# Build test variables
|
||||
variables = {
|
||||
**_get_sample_variables(code),
|
||||
**test_data.variables,
|
||||
"platform_name": "Wizamart",
|
||||
"vendor_name": vendor.name,
|
||||
"support_email": vendor.contact_email or "support@wizamart.com",
|
||||
"vendor_name": vendor.name if vendor else "Your Store",
|
||||
"support_email": vendor.contact_email if vendor else "support@wizamart.com",
|
||||
}
|
||||
|
||||
try:
|
||||
# Send using email service (will use vendor override if exists)
|
||||
email_svc = EmailService(db)
|
||||
email_log = email_svc.send_template(
|
||||
template_code=code,
|
||||
@@ -500,12 +245,9 @@ def send_test_email(
|
||||
}
|
||||
|
||||
|
||||
def _get_available_languages(db: Session, code: str) -> list[str]:
|
||||
"""Get list of languages that have platform templates."""
|
||||
templates = db.query(EmailTemplate.language).filter(
|
||||
EmailTemplate.code == code
|
||||
).all()
|
||||
return [t.language for t in templates]
|
||||
# =============================================================================
|
||||
# HELPERS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _get_sample_variables(template_code: str) -> dict[str, Any]:
|
||||
@@ -523,7 +265,7 @@ def _get_sample_variables(template_code: str) -> dict[str, Any]:
|
||||
"order_confirmation": {
|
||||
"customer_name": "Jane Doe",
|
||||
"order_number": "ORD-12345",
|
||||
"order_total": "99.99",
|
||||
"order_total": "€99.99",
|
||||
"order_items_count": "3",
|
||||
"order_date": "2024-01-15",
|
||||
"shipping_address": "123 Main St, Luxembourg City, L-1234",
|
||||
@@ -542,4 +284,4 @@ def _get_sample_variables(template_code: str) -> dict[str, Any]:
|
||||
"expires_in_days": "7",
|
||||
},
|
||||
}
|
||||
return samples.get(template_code, {"platform_name": "Wizamart"})
|
||||
return samples.get(template_code, {})
|
||||
|
||||
Reference in New Issue
Block a user