refactor(api): extract timeline holiday procedures

This commit is contained in:
2026-03-31 18:53:20 +02:00
parent 1c5c203a89
commit 424ee24280
3 changed files with 166 additions and 43 deletions
@@ -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" }],
});
});
});
@@ -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<typeof loadTimelineHolidayOverlays>[0];
type TimelineHolidaySelfServiceContext = Parameters<typeof buildSelfServiceTimelineInput>[0];
type TimelineHolidaySelfServiceInput = Parameters<typeof buildSelfServiceTimelineInput>[1];
type TimelineHolidayDetailInput = Parameters<typeof buildTimelineEntriesDetailInput>[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),
});
}
@@ -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)),
};