refactor(api): extract effort rule support

This commit is contained in:
2026-03-31 14:05:20 +02:00
parent c839b18d4e
commit 59c84dfe4f
3 changed files with 398 additions and 108 deletions
@@ -0,0 +1,199 @@
import { describe, expect, it } from "vitest";
import {
buildEstimateDemandLineRows,
buildEffortRuleCreateManyRows,
buildEffortRuleNestedCreateRows,
buildEffortRuleSetCreateData,
buildEffortRuleSetUpdateData,
effortRuleInclude,
toEffortRuleEngineInputs,
toScopeItemInputs,
} from "../router/effort-rule-support.js";
describe("effort rule support", () => {
it("exposes the rule include ordering", () => {
expect(effortRuleInclude).toEqual({
rules: { orderBy: { sortOrder: "asc" } },
});
});
it("builds rule rows and preserves fallback sort order", () => {
expect(buildEffortRuleCreateManyRows([
{
scopeType: "shot",
discipline: "Compositing",
chapter: "VFX",
unitMode: "per_frame",
hoursPerUnit: 0.08,
},
{
scopeType: "asset",
discipline: "Modeling",
unitMode: "per_item",
hoursPerUnit: 8,
description: "Hero asset",
sortOrder: 5,
},
], "ers_1")).toEqual([
{
ruleSetId: "ers_1",
scopeType: "shot",
discipline: "Compositing",
chapter: "VFX",
unitMode: "per_frame",
hoursPerUnit: 0.08,
sortOrder: 0,
},
{
ruleSetId: "ers_1",
scopeType: "asset",
discipline: "Modeling",
unitMode: "per_item",
hoursPerUnit: 8,
description: "Hero asset",
sortOrder: 5,
},
]);
expect(buildEffortRuleNestedCreateRows([
{
scopeType: "shot",
discipline: "Compositing",
unitMode: "per_frame",
hoursPerUnit: 0.08,
},
])).toEqual([
{
scopeType: "shot",
discipline: "Compositing",
unitMode: "per_frame",
hoursPerUnit: 0.08,
sortOrder: 0,
},
]);
});
it("builds create and sparse update payloads", () => {
expect(buildEffortRuleSetCreateData({
name: "VFX Standard",
description: "Default effort rules",
isDefault: true,
rules: [
{
scopeType: "shot",
discipline: "Compositing",
unitMode: "per_frame",
hoursPerUnit: 0.08,
sortOrder: 0,
},
],
})).toEqual({
name: "VFX Standard",
description: "Default effort rules",
isDefault: true,
rules: {
create: [
{
scopeType: "shot",
discipline: "Compositing",
unitMode: "per_frame",
hoursPerUnit: 0.08,
sortOrder: 0,
},
],
},
});
expect(buildEffortRuleSetUpdateData({
description: null,
isDefault: false,
})).toEqual({
description: null,
isDefault: false,
});
});
it("maps scope items and rules into engine inputs", () => {
expect(toScopeItemInputs([
{
name: "Shot_001",
scopeType: "shot",
frameCount: 100,
itemCount: null,
unitMode: "per_frame",
},
])).toEqual([
{
name: "Shot_001",
scopeType: "shot",
frameCount: 100,
itemCount: null,
unitMode: "per_frame",
},
]);
expect(toEffortRuleEngineInputs([
{
scopeType: "shot",
discipline: "Compositing",
chapter: null,
unitMode: "per_frame",
hoursPerUnit: 0.08,
sortOrder: 0,
},
])).toEqual([
{
scopeType: "shot",
discipline: "Compositing",
chapter: null,
unitMode: "per_frame",
hoursPerUnit: 0.08,
sortOrder: 0,
},
]);
});
it("builds estimate demand line rows with the current optional chapter semantics", () => {
expect(buildEstimateDemandLineRows({
estimateVersionId: "ver_1",
currency: "EUR",
ruleSet: { id: "ers_1", name: "VFX Standard" },
lines: [
{
scopeItemName: "Shot_001",
scopeType: "shot",
discipline: "Compositing",
chapter: "",
hours: 16,
unitMode: "per_frame",
unitCount: 200,
hoursPerUnit: 0.08,
},
],
})).toEqual([
{
estimateVersionId: "ver_1",
lineType: "LABOR",
name: "Compositing — Shot_001",
hours: 16,
costRateCents: 0,
billRateCents: 0,
currency: "EUR",
costTotalCents: 0,
priceTotalCents: 0,
monthlySpread: {},
staffingAttributes: {},
metadata: {
effortRule: {
ruleSetId: "ers_1",
ruleSetName: "VFX Standard",
discipline: "Compositing",
unitMode: "per_frame",
unitCount: 200,
hoursPerUnit: 0.08,
},
},
},
]);
});
});