test(api): cover holiday-aware budget and shoring tools
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||
return {
|
||||
...actual,
|
||||
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
|
||||
listAssignmentBookings: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../lib/audit.js", () => ({
|
||||
createAuditEntry: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
import { getDashboardBudgetForecast } from "@capakraken/application";
|
||||
import { executeTool } from "../router/assistant-tools.js";
|
||||
import { createToolContext } from "./assistant-tools-holiday-capacity-test-helpers.js";
|
||||
|
||||
describe("assistant holiday-aware budget and shoring tools", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getDashboardBudgetForecast).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("returns holiday-aware budget forecast data from the dashboard use-case", async () => {
|
||||
vi.mocked(getDashboardBudgetForecast).mockResolvedValue([
|
||||
{
|
||||
projectId: "project_1",
|
||||
projectName: "Gelddruckmaschine",
|
||||
shortCode: "GDM",
|
||||
budgetCents: 100_000,
|
||||
spentCents: 60_000,
|
||||
burnRate: 5_000,
|
||||
pctUsed: 60,
|
||||
estimatedExhaustionDate: "2026-02-20",
|
||||
derivation: {
|
||||
periodStart: "2026-02-01",
|
||||
periodEnd: "2026-02-28",
|
||||
calendarContextCount: 1,
|
||||
holidayAwareAssignmentCount: 1,
|
||||
fallbackAssignmentCount: 0,
|
||||
baseBurnRateCents: 6_000,
|
||||
adjustedBurnRateCents: 5_000,
|
||||
publicHolidayDayEquivalent: 1,
|
||||
publicHolidayCostDeductionCents: 1_000,
|
||||
absenceDayEquivalent: 0,
|
||||
absenceCostDeductionCents: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const ctx = createToolContext({}, ["viewCosts"]);
|
||||
const result = await executeTool("get_budget_forecast", "{}", ctx);
|
||||
const parsed = JSON.parse(result.content) as {
|
||||
forecasts: Array<{
|
||||
projectName: string;
|
||||
shortCode: string;
|
||||
budgetCents: number;
|
||||
spentCents: number;
|
||||
remainingCents: number;
|
||||
projectedCents: number;
|
||||
burnRateCents: number;
|
||||
burnStatus: string;
|
||||
derivation: {
|
||||
baseBurnRateCents: number;
|
||||
adjustedBurnRateCents: number;
|
||||
publicHolidayCostDeductionCents: number;
|
||||
} | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
expect(parsed.forecasts).toEqual([
|
||||
expect.objectContaining({
|
||||
projectName: "Gelddruckmaschine",
|
||||
shortCode: "GDM",
|
||||
budgetCents: 100_000,
|
||||
spentCents: 60_000,
|
||||
remainingCents: 40_000,
|
||||
projectedCents: 100_000,
|
||||
burnRateCents: 5_000,
|
||||
derivation: expect.objectContaining({
|
||||
baseBurnRateCents: 6_000,
|
||||
adjustedBurnRateCents: 5_000,
|
||||
publicHolidayCostDeductionCents: 1_000,
|
||||
}),
|
||||
burnStatus: "on_track",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses holiday-aware assignment hours for assistant shoring ratio", async () => {
|
||||
const db = {
|
||||
project: {
|
||||
findUnique: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
id: "project_1",
|
||||
name: "Holiday Project",
|
||||
shortCode: "HP",
|
||||
status: "ACTIVE",
|
||||
responsiblePerson: null,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: "project_1",
|
||||
name: "Holiday Project",
|
||||
shoringThreshold: 55,
|
||||
onshoreCountryCode: "DE",
|
||||
}),
|
||||
},
|
||||
assignment: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
resourceId: "res_by",
|
||||
hoursPerDay: 8,
|
||||
startDate: new Date("2026-01-06T00:00:00.000Z"),
|
||||
endDate: new Date("2026-01-06T00:00:00.000Z"),
|
||||
resource: {
|
||||
id: "res_by",
|
||||
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
|
||||
countryId: "country_de",
|
||||
federalState: "BY",
|
||||
metroCityId: null,
|
||||
country: { code: "DE" },
|
||||
metroCity: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
resourceId: "res_in",
|
||||
hoursPerDay: 8,
|
||||
startDate: new Date("2026-01-06T00:00:00.000Z"),
|
||||
endDate: new Date("2026-01-06T00:00:00.000Z"),
|
||||
resource: {
|
||||
id: "res_in",
|
||||
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
|
||||
countryId: "country_in",
|
||||
federalState: null,
|
||||
metroCityId: null,
|
||||
country: { code: "IN" },
|
||||
metroCity: null,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
vacation: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
};
|
||||
const ctx = createToolContext(db);
|
||||
|
||||
const result = await executeTool(
|
||||
"get_shoring_ratio",
|
||||
JSON.stringify({ projectId: "project_1" }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result.content).toContain("0% onshore (DE), 100% offshore");
|
||||
expect(result.content).toContain("IN 100% (1 people)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
|
||||
import type { ToolContext } from "../router/assistant-tools.js";
|
||||
|
||||
export function createToolContext(
|
||||
db: Record<string, unknown>,
|
||||
permissions: string[] = [],
|
||||
userRole: SystemRole = SystemRole.ADMIN,
|
||||
): ToolContext {
|
||||
return {
|
||||
db: db as ToolContext["db"],
|
||||
userId: "user_1",
|
||||
userRole,
|
||||
permissions: new Set(permissions) as ToolContext["permissions"],
|
||||
session: {
|
||||
user: { email: "assistant@example.com", name: "Assistant User", image: null },
|
||||
expires: "2026-03-29T00:00:00.000Z",
|
||||
},
|
||||
dbUser: {
|
||||
id: "user_1",
|
||||
systemRole: userRole,
|
||||
permissionOverrides: null,
|
||||
},
|
||||
roleDefaults: null,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user