import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { allocationRouter } from "../router/allocation.js"; import { emitAllocationCreated, emitAllocationDeleted, emitNotificationCreated } from "../sse/event-bus.js"; import { checkBudgetThresholds } from "../lib/budget-alerts.js"; import { generateAutoSuggestions } from "../lib/auto-staffing.js"; import { invalidateDashboardCache } from "../lib/cache.js"; import { logger } from "../lib/logger.js"; import { dispatchWebhooks } from "../lib/webhook-dispatcher.js"; import { createCallerFactory } from "../trpc.js"; vi.mock("../sse/event-bus.js", () => ({ emitAllocationCreated: vi.fn(), emitAllocationDeleted: vi.fn(), emitAllocationUpdated: vi.fn(), emitNotificationCreated: vi.fn(), })); vi.mock("../lib/budget-alerts.js", () => ({ checkBudgetThresholds: vi.fn(), })); vi.mock("../lib/cache.js", () => ({ invalidateDashboardCache: vi.fn(), })); vi.mock("../lib/auto-staffing.js", () => ({ generateAutoSuggestions: vi.fn(), })); 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(), }, })); const createCaller = createCallerFactory(allocationRouter); beforeEach(() => { vi.clearAllMocks(); }); function createManagerCaller(db: Record) { return createCaller({ session: { user: { email: "manager@example.com", name: "Manager", image: null }, expires: "2026-03-13T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.MANAGER, permissionOverrides: null, }, }); } function createControllerCaller(db: Record) { return createCaller({ session: { user: { email: "controller@example.com", name: "Controller", image: null }, expires: "2026-03-13T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_controller", systemRole: SystemRole.CONTROLLER, permissionOverrides: null, }, }); } function createProtectedCaller(db: Record) { return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2026-03-13T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: null, }, }); } function createProtectedCallerWithOverrides( db: Record, overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null, ) { return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2026-03-13T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: overrides, }, }); } describe("allocation router authorization", () => { const planningWindow = { resourceId: "resource_1", startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-02T00:00:00.000Z"), hoursPerDay: 8, }; it("allows controllers to read assignment lists through the planning audience", async () => { const assignment = { id: "assignment_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-02T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 40000, status: AllocationStatus.ACTIVE, metadata: {}, resource: null, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: null, demandRequirement: null, }; const db = { assignment: { findMany: vi.fn().mockResolvedValue([assignment]), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createControllerCaller(db); const result = await caller.listAssignments({}); expect(result).toEqual([assignment]); expect(db.assignment.findMany).toHaveBeenCalledWith({ where: {}, include: expect.any(Object), orderBy: { startDate: "asc" }, }); }); it("allows explicit viewPlanning overrides to read assignment lists", async () => { const assignment = { id: "assignment_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-02T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 40000, status: AllocationStatus.ACTIVE, metadata: {}, resource: null, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: null, demandRequirement: null, }; const db = { assignment: { findMany: vi.fn().mockResolvedValue([assignment]), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createProtectedCallerWithOverrides(db, { granted: [PermissionKey.VIEW_PLANNING], }); const result = await caller.listAssignments({}); expect(result).toEqual([assignment]); }); it("does not treat viewCosts as a substitute for viewPlanning on planning reads", async () => { const caller = createProtectedCallerWithOverrides({}, { granted: [PermissionKey.VIEW_COSTS], }); await expect(caller.listAssignments({})).rejects.toMatchObject({ code: "FORBIDDEN", message: "Planning read access required", }); }); it.each([ { name: "list", invoke: (caller: ReturnType) => caller.list({}) }, { name: "listView", invoke: (caller: ReturnType) => caller.listView({}) }, { name: "listDemands", invoke: (caller: ReturnType) => caller.listDemands({}) }, { name: "listAssignments", invoke: (caller: ReturnType) => caller.listAssignments({}) }, { name: "getAssignmentById", invoke: (caller: ReturnType) => caller.getAssignmentById({ id: "assignment_1" }), }, { name: "resolveAssignment", invoke: (caller: ReturnType) => caller.resolveAssignment({ assignmentId: "assignment_1" }), }, { name: "getDemandRequirementById", invoke: (caller: ReturnType) => caller.getDemandRequirementById({ id: "demand_1" }), }, { name: "checkResourceAvailability", invoke: (caller: ReturnType) => caller.checkResourceAvailability(planningWindow), }, { name: "getResourceAvailabilityView", invoke: (caller: ReturnType) => caller.getResourceAvailabilityView(planningWindow), }, { name: "getResourceAvailabilitySummary", invoke: (caller: ReturnType) => caller.getResourceAvailabilitySummary(planningWindow), }, ])("requires planning read access for $name", async ({ invoke }) => { const caller = createProtectedCaller({}); await expect(invoke(caller)).rejects.toMatchObject({ code: "FORBIDDEN", message: "Planning read access required", }); }); }); function createDemandWorkflowDb(overrides: Record = {}) { const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1", name: "Project One" }), }, role: { findUnique: vi.fn().mockResolvedValue({ name: "FX Artist" }), }, user: { findMany: vi.fn().mockResolvedValue([{ id: "mgr_1" }, { id: "admin_1" }]), }, notification: { create: vi.fn().mockImplementation(async ({ data }: { data: { userId: string } }) => ({ id: `notif_${data.userId}`, })), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, }; return { ...db, ...overrides, project: { ...db.project, ...(overrides.project as Record | undefined) }, role: { ...db.role, ...(overrides.role as Record | undefined) }, user: { ...db.user, ...(overrides.user as Record | undefined) }, notification: { ...db.notification, ...(overrides.notification as Record | undefined), }, auditLog: { ...db.auditLog, ...(overrides.auditLog as Record | undefined) }, }; } describe("allocation entry resolution router", () => { it("excludes regional holidays from resource availability coverage", async () => { const db = { resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", displayName: "Bruce Banner", eid: "E-001", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, countryId: "country_de", federalState: "BY", metroCityId: null, country: { dailyWorkingHours: 8, code: "DE" }, metroCity: null, }), }, assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assignment_1", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), hoursPerDay: 8, status: AllocationStatus.CONFIRMED, project: { name: "Gamma", shortCode: "GAM" }, }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createManagerCaller(db); const result = await caller.checkResourceAvailability({ resourceId: "resource_1", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), hoursPerDay: 8, }); expect(result).toMatchObject({ dailyCapacity: 8, totalWorkingDays: 1, availableDays: 0, partialDays: 0, conflictDays: 1, totalAvailableHours: 0, totalRequestedHours: 8, coveragePercent: 0, }); }); it("returns the canonical resource availability summary shape", async () => { const db = { resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", displayName: "Bruce Banner", eid: "E-001", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, countryId: "country_de", federalState: "BY", metroCityId: null, country: { dailyWorkingHours: 8, code: "DE" }, metroCity: null, }), }, assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assignment_1", startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-01T00:00:00.000Z"), hoursPerDay: 4, status: "CONFIRMED", project: { name: "Gelddruckmaschine", shortCode: "GDM" }, }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([ { id: "vac_1", type: "ANNUAL", status: "APPROVED", startDate: new Date("2026-04-02T00:00:00.000Z"), endDate: new Date("2026-04-02T00:00:00.000Z"), isHalfDay: true, halfDayPart: "AFTERNOON", }, ]), }, }; const caller = createManagerCaller(db); const result = await caller.getResourceAvailabilitySummary({ resourceId: "resource_1", startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-02T00:00:00.000Z"), hoursPerDay: 8, }); expect(result).toEqual({ resource: "Bruce Banner", period: "2026-04-01 to 2026-04-02", fte: null, workingDays: 2, periodAvailableHours: 16, periodBookedHours: 4, periodRemainingHours: 12, maxHoursPerDay: 8, currentBookedHoursPerDay: 2, availableHoursPerDay: 6, isFullyAvailable: false, existingAllocations: [ { project: "Gelddruckmaschine (GDM)", hoursPerDay: 4, status: "CONFIRMED", start: "2026-04-01", end: "2026-04-01", }, ], vacations: [ { type: "ANNUAL", start: "2026-04-02", end: "2026-04-02", isHalfDay: true, }, ], }); }); it("creates an open demand through allocation.create without requiring isPlaceholder", async () => { const createdDemandRequirement = { id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 2, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_fx", name: "FX Artist", color: "#222222" }, }; const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1" }), }, demandRequirement: { create: vi.fn().mockResolvedValue(createdDemandRequirement), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.create({ projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 2, status: AllocationStatus.PROPOSED, metadata: {}, }); expect(result.id).toBe("demand_1"); expect(result.isPlaceholder).toBe(true); expect(db.demandRequirement.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ headcount: 2, }), }), ); }); it("creates an assignment through allocation.create without requiring isPlaceholder", async () => { const createdAssignment = { id: "assignment_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 40000, status: AllocationStatus.ACTIVE, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000, }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: null, }; const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1" }), }, resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }), }, allocation: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn(), }, assignment: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(createdAssignment), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.create({ resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", status: AllocationStatus.ACTIVE, metadata: {}, }); expect(result.id).toBe("assignment_1"); expect(result.isPlaceholder).toBe(false); expect(db.allocation.create).not.toHaveBeenCalled(); expect(db.assignment.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ resourceId: "resource_1", }), }), ); }); it("creates an explicit demand requirement without dual-writing a legacy allocation row", async () => { vi.mocked(emitAllocationCreated).mockClear(); vi.mocked(emitNotificationCreated).mockClear(); const createdDemandRequirement = { id: "demand_explicit_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 2, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_fx", name: "FX Artist", color: "#222222" }, }; const db = createDemandWorkflowDb({ demandRequirement: { create: vi.fn().mockResolvedValue(createdDemandRequirement), }, }) as Record; Object.assign(db, { $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }); const caller = createManagerCaller(db); const result = await caller.createDemandRequirement({ projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 2, status: AllocationStatus.PROPOSED, metadata: {}, }); expect(result.id).toBe("demand_explicit_1"); expect((db as { allocation?: { create?: unknown } }).allocation?.create).toBeUndefined(); expect(db.demandRequirement.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ headcount: 2, }), }), ); expect(emitAllocationCreated).toHaveBeenCalledWith({ id: "demand_explicit_1", projectId: "project_1", resourceId: null, }); expect(db.notification.create).toHaveBeenCalledTimes(2); expect(emitNotificationCreated).toHaveBeenCalledTimes(2); }); it("creates a canonical demand draft with router-owned defaults", async () => { vi.mocked(emitAllocationCreated).mockClear(); vi.mocked(emitNotificationCreated).mockClear(); const createdDemandRequirement = { id: "demand_draft_1", projectId: "project_1", startDate: new Date("2026-04-01"), endDate: new Date("2026-04-15"), hoursPerDay: 6, percentage: 75, role: "Designer", roleId: "role_design", headcount: 2, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_design", name: "Designer", color: "#0099FF" }, }; const db = createDemandWorkflowDb({ demandRequirement: { create: vi.fn().mockResolvedValue(createdDemandRequirement), }, }) as Record; Object.assign(db, { $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }); const caller = createManagerCaller(db); const result = await caller.createDemand({ projectId: "project_1", role: "Designer", roleId: "role_design", headcount: 2, hoursPerDay: 6, startDate: new Date("2026-04-01"), endDate: new Date("2026-04-15"), }); expect(result.id).toBe("demand_draft_1"); expect(db.demandRequirement.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ headcount: 2, percentage: 75, status: AllocationStatus.PROPOSED, metadata: {}, }), }), ); expect(emitAllocationCreated).toHaveBeenCalledWith({ id: "demand_draft_1", projectId: "project_1", resourceId: null, }); }); it("logs and swallows background side-effect failures during demand creation", async () => { vi.mocked(invalidateDashboardCache).mockRejectedValueOnce(new Error("redis unavailable")); vi.mocked(checkBudgetThresholds).mockRejectedValueOnce(new Error("budget alerts unavailable")); vi.mocked(generateAutoSuggestions).mockRejectedValueOnce(new Error("auto suggestions unavailable")); const createdDemandRequirement = { id: "demand_safe_1", projectId: "project_1", startDate: new Date("2026-04-01"), endDate: new Date("2026-04-15"), hoursPerDay: 6, percentage: 75, role: "Designer", roleId: "role_design", headcount: 2, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_design", name: "Designer", color: "#0099FF" }, }; const db = createDemandWorkflowDb({ demandRequirement: { create: vi.fn().mockResolvedValue(createdDemandRequirement), }, }) as Record; Object.assign(db, { $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }); const caller = createManagerCaller(db); const result = await caller.createDemand({ projectId: "project_1", role: "Designer", roleId: "role_design", headcount: 2, hoursPerDay: 6, startDate: new Date("2026-04-01"), endDate: new Date("2026-04-15"), }); await Promise.resolve(); await Promise.resolve(); expect(result.id).toBe("demand_safe_1"); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "invalidateDashboardCache" }), "Allocation background side effect failed", ); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "checkBudgetThresholds", projectId: "project_1" }), "Allocation background side effect failed", ); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "generateAutoSuggestions", demandRequirementId: "demand_safe_1" }), "Allocation background side effect failed", ); }); it("logs and swallows background webhook failures during allocation creation", async () => { vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook unavailable")); const createdAssignment = { id: "assignment_safe_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 40000, status: AllocationStatus.ACTIVE, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000, }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: null, }; const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1" }), }, resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }), }, allocation: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn(), }, assignment: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(createdAssignment), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.create({ resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", status: AllocationStatus.ACTIVE, metadata: {}, }); await Promise.resolve(); await Promise.resolve(); expect(result.id).toBe("assignment_safe_1"); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "dispatchWebhooks", event: "allocation.created" }), "Allocation background side effect failed", ); }); it("creates an explicit assignment without dual-writing a legacy allocation row", async () => { vi.mocked(emitAllocationCreated).mockClear(); const createdAssignment = { id: "assignment_explicit_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 40000, status: AllocationStatus.ACTIVE, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000, }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: null, }; const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1" }), }, resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }), }, allocation: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn(), }, assignment: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(createdAssignment), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.createAssignment({ resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", status: AllocationStatus.ACTIVE, metadata: {}, }); expect(result.id).toBe("assignment_explicit_1"); expect(db.allocation.create).not.toHaveBeenCalled(); expect(db.assignment.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ resourceId: "resource_1", }), }), ); expect(emitAllocationCreated).toHaveBeenCalledWith({ id: "assignment_explicit_1", projectId: "project_1", resourceId: "resource_1", }); }); it("assigns a resource to demand and returns the hydrated demand view", async () => { const demandView = { id: "demand_1", projectId: "project_1", startDate: new Date("2026-05-01T00:00:00.000Z"), endDate: new Date("2026-05-15T00:00:00.000Z"), hoursPerDay: 6, percentage: 75, role: "Designer", roleId: "role_1", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_1", name: "Designer", color: "#00AAFF" }, assignments: [], }; const createdAssignment = { id: "assignment_1", demandRequirementId: "demand_1", resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-05-01T00:00:00.000Z"), endDate: new Date("2026-05-15T00:00:00.000Z"), hoursPerDay: 6, percentage: 75, role: "Designer", roleId: "role_1", dailyCostCents: 42000, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 7000, }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_1", name: "Designer", color: "#00AAFF" }, demandRequirement: demandView, }; const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1" }), }, resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", lcrCents: 7000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }), }, demandRequirement: { findUnique: vi.fn() .mockResolvedValueOnce({ id: "demand_1", projectId: "project_1", startDate: new Date("2026-05-01T00:00:00.000Z"), endDate: new Date("2026-05-15T00:00:00.000Z"), hoursPerDay: 6, role: "Designer", roleId: "role_1", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, }) .mockResolvedValueOnce({ id: "demand_1", projectId: "project_1", }) .mockResolvedValueOnce(demandView), update: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1", headcount: 1, status: AllocationStatus.COMPLETED, }), }, assignment: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(createdAssignment), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.assignResourceToDemand({ demandRequirementId: "demand_1", resourceId: "resource_1", }); expect(result.assignment.id).toBe("assignment_1"); expect(result.demandRequirement.project.shortCode).toBe("PRJ"); expect(result.demandRequirement.roleEntity?.name).toBe("Designer"); expect(db.assignment.create).toHaveBeenCalledTimes(1); }); it("deletes an explicit demand requirement without routing through allocation.delete", async () => { vi.mocked(emitAllocationDeleted).mockClear(); const existingDemandRequirement = { id: "demand_explicit_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 2, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ", status: "ACTIVE", endDate: new Date("2026-03-20"), }, roleEntity: { id: "role_fx", name: "FX Artist", color: "#222222" }, assignments: [], }; const db = { demandRequirement: { findUnique: vi.fn().mockResolvedValue(existingDemandRequirement), delete: vi.fn().mockResolvedValue(existingDemandRequirement), }, assignment: { updateMany: vi.fn().mockResolvedValue({ count: 0 }), }, allocation: { findUnique: vi.fn().mockResolvedValue(null), delete: vi.fn(), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.deleteDemandRequirement({ id: "demand_explicit_1" }); expect(result).toEqual({ success: true }); expect(db.assignment.updateMany).toHaveBeenCalledWith({ where: { demandRequirementId: "demand_explicit_1" }, data: { demandRequirementId: null }, }); expect(db.demandRequirement.delete).toHaveBeenCalledWith({ where: { id: "demand_explicit_1" }, }); expect(db.allocation.delete).not.toHaveBeenCalled(); expect(emitAllocationDeleted).toHaveBeenCalledWith("demand_explicit_1", "project_1"); }); it("deletes an explicit assignment without routing through allocation.delete", async () => { vi.mocked(emitAllocationDeleted).mockClear(); const existingAssignment = { id: "assignment_explicit_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 40000, status: AllocationStatus.ACTIVE, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000, }, project: { id: "project_1", name: "Project One", shortCode: "PRJ", status: "ACTIVE", endDate: new Date("2026-03-20"), }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: null, }; const db = { assignment: { findUnique: vi.fn().mockResolvedValue(existingAssignment), delete: vi.fn().mockResolvedValue(existingAssignment), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.deleteAssignment({ id: "assignment_explicit_1" }); expect(result).toEqual({ success: true }); expect(db.assignment.delete).toHaveBeenCalledWith({ where: { id: "assignment_explicit_1" }, }); expect(emitAllocationDeleted).toHaveBeenCalledWith("assignment_explicit_1", "project_1", "resource_1"); }); it("updates an explicit demand row through allocation.update", async () => { const existingDemand = { id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_fx", name: "FX Artist", color: "#222222" }, }; const updatedDemand = { ...existingDemand, headcount: 2, status: AllocationStatus.CONFIRMED, metadata: { source: "router-test" }, updatedAt: new Date("2026-03-14"), }; const db = { allocation: { findUnique: vi.fn().mockResolvedValue(null), }, demandRequirement: { findUnique: vi.fn().mockResolvedValue(existingDemand), update: vi.fn().mockResolvedValue(updatedDemand), }, assignment: { findUnique: vi.fn().mockResolvedValue(null), update: vi.fn(), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.update({ id: "demand_1", data: { headcount: 2, status: AllocationStatus.CONFIRMED, metadata: { source: "router-test" }, }, }); expect(result.id).toBe("demand_1"); expect(result.isPlaceholder).toBe(true); expect(result.headcount).toBe(2); expect(db.demandRequirement.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "demand_1" }, }), ); }); it("updates a demand row by its direct id", async () => { const existingDemand = { id: "demand_stale", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_fx", name: "FX Artist", color: "#222222" }, }; const updatedDemand = { ...existingDemand, headcount: 2, status: AllocationStatus.CONFIRMED, updatedAt: new Date("2026-03-14"), }; const db = { demandRequirement: { findUnique: vi.fn().mockImplementation( ({ where }: { where: { id?: string } }) => { if (where.id === "demand_stale") { return existingDemand; } return null; }, ), update: vi.fn().mockResolvedValue(updatedDemand), }, assignment: { findUnique: vi.fn().mockResolvedValue(null), update: vi.fn(), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.update({ id: "demand_stale", data: { headcount: 2, status: AllocationStatus.CONFIRMED, }, }); expect(result.id).toBe("demand_stale"); expect(result.isPlaceholder).toBe(true); expect(db.demandRequirement.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "demand_stale" }, }), ); }); it("batch deletes explicit demand and assignment rows through allocation.batchDelete", async () => { const explicitDemand = { id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_fx", name: "FX Artist", color: "#222222" }, }; const explicitAssignment = { id: "assignment_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 32000, status: AllocationStatus.ACTIVE, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000 }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: null, }; const db = { allocation: { findUnique: vi.fn().mockResolvedValue(null), }, demandRequirement: { findUnique: vi.fn().mockImplementation(({ where }: { where: { id: string } }) => where.id === "demand_1" ? explicitDemand : null, ), delete: vi.fn().mockResolvedValue({}), }, assignment: { findUnique: vi.fn().mockImplementation(({ where }: { where: { id: string } }) => where.id === "assignment_1" ? explicitAssignment : null, ), updateMany: vi.fn().mockResolvedValue({ count: 0 }), delete: vi.fn().mockResolvedValue({}), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.batchDelete({ ids: ["demand_1", "assignment_1"], }); expect(result.count).toBe(2); expect(db.assignment.updateMany).toHaveBeenCalledWith({ where: { demandRequirementId: "demand_1" }, data: { demandRequirementId: null }, }); expect(db.demandRequirement.delete).toHaveBeenCalledWith({ where: { id: "demand_1" }, }); expect(db.assignment.delete).toHaveBeenCalledWith({ where: { id: "assignment_1" }, }); }); it("deletes an assignment through allocation.delete by its direct id", async () => { const existingAssignment = { id: "assignment_stale", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 32000, status: AllocationStatus.ACTIVE, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000 }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: null, }; const db = { demandRequirement: { findUnique: vi.fn().mockResolvedValue(null), }, assignment: { findUnique: vi.fn().mockImplementation( ({ where }: { where: { id?: string } }) => { if (where.id === "assignment_stale") { return existingAssignment; } return null; }, ), delete: vi.fn().mockResolvedValue({}), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.delete({ id: "assignment_stale", }); expect(result).toEqual({ success: true }); expect(db.assignment.delete).toHaveBeenCalledWith({ where: { id: "assignment_stale" }, }); }); });