import { AllocationStatus, SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; import { allocationRouter } from "../router/allocation.js"; import { emitAllocationCreated, emitAllocationDeleted, emitNotificationCreated } from "../sse/event-bus.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/webhook-dispatcher.js", () => ({ dispatchWebhooks: vi.fn().mockResolvedValue(undefined), })); const createCaller = createCallerFactory(allocationRouter); 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 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" }, }, ]), }, }; 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("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 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("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"); }); 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" }, }); }); });