Files
orion/app/modules/messaging/routes/api/admin_email_templates.py
Samir Boulahtit 4aa6f76e46
Some checks failed
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff (push) Successful in 10s
refactor(arch): move auth schemas to tenancy module and add cross-module service methods
Move all auth schemas (UserContext, UserLogin, LoginResponse, etc.) from
legacy models/schema/auth.py to app/modules/tenancy/schemas/auth.py per
MOD-019. Update 84 import sites across 14 modules. Legacy file now
re-exports for backwards compatibility.

Add missing tenancy service methods for cross-module consumers:
- merchant_service.get_merchant_by_owner_id()
- merchant_service.get_merchant_count_for_owner()
- admin_service.get_user_by_id() (public, was private-only)
- platform_service.get_active_store_count()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:57:04 +01:00

356 lines
10 KiB
Python

# app/modules/messaging/routes/api/admin_email_templates.py
"""
Admin email template management endpoints.
Allows platform administrators to:
- View all email templates
- Edit template content for all languages
- Preview templates with sample data
- Send test emails
- View email logs
"""
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_admin_api
from app.core.database import get_db
from app.modules.messaging.services.email_service import EmailService
from app.modules.messaging.services.email_template_service import EmailTemplateService
from app.modules.tenancy.schemas.auth import UserContext
admin_email_templates_router = APIRouter(prefix="/email-templates")
logger = logging.getLogger(__name__)
# =============================================================================
# SCHEMAS
# =============================================================================
class TemplateUpdate(BaseModel):
"""Schema for updating a platform template."""
subject: str = Field(..., min_length=1, max_length=500)
body_html: str = Field(..., min_length=1)
body_text: str | None = None
class PreviewRequest(BaseModel):
"""Schema for previewing a template."""
template_code: str
language: str = "en"
variables: dict[str, Any] = {}
class TestEmailRequest(BaseModel):
"""Schema for sending a test email."""
template_code: str
language: str = "en"
to_email: EmailStr
variables: dict[str, Any] = {}
class TemplateListItem(BaseModel):
"""Schema for a template in the list."""
code: str
name: str
description: str | None = None
category: str
languages: list[str] # Matches service output field name
is_platform_only: bool = False
variables: list[str] = []
class Config:
from_attributes = True
class TemplateListResponse(BaseModel):
"""Response schema for listing templates."""
templates: list[TemplateListItem]
class CategoriesResponse(BaseModel):
"""Response schema for template categories."""
categories: list[str]
# =============================================================================
# ENDPOINTS
# =============================================================================
@admin_email_templates_router.get("", response_model=TemplateListResponse)
def list_templates(
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
List all platform email templates.
Returns templates grouped by code with available languages.
"""
service = EmailTemplateService(db)
return TemplateListResponse(templates=service.list_platform_templates())
@admin_email_templates_router.get("/categories", response_model=CategoriesResponse)
def get_categories(
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""Get list of email template categories."""
service = EmailTemplateService(db)
return CategoriesResponse(categories=service.get_template_categories())
@admin_email_templates_router.get("/{code}")
def get_template(
code: str,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Get a specific template with all language versions.
Returns template metadata and content for all available languages.
"""
service = EmailTemplateService(db)
return service.get_platform_template(code)
@admin_email_templates_router.get("/{code}/{language}")
def get_template_language(
code: str,
language: str,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Get a specific template for a specific language.
Returns template content with variables information.
"""
service = EmailTemplateService(db)
template = service.get_platform_template_language(code, language)
return {
"code": template.code,
"language": template.language,
"name": template.name,
"description": template.description,
"category": template.category,
"subject": template.subject,
"body_html": template.body_html,
"body_text": template.body_text,
"variables": template.variables,
"required_variables": template.required_variables,
"is_platform_only": template.is_platform_only,
}
@admin_email_templates_router.put("/{code}/{language}")
def update_template(
code: str,
language: str,
template_data: TemplateUpdate,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Update a platform email template.
Updates the template content for a specific language.
"""
service = EmailTemplateService(db)
service.update_platform_template(
code=code,
language=language,
subject=template_data.subject,
body_html=template_data.body_html,
body_text=template_data.body_text,
)
db.commit()
return {"message": "Template updated successfully"}
@admin_email_templates_router.post("/{code}/preview")
def preview_template(
code: str,
preview_data: PreviewRequest,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Preview a template with sample variables.
Renders the template with provided variables and returns the result.
"""
service = EmailTemplateService(db)
# Merge with sample variables if not provided
variables = {
**_get_sample_variables(code),
**preview_data.variables,
}
return service.preview_template(code, preview_data.language, variables)
@admin_email_templates_router.post("/{code}/test")
def send_test_email(
code: str,
test_data: TestEmailRequest,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Send a test email using the template.
Sends the template to the specified email address with sample data.
"""
# Merge with sample variables
variables = {
**_get_sample_variables(code),
**test_data.variables,
}
try:
email_svc = EmailService(db)
email_log = email_svc.send_template(
template_code=code,
to_email=test_data.to_email,
variables=variables,
language=test_data.language,
)
if email_log.status == "sent":
return {
"success": True,
"message": f"Test email sent to {test_data.to_email}",
}
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),
}
@admin_email_templates_router.get("/{code}/logs")
def get_template_logs(
code: str,
limit: int = 50,
offset: int = 0,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Get email logs for a specific template.
Returns recent email send attempts for the template.
"""
service = EmailTemplateService(db)
logs, total = service.get_template_logs(code, limit, offset)
return {
"logs": logs,
"total": total,
"limit": limit,
"offset": offset,
}
# =============================================================================
# HELPERS
# =============================================================================
def _get_sample_variables(template_code: str) -> dict[str, Any]:
"""Get sample variables for testing templates."""
samples = {
"signup_welcome": {
"first_name": "John",
"merchant_name": "Acme Corp",
"email": "john@example.com",
"store_code": "acme",
"login_url": "https://example.com/login",
"trial_days": "14",
"tier_name": "Business",
"platform_name": "Orion",
},
"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",
"platform_name": "Orion",
},
"password_reset": {
"customer_name": "John Doe",
"reset_link": "https://example.com/reset?token=abc123",
"expiry_hours": "1",
"platform_name": "Orion",
},
"team_invite": {
"invitee_name": "Jane",
"inviter_name": "John",
"store_name": "Acme Corp",
"role": "Admin",
"accept_url": "https://example.com/accept",
"expires_in_days": "7",
"platform_name": "Orion",
},
"subscription_welcome": {
"store_name": "Acme Corp",
"tier_name": "Business",
"billing_cycle": "Monthly",
"amount": "€49.99",
"next_billing_date": "2024-02-15",
"dashboard_url": "https://example.com/dashboard",
"platform_name": "Orion",
},
"payment_failed": {
"store_name": "Acme Corp",
"tier_name": "Business",
"amount": "€49.99",
"retry_date": "2024-01-18",
"update_payment_url": "https://example.com/billing",
"support_email": "support@orion.lu",
"platform_name": "Orion",
},
"subscription_cancelled": {
"store_name": "Acme Corp",
"tier_name": "Business",
"end_date": "2024-02-15",
"reactivate_url": "https://example.com/billing",
"platform_name": "Orion",
},
"trial_ending": {
"store_name": "Acme Corp",
"tier_name": "Business",
"days_remaining": "3",
"trial_end_date": "2024-01-18",
"upgrade_url": "https://example.com/upgrade",
"features_list": "Unlimited products, API access, Priority support",
"platform_name": "Orion",
},
}
return samples.get(template_code, {"platform_name": "Orion"})