import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { Weekday } from "@/generated/prisma/enums"; import { addDays, todayIn, wallClockToInstant, weekdayOf } from "@/lib/time/business-time"; import type { CalendarDate } from "@/lib/time/business-time"; import { getAvailableDates, getAvailableTimeSlots, validateBookingSlot, } from "@/services/scheduling/availability.service"; import { cleanupTestBusiness, createTestAppointment, createTestBusiness, createTestCustomer, createTestStaff, prisma, } from "@/services/scheduling/__tests__/fixtures"; import type { TestBusiness } from "@/services/scheduling/__tests__/fixtures"; import { createBusinessHoliday, deleteBusinessHoliday, findHolidayConflicts, getClosedDates, listBusinessHolidays, } from "../holiday.service"; function nextWeekday(timezone: string, target: Weekday, minDaysAhead = 7): CalendarDate { let date = addDays(todayIn(timezone), minDaysAhead); for (let i = 0; i < 8; i++) { if (weekdayOf(date) === target) return date; date = addDays(date, 1); } throw new Error("unreachable"); } describe("business holidays (integration)", () => { let business: TestBusiness; let monday: CalendarDate; let tuesday: CalendarDate; let wednesday: CalendarDate; beforeEach(async () => { business = await createTestBusiness(); monday = nextWeekday(business.timezone, Weekday.MONDAY); tuesday = addDays(monday, 1); wednesday = addDays(monday, 2); }); afterEach(async () => { await cleanupTestBusiness(business); }); const addHoliday = (name: string, startDate: CalendarDate, endDate: CalendarDate) => createBusinessHoliday(prisma, { businessId: business.businessId, holiday: { name, startDate, endDate }, }); it("stores and lists holidays as inclusive calendar dates", async () => { await addHoliday("Refit", monday, tuesday); expect(await listBusinessHolidays(prisma, business.businessId)).toEqual([ expect.objectContaining({ name: "Refit", startDate: monday, endDate: tuesday }), ]); }); it("closes every date in a range, but not the days around it", async () => { await createTestStaff(business); await addHoliday("Refit", monday, tuesday); const { dates } = await getAvailableDates(prisma, { businessId: business.businessId, serviceIds: [business.haircutServiceId], fromDate: monday, toDate: wednesday, }); expect(dates).not.toContain(monday); expect(dates).not.toContain(tuesday); expect(dates).toContain(wednesday); }); it("offers no time slots on a holiday", async () => { await createTestStaff(business); await addHoliday("Bank holiday", monday, monday); const { slots, validation } = await getAvailableTimeSlots(prisma, { businessId: business.businessId, serviceIds: [business.haircutServiceId], date: monday, }); expect(validation.valid).toBe(true); expect(slots).toEqual([]); }); it("refuses to book a holiday directly, naming it", async () => { const { staffId } = await createTestStaff(business); await addHoliday("Bank holiday", monday, monday); const result = await validateBookingSlot(prisma, { businessId: business.businessId, serviceIds: [business.haircutServiceId], staffId, startAt: wallClockToInstant(monday, 10 * 60, business.timezone), }); expect(result.validation.valid).toBe(false); expect(result.validation.issues.map((i) => i.code)).toContain("BUSINESS_HOLIDAY"); expect(result.validation.issues.find((i) => i.code === "BUSINESS_HOLIDAY")?.message).toContain( "Bank holiday", ); }); it("reopens the dates once the holiday is removed", async () => { await createTestStaff(business); const created = await addHoliday("Bank holiday", monday, monday); if (!created.success) throw new Error("setup failed"); expect( await deleteBusinessHoliday(prisma, { businessId: business.businessId, holidayId: created.id, }), ).toBe(true); const { dates } = await getAvailableDates(prisma, { businessId: business.businessId, serviceIds: [business.haircutServiceId], fromDate: monday, toDate: monday, }); expect(dates).toContain(monday); }); it("never deletes another business's holiday", async () => { const created = await addHoliday("Bank holiday", monday, monday); if (!created.success) throw new Error("setup failed"); expect( await deleteBusinessHoliday(prisma, { businessId: "someone-else", holidayId: created.id }), ).toBe(false); expect(await listBusinessHolidays(prisma, business.businessId)).toHaveLength(1); }); it("only reports closed dates inside the window asked for", async () => { await addHoliday("Long break", monday, addDays(monday, 10)); const closed = await getClosedDates(prisma, { businessId: business.businessId, fromDate: tuesday, toDate: wednesday, }); expect([...closed.keys()]).toEqual([tuesday, wednesday]); expect(closed.get(tuesday)).toBe("Long break"); }); it("reports appointments already booked in the range, without cancelling them", async () => { const { staffId } = await createTestStaff(business); const { customerId } = await createTestCustomer(business); const inRange = await createTestAppointment(business, { customerId, staffId, startAt: wallClockToInstant(monday, 10 * 60, business.timezone), }); await createTestAppointment(business, { customerId, staffId, startAt: wallClockToInstant(addDays(monday, 5), 10 * 60, business.timezone), }); const conflicts = await findHolidayConflicts(prisma, { businessId: business.businessId, businessTimezone: business.timezone, startDate: monday, endDate: tuesday, }); expect(conflicts.map((c) => c.id)).toEqual([inRange.appointmentId]); const stored = await prisma.appointment.findUniqueOrThrow({ where: { id: inRange.appointmentId }, }); expect(stored.status).not.toBe("CANCELLED"); }); });