"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.emailTemplatesService = exports.EmailTemplatesService = void 0; const client_1 = require("@prisma/client"); const logger_1 = require("../../utils/logger"); const email_service_1 = require("../../services/email.service"); const prisma = new client_1.PrismaClient(); class EmailTemplatesService { /** * List email templates with pagination, search, and filters */ async list(params) { const { page, limit, search, category, isActive } = params; const skip = (page - 1) * limit; // Build where clause const where = { ...(search && { OR: [ { key: { contains: search, mode: 'insensitive' } }, { name: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, ], }), ...(category && { category }), ...(isActive !== undefined && { isActive }), }; const [data, total] = await Promise.all([ prisma.emailTemplate.findMany({ where, skip, take: limit, orderBy: [{ isSystem: 'desc' }, { category: 'asc' }, { name: 'asc' }], include: { variables: { orderBy: { sortOrder: 'asc' }, }, _count: { select: { versions: true, testLogs: true, }, }, createdBy: { select: { id: true, name: true, email: true }, }, updatedBy: { select: { id: true, name: true, email: true }, }, }, }), prisma.emailTemplate.count({ where }), ]); return { templates: data, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; } /** * Get a single email template by ID */ async getById(id) { const template = await prisma.emailTemplate.findUnique({ where: { id }, include: { variables: { orderBy: { sortOrder: 'asc' }, }, _count: { select: { versions: true, testLogs: true, }, }, createdBy: { select: { id: true, name: true, email: true }, }, updatedBy: { select: { id: true, name: true, email: true }, }, }, }); if (!template) { throw new Error('Template not found'); } return template; } /** * Get template by key */ async getByKey(key) { const template = await prisma.emailTemplate.findUnique({ where: { key }, include: { variables: { orderBy: { sortOrder: 'asc' }, }, }, }); return template; } /** * Create a new email template */ async create(data, userId) { // Check for duplicate key const existing = await prisma.emailTemplate.findUnique({ where: { key: data.key }, }); if (existing) { throw new Error(`Template with key "${data.key}" already exists`); } // Validate template content const validation = this.validateTemplate({ htmlContent: data.htmlContent, textContent: data.textContent, subjectLine: data.subjectLine, }); if (!validation.valid) { throw new Error(`Template validation failed: ${validation.errors.join(', ')}`); } // Create template with variables and initial version in transaction const template = await prisma.$transaction(async (tx) => { const newTemplate = await tx.emailTemplate.create({ data: { key: data.key, name: data.name, description: data.description, category: data.category, subjectLine: data.subjectLine, htmlContent: data.htmlContent, textContent: data.textContent, isSystem: false, // User-created templates are never system templates isActive: data.isActive ?? true, createdByUserId: userId, variables: data.variables ? { create: data.variables, } : undefined, }, include: { variables: true, }, }); // Create initial version await tx.emailTemplateVersion.create({ data: { templateId: newTemplate.id, versionNumber: 1, subjectLine: data.subjectLine, htmlContent: data.htmlContent, textContent: data.textContent, changeNotes: 'Initial version', createdByUserId: userId, }, }); return newTemplate; }); logger_1.logger.info(`Email template created: ${template.key} by user ${userId}`); // Clear email service cache for this template email_service_1.emailService.clearDatabaseCache(template.key); return template; } /** * Update an email template */ async update(id, data, userId) { const existing = await this.getById(id); // Validate template content if provided if (data.htmlContent || data.textContent) { const validation = this.validateTemplate({ htmlContent: data.htmlContent ?? existing.htmlContent, textContent: data.textContent ?? existing.textContent, subjectLine: data.subjectLine ?? existing.subjectLine, }); if (!validation.valid) { throw new Error(`Template validation failed: ${validation.errors.join(', ')}`); } } // Update template and create new version if content changed const template = await prisma.$transaction(async (tx) => { // Update template const updated = await tx.emailTemplate.update({ where: { id }, data: { ...(data.name && { name: data.name }), ...(data.description !== undefined && { description: data.description }), ...(data.category && { category: data.category }), ...(data.subjectLine && { subjectLine: data.subjectLine }), ...(data.htmlContent && { htmlContent: data.htmlContent }), ...(data.textContent && { textContent: data.textContent }), ...(data.isActive !== undefined && { isActive: data.isActive }), updatedByUserId: userId, // Handle variables update ...(data.variables && { variables: { deleteMany: {}, create: data.variables, }, }), }, include: { variables: true, }, }); // Create new version if content changed if (data.subjectLine || data.htmlContent || data.textContent) { const latestVersion = await tx.emailTemplateVersion.findFirst({ where: { templateId: id }, orderBy: { versionNumber: 'desc' }, }); const nextVersionNumber = (latestVersion?.versionNumber ?? 0) + 1; await tx.emailTemplateVersion.create({ data: { templateId: id, versionNumber: nextVersionNumber, subjectLine: data.subjectLine ?? existing.subjectLine, htmlContent: data.htmlContent ?? existing.htmlContent, textContent: data.textContent ?? existing.textContent, changeNotes: `Updated via admin interface`, createdByUserId: userId, }, }); logger_1.logger.info(`Created version ${nextVersionNumber} for template ${existing.key}`); } return updated; }); logger_1.logger.info(`Email template updated: ${existing.key} by user ${userId}`); // Clear email service cache email_service_1.emailService.clearDatabaseCache(existing.key); return template; } /** * Delete an email template */ async delete(id) { const existing = await this.getById(id); if (existing.isSystem) { throw new Error('Cannot delete system templates'); } await prisma.emailTemplate.delete({ where: { id }, }); logger_1.logger.info(`Email template deleted: ${existing.key}`); // Clear email service cache email_service_1.emailService.clearDatabaseCache(existing.key); } /** * Get version history for a template */ async getVersions(templateId) { const versions = await prisma.emailTemplateVersion.findMany({ where: { templateId }, orderBy: { versionNumber: 'desc' }, include: { createdBy: { select: { id: true, name: true, email: true }, }, }, }); return versions; } /** * Get a specific version */ async getVersion(templateId, versionNumber) { const version = await prisma.emailTemplateVersion.findUnique({ where: { templateId_versionNumber: { templateId, versionNumber, }, }, include: { createdBy: { select: { id: true, name: true, email: true }, }, }, }); if (!version) { throw new Error('Version not found'); } return version; } /** * Rollback to a previous version */ async rollbackToVersion(templateId, data, userId) { const template = await this.getById(templateId); const targetVersion = await this.getVersion(templateId, data.versionNumber); // Create new version with content from target version const result = await prisma.$transaction(async (tx) => { const latestVersion = await tx.emailTemplateVersion.findFirst({ where: { templateId }, orderBy: { versionNumber: 'desc' }, }); const nextVersionNumber = (latestVersion?.versionNumber ?? 0) + 1; // Update template to target version content const updated = await tx.emailTemplate.update({ where: { id: templateId }, data: { subjectLine: targetVersion.subjectLine, htmlContent: targetVersion.htmlContent, textContent: targetVersion.textContent, updatedByUserId: userId, }, }); // Create new version (rollback creates a new version, doesn't revert history) await tx.emailTemplateVersion.create({ data: { templateId, versionNumber: nextVersionNumber, subjectLine: targetVersion.subjectLine, htmlContent: targetVersion.htmlContent, textContent: targetVersion.textContent, changeNotes: data.changeNotes ?? `Rolled back to version ${data.versionNumber}`, createdByUserId: userId, }, }); return updated; }); logger_1.logger.info(`Template ${template.key} rolled back to version ${data.versionNumber} by user ${userId}`); // Clear email service cache email_service_1.emailService.clearDatabaseCache(template.key); return result; } /** * Validate template syntax */ validateTemplate(data) { const errors = []; const warnings = []; const variables = new Set(); // Extract variables from content const variableRegex = /\{\{([A-Z_]+)\}\}/g; const conditionalRegex = /\{\{#if\s+([A-Z_]+)\}\}|\{\{\/if\}\}/g; // Check HTML content let match; while ((match = variableRegex.exec(data.htmlContent)) !== null) { variables.add(match[1]); } while ((match = conditionalRegex.exec(data.htmlContent)) !== null) { if (match[1]) variables.add(match[1]); } // Check text content while ((match = variableRegex.exec(data.textContent)) !== null) { variables.add(match[1]); } while ((match = conditionalRegex.exec(data.textContent)) !== null) { if (match[1]) variables.add(match[1]); } // Check subject line if provided if (data.subjectLine) { while ((match = variableRegex.exec(data.subjectLine)) !== null) { variables.add(match[1]); } } // Check for unmatched conditionals const ifCount = (data.htmlContent.match(/\{\{#if/g) || []).length; const endifCount = (data.htmlContent.match(/\{\{\/if\}\}/g) || []).length; if (ifCount !== endifCount) { errors.push('Unmatched {{#if}} conditional blocks in HTML content'); } const ifCountText = (data.textContent.match(/\{\{#if/g) || []).length; const endifCountText = (data.textContent.match(/\{\{\/if\}\}/g) || []).length; if (ifCountText !== endifCountText) { errors.push('Unmatched {{#if}} conditional blocks in text content'); } // Warn if no variables found if (variables.size === 0) { warnings.push('No template variables found'); } return { valid: errors.length === 0, errors, warnings, extractedVariables: Array.from(variables), }; } /** * Send test email */ async sendTestEmail(templateId, data, userId) { const template = await this.getById(templateId); try { // Process template with test data let htmlContent = template.htmlContent; let textContent = template.textContent; let subjectLine = template.subjectLine; // Replace variables for (const [key, value] of Object.entries(data.testData)) { const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); htmlContent = htmlContent.replace(regex, value); textContent = textContent.replace(regex, value); subjectLine = subjectLine.replace(regex, value); } // Handle conditionals (simple implementation) for (const [key, value] of Object.entries(data.testData)) { const ifRegex = new RegExp(`\\{\\{#if\\s+${key}\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}`, 'g'); const shouldShow = value && value !== 'false' && value !== '0'; htmlContent = htmlContent.replace(ifRegex, shouldShow ? '$1' : ''); textContent = textContent.replace(ifRegex, shouldShow ? '$1' : ''); } // Send email via email service const result = await email_service_1.emailService.sendEmail({ to: data.recipientEmail, subject: subjectLine, text: textContent, html: htmlContent, }); // Log test email await prisma.emailTemplateTestLog.create({ data: { templateId, recipientEmail: data.recipientEmail, testData: data.testData, success: true, messageId: result.messageId, sentByUserId: userId, }, }); logger_1.logger.info(`Test email sent for template ${template.key} to ${data.recipientEmail}`); return { success: true, messageId: result.messageId }; } catch (error) { // Log failed test await prisma.emailTemplateTestLog.create({ data: { templateId, recipientEmail: data.recipientEmail, testData: data.testData, success: false, errorMessage: error instanceof Error ? error.message : String(error), sentByUserId: userId, }, }); logger_1.logger.error(`Test email failed for template ${template.key}: ${error}`); throw error; } } /** * Get test logs for a template */ async getTestLogs(templateId, limit = 10) { const logs = await prisma.emailTemplateTestLog.findMany({ where: { templateId }, orderBy: { sentAt: 'desc' }, take: limit, include: { sentBy: { select: { id: true, name: true, email: true }, }, }, }); return logs; } } exports.EmailTemplatesService = EmailTemplatesService; exports.emailTemplatesService = new EmailTemplatesService(); //# sourceMappingURL=email-templates.service.js.map