# Template Version History ## Overview The Template Version History system provides comprehensive audit trails for email template changes with automatic version creation, rollback capability, and change tracking. Every template save creates a new version snapshot, preserving the complete history of modifications with metadata about who changed what and why. **Key Features:** - **Automatic Version Creation** — Every save creates a new version (no manual versioning) - **Auto-Incrementing Version Numbers** — Sequential numbering (1, 2, 3...) per template - **Complete Snapshots** — Stores subject line, HTML content, and text content - **Change Notes** — Optional admin-provided descriptions of changes - **User Attribution** — Tracks who created each version - **Rollback Capability** — Restore any previous version (non-destructive) - **Version Comparison** — Visual diff between any two versions - **Audit Trail** — Full history for compliance and debugging **Benefits:** - **Accident Recovery** — Undo mistakes by rolling back to previous version - **Change Tracking** — See what changed and when - **Compliance** — Audit trail for regulatory requirements - **Collaboration** — Multiple admins can see each other's changes - **Experimentation** — Safely test changes knowing you can rollback - **Documentation** — Change notes explain why changes were made --- ## Architecture ```mermaid flowchart TB subgraph "Version Creation Flow" Save[Admin Saves Template] FindMax[Find Max Version Number] Increment[Increment to Next Version] CreateVersion[Create EmailTemplateVersion] UpdateTemplate[Update EmailTemplate] Save --> FindMax FindMax --> Increment Increment --> CreateVersion CreateVersion --> UpdateTemplate end subgraph "Database Models" Template[(EmailTemplate)] Versions[(EmailTemplateVersion)] Template -->|1:N| Versions end subgraph "Version Data" Snapshot[Content Snapshot
subject, HTML, text] Meta[Metadata
version number, change notes] Attribution[Attribution
created by user, timestamp] Snapshot --> Versions Meta --> Versions Attribution --> Versions end subgraph "Version Operations" List[List Version History] Compare[Compare Two Versions] Rollback[Rollback to Version] View[View Version Details] Versions --> List Versions --> Compare Versions --> Rollback Versions --> View end subgraph "Rollback Flow" SelectVersion[Select Old Version] LoadContent[Load Old Content] UpdateCurrent[Update Current Template] CreateNewVersion[Create New Version
'Rolled back to vX'] SelectVersion --> LoadContent LoadContent --> UpdateCurrent UpdateCurrent --> CreateNewVersion CreateNewVersion --> Versions end Save --> Template CreateVersion --> Versions Rollback --> Template style Save fill:#4a90e2,color:#fff style CreateVersion fill:#50c878,color:#fff style Rollback fill:#ff6b6b,color:#fff ``` **Component Responsibilities:** - **EmailTemplateVersion** — Version snapshot storage with metadata - **Version Service** — Auto-increment logic, version creation - **Rollback Service** — Restore old version as new version (non-destructive) - **Comparison Service** — Diff generation between versions - **Audit Log** — User attribution and change notes --- ## Database Model ### EmailTemplateVersion Schema **Table:** `email_template_versions` | Field | Type | Description | |-------|------|-------------| | `id` | String (CUID) | Primary key | | `templateId` | String | Foreign key to EmailTemplate | | `versionNumber` | Int | Auto-incremented version (1, 2, 3...) | | `subjectLine` | String | Subject line snapshot | | `htmlContent` | Text | HTML content snapshot | | `textContent` | Text | Plain text content snapshot | | `changeNotes` | String (optional) | Admin-provided change description | | `createdByUserId` | String (optional) | User who created this version | | `createdAt` | DateTime | Version creation timestamp | **Relations:** - `template` — EmailTemplate (N:1) - `createdBy` — User (N:1) **Constraints:** - Unique index on `(templateId, versionNumber)` for version lookup - Auto-increment logic in service layer (finds max + 1) - No ON DELETE CASCADE (preserve versions even if template deleted) **Prisma Schema:** ```prisma model EmailTemplateVersion { id String @id @default(cuid()) templateId String versionNumber Int subjectLine String htmlContent String @db.Text textContent String @db.Text changeNotes String? createdByUserId String? createdAt DateTime @default(now()) template EmailTemplate @relation(fields: [templateId], references: [id]) createdBy User? @relation(fields: [createdByUserId], references: [id]) @@unique([templateId, versionNumber]) @@index([templateId]) @@index([createdAt]) @@map("email_template_versions") } ``` --- ## Version Creation ### Automatic Versioning on Save **When Versions Are Created:** - Admin saves template via EmailTemplateEditorPage - API `PUT /api/email-templates/:id` endpoint called - Version created BEFORE updating template (snapshot current state) **Auto-Increment Logic:** ```typescript // api/src/modules/email-templates/email-templates.service.ts async function createVersion( templateId: string, options: { changeNotes?: string; createdByUserId?: string; } ) { // 1. Find max version number for this template const maxVersion = await prisma.emailTemplateVersion.findFirst({ where: { templateId }, orderBy: { versionNumber: 'desc' }, select: { versionNumber: true }, }); const nextVersion = (maxVersion?.versionNumber || 0) + 1; // 2. Load current template content const template = await prisma.emailTemplate.findUnique({ where: { id: templateId }, }); if (!template) { throw new Error('Template not found'); } // 3. Create version snapshot const version = await prisma.emailTemplateVersion.create({ data: { templateId, versionNumber: nextVersion, subjectLine: template.subjectLine, htmlContent: template.htmlContent, textContent: template.textContent, changeNotes: options.changeNotes, createdByUserId: options.createdByUserId, }, }); return version; } ``` **Save Template with Versioning:** ```typescript // api/src/modules/email-templates/email-templates.routes.ts router.put('/:id', requireRole(SUPER_ADMIN), async (req, res) => { const { id } = req.params; const { subjectLine, htmlContent, textContent, changeNotes } = req.body; try { // 1. Create version BEFORE updating (snapshot current state) await createVersion(id, { changeNotes, createdByUserId: req.user!.id, }); // 2. Update template with new content const updatedTemplate = await prisma.emailTemplate.update({ where: { id }, data: { subjectLine, htmlContent, textContent, updatedByUserId: req.user!.id, }, }); res.json(updatedTemplate); } catch (error) { logger.error('Failed to save template', { error, templateId: id }); res.status(500).json({ error: 'Failed to save template' }); } }); ``` **Important:** Version is created BEFORE updating template, so version snapshots the OLD content (not the new content). This preserves the exact state before the change. --- ### Version Number Sequence **Sequence Rules:** - Starts at 1 for first version - Increments by 1 for each save - Per-template sequence (not global) - No gaps in sequence **Example Timeline:** | Action | Version | Subject | HTML | Change Notes | |--------|---------|---------|------|--------------| | Create template | 1 | "Welcome!" | `

