Files
classroomio/prd/email-template-customization/README.md
T

344 lines
13 KiB
Markdown
Raw Normal View History

2026-03-09 11:18:22 +01:00
# Email Template Customization and Translation PRD
## Purpose
Enable organization admins to manage outbound email templates without code changes by editing:
- Email logo
- Email content
- Whether each email should be sent
This also introduces localization so email content is not hardcoded to English only.
## Problem Statement
Today, all templates in `packages/email/src/emails/*.ts` are:
- Hardcoded in English
- Hardcoded in code (no admin editing surface)
- Always sent when the related business event occurs
- Coupled to a static ClassroomIO logo in `getDefaultTemplate(...)`
This limits localization, white-labeling, and product control.
## Decision Summary
- v1 allows editing system-managed email template IDs only.
- Org admins cannot create new email event types or custom triggers from the UI.
- Engineering can add new system email IDs where legacy raw emails still exist.
- v1 supports per-template:
- `logo`
- `content`
- `isEnabled` (send toggle)
- v1 supports locale variants with fallback behavior.
## Current-State Audit
Legend: `available now`, `partial`, `missing`
| Capability | Availability | Current State | Effort | Should Implement |
| --- | --- | --- | --- | --- |
| Edit logo per template | missing | Static logo URL in default wrapper | Medium | [x] |
| Edit content per template | missing | HTML strings are hardcoded in TS files | Medium | [x] |
| Per-email send toggle | missing | Email sends are trigger-based only | Medium | [x] |
| Multi-language email content | missing | English-only template copy | Medium | [x] |
| Create new email types | missing | Requires code updates in registry/constants | High | [ ] (v1 non-goal) |
| Template preview before save | missing | No preview API/UI | Low | [x] |
| Audit trail for template changes | missing | No history | Medium | [x] |
## Existing Email IDs (Current Catalog)
From `packages/email/src/utils/constants.ts`:
- `forgotPassword`
- `welcome`
- `verifyEmail`
- `onPasswordReset`
- `inviteTeacher`
- `newsfeedPost`
- `newsfeedComment`
- `studentCourseWelcome`
- `studentCourseInvite`
- `teacherStudentJoined`
- `teacherStudentBuyRequest`
- `studentProvePayment`
- `teacherCourseWelcome`
## Goals
1. Organization admins can configure logo, content, and send toggle for each existing email ID.
2. Admins can manage localized content using supported locales (`en`, `hi`, `fr`, `pt`, `de`, `vi`, `ru`, `es`, `pl`, `da`).
3. Send pipeline resolves the best template by organization + locale with safe fallback.
4. Template resolution should not break existing `sendEmail(...)` call sites.
5. Keep type-safe Zod field validation already provided by `@cio/email`.
6. Migrate raw `deliverEmail(...)` product notifications into managed template IDs.
## Non-Goals (v1)
- Letting admins create brand new email types, custom event triggers, or campaign flows.
- Editing `subject`, `from`, or `replyTo` in dashboard.
- Drag-and-drop email builder.
- Advanced analytics (open rate, click rate).
- Attachments.
## Legacy Raw Emails to Standardize
Current product emails bypassing template registry:
- `apps/api/src/services/submission/submission.ts`:
- student submission status update email
- teacher notification on new student submission
Plan:
- Create system-managed IDs for these flows.
- Move content to template registry + editable overrides.
- Keep behavior compatible while migrating call sites.
## Functional Requirements
### Template Editing
Per existing email ID, admins can:
- Upload/select logo URL
- Edit localized HTML content
- Enable or disable sending
### Localization
- Template content is saved per locale.
- Fallback order at send time:
1. Org + emailId + requested locale
2. Org + emailId + `en`
3. Code default template renderer (current hardcoded fallback)
### Send Toggle
- If template for an email ID is disabled, send is skipped and logged.
- Skip must not fail business actions (invites, onboarding, etc.).
- `required` email IDs can be protected from disabling (recommendation below).
### Placeholder Safety
- Content can use variable placeholders from each template schema (for example `{{inviteLink}}`, `{{studentName}}`).
- Save validation rejects unknown placeholders for the selected email ID.
- Render-time validation continues using existing Zod schema.
## Product Rules (Recommended)
1. Email IDs classified as:
- `required`: `forgotPassword`, `verifyEmail`, `onPasswordReset`
- `optional`: all remaining IDs
2. `required` IDs cannot be disabled in UI/API.
3. Missing localized content shows explicit fallback badge (`Using English fallback`).
## Proposed Architecture
### Data Model (New)
#### `organization_email_template`
Organization-scoped override rows for existing email IDs.
Suggested fields:
- `id` (uuid, pk)
- `organization_id` (uuid, fk -> `organization.id`, indexed)
- `email_id` (text, constrained to known email IDs)
- `locale` (`LOCALE`, default `en`)
- `is_enabled` (boolean, default `true`)
- `logo_url` (text, nullable)
- `content` (text, nullable; null means fallback to code default content)
- `updated_by_profile_id` (uuid, fk -> `profile.id`)
- `created_at`, `updated_at`
Constraints:
- unique `(organization_id, email_id, locale)`
- index `(organization_id, email_id)`
- index `(organization_id, locale)`
Notes:
- Row presence means override intent.
- `content = null` allows logo/toggle-only overrides.
#### `organization_email_template_audit`
Append-only audit trail of changes.
Suggested fields:
- `id` (uuid, pk)
- `organization_id` (uuid, indexed)
- `email_template_id` (uuid, fk -> `organization_email_template.id`)
- `action` (`CREATED` | `UPDATED` | `DELETED` | `ENABLED` | `DISABLED`)
- `before` (jsonb)
- `after` (jsonb)
- `actor_profile_id` (uuid)
- `created_at`
## Backend Plan
### Validation (`packages/utils/src/validation/email-template/`)
- `ZListEmailTemplatesQuery`
- `ZGetEmailTemplateParams`
- `ZUpsertEmailTemplate`
- `ZDeleteEmailTemplate`
- `ZPreviewEmailTemplate`
`ZUpsertEmailTemplate` (v1):
- `emailId: EmailId`
- `locale: TLocale`
- `isEnabled?: boolean`
- `logoUrl?: string | null`
- `content?: string | null`
### Queries (`packages/db/src/queries/email-template/`)
- `listOrganizationEmailTemplates`
- `getOrganizationEmailTemplate`
- `upsertOrganizationEmailTemplate`
- `deleteOrganizationEmailTemplate`
- `createOrganizationEmailTemplateAudit`
- `getResolvedOrganizationEmailTemplate` (with fallback)
### Services (`apps/api/src/services/email-template/`)
- `listEmailTemplatesService`
- `getEmailTemplateService`
- `upsertEmailTemplateService`
- `deleteEmailTemplateService`
- `previewEmailTemplateService`
- `resolveEmailTemplateForSendService`
### Routes (`apps/api/src/routes/email-template/`)
Mounted under organization context:
- `GET /organization/email-templates`
- `GET /organization/email-templates/:emailId`
- `PUT /organization/email-templates/:emailId`
- `DELETE /organization/email-templates/:emailId`
- `POST /organization/email-templates/:emailId/preview`
All routes: `authMiddleware` + `orgAdminMiddleware`.
## Send Pipeline Integration
### Current
`sendEmail(emailId, { to, fields, ... })` renders from registered code template directly.
### Proposed
Add optional send context:
- `organizationId?: string`
- `locale?: TLocale`
Flow:
1. Existing template schema validation remains unchanged.
2. If `organizationId` exists, resolve override with locale fallback.
3. Determine final `isEnabled`.
4. If disabled, return skipped result `{ success: true, skipped: true, reason: 'TEMPLATE_DISABLED' }`.
5. Render final email wrapper with resolved `logoUrl` and `content` (or default fallback).
Implementation note:
- Keep backward compatibility by making new context optional.
- Existing call sites without org context continue to use default code templates.
## Frontend Plan (Dashboard)
### Route
- `apps/dashboard/src/routes/org/[slug]/settings/emails/+page.svelte`
### Navigation
Add settings tab item under org settings:
- title: `Emails`
- path: `/settings/emails`
### Feature Module
`apps/dashboard/src/lib/features/email-template/`
- `api/email-template.svelte.ts`
- `utils/types.ts`
- `utils/email-template-utils.ts`
- `components/email-template-list.svelte`
- `components/email-template-editor.svelte`
- `components/email-template-preview.svelte`
- `pages/email-template-settings.svelte`
### UX (v1)
- Left panel: list of existing email IDs + enabled status.
- Top controls: locale selector + fallback indicator.
- Editor:
- logo upload/URL
- content editor (HTML)
- placeholder helper for selected email ID
- enable/disable switch
- Preview pane renders current locale result.
- Save + Reset actions.
### Copy and Translations
- No hardcoded UI strings.
- Add keys in `apps/dashboard/src/lib/utils/translations/en.json` and other locales as needed.
## Phased Rollout
### Phase 0: Foundation
1. DB tables + migration.
2. Validation + queries + services + routes.
3. Add metadata catalog for email IDs and allowed placeholders.
4. Add system-managed IDs for legacy raw submission emails.
### Phase 1: Dashboard Management
1. Add org settings email page.
2. List, edit, save, reset template overrides.
3. Preview endpoint + preview UI.
### Phase 2: Send Pipeline Adoption
1. Add optional `organizationId`/`locale` context in send flow.
2. Update org-scoped call sites to pass context.
3. Add skip logging for disabled templates.
### Phase 3: Hardening
1. Audit logs UI/read endpoint.
2. Placeholder validation improvements and warnings.
3. Performance optimizations and caching.
## Migration Plan
No hard data migration is required for v1 because defaults already exist in code.
Rollout strategy:
1. Ship schema and API with no behavioral change.
2. Ship UI and allow override creation.
3. Enable send-time override resolution.
4. Incrementally update send call sites to pass org/locale context.
## Risks and Mitigations
- Risk: Admin enters broken HTML.
- Mitigation: sanitize on save + render preview with warnings.
- Risk: Required variables removed from content.
- Mitigation: placeholder validation against schema keys.
- Risk: Disabling critical auth emails blocks account flows.
- Mitigation: `required` email IDs are non-disableable.
- Risk: Missing org context in some send call sites.
- Mitigation: safe fallback to existing code templates until upgraded.
- Risk: Large refactor footprint across services.
- Mitigation: optional send context and gradual adoption by domain.
## Success Metrics
- At least 80% of active organizations with non-default email templates in first 60 days.
- At least 90% of org-scoped email sends resolved through template override service.
- Zero critical auth incidents caused by template disable misconfiguration.
- Reduction in support requests about email branding/language by at least 50%.
## Open Questions
1. Should `welcome` email be org-customizable or remain global/system-owned?
2. Do we allow plain-text editor mode in addition to HTML mode?
3. Should logo override be per-template or org-global default with optional per-template override?
4. Should locale resolution use recipient profile locale automatically when context locale is omitted?
5. Do we need test-send endpoint in v1 or can preview-only ship first?
## Kanban Board (Ideation Phase)
| Ticket | Title | Todo | In Progress | Verification | Done |
| --- | --- | --- | --- | --- | --- |
| ET-1 | DB: add `organization_email_template` table | [ ] | [ ] | [ ] | [ ] |
| ET-2 | DB: add `organization_email_template_audit` table | [ ] | [ ] | [ ] | [ ] |
| ET-3 | Validation schemas for email-template APIs | [ ] | [ ] | [ ] | [ ] |
| ET-4 | Queries for template CRUD + resolve fallback | [ ] | [ ] | [ ] | [ ] |
| ET-5 | Service layer for list/get/upsert/delete/preview | [ ] | [ ] | [ ] | [ ] |
| ET-6 | Organization routes `/email-templates` | [ ] | [ ] | [ ] | [ ] |
| ET-7 | Dashboard settings page for templates | [ ] | [ ] | [ ] | [ ] |
| ET-8 | Locale-aware editor and fallback indicators | [ ] | [ ] | [ ] | [ ] |
| ET-9 | Preview rendering endpoint + UI pane | [ ] | [ ] | [ ] | [ ] |
| ET-10 | Send pipeline context and override resolution | [ ] | [ ] | [ ] | [ ] |
| ET-11 | Update org-scoped send call sites | [ ] | [ ] | [ ] | [ ] |
| ET-12 | Required-email toggle guardrails | [ ] | [ ] | [ ] | [ ] |
| ET-13 | Audit log write/read integration | [ ] | [ ] | [ ] | [ ] |
| ET-14 | Caching/performance pass | [ ] | [ ] | [ ] | [ ] |
| ET-15 | Migrate raw submission emails to managed template IDs | [ ] | [ ] | [ ] | [ ] |
## Verification Checklist
- [ ] `pnpm --filter @cio/db build`
- [ ] `pnpm --filter @cio/utils build`
- [ ] `pnpm --filter @cio/email build`
- [ ] `pnpm --filter @cio/api build`
- [ ] `pnpm --filter @cio/dashboard build`
- [ ] Manual: edit template logo + save + preview
- [ ] Manual: edit localized content + locale fallback behavior
- [ ] Manual: disable optional email and verify skip behavior
- [ ] Manual: ensure required emails cannot be disabled
- [ ] Manual: send flow still works for routes not yet migrated to org context