3c0179fcec
Prevents mutations from committing without an audit trail if the auditLog.create call fails after the main write already succeeded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
638 lines
22 KiB
TypeScript
638 lines
22 KiB
TypeScript
import { SystemRole } from "@capakraken/shared";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { experienceMultiplierRouter } from "../router/experience-multiplier.js";
|
|
import { createCallerFactory } from "../trpc.js";
|
|
|
|
// Mock the engine — we focus on the router/DB layer, not the pure engine logic
|
|
vi.mock("@capakraken/engine", () => ({
|
|
applyExperienceMultipliers: vi.fn().mockReturnValue({
|
|
adjustedCostRateCents: 12000,
|
|
adjustedBillRateCents: 18000,
|
|
adjustedHours: 110,
|
|
appliedRules: ["Rate multiplied (chapter=VFX): cost x1.2, bill x1.2"],
|
|
}),
|
|
applyExperienceMultipliersBatch: vi.fn().mockReturnValue({
|
|
results: [
|
|
{
|
|
adjustedCostRateCents: 12000,
|
|
adjustedBillRateCents: 18000,
|
|
adjustedHours: 110,
|
|
appliedRules: ["Rate multiplied (chapter=VFX): cost x1.2, bill x1.2"],
|
|
},
|
|
],
|
|
totalOriginalHours: 100,
|
|
totalAdjustedHours: 110,
|
|
linesAdjusted: 1,
|
|
}),
|
|
}));
|
|
|
|
const createCaller = createCallerFactory(experienceMultiplierRouter);
|
|
|
|
function createControllerCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "ctrl@example.com", name: "Controller", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "user_ctrl",
|
|
systemRole: SystemRole.CONTROLLER,
|
|
permissionOverrides: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
function createManagerCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "mgr@example.com", name: "Manager", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "user_mgr",
|
|
systemRole: SystemRole.MANAGER,
|
|
permissionOverrides: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
// ── Sample data factories ────────────────────────────────────────────────────
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
function sampleDemandLine(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: "dl_1",
|
|
name: "Compositing Senior",
|
|
chapter: "VFX",
|
|
costRateCents: 10000,
|
|
billRateCents: 15000,
|
|
hours: 100,
|
|
costTotalCents: 1000000,
|
|
priceTotalCents: 1500000,
|
|
metadata: null,
|
|
staffingAttributes: null,
|
|
createdAt: new Date(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ─── list ────────────────────────────────────────────────────────────────────
|
|
|
|
describe("experienceMultiplier.list", () => {
|
|
it("returns sets ordered by isDefault desc, name asc", async () => {
|
|
const sets = [
|
|
sampleMultiplierSet(),
|
|
sampleMultiplierSet({ id: "ems_2", name: "Custom", isDefault: false }),
|
|
];
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
findMany: vi.fn().mockResolvedValue(sets),
|
|
},
|
|
};
|
|
|
|
const caller = createControllerCaller(db);
|
|
const result = await caller.list();
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(db.experienceMultiplierSet.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── getById ─────────────────────────────────────────────────────────────────
|
|
|
|
describe("experienceMultiplier.getById", () => {
|
|
it("returns the multiplier set when found", async () => {
|
|
const set = sampleMultiplierSet();
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
findUnique: vi.fn().mockResolvedValue(set),
|
|
},
|
|
};
|
|
|
|
const caller = createControllerCaller(db);
|
|
const result = await caller.getById({ id: "ems_1" });
|
|
|
|
expect(result.id).toBe("ems_1");
|
|
expect(result.rules).toHaveLength(1);
|
|
});
|
|
|
|
it("throws NOT_FOUND when set does not exist", async () => {
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
},
|
|
};
|
|
|
|
const caller = createControllerCaller(db);
|
|
await expect(caller.getById({ id: "nonexistent" })).rejects.toThrow(
|
|
"Experience multiplier set not found",
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── create ──────────────────────────────────────────────────────────────────
|
|
|
|
describe("experienceMultiplier.create", () => {
|
|
it("creates a set with rules", async () => {
|
|
const created = sampleMultiplierSet();
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
|
create: vi.fn().mockResolvedValue(created),
|
|
},
|
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
const result = await caller.create({
|
|
name: "Standard Multipliers",
|
|
isDefault: false,
|
|
rules: [
|
|
{
|
|
chapter: "VFX",
|
|
costMultiplier: 1.2,
|
|
billMultiplier: 1.2,
|
|
sortOrder: 0,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result.id).toBe("ems_1");
|
|
expect(db.experienceMultiplierSet.create).toHaveBeenCalledTimes(1);
|
|
// isDefault was false, so updateMany should NOT have been called
|
|
expect(db.experienceMultiplierSet.updateMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("unsets other defaults when creating a new default set", async () => {
|
|
const created = sampleMultiplierSet({ isDefault: true });
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
|
create: vi.fn().mockResolvedValue(created),
|
|
},
|
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await caller.create({
|
|
name: "Standard Multipliers",
|
|
isDefault: true,
|
|
rules: [],
|
|
});
|
|
|
|
expect(db.experienceMultiplierSet.updateMany).toHaveBeenCalledWith({
|
|
where: { isDefault: true },
|
|
data: { isDefault: false },
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── update ──────────────────────────────────────────────────────────────────
|
|
|
|
describe("experienceMultiplier.update", () => {
|
|
it("updates name and description without touching rules", async () => {
|
|
const existing = sampleMultiplierSet();
|
|
const updated = { ...existing, name: "Updated Name" };
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
findUnique: vi.fn().mockResolvedValue(existing),
|
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
|
update: vi.fn().mockResolvedValue(updated),
|
|
},
|
|
experienceMultiplierRule: {
|
|
deleteMany: vi.fn(),
|
|
createMany: vi.fn(),
|
|
},
|
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
const result = await caller.update({ id: "ems_1", name: "Updated Name" });
|
|
|
|
expect(result.name).toBe("Updated Name");
|
|
// No rules provided, so rule replacement should not happen
|
|
expect(db.experienceMultiplierRule.deleteMany).not.toHaveBeenCalled();
|
|
expect(db.experienceMultiplierRule.createMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("replaces rules when rules array is provided", async () => {
|
|
const existing = sampleMultiplierSet();
|
|
const updated = sampleMultiplierSet({ name: "Updated" });
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
findUnique: vi.fn().mockResolvedValue(existing),
|
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
|
update: vi.fn().mockResolvedValue(updated),
|
|
},
|
|
experienceMultiplierRule: {
|
|
deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
|
|
createMany: vi.fn().mockResolvedValue({ count: 2 }),
|
|
},
|
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await caller.update({
|
|
id: "ems_1",
|
|
rules: [
|
|
{ chapter: "VFX", costMultiplier: 1.3, billMultiplier: 1.3, sortOrder: 0 },
|
|
{ location: "India", costMultiplier: 0.7, billMultiplier: 0.9, shoringRatio: 0.5, sortOrder: 1 },
|
|
],
|
|
});
|
|
|
|
expect(db.experienceMultiplierRule.deleteMany).toHaveBeenCalledWith({
|
|
where: { multiplierSetId: "ems_1" },
|
|
});
|
|
expect(db.experienceMultiplierRule.createMany).toHaveBeenCalledWith({
|
|
data: expect.arrayContaining([
|
|
expect.objectContaining({ multiplierSetId: "ems_1", chapter: "VFX" }),
|
|
expect.objectContaining({ multiplierSetId: "ems_1", location: "India" }),
|
|
]),
|
|
});
|
|
});
|
|
|
|
it("throws NOT_FOUND when set does not exist", async () => {
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
updateMany: vi.fn(),
|
|
update: vi.fn(),
|
|
},
|
|
experienceMultiplierRule: { deleteMany: vi.fn(), createMany: vi.fn() },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await expect(caller.update({ id: "nonexistent", name: "X" })).rejects.toThrow(
|
|
"Experience multiplier set not found",
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── delete ──────────────────────────────────────────────────────────────────
|
|
|
|
describe("experienceMultiplier.delete", () => {
|
|
it("deletes the set and returns its id", async () => {
|
|
const existing = sampleMultiplierSet();
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
findUnique: vi.fn().mockResolvedValue(existing),
|
|
delete: vi.fn().mockResolvedValue(existing),
|
|
},
|
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
const result = await caller.delete({ id: "ems_1" });
|
|
|
|
expect(result).toEqual({ id: "ems_1" });
|
|
expect(db.experienceMultiplierSet.delete).toHaveBeenCalledWith({ where: { id: "ems_1" } });
|
|
});
|
|
|
|
it("throws NOT_FOUND when set does not exist", async () => {
|
|
const db = {
|
|
experienceMultiplierSet: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
delete: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await expect(caller.delete({ id: "nonexistent" })).rejects.toThrow(
|
|
"Experience multiplier set not found",
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── preview ─────────────────────────────────────────────────────────────────
|
|
|
|
describe("experienceMultiplier.preview", () => {
|
|
function makeEstimate(demandLines: unknown[] = [sampleDemandLine()]) {
|
|
return {
|
|
id: "est_1",
|
|
versions: [
|
|
{
|
|
id: "v_1",
|
|
versionNumber: 1,
|
|
status: "WORKING",
|
|
demandLines,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
it("returns preview results with summary stats", async () => {
|
|
const estimate = makeEstimate();
|
|
const multiplierSet = sampleMultiplierSet();
|
|
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
|
};
|
|
|
|
const caller = createControllerCaller(db);
|
|
const result = await caller.preview({ estimateId: "est_1", multiplierSetId: "ems_1" });
|
|
|
|
expect(result.previews).toHaveLength(1);
|
|
expect(result.demandLineCount).toBe(1);
|
|
expect(result.multiplierSetName).toBe("Standard Multipliers");
|
|
expect(result.ruleCount).toBe(1);
|
|
// The mock returns adjusted values different from original, so hasChanges = true
|
|
expect(result.previews[0].hasChanges).toBe(true);
|
|
expect(result.previews[0].originalCostRateCents).toBe(10000);
|
|
expect(result.previews[0].adjustedCostRateCents).toBe(12000);
|
|
expect(result.linesChanged).toBe(1);
|
|
});
|
|
|
|
it("throws NOT_FOUND when estimate does not exist", async () => {
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(null) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(sampleMultiplierSet()) },
|
|
};
|
|
|
|
const caller = createControllerCaller(db);
|
|
await expect(caller.preview({ estimateId: "nope", multiplierSetId: "ems_1" })).rejects.toThrow(
|
|
"Estimate not found",
|
|
);
|
|
});
|
|
|
|
it("throws NOT_FOUND when multiplier set does not exist", async () => {
|
|
const estimate = makeEstimate();
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(null) },
|
|
};
|
|
|
|
const caller = createControllerCaller(db);
|
|
await expect(caller.preview({ estimateId: "est_1", multiplierSetId: "nope" })).rejects.toThrow(
|
|
"Experience multiplier set not found",
|
|
);
|
|
});
|
|
|
|
it("throws NOT_FOUND when estimate has no versions", async () => {
|
|
const estimate = { id: "est_1", versions: [] };
|
|
const multiplierSet = sampleMultiplierSet();
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
|
};
|
|
|
|
const caller = createControllerCaller(db);
|
|
await expect(caller.preview({ estimateId: "est_1", multiplierSetId: "ems_1" })).rejects.toThrow(
|
|
"Estimate has no versions",
|
|
);
|
|
});
|
|
|
|
it("reports no changes when rates are unchanged", async () => {
|
|
// Import the mock to override for this test
|
|
const { applyExperienceMultipliers } = await import("@capakraken/engine");
|
|
const mockFn = applyExperienceMultipliers as ReturnType<typeof vi.fn>;
|
|
mockFn.mockReturnValueOnce({
|
|
adjustedCostRateCents: 10000,
|
|
adjustedBillRateCents: 15000,
|
|
adjustedHours: 100,
|
|
appliedRules: ["No matching rule found -- values unchanged."],
|
|
});
|
|
|
|
const estimate = makeEstimate();
|
|
const multiplierSet = sampleMultiplierSet();
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
|
};
|
|
|
|
const caller = createControllerCaller(db);
|
|
const result = await caller.preview({ estimateId: "est_1", multiplierSetId: "ems_1" });
|
|
|
|
expect(result.linesChanged).toBe(0);
|
|
expect(result.previews[0].hasChanges).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── applyRules ──────────────────────────────────────────────────────────────
|
|
|
|
describe("experienceMultiplier.applyRules", () => {
|
|
function makeEstimate(versionStatus: string, demandLines: unknown[] = [sampleDemandLine()]) {
|
|
return {
|
|
id: "est_1",
|
|
versions: [
|
|
{
|
|
id: "v_1",
|
|
versionNumber: 1,
|
|
status: versionStatus,
|
|
demandLines,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
it("updates demand lines with adjusted rates and creates audit log", async () => {
|
|
const estimate = makeEstimate("WORKING");
|
|
const multiplierSet = sampleMultiplierSet();
|
|
const db: Record<string, unknown> = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
|
estimateDemandLine: {
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
const result = await caller.applyRules({
|
|
estimateId: "est_1",
|
|
multiplierSetId: "ems_1",
|
|
});
|
|
|
|
expect(result.linesUpdated).toBe(1);
|
|
expect(result.totalOriginalHours).toBe(100);
|
|
expect(result.totalAdjustedHours).toBe(110);
|
|
expect(db.estimateDemandLine.update).toHaveBeenCalledTimes(1);
|
|
|
|
// Verify the update call contains the adjusted rates and metadata
|
|
const updateCall = db.estimateDemandLine.update.mock.calls[0][0];
|
|
expect(updateCall.where).toEqual({ id: "dl_1" });
|
|
expect(updateCall.data.costRateCents).toBe(12000);
|
|
expect(updateCall.data.billRateCents).toBe(18000);
|
|
expect(updateCall.data.hours).toBe(110);
|
|
expect(updateCall.data.costTotalCents).toBe(Math.round(12000 * 110));
|
|
expect(updateCall.data.priceTotalCents).toBe(Math.round(18000 * 110));
|
|
expect(updateCall.data.metadata.experienceMultiplier).toEqual(
|
|
expect.objectContaining({
|
|
setId: "ems_1",
|
|
setName: "Standard Multipliers",
|
|
originalCostRateCents: 10000,
|
|
originalBillRateCents: 15000,
|
|
originalHours: 100,
|
|
}),
|
|
);
|
|
|
|
// Audit log should be created
|
|
expect(db.auditLog.create).toHaveBeenCalledTimes(1);
|
|
const auditCall = db.auditLog.create.mock.calls[0][0];
|
|
expect(auditCall.data.entityType).toBe("Estimate");
|
|
expect(auditCall.data.entityId).toBe("est_1");
|
|
expect(auditCall.data.action).toBe("UPDATE");
|
|
expect(auditCall.data.userId).toBe("user_mgr");
|
|
});
|
|
|
|
it("skips unchanged lines (no update call)", async () => {
|
|
const { applyExperienceMultipliersBatch } = await import("@capakraken/engine");
|
|
const mockFn = applyExperienceMultipliersBatch as ReturnType<typeof vi.fn>;
|
|
mockFn.mockReturnValueOnce({
|
|
results: [
|
|
{
|
|
adjustedCostRateCents: 10000,
|
|
adjustedBillRateCents: 15000,
|
|
adjustedHours: 100,
|
|
appliedRules: ["No matching rule found."],
|
|
},
|
|
],
|
|
totalOriginalHours: 100,
|
|
totalAdjustedHours: 100,
|
|
linesAdjusted: 0,
|
|
});
|
|
|
|
const estimate = makeEstimate("WORKING");
|
|
const multiplierSet = sampleMultiplierSet();
|
|
const db: Record<string, unknown> = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
|
estimateDemandLine: {
|
|
update: vi.fn(),
|
|
},
|
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
const result = await caller.applyRules({
|
|
estimateId: "est_1",
|
|
multiplierSetId: "ems_1",
|
|
});
|
|
|
|
expect(result.linesUpdated).toBe(0);
|
|
expect((db.estimateDemandLine as Record<string, ReturnType<typeof vi.fn>>).update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects applying to a non-WORKING version", async () => {
|
|
const estimate = makeEstimate("SUBMITTED");
|
|
const multiplierSet = sampleMultiplierSet();
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
|
estimateDemandLine: { update: vi.fn() },
|
|
auditLog: { create: vi.fn() },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await expect(
|
|
caller.applyRules({ estimateId: "est_1", multiplierSetId: "ems_1" }),
|
|
).rejects.toThrow("Can only apply multipliers to a WORKING version");
|
|
});
|
|
|
|
it("throws NOT_FOUND when estimate does not exist", async () => {
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(null) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(sampleMultiplierSet()) },
|
|
estimateDemandLine: { update: vi.fn() },
|
|
auditLog: { create: vi.fn() },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await expect(
|
|
caller.applyRules({ estimateId: "nope", multiplierSetId: "ems_1" }),
|
|
).rejects.toThrow("Estimate not found");
|
|
});
|
|
|
|
it("throws NOT_FOUND when multiplier set does not exist", async () => {
|
|
const estimate = makeEstimate("WORKING");
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(null) },
|
|
estimateDemandLine: { update: vi.fn() },
|
|
auditLog: { create: vi.fn() },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await expect(
|
|
caller.applyRules({ estimateId: "est_1", multiplierSetId: "nope" }),
|
|
).rejects.toThrow("Experience multiplier set not found");
|
|
});
|
|
|
|
it("throws NOT_FOUND when estimate has no versions", async () => {
|
|
const estimate = { id: "est_1", versions: [] };
|
|
const db = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(sampleMultiplierSet()) },
|
|
estimateDemandLine: { update: vi.fn() },
|
|
auditLog: { create: vi.fn() },
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await expect(
|
|
caller.applyRules({ estimateId: "est_1", multiplierSetId: "ems_1" }),
|
|
).rejects.toThrow("Estimate has no versions");
|
|
});
|
|
|
|
it("preserves existing metadata when updating demand lines", async () => {
|
|
const lineWithMetadata = sampleDemandLine({
|
|
metadata: { someField: "existing-value", anotherField: 42 },
|
|
});
|
|
const estimate = makeEstimate("WORKING", [lineWithMetadata]);
|
|
const multiplierSet = sampleMultiplierSet();
|
|
const db: Record<string, unknown> = {
|
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
|
estimateDemandLine: {
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
|
};
|
|
|
|
const caller = createManagerCaller(db);
|
|
await caller.applyRules({ estimateId: "est_1", multiplierSetId: "ems_1" });
|
|
|
|
const updateCall = db.estimateDemandLine.update.mock.calls[0][0];
|
|
// Existing metadata fields should be preserved alongside experienceMultiplier
|
|
expect(updateCall.data.metadata.someField).toBe("existing-value");
|
|
expect(updateCall.data.metadata.anotherField).toBe(42);
|
|
expect(updateCall.data.metadata.experienceMultiplier).toBeDefined();
|
|
});
|
|
});
|