466 lines
23 KiB
TypeScript
466 lines
23 KiB
TypeScript
import { PrismaClient, UserRole, EmailTemplateCategory } from '@prisma/client';
|
||
import bcrypt from 'bcryptjs';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { env } from '../src/config/env';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
async function main() {
|
||
console.log('Seeding database...');
|
||
|
||
// Create default super admin from environment variables
|
||
// Skip creation if password is a placeholder (contains "CHANGE_THIS")
|
||
const initialAdminEmail = env.INITIAL_ADMIN_EMAIL;
|
||
const initialAdminPassword = env.INITIAL_ADMIN_PASSWORD;
|
||
|
||
let admin: { id: string; email: string } | null = null;
|
||
|
||
// Validate password meets policy before proceeding
|
||
if (initialAdminPassword.length < 12 ||
|
||
!/[A-Z]/.test(initialAdminPassword) ||
|
||
!/[a-z]/.test(initialAdminPassword) ||
|
||
!/[0-9]/.test(initialAdminPassword)) {
|
||
console.warn('⚠️ INITIAL_ADMIN_PASSWORD does not meet password policy (12+ chars, uppercase, lowercase, digit)');
|
||
console.warn('⚠️ Skipping admin user creation. Please set a valid password in .env');
|
||
} else if (initialAdminPassword.includes('CHANGE_THIS') || initialAdminPassword.includes('REQUIRED')) {
|
||
console.warn('⚠️ INITIAL_ADMIN_PASSWORD contains placeholder value');
|
||
console.warn('⚠️ Skipping admin user creation. Please set a real password in .env');
|
||
} else {
|
||
const hashedPassword = await bcrypt.hash(initialAdminPassword, 10);
|
||
|
||
admin = await prisma.user.upsert({
|
||
where: { email: initialAdminEmail },
|
||
update: {
|
||
password: hashedPassword,
|
||
emailVerified: true,
|
||
status: 'ACTIVE',
|
||
},
|
||
create: {
|
||
email: initialAdminEmail,
|
||
password: hashedPassword,
|
||
name: 'Admin',
|
||
role: UserRole.SUPER_ADMIN,
|
||
roles: JSON.parse(JSON.stringify([UserRole.SUPER_ADMIN])),
|
||
emailVerified: true,
|
||
},
|
||
});
|
||
|
||
console.log(`✅ Created/updated admin user: ${admin.email}`);
|
||
}
|
||
|
||
// If admin wasn't created (placeholder password), try to find existing admin
|
||
if (!admin) {
|
||
admin = await prisma.user.findFirst({
|
||
where: { role: UserRole.SUPER_ADMIN },
|
||
select: { id: true, email: true },
|
||
});
|
||
if (admin) {
|
||
console.log(`ℹ️ Found existing admin user: ${admin.email}`);
|
||
}
|
||
}
|
||
|
||
// Create default map settings
|
||
await prisma.mapSettings.upsert({
|
||
where: { id: 'default' },
|
||
update: {},
|
||
create: {
|
||
id: 'default',
|
||
latitude: 53.5461,
|
||
longitude: -113.4938,
|
||
zoom: 11,
|
||
walkSheetTitle: 'Walk Sheet',
|
||
walkSheetSubtitle: '',
|
||
walkSheetFooter: '',
|
||
},
|
||
});
|
||
|
||
console.log('Created default map settings');
|
||
|
||
// Phase 3: v1 data migration will go here
|
||
// - Export NocoDB data via API
|
||
// - Import influence_users + login tables → unified users
|
||
// - Deduplicate by email
|
||
// - Hash plaintext passwords
|
||
// - Import campaigns, locations, shifts, cuts, etc.
|
||
|
||
// Create default page blocks for landing page builder
|
||
const defaultBlocks = [
|
||
{
|
||
id: 'default-hero',
|
||
type: 'hero',
|
||
label: 'Hero Section',
|
||
category: 'Headers',
|
||
sortOrder: 1,
|
||
schema: {
|
||
title: { type: 'string', label: 'Title' },
|
||
subtitle: { type: 'string', label: 'Subtitle' },
|
||
backgroundImage: { type: 'string', label: 'Background Image URL' },
|
||
ctaText: { type: 'string', label: 'Button Text' },
|
||
ctaUrl: { type: 'string', label: 'Button URL' },
|
||
},
|
||
defaults: {
|
||
title: 'Welcome to Our Campaign',
|
||
subtitle: 'Join us in making a difference in your community.',
|
||
backgroundImage: '',
|
||
ctaText: 'Get Involved',
|
||
ctaUrl: '#',
|
||
},
|
||
},
|
||
{
|
||
id: 'default-text',
|
||
type: 'text',
|
||
label: 'Text Block',
|
||
category: 'Content',
|
||
sortOrder: 2,
|
||
schema: {
|
||
heading: { type: 'string', label: 'Heading' },
|
||
body: { type: 'text', label: 'Body Text' },
|
||
},
|
||
defaults: {
|
||
heading: 'About Us',
|
||
body: 'Tell your story here. Explain your mission, values, and what drives your campaign forward.',
|
||
},
|
||
},
|
||
{
|
||
id: 'default-features',
|
||
type: 'features',
|
||
label: 'Features Grid',
|
||
category: 'Content',
|
||
sortOrder: 3,
|
||
schema: {
|
||
features: { type: 'array', label: 'Features', items: { title: 'string', description: 'string', icon: 'string' } },
|
||
},
|
||
defaults: {
|
||
features: [
|
||
{ title: 'Community Action', description: 'Organize local events and initiatives.', icon: '' },
|
||
{ title: 'Advocacy', description: 'Email your representatives directly.', icon: '' },
|
||
{ title: 'Volunteer', description: 'Sign up for shifts and make a difference.', icon: '' },
|
||
],
|
||
},
|
||
},
|
||
{
|
||
id: 'default-cta',
|
||
type: 'cta',
|
||
label: 'Call to Action',
|
||
category: 'Actions',
|
||
sortOrder: 4,
|
||
schema: {
|
||
heading: { type: 'string', label: 'Heading' },
|
||
description: { type: 'string', label: 'Description' },
|
||
buttonText: { type: 'string', label: 'Button Text' },
|
||
buttonUrl: { type: 'string', label: 'Button URL' },
|
||
},
|
||
defaults: {
|
||
heading: 'Ready to Take Action?',
|
||
description: 'Join thousands of community members making their voices heard.',
|
||
buttonText: 'Join Now',
|
||
buttonUrl: '#',
|
||
},
|
||
},
|
||
{
|
||
id: 'default-testimonials',
|
||
type: 'testimonials',
|
||
label: 'Testimonials',
|
||
category: 'Content',
|
||
sortOrder: 5,
|
||
schema: {
|
||
quotes: { type: 'array', label: 'Quotes', items: { text: 'string', author: 'string', role: 'string' } },
|
||
},
|
||
defaults: {
|
||
quotes: [
|
||
{ text: 'This platform made it so easy to contact my representatives.', author: 'Jane D.', role: 'Community Member' },
|
||
{ text: 'I signed up for a volunteer shift and it changed my perspective.', author: 'Mark S.', role: 'Volunteer' },
|
||
],
|
||
},
|
||
},
|
||
{
|
||
id: 'default-contact-form',
|
||
type: 'contact-form',
|
||
label: 'Contact Form',
|
||
category: 'Actions',
|
||
sortOrder: 6,
|
||
schema: {
|
||
heading: { type: 'string', label: 'Heading' },
|
||
fields: { type: 'array', label: 'Fields', items: { name: 'string', type: 'string', required: 'boolean' } },
|
||
},
|
||
defaults: {
|
||
heading: 'Get in Touch',
|
||
fields: [
|
||
{ name: 'Name', type: 'text', required: true },
|
||
{ name: 'Email', type: 'email', required: true },
|
||
{ name: 'Message', type: 'textarea', required: true },
|
||
],
|
||
},
|
||
},
|
||
{
|
||
id: 'default-video',
|
||
type: 'video',
|
||
label: 'Video Player',
|
||
category: 'Media',
|
||
sortOrder: 7,
|
||
schema: {
|
||
videoId: { type: 'number', label: 'Video ID', required: true },
|
||
playerType: { type: 'select', label: 'Player Type', options: ['standard', 'advanced'], default: 'standard' },
|
||
width: { type: 'string', label: 'Width', default: '100%' },
|
||
height: { type: 'string', label: 'Height', default: 'auto' },
|
||
autoplay: { type: 'boolean', label: 'Autoplay', default: false },
|
||
controls: { type: 'boolean', label: 'Show Controls', default: true },
|
||
showReactions: { type: 'boolean', label: 'Show Reactions (Advanced Only)', default: true },
|
||
},
|
||
defaults: {
|
||
videoId: null,
|
||
playerType: 'standard',
|
||
width: '100%',
|
||
height: 'auto',
|
||
autoplay: false,
|
||
controls: true,
|
||
showReactions: true,
|
||
},
|
||
},
|
||
];
|
||
|
||
for (const block of defaultBlocks) {
|
||
await prisma.pageBlock.upsert({
|
||
where: { id: block.id },
|
||
update: {},
|
||
create: block,
|
||
});
|
||
}
|
||
|
||
console.log(`Created ${defaultBlocks.length} default page blocks`);
|
||
|
||
// Seed email templates (skip if no admin user)
|
||
if (admin) {
|
||
await seedEmailTemplates(admin);
|
||
} else {
|
||
console.warn('⚠️ No admin user found - skipping email template seeding');
|
||
}
|
||
|
||
console.log('Seed complete.');
|
||
}
|
||
|
||
/**
|
||
* Seed email templates from filesystem
|
||
*/
|
||
async function seedEmailTemplates(admin: { id: string; email: string }) {
|
||
console.log('Seeding email templates...');
|
||
|
||
const templatesDir = path.join(__dirname, '../src/templates/email');
|
||
|
||
const templateDefinitions = [
|
||
{
|
||
key: 'campaign-email',
|
||
name: 'Campaign Email to Representative',
|
||
description: 'Email sent when a constituent contacts their elected representative through a campaign',
|
||
category: EmailTemplateCategory.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...', 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', isRequired: false, isConditional: true, sampleValue: 'Federal', sortOrder: 6 },
|
||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', 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: EmailTemplateCategory.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', 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...', 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/abc123', 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/abc123', sortOrder: 6 },
|
||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', 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: EmailTemplateCategory.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', 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: EmailTemplateCategory.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', isRequired: false, isConditional: true, sampleValue: 'We will be canvassing the downtown area...', 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', 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: EmailTemplateCategory.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: EmailTemplateCategory.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: EmailTemplateCategory.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: EmailTemplateCategory.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 },
|
||
],
|
||
},
|
||
];
|
||
|
||
let seededCount = 0;
|
||
let skippedCount = 0;
|
||
|
||
for (const templateDef of templateDefinitions) {
|
||
try {
|
||
// Check if template already exists
|
||
const existing = await prisma.emailTemplate.findUnique({
|
||
where: { key: templateDef.key },
|
||
});
|
||
|
||
if (existing) {
|
||
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.warn(` ⚠️ Template files not found for ${templateDef.key}`);
|
||
continue;
|
||
}
|
||
|
||
const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
|
||
const textContent = fs.readFileSync(txtPath, 'utf-8');
|
||
|
||
// Create template with variables and initial version
|
||
await prisma.$transaction(async (tx) => {
|
||
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: admin.id,
|
||
variables: {
|
||
create: templateDef.variables,
|
||
},
|
||
},
|
||
});
|
||
|
||
// Create initial version
|
||
await tx.emailTemplateVersion.create({
|
||
data: {
|
||
templateId: template.id,
|
||
versionNumber: 1,
|
||
subjectLine: templateDef.subjectLine,
|
||
htmlContent,
|
||
textContent,
|
||
changeNotes: 'Initial migration from filesystem',
|
||
createdByUserId: admin.id,
|
||
},
|
||
});
|
||
});
|
||
|
||
seededCount++;
|
||
} catch (error) {
|
||
console.error(` ❌ Error seeding template ${templateDef.key}:`, error);
|
||
}
|
||
}
|
||
|
||
console.log(`Email templates seeded: ${seededCount} created, ${skippedCount} skipped`);
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error('Seed error:', e);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|