refactor(api): extract timeline cost load support

This commit is contained in:
2026-03-31 17:53:59 +02:00
parent eef91a1068
commit ff57fc24ce
4 changed files with 278 additions and 198 deletions
@@ -0,0 +1,88 @@
import { VacationType } from "@capakraken/db";
import { describe, expect, it } from "vitest";
import {
buildTimelineAbsenceDays,
loadTimelineCalculationRules,
} from "../router/timeline-cost-load-support.js";
describe("timeline cost load support", () => {
it("falls back to default calculation rules when the optional model is unavailable", async () => {
const rules = await loadTimelineCalculationRules({
vacation: {
findMany: async () => [],
},
} as never);
expect(rules.length).toBeGreaterThan(0);
});
it("expands approved vacations, sickness, and public holidays into absence days", async () => {
const result = await buildTimelineAbsenceDays({
vacation: {
findMany: async () => [
{
startDate: new Date("2026-04-03T00:00:00.000Z"),
endDate: new Date("2026-04-04T00:00:00.000Z"),
type: VacationType.VACATION,
isHalfDay: false,
},
{
startDate: new Date("2026-04-05T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
type: VacationType.SICK,
isHalfDay: true,
},
{
startDate: new Date("2026-04-06T00:00:00.000Z"),
endDate: new Date("2026-04-06T00:00:00.000Z"),
type: VacationType.PUBLIC_HOLIDAY,
isHalfDay: false,
},
],
},
} as never, "resource_1", new Date("2026-04-01T00:00:00.000Z"), new Date("2026-04-10T00:00:00.000Z"));
expect(result.absenceDays).toEqual([
{
date: new Date("2026-04-03T00:00:00.000Z"),
type: "VACATION",
},
{
date: new Date("2026-04-04T00:00:00.000Z"),
type: "VACATION",
},
{
date: new Date("2026-04-05T00:00:00.000Z"),
type: "SICK",
isHalfDay: true,
},
{
date: new Date("2026-04-06T00:00:00.000Z"),
type: "PUBLIC_HOLIDAY",
},
]);
expect(result.legacyVacationDates).toEqual([
new Date("2026-04-03T00:00:00.000Z"),
new Date("2026-04-04T00:00:00.000Z"),
]);
});
it("treats missing optional vacation tables as no absences", async () => {
const result = await buildTimelineAbsenceDays({
vacation: {
findMany: async () => {
throw {
code: "P2021",
message: "The table `vacations` does not exist.",
meta: { table: "vacations" },
};
},
},
} as never, "resource_1", new Date("2026-04-01T00:00:00.000Z"), new Date("2026-04-10T00:00:00.000Z"));
expect(result).toEqual({
absenceDays: [],
legacyVacationDates: [],
});
});
});
@@ -1,88 +1,75 @@
import { VacationType } from "@capakraken/db";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
const { calculateAllocationMock } = vi.hoisted(() => ({
calculateAllocationMock: vi.fn(),
}));
vi.mock("@capakraken/engine", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/engine")>();
return {
...actual,
calculateAllocation: calculateAllocationMock,
};
});
vi.mock("../router/timeline-cost-load-support.js", () => ({
buildTimelineAbsenceDays: vi.fn(),
loadTimelineCalculationRules: vi.fn(),
}));
import {
buildTimelineAbsenceDays,
loadTimelineCalculationRules,
} from "../router/timeline-cost-support.js";
} from "../router/timeline-cost-load-support.js";
import { calculateTimelineAllocationDailyCost } from "../router/timeline-cost-support.js";
const buildTimelineAbsenceDaysMock = vi.mocked(buildTimelineAbsenceDays);
const loadTimelineCalculationRulesMock = vi.mocked(loadTimelineCalculationRules);
describe("timeline cost support", () => {
it("falls back to default calculation rules when the optional model is unavailable", async () => {
const rules = await loadTimelineCalculationRules({
vacation: {
findMany: async () => [],
},
} as never);
expect(rules.length).toBeGreaterThan(0);
});
it("expands approved vacations, sickness, and public holidays into absence days", async () => {
const result = await buildTimelineAbsenceDays({
vacation: {
findMany: async () => [
{
startDate: new Date("2026-04-03T00:00:00.000Z"),
endDate: new Date("2026-04-04T00:00:00.000Z"),
type: VacationType.VACATION,
isHalfDay: false,
},
{
startDate: new Date("2026-04-05T00:00:00.000Z"),
endDate: new Date("2026-04-05T00:00:00.000Z"),
type: VacationType.SICK,
isHalfDay: true,
},
{
startDate: new Date("2026-04-06T00:00:00.000Z"),
endDate: new Date("2026-04-06T00:00:00.000Z"),
type: VacationType.PUBLIC_HOLIDAY,
isHalfDay: false,
},
],
},
} as never, "resource_1", new Date("2026-04-01T00:00:00.000Z"), new Date("2026-04-10T00:00:00.000Z"));
expect(result.absenceDays).toEqual([
{
date: new Date("2026-04-03T00:00:00.000Z"),
type: "VACATION",
},
{
date: new Date("2026-04-04T00:00:00.000Z"),
type: "VACATION",
},
{
date: new Date("2026-04-05T00:00:00.000Z"),
type: "SICK",
isHalfDay: true,
},
{
date: new Date("2026-04-06T00:00:00.000Z"),
type: "PUBLIC_HOLIDAY",
},
]);
expect(result.legacyVacationDates).toEqual([
new Date("2026-04-03T00:00:00.000Z"),
new Date("2026-04-04T00:00:00.000Z"),
]);
});
it("treats missing optional vacation tables as no absences", async () => {
const result = await buildTimelineAbsenceDays({
vacation: {
findMany: async () => {
throw {
code: "P2021",
message: "The table `vacations` does not exist.",
meta: { table: "vacations" },
};
},
},
} as never, "resource_1", new Date("2026-04-01T00:00:00.000Z"), new Date("2026-04-10T00:00:00.000Z"));
expect(result).toEqual({
absenceDays: [],
legacyVacationDates: [],
it("builds timeline allocation daily cost from loaded absences and rules", async () => {
buildTimelineAbsenceDaysMock.mockResolvedValueOnce({
absenceDays: [{ date: new Date("2026-04-03T00:00:00.000Z"), type: "VACATION" }],
legacyVacationDates: [new Date("2026-04-03T00:00:00.000Z")],
});
loadTimelineCalculationRulesMock.mockResolvedValueOnce([{ id: "rule_1" }] as never);
calculateAllocationMock.mockReturnValueOnce({ dailyCostCents: 54_321 });
await expect(
calculateTimelineAllocationDailyCost({
db: {} as never,
resourceId: "resource_1",
lcrCents: 5_000,
hoursPerDay: 8,
startDate: new Date("2026-04-01T00:00:00.000Z"),
endDate: new Date("2026-04-10T00:00:00.000Z"),
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
} as never,
includeSaturday: true,
}),
).resolves.toBe(54_321);
expect(buildTimelineAbsenceDaysMock).toHaveBeenCalledWith(
{},
"resource_1",
new Date("2026-04-01T00:00:00.000Z"),
new Date("2026-04-10T00:00:00.000Z"),
);
expect(loadTimelineCalculationRulesMock).toHaveBeenCalledWith({});
expect(calculateAllocationMock).toHaveBeenCalledWith(
expect.objectContaining({
lcrCents: 5_000,
hoursPerDay: 8,
includeSaturday: true,
vacationDates: [new Date("2026-04-03T00:00:00.000Z")],
absenceDays: [{ date: new Date("2026-04-03T00:00:00.000Z"), type: "VACATION" }],
calculationRules: [{ id: "rule_1" }],
}),
);
});
});