Files
orion/app/api/v1/vendor/email_templates.py
Samir Boulahtit 6df7167f80 fix: resolve JS-005, JS-006, SVC-006 architecture violations
- 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>
2026-01-03 20:00:10 +01:00

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, {})