226 lines
7.6 KiB
Python
226 lines
7.6 KiB
Python
|
|
"""
|
||
|
|
Email Templates API Router
|
||
|
|
Management of system and customer-specific email templates
|
||
|
|
"""
|
||
|
|
from typing import List, Optional, Dict, Any
|
||
|
|
from fastapi import APIRouter, HTTPException, Query
|
||
|
|
from pydantic import BaseModel, Json
|
||
|
|
from app.core.database import execute_query
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
router = APIRouter(prefix="/email-templates", tags=["Email Templates"])
|
||
|
|
|
||
|
|
# --- Models ---
|
||
|
|
|
||
|
|
class EmailTemplateBase(BaseModel):
|
||
|
|
name: str
|
||
|
|
slug: str
|
||
|
|
subject: str
|
||
|
|
body: str
|
||
|
|
category: str = "general"
|
||
|
|
description: Optional[str] = None
|
||
|
|
variables: Dict[str, str] = {}
|
||
|
|
customer_id: Optional[int] = None
|
||
|
|
|
||
|
|
class EmailTemplateCreate(EmailTemplateBase):
|
||
|
|
pass
|
||
|
|
|
||
|
|
class EmailTemplateUpdate(BaseModel):
|
||
|
|
name: Optional[str] = None
|
||
|
|
subject: Optional[str] = None
|
||
|
|
body: Optional[str] = None
|
||
|
|
category: Optional[str] = None
|
||
|
|
description: Optional[str] = None
|
||
|
|
variables: Optional[Dict[str, str]] = None
|
||
|
|
customer_id: Optional[int] = None
|
||
|
|
|
||
|
|
class EmailTemplate(BaseModel):
|
||
|
|
id: int
|
||
|
|
name: str
|
||
|
|
slug: str
|
||
|
|
subject: str
|
||
|
|
body: str
|
||
|
|
category: str
|
||
|
|
description: Optional[str] = None
|
||
|
|
variables: Optional[Dict[str, str]] = {}
|
||
|
|
is_system: bool
|
||
|
|
customer_id: Optional[int] = None
|
||
|
|
created_at: datetime
|
||
|
|
updated_at: datetime
|
||
|
|
|
||
|
|
# --- Endpoints ---
|
||
|
|
|
||
|
|
@router.get("/", response_model=List[EmailTemplate])
|
||
|
|
async def get_email_templates(
|
||
|
|
category: Optional[str] = None,
|
||
|
|
customer_id: Optional[int] = None
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Get all email templates.
|
||
|
|
Optionally filter by category or specific customer.
|
||
|
|
"""
|
||
|
|
sql = """
|
||
|
|
SELECT id, name, slug, subject, body, category, description, variables,
|
||
|
|
is_system, customer_id, created_at, updated_at
|
||
|
|
FROM email_templates
|
||
|
|
WHERE 1=1
|
||
|
|
"""
|
||
|
|
params = []
|
||
|
|
|
||
|
|
if category:
|
||
|
|
sql += " AND category = %s"
|
||
|
|
params.append(category)
|
||
|
|
|
||
|
|
if customer_id is not None:
|
||
|
|
# Fetch global templates (customer_id IS NULL) AND this specific customer's templates
|
||
|
|
sql += " AND (customer_id IS NULL OR customer_id = %s)"
|
||
|
|
params.append(customer_id)
|
||
|
|
|
||
|
|
sql += " ORDER BY category, name"
|
||
|
|
|
||
|
|
rows = execute_query(sql, tuple(params))
|
||
|
|
return rows
|
||
|
|
|
||
|
|
@router.get("/{template_id}", response_model=EmailTemplate)
|
||
|
|
async def get_email_template(template_id: int):
|
||
|
|
"""Get a single template by ID"""
|
||
|
|
sql = """
|
||
|
|
SELECT id, name, slug, subject, body, category, description, variables,
|
||
|
|
is_system, customer_id, created_at, updated_at
|
||
|
|
FROM email_templates
|
||
|
|
WHERE id = %s
|
||
|
|
"""
|
||
|
|
rows = execute_query(sql, (template_id,))
|
||
|
|
if not rows:
|
||
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
||
|
|
return rows[0]
|
||
|
|
|
||
|
|
@router.post("/", response_model=EmailTemplate)
|
||
|
|
async def create_email_template(template: EmailTemplateCreate):
|
||
|
|
"""Create a new email template"""
|
||
|
|
# Check for slug uniqueness
|
||
|
|
check_sql = "SELECT id FROM email_templates WHERE slug = %s AND (customer_id IS NULL OR customer_id = %s)"
|
||
|
|
check_val = (template.customer_id,) if template.customer_id else (None,)
|
||
|
|
# If customer_id is None, we check where customer_id IS NULL.
|
||
|
|
# Actually, SQL `slug = %s AND customer_id = %s` works if we handle NULL correctly in python -> SQL
|
||
|
|
|
||
|
|
# Simpler uniqueness check in python logic tailored to the constraint
|
||
|
|
if template.customer_id:
|
||
|
|
existing = execute_query(
|
||
|
|
"SELECT id FROM email_templates WHERE slug = %s AND customer_id = %s",
|
||
|
|
(template.slug, template.customer_id)
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
existing = execute_query(
|
||
|
|
"SELECT id FROM email_templates WHERE slug = %s AND customer_id IS NULL",
|
||
|
|
(template.slug,)
|
||
|
|
)
|
||
|
|
|
||
|
|
if existing:
|
||
|
|
raise HTTPException(status_code=400, detail=f"A template with slug '{template.slug}' already exists for this context.")
|
||
|
|
|
||
|
|
sql = """
|
||
|
|
INSERT INTO email_templates
|
||
|
|
(name, slug, subject, body, category, description, variables, customer_id, is_system)
|
||
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, FALSE)
|
||
|
|
RETURNING id, name, slug, subject, body, category, description, variables,
|
||
|
|
is_system, customer_id, created_at, updated_at
|
||
|
|
"""
|
||
|
|
|
||
|
|
params = (
|
||
|
|
template.name,
|
||
|
|
template.slug,
|
||
|
|
template.subject,
|
||
|
|
template.body,
|
||
|
|
template.category,
|
||
|
|
template.description,
|
||
|
|
json.dumps(template.variables),
|
||
|
|
template.customer_id
|
||
|
|
)
|
||
|
|
|
||
|
|
try:
|
||
|
|
rows = execute_query(sql, params)
|
||
|
|
return rows[0]
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error creating template: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
@router.put("/{template_id}", response_model=EmailTemplate)
|
||
|
|
async def update_email_template(template_id: int, update: EmailTemplateUpdate):
|
||
|
|
"""Update an existing template"""
|
||
|
|
# Verify existence
|
||
|
|
current = execute_query("SELECT is_system FROM email_templates WHERE id = %s", (template_id,))
|
||
|
|
if not current:
|
||
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
||
|
|
|
||
|
|
is_system = current[0]['is_system']
|
||
|
|
|
||
|
|
# Prevent changing critical fields on system templates?
|
||
|
|
# Usually we allow editing subject/body, but maybe not slug.
|
||
|
|
# For now, we allow everything except slug changes on system templates if defined so,
|
||
|
|
# but the logic below updates whatever is provided.
|
||
|
|
|
||
|
|
fields = []
|
||
|
|
params = []
|
||
|
|
|
||
|
|
if update.name is not None:
|
||
|
|
fields.append("name = %s")
|
||
|
|
params.append(update.name)
|
||
|
|
|
||
|
|
if update.subject is not None:
|
||
|
|
fields.append("subject = %s")
|
||
|
|
params.append(update.subject)
|
||
|
|
|
||
|
|
if update.body is not None:
|
||
|
|
fields.append("body = %s")
|
||
|
|
params.append(update.body)
|
||
|
|
|
||
|
|
if update.category is not None:
|
||
|
|
fields.append("category = %s")
|
||
|
|
params.append(update.category)
|
||
|
|
|
||
|
|
if update.description is not None:
|
||
|
|
fields.append("description = %s")
|
||
|
|
params.append(update.description)
|
||
|
|
|
||
|
|
if update.variables is not None:
|
||
|
|
fields.append("variables = %s")
|
||
|
|
params.append(json.dumps(update.variables))
|
||
|
|
|
||
|
|
if update.customer_id is not None:
|
||
|
|
fields.append("customer_id = %s")
|
||
|
|
params.append(update.customer_id)
|
||
|
|
|
||
|
|
# Don't change slug if it's a system template?
|
||
|
|
# Usually slugs are fixed for system templates so the code can find them.
|
||
|
|
# But for now we don't expose slug update in the provided model if we want to be strict.
|
||
|
|
# Wait, the frontend might need to update other things.
|
||
|
|
# Let's assume we don't update slug for now via this endpoint as per my minimalist implementation.
|
||
|
|
|
||
|
|
if not fields:
|
||
|
|
raise HTTPException(status_code=400, detail="No fields to update")
|
||
|
|
|
||
|
|
fields.append("updated_at = NOW()")
|
||
|
|
|
||
|
|
sql = f"UPDATE email_templates SET {', '.join(fields)} WHERE id = %s RETURNING *"
|
||
|
|
params.append(template_id)
|
||
|
|
|
||
|
|
rows = execute_query(sql, tuple(params))
|
||
|
|
return rows[0]
|
||
|
|
|
||
|
|
@router.delete("/{template_id}")
|
||
|
|
async def delete_email_template(template_id: int):
|
||
|
|
"""Delete a template (Non-system only)"""
|
||
|
|
current = execute_query("SELECT is_system FROM email_templates WHERE id = %s", (template_id,))
|
||
|
|
if not current:
|
||
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
||
|
|
|
||
|
|
if current[0]['is_system']:
|
||
|
|
raise HTTPException(status_code=403, detail="Cannot delete system templates")
|
||
|
|
|
||
|
|
execute_query("DELETE FROM email_templates WHERE id = %s", (template_id,))
|
||
|
|
return {"message": "Template deleted successfully"}
|