refactor(api): extract experience multiplier support

This commit is contained in:
2026-03-31 14:07:21 +02:00
parent 59c84dfe4f
commit 02275bac07
3 changed files with 408 additions and 127 deletions
@@ -0,0 +1,195 @@
import { describe, expect, it } from "vitest";
import {
buildExperienceMultiplierCreateManyRows,
buildExperienceMultiplierDemandLineUpdateData,
buildExperienceMultiplierInput,
buildExperienceMultiplierNestedCreateRows,
buildExperienceMultiplierSetCreateData,
buildExperienceMultiplierSetUpdateData,
experienceMultiplierRuleInclude,
hasExperienceMultiplierChanges,
toExperienceMultiplierEngineRules,
} from "../router/experience-multiplier-support.js";
describe("experience multiplier support", () => {
it("exposes the rule include ordering", () => {
expect(experienceMultiplierRuleInclude).toEqual({
rules: { orderBy: { sortOrder: "asc" } },
});
});
it("builds create-many and nested-create rule rows", () => {
expect(buildExperienceMultiplierCreateManyRows([
{
chapter: "VFX",
costMultiplier: 1.2,
billMultiplier: 1.3,
},
{
location: "India",
level: "Senior",
costMultiplier: 0.8,
billMultiplier: 0.9,
shoringRatio: 0.5,
additionalEffortRatio: 0.1,
description: "Offshore senior",
sortOrder: 5,
},
], "ems_1")).toEqual([
{
multiplierSetId: "ems_1",
chapter: "VFX",
costMultiplier: 1.2,
billMultiplier: 1.3,
sortOrder: 0,
},
{
multiplierSetId: "ems_1",
location: "India",
level: "Senior",
costMultiplier: 0.8,
billMultiplier: 0.9,
shoringRatio: 0.5,
additionalEffortRatio: 0.1,
description: "Offshore senior",
sortOrder: 5,
},
]);
expect(buildExperienceMultiplierNestedCreateRows([
{
chapter: "Animation",
costMultiplier: 1.1,
billMultiplier: 1.2,
},
])).toEqual([
{
chapter: "Animation",
costMultiplier: 1.1,
billMultiplier: 1.2,
sortOrder: 0,
},
]);
});
it("builds create and sparse update payloads", () => {
expect(buildExperienceMultiplierSetCreateData({
name: "Standard Multipliers",
description: "Default adjustments",
isDefault: true,
rules: [
{
chapter: "VFX",
costMultiplier: 1.2,
billMultiplier: 1.2,
sortOrder: 0,
},
],
})).toEqual({
name: "Standard Multipliers",
description: "Default adjustments",
isDefault: true,
rules: {
create: [
{
chapter: "VFX",
costMultiplier: 1.2,
billMultiplier: 1.2,
sortOrder: 0,
},
],
},
});
expect(buildExperienceMultiplierSetUpdateData({
description: null,
isDefault: false,
})).toEqual({
description: null,
isDefault: false,
});
});
it("maps db rules and demand lines into engine-facing inputs", () => {
expect(toExperienceMultiplierEngineRules([
{
chapter: "VFX",
location: null,
level: null,
costMultiplier: 1.2,
billMultiplier: 1.1,
shoringRatio: null,
additionalEffortRatio: null,
description: null,
},
])).toEqual([
{
chapter: "VFX",
costMultiplier: 1.2,
billMultiplier: 1.1,
},
]);
expect(buildExperienceMultiplierInput({
id: "dl_1",
name: "Comp Senior",
chapter: "VFX",
costRateCents: 10000,
billRateCents: 15000,
hours: 100,
metadata: { location: "India" },
staffingAttributes: { level: "Senior" },
})).toEqual({
chapter: "VFX",
costRateCents: 10000,
billRateCents: 15000,
hours: 100,
location: "India",
level: "Senior",
});
});
it("detects changes and builds demand line update data", () => {
const line = {
id: "dl_1",
name: "Comp Senior",
chapter: "VFX",
costRateCents: 10000,
billRateCents: 15000,
hours: 100,
metadata: { location: "India", existing: true },
staffingAttributes: { level: "Senior" },
};
const result = {
adjustedCostRateCents: 12000,
adjustedBillRateCents: 18000,
adjustedHours: 110,
appliedRules: ["VFX uplift"],
};
expect(hasExperienceMultiplierChanges(line, result)).toBe(true);
expect(buildExperienceMultiplierDemandLineUpdateData({
line,
result,
multiplierSet: { id: "ems_1", name: "Standard" },
})).toEqual({
costRateCents: 12000,
billRateCents: 18000,
hours: 110,
costTotalCents: Math.round(12000 * 110),
priceTotalCents: Math.round(18000 * 110),
metadata: {
location: "India",
existing: true,
experienceMultiplier: {
setId: "ems_1",
setName: "Standard",
appliedRules: ["VFX uplift"],
originalCostRateCents: 10000,
originalBillRateCents: 15000,
originalHours: 100,
},
},
});
});
});