refactor(api): extract experience multiplier procedures

This commit is contained in:
2026-03-31 20:02:15 +02:00
parent 24b8ba6c12
commit 34b4b3cab4
3 changed files with 557 additions and 273 deletions
@@ -0,0 +1,215 @@
import { SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
const { createAuditEntry } = vi.hoisted(() => ({
createAuditEntry: vi.fn(),
}));
vi.mock("../lib/audit.js", () => ({
createAuditEntry,
}));
vi.mock("@capakraken/engine", () => ({
applyExperienceMultipliers: vi.fn().mockReturnValue({
adjustedCostRateCents: 12_000,
adjustedBillRateCents: 18_000,
adjustedHours: 110,
appliedRules: ["Rate multiplied (chapter=VFX): cost x1.2, bill x1.2"],
}),
applyExperienceMultipliersBatch: vi.fn().mockReturnValue({
results: [
{
adjustedCostRateCents: 12_000,
adjustedBillRateCents: 18_000,
adjustedHours: 110,
appliedRules: ["Rate multiplied (chapter=VFX): cost x1.2, bill x1.2"],
},
],
totalOriginalHours: 100,
totalAdjustedHours: 110,
linesAdjusted: 1,
}),
}));
import {
applyExperienceMultiplierRules,
createExperienceMultiplierSet,
previewExperienceMultipliers,
} from "../router/experience-multiplier-procedure-support.js";
function createManagerContext(db: Record<string, unknown>) {
return {
db: db as never,
dbUser: {
id: "user_mgr",
systemRole: SystemRole.MANAGER,
permissionOverrides: null,
},
};
}
function createControllerContext(db: Record<string, unknown>) {
return {
db: db as never,
dbUser: {
id: "user_ctrl",
systemRole: SystemRole.CONTROLLER,
permissionOverrides: null,
},
};
}
function sampleMultiplierSet(overrides: Record<string, unknown> = {}) {
return {
id: "ems_1",
name: "Standard Multipliers",
description: null,
isDefault: true,
createdAt: new Date(),
updatedAt: new Date(),
rules: [
{
id: "emr_1",
multiplierSetId: "ems_1",
chapter: "VFX",
location: null,
level: null,
costMultiplier: 1.2,
billMultiplier: 1.2,
shoringRatio: null,
additionalEffortRatio: null,
description: null,
sortOrder: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
],
...overrides,
};
}
describe("experience multiplier procedure support", () => {
it("creates a multiplier set and audits it", async () => {
const create = vi.fn().mockResolvedValue(sampleMultiplierSet());
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
const result = await createExperienceMultiplierSet(createManagerContext({
experienceMultiplierSet: {
updateMany,
create,
},
}), {
name: "Standard Multipliers",
description: "Default",
isDefault: true,
rules: [],
});
expect(updateMany).toHaveBeenCalledWith({
where: { isDefault: true },
data: { isDefault: false },
});
expect(create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
name: "Standard Multipliers",
isDefault: true,
}),
}));
expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
entityType: "ExperienceMultiplierSet",
action: "CREATE",
entityId: result.id,
}));
});
it("previews adjusted demand lines for the latest estimate version", async () => {
const result = await previewExperienceMultipliers(createControllerContext({
estimate: {
findUnique: vi.fn().mockResolvedValue({
id: "est_1",
versions: [
{
id: "v_1",
versionNumber: 2,
demandLines: [
{
id: "dl_1",
name: "Compositing Senior",
chapter: "VFX",
costRateCents: 10_000,
billRateCents: 15_000,
hours: 100,
metadata: null,
staffingAttributes: null,
},
],
},
],
}),
},
experienceMultiplierSet: {
findUnique: vi.fn().mockResolvedValue(sampleMultiplierSet()),
},
}), {
estimateId: "est_1",
multiplierSetId: "ems_1",
});
expect(result.demandLineCount).toBe(1);
expect(result.linesChanged).toBe(1);
expect(result.totalOriginalCostCents).toBe(1_000_000);
expect(result.totalAdjustedCostCents).toBe(1_320_000);
});
it("updates changed demand lines and audits estimate updates", async () => {
const update = vi.fn().mockResolvedValue({});
const auditCreate = vi.fn().mockResolvedValue({});
const result = await applyExperienceMultiplierRules(createManagerContext({
estimate: {
findUnique: vi.fn().mockResolvedValue({
id: "est_1",
versions: [
{
id: "v_1",
versionNumber: 2,
status: "WORKING",
demandLines: [
{
id: "dl_1",
name: "Compositing Senior",
chapter: "VFX",
costRateCents: 10_000,
billRateCents: 15_000,
hours: 100,
metadata: null,
staffingAttributes: null,
},
],
},
],
}),
},
experienceMultiplierSet: {
findUnique: vi.fn().mockResolvedValue(sampleMultiplierSet()),
},
estimateDemandLine: {
update,
},
auditLog: {
create: auditCreate,
},
}), {
estimateId: "est_1",
multiplierSetId: "ems_1",
});
expect(update).toHaveBeenCalledTimes(1);
expect(auditCreate).toHaveBeenCalledTimes(1);
expect(result).toEqual({
linesUpdated: 1,
totalOriginalHours: 100,
totalAdjustedHours: 110,
});
});
});