import { TRPCError } from "@trpc/server"; import { describe, expect, it } from "vitest"; import { assertTimelineDateRangeValid, buildTimelineAllocationEntryUpdate, buildTimelineAllocationMetadata, buildTimelineBatchShiftAuditChanges, buildTimelineQuickAssignAssignmentInput, buildTimelineQuickAssignMetadata, calculateTimelineAllocationPercentage, shiftTimelineAllocationWindow, validateTimelineAllocationDateRanges, } from "../router/timeline-allocation-mutation-support.js"; describe("timeline allocation mutation support", () => { it("preserves existing metadata while updating includeSaturday", () => { const result = buildTimelineAllocationMetadata({ existingMetadata: { recurrence: { frequency: "weekly", interval: 2 }, includeSaturday: false, }, includeSaturday: true, }); expect(result).toEqual({ metadata: { recurrence: { frequency: "weekly", interval: 2 }, includeSaturday: true, }, includeSaturday: true, }); }); it("rejects inverted date ranges", () => { expect(() => assertTimelineDateRangeValid( new Date("2026-04-10T00:00:00.000Z"), new Date("2026-04-09T00:00:00.000Z"), )).toThrowError(TRPCError); }); it("rounds allocation percentages and clamps them to 100", () => { expect(calculateTimelineAllocationPercentage(3.9)).toBe(49); expect(calculateTimelineAllocationPercentage(9)).toBe(100); }); it("builds source metadata for quick assign operations", () => { expect(buildTimelineQuickAssignMetadata("quickAssign")).toEqual({ source: "quickAssign" }); expect(buildTimelineQuickAssignMetadata("batchQuickAssign")).toEqual({ source: "batchQuickAssign" }); }); it("builds assignment create payloads for quick assign flows", () => { expect( buildTimelineQuickAssignAssignmentInput({ resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-05T00:00:00.000Z"), hoursPerDay: 6, role: "Engineer", roleId: "role_1", status: "PROPOSED", source: "quickAssign", }), ).toEqual({ resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-05T00:00:00.000Z"), hoursPerDay: 6, percentage: 75, role: "Engineer", roleId: "role_1", status: "PROPOSED", metadata: { source: "quickAssign" }, }); }); it("shifts and clamps allocation windows for each batch-shift mode", () => { expect( shiftTimelineAllocationWindow({ startDate: new Date("2026-04-10T00:00:00.000Z"), endDate: new Date("2026-04-12T00:00:00.000Z"), daysDelta: 2, mode: "move", }), ).toEqual({ startDate: new Date("2026-04-12T00:00:00.000Z"), endDate: new Date("2026-04-14T00:00:00.000Z"), }); expect( shiftTimelineAllocationWindow({ startDate: new Date("2026-04-10T00:00:00.000Z"), endDate: new Date("2026-04-12T00:00:00.000Z"), daysDelta: 5, mode: "resize-start", }), ).toEqual({ startDate: new Date("2026-04-12T00:00:00.000Z"), endDate: new Date("2026-04-12T00:00:00.000Z"), }); expect( shiftTimelineAllocationWindow({ startDate: new Date("2026-04-10T00:00:00.000Z"), endDate: new Date("2026-04-12T00:00:00.000Z"), daysDelta: -5, mode: "resize-end", }), ).toEqual({ startDate: new Date("2026-04-10T00:00:00.000Z"), endDate: new Date("2026-04-10T00:00:00.000Z"), }); }); it("builds batch-shift audit payloads", () => { expect( buildTimelineBatchShiftAuditChanges({ mode: "move", daysDelta: 3, count: 2, }), ).toEqual({ operation: "batchShift", mode: "move", daysDelta: 3, count: 2, }); }); it("validates date ranges in bulk", () => { expect(() => validateTimelineAllocationDateRanges([ { startDate: new Date("2026-04-10T00:00:00.000Z"), endDate: new Date("2026-04-11T00:00:00.000Z"), }, { startDate: new Date("2026-04-12T00:00:00.000Z"), endDate: new Date("2026-04-09T00:00:00.000Z"), }, ])).toThrowError(TRPCError); }); it("builds shared update payloads for demand and assignment changes", () => { expect( buildTimelineAllocationEntryUpdate({ hoursPerDay: 7.5, startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-10T00:00:00.000Z"), metadata: { includeSaturday: true }, dailyCostCents: 48000, role: "Architect", }), ).toEqual({ demandRequirementUpdate: { hoursPerDay: 7.5, startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-10T00:00:00.000Z"), metadata: { includeSaturday: true }, role: "Architect", }, assignmentUpdate: { hoursPerDay: 7.5, startDate: new Date("2026-04-01T00:00:00.000Z"), endDate: new Date("2026-04-10T00:00:00.000Z"), dailyCostCents: 48000, metadata: { includeSaturday: true }, role: "Architect", }, }); }); });