Add comprehensive email template management for both admin and vendors:
Admin Features:
- Email templates management page at /admin/email-templates
- Edit platform templates with language support (en, fr, de, lb)
- Preview templates with sample variables
- Send test emails
- View email logs per template
Vendor Features:
- Email templates customization page at /vendor/{code}/email-templates
- Override platform templates with vendor-specific versions
- Preview and test customized templates
- Revert to platform defaults
Technical Changes:
- Migration for vendor_email_templates table
- VendorEmailTemplate model with override management
- Enhanced EmailService with language resolution chain
(customer preferred -> vendor preferred -> platform default)
- Branding resolution (Wizamart default, removed for whitelabel)
- Platform-only template protection (billing templates)
- Admin and vendor API endpoints with full CRUD
- Updated seed script with billing and team templates
Files: 22 changed, ~3,900 lines added
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
230 lines
6.0 KiB
Python
230 lines
6.0 KiB
Python
# models/database/vendor_email_template.py
|
|
"""
|
|
Vendor email template override model.
|
|
|
|
Allows vendors to customize platform email templates with their own content.
|
|
Platform-only templates cannot be overridden (e.g., billing, subscription emails).
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
Column,
|
|
DateTime,
|
|
ForeignKey,
|
|
Integer,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import Session, relationship
|
|
|
|
from app.core.database import Base
|
|
|
|
from .base import TimestampMixin
|
|
|
|
|
|
class VendorEmailTemplate(Base, TimestampMixin):
|
|
"""
|
|
Vendor-specific email template override.
|
|
|
|
Each vendor can customize email templates for their shop.
|
|
Overrides are per-template-code and per-language.
|
|
|
|
When sending emails:
|
|
1. Check if vendor has an override for the template+language
|
|
2. If yes, use vendor's version
|
|
3. If no, fall back to platform template
|
|
|
|
Platform-only templates (is_platform_only=True on EmailTemplate)
|
|
cannot be overridden.
|
|
"""
|
|
|
|
__tablename__ = "vendor_email_templates"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
|
|
# Vendor relationship
|
|
vendor_id = Column(
|
|
Integer,
|
|
ForeignKey("vendors.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
# Template identification (references EmailTemplate.code, not FK)
|
|
template_code = Column(String(100), nullable=False, index=True)
|
|
language = Column(String(5), nullable=False, default="en")
|
|
|
|
# Optional custom name (if None, uses platform template name)
|
|
name = Column(String(255), nullable=True)
|
|
|
|
# Email content
|
|
subject = Column(String(500), nullable=False)
|
|
body_html = Column(Text, nullable=False)
|
|
body_text = Column(Text, nullable=True)
|
|
|
|
# Status
|
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
|
|
# Relationships
|
|
vendor = relationship("Vendor", back_populates="email_templates")
|
|
|
|
# Unique constraint: one override per vendor+template+language
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"vendor_id",
|
|
"template_code",
|
|
"language",
|
|
name="uq_vendor_email_template_code_language",
|
|
),
|
|
{"sqlite_autoincrement": True},
|
|
)
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"<VendorEmailTemplate("
|
|
f"vendor_id={self.vendor_id}, "
|
|
f"code='{self.template_code}', "
|
|
f"language='{self.language}')>"
|
|
)
|
|
|
|
@classmethod
|
|
def get_override(
|
|
cls,
|
|
db: Session,
|
|
vendor_id: int,
|
|
template_code: str,
|
|
language: str,
|
|
) -> "VendorEmailTemplate | None":
|
|
"""
|
|
Get vendor's template override if it exists.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID
|
|
template_code: Template code to look up
|
|
language: Language code (en, fr, de, lb)
|
|
|
|
Returns:
|
|
VendorEmailTemplate if override exists, None otherwise
|
|
"""
|
|
return (
|
|
db.query(cls)
|
|
.filter(
|
|
cls.vendor_id == vendor_id,
|
|
cls.template_code == template_code,
|
|
cls.language == language,
|
|
cls.is_active == True, # noqa: E712
|
|
)
|
|
.first()
|
|
)
|
|
|
|
@classmethod
|
|
def get_all_overrides_for_vendor(
|
|
cls,
|
|
db: Session,
|
|
vendor_id: int,
|
|
) -> list["VendorEmailTemplate"]:
|
|
"""
|
|
Get all template overrides for a vendor.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID
|
|
|
|
Returns:
|
|
List of VendorEmailTemplate objects
|
|
"""
|
|
return (
|
|
db.query(cls)
|
|
.filter(
|
|
cls.vendor_id == vendor_id,
|
|
cls.is_active == True, # noqa: E712
|
|
)
|
|
.order_by(cls.template_code, cls.language)
|
|
.all()
|
|
)
|
|
|
|
@classmethod
|
|
def create_or_update(
|
|
cls,
|
|
db: Session,
|
|
vendor_id: int,
|
|
template_code: str,
|
|
language: str,
|
|
subject: str,
|
|
body_html: str,
|
|
body_text: str | None = None,
|
|
name: str | None = None,
|
|
) -> "VendorEmailTemplate":
|
|
"""
|
|
Create or update a vendor email template override.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID
|
|
template_code: Template code
|
|
language: Language code
|
|
subject: Email subject
|
|
body_html: HTML body content
|
|
body_text: Optional plain text body
|
|
name: Optional custom name
|
|
|
|
Returns:
|
|
Created or updated VendorEmailTemplate
|
|
"""
|
|
existing = cls.get_override(db, vendor_id, template_code, language)
|
|
|
|
if existing:
|
|
existing.subject = subject
|
|
existing.body_html = body_html
|
|
existing.body_text = body_text
|
|
existing.name = name
|
|
existing.updated_at = datetime.utcnow()
|
|
return existing
|
|
|
|
new_template = cls(
|
|
vendor_id=vendor_id,
|
|
template_code=template_code,
|
|
language=language,
|
|
subject=subject,
|
|
body_html=body_html,
|
|
body_text=body_text,
|
|
name=name,
|
|
)
|
|
db.add(new_template)
|
|
return new_template
|
|
|
|
@classmethod
|
|
def delete_override(
|
|
cls,
|
|
db: Session,
|
|
vendor_id: int,
|
|
template_code: str,
|
|
language: str,
|
|
) -> bool:
|
|
"""
|
|
Delete a vendor's template override (revert to platform default).
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID
|
|
template_code: Template code
|
|
language: Language code
|
|
|
|
Returns:
|
|
True if deleted, False if not found
|
|
"""
|
|
deleted = (
|
|
db.query(cls)
|
|
.filter(
|
|
cls.vendor_id == vendor_id,
|
|
cls.template_code == template_code,
|
|
cls.language == language,
|
|
)
|
|
.delete()
|
|
)
|
|
return deleted > 0
|