refactor(api): extract timeline project query and read helpers

This commit is contained in:
2026-03-31 16:01:35 +02:00
parent fda6bcab74
commit 803de725ad
7 changed files with 357 additions and 288 deletions
@@ -14,13 +14,9 @@ vi.mock("../router/timeline-holiday-read.js", async () => {
});
import {
buildTimelineBudgetStatusAllocations,
buildTimelineBudgetStatusResponse,
buildTimelineProjectContextDetailResponse,
buildTimelineProjectContextResponse,
buildTimelineProjectContextSummary,
buildTimelineShiftValidationBookings,
buildTimelineShiftPreviewDetailResponse,
loadTimelineProjectContextDetailArtifacts,
resolveTimelineProjectContextPeriod,
} from "../router/timeline-project-context-support.js";
@@ -366,116 +362,6 @@ describe("timeline project context support", () => {
});
});
it("builds shift preview detail payloads with formatted dates", () => {
expect(buildTimelineShiftPreviewDetailResponse({
project: {
id: "project_1",
name: "Project One",
shortCode: "PRJ",
status: "ACTIVE",
responsiblePerson: "Alice",
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-10T00:00:00.000Z"),
},
requestedShift: {
newStartDate: new Date("2026-04-03T00:00:00.000Z"),
newEndDate: new Date("2026-04-12T00:00:00.000Z"),
},
preview: { valid: true },
})).toEqual({
project: {
id: "project_1",
name: "Project One",
shortCode: "PRJ",
status: "ACTIVE",
responsiblePerson: "Alice",
startDate: "2026-04-01",
endDate: "2026-04-10",
},
requestedShift: {
newStartDate: "2026-04-03",
newEndDate: "2026-04-12",
},
preview: { valid: true },
});
});
it("maps shift validation bookings and budget status payloads", () => {
expect(
buildTimelineShiftValidationBookings([
{
id: "booking_1",
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 8,
status: "CONFIRMED",
},
{
id: "booking_2",
resourceId: null,
projectId: "project_1",
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 4,
status: "PROPOSED",
},
]),
).toEqual([
{
id: "booking_1",
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 8,
status: "CONFIRMED",
},
]);
expect(
buildTimelineBudgetStatusAllocations([
{
status: "CONFIRMED",
dailyCostCents: 40000,
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 8,
},
]),
).toEqual([
{
status: "CONFIRMED",
dailyCostCents: 40000,
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 8,
},
]);
expect(
buildTimelineBudgetStatusResponse({
project: {
name: "Project One",
shortCode: "PRJ",
budgetCents: 120000,
},
budgetStatus: {
withinBudget: true,
varianceCents: 2000,
},
totalAllocations: 3,
}),
).toEqual({
withinBudget: true,
varianceCents: 2000,
projectName: "Project One",
projectCode: "PRJ",
totalAllocations: 3,
budgetCents: 120000,
});
});
it("loads detail artifacts with formatted holiday overlays only when resources exist", async () => {
loadTimelineHolidayOverlaysMock.mockResolvedValueOnce([
@@ -0,0 +1,39 @@
import { TRPCError } from "@trpc/server";
import { describe, expect, it, vi } from "vitest";
import {
findTimelineProjectOrThrow,
timelineBudgetStatusProjectSelect,
} from "../router/timeline-project-query-support.js";
describe("timeline project query support", () => {
it("loads a project with the requested select", async () => {
const project = { id: "project_1", name: "Project One", budgetCents: 120000 };
const findUnique = vi.fn().mockResolvedValue(project);
await expect(findTimelineProjectOrThrow({
project: { findUnique },
} as never, {
projectId: "project_1",
select: timelineBudgetStatusProjectSelect,
})).resolves.toEqual(project);
expect(findUnique).toHaveBeenCalledWith({
where: { id: "project_1" },
select: timelineBudgetStatusProjectSelect,
});
});
it("throws a not found error when the project does not exist", async () => {
const findUnique = vi.fn().mockResolvedValue(null);
await expect(findTimelineProjectOrThrow({
project: { findUnique },
} as never, {
projectId: "missing_project",
select: timelineBudgetStatusProjectSelect,
})).rejects.toThrowError(new TRPCError({
code: "NOT_FOUND",
message: "Project not found",
}));
});
});
@@ -0,0 +1,120 @@
import { describe, expect, it } from "vitest";
import {
buildTimelineBudgetStatusAllocations,
buildTimelineBudgetStatusResponse,
buildTimelineShiftPreviewDetailResponse,
buildTimelineShiftValidationBookings,
} from "../router/timeline-project-read-support.js";
describe("timeline project read support", () => {
it("builds shift preview detail payloads with formatted dates", () => {
expect(buildTimelineShiftPreviewDetailResponse({
project: {
id: "project_1",
name: "Project One",
shortCode: "PRJ",
status: "ACTIVE",
responsiblePerson: "Alice",
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-10T00:00:00.000Z"),
},
requestedShift: {
newStartDate: new Date("2026-04-03T00:00:00.000Z"),
newEndDate: new Date("2026-04-12T00:00:00.000Z"),
},
preview: { valid: true },
})).toEqual({
project: {
id: "project_1",
name: "Project One",
shortCode: "PRJ",
status: "ACTIVE",
responsiblePerson: "Alice",
startDate: "2026-04-01",
endDate: "2026-04-10",
},
requestedShift: {
newStartDate: "2026-04-03",
newEndDate: "2026-04-12",
},
preview: { valid: true },
});
});
it("maps shift validation bookings and budget status payloads", () => {
expect(
buildTimelineShiftValidationBookings([
{
id: "booking_1",
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 8,
status: "CONFIRMED",
},
{
id: "booking_2",
resourceId: null,
projectId: "project_1",
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 4,
status: "PROPOSED",
},
]),
).toEqual([
{
id: "booking_1",
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 8,
status: "CONFIRMED",
},
]);
expect(
buildTimelineBudgetStatusAllocations([
{
status: "CONFIRMED",
dailyCostCents: 40000,
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 8,
},
]),
).toEqual([
{
status: "CONFIRMED",
dailyCostCents: 40000,
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
hoursPerDay: 8,
},
]);
expect(
buildTimelineBudgetStatusResponse({
project: {
name: "Project One",
shortCode: "PRJ",
budgetCents: 120000,
},
budgetStatus: {
withinBudget: true,
varianceCents: 2000,
},
totalAllocations: 3,
}),
).toEqual({
withinBudget: true,
varianceCents: 2000,
projectName: "Project One",
projectCode: "PRJ",
totalAllocations: 3,
budgetCents: 120000,
});
});
});