Files
orion/app/modules/cms/docs/email-templates.md
Samir Boulahtit f141cc4e6a docs: migrate module documentation to single source of truth
Move 39 documentation files from top-level docs/ into each module's
docs/ folder, accessible via symlinks from docs/modules/. Create
data-model.md files for 10 modules with full schema documentation.
Replace originals with redirect stubs. Remove empty guide stubs.

Modules migrated: tenancy, billing, loyalty, marketplace, orders,
messaging, cms, catalog, inventory, hosting, prospecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:38:37 +01:00

14 KiB

Email Template System

Overview

The email template system provides comprehensive email customization for the Orion platform with the following features:

  • Platform-level templates with store overrides
  • Orion branding by default (removed for Enterprise whitelabel tier)
  • Platform-only templates that cannot be overridden (billing, subscriptions)
  • Admin UI for editing platform templates
  • Store UI for customizing customer-facing emails
  • 4-language support (en, fr, de, lb)
  • Smart language resolution (customer → store → 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 stores
required_variables Text Comma-separated required variables

Key Methods:

  • get_by_code_and_language(db, code, language) - Get specific template
  • get_overridable_templates(db) - Get templates stores can customize

StoreEmailTemplate (Store Overrides)

File: models/database/store_email_template.py

Column Type Description
id Integer Primary key
store_id Integer FK to stores.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, store_id, code, language) - Get store override
  • create_or_update(db, store_id, code, language, ...) - Upsert override
  • delete_override(db, store_id, code, language) - Revert to platform default
  • get_all_overrides_for_store(db, store_id) - List all store overrides

Unique Constraint

UNIQUE (store_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

Store Methods

Method Description
list_overridable_templates(store_id) List templates store can customize
get_store_template(store_id, code, language) Get template (override or platform default)
create_or_update_store_override(store_id, code, language, data) Save store customization
delete_store_override(store_id, code, language) Revert to platform default
preview_store_template(store_id, code, language, variables) Preview with store branding

Usage Example

from app.services.email_template_service import EmailTemplateService

service = EmailTemplateService(db)

# List templates for admin
templates = service.list_platform_templates()

# Get store's view of a template
template_data = service.get_store_template(store_id, "order_confirmation", "fr")

# Create store override
service.create_or_update_store_override(
    store_id=store.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:

  1. Customer preferred language (if customer exists)
  2. Store storefront language (store.storefront_language)
  3. Platform default (en)
def resolve_language(
    self,
    customer_id: int | None,
    store_id: int | None,
    explicit_language: str | None = None
) -> str

Template Resolution

def resolve_template(
    self,
    template_code: str,
    language: str,
    store_id: int | None = None
) -> ResolvedTemplate

Resolution order:

  1. If store_id provided and template not platform-only:
    • Look for StoreEmailTemplate override
    • Fall back to platform EmailTemplate
  2. If no store or platform-only:
    • Use platform EmailTemplate
  3. Language fallback: requested_languageen

Branding Resolution

def get_branding(self, store_id: int | None) -> BrandingContext
Scenario Platform Name Platform Logo
No store Orion Orion logo
Standard store Orion Orion logo
Whitelabel store Store name Store logo

Whitelabel is determined by the white_label feature flag on the store.


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

Store API

File: app/api/v1/store/email_templates.py

Method Endpoint Description
GET /api/v1/store/email-templates List overridable templates
GET /api/v1/store/email-templates/{code} Get template with override status
GET /api/v1/store/email-templates/{code}/{language} Get specific language (override or default)
PUT /api/v1/store/email-templates/{code}/{language} Create/update override
DELETE /api/v1/store/email-templates/{code}/{language} Reset to platform default
POST /api/v1/store/email-templates/{code}/preview Preview with store branding
POST /api/v1/store/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

Store UI

Page: /store/{store_code}/email-templates Template: app/templates/store/email-templates.html JavaScript: static/store/js/email-templates.js

Features:

  • List of overridable templates with customization status
  • Language override badges (green = customized)
  • Edit modal with:
    • Language tabs
    • Source indicator (store 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 store 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 "Orion" or store name (whitelabel)
platform_logo_url Platform logo URL
support_email Support email address
store_name Store business name
store_logo_url Store logo URL

Template-Specific Variables

signup_welcome

  • first_name, merchant_name, email, store_code
  • login_url, trial_days, tier_name

order_confirmation

  • customer_name, order_number, order_total
  • order_items_count, order_date, shipping_address

password_reset

  • customer_name, reset_link, expiry_hours

team_invite

  • invitee_name, inviter_name, store_name
  • role, accept_url, expires_in_days

Migration

File: alembic/versions/u9c0d1e2f3g4_add_store_email_templates.py

Run migration:

alembic upgrade head

The migration:

  1. Adds is_platform_only and required_variables columns to email_templates
  2. Creates store_email_templates table
  3. Adds unique constraint on (store_id, template_code, language)
  4. Creates indexes for performance

Seeding Templates

File: scripts/seed/seed_email_templates.py

Run seed script:

python scripts/seed/seed_email_templates.py

The script:

  • Creates/updates all platform templates
  • Supports all 4 languages for customer-facing templates
  • Sets is_platform_only flag for billing templates

Security Considerations

  1. XSS Prevention: HTML templates are rendered server-side with Jinja2 escaping
  2. Access Control: Stores can only view/edit their own overrides
  3. Platform-only Protection: API enforces is_platform_only flag
  4. Template Validation: Jinja2 syntax validated before save
  5. Rate Limiting: Test email sending subject to rate limits
  6. 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"
    },
    store_id=store.id,  # Optional: enables store override lookup
    customer_id=customer.id,  # Optional: for language resolution
    language="fr"  # Optional: explicit language override
)

Creating a Store Override

from models.database.store_email_template import StoreEmailTemplate

override = StoreEmailTemplate.create_or_update(
    db=db,
    store_id=store.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

StoreEmailTemplate.delete_override(
    db=db,
    store_id=store.id,
    template_code="order_confirmation",
    language="fr"
)
db.commit()

File Structure

├── alembic/versions/
│   └── u9c0d1e2f3g4_add_store_email_templates.py
├── app/
│   ├── api/v1/
│   │   ├── admin/
│   │   │   └── email_templates.py
│   │   └── store/
│   │       └── email_templates.py
│   ├── routes/
│   │   ├── admin_pages.py  (route added)
│   │   └── store_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)
│       └── store/
│           ├── email-templates.html
│           └── partials/sidebar.html (link added)
├── models/
│   ├── database/
│   │   ├── email.py (enhanced)
│   │   └── store_email_template.py
│   └── schema/
│       └── email.py
├── scripts/
│   └── seed_email_templates.py (enhanced)
└── static/
    ├── admin/js/
    │   └── email-templates.js
    └── store/js/
        └── email-templates.js