@@ -270,10 +270,6 @@ const medicalAlertPool: MedicalAlert[] = [
{ id : "" , type : "other" , description : "Seizure history — avoid flashing lights" , severity : "high" } ,
{ id : "" , type : "other" , description : "Luxating patella — short walks only" , severity : "medium" } ,
{ id : "" , type : "other" , description : "Ear infections — dry thoroughly after bath" , severity : "low" } ,
{ id : "" , type : "behavioral" , description : "Anxiety — calm environment preferred" , severity : "low" } ,
{ id : "" , type : "behavioral" , description : "Fear-based aggression — approach with caution" , severity : "high" } ,
{ id : "" , type : "skin" , description : "Contact dermatitis — avoid harsh chemicals" , severity : "medium" } ,
{ id : "" , type : "skin" , description : "Hot spots — monitor and report any worsening" , severity : "high" } ,
] ;
const preferredCutPool : string [ ] = [
@@ -389,19 +385,78 @@ const servicesDef = [
{ id : "b0000001-0000-0000-0000-00000000000a" , name : "Sanitary Trim" , desc : "Hygienic trim of paw pads, face, and sanitary areas" , price : 2500 , dur : 20 } ,
] ;
// ── UAT staff account seeding (shared between seed paths) ─────────────────────
// ── Known-users-only seed (prod/demo) ────────────────── ─────────────────────
/**
* Seeds or upserts the deterministic UAT staff accounts with numeric OIDC subs
* from SEED_UAT_*_OIDC_SUB / SEED_UAT_GROOMER_OIDC_SUBS env var s.
*
* In the full seed path this must run AFTER random staff are created so the
* deterministic upserts land on the correct rows (groomers referenced by the
* UAT test-client appointment logic use groomers[0] etc.).
*
* In seedKnownUsers() this replaces the inline UAT-staff block.
* Seeds only the minimal known users for prod/demo environments.
* Creates: Demo Manager staff + Demo Client + Demo Dog + basic service s.
* Idempotent: skips creation if records already exist.
*/
async function seedUatStaffAccounts ( db : ReturnType < typeof drizzle > ) {
async function seedKnownUsers() {
const url = process . env . DATABASE_URL ;
if ( ! url ) {
console . error ( "DATABASE_URL is not set" ) ;
process . exit ( 1 ) ;
}
const client = postgres ( url , { max : 5 } ) ;
const db = drizzle ( client , { schema } ) ;
console . log ( "Seeding known users (prod/demo mode)...\n" ) ;
const KNOWN_STAFF_ID = "00000000-0000-0000-0000-000000000001" ;
const DEMO_CLIENT_ID = "00000000-0000-0000-0000-000000000002" ;
const DEMO_PET_ID = "00000000-0000-0000-0000-000000000003" ;
// ── Staff: Demo Manager ──
const [ existingStaff ] = await db
. select ( )
. from ( schema . staff )
. where ( eq ( schema . staff . email , "demo-manager@groombook.dev" ) )
. limit ( 1 ) ;
if ( existingStaff ) {
console . log ( ` ✓ Staff ' ${ existingStaff . name } ' already exists — skipping ` ) ;
} else {
await db . insert ( schema . staff ) . values ( {
id : KNOWN_STAFF_ID ,
name : "Demo Manager" ,
email : "demo-manager@groombook.dev" ,
oidcSub : "demo-manager-001" ,
role : "manager" ,
isSuperUser : true ,
active : true ,
} ) ;
console . log ( "✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)" ) ;
}
// ── Staff: SEED_ADMIN_EMAIL admin ──
const adminEmail = process . env . SEED_ADMIN_EMAIL ;
if ( adminEmail ) {
const adminName = process . env . SEED_ADMIN_NAME ? ? "Admin" ;
const ADMIN_STAFF_ID = "00000000-0000-0000-0000-000000000002" ;
const [ existingAdmin ] = await db
. select ( )
. from ( schema . staff )
. where ( eq ( schema . staff . email , adminEmail ) )
. limit ( 1 ) ;
if ( existingAdmin ) {
console . log ( ` ✓ Staff admin ' ${ existingAdmin . name } ' already exists — skipping ` ) ;
} else {
await db . insert ( schema . staff ) . values ( {
id : ADMIN_STAFF_ID ,
name : adminName ,
email : adminEmail ,
oidcSub : adminEmail ,
role : "manager" ,
isSuperUser : true ,
active : true ,
} ) ;
console . log ( ` ✓ Created staff admin ' ${ adminName } ' ( ${ adminEmail } ) ` ) ;
}
}
// ── Staff: UAT Super User (oidcSub from SEED_UAT_SUPER_OIDC_SUB env var) ──
const uatSuperOidcSub = process . env . SEED_UAT_SUPER_OIDC_SUB ;
if ( uatSuperOidcSub ) {
@@ -609,45 +664,8 @@ async function seedUatStaffAccounts(db: ReturnType<typeof drizzle>) {
. from ( schema . pets )
. where ( eq ( schema . pets . id , pet . id ) )
. limit ( 1 ) ;
if ( existing ) {
// Upsert so extended fields are always populated on re-runs
await db . insert ( schema . pets )
. values ( {
id : pet.id ,
clientId : uatCustomerClientId ,
name : pet.name ,
species : pet.species ,
breed : pet.breed ,
weightKg : pet.weight ,
dateOfBirth : new Date ( ` ${ pet . dob } T00:00:00Z ` ) ,
image : pet.image ,
temperamentScore : randInt ( 1 , 5 ) ,
temperamentFlags : pickN ( temperamentFlagPool , randInt ( 1 , 3 ) ) ,
medicalAlerts : [ ] ,
preferredCuts : pickN ( preferredCutPool , randInt ( 1 , 2 ) ) ,
coatType : pick ( coatTypePool ) ,
petSizeCategory : pick ( petSizeCategoryPool ) ,
} )
. onConflictDoUpdate ( {
target : schema.pets.id ,
set : {
clientId : uatCustomerClientId ,
name : pet.name ,
species : pet.species ,
breed : pet.breed ,
weightKg : pet.weight ,
dateOfBirth : new Date ( ` ${ pet . dob } T00:00:00Z ` ) ,
image : pet.image ,
temperamentScore : randInt ( 1 , 5 ) ,
temperamentFlags : pickN ( temperamentFlagPool , randInt ( 1 , 3 ) ) ,
medicalAlerts : [ ] ,
preferredCuts : pickN ( preferredCutPool , randInt ( 1 , 2 ) ) ,
coatType : pick ( coatTypePool ) ,
petSizeCategory : pick ( petSizeCategoryPool ) ,
} ,
} ) ;
console . log ( ` ✓ Upserted UAT pet ' ${ pet . name } ' with extended fields ` ) ;
console . log ( ` ✓ UAT Pet ' ${ existing . name } ' al ready exists — skipping ` ) ;
} else {
await db . insert ( schema . pets ) . values ( {
id : pet.id ,
@@ -658,94 +676,10 @@ async function seedUatStaffAccounts(db: ReturnType<typeof drizzle>) {
weightKg : pet.weight ,
dateOfBirth : new Date ( ` ${ pet . dob } T00:00:00Z ` ) ,
image : pet.image ,
temperamentScore : randInt ( 1 , 5 ) ,
temperamentFlags : pickN ( temperamentFlagPool , randInt ( 1 , 3 ) ) ,
medicalAlerts : [ ] ,
preferredCuts : pickN ( preferredCutPool , randInt ( 1 , 2 ) ) ,
coatType : pick ( coatTypePool ) ,
petSizeCategory : pick ( petSizeCategoryPool ) ,
} ) ;
console . log ( ` ✓ Created UAT pet ' ${ pet . name } ' with extended fields ` ) ;
console . log ( ` ✓ Created UAT pet ' ${ pet . name } ' ` ) ;
}
}
}
// ── Known-users-only seed (prod/demo) ───────────────────────────────────────
/**
* Seeds only the minimal known users for prod/demo environments.
* Creates: Demo Manager staff + Demo Client + Demo Dog + basic services.
* Idempotent: skips creation if records already exist.
*/
async function seedKnownUsers() {
const url = process . env . DATABASE_URL ;
if ( ! url ) {
console . error ( "DATABASE_URL is not set" ) ;
process . exit ( 1 ) ;
}
const client = postgres ( url , { max : 5 } ) ;
const db = drizzle ( client , { schema } ) ;
console . log ( "Seeding known users (prod/demo mode)...\n" ) ;
const KNOWN_STAFF_ID = "00000000-0000-0000-0000-000000000001" ;
const DEMO_CLIENT_ID = "00000000-0000-0000-0000-000000000002" ;
const DEMO_PET_ID = "00000000-0000-0000-0000-000000000003" ;
// ── Staff: Demo Manager ──
const [ existingStaff ] = await db
. select ( )
. from ( schema . staff )
. where ( eq ( schema . staff . email , "demo-manager@groombook.dev" ) )
. limit ( 1 ) ;
if ( existingStaff ) {
console . log ( ` ✓ Staff ' ${ existingStaff . name } ' already exists — skipping ` ) ;
} else {
await db . insert ( schema . staff ) . values ( {
id : KNOWN_STAFF_ID ,
name : "Demo Manager" ,
email : "demo-manager@groombook.dev" ,
oidcSub : "demo-manager-001" ,
role : "manager" ,
isSuperUser : true ,
active : true ,
} ) ;
console . log ( "✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)" ) ;
}
// ── Staff: SEED_ADMIN_EMAIL admin ──
const adminEmail = process . env . SEED_ADMIN_EMAIL ;
if ( adminEmail ) {
const adminName = process . env . SEED_ADMIN_NAME ? ? "Admin" ;
const ADMIN_STAFF_ID = "00000000-0000-0000-0000-000000000002" ;
const [ existingAdmin ] = await db
. select ( )
. from ( schema . staff )
. where ( eq ( schema . staff . email , adminEmail ) )
. limit ( 1 ) ;
if ( existingAdmin ) {
console . log ( ` ✓ Staff admin ' ${ existingAdmin . name } ' already exists — skipping ` ) ;
} else {
await db . insert ( schema . staff ) . values ( {
id : ADMIN_STAFF_ID ,
name : adminName ,
email : adminEmail ,
oidcSub : adminEmail ,
role : "manager" ,
isSuperUser : true ,
active : true ,
} ) ;
console . log ( ` ✓ Created staff admin ' ${ adminName } ' ( ${ adminEmail } ) ` ) ;
}
}
// ── UAT staff accounts + Better Auth credentials (shared impl) ──────────────
// Extracted into seedUatStaffAccounts() so it runs in both seedKnownUsers()
// and the full seed() UAT branch.
await seedUatStaffAccounts ( db ) ;
// ── Services: idempotent upsert using name as unique key ─────────────────────
// UNIQUE constraint on services.name (migration 0020) must exist first.
@@ -913,10 +847,30 @@ async function seed() {
console . log ( ` ✓ Upserted admin staff ' ${ adminName } ' ( ${ adminEmail } ) ` ) ;
}
// ── UAT staff accounts + Better Auth credentials (shared impl) ──────────── ──
// Seeds deterministic UAT staff with numeric OIDC subs and Better Auth credentials.
// Must run AFTER random staff are created so upserts land correctly.
await seedUatStaffAccounts ( db ) ;
// ── UAT Groomer Personas (SEED_UAT_GROOMER_EMAILS + SEED_UAT_GROOMER_NAMES) ──
const groomerEmails = process . env . SEED_UAT_GROOMER_EMAILS ? . split ( "," ) . map ( ( e ) = > e . trim ( ) ) . filter ( Boolean ) ? ? [ ] ;
const groomerNames = process . env . SEED_UAT_GROOMER_NAMES ? . split ( "," ) . map ( ( n ) = > n . trim ( ) ) . filter ( Boolean ) ? ? [ ] ;
const groomerCount = Math . min ( groomerEmails . length , groomerNames . length ) ;
for ( let i = 0 ; i < groomerCount ; i ++ ) {
const email = groomerEmails [ i ] ! ;
const name = groomerNames [ i ] ! ;
const staffId = ` 00000000-0000-0000-0000- ${ String ( 5 + i ) . padStart ( 12 , "0" ) } ` ;
await db . insert ( schema . staff )
. values ( {
id : staffId ,
name ,
email ,
oidcSub : email ,
role : "groomer" ,
isSuperUser : false ,
active : true ,
} )
. onConflictDoUpdate ( {
target : schema.staff.email ,
set : { id : staffId , name , role : "groomer" , isSuperUser : false , active : true } ,
} ) ;
console . log ( ` ✓ Upserted groomer ' ${ name } ' ( ${ email } ) ` ) ;
}
// ── Services ──
// Upsert services using name as unique key. With deterministic IDs in
@@ -1009,7 +963,6 @@ async function seed() {
temperamentScore : randInt ( 1 , 5 ) ,
temperamentFlags : pickN ( temperamentFlagPool , randInt ( 1 , 3 ) ) ,
medicalAlerts : ( ( ) = > {
// ~30% of random-pool pets have alerts — lands squarely in the 25– 35% AC band
if ( rand ( ) < 0.3 ) {
const count = rand ( ) < 0.7 ? 1 : 2 ;
return pickN ( medicalAlertPool , count ) . map ( ( a ) = > ( { . . . a , id : uuid ( ) } ) ) ;
@@ -1106,14 +1059,7 @@ async function seed() {
temperamentScore : randInt ( 1 , 5 ) ,
temperamentFlags : pickN ( temperamentFlagPool , randInt ( 1 , 3 ) ) ,
medicalAlerts : ( ( ) = > {
// ~30% of pets get alerts; TestCooper/TestRocky get deterministic types
if ( rand ( ) < 0.3 ) {
if ( uc . petName === "TestCooper" ) {
return pickN ( medicalAlertPool . filter ( ( a ) = > a . type === "behavioral" ) , 1 ) . map ( ( a ) = > ( { . . . a , id : uuid ( ) } ) ) ;
}
if ( uc . petName === "TestRocky" ) {
return pickN ( medicalAlertPool . filter ( ( a ) = > a . type === "skin" ) , 1 ) . map ( ( a ) = > ( { . . . a , id : uuid ( ) } ) ) ;
}
const count = rand ( ) < 0.7 ? 1 : 2 ;
return pickN ( medicalAlertPool , count ) . map ( ( a ) = > ( { . . . a , id : uuid ( ) } ) ) ;
}
@@ -1136,14 +1082,7 @@ async function seed() {
temperamentScore : randInt ( 1 , 5 ) ,
temperamentFlags : pickN ( temperamentFlagPool , randInt ( 1 , 3 ) ) ,
medicalAlerts : ( ( ) = > {
// ~30% of pets get alerts; TestCooper/TestRocky get deterministic types
if ( rand ( ) < 0.3 ) {
if ( uc . petName === "TestCooper" ) {
return pickN ( medicalAlertPool . filter ( ( a ) = > a . type === "behavioral" ) , 1 ) . map ( ( a ) = > ( { . . . a , id : uuid ( ) } ) ) ;
}
if ( uc . petName === "TestRocky" ) {
return pickN ( medicalAlertPool . filter ( ( a ) = > a . type === "skin" ) , 1 ) . map ( ( a ) = > ( { . . . a , id : uuid ( ) } ) ) ;
}
const count = rand ( ) < 0.7 ? 1 : 2 ;
return pickN ( medicalAlertPool , count ) . map ( ( a ) = > ( { . . . a , id : uuid ( ) } ) ) ;
}