import { beforeEach, describe, expect, it, vi } from "vitest"; import { AvailabilityValidationError, listProviderAvailability, replaceProviderAvailability } from "./AvailabilityService.server"; const dbMock = vi.hoisted(() => ({ availabilityRule: { findMany: vi.fn(), deleteMany: vi.fn(), createMany: vi.fn(), }, availabilityException: { findMany: vi.fn(), deleteMany: vi.fn(), createMany: vi.fn(), }, $transaction: vi.fn(), })); vi.mock("~/utils/db.server", () => ({ db: dbMock })); describe("AvailabilityService.server", () => { beforeEach(() => { vi.clearAllMocks(); dbMock.$transaction.mockImplementation(async (fn: (tx: typeof dbMock) => Promise) => fn(dbMock)); }); it("loads and sorts provider availability", async () => { dbMock.availabilityRule.findMany.mockResolvedValue([ { id: "rule_2", dayOfWeek: "wednesday", startTime: "14:00", endTime: "17:00", timezone: "Europe/Berlin" }, { id: "rule_1", dayOfWeek: "monday", startTime: "09:00", endTime: "12:00", timezone: "Europe/Berlin" }, ]); dbMock.availabilityException.findMany.mockResolvedValue([ { id: "ex_2", date: new Date("2026-07-11T00:00:00Z"), kind: "custom_window", startTime: "10:00", endTime: "13:00", reason: "Workshop", timezone: "Europe/Berlin" }, { id: "ex_1", date: new Date("2026-07-10T00:00:00Z"), kind: "unavailable", startTime: null, endTime: null, reason: "Urlaub", timezone: "Europe/Berlin" }, ]); await expect(listProviderAvailability("tenant_1", "user_1")).resolves.toEqual({ providerUserId: "user_1", rules: [ { id: "rule_1", dayOfWeek: "monday", startTime: "09:00", endTime: "12:00", timezone: "Europe/Berlin" }, { id: "rule_2", dayOfWeek: "wednesday", startTime: "14:00", endTime: "17:00", timezone: "Europe/Berlin" }, ], exceptions: [ { id: "ex_1", date: "2026-07-10", kind: "unavailable", startTime: null, endTime: null, reason: "Urlaub", timezone: "Europe/Berlin" }, { id: "ex_2", date: "2026-07-11", kind: "custom_window", startTime: "10:00", endTime: "13:00", reason: "Workshop", timezone: "Europe/Berlin" }, ], }); }); it("replaces rules and exceptions atomically with normalized values", async () => { await replaceProviderAvailability({ tenantId: "tenant_1", providerUserId: "user_1", rules: [{ dayOfWeek: "monday", startTime: "09:00", endTime: "12:00", timezone: " Europe/Berlin " }], exceptions: [ { date: "2026-07-10", kind: "unavailable", startTime: "09:00", endTime: "10:00", reason: " Urlaub ", timezone: "Europe/Berlin" }, { date: "2026-07-11", kind: "custom_window", startTime: "10:00", endTime: "13:00", reason: null, timezone: "Europe/Berlin" }, ], }); expect(dbMock.$transaction).toHaveBeenCalledOnce(); expect(dbMock.availabilityRule.deleteMany).toHaveBeenCalledWith({ where: { tenantId: "tenant_1", providerUserId: "user_1" } }); expect(dbMock.availabilityException.deleteMany).toHaveBeenCalledWith({ where: { tenantId: "tenant_1", providerUserId: "user_1" } }); expect(dbMock.availabilityRule.createMany).toHaveBeenCalledWith({ data: [{ tenantId: "tenant_1", providerUserId: "user_1", dayOfWeek: "monday", startTime: "09:00", endTime: "12:00", timezone: "Europe/Berlin" }], }); expect(dbMock.availabilityException.createMany).toHaveBeenCalledWith({ data: [ { tenantId: "tenant_1", providerUserId: "user_1", date: new Date("2026-07-10T00:00:00Z"), kind: "unavailable", startTime: null, endTime: null, reason: "Urlaub", timezone: "Europe/Berlin" }, { tenantId: "tenant_1", providerUserId: "user_1", date: new Date("2026-07-11T00:00:00Z"), kind: "custom_window", startTime: "10:00", endTime: "13:00", reason: null, timezone: "Europe/Berlin" }, ], }); }); it("rejects invalid time windows before writing", async () => { await expect( replaceProviderAvailability({ tenantId: "tenant_1", providerUserId: "user_1", rules: [{ dayOfWeek: "monday", startTime: "12:00", endTime: "09:00", timezone: "Europe/Berlin" }], exceptions: [], }), ).rejects.toBeInstanceOf(AvailabilityValidationError); expect(dbMock.$transaction).not.toHaveBeenCalled(); }); it("requires custom-window exception times before writing", async () => { await expect( replaceProviderAvailability({ tenantId: "tenant_1", providerUserId: "user_1", rules: [], exceptions: [{ date: "2026-07-10", kind: "custom_window", timezone: "Europe/Berlin" }], }), ).rejects.toMatchObject({ code: "bookings.error.invalidTimeWindow" }); expect(dbMock.$transaction).not.toHaveBeenCalled(); }); });