diff --git a/packages/api/src/__tests__/timeline-holiday-procedure-support.test.ts b/packages/api/src/__tests__/timeline-holiday-procedure-support.test.ts new file mode 100644 index 0000000..a2bc6ca --- /dev/null +++ b/packages/api/src/__tests__/timeline-holiday-procedure-support.test.ts @@ -0,0 +1,117 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../router/timeline-read-shared.js", () => ({ + buildSelfServiceTimelineInput: vi.fn(), + buildTimelineEntriesDetailInput: vi.fn(), +})); + +vi.mock("../router/timeline-holiday-load-support.js", () => ({ + loadTimelineHolidayOverlays: vi.fn(), +})); + +vi.mock("../router/timeline-holiday-support.js", () => ({ + buildTimelineHolidayOverlayDetailResponse: vi.fn(), + formatTimelineHolidayOverlays: vi.fn(), +})); + +import { + buildSelfServiceTimelineInput, + buildTimelineEntriesDetailInput, +} from "../router/timeline-read-shared.js"; +import { loadTimelineHolidayOverlays } from "../router/timeline-holiday-load-support.js"; +import { + buildTimelineHolidayOverlayDetailResponse, + formatTimelineHolidayOverlays, +} from "../router/timeline-holiday-support.js"; +import { + readMyTimelineHolidayOverlays, + readTimelineHolidayOverlayDetail, +} from "../router/timeline-holiday-procedure-support.js"; + +const buildSelfServiceTimelineInputMock = vi.mocked(buildSelfServiceTimelineInput); +const buildTimelineEntriesDetailInputMock = vi.mocked(buildTimelineEntriesDetailInput); +const loadTimelineHolidayOverlaysMock = vi.mocked(loadTimelineHolidayOverlays); +const buildTimelineHolidayOverlayDetailResponseMock = vi.mocked(buildTimelineHolidayOverlayDetailResponse); +const formatTimelineHolidayOverlaysMock = vi.mocked(formatTimelineHolidayOverlays); + +describe("timeline holiday procedure support", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns an empty list when self-service scope cannot be resolved", async () => { + buildSelfServiceTimelineInputMock.mockResolvedValueOnce(null); + + await expect( + readMyTimelineHolidayOverlays( + { db: {}, dbUser: { id: "user_1" } } as never, + { + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-07T00:00:00.000Z"), + }, + ), + ).resolves.toEqual([]); + + expect(loadTimelineHolidayOverlaysMock).not.toHaveBeenCalled(); + }); + + it("loads self-service holiday overlays when a scope is available", async () => { + const ctx = { db: {}, dbUser: { id: "user_1" } } as never; + const input = { + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-07T00:00:00.000Z"), + }; + const selfServiceInput = { + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-07T00:00:00.000Z"), + resourceIds: ["resource_1"], + }; + + buildSelfServiceTimelineInputMock.mockResolvedValueOnce(selfServiceInput as never); + loadTimelineHolidayOverlaysMock.mockResolvedValueOnce([{ id: "overlay_1" }] as never); + + await expect(readMyTimelineHolidayOverlays(ctx, input)).resolves.toEqual([{ id: "overlay_1" }]); + + expect(buildSelfServiceTimelineInputMock).toHaveBeenCalledWith(ctx, input); + expect(loadTimelineHolidayOverlaysMock).toHaveBeenCalledWith(ctx.db, selfServiceInput); + }); + + it("builds holiday overlay detail responses from resolved detail inputs", async () => { + buildTimelineEntriesDetailInputMock.mockReturnValueOnce({ + period: { + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-07T00:00:00.000Z"), + }, + filters: { countryCodes: ["DE"] }, + timelineInput: { + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-07T00:00:00.000Z"), + countryCodes: ["DE"], + }, + } as never); + loadTimelineHolidayOverlaysMock.mockResolvedValueOnce([{ id: "overlay_1" }] as never); + formatTimelineHolidayOverlaysMock.mockReturnValueOnce([{ id: "formatted_1" }] as never); + buildTimelineHolidayOverlayDetailResponseMock.mockReturnValueOnce({ detail: true } as never); + + await expect( + readTimelineHolidayOverlayDetail({} as never, { + startDate: "2026-04-01", + endDate: "2026-04-07", + countryCodes: ["DE"], + }), + ).resolves.toEqual({ detail: true }); + + expect(loadTimelineHolidayOverlaysMock).toHaveBeenCalledWith({}, { + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-07T00:00:00.000Z"), + countryCodes: ["DE"], + }); + expect(formatTimelineHolidayOverlaysMock).toHaveBeenCalledWith([{ id: "overlay_1" }]); + expect(buildTimelineHolidayOverlayDetailResponseMock).toHaveBeenCalledWith({ + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-07T00:00:00.000Z"), + filters: { countryCodes: ["DE"] }, + overlays: [{ id: "formatted_1" }], + }); + }); +}); diff --git a/packages/api/src/router/timeline-holiday-procedure-support.ts b/packages/api/src/router/timeline-holiday-procedure-support.ts new file mode 100644 index 0000000..ea7f8f6 --- /dev/null +++ b/packages/api/src/router/timeline-holiday-procedure-support.ts @@ -0,0 +1,41 @@ +import { + buildSelfServiceTimelineInput, + buildTimelineEntriesDetailInput, +} from "./timeline-read-shared.js"; +import { loadTimelineHolidayOverlays } from "./timeline-holiday-load-support.js"; +import { + buildTimelineHolidayOverlayDetailResponse, + formatTimelineHolidayOverlays, +} from "./timeline-holiday-support.js"; + +type TimelineHolidayProcedureDb = Parameters[0]; +type TimelineHolidaySelfServiceContext = Parameters[0]; +type TimelineHolidaySelfServiceInput = Parameters[1]; +type TimelineHolidayDetailInput = Parameters[0]; + +export async function readMyTimelineHolidayOverlays( + ctx: TimelineHolidaySelfServiceContext, + input: TimelineHolidaySelfServiceInput, +) { + const selfServiceInput = await buildSelfServiceTimelineInput(ctx, input); + if (!selfServiceInput) { + return []; + } + + return loadTimelineHolidayOverlays(ctx.db, selfServiceInput); +} + +export async function readTimelineHolidayOverlayDetail( + db: TimelineHolidayProcedureDb, + input: TimelineHolidayDetailInput, +) { + const { period, filters, timelineInput } = buildTimelineEntriesDetailInput(input); + const holidayOverlays = await loadTimelineHolidayOverlays(db, timelineInput); + + return buildTimelineHolidayOverlayDetailResponse({ + startDate: period.startDate, + endDate: period.endDate, + filters, + overlays: formatTimelineHolidayOverlays(holidayOverlays), + }); +} diff --git a/packages/api/src/router/timeline-holiday-read.ts b/packages/api/src/router/timeline-holiday-read.ts index a294a69..16c6d9d 100644 --- a/packages/api/src/router/timeline-holiday-read.ts +++ b/packages/api/src/router/timeline-holiday-read.ts @@ -1,16 +1,16 @@ -import { z } from "zod"; import { controllerProcedure, protectedProcedure } from "../trpc.js"; import { - buildSelfServiceTimelineInput, - createTimelineDateRange, - createTimelineFilters, + TimelineDetailFiltersSchema, TimelineWindowFiltersSchema, } from "./timeline-read-shared.js"; import { loadTimelineHolidayOverlays, } from "./timeline-holiday-load-support.js"; import { - buildTimelineHolidayOverlayDetailResponse, + readMyTimelineHolidayOverlays, + readTimelineHolidayOverlayDetail, +} from "./timeline-holiday-procedure-support.js"; +import { formatTimelineHolidayOverlays, summarizeTimelineHolidayOverlays, type TimelineHolidayOverlayRecord, @@ -40,44 +40,9 @@ export const timelineHolidayReadProcedures = { getMyHolidayOverlays: protectedProcedure .input(TimelineWindowFiltersSchema) - .query(async ({ ctx, input }) => { - const selfServiceInput = await buildSelfServiceTimelineInput(ctx, input); - if (!selfServiceInput) { - return []; - } - - return loadTimelineHolidayOverlays(ctx.db, selfServiceInput); - }), + .query(async ({ ctx, input }) => readMyTimelineHolidayOverlays(ctx, input)), getHolidayOverlayDetail: controllerProcedure - .input( - z.object({ - startDate: z.string().optional(), - endDate: z.string().optional(), - durationDays: z.number().int().min(1).max(366).optional(), - resourceIds: z.array(z.string()).optional(), - projectIds: z.array(z.string()).optional(), - clientIds: z.array(z.string()).optional(), - chapters: z.array(z.string()).optional(), - eids: z.array(z.string()).optional(), - countryCodes: z.array(z.string()).optional(), - }), - ) - .query(async ({ ctx, input }) => { - const { startDate, endDate } = createTimelineDateRange(input); - const filters = createTimelineFilters(input); - const holidayOverlays = await loadTimelineHolidayOverlays(ctx.db, { - ...filters, - startDate, - endDate, - }); - const formattedOverlays = formatHolidayOverlays(holidayOverlays); - - return buildTimelineHolidayOverlayDetailResponse({ - startDate, - endDate, - filters, - overlays: formattedOverlays, - }); - }), + .input(TimelineDetailFiltersSchema) + .query(async ({ ctx, input }) => readTimelineHolidayOverlayDetail(ctx.db, input)), };