feat: implement email template system with vendor overrides

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>
This commit is contained in:
2026-01-03 18:29:26 +01:00
parent 2e1a2fc9fc
commit c52af2a155
22 changed files with 3882 additions and 119 deletions

View File

@@ -0,0 +1,114 @@
# alembic/versions/u9c0d1e2f3g4_add_vendor_email_templates.py
"""Add vendor email templates and enhance email_templates table.
Revision ID: u9c0d1e2f3g4
Revises: t8b9c0d1e2f3
Create Date: 2026-01-03
Changes:
- Add is_platform_only column to email_templates (templates that vendors cannot override)
- Add required_variables column to email_templates (JSON list of required variables)
- Create vendor_email_templates table for vendor-specific template overrides
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "u9c0d1e2f3g4"
down_revision = "t8b9c0d1e2f3"
branch_labels = None
depends_on = None
def upgrade():
# Add new columns to email_templates
op.add_column(
"email_templates",
sa.Column("is_platform_only", sa.Boolean(), nullable=False, server_default="0"),
)
op.add_column(
"email_templates",
sa.Column("required_variables", sa.Text(), nullable=True),
)
# Create vendor_email_templates table
op.create_table(
"vendor_email_templates",
sa.Column("id", sa.Integer(), nullable=False, autoincrement=True),
sa.Column("vendor_id", sa.Integer(), nullable=False),
sa.Column("template_code", sa.String(100), nullable=False),
sa.Column("language", sa.String(5), nullable=False, server_default="en"),
sa.Column("name", sa.String(255), nullable=True),
sa.Column("subject", sa.String(500), nullable=False),
sa.Column("body_html", sa.Text(), nullable=False),
sa.Column("body_text", sa.Text(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"),
sa.Column(
"created_at",
sa.DateTime(),
nullable=False,
server_default=sa.text("CURRENT_TIMESTAMP"),
),
sa.Column(
"updated_at",
sa.DateTime(),
nullable=False,
server_default=sa.text("CURRENT_TIMESTAMP"),
),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(
["vendor_id"],
["vendors.id"],
name="fk_vendor_email_templates_vendor_id",
ondelete="CASCADE",
),
sa.UniqueConstraint(
"vendor_id",
"template_code",
"language",
name="uq_vendor_email_template_code_language",
),
)
# Create indexes for performance
op.create_index(
"ix_vendor_email_templates_vendor_id",
"vendor_email_templates",
["vendor_id"],
)
op.create_index(
"ix_vendor_email_templates_template_code",
"vendor_email_templates",
["template_code"],
)
op.create_index(
"ix_vendor_email_templates_lookup",
"vendor_email_templates",
["vendor_id", "template_code", "language"],
)
# Add unique constraint to email_templates for code+language
# This ensures we can reliably look up platform templates
op.create_index(
"ix_email_templates_code_language",
"email_templates",
["code", "language"],
unique=True,
)
def downgrade():
# Drop indexes
op.drop_index("ix_email_templates_code_language", table_name="email_templates")
op.drop_index("ix_vendor_email_templates_lookup", table_name="vendor_email_templates")
op.drop_index("ix_vendor_email_templates_template_code", table_name="vendor_email_templates")
op.drop_index("ix_vendor_email_templates_vendor_id", table_name="vendor_email_templates")
# Drop vendor_email_templates table
op.drop_table("vendor_email_templates")
# Remove new columns from email_templates
op.drop_column("email_templates", "required_variables")
op.drop_column("email_templates", "is_platform_only")