From d61607f4c5f24e3b5524196d87bc60f4e985634b Mon Sep 17 00:00:00 2001 From: Flea Flicker <22+gb_flea@noreply.git.farh.net> Date: Tue, 9 Jun 2026 09:53:04 +0000 Subject: [PATCH] feat(seed): seed upcoming appointments across statuses for UAT portal customer (GRO-2311) (#201) --- packages/db/src/seed.ts | 170 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index b519c04..f6bdf4c 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -832,6 +832,168 @@ async function seedUatGroomerLinkage( ); } +// ── GRO-2311 / GRO-2313: portal customer StatusBadge coverage ──────────────── + +/** + * GRO-2311 / GRO-2313: give the UAT portal customer (`uat-customer@groombook.dev`) + * a deterministic spread of appointments so the customer-portal StatusBadge + * palette can be LIVE-observed (not just code-verified against the bundle). + * + * Scope is the subset of badge states reachable from the `appointment_status` + * enum (`scheduled, confirmed, in_progress, completed, cancelled, no_show`) — + * the portal's renders `appointment.status` verbatim. `pending` + * and `waitlisted` are NOT valid appointment statuses and cannot be seeded; the + * styled `no_show`→`no-show` badge fix and any pending/waitlisted derivation are + * tracked separately in GRO-2319 (web). CTO-approved Option A on GRO-2313. + * + * - confirmed → future startTime → renders as an Upcoming card (Confirmed badge) + * - scheduled → future startTime → renders as an Upcoming card (Scheduled badge) + * - cancelled → past startTime → Past tab (isUpcoming excludes cancelled) + * - no_show → past startTime → Past tab (raw `no_show` label until GRO-2319) + * + * The existing GRO-2100 `completed` appointment (a0000001-…-0001) is left + * untouched (AC #4), so Completed is also covered. + * + * Idempotent: each appointment uses a fixed UUID and is upserted with + * onConflictDoNothing, so the hourly reset-demo-data CronJob (which TRUNCATEs + * then re-seeds) and non-truncating dev re-seeds never dup-key + * (see GRO-2033 for the dup-key class). + */ +async function seedUatCustomerPortalAppointments( + db: ReturnType, + customerClientId: string | null, +): Promise { + const LINKED_PET_ID = "c0000001-0000-0000-0000-000000000002"; // UAT Pup Alpha + + // Skip silently outside the UAT persona profile (e.g. a dev/test seed that + // never created the UAT Customer client). + if (!customerClientId) { + return; + } + + // The customer's pet must exist (pets are NOT truncated on reset, so this is + // stable). Defensive: bail cleanly if the persona pet is absent. + const [linkedPet] = await db + .select({ id: schema.pets.id }) + .from(schema.pets) + .where(eq(schema.pets.id, LINKED_PET_ID)) + .limit(1); + if (!linkedPet) { + console.warn(`⚠ GRO-2311: UAT Pup Alpha (${LINKED_PET_ID}) not found — skipping portal appointment seed`); + return; + } + + // Stable "Bath & Brush" service; fall back to any active service. + const BATH_AND_BRUSH_ID = "b0000001-0000-0000-0000-000000000001"; + const [bathService] = await db + .select({ id: schema.services.id }) + .from(schema.services) + .where(eq(schema.services.id, BATH_AND_BRUSH_ID)) + .limit(1); + + let serviceId: string; + if (bathService) { + serviceId = bathService.id; + } else { + const [fallback] = await db + .select({ id: schema.services.id }) + .from(schema.services) + .where(eq(schema.services.active, true)) + .limit(1); + if (!fallback) { + console.warn(`⚠ GRO-2311: no active services found — skipping portal appointment seed`); + return; + } + serviceId = fallback.id; + } + + // Attach the UAT groomer when present (nicer "with " card); else null + // ("First Available"). Either way these are the customer's own appointments — + // no new groomer↔pet linkage invariant is created (uses the already-linked + // Pup Alpha), so GRO-1987 TC-UAT-3 (403 on the UNLINKED Pup Beta) is unaffected. + const [uatGroomerStaff] = await db + .select({ id: schema.staff.id }) + .from(schema.staff) + .where(eq(schema.staff.email, "uat-groomer@groombook.dev")) + .limit(1); + const staffId = uatGroomerStaff?.id ?? null; + + // Anchor all times to local wall-clock so future/past holds regardless of the + // hourly reset cadence. + const at = (deltaDays: number, hour: number): Date => { + const d = new Date(); + d.setDate(d.getDate() + deltaDays); + d.setHours(hour, 0, 0, 0); + return d; + }; + const DURATION_MS = 45 * 60 * 1000; + + const rows = [ + { + id: "a0000001-0000-0000-0000-000000000002", + status: "confirmed" as const, + start: at(3, 10), + confirmationStatus: "confirmed", + confirmedAt: new Date(), + cancelledAt: null as Date | null, + notes: "GRO-2311: upcoming confirmed appointment for portal StatusBadge coverage.", + }, + { + id: "a0000001-0000-0000-0000-000000000003", + status: "scheduled" as const, + start: at(5, 14), + confirmationStatus: "pending", + confirmedAt: null as Date | null, + cancelledAt: null as Date | null, + notes: "GRO-2311: upcoming scheduled appointment for portal StatusBadge coverage.", + }, + { + id: "a0000001-0000-0000-0000-000000000004", + status: "cancelled" as const, + start: at(-3, 11), + confirmationStatus: "cancelled", + confirmedAt: null as Date | null, + cancelledAt: new Date(), + notes: "GRO-2311: cancelled appointment (Past tab) for portal StatusBadge coverage.", + }, + { + id: "a0000001-0000-0000-0000-000000000005", + status: "no_show" as const, + start: at(-10, 9), + confirmationStatus: "confirmed", + confirmedAt: null as Date | null, + cancelledAt: null as Date | null, + notes: "GRO-2311: no_show appointment (Past tab) for portal StatusBadge coverage.", + }, + ]; + + await db + .insert(schema.appointments) + .values( + rows.map((r) => ({ + id: r.id, + clientId: customerClientId, + petId: LINKED_PET_ID, + serviceId, + staffId, + batherStaffId: null, + status: r.status, + startTime: r.start, + endTime: new Date(r.start.getTime() + DURATION_MS), + notes: r.notes, + priceCents: null, + confirmationStatus: r.confirmationStatus, + confirmedAt: r.confirmedAt, + cancelledAt: r.cancelledAt, + })), + ) + .onConflictDoNothing({ target: schema.appointments.id }); + + console.log( + `✓ GRO-2311: seeded ${rows.length} portal StatusBadge appointments (confirmed/scheduled/cancelled/no_show) for UAT customer`, + ); +} + // ── GRO-2225: deterministic route-optimization cohort ──────────────────────── /** @@ -1113,6 +1275,10 @@ async function seedKnownUsers() { // to attach to the appointment; on a fresh reset there are none yet at // the time seedUatStaffAccounts() returns). await seedUatGroomerLinkage(db, uatCustomerClientId); + // GRO-2311 / GRO-2313: portal customer StatusBadge palette coverage (reachable + // appointment statuses only). Runs after the groomer linkage so the customer + // client + Pup Alpha already exist. + await seedUatCustomerPortalAppointments(db, uatCustomerClientId); // ── Client: Demo Client ── const [existingClient] = await db @@ -1375,6 +1541,10 @@ export async function runSeedBody( // to attach to the appointment; on a fresh reset there are none yet at // the time seedUatStaffAccounts() returns). await seedUatGroomerLinkage(db, uatCustomerClientId); + // GRO-2311 / GRO-2313: portal customer StatusBadge palette coverage (reachable + // appointment statuses only). Runs after the groomer linkage so the customer + // client + Pup Alpha already exist. + await seedUatCustomerPortalAppointments(db, uatCustomerClientId); // GRO-2225: deterministic pre-geocoded route cohort + fixed-date appointments // for the UAT groomer. Must run AFTER services are seeded (it looks up a