Hello

` | (initial version) | | Edit subject | 2 | "Welcome to Our Platform!" | `

Hello

` | "Made subject more descriptive" | | Add content | 3 | "Welcome to Our Platform!" | `

Hello {{USER_NAME}}

` | "Added user name variable" | | Rollback to v1 | 4 | "Welcome!" | `

Hello

` | "Rolled back to version 1" | **Note:** Rollback creates NEW version (v4 in example), doesn't delete v2 and v3. This preserves complete audit trail. --- ### Change Notes **Purpose:** Describe what changed and why (audit trail documentation) **When Prompted:** - EmailTemplateEditorPage shows "Change Notes" field on save - Optional but recommended - Stored in `changeNotes` field **Examples:** **Good Change Notes:** ``` - "Added phone number conditional block" - "Fixed typo in subject line" - "Updated shift location variable to include address" - "Removed deprecated campaign URL variable" - "Rolled back to version 5 due to rendering issue" ``` **Poor Change Notes:** ``` - "update" (not descriptive) - "changes" (too vague) - "" (empty, no context) ``` **Implementation:** ```typescript // EmailTemplateEditorPage.tsx const [saveModalVisible, setSaveModalVisible] = useState(false); const [changeNotes, setChangeNotes] = useState(''); const handleSave = async () => { await api.put(`/api/email-templates/${id}`, { subjectLine, htmlContent, textContent, changeNotes: changeNotes || undefined, // Optional }); message.success('Template saved successfully'); navigate('/app/email-templates'); }; // Modal UI