/** * Prepares the end-to-end test database, then exits. Run before the e2e backend starts * (see playwright.config.ts): drops and recreates the database, applies the migrations, * and seeds settings, users and events. Writes e2e/.data/seed.json with the ids and * login tokens the tests use. * * Database: E2E_DATABASE_URL, or backend/.env's DATABASE_URL with "_e2e" added to the * database name. It is always a separate database — never the one you develop against. */ const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); const root = path.join(__dirname, '..', '..'); const backend = path.join(root, 'backend'); const req = (m) => require(require.resolve(m, { paths: [backend] })); req('dotenv').config({ path: path.join(backend, '.env') }); const { e2eDatabaseUrl } = require('./db-url'); async function recreateDatabase(url) { const { Client } = req('pg'); const target = new URL(url); const dbName = decodeURIComponent(target.pathname.replace(/^\//, '')); if (!/_e2e$|^e2e|test/i.test(dbName)) throw new Error(`Refusing to reset "${dbName}" — the e2e database name must contain "e2e" or "test".`); const admin = new URL(url); admin.pathname = '/postgres'; admin.search = ''; const client = new Client({ connectionString: admin.toString() }); await client.connect(); await client.query(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`); await client.query(`CREATE DATABASE "${dbName}"`); await client.end(); } async function seed(url) { process.env.DATABASE_URL = url; const prisma = require(path.join(backend, 'src/config/db')); const { generateToken, hashPassword } = require(path.join(backend, 'src/config/auth')); const { generateTicketsForRegistration } = require(path.join(backend, 'src/utils/ticketUtils')); await prisma.appSetting.createMany({ data: [ { key: 'setup_complete', value: 'true' }, { key: 'org_name', value: 'E2E Church' }, { key: 'org_address', value: '1 Test Street, Pretoria' }, ], }); const password = await hashPassword('E2e-password-123'); const mk = (role, name, email, phoneNumber) => prisma.user.create({ data: { name, email, phoneNumber, role, password, isActive: true, updatedAt: new Date() }, }); const users = { admin: await mk('admin', 'Ada Admin', 'admin@e2e.test', '0820000001'), supervisor: await mk('supervisor', 'Sam Supervisor', 'supervisor@e2e.test', '0820000002'), staff: await mk('staff', 'Stan Staff', 'staff@e2e.test', '0820000003'), user: await mk('user', 'Thandi Test', 'user@e2e.test', '0820000004'), }; const D = 86400000; const now = Date.now(); const at = (days, hourUtc = 16, minute = 30) => { const d = new Date(now + days * D); d.setUTCHours(hourUtc, minute, 0, 0); return d; }; const free = await prisma.event.create({ data: { title: 'E2E Free Evening', description: 'A free evening for the tests.', startDate: at(1), endDate: at(1, 19), price: 0, location: '1 Test Street, Pretoria', createdById: users.supervisor.id, eventOptions: { create: [{ name: 'Entry', price: 0, isMainTicket: true }] }, }, }); const paid = await prisma.event.create({ data: { title: 'E2E Paid Conference', description: 'A paid one-day conference.', startDate: at(3, 7), endDate: at(3, 15), price: 150, location: '1 Test Street, Pretoria', createdById: users.supervisor.id, eventOptions: { create: [{ name: 'Conference ticket', price: 150, isMainTicket: true }] }, }, }); // Multi-day: week 1 was a week ago, week 2 is in progress now, then three more weeks. const todayStart = new Date(now - 30 * 60 * 1000); const sessions = [-7, 0, 7, 14, 21].map((days, i) => { const startsAt = new Date(todayStart.getTime() + days * D); return { label: `Week ${i + 1}`, startsAt, endsAt: new Date(startsAt.getTime() + 90 * 60 * 1000), order: i }; }); const course = await prisma.event.create({ data: { title: 'E2E Alpha Course', description: 'Five weekly evenings.', startDate: sessions[0].startsAt, endDate: sessions[4].endsAt, price: 300, location: '1 Test Street, Pretoria', createdById: users.supervisor.id, scheduleType: 'sessions', lateJoinEnabled: true, latePricing: 'prorata', checkInWindow: 'whole_day', certificatesEnabled: true, certificateConfig: { template: 'classic', title: 'Certificate of Completion', body: '{name} completed {event}.', rule: { type: 'count', value: 2 } }, sessions: { create: sessions }, eventOptions: { create: [{ name: 'Course ticket', price: 300, isMainTicket: true }] }, }, include: { sessions: { orderBy: { startsAt: 'asc' } }, eventOptions: true }, }); await prisma.event.update({ where: { id: course.id }, data: { lateJoinUntilSessionId: course.sessions[2].id } }); // A drop-in class: three upcoming classes, booked per class (the first is full). const classes = [1, 8, 15].map((days, i) => { const startsAt = at(days, 16, 0); return { label: `Class ${i + 1}`, startsAt, endsAt: new Date(startsAt.getTime() + 60 * 60 * 1000), order: i, capacity: i === 0 ? 1 : null }; }); const dropIn = await prisma.event.create({ data: { title: 'E2E Drop-in Class', description: 'Come to the classes you like.', startDate: classes[0].startsAt, endDate: classes[2].endsAt, price: 50, location: '1 Test Street, Pretoria', createdById: users.supervisor.id, scheduleType: 'sessions', attendanceMode: 'pick', waitlistEnabled: true, sessions: { create: classes }, eventOptions: { create: [{ name: 'Drop-in', price: 50, isMainTicket: true, sessionMode: 'pick' }] }, }, include: { sessions: { orderBy: { startsAt: 'asc' } }, eventOptions: true }, }); // A conference sold per pass: people tick the days they'll come, one price for the pass. const days = [2, 3, 4].map((d, i) => { const startsAt = at(d, 7, 0); return { label: `Day ${i + 1}`, startsAt, endsAt: new Date(startsAt.getTime() + 8 * 60 * 60 * 1000), order: i }; }); const conference = await prisma.event.create({ data: { title: 'E2E Conference', description: 'Three days; come to the days you can.', startDate: days[0].startsAt, endDate: days[2].endsAt, price: 200, location: '1 Test Street, Pretoria', createdById: users.supervisor.id, scheduleType: 'sessions', attendanceMode: 'pick', sessions: { create: days }, eventOptions: { create: [{ name: 'Conference pass', price: 200, isMainTicket: true, sessionMode: 'all' }] }, }, }); // Someone else already has the only place in class 1. await prisma.registration.create({ data: { userId: users.staff.id, eventId: dropIn.id, status: 'paid', registrationOptions: { create: [{ eventOptionId: dropIn.eventOptions[0].id, quantity: 1, priceSnapshot: 50, sessionId: dropIn.sessions[0].id, tranches: { create: [{ quantity: 1, priceSnapshot: 50 }] } }] }, }, }); // Thandi is booked on the course and was checked in for week 1. const reg = await prisma.registration.create({ data: { userId: users.user.id, eventId: course.id, status: 'paid', registrationOptions: { create: [{ eventOptionId: course.eventOptions[0].id, quantity: 1, priceSnapshot: 300, tranches: { create: [{ quantity: 1, priceSnapshot: 300 }] } }] }, }, }); await prisma.payment.create({ data: { amount: 300, method: 'cash', userId: users.user.id, registrationId: reg.id, eventId: course.id, recordedById: users.supervisor.id } }); await generateTicketsForRegistration(reg.id); const ticket = await prisma.ticket.findFirst({ where: { eventId: course.id } }); await prisma.ticketUsage.create({ data: { ticketId: ticket.id, scannedById: users.staff.id, sessionId: course.sessions[0].id, scannedAt: course.sessions[0].startsAt } }); const out = { users: Object.fromEntries(Object.entries(users).map(([k, u]) => [k, { id: u.id, name: u.name, email: u.email, token: generateToken(u.id, u.role, u.tokenVersion || 0) }])), events: { free: free.id, paid: paid.id, course: course.id, dropIn: dropIn.id, conference: conference.id }, course: { sessions: course.sessions.map(s => ({ id: s.id, label: s.label })), ticketId: ticket.id, qrCode: ticket.qrCode }, }; fs.mkdirSync(path.join(root, 'e2e', '.data'), { recursive: true }); fs.writeFileSync(path.join(root, 'e2e', '.data', 'seed.json'), JSON.stringify(out, null, 2)); await prisma.$disconnect?.(); } (async () => { const url = e2eDatabaseUrl(); await recreateDatabase(url); execSync('npx prisma migrate deploy', { cwd: backend, env: { ...process.env, DATABASE_URL: url }, stdio: 'ignore' }); await seed(url); console.log('[e2e] Database ready:', new URL(url).pathname.slice(1)); process.exit(0); })().catch((e) => { console.error('[e2e] Preparing the database failed:', e); process.exit(1); });