feat(seed): seed upcoming appointments across statuses for UAT portal customer (GRO-2311)
Add seedUatCustomerPortalAppointments: deterministic, idempotent appointments for uat-customer@groombook.dev covering the reachable StatusBadge palette so the portal can live-render badges (not just code-verify the bundle): - upcoming confirmed (Confirmed badge, Upcoming card) - upcoming scheduled (Scheduled badge, Upcoming card) - past cancelled (Cancelled badge, Past tab) - past no_show (raw no_show label until GRO-2319 badge-key fix) Existing GRO-2100 completed appointment is untouched (AC #4). Stable UUIDs + onConflictDoNothing keep the hourly reset/seed re-runnable (GRO-2033 dup-key class). pending/waitlisted are not valid appointment_status values and are not seedable; web-side work tracked in GRO-2319. CTO-approved Option A on GRO-2313. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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 <StatusBadge> 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<typeof drizzle>,
|
||||||
|
customerClientId: string | null,
|
||||||
|
): Promise<void> {
|
||||||
|
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 <groomer>" 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 ────────────────────────
|
// ── 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
|
// to attach to the appointment; on a fresh reset there are none yet at
|
||||||
// the time seedUatStaffAccounts() returns).
|
// the time seedUatStaffAccounts() returns).
|
||||||
await seedUatGroomerLinkage(db, uatCustomerClientId);
|
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 ──
|
// ── Client: Demo Client ──
|
||||||
const [existingClient] = await db
|
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
|
// to attach to the appointment; on a fresh reset there are none yet at
|
||||||
// the time seedUatStaffAccounts() returns).
|
// the time seedUatStaffAccounts() returns).
|
||||||
await seedUatGroomerLinkage(db, uatCustomerClientId);
|
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
|
// GRO-2225: deterministic pre-geocoded route cohort + fixed-date appointments
|
||||||
// for the UAT groomer. Must run AFTER services are seeded (it looks up a
|
// for the UAT groomer. Must run AFTER services are seeded (it looks up a
|
||||||
|
|||||||
Reference in New Issue
Block a user