1064 lines
55 KiB
TypeScript
1064 lines
55 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';
|
||
import { initEncryption, encrypt } from '../src/utils/crypto';
|
||
|
||
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, 12);
|
||
|
||
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');
|
||
|
||
// Seed SiteSettings with .env SMTP values (only if no row exists yet)
|
||
const existingSettings = await prisma.siteSettings.findFirst();
|
||
if (!existingSettings) {
|
||
// Initialize encryption so we can encrypt the SMTP password
|
||
const encryptionKey = env.ENCRYPTION_KEY || env.JWT_ACCESS_SECRET;
|
||
initEncryption(encryptionKey);
|
||
|
||
const isMailhog = env.EMAIL_TEST_MODE === 'true' || env.SMTP_HOST === 'mailhog-changemaker';
|
||
|
||
await prisma.siteSettings.create({
|
||
data: {
|
||
smtpHost: env.SMTP_HOST,
|
||
smtpPort: env.SMTP_PORT,
|
||
smtpUser: env.SMTP_USER,
|
||
smtpPass: env.SMTP_PASS ? encrypt(env.SMTP_PASS) : '',
|
||
smtpFromAddress: env.SMTP_FROM,
|
||
emailFromName: env.SMTP_FROM_NAME,
|
||
smtpActiveProvider: isMailhog ? 'mailhog' : 'production',
|
||
emailTestMode: env.EMAIL_TEST_MODE === 'true',
|
||
testEmailRecipient: env.TEST_EMAIL_RECIPIENT,
|
||
navConfig: {
|
||
items: [
|
||
{ id: 'home', label: 'Home', path: '/', icon: 'HomeOutlined', enabled: true, order: 0, type: 'builtin', external: true },
|
||
{ id: 'campaigns', label: 'Campaigns', path: '/campaigns', icon: 'SendOutlined', enabled: true, order: 1, type: 'builtin', featureFlag: 'enableInfluence' },
|
||
{ id: 'map', label: 'Map', path: '/map', icon: 'EnvironmentOutlined', enabled: true, order: 2, type: 'builtin', featureFlag: 'enableMap' },
|
||
{ id: 'shifts', label: 'Shifts', path: '/shifts', icon: 'CalendarOutlined', enabled: true, order: 3, type: 'builtin', featureFlag: 'enableMap' },
|
||
{ id: 'events', label: 'Events', path: '/events', icon: 'CalendarOutlined', enabled: true, order: 4, type: 'builtin', featureFlag: 'enableEvents', external: true },
|
||
{ id: 'gallery', label: 'Gallery', path: '/gallery', icon: 'PlayCircleOutlined', enabled: true, order: 5, type: 'builtin', featureFlag: 'enableMediaFeatures' },
|
||
{ id: 'pricing', label: 'Pricing', path: '/pricing', icon: 'DollarOutlined', enabled: true, order: 6, type: 'builtin', featureFlag: 'enablePayments' },
|
||
{ id: 'shop', label: 'Shop', path: '/shop', icon: 'ShoppingOutlined', enabled: true, order: 7, type: 'builtin', featureFlag: 'enablePayments' },
|
||
{ id: 'donate', label: 'Donate', path: '/donate', icon: 'HeartOutlined', enabled: true, order: 8, type: 'builtin', featureFlag: 'enablePayments' },
|
||
{ id: 'landing', label: 'Website', path: '$landing', icon: 'GlobalOutlined', enabled: false, order: 9, type: 'builtin', external: true },
|
||
{ id: 'docs', label: 'Docs', path: '$docs', icon: 'BookOutlined', enabled: false, order: 10, type: 'builtin', external: true },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
console.log('Created SiteSettings with SMTP config + navConfig from .env');
|
||
} else {
|
||
console.log('SiteSettings already exists, skipping SMTP seeding');
|
||
|
||
// Seed navConfig if null (existing installations)
|
||
if (!existingSettings.navConfig) {
|
||
const defaultNavConfig = {
|
||
items: [
|
||
{ id: 'home', label: 'Home', path: '/', icon: 'HomeOutlined', enabled: true, order: 0, type: 'builtin', external: true },
|
||
{ id: 'campaigns', label: 'Campaigns', path: '/campaigns', icon: 'SendOutlined', enabled: true, order: 1, type: 'builtin', featureFlag: 'enableInfluence' },
|
||
{ id: 'map', label: 'Map', path: '/map', icon: 'EnvironmentOutlined', enabled: true, order: 2, type: 'builtin', featureFlag: 'enableMap' },
|
||
{ id: 'shifts', label: 'Shifts', path: '/shifts', icon: 'CalendarOutlined', enabled: true, order: 3, type: 'builtin', featureFlag: 'enableMap' },
|
||
{ id: 'events', label: 'Events', path: '/events', icon: 'CalendarOutlined', enabled: true, order: 4, type: 'builtin', featureFlag: 'enableEvents', external: true },
|
||
{ id: 'gallery', label: 'Gallery', path: '/gallery', icon: 'PlayCircleOutlined', enabled: true, order: 5, type: 'builtin', featureFlag: 'enableMediaFeatures' },
|
||
{ id: 'pricing', label: 'Pricing', path: '/pricing', icon: 'DollarOutlined', enabled: true, order: 6, type: 'builtin', featureFlag: 'enablePayments' },
|
||
{ id: 'shop', label: 'Shop', path: '/shop', icon: 'ShoppingOutlined', enabled: true, order: 7, type: 'builtin', featureFlag: 'enablePayments' },
|
||
{ id: 'donate', label: 'Donate', path: '/donate', icon: 'HeartOutlined', enabled: true, order: 8, type: 'builtin', featureFlag: 'enablePayments' },
|
||
{ id: 'landing', label: 'Website', path: '$landing', icon: 'GlobalOutlined', enabled: false, order: 9, type: 'builtin', external: true },
|
||
{ id: 'docs', label: 'Docs', path: '$docs', icon: 'BookOutlined', enabled: false, order: 10, type: 'builtin', external: true },
|
||
],
|
||
};
|
||
await prisma.siteSettings.update({
|
||
where: { id: existingSettings.id },
|
||
data: { navConfig: defaultNavConfig },
|
||
});
|
||
console.log('Seeded default navConfig on existing SiteSettings');
|
||
}
|
||
}
|
||
|
||
// 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,
|
||
},
|
||
},
|
||
{
|
||
id: 'default-video-card',
|
||
type: 'video-card',
|
||
label: 'Video Card',
|
||
category: 'Media',
|
||
sortOrder: 8,
|
||
schema: {
|
||
videoId: { type: 'number', label: 'Video ID', required: true },
|
||
title: { type: 'string', label: 'Title' },
|
||
durationSeconds: { type: 'number', label: 'Duration (seconds)', default: 0 },
|
||
quality: { type: 'string', label: 'Quality (e.g. 1080p)' },
|
||
viewCount: { type: 'number', label: 'View Count', default: 0 },
|
||
},
|
||
defaults: {
|
||
videoId: null,
|
||
title: '',
|
||
durationSeconds: 0,
|
||
quality: '',
|
||
viewCount: 0,
|
||
},
|
||
},
|
||
{
|
||
id: 'default-donate-button',
|
||
type: 'donate-button',
|
||
label: 'Donate Button',
|
||
category: 'Payments',
|
||
sortOrder: 9,
|
||
schema: {
|
||
buttonText: { type: 'string', label: 'Button Text', default: 'Donate Now' },
|
||
showAmounts: { type: 'boolean', label: 'Show Suggested Amounts', default: true },
|
||
heading: { type: 'string', label: 'Heading', default: 'Support Our Cause' },
|
||
description: { type: 'string', label: 'Description' },
|
||
},
|
||
defaults: {
|
||
buttonText: 'Donate Now',
|
||
showAmounts: true,
|
||
heading: 'Support Our Cause',
|
||
description: 'Your contribution helps us create lasting change in our community.',
|
||
},
|
||
},
|
||
{
|
||
id: 'default-pricing-table',
|
||
type: 'pricing-table',
|
||
label: 'Pricing Table',
|
||
category: 'Payments',
|
||
sortOrder: 10,
|
||
schema: {
|
||
showYearly: { type: 'boolean', label: 'Show Yearly Toggle', default: true },
|
||
heading: { type: 'string', label: 'Heading', default: 'Choose Your Plan' },
|
||
description: { type: 'string', label: 'Description' },
|
||
},
|
||
defaults: {
|
||
showYearly: true,
|
||
heading: 'Choose Your Plan',
|
||
description: 'Get access to exclusive content and features.',
|
||
},
|
||
},
|
||
{
|
||
id: 'default-product-card',
|
||
type: 'product-card',
|
||
label: 'Product Card',
|
||
category: 'Payments',
|
||
sortOrder: 11,
|
||
schema: {
|
||
productSlug: { type: 'string', label: 'Product Slug', required: true },
|
||
buttonText: { type: 'string', label: 'Button Text', default: 'Buy Now' },
|
||
},
|
||
defaults: {
|
||
productSlug: '',
|
||
buttonText: 'Buy Now',
|
||
},
|
||
},
|
||
{
|
||
id: 'default-campaign-form',
|
||
type: 'campaign-form',
|
||
label: 'Campaign Email Form',
|
||
category: 'Influence',
|
||
sortOrder: 13,
|
||
schema: {
|
||
campaignSlug: { type: 'string', label: 'Campaign Slug', required: true },
|
||
compact: { type: 'boolean', label: 'Compact Mode', default: false },
|
||
},
|
||
defaults: {
|
||
campaignSlug: '',
|
||
compact: false,
|
||
},
|
||
},
|
||
{
|
||
id: 'default-gancio-events',
|
||
type: 'gancio-events',
|
||
label: 'Event Calendar',
|
||
category: 'Content',
|
||
sortOrder: 12,
|
||
schema: {
|
||
maxlength: { type: 'number', label: 'Max Events', default: 10 },
|
||
theme: { type: 'select', label: 'Theme', options: ['dark', 'light'], default: 'dark' },
|
||
tags: { type: 'string', label: 'Filter by Tags (comma-separated)' },
|
||
title: { type: 'string', label: 'Section Title', default: 'Upcoming Events' },
|
||
},
|
||
defaults: {
|
||
maxlength: 10,
|
||
theme: 'dark',
|
||
tags: '',
|
||
title: 'Upcoming Events',
|
||
},
|
||
},
|
||
{
|
||
id: 'default-photo',
|
||
type: 'photo',
|
||
label: 'Photo',
|
||
category: 'Media',
|
||
sortOrder: 14,
|
||
schema: {
|
||
photoId: { type: 'number', label: 'Photo ID', required: true },
|
||
size: { type: 'select', label: 'Size', options: ['thumb', 'medium', 'large'], default: 'large' },
|
||
caption: { type: 'string', label: 'Caption' },
|
||
linkToGallery: { type: 'boolean', label: 'Link to Gallery', default: true },
|
||
alignment: { type: 'select', label: 'Alignment', options: ['left', 'center', 'right'], default: 'center' },
|
||
maxWidth: { type: 'string', label: 'Max Width', default: '100%' },
|
||
},
|
||
defaults: {
|
||
photoId: null,
|
||
size: 'large',
|
||
caption: '',
|
||
linkToGallery: true,
|
||
alignment: 'center',
|
||
maxWidth: '100%',
|
||
},
|
||
},
|
||
{
|
||
id: 'default-photo-card',
|
||
type: 'photo-card',
|
||
label: 'Photo Card',
|
||
category: 'Media',
|
||
sortOrder: 15,
|
||
schema: {
|
||
photoId: { type: 'number', label: 'Photo ID', required: true },
|
||
title: { type: 'string', label: 'Title' },
|
||
description: { type: 'string', label: 'Description' },
|
||
showMetadata: { type: 'boolean', label: 'Show Metadata (format, dimensions)', default: true },
|
||
},
|
||
defaults: {
|
||
photoId: null,
|
||
title: '',
|
||
description: '',
|
||
showMetadata: true,
|
||
},
|
||
},
|
||
{
|
||
id: 'default-photo-album',
|
||
type: 'photo-album',
|
||
label: 'Photo Album',
|
||
category: 'Media',
|
||
sortOrder: 16,
|
||
schema: {
|
||
albumId: { type: 'number', label: 'Album ID', required: true },
|
||
columns: { type: 'select', label: 'Columns', options: ['2', '3', '4'], default: '3' },
|
||
maxPhotos: { type: 'number', label: 'Max Photos', default: 12 },
|
||
showTitle: { type: 'boolean', label: 'Show Album Title', default: true },
|
||
},
|
||
defaults: {
|
||
albumId: null,
|
||
columns: '3',
|
||
maxPhotos: 12,
|
||
showTitle: true,
|
||
},
|
||
},
|
||
{
|
||
id: 'default-scheduling-poll',
|
||
type: 'scheduling-poll',
|
||
label: 'Scheduling Poll',
|
||
category: 'Influence',
|
||
sortOrder: 17,
|
||
schema: {
|
||
pollSlug: { type: 'string', label: 'Poll Slug', required: true },
|
||
showComments: { type: 'boolean', label: 'Show Comments', default: true },
|
||
title: { type: 'string', label: 'Section Title', default: 'Vote on a Meeting Time' },
|
||
},
|
||
defaults: {
|
||
pollSlug: '',
|
||
showComments: true,
|
||
title: 'Vote on a Meeting Time',
|
||
},
|
||
},
|
||
];
|
||
|
||
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');
|
||
}
|
||
|
||
// Seed SMS notification templates
|
||
await seedSmsNotificationTemplates();
|
||
|
||
// Seed pre-made gallery ads (all inactive by default — admin enables manually)
|
||
await seedGalleryAds();
|
||
|
||
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 },
|
||
],
|
||
},
|
||
{
|
||
key: 'donation-receipt',
|
||
name: 'Donation Receipt',
|
||
description: 'Receipt email sent to donors after a successful donation payment',
|
||
category: EmailTemplateCategory.PAYMENT,
|
||
subjectLine: 'Donation Receipt — {{ORGANIZATION_NAME}}',
|
||
isSystem: true,
|
||
variables: [
|
||
{ key: 'RECIPIENT_NAME', label: 'Donor Name', description: 'Name of the donor', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
|
||
{ key: 'AMOUNT', label: 'Amount', description: 'Donation amount formatted with dollar sign', isRequired: true, isConditional: false, sampleValue: '$25.00', sortOrder: 1 },
|
||
{ key: 'ORDER_ID', label: 'Order ID', description: 'Unique reference ID for the donation', isRequired: true, isConditional: false, sampleValue: 'clxyz123abc', sortOrder: 2 },
|
||
{ key: 'DONATION_DATE', label: 'Donation Date', description: 'Date the donation was completed', isRequired: true, isConditional: false, sampleValue: 'February 17, 2026', sortOrder: 3 },
|
||
{ key: 'DONOR_MESSAGE', label: 'Donor Message', description: 'Optional message from the donor', isRequired: false, isConditional: true, sampleValue: 'Keep up the great work!', sortOrder: 4 },
|
||
{ key: 'IS_ANONYMOUS', label: 'Is Anonymous', description: 'Whether the donation is anonymous', isRequired: false, isConditional: true, sampleValue: 'true', sortOrder: 5 },
|
||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 6 },
|
||
],
|
||
},
|
||
{
|
||
key: 'product-receipt',
|
||
name: 'Product Purchase Receipt',
|
||
description: 'Receipt email sent to buyers after a successful product purchase',
|
||
category: EmailTemplateCategory.PAYMENT,
|
||
subjectLine: 'Purchase Receipt — {{ORGANIZATION_NAME}}',
|
||
isSystem: true,
|
||
variables: [
|
||
{ key: 'RECIPIENT_NAME', label: 'Buyer Name', description: 'Name of the buyer', isRequired: true, isConditional: false, sampleValue: 'John Smith', sortOrder: 0 },
|
||
{ key: 'AMOUNT', label: 'Amount', description: 'Purchase amount formatted with dollar sign', isRequired: true, isConditional: false, sampleValue: '$49.99', sortOrder: 1 },
|
||
{ key: 'ORDER_ID', label: 'Order ID', description: 'Unique order reference ID', isRequired: true, isConditional: false, sampleValue: 'clxyz456def', sortOrder: 2 },
|
||
{ key: 'PRODUCT_TITLE', label: 'Product Title', description: 'Title of the purchased product', isRequired: true, isConditional: false, sampleValue: 'Community Toolkit', sortOrder: 3 },
|
||
{ key: 'PRODUCT_TYPE', label: 'Product Type', description: 'Type of product (DIGITAL, EVENT, DONATION)', isRequired: true, isConditional: false, sampleValue: 'DIGITAL', sortOrder: 4 },
|
||
{ key: 'PURCHASE_DATE', label: 'Purchase Date', description: 'Date of purchase', isRequired: true, isConditional: false, sampleValue: 'February 17, 2026', sortOrder: 5 },
|
||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 6 },
|
||
],
|
||
},
|
||
{
|
||
key: 'subscription-welcome',
|
||
name: 'Subscription Welcome',
|
||
description: 'Welcome email sent when a user subscribes to a plan',
|
||
category: EmailTemplateCategory.PAYMENT,
|
||
subjectLine: 'Welcome to {{PLAN_NAME}} — {{ORGANIZATION_NAME}}',
|
||
isSystem: true,
|
||
variables: [
|
||
{ key: 'RECIPIENT_NAME', label: 'User Name', description: 'Name of the subscriber', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
|
||
{ key: 'PLAN_NAME', label: 'Plan Name', description: 'Name of the subscription plan', isRequired: true, isConditional: false, sampleValue: 'Pro Plan', sortOrder: 1 },
|
||
{ key: 'AMOUNT', label: 'Amount', description: 'Subscription price formatted with dollar sign', isRequired: true, isConditional: false, sampleValue: '$9.99', sortOrder: 2 },
|
||
{ key: 'FREQUENCY', label: 'Billing Frequency', description: 'How often the subscription renews', isRequired: true, isConditional: false, sampleValue: 'per month', sortOrder: 3 },
|
||
{ key: 'RENEWAL_DATE', label: 'Renewal Date', description: 'Next renewal date', isRequired: true, isConditional: false, sampleValue: 'March 17, 2026', sortOrder: 4 },
|
||
{ key: 'SUBSCRIPTION_ID', label: 'Subscription ID', description: 'Stripe subscription ID', isRequired: true, isConditional: false, sampleValue: 'sub_1234567890', sortOrder: 5 },
|
||
{ key: 'LOGIN_URL', label: 'Login URL', description: 'URL to log in to the platform', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/login', sortOrder: 6 },
|
||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 7 },
|
||
],
|
||
},
|
||
{
|
||
key: 'admin-shift-signup-alert',
|
||
name: 'Admin: New Shift Signup Alert',
|
||
description: 'Notification sent to admins when a volunteer signs up for a shift',
|
||
category: EmailTemplateCategory.MAP,
|
||
subjectLine: 'New shift signup — {{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: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 1 },
|
||
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 2 },
|
||
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer who signed up', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 3 },
|
||
{ key: 'VOLUNTEER_EMAIL', label: 'Volunteer Email', description: 'Email of the volunteer', isRequired: true, isConditional: false, sampleValue: 'jane@example.com', sortOrder: 4 },
|
||
{ key: 'SIGNUP_SOURCE', label: 'Signup Source', description: 'How the volunteer signed up (Public Form, Authenticated Volunteer)', isRequired: true, isConditional: false, sampleValue: 'Public Form', sortOrder: 5 },
|
||
{ key: 'ADMIN_URL', label: 'Admin URL', description: 'URL to the admin shifts page', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/app/map/shifts', sortOrder: 6 },
|
||
],
|
||
},
|
||
{
|
||
key: 'admin-response-submitted-alert',
|
||
name: 'Admin: Response Submitted Alert',
|
||
description: 'Notification sent to admins when a new response is submitted to the response wall',
|
||
category: EmailTemplateCategory.INFLUENCE,
|
||
subjectLine: 'New response submitted — {{CAMPAIGN_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: 'CAMPAIGN_TITLE', label: 'Campaign Title', description: 'Title of the campaign', isRequired: true, isConditional: false, sampleValue: 'Support Climate Action Bill C-12', sortOrder: 1 },
|
||
{ key: 'REPRESENTATIVE_NAME', label: 'Representative Name', description: 'Name of the representative the response is about', isRequired: true, isConditional: false, sampleValue: 'Hon. John Smith', sortOrder: 2 },
|
||
{ key: 'RESPONSE_TYPE', label: 'Response Type', description: 'Type of response (Support, Oppose, etc.)', isRequired: true, isConditional: false, sampleValue: 'SUPPORT', sortOrder: 3 },
|
||
{ key: 'SUBMITTER_NAME', label: 'Submitter Name', description: 'Name of the person who submitted', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 4 },
|
||
{ key: 'ADMIN_URL', label: 'Admin URL', description: 'URL to the admin responses page', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/app/influence/responses', sortOrder: 5 },
|
||
],
|
||
},
|
||
{
|
||
key: 'admin-sign-requested-alert',
|
||
name: 'Admin: Sign Requested Alert',
|
||
description: 'Notification sent to admins when a resident requests a yard sign during canvassing',
|
||
category: EmailTemplateCategory.MAP,
|
||
subjectLine: 'Sign requested — {{ADDRESS}}',
|
||
isSystem: true,
|
||
variables: [
|
||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
|
||
{ key: 'VOLUNTEER_NAME', label: 'Canvasser Name', description: 'Name of the volunteer who recorded the sign request', isRequired: true, isConditional: false, sampleValue: 'John Smith', sortOrder: 1 },
|
||
{ key: 'ADDRESS', label: 'Address', description: 'Street address where sign was requested', isRequired: true, isConditional: false, sampleValue: '123 Main Street', sortOrder: 2 },
|
||
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the canvassing shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 3 },
|
||
{ key: 'SIGN_SIZE', label: 'Sign Size', description: 'Requested sign size', isRequired: false, isConditional: true, sampleValue: 'Large', sortOrder: 4 },
|
||
{ key: 'ADMIN_URL', label: 'Admin URL', description: 'URL to the admin canvass dashboard', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/app/canvass/dashboard', sortOrder: 5 },
|
||
],
|
||
},
|
||
{
|
||
key: 'volunteer-session-summary',
|
||
name: 'Volunteer: Canvass Session Summary',
|
||
description: 'Summary email sent to a volunteer after completing a canvassing session',
|
||
category: EmailTemplateCategory.MAP,
|
||
subjectLine: 'Canvass session summary — {{CUT_NAME}}',
|
||
isSystem: true,
|
||
variables: [
|
||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
|
||
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
|
||
{ key: 'CUT_NAME', label: 'Cut/Area Name', description: 'Name of the canvassing area', isRequired: true, isConditional: false, sampleValue: 'Downtown Core', sortOrder: 2 },
|
||
{ key: 'SESSION_DATE', label: 'Session Date', description: 'Date of the session', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 3 },
|
||
{ key: 'VISIT_COUNT', label: 'Visit Count', description: 'Number of doors visited', isRequired: true, isConditional: false, sampleValue: '42', sortOrder: 4 },
|
||
{ key: 'DURATION_MINUTES', label: 'Duration (minutes)', description: 'Session duration in minutes', isRequired: true, isConditional: false, sampleValue: '95', sortOrder: 5 },
|
||
{ key: 'DISTANCE_KM', label: 'Distance (km)', description: 'Distance walked in kilometers', isRequired: true, isConditional: false, sampleValue: '2.3', sortOrder: 6 },
|
||
{ key: 'OUTCOME_BREAKDOWN', label: 'Outcome Breakdown', description: 'HTML table (email) or text list (plain text) of visit outcomes', isRequired: false, isConditional: true, sampleValue: 'Spoke With: 20, Not Home: 15, Refused: 7', sortOrder: 7 },
|
||
],
|
||
},
|
||
{
|
||
key: 'volunteer-cancellation-ack',
|
||
name: 'Volunteer: Signup Cancellation Acknowledgement',
|
||
description: 'Confirmation email sent to a volunteer when their shift signup is cancelled',
|
||
category: EmailTemplateCategory.MAP,
|
||
subjectLine: 'Signup cancelled — {{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: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
|
||
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the cancelled shift signup', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 2 },
|
||
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 3 },
|
||
{ key: 'SHIFT_TIME', label: 'Shift Time', description: 'Time range of the shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM — 2:00 PM', sortOrder: 4 },
|
||
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse available shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', sortOrder: 5 },
|
||
],
|
||
},
|
||
{
|
||
key: 'volunteer-shift-thank-you',
|
||
name: 'Volunteer: Post-Shift Thank You',
|
||
description: 'Thank-you email sent to a volunteer 2 hours after their shift ends',
|
||
category: EmailTemplateCategory.MAP,
|
||
subjectLine: 'Thank you for volunteering — {{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: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
|
||
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 2 },
|
||
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 3 },
|
||
{ key: 'SHIFT_TIME', label: 'Shift Time', description: 'Time range of the shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM — 2:00 PM', sortOrder: 4 },
|
||
{ key: 'SHIFT_LOCATION', label: 'Shift Location', description: 'Meeting location for the shift', isRequired: false, isConditional: true, sampleValue: 'City Hall', sortOrder: 5 },
|
||
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse upcoming shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', sortOrder: 6 },
|
||
],
|
||
},
|
||
{
|
||
key: 'volunteer-reengagement',
|
||
name: 'Volunteer: Re-Engagement',
|
||
description: 'Re-engagement email sent to volunteers who have been inactive for a configurable period',
|
||
category: EmailTemplateCategory.MAP,
|
||
subjectLine: 'We miss you — {{ORGANIZATION_NAME}}',
|
||
isSystem: true,
|
||
variables: [
|
||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
|
||
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
|
||
{ key: 'LAST_ACTIVITY_DATE', label: 'Last Activity Date', description: 'Date of the volunteer\'s last activity', isRequired: true, isConditional: false, sampleValue: 'January 15, 2026', sortOrder: 2 },
|
||
{ key: 'LAST_ACTIVITY_TYPE', label: 'Last Activity Type', description: 'Type of the volunteer\'s last activity', isRequired: true, isConditional: false, sampleValue: 'Shift Signup', sortOrder: 3 },
|
||
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse upcoming shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', 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`);
|
||
}
|
||
|
||
/**
|
||
* Seed default SMS notification templates
|
||
*/
|
||
async function seedSmsNotificationTemplates() {
|
||
console.log('Seeding SMS notification templates...');
|
||
|
||
const templates = [
|
||
{
|
||
name: 'shift-reminder',
|
||
template: 'Hi {name}, reminder: {shiftTitle} is tomorrow at {shiftTime}. Location: {shiftLocation}',
|
||
description: 'Sent before a volunteer shift as a reminder',
|
||
category: 'notification',
|
||
},
|
||
{
|
||
name: 'shift-signup-confirm',
|
||
template: "Hi {name}, you're signed up for {shiftTitle} on {shiftDate} at {shiftTime}. See you there!",
|
||
description: 'Sent when a volunteer signs up for a shift',
|
||
category: 'notification',
|
||
},
|
||
{
|
||
name: 'volunteer-welcome',
|
||
template: 'Welcome to the team, {name}! Thanks for signing up as a volunteer.',
|
||
description: 'Sent when a new volunteer account is created',
|
||
category: 'notification',
|
||
},
|
||
];
|
||
|
||
let seeded = 0;
|
||
let skipped = 0;
|
||
|
||
for (const t of templates) {
|
||
const existing = await prisma.smsMessageTemplate.findFirst({ where: { name: t.name } });
|
||
if (existing) {
|
||
skipped++;
|
||
continue;
|
||
}
|
||
await prisma.smsMessageTemplate.create({
|
||
data: {
|
||
name: t.name,
|
||
template: t.template,
|
||
description: t.description,
|
||
category: t.category,
|
||
},
|
||
});
|
||
seeded++;
|
||
}
|
||
|
||
console.log(`SMS notification templates seeded: ${seeded} created, ${skipped} skipped`);
|
||
}
|
||
|
||
/**
|
||
* Seed pre-made gallery ads
|
||
*/
|
||
async function seedGalleryAds() {
|
||
console.log('Seeding gallery ads...');
|
||
|
||
const defaultAds = [
|
||
{
|
||
type: 'system',
|
||
variant: 'standard',
|
||
title: 'Join the Community',
|
||
subtitle: 'Create a free account to upvote, comment, and save favorites',
|
||
ctaText: 'Sign Up Free',
|
||
ctaStyle: 'primary',
|
||
linkUrl: '/login',
|
||
visibility: 'anonymous',
|
||
frequency: 8,
|
||
position: 1,
|
||
iconEmoji: null,
|
||
bgColor: null,
|
||
imagePath: null,
|
||
placements: ['gallery', 'docs'],
|
||
},
|
||
{
|
||
type: 'payment_subscribe',
|
||
variant: 'highlight',
|
||
title: 'Unlock Premium Content',
|
||
subtitle: 'Subscribe for exclusive videos, early access, and more',
|
||
ctaText: 'View Plans',
|
||
ctaStyle: 'primary',
|
||
linkUrl: '/pricing',
|
||
visibility: 'non_subscriber',
|
||
frequency: 12,
|
||
position: 2,
|
||
iconEmoji: null,
|
||
bgColor: null,
|
||
imagePath: null,
|
||
placements: ['gallery', 'pricing', 'landing_page', 'docs'],
|
||
},
|
||
{
|
||
type: 'payment_donate',
|
||
variant: 'standard',
|
||
title: 'Support Our Mission',
|
||
subtitle: 'Your donation helps us create more content',
|
||
ctaText: 'Donate Now',
|
||
ctaStyle: 'primary',
|
||
linkUrl: '/donate',
|
||
visibility: 'everyone',
|
||
frequency: 18,
|
||
position: 3,
|
||
iconEmoji: null,
|
||
bgColor: null,
|
||
imagePath: null,
|
||
placements: ['gallery', 'donate', 'landing_page', 'docs'],
|
||
},
|
||
{
|
||
type: 'payment_shop',
|
||
variant: 'standard',
|
||
title: 'Browse the Shop',
|
||
subtitle: 'Exclusive merchandise, downloads, and event tickets',
|
||
ctaText: 'Shop Now',
|
||
ctaStyle: 'primary',
|
||
linkUrl: '/shop',
|
||
visibility: 'everyone',
|
||
frequency: 24,
|
||
position: 4,
|
||
iconEmoji: null,
|
||
bgColor: null,
|
||
imagePath: null,
|
||
placements: ['gallery', 'shop', 'landing_page'],
|
||
},
|
||
{
|
||
type: 'system',
|
||
variant: 'standard',
|
||
title: 'Take Action',
|
||
subtitle: 'Join an advocacy campaign and make your voice heard',
|
||
ctaText: 'View Campaigns',
|
||
ctaStyle: 'primary',
|
||
linkUrl: '/campaigns',
|
||
visibility: 'everyone',
|
||
frequency: 18,
|
||
position: 5,
|
||
iconEmoji: null,
|
||
bgColor: null,
|
||
imagePath: null,
|
||
placements: ['gallery', 'campaigns_list', 'shifts', 'landing_page', 'docs'],
|
||
},
|
||
{
|
||
type: 'system',
|
||
variant: 'standard',
|
||
title: 'Volunteer With Us',
|
||
subtitle: 'Sign up for a shift and help make a difference',
|
||
ctaText: 'See Shifts',
|
||
ctaStyle: 'primary',
|
||
linkUrl: '/shifts',
|
||
visibility: 'everyone',
|
||
frequency: 24,
|
||
position: 6,
|
||
iconEmoji: null,
|
||
bgColor: null,
|
||
imagePath: null,
|
||
placements: ['gallery', 'shifts', 'campaigns_list', 'landing_page'],
|
||
},
|
||
];
|
||
|
||
let seeded = 0;
|
||
let skipped = 0;
|
||
|
||
for (const ad of defaultAds) {
|
||
const existing = await prisma.ad.findFirst({
|
||
where: { type: ad.type, title: ad.title },
|
||
});
|
||
|
||
if (existing) {
|
||
skipped++;
|
||
continue;
|
||
}
|
||
|
||
await prisma.ad.create({
|
||
data: {
|
||
...ad,
|
||
isSystemAd: true,
|
||
isActive: false,
|
||
},
|
||
});
|
||
seeded++;
|
||
}
|
||
|
||
console.log(`Gallery ads seeded: ${seeded} created, ${skipped} skipped`);
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error('Seed error:', e);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|