refactor(api): extract effort rule procedures

This commit is contained in:
2026-03-31 19:59:36 +02:00
parent cec4169bea
commit 24b8ba6c12
3 changed files with 519 additions and 251 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", () => ({
expandScopeToEffort: vi.fn().mockReturnValue({
lines: [
{
scopeItemName: "Shot_001",
discipline: "Compositing",
chapter: null,
hours: 16,
unitMode: "per_frame",
unitCount: 200,
hoursPerUnit: 0.08,
},
],
warnings: [],
unmatchedScopeItems: [],
}),
aggregateByDiscipline: vi.fn().mockReturnValue({
Compositing: { totalHours: 16, lineCount: 1 },
}),
}));
import {
applyEffortRules,
createEffortRuleSet,
previewEffortRules,
} from "../router/effort-rule-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 sampleRuleSet(overrides: Record<string, unknown> = {}) {
return {
id: "ers_1",
name: "VFX Standard",
description: null,
isDefault: true,
createdAt: new Date(),
updatedAt: new Date(),
rules: [
{
id: "er_1",
ruleSetId: "ers_1",
scopeType: "shot",
discipline: "Compositing",
chapter: null,
unitMode: "per_frame",
hoursPerUnit: 0.08,
description: null,
sortOrder: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
],
...overrides,
};
}
describe("effort rule procedure support", () => {
it("creates a rule set and audits it", async () => {
const create = vi.fn().mockResolvedValue(sampleRuleSet());
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
const result = await createEffortRuleSet(createManagerContext({
effortRuleSet: {
updateMany,
create,
},
}), {
name: "VFX Standard",
description: "Default",
isDefault: true,
rules: [],
});
expect(updateMany).toHaveBeenCalledWith({
where: { isDefault: true },
data: { isDefault: false },
});
expect(create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
name: "VFX Standard",
isDefault: true,
}),
}));
expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
entityType: "EffortRuleSet",
action: "CREATE",
entityId: result.id,
}));
});
it("previews expanded effort using the latest estimate version", async () => {
const result = await previewEffortRules(createControllerContext({
estimate: {
findUnique: vi.fn().mockResolvedValue({
id: "est_1",
versions: [
{
id: "v_1",
versionNumber: 2,
scopeItems: [
{
name: "Shot_001",
scopeType: "shot",
frameCount: 200,
itemCount: null,
unitMode: "per_frame",
},
],
},
],
}),
},
effortRuleSet: {
findUnique: vi.fn().mockResolvedValue(sampleRuleSet()),
},
}), {
estimateId: "est_1",
ruleSetId: "ers_1",
});
expect(result.scopeItemCount).toBe(1);
expect(result.ruleCount).toBe(1);
expect(result.aggregated).toEqual({
Compositing: { totalHours: 16, lineCount: 1 },
});
});
it("replaces demand lines and writes an estimate audit when rules are applied", async () => {
const deleteMany = vi.fn().mockResolvedValue({ count: 1 });
const createMany = vi.fn().mockResolvedValue({ count: 1 });
const auditCreate = vi.fn().mockResolvedValue({});
const result = await applyEffortRules(createManagerContext({
estimate: {
findUnique: vi.fn().mockResolvedValue({
id: "est_1",
baseCurrency: "EUR",
versions: [
{
id: "v_1",
versionNumber: 2,
status: "WORKING",
scopeItems: [
{
name: "Shot_001",
scopeType: "shot",
frameCount: 200,
itemCount: null,
unitMode: "per_frame",
},
],
demandLines: [{ id: "old_1" }],
},
],
}),
},
effortRuleSet: {
findUnique: vi.fn().mockResolvedValue(sampleRuleSet()),
},
estimateDemandLine: {
deleteMany,
createMany,
},
auditLog: {
create: auditCreate,
},
}), {
estimateId: "est_1",
ruleSetId: "ers_1",
mode: "replace",
});
expect(deleteMany).toHaveBeenCalledWith({
where: { estimateVersionId: "v_1" },
});
expect(createMany).toHaveBeenCalledTimes(1);
expect(auditCreate).toHaveBeenCalledTimes(1);
expect(result).toEqual({
linesGenerated: 1,
warnings: [],
unmatchedScopeItems: [],
});
});
});