diff --git a/docs/implementation/email-templates-architecture.md b/docs/implementation/email-templates-architecture.md index f666ff3b..d3d4b57b 100644 --- a/docs/implementation/email-templates-architecture.md +++ b/docs/implementation/email-templates-architecture.md @@ -1,289 +1,396 @@ -# Email System Enhancements - Implementation Plan +# Email Template System ## Overview -Enhance the email system to support: -- Platform-level templates with vendor overrides -- Wizamart branding by default (removed for Enterprise whitelabel tier) -- Platform-only templates that cannot be overridden -- Admin UI for editing platform templates -- 4-language support (en, fr, de, lb) -- Smart language resolution (customer → vendor → platform default) +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) --- -## Phase 1: Database & Model Foundation +## Architecture -### 1.1 Update EmailTemplate Model +### Database Models +#### EmailTemplate (Platform Templates) **File:** `models/database/email.py` -Add columns: -- `is_platform_only: bool = False` - Cannot be overridden by vendors -- `required_variables: JSON` - List of variables that must be provided +| 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 | -```python -is_platform_only = Column(Boolean, default=False, nullable=False) -required_variables = Column(JSON, default=list) -``` - -### 1.2 Create VendorEmailTemplate Model +**Key Methods:** +- `get_by_code_and_language(db, code, language)` - Get specific template +- `get_overridable_templates(db)` - Get templates vendors can customize +#### VendorEmailTemplate (Vendor Overrides) **File:** `models/database/vendor_email_template.py` -```python -class VendorEmailTemplate(Base): - __tablename__ = "vendor_email_templates" +| 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 | - id: int - vendor_id: int (FK → vendors.id) - template_code: str # References EmailTemplate.code - language: str # en, fr, de, lb - subject: str - body_html: str - body_text: str - is_active: bool = True - created_at: datetime - updated_at: datetime +**Key Methods:** +- `get_override(db, vendor_id, code, language)` - Get vendor override +- `create_or_update(db, vendor_id, code, language, ...)` - Upsert override +- `delete_override(db, vendor_id, code, language)` - Revert to platform default +- `get_all_overrides_for_vendor(db, vendor_id)` - List all vendor overrides - # Unique constraint: (vendor_id, template_code, language) +### Unique Constraint +```sql +UNIQUE (vendor_id, template_code, language) ``` -### 1.3 Migration - -**File:** `alembic/versions/xxx_add_vendor_email_templates.py` - -- Add columns to email_templates table -- Create vendor_email_templates table -- Add indexes for lookup performance - --- -## Phase 2: Email Service Enhancement - -### 2.1 Language Resolution - -**Priority order for customer-facing emails:** -1. `customer.preferred_language` (if customer exists) -2. `vendor.storefront_language` -3. Platform default (`en`) - -**Priority order for vendor-facing emails:** -1. `vendor.preferred_language` -2. Platform default (`en`) - -### 2.2 Template Resolution +## Email Service **File:** `app/services/email_service.py` +### Language Resolution + +Priority order for determining email language: + +1. **Customer preferred language** (if customer exists) +2. **Vendor storefront language** (vendor.storefront_language) +3. **Platform default** (`en`) + +```python +def resolve_language( + self, + customer_id: int | None, + vendor_id: int | None, + explicit_language: str | None = None +) -> str +``` + +### Template Resolution + ```python def resolve_template( self, template_code: str, language: str, vendor_id: int | None = None -) -> tuple[str, str, str]: # subject, body_html, body_text - """ - Resolve template with vendor override support. - - 1. If vendor_id provided and template not platform-only: - - Look for VendorEmailTemplate override - - Fall back to platform EmailTemplate - 2. If no vendor or platform-only: - - Use platform EmailTemplate - 3. Language fallback: requested → en - """ +) -> ResolvedTemplate ``` -### 2.3 Branding Resolution +Resolution order: +1. If `vendor_id` provided and template **not** platform-only: + - Look for `VendorEmailTemplate` override + - Fall back to platform `EmailTemplate` +2. If no vendor or platform-only: + - Use platform `EmailTemplate` +3. Language fallback: `requested_language` → `en` + +### Branding Resolution ```python -def get_branding(self, vendor_id: int | None) -> dict: - """ - Get branding variables for email. - - Returns: - - platform_name: "Wizamart" or vendor.name (if whitelabel) - - platform_logo: Wizamart logo or vendor logo (if whitelabel) - - support_email: platform or vendor support email - - vendor_name: Always vendor.name - - vendor_logo: Always vendor logo - """ - if vendor_id: - vendor = self._get_vendor(vendor_id) - if has_feature(vendor, "white_label"): - return { - "platform_name": vendor.name, - "platform_logo": vendor.logo_url, - ... - } - return { - "platform_name": "Wizamart", - "platform_logo": PLATFORM_LOGO_URL, - ... - } +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. + --- -## Phase 3: Admin API & UI +## API Endpoints -### 3.1 Admin API Endpoints +### Admin API **File:** `app/api/v1/admin/email_templates.py` | Method | Endpoint | Description | |--------|----------|-------------| -| GET | `/email-templates` | List all platform templates | -| GET | `/email-templates/{code}` | Get template (all languages) | -| PUT | `/email-templates/{code}/{language}` | Update template | -| POST | `/email-templates/{code}/preview` | Preview with sample data | -| POST | `/email-templates/{code}/test` | Send test email | +| 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 | -### 3.2 Admin UI - -**File:** `app/templates/admin/email-templates.html` - -Features: -- List view with template codes, categories, language badges -- Edit view with: - - Language tabs (en, fr, de, lb) - - Subject field - - Rich text editor for body_html - - Plain text preview for body_text - - Variable reference panel - - Preview pane - - Test send button -- Platform-only badge/indicator -- Required variables display - ---- - -## Phase 4: Vendor API & UI - -### 4.1 Vendor API Endpoints +### Vendor API **File:** `app/api/v1/vendor/email_templates.py` | Method | Endpoint | Description | |--------|----------|-------------| -| GET | `/email-templates` | List overridable templates | -| GET | `/email-templates/{code}` | Get vendor override or default | -| PUT | `/email-templates/{code}/{language}` | Create/update override | -| DELETE | `/email-templates/{code}/{language}` | Reset to platform default | -| POST | `/email-templates/{code}/preview` | Preview with vendor branding | -| POST | `/email-templates/{code}/test` | Send test email | +| 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 | -### 4.2 Vendor UI +--- -**File:** `app/templates/vendor/email-templates.html` +## User Interface + +### Admin UI + +**Page:** `/admin/email-templates` +**Template:** `app/templates/admin/email-templates.html` +**JavaScript:** `static/admin/js/email-templates.js` Features: -- List view with templates, showing "Customized" vs "Using Default" -- Edit view similar to admin but: - - Can only edit non-platform-only templates - - "Reset to Default" button - - Shows platform default as reference -- Tier gating: Only accessible for Business+ tiers +- 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 --- -## Phase 5: Missing Templates +## Template Categories -### Platform-Only Templates (Billing/Subscription) - -| Code | Category | Description | -|------|----------|-------------| -| `tier_upgrade` | BILLING | Vendor upgraded subscription | -| `tier_downgrade` | BILLING | Vendor downgraded subscription | -| `payment_failed` | BILLING | Subscription payment failed | -| `subscription_cancelled` | BILLING | Subscription cancelled | -| `trial_ending` | BILLING | Trial period ending soon | -| `usage_limit_warning` | BILLING | Approaching usage limits | - -### Overridable Templates - -| Code | Category | Description | -|------|----------|-------------| -| `team_invite` | AUTH | Team member invitation | -| `order_shipped` | ORDERS | Order has shipped | -| `shipping_update` | ORDERS | Shipping status update | -| `low_stock_alert` | SYSTEM | Low inventory warning | +| 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 | --- -## Language Support Matrix +## Available Templates -| Language | Code | Platform Default | -|----------|------|------------------| -| English | `en` | Yes (fallback) | -| French | `fr` | No | -| German | `de` | No | -| Luxembourgish | `lb` | No | +### Customer-Facing (Overridable) -**Fallback Logic:** -``` -requested_language → en (if template not found) +| 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_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`, `vendor_name` +- `role`, `accept_url`, `expires_in_days` + +--- + +## Migration + +**File:** `alembic/versions/u9c0d1e2f3g4_add_vendor_email_templates.py` + +Run migration: +```bash +alembic upgrade head ``` ---- - -## Files to Create/Modify - -### Phase 1 (Database) -| File | Action | -|------|--------| -| `alembic/versions/xxx_add_vendor_email_templates.py` | Create | -| `models/database/email.py` | Modify | -| `models/database/vendor_email_template.py` | Create | -| `models/database/__init__.py` | Modify | -| `models/schema/email.py` | Create | - -### Phase 2 (Service) -| File | Action | -|------|--------| -| `app/services/email_service.py` | Modify | - -### Phase 3 (Admin) -| File | Action | -|------|--------| -| `app/api/v1/admin/email_templates.py` | Create | -| `app/api/v1/admin/__init__.py` | Modify | -| `app/templates/admin/email-templates.html` | Create | -| `app/routes/admin_pages.py` | Modify | -| `static/admin/js/email-templates.js` | Create | - -### Phase 4 (Vendor) -| File | Action | -|------|--------| -| `app/api/v1/vendor/email_templates.py` | Create | -| `app/api/v1/vendor/__init__.py` | Modify | -| `app/templates/vendor/email-templates.html` | Create | -| `app/routes/vendor_pages.py` | Modify | -| `static/vendor/js/email-templates.js` | Create | - -### Phase 5 (Templates) -| File | Action | -|------|--------| -| `scripts/seed_email_templates.py` | Modify | +The migration: +1. Adds `is_platform_only` and `required_variables` columns to `email_templates` +2. Creates `vendor_email_templates` table +3. Adds unique constraint on `(vendor_id, template_code, language)` +4. Creates indexes for performance --- -## Execution Order +## Seeding Templates -1. **Phase 1**: Database migration and models (~30 min) -2. **Phase 2**: Email service enhancement (~45 min) -3. **Phase 3**: Admin API and UI (~1-2 hours) -4. **Phase 4**: Vendor API and UI (~1-2 hours) -5. **Phase 5**: Add missing templates (~30 min) -6. **Testing**: Integration tests for all phases +**File:** `scripts/seed_email_templates.py` + +Run seed script: +```bash +python scripts/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**: Sanitize HTML in templates before storage -2. **Access Control**: Vendors can only edit their own templates -3. **Platform-only Protection**: API enforces is_platform_only flag -4. **Template Validation**: Ensure required variables are present in template -5. **Rate Limiting**: Test email sending limited to prevent abuse +1. **XSS Prevention**: HTML templates are rendered server-side with Jinja2 escaping +2. **Access Control**: Vendors 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 + +```python +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 + +```python +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="...", + body_text="Plain text version..." +) +db.commit() +``` + +### Reverting to Platform Default + +```python +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) +│ └── 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 +```