refactor(api): extract project procedures

This commit is contained in:
2026-03-31 21:28:56 +02:00
parent b1799e4f54
commit e34c22f3b0
4 changed files with 334 additions and 93 deletions
@@ -0,0 +1,204 @@
import { FieldType, ProjectStatus } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
const {
countPlanningEntries,
loadProjectPlanningReadModel,
getProjectShoringRatio,
} = vi.hoisted(() => ({
countPlanningEntries: vi.fn(),
loadProjectPlanningReadModel: vi.fn(),
getProjectShoringRatio: vi.fn(),
}));
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
countPlanningEntries,
};
});
vi.mock("../router/project-planning-read-model.js", () => ({
loadProjectPlanningReadModel,
}));
vi.mock("../router/project-shoring-ratio.js", () => ({
getProjectShoringRatio,
}));
import {
getProjectById,
getProjectShoringRatioData,
listProjects,
} from "../router/project-procedure-support.js";
function createContext(db: Record<string, unknown>) {
return { db: db as never };
}
describe("project-procedure-support", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("lists projects with planning counts and dynamic field filters", async () => {
const findMany = vi.fn().mockResolvedValue([
{
id: "project_1",
name: "Platform Refresh",
shortCode: "PRJ-1",
status: ProjectStatus.ACTIVE,
startDate: new Date("2026-01-01T00:00:00.000Z"),
},
]);
const count = vi.fn().mockResolvedValue(1);
countPlanningEntries.mockResolvedValue({
countsByProjectId: new Map([["project_1", 3]]),
});
const result = await listProjects(
createContext({
project: {
findMany,
count,
},
}),
{
limit: 50,
page: 1,
status: ProjectStatus.ACTIVE,
search: "Platform",
customFieldFilters: [
{ key: "market", value: "de", type: FieldType.TEXT },
],
},
);
expect(result.total).toBe(1);
expect(result.projects).toEqual([
expect.objectContaining({
id: "project_1",
_count: { allocations: 3 },
}),
]);
expect(findMany).toHaveBeenCalledWith({
where: {
status: ProjectStatus.ACTIVE,
OR: [
{ name: { contains: "Platform", mode: "insensitive" } },
{ shortCode: { contains: "Platform", mode: "insensitive" } },
],
AND: [
{
dynamicFields: {
path: ["market"],
string_contains: "de",
},
},
],
},
skip: 0,
take: 51,
orderBy: [{ startDate: "asc" }, { id: "asc" }],
});
expect(count).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
status: ProjectStatus.ACTIVE,
}),
}));
expect(countPlanningEntries).toHaveBeenCalledWith(
expect.objectContaining({
project: expect.objectContaining({
findMany,
count,
}),
}),
{ projectIds: ["project_1"] },
);
});
it("returns a project detail enriched with planning read model data", async () => {
const findUnique = vi.fn().mockResolvedValue({
id: "project_1",
name: "Platform Refresh",
blueprint: null,
});
loadProjectPlanningReadModel.mockResolvedValue({
readModel: {
assignments: [{ id: "assignment_1" }],
demands: [{ id: "demand_1" }],
},
});
const result = await getProjectById(
createContext({
project: { findUnique },
}),
{ id: "project_1" },
);
expect(result).toEqual({
id: "project_1",
name: "Platform Refresh",
blueprint: null,
allocations: [{ id: "assignment_1" }],
demands: [{ id: "demand_1" }],
assignments: [{ id: "assignment_1" }],
});
expect(findUnique).toHaveBeenCalledWith({
where: { id: "project_1" },
include: { blueprint: true },
});
expect(loadProjectPlanningReadModel).toHaveBeenCalledWith(
expect.objectContaining({
project: expect.objectContaining({ findUnique }),
}),
{ projectId: "project_1" },
);
});
it("throws not found when the requested project is missing", async () => {
loadProjectPlanningReadModel.mockResolvedValue({
readModel: { assignments: [], demands: [] },
});
await expect(
getProjectById(
createContext({
project: { findUnique: vi.fn().mockResolvedValue(null) },
}),
{ id: "missing" },
),
).rejects.toThrowError(new TRPCError({
code: "NOT_FOUND",
message: "Project not found",
}));
});
it("delegates shoring ratio reads to the dedicated shoring helper", async () => {
getProjectShoringRatio.mockResolvedValue({
totalHours: 40,
onshoreRatio: 60,
offshoreRatio: 40,
});
const result = await getProjectShoringRatioData(
createContext({
project: { findUnique: vi.fn() },
}),
{ projectId: "project_1" },
);
expect(result).toEqual({
totalHours: 40,
onshoreRatio: 60,
offshoreRatio: 40,
});
expect(getProjectShoringRatio).toHaveBeenCalledWith(
expect.any(Object),
"project_1",
);
});
});