Some checks failed
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>
356 lines
10 KiB
Python
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"})
|