- JS-005: Add initialization guards to email-templates.js (admin/vendor) - JS-006: Add try/catch error handling to content-pages.js init - SVC-006: Move db.commit() from services to endpoints for proper transaction control in email_template_service and vendor_team_service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
292 lines
8.4 KiB
Python
292 lines
8.4 KiB
Python
# app/api/v1/vendor/email_templates.py
|
|
"""
|
|
Vendor email template override endpoints.
|
|
|
|
Allows vendors to customize platform email templates with their own content.
|
|
Platform-only templates (billing, subscription) cannot be overridden.
|
|
|
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
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.user import User
|
|
|
|
router = APIRouter(prefix="/email-templates")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# =============================================================================
|
|
# SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class VendorTemplateUpdate(BaseModel):
|
|
"""Schema for creating/updating a vendor template override."""
|
|
|
|
subject: str = Field(..., min_length=1, max_length=500)
|
|
body_html: str = Field(..., min_length=1)
|
|
body_text: str | None = None
|
|
name: str | None = Field(None, max_length=255)
|
|
|
|
|
|
class TemplatePreviewRequest(BaseModel):
|
|
"""Schema for previewing a template."""
|
|
|
|
language: str = "en"
|
|
variables: dict[str, Any] = {}
|
|
|
|
|
|
class TemplateTestRequest(BaseModel):
|
|
"""Schema for sending a test email."""
|
|
|
|
to_email: EmailStr
|
|
language: str = "en"
|
|
variables: dict[str, Any] = {}
|
|
|
|
|
|
# =============================================================================
|
|
# ENDPOINTS
|
|
# =============================================================================
|
|
|
|
|
|
@router.get("")
|
|
def list_overridable_templates(
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
List all email templates that the vendor can customize.
|
|
|
|
Returns platform templates with vendor override status.
|
|
Platform-only templates (billing, subscription) are excluded.
|
|
"""
|
|
vendor_id = current_user.token_vendor_id
|
|
service = EmailTemplateService(db)
|
|
return service.list_overridable_templates(vendor_id)
|
|
|
|
|
|
@router.get("/{code}")
|
|
def get_template(
|
|
code: str,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get a specific template with all language versions.
|
|
|
|
Returns platform template details and vendor overrides for each language.
|
|
"""
|
|
vendor_id = current_user.token_vendor_id
|
|
service = EmailTemplateService(db)
|
|
return service.get_vendor_template(vendor_id, code)
|
|
|
|
|
|
@router.get("/{code}/{language}")
|
|
def get_template_language(
|
|
code: str,
|
|
language: str,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get a specific template for a specific language.
|
|
|
|
Returns vendor override if exists, otherwise platform template.
|
|
"""
|
|
vendor_id = current_user.token_vendor_id
|
|
service = EmailTemplateService(db)
|
|
return service.get_vendor_template_language(vendor_id, code, language)
|
|
|
|
|
|
@router.put("/{code}/{language}")
|
|
def update_template_override(
|
|
code: str,
|
|
language: str,
|
|
template_data: VendorTemplateUpdate,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Create or update a vendor template override.
|
|
|
|
Creates a vendor-specific version of the email template.
|
|
The platform template remains unchanged.
|
|
"""
|
|
vendor_id = current_user.token_vendor_id
|
|
service = EmailTemplateService(db)
|
|
|
|
result = service.create_or_update_vendor_override(
|
|
vendor_id=vendor_id,
|
|
code=code,
|
|
language=language,
|
|
subject=template_data.subject,
|
|
body_html=template_data.body_html,
|
|
body_text=template_data.body_text,
|
|
name=template_data.name,
|
|
)
|
|
db.commit()
|
|
|
|
return result
|
|
|
|
|
|
@router.delete("/{code}/{language}")
|
|
def delete_template_override(
|
|
code: str,
|
|
language: str,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Delete a vendor template override.
|
|
|
|
Reverts to using the platform default template for this language.
|
|
"""
|
|
vendor_id = current_user.token_vendor_id
|
|
service = EmailTemplateService(db)
|
|
service.delete_vendor_override(vendor_id, code, language)
|
|
db.commit()
|
|
|
|
return {
|
|
"message": "Template override deleted - reverted to platform default",
|
|
"code": code,
|
|
"language": language,
|
|
}
|
|
|
|
|
|
@router.post("/{code}/preview")
|
|
def preview_template(
|
|
code: str,
|
|
preview_data: TemplatePreviewRequest,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Preview a template with sample variables.
|
|
|
|
Uses vendor override if exists, otherwise platform template.
|
|
"""
|
|
vendor_id = current_user.token_vendor_id
|
|
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
|
|
service = EmailTemplateService(db)
|
|
|
|
# Add branding variables
|
|
variables = {
|
|
**_get_sample_variables(code),
|
|
**preview_data.variables,
|
|
"platform_name": "Wizamart",
|
|
"vendor_name": vendor.name if vendor else "Your Store",
|
|
"support_email": vendor.contact_email if vendor else "support@wizamart.com",
|
|
}
|
|
|
|
return service.preview_vendor_template(
|
|
vendor_id=vendor_id,
|
|
code=code,
|
|
language=preview_data.language,
|
|
variables=variables,
|
|
)
|
|
|
|
|
|
@router.post("/{code}/test")
|
|
def send_test_email(
|
|
code: str,
|
|
test_data: TemplateTestRequest,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Send a test email using the template.
|
|
|
|
Uses vendor override if exists, otherwise platform template.
|
|
"""
|
|
vendor_id = current_user.token_vendor_id
|
|
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
|
|
|
|
# Build test variables
|
|
variables = {
|
|
**_get_sample_variables(code),
|
|
**test_data.variables,
|
|
"platform_name": "Wizamart",
|
|
"vendor_name": vendor.name if vendor else "Your Store",
|
|
"support_email": vendor.contact_email if vendor else "support@wizamart.com",
|
|
}
|
|
|
|
try:
|
|
email_svc = EmailService(db)
|
|
email_log = email_svc.send_template(
|
|
template_code=code,
|
|
to_email=test_data.to_email,
|
|
variables=variables,
|
|
vendor_id=vendor_id,
|
|
language=test_data.language,
|
|
)
|
|
|
|
if email_log.status == "sent":
|
|
return {
|
|
"success": True,
|
|
"message": f"Test email sent to {test_data.to_email}",
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"message": email_log.error_message or "Failed to send email",
|
|
}
|
|
except Exception as e:
|
|
logger.exception(f"Failed to send test email: {e}")
|
|
return {
|
|
"success": False,
|
|
"message": str(e),
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# HELPERS
|
|
# =============================================================================
|
|
|
|
|
|
def _get_sample_variables(template_code: str) -> dict[str, Any]:
|
|
"""Get sample variables for testing templates."""
|
|
samples = {
|
|
"signup_welcome": {
|
|
"first_name": "John",
|
|
"company_name": "Acme Corp",
|
|
"email": "john@example.com",
|
|
"vendor_code": "acme",
|
|
"login_url": "https://example.com/login",
|
|
"trial_days": "14",
|
|
"tier_name": "Business",
|
|
},
|
|
"order_confirmation": {
|
|
"customer_name": "Jane Doe",
|
|
"order_number": "ORD-12345",
|
|
"order_total": "€99.99",
|
|
"order_items_count": "3",
|
|
"order_date": "2024-01-15",
|
|
"shipping_address": "123 Main St, Luxembourg City, L-1234",
|
|
},
|
|
"password_reset": {
|
|
"customer_name": "John Doe",
|
|
"reset_link": "https://example.com/reset?token=abc123",
|
|
"expiry_hours": "1",
|
|
},
|
|
"team_invite": {
|
|
"invitee_name": "Jane",
|
|
"inviter_name": "John",
|
|
"vendor_name": "Acme Corp",
|
|
"role": "Admin",
|
|
"accept_url": "https://example.com/accept",
|
|
"expires_in_days": "7",
|
|
},
|
|
}
|
|
return samples.get(template_code, {})
|