Technical Documentation: - docs/development/architecture-fixes-2026-01.md: Complete guide to all architecture validation fixes (62 -> 0 errors) User Guides: - docs/guides/email-templates.md: How-to guide for vendors and admins to use the email template customization system Implementation Docs: - docs/implementation/password-reset-implementation.md: Technical documentation for the password reset feature - Updated email-templates-architecture.md with EmailTemplateService documentation and related links Bugfix: - Fixed TemplateListItem Pydantic model to match service output (languages vs available_languages field name) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
Email Template System
Overview
The email template system provides comprehensive email customization for the Wizamart platform with the following features:
- Platform-level templates with vendor overrides
- Wizamart branding by default (removed for Enterprise whitelabel tier)
- Platform-only templates that cannot be overridden (billing, subscriptions)
- Admin UI for editing platform templates
- Vendor UI for customizing customer-facing emails
- 4-language support (en, fr, de, lb)
- Smart language resolution (customer → vendor → platform default)
Architecture
Database Models
EmailTemplate (Platform Templates)
File: models/database/email.py
| Column | Type | Description |
|---|---|---|
id |
Integer | Primary key |
code |
String(100) | Unique template identifier |
language |
String(5) | Language code (en, fr, de, lb) |
name |
String(255) | Human-readable name |
description |
Text | Template description |
category |
Enum | AUTH, ORDERS, BILLING, SYSTEM, MARKETING |
subject |
String(500) | Email subject line (Jinja2) |
body_html |
Text | HTML body (Jinja2) |
body_text |
Text | Plain text body (Jinja2) |
variables |
JSON | List of available variables |
is_platform_only |
Boolean | Cannot be overridden by vendors |
required_variables |
Text | Comma-separated required variables |
Key Methods:
get_by_code_and_language(db, code, language)- Get specific templateget_overridable_templates(db)- Get templates vendors can customize
VendorEmailTemplate (Vendor Overrides)
File: models/database/vendor_email_template.py
| Column | Type | Description |
|---|---|---|
id |
Integer | Primary key |
vendor_id |
Integer | FK to vendors.id |
template_code |
String(100) | References EmailTemplate.code |
language |
String(5) | Language code |
name |
String(255) | Custom name (optional) |
subject |
String(500) | Custom subject |
body_html |
Text | Custom HTML body |
body_text |
Text | Custom plain text body |
created_at |
DateTime | Creation timestamp |
updated_at |
DateTime | Last update timestamp |
Key Methods:
get_override(db, vendor_id, code, language)- Get vendor overridecreate_or_update(db, vendor_id, code, language, ...)- Upsert overridedelete_override(db, vendor_id, code, language)- Revert to platform defaultget_all_overrides_for_vendor(db, vendor_id)- List all vendor overrides
Unique Constraint
UNIQUE (vendor_id, template_code, language)
Email Template Service
File: app/services/email_template_service.py
The EmailTemplateService encapsulates all email template business logic, keeping API endpoints clean and focused on request/response handling.
Admin Methods
| Method | Description |
|---|---|
list_platform_templates() |
List all platform templates grouped by code |
get_template_categories() |
Get list of template categories |
get_platform_template(code) |
Get template with all language versions |
update_platform_template(code, language, data) |
Update platform template content |
preview_template(code, language, variables) |
Generate preview with sample data |
get_template_logs(code, limit) |
Get email logs for template |
Vendor Methods
| Method | Description |
|---|---|
list_overridable_templates(vendor_id) |
List templates vendor can customize |
get_vendor_template(vendor_id, code, language) |
Get template (override or platform default) |
create_or_update_vendor_override(vendor_id, code, language, data) |
Save vendor customization |
delete_vendor_override(vendor_id, code, language) |
Revert to platform default |
preview_vendor_template(vendor_id, code, language, variables) |
Preview with vendor branding |
Usage Example
from app.services.email_template_service import EmailTemplateService
service = EmailTemplateService(db)
# List templates for admin
templates = service.list_platform_templates()
# Get vendor's view of a template
template_data = service.get_vendor_template(vendor_id, "order_confirmation", "fr")
# Create vendor override
service.create_or_update_vendor_override(
vendor_id=vendor.id,
code="order_confirmation",
language="fr",
subject="Votre commande {{ order_number }}",
body_html="<html>...</html>",
body_text="Plain text...",
)
Email Service
File: app/services/email_service.py
Language Resolution
Priority order for determining email language:
- Customer preferred language (if customer exists)
- Vendor storefront language (vendor.storefront_language)
- Platform default (
en)
def resolve_language(
self,
customer_id: int | None,
vendor_id: int | None,
explicit_language: str | None = None
) -> str
Template Resolution
def resolve_template(
self,
template_code: str,
language: str,
vendor_id: int | None = None
) -> ResolvedTemplate
Resolution order:
- If
vendor_idprovided and template not platform-only:- Look for
VendorEmailTemplateoverride - Fall back to platform
EmailTemplate
- Look for
- If no vendor or platform-only:
- Use platform
EmailTemplate
- Use platform
- Language fallback:
requested_language→en
Branding Resolution
def get_branding(self, vendor_id: int | None) -> BrandingContext
| Scenario | Platform Name | Platform Logo |
|---|---|---|
| No vendor | Wizamart | Wizamart logo |
| Standard vendor | Wizamart | Wizamart logo |
| Whitelabel vendor | Vendor name | Vendor logo |
Whitelabel is determined by the white_label feature flag on the vendor.
API Endpoints
Admin API
File: app/api/v1/admin/email_templates.py
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/admin/email-templates |
List all platform templates |
| GET | /api/v1/admin/email-templates/categories |
Get template categories |
| GET | /api/v1/admin/email-templates/{code} |
Get template (all languages) |
| GET | /api/v1/admin/email-templates/{code}/{language} |
Get specific language version |
| PUT | /api/v1/admin/email-templates/{code}/{language} |
Update template |
| POST | /api/v1/admin/email-templates/{code}/preview |
Preview with sample data |
| POST | /api/v1/admin/email-templates/{code}/test |
Send test email |
| GET | /api/v1/admin/email-templates/{code}/logs |
View email logs for template |
Vendor API
File: app/api/v1/vendor/email_templates.py
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/vendor/email-templates |
List overridable templates |
| GET | /api/v1/vendor/email-templates/{code} |
Get template with override status |
| GET | /api/v1/vendor/email-templates/{code}/{language} |
Get specific language (override or default) |
| PUT | /api/v1/vendor/email-templates/{code}/{language} |
Create/update override |
| DELETE | /api/v1/vendor/email-templates/{code}/{language} |
Reset to platform default |
| POST | /api/v1/vendor/email-templates/{code}/preview |
Preview with vendor branding |
| POST | /api/v1/vendor/email-templates/{code}/test |
Send test email |
User Interface
Admin UI
Page: /admin/email-templates
Template: app/templates/admin/email-templates.html
JavaScript: static/admin/js/email-templates.js
Features:
- Template list with category filtering
- Edit modal with language tabs (en, fr, de, lb)
- Platform-only indicator badge
- Variable reference panel
- HTML preview in iframe
- Send test email functionality
Vendor UI
Page: /vendor/{vendor_code}/email-templates
Template: app/templates/vendor/email-templates.html
JavaScript: static/vendor/js/email-templates.js
Features:
- List of overridable templates with customization status
- Language override badges (green = customized)
- Edit modal with:
- Language tabs
- Source indicator (vendor override vs platform default)
- Platform template reference
- Revert to default button
- Preview and test email functionality
Template Categories
| Category | Description | Platform-Only |
|---|---|---|
| AUTH | Authentication emails (welcome, password reset) | No |
| ORDERS | Order-related emails (confirmation, shipped) | No |
| BILLING | Subscription/payment emails | Yes |
| SYSTEM | System emails (team invites, alerts) | No |
| MARKETING | Marketing/promotional emails | No |
Available Templates
Customer-Facing (Overridable)
| Code | Category | Languages | Description |
|---|---|---|---|
signup_welcome |
AUTH | en, fr, de, lb | Welcome email after vendor signup |
order_confirmation |
ORDERS | en, fr, de, lb | Order confirmation to customer |
password_reset |
AUTH | en, fr, de, lb | Password reset link |
team_invite |
SYSTEM | en | Team member invitation |
Platform-Only (Not Overridable)
| Code | Category | Languages | Description |
|---|---|---|---|
subscription_welcome |
BILLING | en | Subscription confirmation |
payment_failed |
BILLING | en | Failed payment notification |
subscription_cancelled |
BILLING | en | Cancellation confirmation |
trial_ending |
BILLING | en | Trial ending reminder |
Template Variables
Common Variables (Injected Automatically)
| Variable | Description |
|---|---|
platform_name |
"Wizamart" or vendor name (whitelabel) |
platform_logo_url |
Platform logo URL |
support_email |
Support email address |
vendor_name |
Vendor business name |
vendor_logo_url |
Vendor logo URL |
Template-Specific Variables
signup_welcome
first_name,company_name,email,vendor_codelogin_url,trial_days,tier_name
order_confirmation
customer_name,order_number,order_totalorder_items_count,order_date,shipping_address
password_reset
customer_name,reset_link,expiry_hours
team_invite
invitee_name,inviter_name,vendor_namerole,accept_url,expires_in_days
Migration
File: alembic/versions/u9c0d1e2f3g4_add_vendor_email_templates.py
Run migration:
alembic upgrade head
The migration:
- Adds
is_platform_onlyandrequired_variablescolumns toemail_templates - Creates
vendor_email_templatestable - Adds unique constraint on
(vendor_id, template_code, language) - Creates indexes for performance
Seeding Templates
File: scripts/seed_email_templates.py
Run seed script:
python scripts/seed_email_templates.py
The script:
- Creates/updates all platform templates
- Supports all 4 languages for customer-facing templates
- Sets
is_platform_onlyflag for billing templates
Security Considerations
- XSS Prevention: HTML templates are rendered server-side with Jinja2 escaping
- Access Control: Vendors can only view/edit their own overrides
- Platform-only Protection: API enforces
is_platform_onlyflag - Template Validation: Jinja2 syntax validated before save
- Rate Limiting: Test email sending subject to rate limits
- Token Hashing: Password reset tokens stored as SHA256 hashes
Usage Examples
Sending a Template Email
from app.services.email_service import EmailService
email_svc = EmailService(db)
email_log = email_svc.send_template(
template_code="order_confirmation",
to_email="customer@example.com",
variables={
"customer_name": "John Doe",
"order_number": "ORD-12345",
"order_total": "€99.99",
"order_items_count": "3",
"order_date": "2024-01-15",
"shipping_address": "123 Main St, Luxembourg"
},
vendor_id=vendor.id, # Optional: enables vendor override lookup
customer_id=customer.id, # Optional: for language resolution
language="fr" # Optional: explicit language override
)
Creating a Vendor Override
from models.database.vendor_email_template import VendorEmailTemplate
override = VendorEmailTemplate.create_or_update(
db=db,
vendor_id=vendor.id,
template_code="order_confirmation",
language="fr",
subject="Confirmation de votre commande {{ order_number }}",
body_html="<html>...</html>",
body_text="Plain text version..."
)
db.commit()
Reverting to Platform Default
VendorEmailTemplate.delete_override(
db=db,
vendor_id=vendor.id,
template_code="order_confirmation",
language="fr"
)
db.commit()
File Structure
├── alembic/versions/
│ └── u9c0d1e2f3g4_add_vendor_email_templates.py
├── app/
│ ├── api/v1/
│ │ ├── admin/
│ │ │ └── email_templates.py
│ │ └── vendor/
│ │ └── email_templates.py
│ ├── routes/
│ │ ├── admin_pages.py (route added)
│ │ └── vendor_pages.py (route added)
│ ├── services/
│ │ ├── email_service.py (enhanced)
│ │ └── email_template_service.py (new - business logic)
│ └── templates/
│ ├── admin/
│ │ ├── email-templates.html
│ │ └── partials/sidebar.html (link added)
│ └── vendor/
│ ├── email-templates.html
│ └── partials/sidebar.html (link added)
├── models/
│ ├── database/
│ │ ├── email.py (enhanced)
│ │ └── vendor_email_template.py
│ └── schema/
│ └── email.py
├── scripts/
│ └── seed_email_templates.py (enhanced)
└── static/
├── admin/js/
│ └── email-templates.js
└── vendor/js/
└── email-templates.js
Related Documentation
- Email Templates User Guide - How to use the email template system
- Password Reset Implementation - Password reset feature using email templates
- Architecture Fixes (January 2026) - Architecture validation fixes