import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest"; import { PrismaPg } from "@prisma/adapter-pg"; import { PrismaClient } from "@/generated/prisma/client"; import { loadAvailabilityState } from "@/lib/availability-loader"; import { findSlots } from "@/lib/availability"; import { seed, type SeedResult } from "@/lib/seed"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const db = new PrismaClient({ adapter }); let fx: SeedResult; let serviceId: string; let prenatalServiceId: string; let therapistMei: string; let therapistDaniel: string; let roomA: string; beforeAll(async () => { fx = await seed(db); serviceId = fx.services.find((s) => s.name === "60-minute Swedish")!.id; prenatalServiceId = fx.services.find((s) => s.name === "75-minute Prenatal")!.id; therapistMei = fx.therapists.find((t) => t.name === "Mei Tanaka")!.id; therapistDaniel = fx.therapists.find((t) => t.name === "Daniel Costa")!.id; roomA = fx.rooms[0].id; }); afterAll(async () => { await db.$disconnect(); }); beforeEach(async () => { await db.booking.deleteMany(); await db.availabilityOverride.deleteMany(); await db.roomBlock.deleteMany(); }); const D = (iso: string) => new Date(iso); const FROM = D("2026-05-05T00:00:00Z"); const TO = D("2026-05-12T00:00:00Z"); describe("loadAvailabilityState", () => { test("returns null for unknown service", async () => { const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId: "no-such-service", }); expect(result).toBeNull(); }); test("returns null for inactive service", async () => { await db.service.update({ where: { id: serviceId }, data: { active: false }, }); try { const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); expect(result).toBeNull(); } finally { await db.service.update({ where: { id: serviceId }, data: { active: true }, }); } }); test("Swedish service: all 10 seeded therapists are eligible", async () => { const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); expect(result).not.toBeNull(); expect(result!.therapists).toHaveLength(10); }); test("Prenatal service: only the 3 prenatal-cert therapists are loaded", async () => { const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId: prenatalServiceId, }); expect(result).not.toBeNull(); // ServiceTherapist allowlist filters to qualified therapists only. expect(result!.therapists).toHaveLength(3); for (const t of result!.therapists) { expect(t.tags.has("prenatal-cert")).toBe(true); } }); test("returns service in ServiceLite shape", async () => { const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); expect(result!.service).toEqual({ id: serviceId, durationMin: 60, bufferAfterMin: 15, requiredTherapistTags: ["swedish"], requiredRoomTags: [], }); }); test("loads all 10 active rooms with tag sets", async () => { const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); expect(result!.rooms).toHaveLength(10); const prenatalRooms = result!.rooms.filter((r) => r.tags.has("prenatal-table"), ); expect(prenatalRooms.length).toBeGreaterThanOrEqual(1); }); test("inactive room is excluded", async () => { await db.room.update({ where: { id: roomA }, data: { active: false }, }); try { const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); expect(result!.rooms.find((r) => r.id === roomA)).toBeUndefined(); } finally { await db.room.update({ where: { id: roomA }, data: { active: true }, }); } }); test("only loads bookings within the window for therapists", async () => { // One booking inside the window, one outside (next month). await db.booking.createMany({ data: [ { customerId: fx.customers[0].id, therapistId: therapistMei, roomId: roomA, serviceId, startsAt: D("2026-05-06T14:00:00Z"), endsAt: D("2026-05-06T15:00:00Z"), roomReleasedAt: D("2026-05-06T15:15:00Z"), status: "CONFIRMED", }, { customerId: fx.customers[0].id, therapistId: therapistMei, roomId: roomA, serviceId, startsAt: D("2026-06-15T14:00:00Z"), endsAt: D("2026-06-15T15:00:00Z"), roomReleasedAt: D("2026-06-15T15:15:00Z"), status: "CONFIRMED", }, ], }); const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); const mei = result!.therapists.find((t) => t.id === therapistMei)!; expect(mei.bookings).toHaveLength(1); expect(mei.bookings[0].startsAt).toEqual(D("2026-05-06T14:00:00Z")); }); test("CANCELLED and COMPLETED bookings are NOT loaded (only active)", async () => { await db.booking.create({ data: { customerId: fx.customers[0].id, therapistId: therapistMei, roomId: roomA, serviceId, startsAt: D("2026-05-06T14:00:00Z"), endsAt: D("2026-05-06T15:00:00Z"), roomReleasedAt: D("2026-05-06T15:15:00Z"), status: "CANCELLED", }, }); const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); const mei = result!.therapists.find((t) => t.id === therapistMei)!; expect(mei.bookings).toHaveLength(0); }); test("BLOCK overrides are loaded for the window", async () => { await db.availabilityOverride.create({ data: { therapistId: therapistMei, startsAt: D("2026-05-06T14:00:00Z"), endsAt: D("2026-05-06T18:00:00Z"), kind: "BLOCK", reason: "doctor visit", }, }); const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); const mei = result!.therapists.find((t) => t.id === therapistMei)!; expect(mei.overrides).toHaveLength(1); expect(mei.overrides[0].kind).toBe("BLOCK"); }); test("RoomBlocks within the window are loaded", async () => { await db.roomBlock.create({ data: { roomId: roomA, startsAt: D("2026-05-07T14:00:00Z"), endsAt: D("2026-05-07T15:00:00Z"), reason: "deep clean", }, }); const result = await loadAvailabilityState(db, { from: FROM, to: TO, serviceId, }); const room = result!.rooms.find((r) => r.id === roomA)!; expect(room.blocks).toHaveLength(1); }); test("loader output integrates with findSlots end-to-end", async () => { // Tuesday May 5 in America/Detroit: working hours Tue-Sat 10:00–19:00 local. // Detroit is UTC-4 (EDT in May), so 10:00 local = 14:00 UTC. const result = await loadAvailabilityState(db, { from: D("2026-05-05T14:00:00Z"), // 10:00 local to: D("2026-05-05T16:00:00Z"), // 12:00 local serviceId, }); expect(result).not.toBeNull(); const slots = findSlots({ service: result!.service, therapists: result!.therapists, rooms: result!.rooms, practiceTz: "America/Detroit", from: D("2026-05-05T14:00:00Z"), to: D("2026-05-05T16:00:00Z"), }); // 60-min service in a 2-hour window: 5 slots (10:00, 10:15, 10:30, 10:45, 11:00). expect(slots).toHaveLength(5); // Every slot should have all 10 therapists and all 10 rooms as candidates // (no bookings exist in this window after beforeEach). for (const slot of slots) { expect(slot.candidateTherapistIds).toHaveLength(10); expect(slot.candidateRoomIds).toHaveLength(10); } }); test("a CONFIRMED booking removes that therapist from candidate list end-to-end", async () => { await db.booking.create({ data: { customerId: fx.customers[0].id, therapistId: therapistDaniel, roomId: roomA, serviceId, startsAt: D("2026-05-05T14:00:00Z"), // 10:00 local endsAt: D("2026-05-05T15:00:00Z"), // 11:00 local roomReleasedAt: D("2026-05-05T15:15:00Z"), status: "CONFIRMED", }, }); const result = await loadAvailabilityState(db, { from: D("2026-05-05T14:00:00Z"), to: D("2026-05-05T16:00:00Z"), serviceId, }); const slots = findSlots({ service: result!.service, therapists: result!.therapists, rooms: result!.rooms, practiceTz: "America/Detroit", from: D("2026-05-05T14:00:00Z"), to: D("2026-05-05T16:00:00Z"), }); // 10:00 slot should not have Daniel as a candidate. const tenAm = slots.find( (s) => s.startsAt.toISOString() === "2026-05-05T14:00:00.000Z", )!; expect(tenAm.candidateTherapistIds).not.toContain(therapistDaniel); expect(tenAm.candidateTherapistIds).toHaveLength(9); }); });