import { OrderType, AllocationType, PermissionKey, ProjectStatus, SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { invalidateDashboardCache } from "../lib/cache.js"; import { logger } from "../lib/logger.js"; import { projectRouter } from "../router/project.js"; import { dispatchWebhooks } from "../lib/webhook-dispatcher.js"; import { createCallerFactory } from "../trpc.js"; vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, countPlanningEntries: vi.fn().mockResolvedValue({ countsByProjectId: new Map() }), listAssignmentBookings: vi.fn().mockResolvedValue([]), }; }); vi.mock("../router/blueprint-validation.js", () => ({ assertBlueprintDynamicFields: vi.fn().mockResolvedValue(undefined), })); vi.mock("../router/project-planning-read-model.js", () => ({ loadProjectPlanningReadModel: vi.fn().mockResolvedValue({ readModel: { assignments: [], demands: [] }, }), })); vi.mock("../lib/cache.js", () => ({ invalidateDashboardCache: vi.fn().mockResolvedValue(undefined), })); vi.mock("../lib/webhook-dispatcher.js", () => ({ dispatchWebhooks: vi.fn().mockResolvedValue(undefined), })); vi.mock("../lib/logger.js", () => ({ logger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), }, })); vi.mock("../ai-client.js", () => ({ isDalleConfigured: vi.fn().mockReturnValue(false), createDalleClient: vi.fn(), parseAiError: vi.fn(), })); const createCaller = createCallerFactory(projectRouter); function createManagerCaller(db: Record) { return createCaller({ session: { user: { email: "manager@example.com", name: "Manager", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "mgr_1", systemRole: SystemRole.MANAGER, permissionOverrides: null, }, }); } function createAdminCaller(db: Record) { return createCaller({ session: { user: { email: "admin@example.com", name: "Admin", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "admin_1", systemRole: SystemRole.ADMIN, permissionOverrides: null, }, }); } function createControllerCaller(db: Record) { return createCaller({ session: { user: { email: "controller@example.com", name: "Controller", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "ctrl_1", systemRole: SystemRole.CONTROLLER, permissionOverrides: null, }, }); } function createProtectedCaller(db: Record) { return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: null, }, }); } function createUnauthenticatedCaller(db: Record) { return createCaller({ session: null, db: db as never, dbUser: null, }); } function createProtectedCallerWithOverrides( db: Record, overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null, ) { return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: overrides, }, }); } const sampleProject = { id: "project_1", shortCode: "PRJ-001", name: "Test Project", orderType: OrderType.CHARGEABLE, allocationType: AllocationType.INT, winProbability: 80, budgetCents: 500_000_00, startDate: new Date("2026-01-01"), endDate: new Date("2026-06-30"), status: ProjectStatus.ACTIVE, responsiblePerson: "Alice", dynamicFields: {}, staffingReqs: [], blueprintId: null, color: null, coverImageUrl: null, coverFocusY: 50, utilizationCategoryId: null, clientId: null, createdAt: new Date("2026-01-01"), updatedAt: new Date("2026-01-01"), }; describe("project router", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("configuration checks", () => { it("requires authentication for image generation configuration checks", async () => { const findUnique = vi.fn(); const caller = createUnauthenticatedCaller({ systemSettings: { findUnique, }, }); await expect(caller.isImageGenConfigured()).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); await expect(caller.isDalleConfigured()).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(findUnique).not.toHaveBeenCalled(); }); it("returns only narrow readiness data for authenticated callers", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "singleton", imageProvider: "dalle", }); const caller = createProtectedCaller({ systemSettings: { findUnique, }, }); const imageGen = await caller.isImageGenConfigured(); const dalle = await caller.isDalleConfigured(); expect(imageGen).toEqual({ configured: false, provider: "dalle", }); expect(dalle).toEqual({ configured: false, }); expect(findUnique).toHaveBeenNthCalledWith(1, { where: { id: "singleton" }, }); expect(findUnique).toHaveBeenNthCalledWith(2, { where: { id: "singleton" }, }); }); }); // ─── create ─────────────────────────────────────────────────────────────── describe("create", () => { it("creates a project and returns its id", async () => { const created = { ...sampleProject, id: "project_new" }; const db: Record = { project: { findUnique: vi.fn().mockResolvedValue(null), // no shortCode conflict create: vi.fn().mockResolvedValue(created), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, webhook: { findMany: vi.fn().mockResolvedValue([]) }, $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)), }; const caller = createManagerCaller(db); const result = await caller.create({ shortCode: "PRJ-001", name: "Test Project", responsiblePerson: "Alice", orderType: OrderType.CHARGEABLE, allocationType: AllocationType.INT, winProbability: 80, budgetCents: 500_000_00, startDate: new Date("2026-01-01"), endDate: new Date("2026-06-30"), }); expect(result.id).toBe("project_new"); expect(db.project.create).toHaveBeenCalled(); expect(db.auditLog.create).toHaveBeenCalled(); }); it("logs and swallows background cache and webhook failures during create", async () => { vi.mocked(invalidateDashboardCache).mockRejectedValueOnce(new Error("redis unavailable")); vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook unavailable")); const created = { ...sampleProject, id: "project_safe_create" }; const db: Record = { project: { findUnique: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue(created), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, webhook: { findMany: vi.fn().mockResolvedValue([]) }, $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)), }; const caller = createManagerCaller(db); const result = await caller.create({ shortCode: "SAFE-001", name: "Safe Project", responsiblePerson: "Alice", orderType: OrderType.CHARGEABLE, allocationType: AllocationType.INT, winProbability: 80, budgetCents: 500_000_00, startDate: new Date("2026-01-01"), endDate: new Date("2026-06-30"), }); await Promise.resolve(); await Promise.resolve(); expect(result.id).toBe("project_safe_create"); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "invalidateDashboardCache" }), "Project background side effect failed", ); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "dispatchWebhooks", event: "project.created" }), "Project background side effect failed", ); }); it("throws CONFLICT when shortCode already exists", async () => { const db = { project: { findUnique: vi.fn().mockResolvedValue(sampleProject), create: vi.fn(), }, auditLog: { create: vi.fn() }, }; const caller = createManagerCaller(db); await expect( caller.create({ shortCode: "PRJ-001", name: "Duplicate", responsiblePerson: "Alice", orderType: OrderType.CHARGEABLE, allocationType: AllocationType.INT, budgetCents: 100_00, startDate: new Date("2026-01-01"), endDate: new Date("2026-06-30"), }), ).rejects.toThrow( expect.objectContaining({ code: "CONFLICT" }), ); }); it("blocks USER role from creating projects", async () => { const db = { project: { findUnique: vi.fn(), create: vi.fn() }, auditLog: { create: vi.fn() }, }; const caller = createProtectedCaller(db); await expect( caller.create({ shortCode: "PRJ-002", name: "Blocked", responsiblePerson: "Alice", orderType: OrderType.CHARGEABLE, allocationType: AllocationType.INT, budgetCents: 100_00, startDate: new Date("2026-01-01"), endDate: new Date("2026-06-30"), }), ).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN" }), ); }); }); // ─── getById ────────────────────────────────────────────────────────────── describe("getById", () => { it("returns the correct project with allocations and demands for controller-level access", async () => { const db = { project: { findUnique: vi.fn().mockResolvedValue({ ...sampleProject, blueprint: null }), }, allocation: { findMany: vi.fn().mockResolvedValue([]) }, demandRequirement: { findMany: vi.fn().mockResolvedValue([]) }, assignment: { findMany: vi.fn().mockResolvedValue([]) }, }; const caller = createControllerCaller(db); const result = await caller.getById({ id: "project_1" }); expect(result.id).toBe("project_1"); expect(result.name).toBe("Test Project"); expect(result).toHaveProperty("allocations"); expect(result).toHaveProperty("demands"); expect(result).toHaveProperty("assignments"); }); it("throws NOT_FOUND when project does not exist", async () => { const db = { project: { findUnique: vi.fn().mockResolvedValue(null) }, allocation: { findMany: vi.fn().mockResolvedValue([]) }, demandRequirement: { findMany: vi.fn().mockResolvedValue([]) }, assignment: { findMany: vi.fn().mockResolvedValue([]) }, }; const caller = createControllerCaller(db); await expect(caller.getById({ id: "missing" })).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); }); it("blocks USER role from loading full project planning context", async () => { const db = { project: { findUnique: vi.fn() }, }; const caller = createProtectedCaller(db); await expect(caller.getById({ id: "project_1" })).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN" }), ); }); }); describe("getShoringRatio", () => { it("excludes regional holidays from shoring weighting", async () => { const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1", name: "Test Project", shoringThreshold: 55, onshoreCountryCode: "DE", }), }, assignment: { findMany: vi.fn().mockResolvedValue([ { id: "a1", resourceId: "res_de", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), hoursPerDay: 8, resource: { id: "res_de", countryId: "country_de", federalState: "BY", metroCityId: null, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0 }, country: { id: "country_de", code: "DE" }, metroCity: null, }, }, { id: "a2", resourceId: "res_es", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), hoursPerDay: 8, resource: { id: "res_es", countryId: "country_es", federalState: null, metroCityId: null, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0 }, country: { id: "country_es", code: "ES" }, metroCity: null, }, }, ]), }, }; const caller = createControllerCaller(db); const result = await caller.getShoringRatio({ projectId: "project_1" }); expect(result.totalHours).toBe(24); expect(result.onshoreRatio).toBe(33); expect(result.offshoreRatio).toBe(67); }); }); // ─── update ─────────────────────────────────────────────────────────────── describe("update", () => { it("updates project fields", async () => { const updated = { ...sampleProject, name: "Updated Name" }; const db: Record = { project: { findUnique: vi.fn().mockResolvedValue(sampleProject), update: vi.fn().mockResolvedValue(updated), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)), }; const caller = createManagerCaller(db); const result = await caller.update({ id: "project_1", data: { name: "Updated Name" }, }); expect(result.name).toBe("Updated Name"); expect(db.project.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "project_1" }, }), ); expect(db.auditLog.create).toHaveBeenCalled(); }); it("throws NOT_FOUND when updating non-existent project", async () => { const db = { project: { findUnique: vi.fn().mockResolvedValue(null), update: vi.fn(), }, auditLog: { create: vi.fn() }, }; const caller = createManagerCaller(db); await expect( caller.update({ id: "missing", data: { name: "X" } }), ).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); }); }); // ─── updateStatus ───────────────────────────────────────────────────────── describe("updateStatus", () => { it("transitions project status", async () => { const updated = { ...sampleProject, status: ProjectStatus.COMPLETED }; const db = { project: { findUnique: vi.fn().mockResolvedValue(sampleProject), update: vi.fn().mockResolvedValue(updated), }, webhook: { findMany: vi.fn().mockResolvedValue([]) }, }; const caller = createManagerCaller(db); const result = await caller.updateStatus({ id: "project_1", status: ProjectStatus.COMPLETED, }); expect(result.status).toBe(ProjectStatus.COMPLETED); expect(db.project.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "project_1" }, data: { status: ProjectStatus.COMPLETED }, }), ); }); it("logs and swallows background failures during status changes", async () => { vi.mocked(invalidateDashboardCache).mockRejectedValueOnce(new Error("redis unavailable")); vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook unavailable")); const updated = { ...sampleProject, status: ProjectStatus.COMPLETED }; const db = { project: { findUnique: vi.fn().mockResolvedValue(sampleProject), update: vi.fn().mockResolvedValue(updated), }, webhook: { findMany: vi.fn().mockResolvedValue([]) }, }; const caller = createManagerCaller(db); const result = await caller.updateStatus({ id: "project_1", status: ProjectStatus.COMPLETED, }); await Promise.resolve(); await Promise.resolve(); expect(result.status).toBe(ProjectStatus.COMPLETED); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "invalidateDashboardCache" }), "Project background side effect failed", ); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "dispatchWebhooks", event: "project.status_changed" }), "Project background side effect failed", ); }); }); // ─── batchUpdateStatus ──────────────────────────────────────────────────── describe("batchUpdateStatus", () => { it("updates multiple projects and returns count", async () => { const db: Record = { project: { update: vi.fn().mockResolvedValue(sampleProject), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)), }; const caller = createManagerCaller(db); const result = await caller.batchUpdateStatus({ ids: ["project_1", "project_2", "project_3"], status: ProjectStatus.ON_HOLD, }); expect(result.count).toBe(3); expect((db.auditLog as Record>).create).toHaveBeenCalled(); }); }); // ─── delete ─────────────────────────────────────────────────────────────── describe("delete", () => { it("deletes a project and cascades related records", async () => { const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1", name: "Test", shortCode: "PRJ" }), delete: vi.fn().mockResolvedValue({}), }, assignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) }, demandRequirement: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) }, calculationRule: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) }, auditLog: { create: vi.fn().mockResolvedValue({}) }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createAdminCaller(db); const result = await caller.delete({ id: "project_1" }); expect(result).toMatchObject({ id: "project_1", name: "Test" }); }); it("throws NOT_FOUND when deleting non-existent project", async () => { const db = { project: { findUnique: vi.fn().mockResolvedValue(null) }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createAdminCaller(db); await expect(caller.delete({ id: "missing" })).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); }); it("requires admin role — blocks manager", async () => { const db = { project: { findUnique: vi.fn() }, $transaction: vi.fn(), }; const caller = createManagerCaller(db); await expect(caller.delete({ id: "project_1" })).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN" }), ); }); }); // ─── batchDelete ────────────────────────────────────────────────────────── describe("batchDelete", () => { it("deletes multiple projects in a transaction", async () => { const projects = [ { id: "p1", name: "A", shortCode: "A1" }, { id: "p2", name: "B", shortCode: "B1" }, ]; const db = { project: { findMany: vi.fn().mockResolvedValue(projects), deleteMany: vi.fn().mockResolvedValue({ count: 2 }), }, assignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) }, demandRequirement: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) }, calculationRule: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) }, auditLog: { create: vi.fn().mockResolvedValue({}) }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createAdminCaller(db); const result = await caller.batchDelete({ ids: ["p1", "p2"] }); expect(result.count).toBe(2); }); it("throws NOT_FOUND when no projects match the ids", async () => { const db = { project: { findMany: vi.fn().mockResolvedValue([]) }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createAdminCaller(db); await expect(caller.batchDelete({ ids: ["missing"] })).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); }); }); // ─── listWithCosts ──────────────────────────────────────────────────────── describe("listWithCosts", () => { it("returns projects with cost data", async () => { const db = { project: { findMany: vi.fn().mockResolvedValue([sampleProject]), }, }; const { listAssignmentBookings } = await import("@capakraken/application"); vi.mocked(listAssignmentBookings).mockResolvedValue([]); const caller = createControllerCaller(db); const result = await caller.listWithCosts({ limit: 20 }); expect(result.projects).toHaveLength(1); expect(result.projects[0]).toHaveProperty("totalCostCents"); expect(result.projects[0]).toHaveProperty("totalPersonDays"); expect(result.projects[0]).toHaveProperty("utilizationPercent"); }); it("calculates cost from assignment bookings", async () => { const db = { project: { findMany: vi.fn().mockResolvedValue([{ ...sampleProject, budgetCents: 100_000_00 }]), }, }; const { listAssignmentBookings } = await import("@capakraken/application"); vi.mocked(listAssignmentBookings).mockResolvedValue([ { id: "a1", projectId: "project_1", resourceId: "res_1", startDate: new Date("2026-01-01"), endDate: new Date("2026-01-05"), hoursPerDay: 8, dailyCostCents: 50000, status: "CONFIRMED", project: { id: "project_1", name: "Test", shortCode: "PRJ", status: "ACTIVE", orderType: "CHARGEABLE", dynamicFields: null }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI" }, }, ]); const caller = createControllerCaller(db); const result = await caller.listWithCosts({ limit: 20 }); expect(result.projects[0]?.totalCostCents).toBeGreaterThan(0); expect(result.projects[0]?.totalPersonDays).toBeGreaterThan(0); }); it("requires controller role — blocks USER", async () => { const db = { project: { findMany: vi.fn() } }; const caller = createProtectedCaller(db); await expect(caller.listWithCosts({ limit: 20 })).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN" }), ); }); }); describe("assistant-facing detail routes", () => { it("returns lightweight project search summaries from the canonical router", async () => { const db = { project: { findMany: vi.fn().mockResolvedValue([ { id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", status: ProjectStatus.ACTIVE, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), client: { name: "Acme Mobility" }, }, ]), }, }; const caller = createControllerCaller(db); const result = await caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 }); expect(result).toEqual([ { id: "project_1", code: "GDM", name: "Gelddruckmaschine", status: "ACTIVE", start: "2026-01-01", end: "2026-03-31", client: "Acme Mobility", }, ]); }); it("returns formatted project search summaries from the canonical router", async () => { const db = { project: { findMany: vi.fn().mockResolvedValue([ { id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", status: ProjectStatus.ACTIVE, budgetCents: 500000, winProbability: 100, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), client: { name: "Acme Mobility" }, _count: { assignments: 3, estimates: 1 }, }, ]), }, }; const caller = createControllerCaller(db); const result = await caller.searchSummariesDetail({ search: "Gelddruckmaschine", limit: 10 }); expect(result).toEqual([ { id: "project_1", code: "GDM", name: "Gelddruckmaschine", status: "ACTIVE", budget: "5.000,00 EUR", winProbability: "100%", start: "2026-01-01", end: "2026-03-31", client: "Acme Mobility", assignmentCount: 3, estimateCount: 1, }, ]); }); it("blocks USER role from detailed project search summaries", async () => { const db = { project: { findMany: vi.fn(), }, }; const caller = createProtectedCaller(db); await expect( caller.searchSummariesDetail({ search: "Gelddruckmaschine", limit: 10 }), ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" })); }); it("blocks USER role from lightweight project search summaries", async () => { const db = { project: { findMany: vi.fn(), }, }; const caller = createProtectedCaller(db); await expect( caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 }), ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" })); }); it("allows explicit viewPlanning overrides to access lightweight project search summaries", async () => { const db = { project: { findMany: vi.fn().mockResolvedValue([ { id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", status: ProjectStatus.ACTIVE, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), client: { name: "Acme Mobility" }, }, ]), }, }; const caller = createProtectedCallerWithOverrides(db, { granted: [PermissionKey.VIEW_PLANNING], }); const result = await caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 }); expect(result).toEqual([ { id: "project_1", code: "GDM", name: "Gelddruckmaschine", status: "ACTIVE", start: "2026-01-01", end: "2026-03-31", client: "Acme Mobility", }, ]); }); it("does not treat viewCosts as a substitute for viewPlanning on lightweight project search summaries", async () => { const db = { project: { findMany: vi.fn(), }, }; const caller = createProtectedCallerWithOverrides(db, { granted: [PermissionKey.VIEW_COSTS], }); await expect( caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 }), ).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN", message: "Planning read access required", }), ); }); it("returns lightweight project identifier reads from the canonical router", async () => { const db = { project: { findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce({ id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", status: ProjectStatus.ACTIVE, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), }), findFirst: vi.fn(), }, }; const caller = createControllerCaller(db); const result = await caller.getByIdentifier({ identifier: "GDM" }); expect(result).toEqual({ id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", status: ProjectStatus.ACTIVE, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), }); }); it("returns formatted project details from the canonical router", async () => { const db = { project: { findUnique: vi.fn() .mockResolvedValueOnce(null) .mockResolvedValueOnce({ id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", status: ProjectStatus.ACTIVE, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), }) .mockResolvedValueOnce({ id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", status: ProjectStatus.ACTIVE, orderType: OrderType.CHARGEABLE, allocationType: AllocationType.INT, budgetCents: 500000, winProbability: 100, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), responsiblePerson: "Bruce Banner", client: { name: "Acme Mobility" }, utilizationCategory: { code: "BILLABLE", name: "Billable" }, _count: { assignments: 3, estimates: 1 }, }), findFirst: vi.fn(), }, assignment: { findMany: vi.fn().mockResolvedValue([ { resource: { displayName: "Bruce Banner", eid: "EMP-001" }, role: "Lead", status: "ACTIVE", hoursPerDay: 8, startDate: new Date("2026-02-01T00:00:00.000Z"), endDate: new Date("2026-02-28T00:00:00.000Z"), }, ]), }, }; const caller = createControllerCaller(db); const result = await caller.getByIdentifierDetail({ identifier: "GDM" }); expect(result).toEqual({ id: "project_1", code: "GDM", name: "Gelddruckmaschine", status: "ACTIVE", orderType: "CHARGEABLE", allocationType: "INT", budget: "5.000,00 EUR", budgetCents: 500000, winProbability: "100%", start: "2026-01-01", end: "2026-03-31", responsible: "Bruce Banner", client: "Acme Mobility", category: "Billable", assignmentCount: 3, estimateCount: 1, topAllocations: [ { resource: "Bruce Banner", eid: "EMP-001", role: "Lead", status: "ACTIVE", hoursPerDay: 8, start: "2026-02-01", end: "2026-02-28", }, ], }); }); it("blocks USER role from detailed project identifier reads", async () => { const db = { project: { findUnique: vi.fn(), }, }; const caller = createProtectedCaller(db); await expect( caller.getByIdentifierDetail({ identifier: "GDM" }), ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" })); }); it("blocks USER role from lightweight project identifier reads", async () => { const db = { project: { findUnique: vi.fn(), findFirst: vi.fn(), }, }; const caller = createProtectedCaller(db); await expect( caller.getByIdentifier({ identifier: "GDM" }), ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" })); }); }); });