Promote: dev → uat (GRO-1945 visit-count hotfix + GRO-1921 UAT reset CronJob fix)
Carries: -a14bb5e17d— GRO-1945 visit-count query hotfix -981a257d2d— Merge of GRO-1945 hotfix into dev -0ab16b82e0— GRO-1921 UAT reset CronJob full-seed fix (PR #106) QA approved (PR #108, Lint Roller). CI green on head SHA0ab16b82e0.
This commit was merged in pull request #108.
This commit is contained in:
+92
-93
@@ -385,78 +385,19 @@ 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 },
|
||||
];
|
||||
|
||||
// ── Known-users-only seed (prod/demo) ───────────────────────────────────────
|
||||
// ── UAT staff account seeding (shared between seed paths) ─────────────────────
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Seeds or upserts the deterministic UAT staff accounts with numeric OIDC subs
|
||||
* from SEED_UAT_*_OIDC_SUB / SEED_UAT_GROOMER_OIDC_SUBS env vars.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
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})`);
|
||||
}
|
||||
}
|
||||
|
||||
async function seedUatStaffAccounts(db: ReturnType<typeof drizzle>) {
|
||||
// ── Staff: UAT Super User (oidcSub from SEED_UAT_SUPER_OIDC_SUB env var) ──
|
||||
const uatSuperOidcSub = process.env.SEED_UAT_SUPER_OIDC_SUB;
|
||||
if (uatSuperOidcSub) {
|
||||
@@ -680,6 +621,84 @@ async function seedKnownUsers() {
|
||||
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.
|
||||
@@ -847,30 +866,10 @@ async function seed() {
|
||||
console.log(`✓ Upserted admin staff '${adminName}' (${adminEmail})`);
|
||||
}
|
||||
|
||||
// ── 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})`);
|
||||
}
|
||||
// ── 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);
|
||||
|
||||
// ── Services ──
|
||||
// Upsert services using name as unique key. With deterministic IDs in
|
||||
|
||||
+6
-5
@@ -12,6 +12,7 @@ import {
|
||||
appointments,
|
||||
staff,
|
||||
services,
|
||||
sql,
|
||||
} from "@groombook/db";
|
||||
import type { AppEnv } from "../middleware/rbac.js";
|
||||
import {
|
||||
@@ -153,11 +154,11 @@ petsRouter.get("/:id/profile-summary", async (c) => {
|
||||
.limit(10);
|
||||
|
||||
// Visit count (completed appointments)
|
||||
const [{ count }] = await db
|
||||
.select({ count: appointments.id })
|
||||
const [countRow] = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(appointments)
|
||||
.where(and(eq(appointments.petId, petId), eq(appointments.status, "completed")))
|
||||
.limit(1);
|
||||
.where(and(eq(appointments.petId, petId), eq(appointments.status, "completed")));
|
||||
const visitCount = countRow?.count ?? 0;
|
||||
|
||||
// Upcoming appointment (next scheduled or confirmed)
|
||||
const [upcoming] = await db
|
||||
@@ -195,7 +196,7 @@ petsRouter.get("/:id/profile-summary", async (c) => {
|
||||
serviceName: h.serviceName,
|
||||
staffName: h.staffName,
|
||||
})),
|
||||
visitCount: Number(count ?? 0),
|
||||
visitCount,
|
||||
upcomingAppointment: upcoming
|
||||
? {
|
||||
id: upcoming.id,
|
||||
|
||||
Reference in New Issue
Block a user