295 lines
20 KiB
JavaScript
295 lines
20 KiB
JavaScript
#!/usr/bin/env tsx
|
|
"use strict";
|
|
/**
|
|
* Email Templates Seed Script
|
|
*
|
|
* Migrates existing email templates from filesystem to database.
|
|
* Run with: npx tsx api/src/scripts/seed-email-templates.ts
|
|
*/
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.seedEmailTemplates = seedEmailTemplates;
|
|
const client_1 = require("@prisma/client");
|
|
const fs = __importStar(require("fs"));
|
|
const path = __importStar(require("path"));
|
|
const TEMPLATES = [
|
|
{
|
|
key: 'campaign-email',
|
|
name: 'Campaign Email to Representative',
|
|
description: 'Email sent when a constituent contacts their elected representative through a campaign',
|
|
category: 'INFLUENCE',
|
|
subjectLine: '{{CAMPAIGN_TITLE}} - Message from {{USER_NAME}}',
|
|
isSystem: true,
|
|
variables: [
|
|
{ key: 'CAMPAIGN_TITLE', label: 'Campaign Title', description: 'Title of the advocacy campaign', isRequired: true, isConditional: false, sampleValue: 'Support Climate Action Bill C-12', sortOrder: 0 },
|
|
{ key: 'MESSAGE', label: 'Message Body', description: 'The message content written by the constituent', isRequired: true, isConditional: false, sampleValue: 'I urge you to support this important legislation that will help address climate change...', sortOrder: 1 },
|
|
{ key: 'USER_NAME', label: 'Sender Name', description: 'Name of the constituent sending the email', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 2 },
|
|
{ key: 'USER_EMAIL', label: 'Sender Email', description: 'Email address of the constituent', isRequired: true, isConditional: false, sampleValue: 'jane@example.com', sortOrder: 3 },
|
|
{ key: 'POSTAL_CODE', label: 'Postal Code', description: 'Postal code of the constituent', isRequired: true, isConditional: false, sampleValue: 'K1A 0B1', sortOrder: 4 },
|
|
{ key: 'RECIPIENT_NAME', label: 'Representative Name', description: 'Name of the representative receiving the email', isRequired: false, isConditional: true, sampleValue: 'Hon. John Smith', sortOrder: 5 },
|
|
{ key: 'RECIPIENT_LEVEL', label: 'Government Level', description: 'Level of government (Federal, Provincial, Municipal)', isRequired: false, isConditional: true, sampleValue: 'Federal', sortOrder: 6 },
|
|
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization running the campaign', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 7 },
|
|
{ key: 'TIMESTAMP', label: 'Timestamp', description: 'Date and time the email was sent', isRequired: true, isConditional: false, sampleValue: '2025-02-11T10:30:00Z', sortOrder: 8 },
|
|
],
|
|
},
|
|
{
|
|
key: 'response-verification',
|
|
name: 'Response Verification Email',
|
|
description: 'Email sent to representatives to verify a submitted response',
|
|
category: 'INFLUENCE',
|
|
subjectLine: 'Verify Your Response - {{CAMPAIGN_TITLE}}',
|
|
isSystem: true,
|
|
variables: [
|
|
{ key: 'CAMPAIGN_TITLE', label: 'Campaign Title', description: 'Title of the advocacy campaign', isRequired: true, isConditional: false, sampleValue: 'Support Climate Action Bill C-12', sortOrder: 0 },
|
|
{ key: 'RESPONSE_TYPE', label: 'Response Type', description: 'Type of response (EMAIL, LETTER, PHONE_CALL, MEETING, etc.)', isRequired: true, isConditional: false, sampleValue: 'SUPPORT', sortOrder: 1 },
|
|
{ key: 'RESPONSE_TEXT', label: 'Response Text', description: 'The text of the response submitted', isRequired: true, isConditional: false, sampleValue: 'I fully support this initiative and will be voting in favor...', sortOrder: 2 },
|
|
{ key: 'SUBMITTER_NAME', label: 'Submitter Name', description: 'Name of the person who submitted the response', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 3 },
|
|
{ key: 'SUBMITTED_DATE', label: 'Submitted Date', description: 'Date the response was submitted', isRequired: true, isConditional: false, sampleValue: 'February 11, 2025', sortOrder: 4 },
|
|
{ key: 'VERIFICATION_URL', label: 'Verification URL', description: 'URL to verify the response', isRequired: true, isConditional: false, sampleValue: 'https://api.cmlite.org/api/responses/123/verify/abc123def456', sortOrder: 5 },
|
|
{ key: 'REPORT_URL', label: 'Report URL', description: 'URL to report the response as invalid', isRequired: true, isConditional: false, sampleValue: 'https://api.cmlite.org/api/responses/123/report/abc123def456', sortOrder: 6 },
|
|
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization running the campaign', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 7 },
|
|
{ key: 'TIMESTAMP', label: 'Timestamp', description: 'Date and time the verification email was sent', isRequired: true, isConditional: false, sampleValue: '2025-02-11T10:30:00Z', sortOrder: 8 },
|
|
],
|
|
},
|
|
{
|
|
key: 'shift-signup-confirmation',
|
|
name: 'Shift Signup Confirmation',
|
|
description: 'Email sent when a volunteer signs up for a shift',
|
|
category: 'MAP',
|
|
subjectLine: 'Shift Confirmation - {{SHIFT_TITLE}}',
|
|
isSystem: true,
|
|
variables: [
|
|
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
|
|
{ key: 'USER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'John Smith', sortOrder: 1 },
|
|
{ key: 'USER_EMAIL', label: 'Volunteer Email', description: 'Email address of the volunteer', isRequired: true, isConditional: false, sampleValue: 'john@example.com', sortOrder: 2 },
|
|
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the volunteer shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 3 },
|
|
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the volunteer shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 15, 2025', sortOrder: 4 },
|
|
{ key: 'SHIFT_TIME', label: 'Shift Time', description: 'Time of the volunteer shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM — 2:00 PM', sortOrder: 5 },
|
|
{ key: 'SHIFT_LOCATION', label: 'Shift Location', description: 'Location of the volunteer shift', isRequired: true, isConditional: false, sampleValue: '123 Main Street', sortOrder: 6 },
|
|
{ key: 'IS_NEW_USER', label: 'Is New User', description: 'Whether this is a new user account (used in conditional blocks)', isRequired: false, isConditional: true, sampleValue: 'true', sortOrder: 7 },
|
|
{ key: 'TEMP_PASSWORD', label: 'Temporary Password', description: 'Temporary password for new user account', isRequired: false, isConditional: true, sampleValue: 'SwiftEagle42', sortOrder: 8 },
|
|
{ key: 'LOGIN_URL', label: 'Login URL', description: 'URL to log in to the system', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/login', sortOrder: 9 },
|
|
],
|
|
},
|
|
{
|
|
key: 'shift-details',
|
|
name: 'Shift Details Reminder',
|
|
description: 'Reminder email sent to all volunteers signed up for a shift',
|
|
category: 'MAP',
|
|
subjectLine: 'Shift Reminder - {{SHIFT_TITLE}} on {{SHIFT_DATE}}',
|
|
isSystem: true,
|
|
variables: [
|
|
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
|
|
{ key: 'USER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'John Smith', sortOrder: 1 },
|
|
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the volunteer shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 2 },
|
|
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the volunteer shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 15, 2025', sortOrder: 3 },
|
|
{ key: 'SHIFT_START_TIME', label: 'Start Time', description: 'Start time of the shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM', sortOrder: 4 },
|
|
{ key: 'SHIFT_END_TIME', label: 'End Time', description: 'End time of the shift', isRequired: true, isConditional: false, sampleValue: '2:00 PM', sortOrder: 5 },
|
|
{ key: 'SHIFT_LOCATION', label: 'Shift Location', description: 'Location of the volunteer shift', isRequired: true, isConditional: false, sampleValue: '123 Main Street', sortOrder: 6 },
|
|
{ key: 'SHIFT_DESCRIPTION', label: 'Shift Description', description: 'Description of the shift (used in conditional blocks)', isRequired: false, isConditional: true, sampleValue: 'We will be canvassing the downtown area to connect with voters...', sortOrder: 7 },
|
|
{ key: 'CURRENT_VOLUNTEERS', label: 'Current Volunteers', description: 'Number of volunteers currently signed up', isRequired: true, isConditional: false, sampleValue: '8', sortOrder: 8 },
|
|
{ key: 'MAX_VOLUNTEERS', label: 'Max Volunteers', description: 'Maximum number of volunteers allowed', isRequired: true, isConditional: false, sampleValue: '10', sortOrder: 9 },
|
|
{ key: 'SHIFT_STATUS', label: 'Shift Status', description: 'Status of the shift (OPEN, FULL, CANCELLED)', isRequired: true, isConditional: false, sampleValue: 'OPEN', sortOrder: 10 },
|
|
],
|
|
},
|
|
{
|
|
key: 'email-verification',
|
|
name: 'Email Verification',
|
|
description: 'Sent to new users to verify their email address after self-registration',
|
|
category: 'SYSTEM',
|
|
subjectLine: 'Verify your email — {{ORGANIZATION_NAME}}',
|
|
isSystem: true,
|
|
variables: [
|
|
{ key: 'RECIPIENT_NAME', label: 'User Name', description: 'Name of the user being verified', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
|
|
{ key: 'VERIFICATION_URL', label: 'Verification URL', description: 'URL the user clicks to verify their email', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/verify-email?token=abc123', sortOrder: 1 },
|
|
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 2 },
|
|
{ key: 'EXPIRY_HOURS', label: 'Expiry Hours', description: 'Number of hours until the verification link expires', isRequired: true, isConditional: false, sampleValue: '24', sortOrder: 3 },
|
|
],
|
|
},
|
|
{
|
|
key: 'password-reset',
|
|
name: 'Password Reset',
|
|
description: 'Sent when a user requests a password reset link',
|
|
category: 'SYSTEM',
|
|
subjectLine: 'Reset your password — {{ORGANIZATION_NAME}}',
|
|
isSystem: true,
|
|
variables: [
|
|
{ key: 'RECIPIENT_NAME', label: 'User Name', description: 'Name of the user requesting a reset', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
|
|
{ key: 'RESET_URL', label: 'Reset URL', description: 'URL the user clicks to reset their password', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/reset-password?token=abc123', sortOrder: 1 },
|
|
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 2 },
|
|
],
|
|
},
|
|
{
|
|
key: 'account-approved',
|
|
name: 'Account Approved',
|
|
description: 'Sent to a user when an admin approves their account after email verification',
|
|
category: 'SYSTEM',
|
|
subjectLine: 'Account approved — {{ORGANIZATION_NAME}}',
|
|
isSystem: true,
|
|
variables: [
|
|
{ key: 'RECIPIENT_NAME', label: 'User Name', description: 'Name of the approved user', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
|
|
{ key: 'LOGIN_URL', label: 'Login URL', description: 'URL to the login page', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/login', sortOrder: 1 },
|
|
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 2 },
|
|
],
|
|
},
|
|
{
|
|
key: 'account-pending-approval',
|
|
name: 'Account Pending Approval (Admin Notification)',
|
|
description: 'Sent to admins when a new user verifies their email and is awaiting approval',
|
|
category: 'SYSTEM',
|
|
subjectLine: 'New user awaiting approval — {{ORGANIZATION_NAME}}',
|
|
isSystem: true,
|
|
variables: [
|
|
{ key: 'NEW_USER_NAME', label: 'New User Name', description: 'Name of the new user requesting approval', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
|
|
{ key: 'NEW_USER_EMAIL', label: 'New User Email', description: 'Email of the new user', isRequired: true, isConditional: false, sampleValue: 'jane@example.com', sortOrder: 1 },
|
|
{ key: 'REGISTERED_AT', label: 'Registration Date', description: 'Date the user registered', isRequired: true, isConditional: false, sampleValue: 'February 16, 2026', sortOrder: 2 },
|
|
{ key: 'ADMIN_URL', label: 'Admin URL', description: 'URL to the admin users page filtered to pending approval', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/app/users?status=PENDING_APPROVAL', sortOrder: 3 },
|
|
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 4 },
|
|
],
|
|
},
|
|
];
|
|
async function seedEmailTemplates() {
|
|
const prisma = new client_1.PrismaClient();
|
|
console.log('🌱 Starting email template seeding...\n');
|
|
// Find or create SUPER_ADMIN user for created_by
|
|
let adminUser = await prisma.user.findFirst({
|
|
where: { role: 'SUPER_ADMIN' },
|
|
});
|
|
if (!adminUser) {
|
|
console.log('⚠️ No SUPER_ADMIN user found. Creating a system user...');
|
|
adminUser = await prisma.user.create({
|
|
data: {
|
|
email: 'system@cmlite.org',
|
|
password: 'not-used-for-login',
|
|
name: 'System',
|
|
role: 'SUPER_ADMIN',
|
|
roles: JSON.parse(JSON.stringify(['SUPER_ADMIN'])),
|
|
status: 'ACTIVE',
|
|
},
|
|
});
|
|
console.log(`✓ Created system user: ${adminUser.email}\n`);
|
|
}
|
|
const templatesDir = path.join(__dirname, '../templates/email');
|
|
let seededCount = 0;
|
|
let skippedCount = 0;
|
|
let errorCount = 0;
|
|
for (const templateDef of TEMPLATES) {
|
|
try {
|
|
console.log(`Processing: ${templateDef.key}`);
|
|
// Check if template already exists
|
|
const existing = await prisma.emailTemplate.findUnique({
|
|
where: { key: templateDef.key },
|
|
});
|
|
if (existing) {
|
|
console.log(` ⏭️ Skipped (already exists)\n`);
|
|
skippedCount++;
|
|
continue;
|
|
}
|
|
// Read HTML and TXT files from filesystem
|
|
const htmlPath = path.join(templatesDir, `${templateDef.key}.html`);
|
|
const txtPath = path.join(templatesDir, `${templateDef.key}.txt`);
|
|
if (!fs.existsSync(htmlPath) || !fs.existsSync(txtPath)) {
|
|
console.log(` ❌ Error: Template files not found\n`);
|
|
errorCount++;
|
|
continue;
|
|
}
|
|
const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
|
|
const textContent = fs.readFileSync(txtPath, 'utf-8');
|
|
// Create template with variables in a transaction
|
|
await prisma.$transaction(async (tx) => {
|
|
// Create template
|
|
const template = await tx.emailTemplate.create({
|
|
data: {
|
|
key: templateDef.key,
|
|
name: templateDef.name,
|
|
description: templateDef.description,
|
|
category: templateDef.category,
|
|
subjectLine: templateDef.subjectLine,
|
|
htmlContent,
|
|
textContent,
|
|
isSystem: templateDef.isSystem,
|
|
isActive: true,
|
|
createdByUserId: adminUser.id,
|
|
variables: {
|
|
create: templateDef.variables.map((v) => ({
|
|
key: v.key,
|
|
label: v.label,
|
|
description: v.description,
|
|
isRequired: v.isRequired,
|
|
isConditional: v.isConditional,
|
|
sampleValue: v.sampleValue,
|
|
sortOrder: v.sortOrder,
|
|
})),
|
|
},
|
|
},
|
|
});
|
|
// Create initial version
|
|
await tx.emailTemplateVersion.create({
|
|
data: {
|
|
templateId: template.id,
|
|
versionNumber: 1,
|
|
subjectLine: templateDef.subjectLine,
|
|
htmlContent,
|
|
textContent,
|
|
changeNotes: 'Initial migration from filesystem',
|
|
createdByUserId: adminUser.id,
|
|
},
|
|
});
|
|
});
|
|
console.log(` ✓ Seeded with ${templateDef.variables.length} variables\n`);
|
|
seededCount++;
|
|
}
|
|
catch (error) {
|
|
console.error(` ❌ Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
errorCount++;
|
|
}
|
|
}
|
|
console.log('═'.repeat(60));
|
|
console.log(`✨ Seeding complete!`);
|
|
console.log(` Seeded: ${seededCount}`);
|
|
console.log(` Skipped: ${skippedCount}`);
|
|
console.log(` Errors: ${errorCount}`);
|
|
console.log('═'.repeat(60));
|
|
await prisma.$disconnect();
|
|
}
|
|
// Only auto-execute when run directly (not when imported as a module)
|
|
if (require.main === module) {
|
|
seedEmailTemplates()
|
|
.catch((error) => {
|
|
console.error('Fatal error:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
//# sourceMappingURL=seed-email-templates.js.map
|