Files
orion/app/modules/messaging/schemas/email.py
Samir Boulahtit ce822af883
Some checks failed
CI / ruff (push) Successful in 9s
CI / pytest (push) Failing after 47m32s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat: production launch — email audit, team invites, security headers, router fixes
- Fix loyalty & monitoring router bugs (_get_router → named routers)
- Implement team invitation email with send_template + seed templates (en/fr/de)
- Add SecurityHeadersMiddleware (nosniff, HSTS, referrer-policy, permissions-policy)
- Build email audit admin page: service, schemas, API, page route, menu, i18n, HTML, JS
- Clean stale TODO in platform-menu-config.js
- Add 67 tests (unit + integration) covering all new functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:24:30 +01:00

319 lines
9.7 KiB
Python

# app/modules/messaging/schemas/email.py
"""
Email template Pydantic schemas for API responses and requests.
Provides schemas for:
- EmailTemplate: Platform email templates
- StoreEmailTemplate: Store-specific template overrides
"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class EmailTemplateBase(BaseModel):
"""Base schema for email templates."""
code: str = Field(..., description="Template code (e.g., 'password_reset')")
language: str = Field(default="en", description="Language code (en, fr, de, lb)")
name: str = Field(..., description="Human-readable template name")
description: str | None = Field(None, description="Template description")
category: str = Field(..., description="Template category (auth, orders, billing, etc.)")
subject: str = Field(..., description="Email subject (supports Jinja2 variables)")
body_html: str = Field(..., description="HTML email body")
body_text: str | None = Field(None, description="Plain text fallback")
variables: list[str] = Field(default_factory=list, description="Available variables")
class EmailTemplateCreate(EmailTemplateBase):
"""Schema for creating an email template."""
required_variables: list[str] = Field(
default_factory=list, description="Required variables"
)
is_platform_only: bool = Field(
default=False, description="Cannot be overridden by stores"
)
class EmailTemplateUpdate(BaseModel):
"""Schema for updating an email template."""
name: str | None = Field(None, description="Human-readable template name")
description: str | None = Field(None, description="Template description")
subject: str | None = Field(None, description="Email subject")
body_html: str | None = Field(None, description="HTML email body")
body_text: str | None = Field(None, description="Plain text fallback")
variables: list[str] | None = Field(None, description="Available variables")
required_variables: list[str] | None = Field(None, description="Required variables")
is_active: bool | None = Field(None, description="Template active status")
class EmailTemplateResponse(BaseModel):
"""Schema for email template API response."""
model_config = ConfigDict(from_attributes=True)
id: int
code: str
language: str
name: str
description: str | None
category: str
subject: str
body_html: str
body_text: str | None
variables: list[str] = Field(default_factory=list)
required_variables: list[str] = Field(default_factory=list)
is_active: bool
is_platform_only: bool
created_at: datetime
updated_at: datetime
@classmethod
def from_db(cls, template) -> "EmailTemplateResponse":
"""Create response from database model."""
return cls(
id=template.id,
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_list,
required_variables=template.required_variables_list,
is_active=template.is_active,
is_platform_only=template.is_platform_only,
created_at=template.created_at,
updated_at=template.updated_at,
)
class EmailTemplateSummary(BaseModel):
"""Summary schema for template list views."""
model_config = ConfigDict(from_attributes=True)
id: int
code: str
name: str
category: str
languages: list[str] = Field(default_factory=list)
is_platform_only: bool
is_active: bool
@classmethod
def from_db_list(cls, templates: list) -> list["EmailTemplateSummary"]:
"""
Create summaries from database models, grouping by code.
Args:
templates: List of EmailTemplate models
Returns:
List of EmailTemplateSummary grouped by template code
"""
# Group templates by code
by_code: dict[str, list] = {}
for t in templates:
if t.code not in by_code:
by_code[t.code] = []
by_code[t.code].append(t)
summaries = []
for code, group in by_code.items():
first = group[0]
summaries.append(
cls(
id=first.id,
code=code,
name=first.name,
category=first.category,
languages=[t.language for t in group],
is_platform_only=first.is_platform_only,
is_active=first.is_active,
)
)
return summaries
# Store Email Template Schemas
class StoreEmailTemplateCreate(BaseModel):
"""Schema for creating a store email template override."""
template_code: str = Field(..., description="Template code to override")
language: str = Field(default="en", description="Language code")
name: str | None = Field(None, description="Custom name (uses platform default if None)")
subject: str = Field(..., description="Custom email subject")
body_html: str = Field(..., description="Custom HTML body")
body_text: str | None = Field(None, description="Custom plain text body")
class StoreEmailTemplateUpdate(BaseModel):
"""Schema for updating a store email template override."""
name: str | None = Field(None, description="Custom name")
subject: str | None = Field(None, description="Custom email subject")
body_html: str | None = Field(None, description="Custom HTML body")
body_text: str | None = Field(None, description="Custom plain text body")
is_active: bool | None = Field(None, description="Override active status")
class StoreEmailTemplateResponse(BaseModel):
"""Schema for store email template override API response."""
model_config = ConfigDict(from_attributes=True)
id: int
store_id: int
template_code: str
language: str
name: str | None
subject: str
body_html: str
body_text: str | None
is_active: bool
created_at: datetime
updated_at: datetime
class EmailTemplateWithOverrideStatus(BaseModel):
"""
Schema showing template with store override status.
Used in store UI to show which templates have been customized.
"""
model_config = ConfigDict(from_attributes=True)
code: str
name: str
category: str
languages: list[str]
is_platform_only: bool
has_override: bool = Field(
default=False, description="Whether store has customized this template"
)
override_languages: list[str] = Field(
default_factory=list,
description="Languages with store overrides",
)
# Email Preview/Test Schemas
class EmailPreviewRequest(BaseModel):
"""Schema for requesting an email preview."""
template_code: str = Field(..., description="Template code")
language: str = Field(default="en", description="Language code")
variables: dict[str, str] = Field(
default_factory=dict, description="Variables to inject"
)
class EmailPreviewResponse(BaseModel):
"""Schema for email preview response."""
subject: str
body_html: str
body_text: str | None
class EmailTestRequest(BaseModel):
"""Schema for sending a test email."""
template_code: str = Field(..., description="Template code")
language: str = Field(default="en", description="Language code")
to_email: str = Field(..., description="Recipient email address")
variables: dict[str, str] = Field(
default_factory=dict, description="Variables to inject"
)
class EmailTestResponse(BaseModel):
"""Schema for test email response."""
success: bool
message: str
email_log_id: int | None = None
# =============================================================================
# Email Log (Audit) Schemas
# =============================================================================
class EmailLogListItem(BaseModel):
"""Compact email log item (no body content)."""
id: int
recipient_email: str
recipient_name: str | None = None
subject: str
status: str
template_code: str | None = None
provider: str | None = None
store_id: int | None = None
related_type: str | None = None
related_id: int | None = None
created_at: str | None = None
sent_at: str | None = None
error_message: str | None = None
class EmailLogDetail(BaseModel):
"""Full email log detail including body content."""
id: int
recipient_email: str
recipient_name: str | None = None
subject: str
status: str
template_code: str | None = None
provider: str | None = None
store_id: int | None = None
user_id: int | None = None
related_type: str | None = None
related_id: int | None = None
from_email: str | None = None
from_name: str | None = None
reply_to: str | None = None
body_html: str | None = None
body_text: str | None = None
error_message: str | None = None
retry_count: int = 0
provider_message_id: str | None = None
created_at: str | None = None
sent_at: str | None = None
delivered_at: str | None = None
opened_at: str | None = None
clicked_at: str | None = None
extra_data: str | None = None
class EmailLogListResponse(BaseModel):
"""Paginated email log list."""
items: list[EmailLogListItem]
total: int
page: int
per_page: int
total_pages: int
class EmailLogStatsResponse(BaseModel):
"""Email log statistics."""
by_status: dict[str, int] = Field(default_factory=dict)
by_template: dict[str, int] = Field(default_factory=dict)
total: int = 0