refactor(api): extract timeline shift procedure support
This commit is contained in:
@@ -0,0 +1,231 @@
|
|||||||
|
import type { ShiftValidationResult } from "@capakraken/shared";
|
||||||
|
import { AllocationStatus } from "@capakraken/shared";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const {
|
||||||
|
calculateAllocationMock,
|
||||||
|
updateAssignmentMock,
|
||||||
|
updateDemandRequirementMock,
|
||||||
|
validateShiftMock,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
validateShiftMock: vi.fn(),
|
||||||
|
calculateAllocationMock: vi.fn(),
|
||||||
|
updateAssignmentMock: vi.fn(),
|
||||||
|
updateDemandRequirementMock: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@capakraken/engine", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@capakraken/engine")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
validateShift: validateShiftMock,
|
||||||
|
calculateAllocation: calculateAllocationMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
updateAssignment: updateAssignmentMock,
|
||||||
|
updateDemandRequirement: updateDemandRequirementMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { buildTimelineProjectShiftEventPayload } from "../router/timeline-shift-support.js";
|
||||||
|
import { applyTimelineProjectShift } from "../router/timeline-shift-procedure-support.js";
|
||||||
|
|
||||||
|
function createValidValidationResult(
|
||||||
|
overrides: Partial<ShiftValidationResult> = {},
|
||||||
|
): ShiftValidationResult {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
conflictDetails: [],
|
||||||
|
costImpact: {
|
||||||
|
currentTotalCents: 100_000,
|
||||||
|
newTotalCents: 112_000,
|
||||||
|
deltaCents: 12_000,
|
||||||
|
budgetCents: 200_000,
|
||||||
|
budgetUtilizationBefore: 50,
|
||||||
|
budgetUtilizationAfter: 56,
|
||||||
|
wouldExceedBudget: false,
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("timeline shift procedure support", () => {
|
||||||
|
it("applies project shift updates and recalculates only staffed assignments", async () => {
|
||||||
|
const validation = createValidValidationResult();
|
||||||
|
validateShiftMock.mockReturnValue(validation);
|
||||||
|
calculateAllocationMock.mockReturnValue({ dailyCostCents: 54_321 });
|
||||||
|
updateDemandRequirementMock.mockResolvedValue({});
|
||||||
|
updateAssignmentMock.mockResolvedValue({});
|
||||||
|
|
||||||
|
const updatedProject = {
|
||||||
|
id: "project_1",
|
||||||
|
startDate: new Date("2026-04-03"),
|
||||||
|
endDate: new Date("2026-04-12"),
|
||||||
|
};
|
||||||
|
const db = {
|
||||||
|
project: {
|
||||||
|
update: vi.fn().mockResolvedValue(updatedProject),
|
||||||
|
},
|
||||||
|
auditLog: {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
vacation: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
calculationRule: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([{ id: "rule_1", priority: 1 }]),
|
||||||
|
},
|
||||||
|
$transaction: vi.fn(async (callback: (tx: typeof db) => unknown) => callback(db)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
project: {
|
||||||
|
id: "project_1",
|
||||||
|
budgetCents: 200_000,
|
||||||
|
winProbability: 80,
|
||||||
|
startDate: new Date("2026-04-01"),
|
||||||
|
endDate: new Date("2026-04-10"),
|
||||||
|
},
|
||||||
|
demandRequirements: [
|
||||||
|
{
|
||||||
|
id: "demand_1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: "assignment_staffed",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
hoursPerDay: 8,
|
||||||
|
metadata: { includeSaturday: true },
|
||||||
|
resource: {
|
||||||
|
lcrCents: 5_000,
|
||||||
|
availability: {
|
||||||
|
monday: 8,
|
||||||
|
tuesday: 8,
|
||||||
|
wednesday: 8,
|
||||||
|
thursday: 8,
|
||||||
|
friday: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "assignment_placeholder",
|
||||||
|
resourceId: null,
|
||||||
|
hoursPerDay: 4,
|
||||||
|
metadata: {},
|
||||||
|
resource: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
shiftPlan: {
|
||||||
|
validationAllocations: [
|
||||||
|
{
|
||||||
|
id: "assignment_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-04-01"),
|
||||||
|
endDate: new Date("2026-04-05"),
|
||||||
|
hoursPerDay: 8,
|
||||||
|
percentage: 100,
|
||||||
|
role: "Compositing",
|
||||||
|
dailyCostCents: 40_000,
|
||||||
|
status: AllocationStatus.ACTIVE,
|
||||||
|
resource: {
|
||||||
|
id: "resource_1",
|
||||||
|
displayName: "Alice",
|
||||||
|
lcrCents: 5_000,
|
||||||
|
availability: {
|
||||||
|
monday: 8,
|
||||||
|
tuesday: 8,
|
||||||
|
wednesday: 8,
|
||||||
|
thursday: 8,
|
||||||
|
friday: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allAllocationsForResource: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await applyTimelineProjectShift({
|
||||||
|
db: db as never,
|
||||||
|
projectId: "project_1",
|
||||||
|
newStartDate: new Date("2026-04-03"),
|
||||||
|
newEndDate: new Date("2026-04-12"),
|
||||||
|
context: context as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(db.project.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: "project_1" },
|
||||||
|
data: {
|
||||||
|
startDate: new Date("2026-04-03"),
|
||||||
|
endDate: new Date("2026-04-12"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(updateDemandRequirementMock).toHaveBeenCalledWith(
|
||||||
|
db,
|
||||||
|
"demand_1",
|
||||||
|
{
|
||||||
|
startDate: new Date("2026-04-03"),
|
||||||
|
endDate: new Date("2026-04-12"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(db.vacation.findMany).toHaveBeenCalledTimes(1);
|
||||||
|
expect(db.calculationRule.findMany).toHaveBeenCalledTimes(1);
|
||||||
|
expect(calculateAllocationMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updateAssignmentMock).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
db,
|
||||||
|
"assignment_staffed",
|
||||||
|
{
|
||||||
|
startDate: new Date("2026-04-03"),
|
||||||
|
endDate: new Date("2026-04-12"),
|
||||||
|
dailyCostCents: 54_321,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(updateAssignmentMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
db,
|
||||||
|
"assignment_placeholder",
|
||||||
|
{
|
||||||
|
startDate: new Date("2026-04-03"),
|
||||||
|
endDate: new Date("2026-04-12"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(db.auditLog.create).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
entityType: "Project",
|
||||||
|
entityId: "project_1",
|
||||||
|
action: "SHIFT",
|
||||||
|
changes: expect.objectContaining({
|
||||||
|
before: {
|
||||||
|
startDate: new Date("2026-04-01"),
|
||||||
|
endDate: new Date("2026-04-10"),
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
startDate: new Date("2026-04-03"),
|
||||||
|
endDate: new Date("2026-04-12"),
|
||||||
|
},
|
||||||
|
costImpact: validation.costImpact,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
project: updatedProject,
|
||||||
|
validation,
|
||||||
|
event: buildTimelineProjectShiftEventPayload({
|
||||||
|
projectId: "project_1",
|
||||||
|
newStartDate: new Date("2026-04-03"),
|
||||||
|
newEndDate: new Date("2026-04-12"),
|
||||||
|
validation,
|
||||||
|
assignments: context.assignments as never,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +1,9 @@
|
|||||||
import type { ShiftValidationResult } from "@capakraken/shared";
|
import type { ShiftValidationResult } from "@capakraken/shared";
|
||||||
import { AllocationStatus } from "@capakraken/shared";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const {
|
const { validateShiftMock } = vi.hoisted(() => ({
|
||||||
calculateAllocationMock,
|
|
||||||
updateAssignmentMock,
|
|
||||||
updateDemandRequirementMock,
|
|
||||||
validateShiftMock,
|
|
||||||
} = vi.hoisted(() => ({
|
|
||||||
validateShiftMock: vi.fn(),
|
validateShiftMock: vi.fn(),
|
||||||
calculateAllocationMock: vi.fn(),
|
|
||||||
updateAssignmentMock: vi.fn(),
|
|
||||||
updateDemandRequirementMock: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@capakraken/engine", async (importOriginal) => {
|
vi.mock("@capakraken/engine", async (importOriginal) => {
|
||||||
@@ -20,21 +11,10 @@ vi.mock("@capakraken/engine", async (importOriginal) => {
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
validateShift: validateShiftMock,
|
validateShift: validateShiftMock,
|
||||||
calculateAllocation: calculateAllocationMock,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("@capakraken/application", async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
updateAssignment: updateAssignmentMock,
|
|
||||||
updateDemandRequirement: updateDemandRequirementMock,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import {
|
import {
|
||||||
applyTimelineProjectShift,
|
|
||||||
assertTimelineProjectShiftValid,
|
assertTimelineProjectShiftValid,
|
||||||
buildTimelineProjectShiftEventPayload,
|
buildTimelineProjectShiftEventPayload,
|
||||||
buildTimelineProjectShiftValidation,
|
buildTimelineProjectShiftValidation,
|
||||||
@@ -66,27 +46,6 @@ describe("timeline shift support", () => {
|
|||||||
const expectedValidation = createValidValidationResult();
|
const expectedValidation = createValidValidationResult();
|
||||||
validateShiftMock.mockReturnValue(expectedValidation);
|
validateShiftMock.mockReturnValue(expectedValidation);
|
||||||
|
|
||||||
const validationAllocations = [
|
|
||||||
{
|
|
||||||
id: "assignment_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
startDate: new Date("2026-04-01"),
|
|
||||||
endDate: new Date("2026-04-05"),
|
|
||||||
hoursPerDay: 8,
|
|
||||||
percentage: 100,
|
|
||||||
role: "Compositing",
|
|
||||||
dailyCostCents: 40_000,
|
|
||||||
status: AllocationStatus.ACTIVE,
|
|
||||||
resource: {
|
|
||||||
id: "resource_1",
|
|
||||||
displayName: "Alice",
|
|
||||||
lcrCents: 5_000,
|
|
||||||
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
|
|
||||||
},
|
|
||||||
allAllocationsForResource: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = buildTimelineProjectShiftValidation({
|
const result = buildTimelineProjectShiftValidation({
|
||||||
context: {
|
context: {
|
||||||
project: {
|
project: {
|
||||||
@@ -99,7 +58,31 @@ describe("timeline shift support", () => {
|
|||||||
demandRequirements: [],
|
demandRequirements: [],
|
||||||
assignments: [],
|
assignments: [],
|
||||||
shiftPlan: {
|
shiftPlan: {
|
||||||
validationAllocations,
|
validationAllocations: [
|
||||||
|
{
|
||||||
|
id: "assignment_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-04-01"),
|
||||||
|
endDate: new Date("2026-04-05"),
|
||||||
|
hoursPerDay: 8,
|
||||||
|
percentage: 100,
|
||||||
|
role: "Compositing",
|
||||||
|
dailyCostCents: 40_000,
|
||||||
|
resource: {
|
||||||
|
id: "resource_1",
|
||||||
|
displayName: "Alice",
|
||||||
|
lcrCents: 5_000,
|
||||||
|
availability: {
|
||||||
|
monday: 8,
|
||||||
|
tuesday: 8,
|
||||||
|
wednesday: 8,
|
||||||
|
thursday: 8,
|
||||||
|
friday: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allAllocationsForResource: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
newStartDate: new Date("2026-04-03"),
|
newStartDate: new Date("2026-04-03"),
|
||||||
@@ -117,7 +100,12 @@ describe("timeline shift support", () => {
|
|||||||
},
|
},
|
||||||
newStartDate: new Date("2026-04-03"),
|
newStartDate: new Date("2026-04-03"),
|
||||||
newEndDate: new Date("2026-04-12"),
|
newEndDate: new Date("2026-04-12"),
|
||||||
allocations: validationAllocations,
|
allocations: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "assignment_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,150 +126,26 @@ describe("timeline shift support", () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies project shift updates and recalculates only staffed assignments", async () => {
|
it("builds project shifted event payloads", () => {
|
||||||
const validation = createValidValidationResult();
|
const validation = createValidValidationResult();
|
||||||
validateShiftMock.mockReturnValue(validation);
|
|
||||||
calculateAllocationMock.mockReturnValue({ dailyCostCents: 54_321 });
|
|
||||||
updateDemandRequirementMock.mockResolvedValue({});
|
|
||||||
updateAssignmentMock.mockResolvedValue({});
|
|
||||||
|
|
||||||
const updatedProject = {
|
expect(
|
||||||
id: "project_1",
|
buildTimelineProjectShiftEventPayload({
|
||||||
startDate: new Date("2026-04-03"),
|
projectId: "project_1",
|
||||||
endDate: new Date("2026-04-12"),
|
newStartDate: new Date("2026-04-03T00:00:00.000Z"),
|
||||||
};
|
newEndDate: new Date("2026-04-12T00:00:00.000Z"),
|
||||||
const db = {
|
validation,
|
||||||
project: {
|
|
||||||
update: vi.fn().mockResolvedValue(updatedProject),
|
|
||||||
},
|
|
||||||
auditLog: {
|
|
||||||
create: vi.fn().mockResolvedValue({}),
|
|
||||||
},
|
|
||||||
vacation: {
|
|
||||||
findMany: vi.fn().mockResolvedValue([]),
|
|
||||||
},
|
|
||||||
calculationRule: {
|
|
||||||
findMany: vi.fn().mockResolvedValue([{ id: "rule_1", priority: 1 }]),
|
|
||||||
},
|
|
||||||
$transaction: vi.fn(async (callback: (tx: typeof db) => unknown) => callback(db)),
|
|
||||||
};
|
|
||||||
|
|
||||||
const context = {
|
|
||||||
project: {
|
|
||||||
id: "project_1",
|
|
||||||
budgetCents: 200_000,
|
|
||||||
winProbability: 80,
|
|
||||||
startDate: new Date("2026-04-01"),
|
|
||||||
endDate: new Date("2026-04-10"),
|
|
||||||
},
|
|
||||||
demandRequirements: [
|
|
||||||
{
|
|
||||||
id: "demand_1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
assignments: [
|
assignments: [
|
||||||
{
|
{ resourceId: "resource_1" },
|
||||||
id: "assignment_staffed",
|
{ resourceId: null },
|
||||||
resourceId: "resource_1",
|
] as never,
|
||||||
hoursPerDay: 8,
|
|
||||||
metadata: { includeSaturday: true },
|
|
||||||
resource: {
|
|
||||||
lcrCents: 5_000,
|
|
||||||
availability: {
|
|
||||||
monday: 8,
|
|
||||||
tuesday: 8,
|
|
||||||
wednesday: 8,
|
|
||||||
thursday: 8,
|
|
||||||
friday: 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "assignment_placeholder",
|
|
||||||
resourceId: null,
|
|
||||||
hoursPerDay: 4,
|
|
||||||
metadata: {},
|
|
||||||
resource: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
shiftPlan: {
|
|
||||||
validationAllocations: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await applyTimelineProjectShift({
|
|
||||||
db: db as never,
|
|
||||||
projectId: "project_1",
|
|
||||||
newStartDate: new Date("2026-04-03"),
|
|
||||||
newEndDate: new Date("2026-04-12"),
|
|
||||||
context: context as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(db.project.update).toHaveBeenCalledWith({
|
|
||||||
where: { id: "project_1" },
|
|
||||||
data: {
|
|
||||||
startDate: new Date("2026-04-03"),
|
|
||||||
endDate: new Date("2026-04-12"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(updateDemandRequirementMock).toHaveBeenCalledWith(
|
|
||||||
db,
|
|
||||||
"demand_1",
|
|
||||||
{
|
|
||||||
startDate: new Date("2026-04-03"),
|
|
||||||
endDate: new Date("2026-04-12"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(db.vacation.findMany).toHaveBeenCalledTimes(1);
|
|
||||||
expect(db.calculationRule.findMany).toHaveBeenCalledTimes(1);
|
|
||||||
expect(calculateAllocationMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(updateAssignmentMock).toHaveBeenNthCalledWith(
|
|
||||||
1,
|
|
||||||
db,
|
|
||||||
"assignment_staffed",
|
|
||||||
{
|
|
||||||
startDate: new Date("2026-04-03"),
|
|
||||||
endDate: new Date("2026-04-12"),
|
|
||||||
dailyCostCents: 54_321,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(updateAssignmentMock).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
db,
|
|
||||||
"assignment_placeholder",
|
|
||||||
{
|
|
||||||
startDate: new Date("2026-04-03"),
|
|
||||||
endDate: new Date("2026-04-12"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(db.auditLog.create).toHaveBeenCalledWith({
|
|
||||||
data: {
|
|
||||||
entityType: "Project",
|
|
||||||
entityId: "project_1",
|
|
||||||
action: "SHIFT",
|
|
||||||
changes: expect.objectContaining({
|
|
||||||
before: {
|
|
||||||
startDate: new Date("2026-04-01"),
|
|
||||||
endDate: new Date("2026-04-10"),
|
|
||||||
},
|
|
||||||
after: {
|
|
||||||
startDate: new Date("2026-04-03"),
|
|
||||||
endDate: new Date("2026-04-12"),
|
|
||||||
},
|
|
||||||
costImpact: validation.costImpact,
|
|
||||||
}),
|
}),
|
||||||
},
|
).toEqual({
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
project: updatedProject,
|
|
||||||
validation,
|
|
||||||
event: buildTimelineProjectShiftEventPayload({
|
|
||||||
projectId: "project_1",
|
projectId: "project_1",
|
||||||
newStartDate: new Date("2026-04-03"),
|
newStartDate: "2026-04-03T00:00:00.000Z",
|
||||||
newEndDate: new Date("2026-04-12"),
|
newEndDate: "2026-04-12T00:00:00.000Z",
|
||||||
validation,
|
costDeltaCents: 12_000,
|
||||||
assignments: context.assignments as never,
|
resourceIds: ["resource_1", null],
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
updateAssignment,
|
||||||
|
updateDemandRequirement,
|
||||||
|
} from "@capakraken/application";
|
||||||
|
import type { PrismaClient } from "@capakraken/db";
|
||||||
|
import {
|
||||||
|
assertTimelineProjectShiftValid,
|
||||||
|
buildTimelineProjectShiftEventPayload,
|
||||||
|
buildTimelineProjectShiftValidation,
|
||||||
|
type LoadedTimelineShiftContext,
|
||||||
|
} from "./timeline-shift-support.js";
|
||||||
|
import {
|
||||||
|
buildTimelineProjectDateRangeUpdate,
|
||||||
|
buildTimelineProjectShiftAuditChanges,
|
||||||
|
buildTimelineShiftedAssignmentUpdate,
|
||||||
|
recalculateShiftedAssignmentDailyCost,
|
||||||
|
} from "./timeline-shift-mutation-support.js";
|
||||||
|
|
||||||
|
export interface ApplyTimelineProjectShiftInput {
|
||||||
|
db: PrismaClient;
|
||||||
|
projectId: string;
|
||||||
|
newStartDate: Date;
|
||||||
|
newEndDate: Date;
|
||||||
|
context: LoadedTimelineShiftContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyTimelineProjectShift(input: ApplyTimelineProjectShiftInput) {
|
||||||
|
const validation = buildTimelineProjectShiftValidation({
|
||||||
|
context: input.context,
|
||||||
|
newStartDate: input.newStartDate,
|
||||||
|
newEndDate: input.newEndDate,
|
||||||
|
});
|
||||||
|
assertTimelineProjectShiftValid(validation);
|
||||||
|
|
||||||
|
const updatedProject = await input.db.$transaction(async (tx) => {
|
||||||
|
const projectRecord = await tx.project.update({
|
||||||
|
where: { id: input.projectId },
|
||||||
|
data: buildTimelineProjectDateRangeUpdate(input),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const demandRequirement of input.context.demandRequirements) {
|
||||||
|
await updateDemandRequirement(
|
||||||
|
tx as unknown as Parameters<typeof updateDemandRequirement>[0],
|
||||||
|
demandRequirement.id,
|
||||||
|
buildTimelineProjectDateRangeUpdate(input),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const assignment of input.context.assignments) {
|
||||||
|
const dailyCostCents = await recalculateShiftedAssignmentDailyCost({
|
||||||
|
db: input.db,
|
||||||
|
assignment,
|
||||||
|
newStartDate: input.newStartDate,
|
||||||
|
newEndDate: input.newEndDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateAssignment(
|
||||||
|
tx as unknown as Parameters<typeof updateAssignment>[0],
|
||||||
|
assignment.id,
|
||||||
|
buildTimelineShiftedAssignmentUpdate({
|
||||||
|
newStartDate: input.newStartDate,
|
||||||
|
newEndDate: input.newEndDate,
|
||||||
|
dailyCostCents,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.auditLog.create({
|
||||||
|
data: {
|
||||||
|
entityType: "Project",
|
||||||
|
entityId: input.projectId,
|
||||||
|
action: "SHIFT",
|
||||||
|
changes: buildTimelineProjectShiftAuditChanges({
|
||||||
|
project: input.context.project,
|
||||||
|
newStartDate: input.newStartDate,
|
||||||
|
newEndDate: input.newEndDate,
|
||||||
|
validation,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return projectRecord;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: updatedProject,
|
||||||
|
validation,
|
||||||
|
event: buildTimelineProjectShiftEventPayload({
|
||||||
|
projectId: input.projectId,
|
||||||
|
newStartDate: input.newStartDate,
|
||||||
|
newEndDate: input.newEndDate,
|
||||||
|
validation,
|
||||||
|
assignments: input.context.assignments,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,20 +1,11 @@
|
|||||||
import {
|
import type {
|
||||||
updateAssignment,
|
SplitAssignmentRecord,
|
||||||
updateDemandRequirement,
|
SplitDemandRequirementRecord,
|
||||||
type SplitAssignmentRecord,
|
|
||||||
type SplitDemandRequirementRecord,
|
|
||||||
} from "@capakraken/application";
|
} from "@capakraken/application";
|
||||||
import type { PrismaClient } from "@capakraken/db";
|
|
||||||
import { validateShift } from "@capakraken/engine";
|
import { validateShift } from "@capakraken/engine";
|
||||||
import type { ShiftValidationResult } from "@capakraken/shared";
|
import type { ShiftValidationResult } from "@capakraken/shared";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import type { TimelineShiftPlan } from "./timeline-shift-planning.js";
|
import type { TimelineShiftPlan } from "./timeline-shift-planning.js";
|
||||||
import {
|
|
||||||
buildTimelineProjectDateRangeUpdate,
|
|
||||||
buildTimelineProjectShiftAuditChanges,
|
|
||||||
buildTimelineShiftedAssignmentUpdate,
|
|
||||||
recalculateShiftedAssignmentDailyCost,
|
|
||||||
} from "./timeline-shift-mutation-support.js";
|
|
||||||
|
|
||||||
export interface TimelineShiftProjectRecord {
|
export interface TimelineShiftProjectRecord {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,14 +22,6 @@ export interface LoadedTimelineShiftContext {
|
|||||||
shiftPlan: TimelineShiftPlan;
|
shiftPlan: TimelineShiftPlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplyTimelineProjectShiftInput {
|
|
||||||
db: PrismaClient;
|
|
||||||
projectId: string;
|
|
||||||
newStartDate: Date;
|
|
||||||
newEndDate: Date;
|
|
||||||
context: LoadedTimelineShiftContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimelineProjectShiftEventPayload extends Record<string, unknown> {
|
export interface TimelineProjectShiftEventPayload extends Record<string, unknown> {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
newStartDate: string;
|
newStartDate: string;
|
||||||
@@ -96,74 +79,3 @@ export function buildTimelineProjectShiftEventPayload(input: {
|
|||||||
resourceIds: input.assignments.map((assignment) => assignment.resourceId),
|
resourceIds: input.assignments.map((assignment) => assignment.resourceId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyTimelineProjectShift(input: ApplyTimelineProjectShiftInput) {
|
|
||||||
const validation = buildTimelineProjectShiftValidation({
|
|
||||||
context: input.context,
|
|
||||||
newStartDate: input.newStartDate,
|
|
||||||
newEndDate: input.newEndDate,
|
|
||||||
});
|
|
||||||
assertTimelineProjectShiftValid(validation);
|
|
||||||
|
|
||||||
const updatedProject = await input.db.$transaction(async (tx) => {
|
|
||||||
const projectRecord = await tx.project.update({
|
|
||||||
where: { id: input.projectId },
|
|
||||||
data: buildTimelineProjectDateRangeUpdate(input),
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const demandRequirement of input.context.demandRequirements) {
|
|
||||||
await updateDemandRequirement(
|
|
||||||
tx as unknown as Parameters<typeof updateDemandRequirement>[0],
|
|
||||||
demandRequirement.id,
|
|
||||||
buildTimelineProjectDateRangeUpdate(input),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const assignment of input.context.assignments) {
|
|
||||||
const dailyCostCents = await recalculateShiftedAssignmentDailyCost({
|
|
||||||
db: input.db,
|
|
||||||
assignment,
|
|
||||||
newStartDate: input.newStartDate,
|
|
||||||
newEndDate: input.newEndDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateAssignment(
|
|
||||||
tx as unknown as Parameters<typeof updateAssignment>[0],
|
|
||||||
assignment.id,
|
|
||||||
buildTimelineShiftedAssignmentUpdate({
|
|
||||||
newStartDate: input.newStartDate,
|
|
||||||
newEndDate: input.newEndDate,
|
|
||||||
dailyCostCents,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx.auditLog.create({
|
|
||||||
data: {
|
|
||||||
entityType: "Project",
|
|
||||||
entityId: input.projectId,
|
|
||||||
action: "SHIFT",
|
|
||||||
changes: buildTimelineProjectShiftAuditChanges({
|
|
||||||
project: input.context.project,
|
|
||||||
newStartDate: input.newStartDate,
|
|
||||||
newEndDate: input.newEndDate,
|
|
||||||
validation,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return projectRecord;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
project: updatedProject,
|
|
||||||
validation,
|
|
||||||
event: buildTimelineProjectShiftEventPayload({
|
|
||||||
projectId: input.projectId,
|
|
||||||
newStartDate: input.newStartDate,
|
|
||||||
newEndDate: input.newEndDate,
|
|
||||||
validation,
|
|
||||||
assignments: input.context.assignments,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createTRPCRouter, managerProcedure, requirePermission } from "../trpc.j
|
|||||||
import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
|
import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
|
||||||
import { timelineReadProcedures } from "./timeline-read.js";
|
import { timelineReadProcedures } from "./timeline-read.js";
|
||||||
import { loadProjectShiftContext } from "./timeline-project-load-support.js";
|
import { loadProjectShiftContext } from "./timeline-project-load-support.js";
|
||||||
import { applyTimelineProjectShift } from "./timeline-shift-support.js";
|
import { applyTimelineProjectShift } from "./timeline-shift-procedure-support.js";
|
||||||
|
|
||||||
export const timelineRouter = createTRPCRouter({
|
export const timelineRouter = createTRPCRouter({
|
||||||
...timelineReadProcedures,
|
...timelineReadProcedures,
|
||||||
|
|||||||
Reference in New Issue
Block a user