@@ -0,0 +1,863 @@
/ * *
* set - demo . js
*
* Replaces the database contents with demo data for showing the system to other
* churches : six upcoming events with registrations , payments , tickets , attendee forms ,
* coupons , a waitlist , camp cabins , sponsorships , costs and funnel analytics .
*
* Everything is removed first EXCEPT the site settings ( AppSetting ) and the admin
* account ( DEMO _ADMIN _EMAIL , default joshua @ crosscode . co . za ) . Dates are relative to
* today , so the events are always in the future whenever this is run .
*
* No emails or WhatsApp messages are sent . Demo people use @ example . com addresses and
* have no phone numbers , so later reminders can ' t reach a real person .
*
* Usage :
* cd backend
* npm run set - demo ( asks for confirmation )
* npm run set - demo -- -- yes ( no prompt )
* /
'use strict' ;
const path = require ( 'path' ) ;
const fs = require ( 'fs' ) ;
const crypto = require ( 'crypto' ) ;
const readline = require ( 'readline' ) ;
require ( 'dotenv' ) . config ( { path : path . resolve ( _ _dirname , '../.env' ) } ) ;
const bcrypt = require ( 'bcryptjs' ) ;
const { v4 : uuidv4 } = require ( 'uuid' ) ;
const { createPrismaClient } = require ( '../src/config/db' ) ;
const prisma = createPrismaClient ( ) ;
const ADMIN _EMAIL = ( process . env . DEMO _ADMIN _EMAIL || 'joshua@crosscode.co.za' ) . trim ( ) . toLowerCase ( ) ;
const DEMO _PASSWORD = 'Demo@1234' ;
const SKIP _PROMPT = process . argv . includes ( '--yes' ) || process . argv . includes ( '-y' ) ;
// Not truncated: migrations, site settings, users (all but the admin are deleted
// separately) and the admin's own account-security rows.
const KEEP _TABLES = new Set ( [
'_prisma_migrations' , 'AppSetting' , 'User' ,
'SecurityEvent' , 'PasswordReset' , 'TwoFactorChallenge' , 'TrustedDevice' ,
] ) ;
const SCHEDULED _EMAILS _PATH = path . resolve ( _ _dirname , '../data/scheduled-emails.json' ) ;
// ─── Random helpers (seeded, so every run produces the same-looking demo) ────
let seed = 20261021 ;
function rand ( ) {
seed = ( seed + 0x6D2B79F5 ) | 0 ;
let t = Math . imul ( seed ^ ( seed >>> 15 ) , 1 | seed ) ;
t = ( t + Math . imul ( t ^ ( t >>> 7 ) , 61 | t ) ) ^ t ;
return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296 ;
}
const int = ( min , max ) => min + Math . floor ( rand ( ) * ( max - min + 1 ) ) ;
const pick = ( arr ) => arr [ Math . floor ( rand ( ) * arr . length ) ] ;
const chance = ( p ) => rand ( ) < p ;
const round2 = ( n ) => Math . round ( n * 100 ) / 100 ;
/** Picks a key from { key: weight }. */
function weighted ( weights ) {
const total = Object . values ( weights ) . reduce ( ( s , w ) => s + w , 0 ) ;
let r = rand ( ) * total ;
for ( const [ key , w ] of Object . entries ( weights ) ) {
if ( ( r -= w ) < 0 ) return key ;
}
return Object . keys ( weights ) [ 0 ] ;
}
function shuffle ( arr ) {
const out = arr . slice ( ) ;
for ( let i = out . length - 1 ; i > 0 ; i -- ) {
const j = Math . floor ( rand ( ) * ( i + 1 ) ) ;
[ out [ i ] , out [ j ] ] = [ out [ j ] , out [ i ] ] ;
}
return out ;
}
// ─── Dates ───────────────────────────────────────────────────────────────────
const NOW = new Date ( ) ;
const MINUTE = 60 * 1000 ;
const HOUR = 60 * MINUTE ;
const DAY = 24 * HOUR ;
/** The first `weekday` (0 = Sunday) at least `minDays` from today, at hour:minute. */
function nextWeekday ( minDays , weekday , hour , minute = 0 ) {
const d = new Date ( NOW ) ;
d . setDate ( d . getDate ( ) + minDays ) ;
while ( d . getDay ( ) !== weekday ) d . setDate ( d . getDate ( ) + 1 ) ;
d . setHours ( hour , minute , 0 , 0 ) ;
return d ;
}
const addTime = ( date , ms ) => new Date ( date . getTime ( ) + ms ) ;
const daysFromNow = ( days ) => addTime ( NOW , days * DAY ) ;
/** A random moment between minDaysAgo and maxDaysAgo, during waking hours, never in the future. */
function pastDate ( maxDaysAgo , minDaysAgo = 0 ) {
const d = new Date ( NOW . getTime ( ) - ( minDaysAgo + rand ( ) * ( maxDaysAgo - minDaysAgo ) ) * DAY ) ;
d . setHours ( int ( 7 , 21 ) , int ( 0 , 59 ) , int ( 0 , 59 ) , 0 ) ;
return d < NOW ? d : new Date ( NOW . getTime ( ) - int ( 5 , 120 ) * MINUTE ) ;
}
/** A moment a little after `from`, clamped to the past. */
function after ( from , minMinutes , maxMinutes ) {
const t = from . getTime ( ) + int ( minMinutes , maxMinutes ) * MINUTE ;
return new Date ( Math . min ( t , NOW . getTime ( ) - MINUTE ) ) ;
}
// ─── People ──────────────────────────────────────────────────────────────────
const FEMALE _FIRST = [
'Thandi' , 'Nomsa' , 'Lerato' , 'Zanele' , 'Palesa' , 'Ayanda' , 'Naledi' , 'Busisiwe' , 'Precious' ,
'Refilwe' , 'Anri' , 'Elmarie' , 'Liezl' , 'Marike' , 'Annelie' , 'Charlene' , 'Michelle' , 'Nicole' ,
'Sarah' , 'Rachel' , 'Hannah' , 'Grace' , 'Megan' , 'Jessica' , 'Lauren' , 'Kelly' , 'Tamryn' , 'Candice' ,
'Fatima' , 'Priya' , 'Kavitha' , 'Nadia' , 'Bianca' , 'Chantelle' , 'Tanya' , 'Mpho' , 'Keabetswe' ,
'Lindiwe' , 'Sibongile' , 'Yolanda' , 'Ruth' , 'Esther' , 'Joanne' , 'Karen' , 'Debbie' , 'Nompumelelo' ,
'Carmen' , 'Simone' , 'Natasha' , 'Ilse' , 'Rochelle' , 'Tumi' , 'Dineo' , 'Hlengiwe' , 'Abigail' ,
] ;
const MALE _FIRST = [
'Sipho' , 'Thabo' , 'Bongani' , 'Mandla' , 'Kagiso' , 'Tshepo' , 'Lwazi' , 'Sizwe' , 'Themba' , 'Musa' ,
'Pieter' , 'Johan' , 'Francois' , 'Hendrik' , 'Marius' , 'Wikus' , 'Riaan' , 'Gerhard' , 'David' ,
'Michael' , 'Daniel' , 'Matthew' , 'Andrew' , 'Jonathan' , 'Ryan' , 'Craig' , 'Brandon' , 'Shaun' ,
'Grant' , 'Kyle' , 'Ashwin' , 'Rajesh' , 'Yusuf' , 'Tumelo' , 'Neo' , 'Karabo' , 'Peter' , 'Mark' ,
'Paul' , 'Simon' , 'Brian' , 'Trevor' , 'Wayne' , 'Chris' , 'Luyanda' , 'Jaco' , 'Stefan' , 'Ernest' ,
'Vusi' , 'Kabelo' , 'Dumisani' , 'Graham' , 'Clive' , 'Nathan' , 'Ruben' , 'Emmanuel' , 'Isaac' ,
] ;
const LAST = [
'Dlamini' , 'Nkosi' , 'Mokoena' , 'Ndlovu' , 'Mthembu' , 'Zulu' , 'Nxumalo' , 'Khumalo' , 'Molefe' ,
'Mahlangu' , 'Sithole' , 'Mabaso' , 'Radebe' , 'Baloyi' , 'Maseko' , 'Ntuli' , 'du Plessis' ,
'van der Merwe' , 'Botha' , 'Steyn' , 'Fourie' , 'Pretorius' , 'Venter' , 'Coetzee' , 'van Niekerk' ,
'Lombard' , 'Nel' , 'Kruger' , 'Smit' , 'Jacobs' , 'Smith' , 'Williams' , 'Brown' , 'Johnson' , 'Adams' ,
'Petersen' , 'Daniels' , 'Hendricks' , 'Abrahams' , 'Isaacs' , 'Naidoo' , 'Pillay' , 'Govender' ,
'Reddy' , 'Moodley' , 'Patel' , 'Campbell' , 'Taylor' , 'Thomas' , 'Wilson' , 'Clarke' , 'Morgan' ,
] ;
const GIRL _FIRST = [
'Amahle' , 'Lesedi' , 'Isabella' , 'Mia' , 'Zoe' , 'Ava' , 'Leah' , 'Kayla' , 'Nandi' , 'Olwethu' , 'Jade' ,
'Chloe' , 'Emma' , 'Lisa' , 'Anika' , 'Karabo' , 'Lilitha' , 'Bella' , 'Gabriella' , 'Imaan' ,
] ;
const BOY _FIRST = [
'Liam' , 'Noah' , 'Ethan' , 'Luke' , 'Josh' , 'Siyabonga' , 'Lethabo' , 'Caleb' , 'Aiden' , 'Jayden' ,
'Ruan' , 'Tiaan' , 'Bandile' , 'Ofentse' , 'Zaid' , 'Reece' , 'Keegan' , 'Asanda' , 'Jordan' , 'Kian' ,
] ;
const emailSlug = ( s ) => s . toLowerCase ( ) . normalize ( 'NFD' ) . replace ( /[^a-z]/g , '' ) ;
// ─── Row collection ──────────────────────────────────────────────────────────
// Everything is built in memory with pre-generated ids, then written with createMany
// in dependency order inside one transaction.
const rows = { } ;
const add = ( model , row ) => {
( rows [ model ] = rows [ model ] || [ ] ) . push ( row ) ;
return row ;
} ;
const INSERT _ORDER = [
'user' , 'event' , 'eventOption' , 'optionVariant' , 'earlyBirdTier' , 'section' , 'sectionOption' ,
'eventForm' , 'eventFormField' , 'coupon' , 'couponEvent' , 'registration' , 'registrationOption' ,
'registrationOptionTranche' , 'attendee' , 'payment' , 'ticket' , 'couponRedemption' ,
'formResponse' , 'formAnswer' , 'consentRequest' , 'waitlistEntry' , 'allocationSet' ,
'allocationGroup' , 'allocationAssignment' , 'eventCost' , 'analyticsEvent' ,
] ;
// Extra facts about demo people that aren't columns (kept out of the inserted rows).
const userMeta = new Map ( ) ;
let admin ;
let passwordHash ;
const staffTeam = [ ] ;
function addUser ( { first , last , gender , role = 'user' , email } ) {
const usedEmails = new Set ( ( rows . user || [ ] ) . map ( u => u . email ) . concat ( ADMIN _EMAIL ) ) ;
let address = email || ` ${ emailSlug ( first ) } . ${ emailSlug ( last ) } @example.com ` ;
for ( let n = 2 ; usedEmails . has ( address ) ; n ++ ) address = ` ${ emailSlug ( first ) } . ${ emailSlug ( last ) } ${ n } @example.com ` ;
const user = add ( 'user' , {
id : uuidv4 ( ) ,
name : ` ${ first } ${ last } ` ,
email : address ,
password : passwordHash ,
role ,
isActive : true ,
notificationPreference : 'email' ,
createdAt : pastDate ( 420 , 40 ) ,
} ) ;
userMeta . set ( user . id , { first , last , gender } ) ;
return user ;
}
function buildPeople ( ) {
staffTeam . push (
addUser ( { first : 'Lerato' , last : 'Khumalo' , gender : 'female' , role : 'supervisor' , email : 'lerato.khumalo@example.com' } ) ,
addUser ( { first : 'Sipho' , last : 'Dlamini' , gender : 'male' , role : 'staff' , email : 'sipho.dlamini@example.com' } ) ,
addUser ( { first : 'Anri' , last : 'Botha' , gender : 'female' , role : 'staff' , email : 'anri.botha@example.com' } ) ,
addUser ( { first : 'Daniel' , last : 'Pillay' , gender : 'male' , role : 'staff' , email : 'daniel.pillay@example.com' } ) ,
) ;
const women = [ ] ;
const men = [ ] ;
for ( let i = 0 ; i < 62 ; i ++ ) women . push ( addUser ( { first : pick ( FEMALE _FIRST ) , last : pick ( LAST ) , gender : 'female' } ) ) ;
for ( let i = 0 ; i < 64 ; i ++ ) men . push ( addUser ( { first : pick ( MALE _FIRST ) , last : pick ( LAST ) , gender : 'male' } ) ) ;
return { women , men , everyone : women . concat ( men ) } ;
}
// ─── Event building blocks ───────────────────────────────────────────────────
function addEvent ( data ) {
return add ( 'event' , {
id : uuidv4 ( ) ,
createdById : admin . id ,
goLiveAt : addTime ( NOW , - 35 * DAY ) ,
createdAt : addTime ( NOW , - 38 * DAY ) ,
isActive : true ,
contactName : 'Lerato Khumalo' ,
contactEmail : 'events@example.com' ,
contactPhone : '021 555 0142' ,
... data ,
} ) ;
}
function addOption ( event , data ) {
return add ( 'eventOption' , { id : uuidv4 ( ) , eventId : event . id , isMainTicket : true , stockLimit : 0 , ... data } ) ;
}
function addVariants ( option , variants ) {
return variants . map ( ( v , order ) => add ( 'optionVariant' , { id : uuidv4 ( ) , eventOptionId : option . id , order , stockLimit : 0 , price : null , ... v } ) ) ;
}
function addTier ( option , data ) {
return add ( 'earlyBirdTier' , { id : uuidv4 ( ) , eventOptionId : option . id , variantId : null , order : 0 , stockLimit : 0 , ... data } ) ;
}
function addSection ( event , name , options ) {
const section = add ( 'section' , { id : uuidv4 ( ) , eventId : event . id , name } ) ;
for ( const o of options ) add ( 'sectionOption' , { id : uuidv4 ( ) , sectionId : section . id , eventOptionId : o . id } ) ;
return section ;
}
function addCost ( event , label , costType , amount , option = null , notes = null ) {
add ( 'eventCost' , { id : uuidv4 ( ) , eventId : event . id , eventOptionId : option ? option . id : null , label , costType , amount , paidFromMethod : null , notes } ) ;
}
// Early-bird tiers already sold, for stock limits.
const tierSold = new Map ( ) ;
/** The price a line bought at `when` gets: cheapest open tier with stock left, else the base price. */
function priceAt ( option , variant , when , quantity ) {
const tiers = ( rows . earlyBirdTier || [ ] )
. filter ( t => t . eventOptionId === option . id && t . variantId === ( variant ? variant . id : null ) )
. sort ( ( a , b ) => a . price - b . price ) ;
for ( const tier of tiers ) {
if ( when >= tier . deadline ) continue ;
const sold = tierSold . get ( tier . id ) || 0 ;
if ( tier . stockLimit > 0 && sold + quantity > tier . stockLimit ) continue ;
tierSold . set ( tier . id , sold + quantity ) ;
return { price : tier . price , tier } ;
}
const base = variant && variant . price !== null ? variant . price : option . price ;
return { price : base , tier : null } ;
}
const line = ( option , quantity , extra = { } ) => ( { option , quantity , ... extra } ) ;
// ─── Registrations ───────────────────────────────────────────────────────────
/ * *
* One registration with its lines , payments and ( once paid ) tickets .
* pay : 'full' | 'split' ( deposit then balance ) | 'deposit' | 'none' | 'cancelled'
* /
function addRegistration ( event , user , lines , { createdAt , pay = 'full' , method = 'card' , deposit = 0 , coupon = null } ) {
const registration = add ( 'registration' , {
id : uuidv4 ( ) ,
userId : user . id ,
eventId : event . id ,
status : 'pending' ,
discountTotal : 0 ,
ticketDelivery : 'payer' ,
analyticsSessionId : null ,
createdAt ,
} ) ;
let subtotal = 0 ;
const regLines = lines . filter ( l => l . quantity > 0 ) . map ( l => {
const { price , tier } = priceAt ( l . option , l . variant || null , createdAt , l . quantity ) ;
const ro = add ( 'registrationOption' , {
id : uuidv4 ( ) ,
registrationId : registration . id ,
eventOptionId : l . option . id ,
variantId : l . variant ? l . variant . id : null ,
quantity : l . quantity ,
priceSnapshot : price ,
appliedTierId : tier ? tier . id : null ,
} ) ;
add ( 'registrationOptionTranche' , {
id : uuidv4 ( ) ,
registrationOptionId : ro . id ,
quantity : l . quantity ,
priceSnapshot : price ,
appliedTierId : tier ? tier . id : null ,
createdAt ,
} ) ;
subtotal += l . quantity * price ;
const attendees = ( l . attendees || [ ] ) . map ( ( a , order ) => add ( 'attendee' , {
id : uuidv4 ( ) ,
registrationId : registration . id ,
registrationOptionId : ro . id ,
eventOptionId : l . option . id ,
variantId : l . variant ? l . variant . id : null ,
order ,
createdAt ,
... a ,
} ) ) ;
return { ro , option : l . option , attendees } ;
} ) ;
if ( coupon && pay !== 'cancelled' ) {
const discount = coupon . type === 'percent' ? round2 ( subtotal * coupon . value / 100 ) : Math . min ( subtotal , coupon . value ) ;
registration . discountTotal = discount ;
add ( 'couponRedemption' , { id : uuidv4 ( ) , couponId : coupon . id , registrationId : registration . id , userId : user . id , discountAmount : discount , createdAt } ) ;
}
const total = round2 ( subtotal - registration . discountTotal ) ;
const payments = [ ] ;
const pay1 = ( amount , at , how = method ) => {
payments . push ( add ( 'payment' , {
id : uuidv4 ( ) ,
amount : round2 ( amount ) ,
method : how ,
userId : user . id ,
recordedById : how === 'card' ? null : pick ( staffTeam ) . id ,
registrationId : registration . id ,
eventId : event . id ,
status : how === 'card' ? 'completed' : null ,
externalId : how === 'card' ? ` demo_ ${ uuidv4 ( ) . replace ( /-/g , '' ) } ` : null ,
createdAt : at ,
} ) ) ;
} ;
const firstPaidAt = after ( createdAt , method === 'card' ? 2 : 600 , method === 'card' ? 15 : 3 * 24 * 60 ) ;
if ( total > 0 ) {
if ( pay === 'full' ) pay1 ( total , firstPaidAt ) ;
if ( pay === 'deposit' || pay === 'split' ) pay1 ( Math . min ( deposit , total ) , firstPaidAt ) ;
if ( pay === 'split' ) pay1 ( total - Math . min ( deposit , total ) , after ( firstPaidAt , 3 * 24 * 60 , 20 * 24 * 60 ) ) ;
}
const paid = round2 ( payments . reduce ( ( s , p ) => s + p . amount , 0 ) ) ;
if ( pay === 'cancelled' ) registration . status = 'cancelled' ;
else if ( paid >= total ) registration . status = 'paid' ;
else if ( paid > 0 ) registration . status = 'partial_paid' ;
if ( registration . status === 'paid' ) {
const issuedAt = payments . length ? payments [ payments . length - 1 ] . createdAt : after ( createdAt , 0 , 1 ) ;
for ( const { ro , attendees } of regLines ) {
for ( const a of attendees ) {
add ( 'ticket' , { id : uuidv4 ( ) , qrCode : uuidv4 ( ) , registrationOptionId : ro . id , userId : user . id , eventId : event . id , quantity : 1 , attendeeId : a . id , holderUserId : a . userId || null , emailSent : true , createdAt : issuedAt } ) ;
}
const unnamed = ro . quantity - attendees . length ;
if ( unnamed > 0 ) {
add ( 'ticket' , { id : uuidv4 ( ) , qrCode : uuidv4 ( ) , registrationOptionId : ro . id , userId : user . id , eventId : event . id , quantity : unnamed , emailSent : true , createdAt : issuedAt } ) ;
}
}
}
return { registration , regLines } ;
}
/** Picks a payment plan from weights like { card: 0.7, eft: 0.1, none: 0.2 }. */
function payPlan ( weights ) {
const choice = weighted ( weights ) ;
return choice === 'none' ? { pay : 'none' , method : 'card' } : { pay : 'full' , method : choice } ;
}
function addForm ( event , fields ) {
const form = add ( 'eventForm' , { id : uuidv4 ( ) , eventId : event . id , isRequired : true } ) ;
return fields . map ( ( f , order ) => add ( 'eventFormField' , { id : uuidv4 ( ) , formId : form . id , order , isRequired : false , helpText : null , options : null , ... f } ) ) ;
}
function addFormResponse ( registration , answers ) {
const response = add ( 'formResponse' , { id : uuidv4 ( ) , registrationId : registration . id , createdAt : registration . createdAt } ) ;
for ( const [ field , value ] of answers ) {
if ( value !== null && value !== '' ) add ( 'formAnswer' , { id : uuidv4 ( ) , responseId : response . id , fieldId : field . id , value : String ( value ) } ) ;
}
}
// ─── The six events ──────────────────────────────────────────────────────────
const IMG = ( id ) => ` https://images.unsplash.com/ ${ id } ?auto=format&fit=crop&w=1600&q=80 ` ;
function buildFamilyFunDay ( people ) {
const start = nextWeekday ( 15 , 6 , 10 ) ;
const event = addEvent ( {
title : 'Family Fun Day' ,
description : 'A full day of fun for the whole family on the church grounds!\n\n'
+ 'Jumping castles, a petting zoo, face painting, a five-a-side soccer tournament and a '
+ 'treasure hunt for the kids — plus coffee, boerewors rolls and a quiet shady spot for the grown-ups.\n\n'
+ 'Bring a picnic blanket and your camp chairs. Children under 3 get in free.' ,
startDate : start ,
endDate : addTime ( start , 5 * HOUR ) ,
registrationDeadline : addTime ( start , - DAY ) ,
price : 80 ,
picture : IMG ( 'photo-1529156069898-49953e39b3ac' ) ,
location : 'Church grounds & sports field' ,
} ) ;
const adult = addOption ( event , { name : 'Adult' , price : 80 } ) ;
const child = addOption ( event , { name : 'Child (3– 12)' , price : 40 } ) ;
const family = addOption ( event , { name : 'Family pass (2 adults + 3 kids)' , price : 220 } ) ;
const lunch = addOption ( event , { name : 'Lunch pack' , price : 65 , isMainTicket : false } ) ;
add ( 'coupon' , {
id : uuidv4 ( ) , code : 'FUNDAY20' , description : 'R20 off for first-time visitors' , type : 'fixed' , value : 20 ,
stackEarlyBird : false , validFrom : addTime ( NOW , - 30 * DAY ) , validUntil : start , maxUses : 50 , maxUsesPerUser : 1 ,
restrictRoles : [ ] , active : true , createdById : admin . id , createdAt : addTime ( NOW , - 30 * DAY ) ,
} ) ;
add ( 'couponEvent' , { id : uuidv4 ( ) , couponId : rows . coupon [ rows . coupon . length - 1 ] . id , eventId : event . id } ) ;
addSection ( event , 'Main gate' , [ adult , child , family ] ) ;
addSection ( event , 'Food stall' , [ lunch ] ) ;
addCost ( event , 'Jumping castles & games hire' , 'once_off' , 4500 ) ;
addCost ( event , 'Face painter' , 'once_off' , 1200 ) ;
addCost ( event , 'Lunch packs' , 'per_item' , 38 , lunch , 'Supplied by the catering team' ) ;
for ( const user of shuffle ( people . everyone ) . slice ( 0 , 30 ) ) {
const lines = chance ( 0.35 )
? [ line ( family , 1 ) ]
: [ line ( adult , int ( 1 , 2 ) ) , line ( child , Number ( weighted ( { 0 : 2 , 1 : 3 , 2 : 3 , 3 : 1 } ) ) ) ] ;
lines . push ( line ( lunch , chance ( 0.6 ) ? int ( 1 , 4 ) : 0 ) ) ;
addRegistration ( event , user , lines , { createdAt : pastDate ( 30 ) , ... payPlan ( { card : 0.72 , eft : 0.12 , none : 0.16 } ) } ) ;
}
return event ;
}
function buildMensBreakfast ( people ) {
const start = nextWeekday ( 8 , 6 , 7 , 30 ) ;
const event = addEvent ( {
title : "Men's Breakfast: Built to Last" ,
description : 'Join the men of the church for a full English breakfast and an honest conversation '
+ 'about faith, work and family.\n\n'
+ 'Our guest speaker is Pastor Ernest Molefe, who has spent twenty years mentoring young fathers. '
+ 'Bring a friend, a neighbour or a colleague — there is a seat for everyone.' ,
startDate : start ,
endDate : addTime ( start , 2.5 * HOUR ) ,
registrationDeadline : addTime ( start , - 2 * DAY ) ,
price : 150 ,
picture : IMG ( 'photo-1414235077428-338989a2e8c0' ) ,
location : 'Church hall' ,
waitlistEnabled : true ,
contactName : 'Sipho Dlamini' ,
} ) ;
const ticket = addOption ( event , { name : 'Breakfast ticket' , price : 150 , stockLimit : 60 } ) ;
addSection ( event , 'Hall entrance' , [ ticket ] ) ;
addCost ( event , 'Breakfast catering' , 'per_item' , 85 , ticket ) ;
addCost ( event , 'Guest speaker gift' , 'once_off' , 750 ) ;
const men = shuffle ( people . men ) ;
let sold = 0 ;
while ( sold < 60 ) {
const user = men . shift ( ) ;
const quantity = Math . min ( chance ( 0.2 ) ? 2 : 1 , 60 - sold ) ;
sold += quantity ;
addRegistration ( event , user , [ line ( ticket , quantity ) ] , { createdAt : pastDate ( 28 ) , ... payPlan ( { card : 0.65 , cash : 0.15 , eft : 0.08 , none : 0.12 } ) } ) ;
}
for ( let i = 0 ; i < 2 ; i ++ ) {
addRegistration ( event , men . shift ( ) , [ line ( ticket , 1 ) ] , { createdAt : pastDate ( 25 , 5 ) , pay : 'cancelled' } ) ;
}
for ( let position = 1 ; position <= 5 ; position ++ ) {
add ( 'waitlistEntry' , {
id : uuidv4 ( ) , eventId : event . id , eventOptionId : ticket . id , userId : men . shift ( ) . id ,
quantity : position === 3 ? 2 : 1 , position , status : 'waiting' , createdAt : pastDate ( 4 ) ,
} ) ;
}
return event ;
}
function buildWomensConference ( people ) {
const start = nextWeekday ( 34 , 5 , 18 ) ;
const event = addEvent ( {
title : 'Rooted Women’ s Conference' ,
description : 'Two days of worship, teaching and real connection for women of every age and season.\n\n'
+ 'Friday evening opens with a worship night. Saturday brings two main sessions, your choice of '
+ 'workshop and a long lunch together in the garden.\n\n'
+ 'Early-bird prices are available for a limited time — gather your small group and register together.' ,
startDate : start ,
endDate : addTime ( start , 22 * HOUR ) ,
registrationDeadline : addTime ( start , - 3 * DAY ) ,
price : 450 ,
picture : IMG ( 'photo-1511632765486-a01980e01a18' ) ,
location : 'Main auditorium' ,
contactName : 'Anri Botha' ,
} ) ;
const full = addOption ( event , { name : 'Full conference (Fri + Sat)' , price : 450 } ) ;
const saturday = addOption ( event , { name : 'Saturday only' , price : 280 } ) ;
const shirt = addOption ( event , { name : 'Conference T-shirt' , price : 180 , isMainTicket : false } ) ;
const sizes = addVariants ( shirt , [ { name : 'S' } , { name : 'M' } , { name : 'L' } , { name : 'XL' } , { name : 'XXL' } ] ) ;
addTier ( full , { deadline : addTime ( NOW , - 8 * DAY ) , price : 300 , stockLimit : 25 , order : 0 } ) ;
addTier ( full , { deadline : addTime ( NOW , 10 * DAY ) , price : 380 , stockLimit : 0 , order : 1 } ) ;
const coupon = add ( 'coupon' , {
id : uuidv4 ( ) , code : 'ROOTED20' , description : '20% off for small groups registering together' , type : 'percent' , value : 20 ,
stackEarlyBird : true , validFrom : addTime ( NOW , - 30 * DAY ) , validUntil : start , maxUses : 30 , maxUsesPerUser : 1 ,
restrictRoles : [ ] , active : true , createdById : admin . id , createdAt : addTime ( NOW , - 30 * DAY ) ,
} ) ;
add ( 'couponEvent' , { id : uuidv4 ( ) , couponId : coupon . id , eventId : event . id } ) ;
addSection ( event , 'Main hall' , [ full , saturday ] ) ;
addSection ( event , 'Merch table' , [ shirt ] ) ;
addCost ( event , 'Venue, sound & lighting' , 'once_off' , 12500 ) ;
addCost ( event , 'Catering (full conference)' , 'per_item' , 120 , full ) ;
addCost ( event , 'Catering (Saturday)' , 'per_item' , 70 , saturday ) ;
addCost ( event , 'T-shirt printing' , 'per_item' , 95 , shirt ) ;
const [ diet , workshop , firstTime , prayer ] = addForm ( event , [
{ type : 'multiple_choice' , label : 'Dietary requirements' , isRequired : true , options : [ 'None' , 'Vegetarian' , 'Vegan' , 'Halaal' , 'Gluten-free' ] } ,
{ type : 'multiple_choice' , label : 'Which Saturday workshop would you like to attend?' , isRequired : true , options : [ 'Prayer & intercession' , 'Marriage & family' , 'Women in leadership' , 'Finding rest' ] } ,
{ type : 'yes_no' , label : 'Is this your first Rooted conference?' , isRequired : true } ,
{ type : 'text' , label : 'Is there anything we can pray with you about?' , helpText : 'Only our prayer team will see this.' } ,
] ) ;
const PRAYERS = [ 'My mom’ s health' , 'A new job' , 'Wisdom for my family' , 'My teenage kids' , 'Healing' , 'Peace in a tough season' ] ;
for ( const user of shuffle ( people . women ) . slice ( 0 , 50 ) ) {
const quantity = Number ( weighted ( { 1 : 70 , 2 : 20 , 3 : 6 , 4 : 4 } ) ) ;
const main = chance ( 0.75 ) ? full : saturday ;
const lines = [ line ( main , quantity ) ] ;
if ( chance ( 0.4 ) ) lines . push ( line ( shirt , 1 , { variant : pick ( sizes ) } ) ) ;
const plan = payPlan ( { card : 0.8 , eft : 0.08 , none : 0.12 } ) ;
const { registration } = addRegistration ( event , user , lines , {
createdAt : pastDate ( 30 ) ,
... plan ,
coupon : quantity >= 2 && plan . pay === 'full' && chance ( 0.5 ) ? coupon : null ,
} ) ;
for ( let i = 0 ; i < quantity ; i ++ ) {
addFormResponse ( registration , [
[ diet , weighted ( { None : 70 , Vegetarian : 12 , Vegan : 4 , Halaal : 8 , 'Gluten-free' : 6 } ) ] ,
[ workshop , pick ( workshop . options ) ] ,
[ firstTime , chance ( 0.35 ) ? 'yes' : 'no' ] ,
[ prayer , chance ( 0.25 ) ? pick ( PRAYERS ) : null ] ,
] ) ;
}
}
return event ;
}
function buildYouthCamp ( people ) {
const start = nextWeekday ( 46 , 1 , 9 ) ;
const event = addEvent ( {
title : 'Ignite Summer Camp' ,
description : 'Four days in the mountains for Grade 7– 12s: hiking, river tubing, campfires, '
+ 'night games and powerful evening services.\n\n'
+ 'The bus leaves the church at 09:00 on Monday and returns on Thursday afternoon. All meals, '
+ 'activities and accommodation are included. A R500 deposit per camper secures a place, with '
+ 'the balance due two weeks before camp.\n\n'
+ 'Want to help a teenager get to camp? Use “Sponsor a spot” to give towards our bursary fund.' ,
startDate : start ,
endDate : addTime ( start , 3 * DAY + 5 * HOUR ) ,
registrationDeadline : addTime ( start , - 14 * DAY ) ,
price : 1650 ,
picture : IMG ( 'photo-1478131143081-80f7f84ca84d' ) ,
location : 'Bergview Camp, Drakensberg' ,
allowNamedAttendees : true ,
requiresParentalConsent : true ,
consentText : 'I am the parent or legal guardian of this camper and give permission for them to attend '
+ 'Ignite Summer Camp, take part in all supervised activities (including hiking and water activities) '
+ 'and travel on the camp bus. In an emergency I authorise the camp leaders to obtain medical treatment.' ,
consentCollectPhoto : true ,
collectGender : true ,
allowSponsorships : true ,
sponsorshipGoalSpots : 10 ,
contactName : 'Daniel Pillay' ,
} ) ;
const camper = addOption ( event , { name : 'Camper (Grade 7– 12)' , price : 1650 , stockLimit : 80 } ) ;
const hoodie = addOption ( event , { name : 'Camp hoodie' , price : 380 , isMainTicket : false } ) ;
const sizes = addVariants ( hoodie , [ { name : 'Youth L' } , { name : 'S' } , { name : 'M' } , { name : 'L' } , { name : 'XL' } ] ) ;
addSection ( event , 'Bus check-in' , [ camper ] ) ;
addCost ( event , 'Campsite (per camper)' , 'per_item' , 820 , camper ) ;
addCost ( event , 'Bus hire' , 'once_off' , 16800 ) ;
addCost ( event , 'Camp hoodies' , 'per_item' , 210 , hoodie ) ;
const campers = [ ] ;
for ( const parent of shuffle ( people . everyone ) . slice ( 0 , 30 ) ) {
const surname = userMeta . get ( parent . id ) . last ;
const count = Number ( weighted ( { 1 : 55 , 2 : 35 , 3 : 10 } ) ) ;
const attendees = [ ] ;
for ( let i = 0 ; i < count ; i ++ ) {
const gender = chance ( 0.5 ) ? 'female' : 'male' ;
// 12– 17 on the first day of camp.
const dateOfBirth = new Date ( start . getTime ( ) - ( int ( 12 , 17 ) * 365.25 + int ( 10 , 350 ) ) * DAY ) ;
dateOfBirth . setUTCHours ( 0 , 0 , 0 , 0 ) ;
attendees . push ( {
name : ` ${ pick ( gender === 'female' ? GIRL _FIRST : BOY _FIRST ) } ${ surname } ` ,
gender ,
dateOfBirth ,
parentName : parent . name ,
parentEmail : parent . email ,
} ) ;
}
const lines = [ line ( camper , count , { attendees } ) ] ;
if ( chance ( 0.4 ) ) lines . push ( line ( hoodie , int ( 1 , count ) , { variant : pick ( sizes ) } ) ) ;
const plan = weighted ( { full : 30 , split : 20 , deposit : 35 , none : 15 } ) ;
const { registration , regLines } = addRegistration ( event , parent , lines , {
createdAt : pastDate ( 32 ) ,
pay : plan ,
method : chance ( 0.5 ) ? 'card' : 'eft' ,
deposit : 500 * count ,
} ) ;
if ( registration . status === 'pending' ) continue ;
for ( const attendee of regLines [ 0 ] . attendees ) {
campers . push ( attendee ) ;
add ( 'consentRequest' , {
id : uuidv4 ( ) ,
attendeeId : attendee . id ,
eventId : event . id ,
tokenHash : crypto . createHash ( 'sha256' ) . update ( uuidv4 ( ) ) . digest ( 'hex' ) ,
status : 'given' ,
textVersion : 1 ,
consentText : event . consentText ,
parentName : parent . name ,
parentEmail : parent . email ,
signedName : parent . name ,
photoConsent : chance ( 0.9 ) ,
respondedAt : after ( registration . createdAt , 30 , 2 * 24 * 60 ) ,
lastSentAt : registration . createdAt ,
expiresAt : event . startDate ,
createdAt : registration . createdAt ,
} ) ;
}
}
// Cabins, with most campers already placed.
const cabins = add ( 'allocationSet' , { id : uuidv4 ( ) , eventId : event . id , name : 'Cabins' , showOnTicket : true , order : 0 } ) ;
const groups = [
[ 'Eagle cabin' , 'male' , staffTeam [ 1 ] ] , [ 'Lion cabin' , 'male' , staffTeam [ 3 ] ] ,
[ 'Dove cabin' , 'female' , staffTeam [ 0 ] ] , [ 'Fawn cabin' , 'female' , staffTeam [ 2 ] ] ,
] . map ( ( [ name , gender , leader ] , order ) => add ( 'allocationGroup' , { id : uuidv4 ( ) , setId : cabins . id , name , capacity : 14 , gender , leaderUserId : leader . id , order } ) ) ;
const placed = new Map ( groups . map ( g => [ g . id , 0 ] ) ) ;
for ( const attendee of campers ) {
if ( ! chance ( 0.8 ) ) continue ;
const group = groups . find ( g => g . gender === attendee . gender && placed . get ( g . id ) < g . capacity ) ;
if ( ! group ) continue ;
placed . set ( group . id , placed . get ( group . id ) + 1 ) ;
add ( 'allocationAssignment' , { id : uuidv4 ( ) , setId : cabins . id , groupId : group . id , attendeeId : attendee . id } ) ;
}
// Sponsor-a-spot donations into the bursary pool.
const MESSAGES = [ 'Every child deserves camp!' , 'Camp changed my life — pass it on.' , 'Praying for a great week.' , null , null ] ;
for ( const sponsor of shuffle ( people . everyone ) . slice ( 0 , 9 ) ) {
const anonymous = chance ( 0.3 ) ;
add ( 'payment' , {
id : uuidv4 ( ) , amount : pick ( [ 500 , 825 , 1000 , 1650 , 1650 , 3300 ] ) , method : 'card' , userId : sponsor . id ,
eventId : event . id , isDonation : true , isSponsorship : true , sponsorName : anonymous ? null : sponsor . name ,
sponsorMessage : pick ( MESSAGES ) , sponsorAnonymous : anonymous , status : 'completed' ,
externalId : ` demo_ ${ uuidv4 ( ) . replace ( /-/g , '' ) } ` , createdAt : pastDate ( 30 ) ,
} ) ;
}
return event ;
}
function buildCarols ( people ) {
const start = nextWeekday ( 55 , 0 , 19 ) ;
const event = addEvent ( {
title : 'Carols by Candlelight' ,
description : 'Our much-loved Christmas evening under the stars.\n\n'
+ 'Bring a blanket and the whole family for carols, the Christmas story told by our children’ s '
+ 'ministry, and hot chocolate afterwards. Entry is free, but please register so we know how '
+ 'many candles and chairs to set out.\n\n'
+ 'A collection will be taken for the local children’ s home.' ,
startDate : start ,
endDate : addTime ( start , 2 * HOUR ) ,
price : 0 ,
picture : IMG ( 'photo-1543589077-47d81606c1bf' ) ,
location : 'Church lawn (hall if it rains)' ,
} ) ;
const entry = addOption ( event , { name : 'Free entry' , price : 0 , stockLimit : 500 } ) ;
addSection ( event , 'Lawn entrance' , [ entry ] ) ;
addCost ( event , 'Candles & songbooks' , 'once_off' , 1800 ) ;
addCost ( event , 'Sound & lighting' , 'once_off' , 3500 ) ;
for ( const user of shuffle ( people . everyone ) . slice ( 0 , 48 ) ) {
const quantity = Number ( weighted ( { 1 : 15 , 2 : 30 , 3 : 15 , 4 : 25 , 5 : 10 , 6 : 5 } ) ) ;
addRegistration ( event , user , [ line ( entry , quantity ) ] , { createdAt : pastDate ( 30 ) } ) ;
}
for ( const donor of shuffle ( people . everyone ) . slice ( 0 , 14 ) ) {
add ( 'payment' , {
id : uuidv4 ( ) , amount : pick ( [ 100 , 150 , 200 , 250 , 500 , 500 , 1000 ] ) , method : 'card' , userId : donor . id ,
eventId : event . id , isDonation : true , status : 'completed' ,
externalId : ` demo_ ${ uuidv4 ( ) . replace ( /-/g , '' ) } ` , createdAt : pastDate ( 30 ) ,
} ) ;
}
return event ;
}
function buildMarriageWeekend ( people ) {
const start = nextWeekday ( 98 , 5 , 17 ) ;
const event = addEvent ( {
title : 'Marriage Enrichment Weekend' ,
description : 'Take a weekend away just for the two of you.\n\n'
+ 'Three relaxed sessions on communication, conflict and dreaming together, led by Pastor Johan '
+ '& Elmarie Venter — with plenty of free time to walk, rest and enjoy the lodge.\n\n'
+ 'Your package covers two nights’ accommodation, all meals and the course material. A R1 000 '
+ 'deposit secures your room; rooms are limited.' ,
startDate : start ,
endDate : addTime ( start , 43 * HOUR ) ,
registrationDeadline : addTime ( start , - 21 * DAY ) ,
price : 3400 ,
picture : IMG ( 'photo-1606800052052-a08af7148866' ) ,
location : 'Kloofzicht Lodge, Muldersdrift' ,
contactName : 'Lerato Khumalo' ,
} ) ;
const couple = addOption ( event , { name : 'Couple package (2 nights)' , price : 3400 } ) ;
const [ garden , mountain ] = addVariants ( couple , [
{ name : 'Garden room' , price : 3400 , stockLimit : 8 } ,
{ name : 'Mountain-view room' , price : 3900 , stockLimit : 6 } ,
] ) ;
addTier ( couple , { variantId : garden . id , deadline : addTime ( NOW , 45 * DAY ) , price : 2990 } ) ;
addTier ( couple , { variantId : mountain . id , deadline : addTime ( NOW , 45 * DAY ) , price : 3450 } ) ;
const spa = addOption ( event , { name : 'Saturday spa treatment (per person)' , price : 650 , isMainTicket : false } ) ;
addCost ( event , 'Lodge accommodation & meals' , 'per_item' , 1950 , couple ) ;
addCost ( event , 'Spa treatments' , 'per_item' , 420 , spa ) ;
addCost ( event , 'Course workbooks' , 'once_off' , 1100 ) ;
const variants = shuffle ( [ ... Array ( 6 ) . fill ( garden ) , ... Array ( 5 ) . fill ( mountain ) ] ) ;
for ( const user of shuffle ( people . everyone ) . slice ( 0 , variants . length ) ) {
const lines = [ line ( couple , 1 , { variant : variants . pop ( ) } ) ] ;
if ( chance ( 0.4 ) ) lines . push ( line ( spa , 2 ) ) ;
const plan = weighted ( { eft : 35 , card : 15 , deposit : 35 , none : 15 } ) ;
addRegistration ( event , user , lines , {
createdAt : pastDate ( 30 ) ,
pay : plan === 'eft' || plan === 'card' ? 'full' : plan ,
method : plan === 'card' ? 'card' : 'eft' ,
deposit : 1000 ,
} ) ;
}
return event ;
}
// ─── Funnel analytics ────────────────────────────────────────────────────────
const SOURCES = { direct : 30 , whatsapp : 35 , facebook : 18 , instagram : 10 , email : 7 } ;
const DEVICES = { mobile : 72 , desktop : 22 , tablet : 6 } ;
function buildAnalytics ( ) {
const session = ( eventId , stages , at ) => {
const sessionId = uuidv4 ( ) ;
const source = weighted ( SOURCES ) ;
const device = weighted ( DEVICES ) ;
let t = at . getTime ( ) - stages . length * int ( 2 , 15 ) * MINUTE ;
for ( const stage of stages ) {
add ( 'analyticsEvent' , { id : uuidv4 ( ) , sessionId , eventId , stage , source , device , createdAt : new Date ( Math . min ( t , at . getTime ( ) ) ) } ) ;
t += int ( 1 , 12 ) * MINUTE ;
}
return sessionId ;
} ;
for ( const event of rows . event ) {
const registrations = ( rows . registration || [ ] ) . filter ( r => r . eventId === event . id ) ;
for ( const reg of registrations ) {
// Most registrations arrive from a tracked session (the rest declined cookies).
if ( chance ( 0.8 ) ) reg . analyticsSessionId = session ( event . id , [ 'view' , 'start' , 'submit' ] , reg . createdAt ) ;
}
const browsers = registrations . length * int ( 3 , 5 ) ;
for ( let i = 0 ; i < browsers ; i ++ ) {
session ( event . id , chance ( 0.3 ) ? [ 'view' , 'start' ] : [ 'view' ] , pastDate ( 30 ) ) ;
}
}
}
// ─── Clearing ────────────────────────────────────────────────────────────────
async function clearData ( tx ) {
const tables = await tx . $queryRaw ` SELECT tablename FROM pg_tables WHERE schemaname = current_schema() ` ;
const toClear = tables . map ( t => t . tablename ) . filter ( t => ! KEEP _TABLES . has ( t ) ) ;
if ( toClear . length ) {
await tx . $executeRawUnsafe ( ` TRUNCATE TABLE ${ toClear . map ( t => ` " ${ t } " ` ) . join ( ', ' ) } CASCADE ` ) ;
}
// Their password resets, 2FA codes and trusted devices go with them (onDelete: Cascade).
const { count } = await tx . user . deleteMany ( { where : { id : { not : admin . id } } } ) ;
await tx . securityEvent . deleteMany ( { where : { OR : [ { userId : null } , { userId : { not : admin . id } } ] } } ) ;
return { tables : toClear . length , users : count } ;
}
function clearScheduledEmails ( ) {
if ( ! fs . existsSync ( SCHEDULED _EMAILS _PATH ) ) return ;
fs . writeFileSync ( SCHEDULED _EMAILS _PATH , JSON . stringify ( { jobs : [ ] } , null , 2 ) , 'utf-8' ) ;
}
// ─── Main ────────────────────────────────────────────────────────────────────
function describeDatabase ( ) {
try {
const u = new URL ( process . env . DATABASE _URL ) ;
return ` ${ u . pathname . replace ( /^\// , '' ) } on ${ u . hostname } ${ u . port ? ` : ${ u . port } ` : '' } ` ;
} catch {
return 'the configured database' ;
}
}
async function confirm ( admin ) {
if ( SKIP _PROMPT ) return true ;
console . log ( ` \n This will DELETE all events, registrations, payments, tickets, coupons and users ` ) ;
console . log ( ` in ${ describeDatabase ( ) } , keeping only the site settings and ${ admin . email } . ` ) ;
const rl = readline . createInterface ( { input : process . stdin , output : process . stdout } ) ;
const answer = await new Promise ( resolve => rl . question ( 'Type "demo" to continue: ' , resolve ) ) ;
rl . close ( ) ;
return answer . trim ( ) . toLowerCase ( ) === 'demo' ;
}
async function main ( ) {
admin = await prisma . user . findFirst ( { where : { email : { equals : ADMIN _EMAIL , mode : 'insensitive' } } } ) ;
if ( ! admin ) {
console . error ( ` Admin account ${ ADMIN _EMAIL } was not found — nothing was changed. ` ) ;
console . error ( 'Set DEMO_ADMIN_EMAIL to the account you want to keep.' ) ;
process . exitCode = 1 ;
return ;
}
if ( admin . role !== 'admin' ) console . warn ( ` [warn] ${ admin . email } has role " ${ admin . role } ", not admin. ` ) ;
if ( ! ( await confirm ( admin ) ) ) {
console . log ( 'Cancelled — nothing was changed.' ) ;
return ;
}
passwordHash = await bcrypt . hash ( DEMO _PASSWORD , 10 ) ;
const people = buildPeople ( ) ;
const events = [
buildMensBreakfast ( people ) ,
buildFamilyFunDay ( people ) ,
buildWomensConference ( people ) ,
buildYouthCamp ( people ) ,
buildCarols ( people ) ,
buildMarriageWeekend ( people ) ,
] ;
buildAnalytics ( ) ;
const cleared = await prisma . $transaction ( async ( tx ) => {
const result = await clearData ( tx ) ;
for ( const model of INSERT _ORDER ) {
if ( rows [ model ] && rows [ model ] . length ) await tx [ model ] . createMany ( { data : rows [ model ] } ) ;
}
return result ;
} , { maxWait : 10 * 1000 , timeout : 5 * 60 * 1000 } ) ;
clearScheduledEmails ( ) ;
const count = ( model , where = ( ) => true ) => ( rows [ model ] || [ ] ) . filter ( where ) . length ;
console . log ( ` \n Cleared ${ cleared . tables } tables and ${ cleared . users } users. ` ) ;
console . log ( '\nDemo events:' ) ;
for ( const event of events ) {
const regs = rows . registration . filter ( r => r . eventId === event . id ) ;
const places = rows . ticket . filter ( t => t . eventId === event . id ) . reduce ( ( s , t ) => s + t . quantity , 0 ) ;
const byStatus = [ 'paid' , 'partial_paid' , 'pending' , 'cancelled' ]
. map ( s => [ s , regs . filter ( r => r . status === s ) . length ] ) . filter ( ( [ , n ] ) => n ) . map ( ( [ s , n ] ) => ` ${ n } ${ s } ` ) . join ( ', ' ) ;
console . log ( ` ${ event . startDate . toDateString ( ) . padEnd ( 16 ) } ${ event . title . padEnd ( 34 ) } ${ String ( regs . length ) . padStart ( 3 ) } registrations ( ${ byStatus } ), ${ places } ticketed places ` ) ;
}
console . log ( ` \n Created ${ count ( 'user' ) } users, ${ count ( 'payment' ) } payments, ${ count ( 'ticket' ) } tickets, ${ count ( 'attendee' ) } named campers, ${ count ( 'analyticsEvent' ) } funnel events. ` ) ;
console . log ( ` \n Kept: site settings and ${ admin . email } . ` ) ;
console . log ( ` Demo staff logins (password ${ DEMO _PASSWORD } ): ` ) ;
for ( const s of staffTeam ) console . log ( ` ${ s . role . padEnd ( 10 ) } ${ s . email } ` ) ;
console . log ( ` Every other demo person also uses ${ DEMO _PASSWORD } . ` ) ;
}
main ( )
. catch ( err => { console . error ( err ) ; process . exitCode = 1 ; } )
. finally ( ( ) => prisma . $disconnect ( ) ) ;