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(); });