import { AllocationStatus } from "@nexus/shared"; import { TRPCError } from "@trpc/server"; import { describe, expect, it, vi } from "vitest"; import { fillDemandRequirement } from "../index.js"; // Minimal assignment shape returned from createAssignment inside the transaction function makeAssignment(overrides: Record = {}) { return { id: "assignment_1", demandRequirementId: "demand_1", resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-27"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 40000, 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: 5000 }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: { id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-27"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", headcount: 1, status: AllocationStatus.PROPOSED, }, ...overrides, }; } function makeDb( demandOverride: Record = {}, txOverrides: Record = {}, ) { const demand = { id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-27"), hoursPerDay: 8, role: "Compositor", roleId: "role_comp", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, ...demandOverride, }; const assignmentCreate = vi.fn().mockResolvedValue(makeAssignment()); const demandRequirementUpdate = vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1", headcount: 0, status: AllocationStatus.COMPLETED, }); const auditLogCreate = vi.fn().mockResolvedValue({}); const vacationFindMany = vi.fn().mockResolvedValue([]); const assignmentFindMany = vi.fn().mockResolvedValue([]); const resourceFindUnique = vi.fn().mockResolvedValue({ id: "resource_1", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }); const projectFindUnique = vi.fn().mockResolvedValue({ id: "project_1" }); return { demandRequirement: { findUnique: vi.fn().mockResolvedValue(demand), }, assignment: { findMany: assignmentFindMany }, $transaction: vi.fn(async (callback: (tx: unknown) => Promise) => callback({ project: { findUnique: projectFindUnique }, resource: { findUnique: resourceFindUnique }, demandRequirement: { findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1" }), update: demandRequirementUpdate, }, assignment: { findMany: assignmentFindMany, create: assignmentCreate, }, vacation: { findMany: vacationFindMany }, auditLog: { create: auditLogCreate }, ...txOverrides, }), ), _assignmentCreate: assignmentCreate, _demandRequirementUpdate: demandRequirementUpdate, }; } describe("fillDemandRequirement", () => { it("happy path: fills an open demand with a valid resource assignment", async () => { const db = makeDb(); const result = await fillDemandRequirement(db as never, { demandRequirementId: "demand_1", resourceId: "resource_1", }); expect(result.assignment.id).toBe("assignment_1"); expect(result.assignment.resourceId).toBe("resource_1"); expect(result.updatedDemandRequirement.id).toBe("demand_1"); expect(db._assignmentCreate).toHaveBeenCalledOnce(); expect(db._demandRequirementUpdate).toHaveBeenCalledOnce(); }); it("throws NOT_FOUND when the demand requirement does not exist", async () => { const db = { demandRequirement: { findUnique: vi.fn().mockResolvedValue(null) }, assignment: { findMany: vi.fn().mockResolvedValue([]) }, $transaction: vi.fn(), }; await expect( fillDemandRequirement(db as never, { demandRequirementId: "nonexistent", resourceId: "resource_1", }), ).rejects.toThrow(TRPCError); await expect( fillDemandRequirement(db as never, { demandRequirementId: "nonexistent", resourceId: "resource_1", }), ).rejects.toMatchObject({ code: "NOT_FOUND" }); }); it("throws BAD_REQUEST when demand requirement is CANCELLED", async () => { const db = makeDb({ status: AllocationStatus.CANCELLED }); await expect( fillDemandRequirement(db as never, { demandRequirementId: "demand_1", resourceId: "resource_1", }), ).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("cancelled") }); // No transaction should have been initiated expect(db.$transaction).not.toHaveBeenCalled(); }); it("throws BAD_REQUEST when demand requirement is already COMPLETED", async () => { const db = makeDb({ status: AllocationStatus.COMPLETED }); await expect( fillDemandRequirement(db as never, { demandRequirementId: "demand_1", resourceId: "resource_1", }), ).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("completed") }); expect(db.$transaction).not.toHaveBeenCalled(); }); it("throws CONFLICT when the same resource is already assigned to the same project with overlapping dates", async () => { // assignment.findMany returns an existing booking for the same resource+project+dates const existingBooking = { id: "assignment_existing", projectId: "project_1", resourceId: "resource_1", startDate: new Date("2026-03-10"), endDate: new Date("2026-03-20"), hoursPerDay: 8, dailyCostCents: 40000, status: AllocationStatus.CONFIRMED, project: { id: "project_1", name: "Project One", shortCode: "PRJ", status: "ACTIVE", orderType: "EXTERNAL", clientId: null, dynamicFields: null, }, resource: { id: "resource_1", displayName: "Alice", chapter: null }, }; const db = { demandRequirement: { findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-27"), hoursPerDay: 8, role: "Compositor", roleId: "role_comp", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, }), }, assignment: { findMany: vi.fn().mockResolvedValue([existingBooking]), }, $transaction: vi.fn(), }; await expect( fillDemandRequirement(db as never, { demandRequirementId: "demand_1", resourceId: "resource_1", }), ).rejects.toMatchObject({ code: "CONFLICT" }); expect(db.$transaction).not.toHaveBeenCalled(); }); it("allows filling when existing assignment is for a different project (no duplicate)", async () => { const differentProjectBooking = { id: "assignment_other", projectId: "project_2", // different project — should not trigger duplicate check resourceId: "resource_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-27"), hoursPerDay: 8, dailyCostCents: 40000, status: AllocationStatus.CONFIRMED, project: { id: "project_2", name: "Project Two", shortCode: "PT2", status: "ACTIVE", orderType: "EXTERNAL", clientId: null, dynamicFields: null, }, resource: { id: "resource_1", displayName: "Alice", chapter: null }, }; // Outer findMany (duplicate check in fillDemandRequirement) sees the different-project booking. // Inside the transaction, createAssignment calls its own listAssignmentBookings — that must // return empty so the availability validator doesn't see an overallocation. const outerFindMany = vi.fn().mockResolvedValue([differentProjectBooking]); const txFindMany = vi.fn().mockResolvedValue([]); const assignmentCreate = vi.fn().mockResolvedValue(makeAssignment()); const demandRequirementUpdate = vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1", headcount: 0, status: AllocationStatus.COMPLETED, }); const db = { demandRequirement: { findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-27"), hoursPerDay: 8, role: "Compositor", roleId: "role_comp", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, }), }, assignment: { findMany: outerFindMany }, $transaction: vi.fn(async (callback: (tx: unknown) => Promise) => callback({ 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, }, }), }, demandRequirement: { findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1" }), update: demandRequirementUpdate, }, assignment: { findMany: txFindMany, create: assignmentCreate }, vacation: { findMany: vi.fn().mockResolvedValue([]) }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }), ), }; const result = await fillDemandRequirement(db as never, { demandRequirementId: "demand_1", resourceId: "resource_1", }); expect(result.assignment.id).toBe("assignment_1"); }); it("uses input hoursPerDay when provided, overriding demand default", async () => { const db = makeDb(); await fillDemandRequirement(db as never, { demandRequirementId: "demand_1", resourceId: "resource_1", hoursPerDay: 4, }); // createAssignment is called inside the transaction — verify it received hoursPerDay: 4 expect(db._assignmentCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ hoursPerDay: 4 }), }), ); }); it("decrement headcount when demand has multiple headcount (headcount > 1)", async () => { const demandRequirementUpdate = vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1", headcount: 1, status: AllocationStatus.PROPOSED, }); const db = { demandRequirement: { findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-27"), hoursPerDay: 8, role: "Compositor", roleId: "role_comp", headcount: 2, status: AllocationStatus.PROPOSED, metadata: {}, }), }, assignment: { findMany: vi.fn().mockResolvedValue([]) }, $transaction: vi.fn(async (callback: (tx: unknown) => Promise) => callback({ 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, }, }), }, demandRequirement: { findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1" }), update: demandRequirementUpdate, }, assignment: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(makeAssignment()), }, vacation: { findMany: vi.fn().mockResolvedValue([]) }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }), ), }; const result = await fillDemandRequirement(db as never, { demandRequirementId: "demand_1", resourceId: "resource_1", }); expect(result.updatedDemandRequirement.headcount).toBe(1); expect(result.updatedDemandRequirement.status).toBe(AllocationStatus.PROPOSED); expect(demandRequirementUpdate).toHaveBeenCalledWith( expect.objectContaining({ data: { headcount: 1 }, }), ); }); it("marks demand COMPLETED when it is the last headcount seat", async () => { const db = makeDb({ headcount: 1 }); const result = await fillDemandRequirement(db as never, { demandRequirementId: "demand_1", resourceId: "resource_1", }); expect(result.updatedDemandRequirement.status).toBe(AllocationStatus.COMPLETED); expect(db._demandRequirementUpdate).toHaveBeenCalledWith( expect.objectContaining({ data: { status: AllocationStatus.COMPLETED }, }), ); }); });