changemaker.lite/api/dist/modules/email-templates/email-templates.service.js

467 lines
18 KiB
JavaScript

"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