refactor: migrate templates and static files to self-contained modules
Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,46 @@ from app.modules.messaging.services.admin_notification_service import (
|
||||
AlertType,
|
||||
Severity,
|
||||
)
|
||||
from app.modules.messaging.services.email_service import (
|
||||
EmailService,
|
||||
EmailProvider,
|
||||
ResolvedTemplate,
|
||||
BrandingContext,
|
||||
send_email,
|
||||
get_provider,
|
||||
get_platform_provider,
|
||||
get_vendor_provider,
|
||||
get_platform_email_config,
|
||||
# Provider classes
|
||||
SMTPProvider,
|
||||
SendGridProvider,
|
||||
MailgunProvider,
|
||||
SESProvider,
|
||||
DebugProvider,
|
||||
# Configurable provider classes
|
||||
ConfigurableSMTPProvider,
|
||||
ConfigurableSendGridProvider,
|
||||
ConfigurableMailgunProvider,
|
||||
ConfigurableSESProvider,
|
||||
# Vendor provider classes
|
||||
VendorSMTPProvider,
|
||||
VendorSendGridProvider,
|
||||
VendorMailgunProvider,
|
||||
VendorSESProvider,
|
||||
# Constants
|
||||
PLATFORM_NAME,
|
||||
PLATFORM_SUPPORT_EMAIL,
|
||||
PLATFORM_DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGES,
|
||||
WHITELABEL_TIERS,
|
||||
POWERED_BY_FOOTER_HTML,
|
||||
POWERED_BY_FOOTER_TEXT,
|
||||
)
|
||||
from app.modules.messaging.services.email_template_service import (
|
||||
EmailTemplateService,
|
||||
TemplateData,
|
||||
VendorOverrideData,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"messaging_service",
|
||||
@@ -39,4 +79,42 @@ __all__ = [
|
||||
"Priority",
|
||||
"AlertType",
|
||||
"Severity",
|
||||
# Email service
|
||||
"EmailService",
|
||||
"EmailProvider",
|
||||
"ResolvedTemplate",
|
||||
"BrandingContext",
|
||||
"send_email",
|
||||
"get_provider",
|
||||
"get_platform_provider",
|
||||
"get_vendor_provider",
|
||||
"get_platform_email_config",
|
||||
# Provider classes
|
||||
"SMTPProvider",
|
||||
"SendGridProvider",
|
||||
"MailgunProvider",
|
||||
"SESProvider",
|
||||
"DebugProvider",
|
||||
# Configurable provider classes
|
||||
"ConfigurableSMTPProvider",
|
||||
"ConfigurableSendGridProvider",
|
||||
"ConfigurableMailgunProvider",
|
||||
"ConfigurableSESProvider",
|
||||
# Vendor provider classes
|
||||
"VendorSMTPProvider",
|
||||
"VendorSendGridProvider",
|
||||
"VendorMailgunProvider",
|
||||
"VendorSESProvider",
|
||||
# Email constants
|
||||
"PLATFORM_NAME",
|
||||
"PLATFORM_SUPPORT_EMAIL",
|
||||
"PLATFORM_DEFAULT_LANGUAGE",
|
||||
"SUPPORTED_LANGUAGES",
|
||||
"WHITELABEL_TIERS",
|
||||
"POWERED_BY_FOOTER_HTML",
|
||||
"POWERED_BY_FOOTER_TEXT",
|
||||
# Email template service
|
||||
"EmailTemplateService",
|
||||
"TemplateData",
|
||||
"VendorOverrideData",
|
||||
]
|
||||
|
||||
1549
app/modules/messaging/services/email_service.py
Normal file
1549
app/modules/messaging/services/email_service.py
Normal file
File diff suppressed because it is too large
Load Diff
717
app/modules/messaging/services/email_template_service.py
Normal file
717
app/modules/messaging/services/email_template_service.py
Normal file
@@ -0,0 +1,717 @@
|
||||
# app/modules/messaging/services/email_template_service.py
|
||||
"""
|
||||
Email Template Service
|
||||
|
||||
Handles business logic for email template management:
|
||||
- Platform template CRUD operations
|
||||
- Vendor template override management
|
||||
- Template preview and testing
|
||||
- Email log queries
|
||||
|
||||
This service layer separates business logic from API endpoints
|
||||
to follow the project's layered architecture.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from jinja2 import Template
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions.base import (
|
||||
AuthorizationException,
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.database.email import EmailCategory, EmailLog, EmailTemplate
|
||||
from models.database.vendor_email_template import VendorEmailTemplate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Supported languages
|
||||
SUPPORTED_LANGUAGES = ["en", "fr", "de", "lb"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateData:
|
||||
"""Template data container."""
|
||||
code: str
|
||||
language: str
|
||||
name: str
|
||||
description: str | None
|
||||
category: str
|
||||
subject: str
|
||||
body_html: str
|
||||
body_text: str | None
|
||||
variables: list[str]
|
||||
required_variables: list[str]
|
||||
is_platform_only: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class VendorOverrideData:
|
||||
"""Vendor override data container."""
|
||||
code: str
|
||||
language: str
|
||||
subject: str
|
||||
body_html: str
|
||||
body_text: str | None
|
||||
name: str | None
|
||||
updated_at: str | None
|
||||
|
||||
|
||||
class EmailTemplateService:
|
||||
"""Service for managing email templates."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
# =========================================================================
|
||||
# ADMIN OPERATIONS
|
||||
# =========================================================================
|
||||
|
||||
def list_platform_templates(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
List all platform email templates grouped by code.
|
||||
|
||||
Returns:
|
||||
List of template summaries with language availability.
|
||||
"""
|
||||
templates = (
|
||||
self.db.query(EmailTemplate)
|
||||
.order_by(EmailTemplate.category, EmailTemplate.code)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Group by code
|
||||
grouped: dict[str, dict] = {}
|
||||
for template in templates:
|
||||
if template.code not in grouped:
|
||||
grouped[template.code] = {
|
||||
"code": template.code,
|
||||
"name": template.name,
|
||||
"category": template.category,
|
||||
"description": template.description,
|
||||
"is_platform_only": template.is_platform_only,
|
||||
"languages": [],
|
||||
"variables": [],
|
||||
}
|
||||
grouped[template.code]["languages"].append(template.language)
|
||||
if template.variables and not grouped[template.code]["variables"]:
|
||||
try:
|
||||
import json
|
||||
grouped[template.code]["variables"] = json.loads(template.variables)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
return list(grouped.values())
|
||||
|
||||
def get_template_categories(self) -> list[str]:
|
||||
"""Get list of all template categories."""
|
||||
return [cat.value for cat in EmailCategory]
|
||||
|
||||
def get_platform_template(self, code: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get a platform template with all language versions.
|
||||
|
||||
Args:
|
||||
code: Template code
|
||||
|
||||
Returns:
|
||||
Template details with all language versions
|
||||
|
||||
Raises:
|
||||
NotFoundError: If template not found
|
||||
"""
|
||||
templates = (
|
||||
self.db.query(EmailTemplate)
|
||||
.filter(EmailTemplate.code == code)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not templates:
|
||||
raise ResourceNotFoundException(f"Template not found: {code}")
|
||||
|
||||
first = templates[0]
|
||||
languages = {}
|
||||
for t in templates:
|
||||
languages[t.language] = {
|
||||
"subject": t.subject,
|
||||
"body_html": t.body_html,
|
||||
"body_text": t.body_text,
|
||||
}
|
||||
|
||||
return {
|
||||
"code": code,
|
||||
"name": first.name,
|
||||
"description": first.description,
|
||||
"category": first.category,
|
||||
"is_platform_only": first.is_platform_only,
|
||||
"variables": self._parse_variables(first.variables),
|
||||
"required_variables": self._parse_required_variables(first.required_variables),
|
||||
"languages": languages,
|
||||
}
|
||||
|
||||
def get_platform_template_language(self, code: str, language: str) -> TemplateData:
|
||||
"""
|
||||
Get a specific language version of a platform template.
|
||||
|
||||
Args:
|
||||
code: Template code
|
||||
language: Language code
|
||||
|
||||
Returns:
|
||||
Template data for the specific language
|
||||
|
||||
Raises:
|
||||
NotFoundError: If template or language not found
|
||||
"""
|
||||
template = EmailTemplate.get_by_code_and_language(self.db, code, language)
|
||||
|
||||
if not template:
|
||||
raise ResourceNotFoundException(f"Template not found: {code}/{language}")
|
||||
|
||||
return TemplateData(
|
||||
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=self._parse_variables(template.variables),
|
||||
required_variables=self._parse_required_variables(template.required_variables),
|
||||
is_platform_only=template.is_platform_only,
|
||||
)
|
||||
|
||||
def update_platform_template(
|
||||
self,
|
||||
code: str,
|
||||
language: str,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Update a platform email template.
|
||||
|
||||
Args:
|
||||
code: Template code
|
||||
language: Language code
|
||||
subject: New subject line
|
||||
body_html: New HTML body
|
||||
body_text: New plain text body (optional)
|
||||
|
||||
Raises:
|
||||
NotFoundError: If template not found
|
||||
ValidationError: If template syntax is invalid
|
||||
"""
|
||||
template = EmailTemplate.get_by_code_and_language(self.db, code, language)
|
||||
|
||||
if not template:
|
||||
raise ResourceNotFoundException(f"Template not found: {code}/{language}")
|
||||
|
||||
# Validate Jinja2 syntax
|
||||
self._validate_template_syntax(subject, body_html, body_text)
|
||||
|
||||
template.subject = subject
|
||||
template.body_html = body_html
|
||||
template.body_text = body_text
|
||||
|
||||
logger.info(f"Updated platform template: {code}/{language}")
|
||||
|
||||
def preview_template(
|
||||
self,
|
||||
code: str,
|
||||
language: str,
|
||||
variables: dict[str, Any],
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Preview a template with sample variables.
|
||||
|
||||
Args:
|
||||
code: Template code
|
||||
language: Language code
|
||||
variables: Variables to render
|
||||
|
||||
Returns:
|
||||
Rendered subject and body
|
||||
|
||||
Raises:
|
||||
NotFoundError: If template not found
|
||||
ValidationError: If rendering fails
|
||||
"""
|
||||
template = EmailTemplate.get_by_code_and_language(self.db, code, language)
|
||||
|
||||
if not template:
|
||||
raise ResourceNotFoundException(f"Template not found: {code}/{language}")
|
||||
|
||||
try:
|
||||
rendered_subject = Template(template.subject).render(variables)
|
||||
rendered_html = Template(template.body_html).render(variables)
|
||||
rendered_text = Template(template.body_text).render(variables) if template.body_text else None
|
||||
except Exception as e:
|
||||
raise ValidationException(f"Template rendering error: {str(e)}")
|
||||
|
||||
return {
|
||||
"subject": rendered_subject,
|
||||
"body_html": rendered_html,
|
||||
"body_text": rendered_text,
|
||||
}
|
||||
|
||||
def get_template_logs(
|
||||
self,
|
||||
code: str,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[dict], int]:
|
||||
"""
|
||||
Get email logs for a specific template.
|
||||
|
||||
Args:
|
||||
code: Template code
|
||||
limit: Max results
|
||||
offset: Skip results
|
||||
|
||||
Returns:
|
||||
Tuple of (logs list, total count)
|
||||
"""
|
||||
query = (
|
||||
self.db.query(EmailLog)
|
||||
.filter(EmailLog.template_code == code)
|
||||
.order_by(EmailLog.created_at.desc())
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
logs = query.offset(offset).limit(limit).all()
|
||||
|
||||
return (
|
||||
[
|
||||
{
|
||||
"id": log.id,
|
||||
"to_email": log.to_email,
|
||||
"status": log.status,
|
||||
"language": log.language,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
"error_message": log.error_message,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
total,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# VENDOR OPERATIONS
|
||||
# =========================================================================
|
||||
|
||||
def list_overridable_templates(self, vendor_id: int) -> dict[str, Any]:
|
||||
"""
|
||||
List all templates that a vendor can customize.
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID
|
||||
|
||||
Returns:
|
||||
Dict with templates list and supported languages
|
||||
"""
|
||||
# Get all overridable platform templates
|
||||
platform_templates = EmailTemplate.get_overridable_templates(self.db)
|
||||
|
||||
# Get all vendor overrides
|
||||
vendor_overrides = VendorEmailTemplate.get_all_overrides_for_vendor(
|
||||
self.db, vendor_id
|
||||
)
|
||||
|
||||
# Build override lookup
|
||||
override_lookup = {}
|
||||
for override in vendor_overrides:
|
||||
key = (override.template_code, override.language)
|
||||
override_lookup[key] = override
|
||||
|
||||
# Build response
|
||||
templates = []
|
||||
for template in platform_templates:
|
||||
# Check which languages have overrides
|
||||
override_languages = []
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
if (template.code, lang) in override_lookup:
|
||||
override_languages.append(lang)
|
||||
|
||||
templates.append({
|
||||
"code": template.code,
|
||||
"name": template.name,
|
||||
"category": template.category,
|
||||
"description": template.description,
|
||||
"available_languages": self._get_template_languages(template.code),
|
||||
"override_languages": override_languages,
|
||||
"has_override": len(override_languages) > 0,
|
||||
"variables": self._parse_required_variables(template.required_variables),
|
||||
})
|
||||
|
||||
return {
|
||||
"templates": templates,
|
||||
"supported_languages": SUPPORTED_LANGUAGES,
|
||||
}
|
||||
|
||||
def get_vendor_template(self, vendor_id: int, code: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get a template with all language versions for a vendor.
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID
|
||||
code: Template code
|
||||
|
||||
Returns:
|
||||
Template details with vendor overrides status
|
||||
|
||||
Raises:
|
||||
NotFoundError: If template not found
|
||||
ForbiddenError: If template is platform-only
|
||||
"""
|
||||
# Get platform template
|
||||
platform_template = (
|
||||
self.db.query(EmailTemplate)
|
||||
.filter(EmailTemplate.code == code)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not platform_template:
|
||||
raise ResourceNotFoundException(f"Template not found: {code}")
|
||||
|
||||
if platform_template.is_platform_only:
|
||||
raise AuthorizationException("This is a platform-only template and cannot be customized")
|
||||
|
||||
# Get all language versions
|
||||
platform_versions = (
|
||||
self.db.query(EmailTemplate)
|
||||
.filter(EmailTemplate.code == code)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get vendor overrides
|
||||
vendor_overrides = (
|
||||
self.db.query(VendorEmailTemplate)
|
||||
.filter(
|
||||
VendorEmailTemplate.vendor_id == vendor_id,
|
||||
VendorEmailTemplate.template_code == code,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
override_lookup = {v.language: v for v in vendor_overrides}
|
||||
platform_lookup = {t.language: t for t in platform_versions}
|
||||
|
||||
# Build language versions
|
||||
languages = {}
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
platform_ver = platform_lookup.get(lang)
|
||||
override_ver = override_lookup.get(lang)
|
||||
|
||||
languages[lang] = {
|
||||
"has_platform_template": platform_ver is not None,
|
||||
"has_vendor_override": override_ver is not None,
|
||||
"platform": {
|
||||
"subject": platform_ver.subject,
|
||||
"body_html": platform_ver.body_html,
|
||||
"body_text": platform_ver.body_text,
|
||||
} if platform_ver else None,
|
||||
"vendor_override": {
|
||||
"subject": override_ver.subject,
|
||||
"body_html": override_ver.body_html,
|
||||
"body_text": override_ver.body_text,
|
||||
"name": override_ver.name,
|
||||
"updated_at": override_ver.updated_at.isoformat() if override_ver else None,
|
||||
} if override_ver else None,
|
||||
}
|
||||
|
||||
return {
|
||||
"code": code,
|
||||
"name": platform_template.name,
|
||||
"category": platform_template.category,
|
||||
"description": platform_template.description,
|
||||
"variables": self._parse_required_variables(platform_template.required_variables),
|
||||
"languages": languages,
|
||||
}
|
||||
|
||||
def get_vendor_template_language(
|
||||
self,
|
||||
vendor_id: int,
|
||||
code: str,
|
||||
language: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get a specific language version for a vendor (override or platform).
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID
|
||||
code: Template code
|
||||
language: Language code
|
||||
|
||||
Returns:
|
||||
Template data with source indicator
|
||||
|
||||
Raises:
|
||||
NotFoundError: If template not found
|
||||
ForbiddenError: If template is platform-only
|
||||
ValidationError: If language not supported
|
||||
"""
|
||||
if language not in SUPPORTED_LANGUAGES:
|
||||
raise ValidationException(f"Unsupported language: {language}")
|
||||
|
||||
# Check if template is overridable
|
||||
platform_template = (
|
||||
self.db.query(EmailTemplate)
|
||||
.filter(EmailTemplate.code == code)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not platform_template:
|
||||
raise ResourceNotFoundException(f"Template not found: {code}")
|
||||
|
||||
if platform_template.is_platform_only:
|
||||
raise AuthorizationException("This is a platform-only template and cannot be customized")
|
||||
|
||||
# Check for vendor override
|
||||
vendor_override = VendorEmailTemplate.get_override(
|
||||
self.db, vendor_id, code, language
|
||||
)
|
||||
|
||||
# Get platform version
|
||||
platform_version = EmailTemplate.get_by_code_and_language(
|
||||
self.db, code, language
|
||||
)
|
||||
|
||||
if vendor_override:
|
||||
return {
|
||||
"code": code,
|
||||
"language": language,
|
||||
"source": "vendor_override",
|
||||
"subject": vendor_override.subject,
|
||||
"body_html": vendor_override.body_html,
|
||||
"body_text": vendor_override.body_text,
|
||||
"name": vendor_override.name,
|
||||
"variables": self._parse_required_variables(platform_template.required_variables),
|
||||
"platform_template": {
|
||||
"subject": platform_version.subject,
|
||||
"body_html": platform_version.body_html,
|
||||
} if platform_version else None,
|
||||
}
|
||||
elif platform_version:
|
||||
return {
|
||||
"code": code,
|
||||
"language": language,
|
||||
"source": "platform",
|
||||
"subject": platform_version.subject,
|
||||
"body_html": platform_version.body_html,
|
||||
"body_text": platform_version.body_text,
|
||||
"name": platform_version.name,
|
||||
"variables": self._parse_required_variables(platform_template.required_variables),
|
||||
"platform_template": None,
|
||||
}
|
||||
else:
|
||||
raise ResourceNotFoundException(f"No template found for language: {language}")
|
||||
|
||||
def create_or_update_vendor_override(
|
||||
self,
|
||||
vendor_id: int,
|
||||
code: str,
|
||||
language: str,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None = None,
|
||||
name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Create or update a vendor template override.
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID
|
||||
code: Template code
|
||||
language: Language code
|
||||
subject: Custom subject
|
||||
body_html: Custom HTML body
|
||||
body_text: Custom plain text body
|
||||
name: Custom template name
|
||||
|
||||
Returns:
|
||||
Result with is_new indicator
|
||||
|
||||
Raises:
|
||||
NotFoundError: If template not found
|
||||
ForbiddenError: If template is platform-only
|
||||
ValidationError: If syntax invalid or language not supported
|
||||
"""
|
||||
if language not in SUPPORTED_LANGUAGES:
|
||||
raise ValidationException(f"Unsupported language: {language}")
|
||||
|
||||
# Check if template exists and is overridable
|
||||
platform_template = (
|
||||
self.db.query(EmailTemplate)
|
||||
.filter(EmailTemplate.code == code)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not platform_template:
|
||||
raise ResourceNotFoundException(f"Template not found: {code}")
|
||||
|
||||
if platform_template.is_platform_only:
|
||||
raise AuthorizationException("This is a platform-only template and cannot be customized")
|
||||
|
||||
# Validate template syntax
|
||||
self._validate_template_syntax(subject, body_html, body_text)
|
||||
|
||||
# Create or update
|
||||
override = VendorEmailTemplate.create_or_update(
|
||||
db=self.db,
|
||||
vendor_id=vendor_id,
|
||||
template_code=code,
|
||||
language=language,
|
||||
subject=subject,
|
||||
body_html=body_html,
|
||||
body_text=body_text,
|
||||
name=name,
|
||||
)
|
||||
|
||||
logger.info(f"Vendor {vendor_id} updated template override: {code}/{language}")
|
||||
|
||||
return {
|
||||
"message": "Template override saved",
|
||||
"code": code,
|
||||
"language": language,
|
||||
"is_new": override.created_at == override.updated_at,
|
||||
}
|
||||
|
||||
def delete_vendor_override(
|
||||
self,
|
||||
vendor_id: int,
|
||||
code: str,
|
||||
language: str,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a vendor template override.
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID
|
||||
code: Template code
|
||||
language: Language code
|
||||
|
||||
Raises:
|
||||
NotFoundError: If override not found
|
||||
ValidationError: If language not supported
|
||||
"""
|
||||
if language not in SUPPORTED_LANGUAGES:
|
||||
raise ValidationException(f"Unsupported language: {language}")
|
||||
|
||||
deleted = VendorEmailTemplate.delete_override(
|
||||
self.db, vendor_id, code, language
|
||||
)
|
||||
|
||||
if not deleted:
|
||||
raise ResourceNotFoundException("No override found for this template and language")
|
||||
|
||||
logger.info(f"Vendor {vendor_id} deleted template override: {code}/{language}")
|
||||
|
||||
def preview_vendor_template(
|
||||
self,
|
||||
vendor_id: int,
|
||||
code: str,
|
||||
language: str,
|
||||
variables: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Preview a vendor template (override or platform).
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID
|
||||
code: Template code
|
||||
language: Language code
|
||||
variables: Variables to render
|
||||
|
||||
Returns:
|
||||
Rendered template with source indicator
|
||||
|
||||
Raises:
|
||||
NotFoundError: If template not found
|
||||
ValidationError: If rendering fails
|
||||
"""
|
||||
# Get template content
|
||||
vendor_override = VendorEmailTemplate.get_override(
|
||||
self.db, vendor_id, code, language
|
||||
)
|
||||
platform_version = EmailTemplate.get_by_code_and_language(
|
||||
self.db, code, language
|
||||
)
|
||||
|
||||
if vendor_override:
|
||||
subject = vendor_override.subject
|
||||
body_html = vendor_override.body_html
|
||||
body_text = vendor_override.body_text
|
||||
source = "vendor_override"
|
||||
elif platform_version:
|
||||
subject = platform_version.subject
|
||||
body_html = platform_version.body_html
|
||||
body_text = platform_version.body_text
|
||||
source = "platform"
|
||||
else:
|
||||
raise ResourceNotFoundException(f"No template found for language: {language}")
|
||||
|
||||
try:
|
||||
rendered_subject = Template(subject).render(variables)
|
||||
rendered_html = Template(body_html).render(variables)
|
||||
rendered_text = Template(body_text).render(variables) if body_text else None
|
||||
except Exception as e:
|
||||
raise ValidationException(f"Template rendering error: {str(e)}")
|
||||
|
||||
return {
|
||||
"source": source,
|
||||
"language": language,
|
||||
"subject": rendered_subject,
|
||||
"body_html": rendered_html,
|
||||
"body_text": rendered_text,
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# HELPER METHODS
|
||||
# =========================================================================
|
||||
|
||||
def _validate_template_syntax(
|
||||
self,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
) -> None:
|
||||
"""Validate Jinja2 template syntax."""
|
||||
try:
|
||||
Template(subject).render({})
|
||||
Template(body_html).render({})
|
||||
if body_text:
|
||||
Template(body_text).render({})
|
||||
except Exception as e:
|
||||
raise ValidationException(f"Invalid template syntax: {str(e)}")
|
||||
|
||||
def _parse_variables(self, variables_json: str | None) -> list[str]:
|
||||
"""Parse variables JSON string to list."""
|
||||
if not variables_json:
|
||||
return []
|
||||
try:
|
||||
import json
|
||||
return json.loads(variables_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return []
|
||||
|
||||
def _parse_required_variables(self, required_vars: str | None) -> list[str]:
|
||||
"""Parse required variables comma-separated string to list."""
|
||||
if not required_vars:
|
||||
return []
|
||||
return [v.strip() for v in required_vars.split(",") if v.strip()]
|
||||
|
||||
def _get_template_languages(self, code: str) -> list[str]:
|
||||
"""Get list of languages available for a template."""
|
||||
templates = (
|
||||
self.db.query(EmailTemplate.language)
|
||||
.filter(EmailTemplate.code == code)
|
||||
.all()
|
||||
)
|
||||
return [t.language for t in templates]
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
from fastapi import UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
from app.modules.core.services.admin_settings_service import admin_settings_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user