fix(api): wrap audit log writes inside their parent transactions
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>
This commit is contained in:
@@ -19,7 +19,7 @@ export function createToolContext(
|
|||||||
},
|
},
|
||||||
): ToolContext {
|
): ToolContext {
|
||||||
const userRole = options?.userRole ?? SystemRole.ADMIN;
|
const userRole = options?.userRole ?? SystemRole.ADMIN;
|
||||||
const mergedDb = {
|
const mergedDb: Record<string, unknown> = {
|
||||||
...defaultDbDefaults,
|
...defaultDbDefaults,
|
||||||
...db,
|
...db,
|
||||||
blueprint: {
|
blueprint: {
|
||||||
@@ -27,6 +27,9 @@ export function createToolContext(
|
|||||||
...(db.blueprint as Record<string, unknown> | undefined),
|
...(db.blueprint as Record<string, unknown> | undefined),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if (!mergedDb["$transaction"]) {
|
||||||
|
mergedDb["$transaction"] = vi.fn(async (fn: (tx: unknown) => unknown) => fn(mergedDb));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
db: mergedDb as ToolContext["db"],
|
db: mergedDb as ToolContext["db"],
|
||||||
userId: "user_1",
|
userId: "user_1",
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ export function createToolContext(
|
|||||||
},
|
},
|
||||||
): ToolContext {
|
): ToolContext {
|
||||||
const userRole = options?.userRole ?? SystemRole.ADMIN;
|
const userRole = options?.userRole ?? SystemRole.ADMIN;
|
||||||
|
const mergedDb: Record<string, unknown> = { ...db };
|
||||||
|
mergedDb["$transaction"] = vi.fn(async (fn: (tx: unknown) => unknown) => fn(mergedDb));
|
||||||
return {
|
return {
|
||||||
db: db as ToolContext["db"],
|
db: mergedDb as ToolContext["db"],
|
||||||
userId: "user_1",
|
userId: "user_1",
|
||||||
userRole,
|
userRole,
|
||||||
permissions: new Set(options?.permissions ?? []),
|
permissions: new Set(options?.permissions ?? []),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import type { ToolContext } from "../router/assistant-tools.js";
|
import type { ToolContext } from "../router/assistant-tools.js";
|
||||||
|
|
||||||
@@ -10,8 +11,12 @@ export function createToolContext(
|
|||||||
},
|
},
|
||||||
): ToolContext {
|
): ToolContext {
|
||||||
const userRole = options?.userRole ?? SystemRole.ADMIN;
|
const userRole = options?.userRole ?? SystemRole.ADMIN;
|
||||||
|
const mergedDb: Record<string, unknown> = { ...db };
|
||||||
|
if (!mergedDb["$transaction"]) {
|
||||||
|
mergedDb["$transaction"] = vi.fn(async (fn: (tx: unknown) => unknown) => fn(mergedDb));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
db: db as ToolContext["db"],
|
db: mergedDb as ToolContext["db"],
|
||||||
userId: "user_1",
|
userId: "user_1",
|
||||||
userRole,
|
userRole,
|
||||||
permissions: new Set(options?.permissions ?? []),
|
permissions: new Set(options?.permissions ?? []),
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ describe("effort rule procedure support", () => {
|
|||||||
const createMany = vi.fn().mockResolvedValue({ count: 1 });
|
const createMany = vi.fn().mockResolvedValue({ count: 1 });
|
||||||
const auditCreate = vi.fn().mockResolvedValue({});
|
const auditCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const result = await applyEffortRules(createManagerContext({
|
const db: Record<string, unknown> = {
|
||||||
estimate: {
|
estimate: {
|
||||||
findUnique: vi.fn().mockResolvedValue({
|
findUnique: vi.fn().mockResolvedValue({
|
||||||
id: "est_1",
|
id: "est_1",
|
||||||
@@ -195,7 +195,10 @@ describe("effort rule procedure support", () => {
|
|||||||
auditLog: {
|
auditLog: {
|
||||||
create: auditCreate,
|
create: auditCreate,
|
||||||
},
|
},
|
||||||
}), {
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await applyEffortRules(createManagerContext(db), {
|
||||||
estimateId: "est_1",
|
estimateId: "est_1",
|
||||||
ruleSetId: "ers_1",
|
ruleSetId: "ers_1",
|
||||||
mode: "replace",
|
mode: "replace",
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ describe("effortRule.applyRules", () => {
|
|||||||
it("replaces existing demand lines in replace mode", async () => {
|
it("replaces existing demand lines in replace mode", async () => {
|
||||||
const estimate = makeEstimate("WORKING", [{ id: "dl_old" }]);
|
const estimate = makeEstimate("WORKING", [{ id: "dl_old" }]);
|
||||||
const ruleSet = sampleRuleSet();
|
const ruleSet = sampleRuleSet();
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
||||||
effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
|
effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
|
||||||
estimateDemandLine: {
|
estimateDemandLine: {
|
||||||
@@ -424,6 +424,7 @@ describe("effortRule.applyRules", () => {
|
|||||||
createMany: vi.fn().mockResolvedValue({ count: 1 }),
|
createMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -444,7 +445,7 @@ describe("effortRule.applyRules", () => {
|
|||||||
it("does not delete existing lines in append mode", async () => {
|
it("does not delete existing lines in append mode", async () => {
|
||||||
const estimate = makeEstimate("WORKING", [{ id: "dl_old" }]);
|
const estimate = makeEstimate("WORKING", [{ id: "dl_old" }]);
|
||||||
const ruleSet = sampleRuleSet();
|
const ruleSet = sampleRuleSet();
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
||||||
effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
|
effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
|
||||||
estimateDemandLine: {
|
estimateDemandLine: {
|
||||||
@@ -452,6 +453,7 @@ describe("effortRule.applyRules", () => {
|
|||||||
createMany: vi.fn().mockResolvedValue({ count: 1 }),
|
createMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -513,7 +515,7 @@ describe("effortRule.applyRules", () => {
|
|||||||
it("creates demand lines with correct metadata shape", async () => {
|
it("creates demand lines with correct metadata shape", async () => {
|
||||||
const estimate = makeEstimate("WORKING");
|
const estimate = makeEstimate("WORKING");
|
||||||
const ruleSet = sampleRuleSet();
|
const ruleSet = sampleRuleSet();
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
||||||
effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
|
effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
|
||||||
estimateDemandLine: {
|
estimateDemandLine: {
|
||||||
@@ -521,6 +523,7 @@ describe("effortRule.applyRules", () => {
|
|||||||
createMany: vi.fn().mockResolvedValue({ count: 1 }),
|
createMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
|
|||||||
@@ -434,9 +434,10 @@ describe("estimate router", () => {
|
|||||||
const estimateCreate = vi.fn().mockResolvedValue(created);
|
const estimateCreate = vi.fn().mockResolvedValue(created);
|
||||||
const auditLogCreate = vi.fn().mockResolvedValue({});
|
const auditLogCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { create: estimateCreate },
|
estimate: { create: estimateCreate },
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
|
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -474,10 +475,11 @@ describe("estimate router", () => {
|
|||||||
});
|
});
|
||||||
const auditLogCreate = vi.fn().mockResolvedValue({});
|
const auditLogCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { create: estimateCreate },
|
estimate: { create: estimateCreate },
|
||||||
project: { findUnique: projectFindUnique },
|
project: { findUnique: projectFindUnique },
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
|
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -699,7 +701,7 @@ describe("estimate router", () => {
|
|||||||
const updateEstimate = vi.fn().mockResolvedValue({});
|
const updateEstimate = vi.fn().mockResolvedValue({});
|
||||||
const auditLogCreate = vi.fn().mockResolvedValue({});
|
const auditLogCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: {
|
estimate: {
|
||||||
findUnique,
|
findUnique,
|
||||||
update: updateEstimate,
|
update: updateEstimate,
|
||||||
@@ -709,13 +711,8 @@ describe("estimate router", () => {
|
|||||||
update: updateVersion,
|
update: updateVersion,
|
||||||
},
|
},
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) =>
|
|
||||||
callback({
|
|
||||||
estimateVersion: { updateMany, update: updateVersion },
|
|
||||||
estimate: { update: updateEstimate },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
db["$transaction"] = vi.fn(async (callback: (tx: unknown) => unknown) => callback(db));
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
const result = await caller.submitVersion({ estimateId: "est_1" });
|
const result = await caller.submitVersion({ estimateId: "est_1" });
|
||||||
@@ -803,7 +800,7 @@ describe("estimate router", () => {
|
|||||||
const updateEstimate = vi.fn().mockResolvedValue({});
|
const updateEstimate = vi.fn().mockResolvedValue({});
|
||||||
const auditLogCreate = vi.fn().mockResolvedValue({});
|
const auditLogCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: {
|
estimate: {
|
||||||
findUnique,
|
findUnique,
|
||||||
update: updateEstimate,
|
update: updateEstimate,
|
||||||
@@ -813,13 +810,8 @@ describe("estimate router", () => {
|
|||||||
update: updateVersion,
|
update: updateVersion,
|
||||||
},
|
},
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) =>
|
|
||||||
callback({
|
|
||||||
estimateVersion: { updateMany, update: updateVersion },
|
|
||||||
estimate: { update: updateEstimate },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
db["$transaction"] = vi.fn(async (callback: (tx: unknown) => unknown) => callback(db));
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
const result = await caller.approveVersion({ estimateId: "est_1" });
|
const result = await caller.approveVersion({ estimateId: "est_1" });
|
||||||
@@ -903,7 +895,7 @@ describe("estimate router", () => {
|
|||||||
const updateEstimate = vi.fn().mockResolvedValue({});
|
const updateEstimate = vi.fn().mockResolvedValue({});
|
||||||
const auditLogCreate = vi.fn().mockResolvedValue({});
|
const auditLogCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: {
|
estimate: {
|
||||||
findUnique,
|
findUnique,
|
||||||
update: updateEstimate,
|
update: updateEstimate,
|
||||||
@@ -915,18 +907,8 @@ describe("estimate router", () => {
|
|||||||
resourceCostSnapshot: { createMany: createSnapshots },
|
resourceCostSnapshot: { createMany: createSnapshots },
|
||||||
estimateMetric: { createMany: createMetrics },
|
estimateMetric: { createMany: createMetrics },
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) =>
|
|
||||||
callback({
|
|
||||||
estimateVersion: { create: createVersion },
|
|
||||||
estimateAssumption: { createMany: createAssumptions },
|
|
||||||
scopeItem: { create: createScopeItem },
|
|
||||||
estimateDemandLine: { createMany: createDemandLines },
|
|
||||||
resourceCostSnapshot: { createMany: createSnapshots },
|
|
||||||
estimateMetric: { createMany: createMetrics },
|
|
||||||
estimate: { update: updateEstimate },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
db["$transaction"] = vi.fn(async (callback: (tx: unknown) => unknown) => callback(db));
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
const result = await caller.createRevision({ estimateId: "est_1" });
|
const result = await caller.createRevision({ estimateId: "est_1" });
|
||||||
@@ -1008,12 +990,13 @@ describe("estimate router", () => {
|
|||||||
const estimateCreate = vi.fn().mockResolvedValue(cloned);
|
const estimateCreate = vi.fn().mockResolvedValue(cloned);
|
||||||
const auditLogCreate = vi.fn().mockResolvedValue({});
|
const auditLogCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: {
|
estimate: {
|
||||||
findUnique: estimateFindUnique,
|
findUnique: estimateFindUnique,
|
||||||
create: estimateCreate,
|
create: estimateCreate,
|
||||||
},
|
},
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
|
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -1026,9 +1009,10 @@ describe("estimate router", () => {
|
|||||||
it("throws NOT_FOUND when source estimate does not exist", async () => {
|
it("throws NOT_FOUND when source estimate does not exist", async () => {
|
||||||
const findUnique = vi.fn().mockResolvedValue(null);
|
const findUnique = vi.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique },
|
estimate: { findUnique },
|
||||||
auditLog: { create: vi.fn() },
|
auditLog: { create: vi.fn() },
|
||||||
|
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -1130,10 +1114,11 @@ describe("estimate router", () => {
|
|||||||
const updateVersion = vi.fn().mockResolvedValue({});
|
const updateVersion = vi.fn().mockResolvedValue({});
|
||||||
const auditLogCreate = vi.fn().mockResolvedValue({});
|
const auditLogCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique: estimateFindUnique },
|
estimate: { findUnique: estimateFindUnique },
|
||||||
estimateVersion: { update: updateVersion },
|
estimateVersion: { update: updateVersion },
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
|
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -1279,10 +1264,11 @@ describe("estimate router", () => {
|
|||||||
const createExport = vi.fn().mockResolvedValue({ id: "exp_1" });
|
const createExport = vi.fn().mockResolvedValue({ id: "exp_1" });
|
||||||
const auditLogCreate = vi.fn().mockResolvedValue({});
|
const auditLogCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique: estimateFindUnique },
|
estimate: { findUnique: estimateFindUnique },
|
||||||
estimateExport: { create: createExport },
|
estimateExport: { create: createExport },
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
|
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -1300,10 +1286,11 @@ describe("estimate router", () => {
|
|||||||
it("throws NOT_FOUND when estimate does not exist", async () => {
|
it("throws NOT_FOUND when estimate does not exist", async () => {
|
||||||
const findUnique = vi.fn().mockResolvedValue(null);
|
const findUnique = vi.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique },
|
estimate: { findUnique },
|
||||||
estimateExport: { create: vi.fn() },
|
estimateExport: { create: vi.fn() },
|
||||||
auditLog: { create: vi.fn() },
|
auditLog: { create: vi.fn() },
|
||||||
|
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ describe("experience multiplier procedure support", () => {
|
|||||||
const update = vi.fn().mockResolvedValue({});
|
const update = vi.fn().mockResolvedValue({});
|
||||||
const auditCreate = vi.fn().mockResolvedValue({});
|
const auditCreate = vi.fn().mockResolvedValue({});
|
||||||
|
|
||||||
const result = await applyExperienceMultiplierRules(createManagerContext({
|
const db: Record<string, unknown> = {
|
||||||
estimate: {
|
estimate: {
|
||||||
findUnique: vi.fn().mockResolvedValue({
|
findUnique: vi.fn().mockResolvedValue({
|
||||||
id: "est_1",
|
id: "est_1",
|
||||||
@@ -199,7 +199,10 @@ describe("experience multiplier procedure support", () => {
|
|||||||
auditLog: {
|
auditLog: {
|
||||||
create: auditCreate,
|
create: auditCreate,
|
||||||
},
|
},
|
||||||
}), {
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await applyExperienceMultiplierRules(createManagerContext(db), {
|
||||||
estimateId: "est_1",
|
estimateId: "est_1",
|
||||||
multiplierSetId: "ems_1",
|
multiplierSetId: "ems_1",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -462,13 +462,14 @@ describe("experienceMultiplier.applyRules", () => {
|
|||||||
it("updates demand lines with adjusted rates and creates audit log", async () => {
|
it("updates demand lines with adjusted rates and creates audit log", async () => {
|
||||||
const estimate = makeEstimate("WORKING");
|
const estimate = makeEstimate("WORKING");
|
||||||
const multiplierSet = sampleMultiplierSet();
|
const multiplierSet = sampleMultiplierSet();
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
||||||
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
||||||
estimateDemandLine: {
|
estimateDemandLine: {
|
||||||
update: vi.fn().mockResolvedValue({}),
|
update: vi.fn().mockResolvedValue({}),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -528,13 +529,14 @@ describe("experienceMultiplier.applyRules", () => {
|
|||||||
|
|
||||||
const estimate = makeEstimate("WORKING");
|
const estimate = makeEstimate("WORKING");
|
||||||
const multiplierSet = sampleMultiplierSet();
|
const multiplierSet = sampleMultiplierSet();
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
||||||
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
||||||
estimateDemandLine: {
|
estimateDemandLine: {
|
||||||
update: vi.fn(),
|
update: vi.fn(),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -544,7 +546,7 @@ describe("experienceMultiplier.applyRules", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.linesUpdated).toBe(0);
|
expect(result.linesUpdated).toBe(0);
|
||||||
expect(db.estimateDemandLine.update).not.toHaveBeenCalled();
|
expect((db.estimateDemandLine as Record<string, ReturnType<typeof vi.fn>>).update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects applying to a non-WORKING version", async () => {
|
it("rejects applying to a non-WORKING version", async () => {
|
||||||
@@ -613,13 +615,14 @@ describe("experienceMultiplier.applyRules", () => {
|
|||||||
});
|
});
|
||||||
const estimate = makeEstimate("WORKING", [lineWithMetadata]);
|
const estimate = makeEstimate("WORKING", [lineWithMetadata]);
|
||||||
const multiplierSet = sampleMultiplierSet();
|
const multiplierSet = sampleMultiplierSet();
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
|
||||||
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
|
||||||
estimateDemandLine: {
|
estimateDemandLine: {
|
||||||
update: vi.fn().mockResolvedValue({}),
|
update: vi.fn().mockResolvedValue({}),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
|
|||||||
@@ -133,10 +133,7 @@ describe("import-export procedure support", () => {
|
|||||||
.mockResolvedValueOnce(null);
|
.mockResolvedValueOnce(null);
|
||||||
const resourceUpdate = vi.fn().mockResolvedValue({ id: "res_1" });
|
const resourceUpdate = vi.fn().mockResolvedValue({ id: "res_1" });
|
||||||
const auditCreate = vi.fn().mockResolvedValue({ id: "audit_1" });
|
const auditCreate = vi.fn().mockResolvedValue({ id: "audit_1" });
|
||||||
|
const db: Record<string, unknown> = {
|
||||||
const result = await importCsv(
|
|
||||||
createContext(
|
|
||||||
{
|
|
||||||
resource: {
|
resource: {
|
||||||
findFirst: resourceFindFirst,
|
findFirst: resourceFindFirst,
|
||||||
update: resourceUpdate,
|
update: resourceUpdate,
|
||||||
@@ -144,9 +141,11 @@ describe("import-export procedure support", () => {
|
|||||||
auditLog: {
|
auditLog: {
|
||||||
create: auditCreate,
|
create: auditCreate,
|
||||||
},
|
},
|
||||||
},
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
[PermissionKey.IMPORT_DATA],
|
};
|
||||||
),
|
|
||||||
|
const result = await importCsv(
|
||||||
|
createContext(db, [PermissionKey.IMPORT_DATA]),
|
||||||
{
|
{
|
||||||
entityType: "resources",
|
entityType: "resources",
|
||||||
rows: [
|
rows: [
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ describe("import-export router", () => {
|
|||||||
});
|
});
|
||||||
const resourceUpdate = vi.fn().mockResolvedValue({ id: "res_1" });
|
const resourceUpdate = vi.fn().mockResolvedValue({ id: "res_1" });
|
||||||
const auditCreate = vi.fn().mockResolvedValue({ id: "audit_1" });
|
const auditCreate = vi.fn().mockResolvedValue({ id: "audit_1" });
|
||||||
|
const importDb: Record<string, unknown> = {
|
||||||
const caller = createProtectedCaller(
|
|
||||||
{
|
|
||||||
resource: {
|
resource: {
|
||||||
findFirst: resourceFindFirst,
|
findFirst: resourceFindFirst,
|
||||||
update: resourceUpdate,
|
update: resourceUpdate,
|
||||||
@@ -83,7 +81,11 @@ describe("import-export router", () => {
|
|||||||
auditLog: {
|
auditLog: {
|
||||||
create: auditCreate,
|
create: auditCreate,
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
importDb["$transaction"] = vi.fn(async (fn: (tx: unknown) => unknown) => fn(importDb));
|
||||||
|
|
||||||
|
const caller = createProtectedCaller(
|
||||||
|
importDb,
|
||||||
{
|
{
|
||||||
role: SystemRole.MANAGER,
|
role: SystemRole.MANAGER,
|
||||||
granted: [PermissionKey.IMPORT_DATA],
|
granted: [PermissionKey.IMPORT_DATA],
|
||||||
|
|||||||
@@ -221,13 +221,14 @@ describe("project router", () => {
|
|||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("creates a project and returns its id", async () => {
|
it("creates a project and returns its id", async () => {
|
||||||
const created = { ...sampleProject, id: "project_new" };
|
const created = { ...sampleProject, id: "project_new" };
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
project: {
|
project: {
|
||||||
findUnique: vi.fn().mockResolvedValue(null), // no shortCode conflict
|
findUnique: vi.fn().mockResolvedValue(null), // no shortCode conflict
|
||||||
create: vi.fn().mockResolvedValue(created),
|
create: vi.fn().mockResolvedValue(created),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
webhook: { findMany: vi.fn().mockResolvedValue([]) },
|
webhook: { findMany: vi.fn().mockResolvedValue([]) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -253,13 +254,14 @@ describe("project router", () => {
|
|||||||
vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook unavailable"));
|
vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook unavailable"));
|
||||||
|
|
||||||
const created = { ...sampleProject, id: "project_safe_create" };
|
const created = { ...sampleProject, id: "project_safe_create" };
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
project: {
|
project: {
|
||||||
findUnique: vi.fn().mockResolvedValue(null),
|
findUnique: vi.fn().mockResolvedValue(null),
|
||||||
create: vi.fn().mockResolvedValue(created),
|
create: vi.fn().mockResolvedValue(created),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
webhook: { findMany: vi.fn().mockResolvedValue([]) },
|
webhook: { findMany: vi.fn().mockResolvedValue([]) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -451,12 +453,13 @@ describe("project router", () => {
|
|||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("updates project fields", async () => {
|
it("updates project fields", async () => {
|
||||||
const updated = { ...sampleProject, name: "Updated Name" };
|
const updated = { ...sampleProject, name: "Updated Name" };
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
project: {
|
project: {
|
||||||
findUnique: vi.fn().mockResolvedValue(sampleProject),
|
findUnique: vi.fn().mockResolvedValue(sampleProject),
|
||||||
update: vi.fn().mockResolvedValue(updated),
|
update: vi.fn().mockResolvedValue(updated),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -558,14 +561,12 @@ describe("project router", () => {
|
|||||||
|
|
||||||
describe("batchUpdateStatus", () => {
|
describe("batchUpdateStatus", () => {
|
||||||
it("updates multiple projects and returns count", async () => {
|
it("updates multiple projects and returns count", async () => {
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
project: {
|
project: {
|
||||||
update: vi.fn().mockResolvedValue(sampleProject),
|
update: vi.fn().mockResolvedValue(sampleProject),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
$transaction: vi.fn((calls: unknown[]) =>
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
Promise.all((calls as Promise<unknown>[]).map(() => sampleProject)),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -575,7 +576,7 @@ describe("project router", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.count).toBe(3);
|
expect(result.count).toBe(3);
|
||||||
expect(db.auditLog.create).toHaveBeenCalled();
|
expect((db.auditLog as Record<string, ReturnType<typeof vi.fn>>).create).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -328,12 +328,13 @@ describe("resource router CRUD", () => {
|
|||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("creates a resource and returns it", async () => {
|
it("creates a resource and returns it", async () => {
|
||||||
const created = { ...sampleResource, id: "res_new", resourceRoles: [] };
|
const created = { ...sampleResource, id: "res_new", resourceRoles: [] };
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
resource: {
|
resource: {
|
||||||
findFirst: vi.fn().mockResolvedValue(null),
|
findFirst: vi.fn().mockResolvedValue(null),
|
||||||
create: vi.fn().mockResolvedValue(created),
|
create: vi.fn().mockResolvedValue(created),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -399,13 +400,14 @@ describe("resource router CRUD", () => {
|
|||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("updates resource fields", async () => {
|
it("updates resource fields", async () => {
|
||||||
const updated = { ...sampleResource, displayName: "Alice Updated" };
|
const updated = { ...sampleResource, displayName: "Alice Updated" };
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
resource: {
|
resource: {
|
||||||
findUnique: vi.fn().mockResolvedValue(sampleResource),
|
findUnique: vi.fn().mockResolvedValue(sampleResource),
|
||||||
update: vi.fn().mockResolvedValue(updated),
|
update: vi.fn().mockResolvedValue(updated),
|
||||||
},
|
},
|
||||||
resourceRole: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
|
resourceRole: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -443,11 +445,12 @@ describe("resource router CRUD", () => {
|
|||||||
describe("deactivate", () => {
|
describe("deactivate", () => {
|
||||||
it("sets isActive to false", async () => {
|
it("sets isActive to false", async () => {
|
||||||
const deactivated = { ...sampleResource, isActive: false };
|
const deactivated = { ...sampleResource, isActive: false };
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
resource: {
|
resource: {
|
||||||
update: vi.fn().mockResolvedValue(deactivated),
|
update: vi.fn().mockResolvedValue(deactivated),
|
||||||
},
|
},
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -469,12 +472,13 @@ describe("resource router CRUD", () => {
|
|||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce({ ...sampleResource, id: "res_1", isActive: false })
|
.mockResolvedValueOnce({ ...sampleResource, id: "res_1", isActive: false })
|
||||||
.mockResolvedValueOnce({ ...sampleResource, id: "res_2", isActive: false });
|
.mockResolvedValueOnce({ ...sampleResource, id: "res_2", isActive: false });
|
||||||
const db = {
|
const auditCreate = vi.fn().mockResolvedValue({});
|
||||||
|
const db: Record<string, unknown> = {
|
||||||
resource: {
|
resource: {
|
||||||
update,
|
update,
|
||||||
},
|
},
|
||||||
$transaction: vi.fn(async (operations: Promise<unknown>[]) => Promise.all(operations)),
|
auditLog: { create: auditCreate },
|
||||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const caller = createManagerCaller(db);
|
const caller = createManagerCaller(db);
|
||||||
@@ -482,7 +486,7 @@ describe("resource router CRUD", () => {
|
|||||||
|
|
||||||
expect(result).toEqual({ count: 2 });
|
expect(result).toEqual({ count: 2 });
|
||||||
expect(db.$transaction).toHaveBeenCalledTimes(1);
|
expect(db.$transaction).toHaveBeenCalledTimes(1);
|
||||||
expect(db.auditLog.create).toHaveBeenCalledWith(
|
expect(auditCreate).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
entityType: "Resource",
|
entityType: "Resource",
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ describe("role procedure support", () => {
|
|||||||
color: "#111111",
|
color: "#111111",
|
||||||
_count: { resourceRoles: 2 },
|
_count: { resourceRoles: 2 },
|
||||||
};
|
};
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
role: {
|
role: {
|
||||||
findUnique: vi.fn().mockResolvedValue(null),
|
findUnique: vi.fn().mockResolvedValue(null),
|
||||||
create: vi.fn().mockResolvedValue(role),
|
create: vi.fn().mockResolvedValue(role),
|
||||||
@@ -230,6 +230,7 @@ describe("role procedure support", () => {
|
|||||||
auditLog: {
|
auditLog: {
|
||||||
create: vi.fn().mockResolvedValue(undefined),
|
create: vi.fn().mockResolvedValue(undefined),
|
||||||
},
|
},
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await createRole(createContext(db), {
|
const result = await createRole(createContext(db), {
|
||||||
@@ -300,7 +301,7 @@ describe("role procedure support", () => {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
_count: { resourceRoles: 1 },
|
_count: { resourceRoles: 1 },
|
||||||
};
|
};
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
role: {
|
role: {
|
||||||
findUnique: vi.fn()
|
findUnique: vi.fn()
|
||||||
.mockResolvedValueOnce(existing)
|
.mockResolvedValueOnce(existing)
|
||||||
@@ -312,6 +313,7 @@ describe("role procedure support", () => {
|
|||||||
auditLog: {
|
auditLog: {
|
||||||
create: vi.fn().mockResolvedValue(undefined),
|
create: vi.fn().mockResolvedValue(undefined),
|
||||||
},
|
},
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await updateRole(createContext(db), UpdateRoleProcedureInputSchema.parse({
|
const result = await updateRole(createContext(db), UpdateRoleProcedureInputSchema.parse({
|
||||||
@@ -371,7 +373,7 @@ describe("role procedure support", () => {
|
|||||||
countPlanningEntries.mockResolvedValue({
|
countPlanningEntries.mockResolvedValue({
|
||||||
countsByRoleId: new Map([["role_fx", 4]]),
|
countsByRoleId: new Map([["role_fx", 4]]),
|
||||||
});
|
});
|
||||||
const db = {
|
const db: Record<string, unknown> = {
|
||||||
role: {
|
role: {
|
||||||
update: vi.fn().mockResolvedValue({
|
update: vi.fn().mockResolvedValue({
|
||||||
id: "role_fx",
|
id: "role_fx",
|
||||||
@@ -385,6 +387,7 @@ describe("role procedure support", () => {
|
|||||||
auditLog: {
|
auditLog: {
|
||||||
create: vi.fn().mockResolvedValue(undefined),
|
create: vi.fn().mockResolvedValue(undefined),
|
||||||
},
|
},
|
||||||
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await deactivateRole(createContext(db), RoleIdInputSchema.parse({ id: "role_fx" }));
|
const result = await deactivateRole(createContext(db), RoleIdInputSchema.parse({ id: "role_fx" }));
|
||||||
|
|||||||
@@ -143,15 +143,14 @@ describe("role router authorization", () => {
|
|||||||
// planningEntry count queries (attachZeroAllocationCount path)
|
// planningEntry count queries (attachZeroAllocationCount path)
|
||||||
const planningEntryFindMany = vi.fn().mockResolvedValue([]);
|
const planningEntryFindMany = vi.fn().mockResolvedValue([]);
|
||||||
|
|
||||||
const caller = createCaller(
|
const db: Record<string, unknown> = {
|
||||||
createContext(
|
|
||||||
{
|
|
||||||
role: { create: roleCreate, findUnique: roleFindUnique },
|
role: { create: roleCreate, findUnique: roleFindUnique },
|
||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
planningEntry: { findMany: planningEntryFindMany },
|
planningEntry: { findMany: planningEntryFindMany },
|
||||||
},
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
|
||||||
{ role: SystemRole.MANAGER },
|
};
|
||||||
),
|
const caller = createCaller(
|
||||||
|
createContext(db, { role: SystemRole.MANAGER }),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should not throw UNAUTHORIZED or FORBIDDEN
|
// Should not throw UNAUTHORIZED or FORBIDDEN
|
||||||
@@ -180,9 +179,7 @@ describe("role router authorization", () => {
|
|||||||
const demandRequirementFindMany = vi.fn().mockResolvedValue([]);
|
const demandRequirementFindMany = vi.fn().mockResolvedValue([]);
|
||||||
const assignmentFindMany = vi.fn().mockResolvedValue([]);
|
const assignmentFindMany = vi.fn().mockResolvedValue([]);
|
||||||
|
|
||||||
const caller = createCaller(
|
const adminDb: Record<string, unknown> = {
|
||||||
createContext(
|
|
||||||
{
|
|
||||||
role: {
|
role: {
|
||||||
findUnique: roleFindUnique,
|
findUnique: roleFindUnique,
|
||||||
delete: roleDelete,
|
delete: roleDelete,
|
||||||
@@ -190,9 +187,10 @@ describe("role router authorization", () => {
|
|||||||
auditLog: { create: auditLogCreate },
|
auditLog: { create: auditLogCreate },
|
||||||
demandRequirement: { findMany: demandRequirementFindMany },
|
demandRequirement: { findMany: demandRequirementFindMany },
|
||||||
assignment: { findMany: assignmentFindMany },
|
assignment: { findMany: assignmentFindMany },
|
||||||
},
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(adminDb)),
|
||||||
{ role: SystemRole.ADMIN },
|
};
|
||||||
),
|
const caller = createCaller(
|
||||||
|
createContext(adminDb, { role: SystemRole.ADMIN }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await caller.delete({ id: "role_1" });
|
const result = await caller.delete({ id: "role_1" });
|
||||||
|
|||||||
@@ -302,10 +302,7 @@ export async function batchUpdateAllocationStatusWithAudit(
|
|||||||
).allocation),
|
).allocation),
|
||||||
);
|
);
|
||||||
|
|
||||||
return updatedAllocations;
|
await tx.auditLog.create({
|
||||||
});
|
|
||||||
|
|
||||||
await db.auditLog.create({
|
|
||||||
data: {
|
data: {
|
||||||
entityType: "Allocation",
|
entityType: "Allocation",
|
||||||
entityId: input.ids.join(","),
|
entityId: input.ids.join(","),
|
||||||
@@ -314,5 +311,8 @@ export async function batchUpdateAllocationStatusWithAudit(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return updatedAllocations;
|
||||||
|
});
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,14 +238,15 @@ export async function applyEffortRules(
|
|||||||
const rules = toEffortRuleEngineInputs(ruleSet.rules);
|
const rules = toEffortRuleEngineInputs(ruleSet.rules);
|
||||||
const result = expandScopeToEffort(scopeItems, rules);
|
const result = expandScopeToEffort(scopeItems, rules);
|
||||||
|
|
||||||
|
await ctx.db.$transaction(async (tx) => {
|
||||||
if (input.mode === "replace") {
|
if (input.mode === "replace") {
|
||||||
await ctx.db.estimateDemandLine.deleteMany({
|
await tx.estimateDemandLine.deleteMany({
|
||||||
where: { estimateVersionId: version.id },
|
where: { estimateVersionId: version.id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.lines.length > 0) {
|
if (result.lines.length > 0) {
|
||||||
await ctx.db.estimateDemandLine.createMany({
|
await tx.estimateDemandLine.createMany({
|
||||||
data: buildEstimateDemandLineRows({
|
data: buildEstimateDemandLineRows({
|
||||||
estimateVersionId: version.id,
|
estimateVersionId: version.id,
|
||||||
currency: estimate.baseCurrency,
|
currency: estimate.baseCurrency,
|
||||||
@@ -255,7 +256,7 @@ export async function applyEffortRules(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Estimate",
|
entityType: "Estimate",
|
||||||
entityId: estimate.id,
|
entityId: estimate.id,
|
||||||
@@ -274,6 +275,7 @@ export async function applyEffortRules(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linesGenerated: result.lines.length,
|
linesGenerated: result.lines.length,
|
||||||
|
|||||||
@@ -65,12 +65,13 @@ export const estimateCommercialProcedures = {
|
|||||||
|
|
||||||
const validated = CommercialTermsSchema.parse(input.terms);
|
const validated = CommercialTermsSchema.parse(input.terms);
|
||||||
|
|
||||||
await ctx.db.estimateVersion.update({
|
await ctx.db.$transaction(async (tx) => {
|
||||||
|
await tx.estimateVersion.update({
|
||||||
where: { id: version.id },
|
where: { id: version.id },
|
||||||
data: { commercialTerms: validated as unknown as Prisma.InputJsonValue },
|
data: { commercialTerms: validated as unknown as Prisma.InputJsonValue },
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Estimate",
|
entityType: "Estimate",
|
||||||
entityId: estimate.id,
|
entityId: estimate.id,
|
||||||
@@ -82,6 +83,7 @@ export const estimateCommercialProcedures = {
|
|||||||
} as Prisma.InputJsonValue,
|
} as Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return { versionId: version.id, terms: validated };
|
return { versionId: version.id, terms: validated };
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -100,30 +100,34 @@ export async function createEstimateRecord(
|
|||||||
await autoFillDemandLineRates(ctx.db, input.demandLines, input.projectId);
|
await autoFillDemandLineRates(ctx.db, input.demandLines, input.projectId);
|
||||||
const enrichedInput = { ...input, demandLines: enrichedLines };
|
const enrichedInput = { ...input, demandLines: enrichedLines };
|
||||||
|
|
||||||
const estimate = await createEstimate(
|
const estimate = await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db as unknown as Parameters<typeof createEstimate>[0],
|
const created = await createEstimate(
|
||||||
|
tx as unknown as Parameters<typeof createEstimate>[0],
|
||||||
withComputedMetrics(enrichedInput, input.baseCurrency),
|
withComputedMetrics(enrichedInput, input.baseCurrency),
|
||||||
);
|
);
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Estimate",
|
entityType: "Estimate",
|
||||||
entityId: estimate.id,
|
entityId: created.id,
|
||||||
action: "CREATE",
|
action: "CREATE",
|
||||||
...withAuditUser(ctx.dbUser?.id),
|
...withAuditUser(ctx.dbUser?.id),
|
||||||
changes: {
|
changes: {
|
||||||
after: {
|
after: {
|
||||||
id: estimate.id,
|
id: created.id,
|
||||||
name: estimate.name,
|
name: created.name,
|
||||||
status: estimate.status,
|
status: created.status,
|
||||||
projectId: estimate.projectId,
|
projectId: created.projectId,
|
||||||
latestVersionNumber: estimate.latestVersionNumber,
|
latestVersionNumber: created.latestVersionNumber,
|
||||||
autoFilledRateCardLines: autoFilledIndices.length,
|
autoFilledRateCardLines: autoFilledIndices.length,
|
||||||
},
|
},
|
||||||
} as Prisma.InputJsonValue,
|
} as Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return created;
|
||||||
|
});
|
||||||
|
|
||||||
return estimate;
|
return estimate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,10 +137,30 @@ export async function cloneEstimateRecord(
|
|||||||
) {
|
) {
|
||||||
let estimate;
|
let estimate;
|
||||||
try {
|
try {
|
||||||
estimate = await cloneEstimate(
|
estimate = await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db as unknown as Parameters<typeof cloneEstimate>[0],
|
const cloned = await cloneEstimate(
|
||||||
|
tx as unknown as Parameters<typeof cloneEstimate>[0],
|
||||||
input,
|
input,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tx.auditLog.create({
|
||||||
|
data: {
|
||||||
|
entityType: "Estimate",
|
||||||
|
entityId: cloned.id,
|
||||||
|
action: "CREATE",
|
||||||
|
...withAuditUser(ctx.dbUser?.id),
|
||||||
|
changes: {
|
||||||
|
after: {
|
||||||
|
id: cloned.id,
|
||||||
|
name: cloned.name,
|
||||||
|
clonedFrom: input.sourceEstimateId,
|
||||||
|
},
|
||||||
|
} as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return cloned;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rethrowEstimateRouterError(error, [
|
rethrowEstimateRouterError(error, [
|
||||||
{
|
{
|
||||||
@@ -146,22 +170,6 @@ export async function cloneEstimateRecord(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
|
||||||
data: {
|
|
||||||
entityType: "Estimate",
|
|
||||||
entityId: estimate.id,
|
|
||||||
action: "CREATE",
|
|
||||||
...withAuditUser(ctx.dbUser?.id),
|
|
||||||
changes: {
|
|
||||||
after: {
|
|
||||||
id: estimate.id,
|
|
||||||
name: estimate.name,
|
|
||||||
clonedFrom: input.sourceEstimateId,
|
|
||||||
},
|
|
||||||
} as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return estimate;
|
return estimate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,10 +202,35 @@ export async function updateEstimateDraftRecord(
|
|||||||
|
|
||||||
let estimate;
|
let estimate;
|
||||||
try {
|
try {
|
||||||
estimate = await updateEstimateDraft(
|
estimate = await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db as unknown as Parameters<typeof updateEstimateDraft>[0],
|
const updated = await updateEstimateDraft(
|
||||||
|
tx as unknown as Parameters<typeof updateEstimateDraft>[0],
|
||||||
withComputedMetrics(enrichedInput, input.baseCurrency ?? "EUR"),
|
withComputedMetrics(enrichedInput, input.baseCurrency ?? "EUR"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tx.auditLog.create({
|
||||||
|
data: {
|
||||||
|
entityType: "Estimate",
|
||||||
|
entityId: updated.id,
|
||||||
|
action: "UPDATE",
|
||||||
|
...withAuditUser(ctx.dbUser?.id),
|
||||||
|
changes: {
|
||||||
|
after: {
|
||||||
|
id: updated.id,
|
||||||
|
name: updated.name,
|
||||||
|
status: updated.status,
|
||||||
|
latestVersionNumber: updated.latestVersionNumber,
|
||||||
|
workingVersionId: updated.versions.find(
|
||||||
|
(version) => version.status === "WORKING",
|
||||||
|
)?.id,
|
||||||
|
autoFilledRateCardLines: autoFilledIndices.length,
|
||||||
|
},
|
||||||
|
} as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rethrowEstimateRouterError(error, [
|
rethrowEstimateRouterError(error, [
|
||||||
{
|
{
|
||||||
@@ -211,27 +244,6 @@ export async function updateEstimateDraftRecord(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
|
||||||
data: {
|
|
||||||
entityType: "Estimate",
|
|
||||||
entityId: estimate.id,
|
|
||||||
action: "UPDATE",
|
|
||||||
...withAuditUser(ctx.dbUser?.id),
|
|
||||||
changes: {
|
|
||||||
after: {
|
|
||||||
id: estimate.id,
|
|
||||||
name: estimate.name,
|
|
||||||
status: estimate.status,
|
|
||||||
latestVersionNumber: estimate.latestVersionNumber,
|
|
||||||
workingVersionId: estimate.versions.find(
|
|
||||||
(version) => version.status === "WORKING",
|
|
||||||
)?.id,
|
|
||||||
autoFilledRateCardLines: autoFilledIndices.length,
|
|
||||||
},
|
|
||||||
} as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return estimate;
|
return estimate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,10 +253,35 @@ export async function createEstimateExportRecord(
|
|||||||
) {
|
) {
|
||||||
let estimate;
|
let estimate;
|
||||||
try {
|
try {
|
||||||
estimate = await createEstimateExport(
|
estimate = await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db as unknown as Parameters<typeof createEstimateExport>[0],
|
const exported = await createEstimateExport(
|
||||||
|
tx as unknown as Parameters<typeof createEstimateExport>[0],
|
||||||
input,
|
input,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const exportedVersion = input.versionId
|
||||||
|
? exported.versions.find((version) => version.id === input.versionId)
|
||||||
|
: exported.versions[0];
|
||||||
|
|
||||||
|
await tx.auditLog.create({
|
||||||
|
data: {
|
||||||
|
entityType: "Estimate",
|
||||||
|
entityId: exported.id,
|
||||||
|
action: "UPDATE",
|
||||||
|
...withAuditUser(ctx.dbUser?.id),
|
||||||
|
changes: {
|
||||||
|
after: {
|
||||||
|
id: exported.id,
|
||||||
|
exportFormat: input.format,
|
||||||
|
exportCount: exportedVersion?.exports.length ?? null,
|
||||||
|
versionId: exportedVersion?.id ?? null,
|
||||||
|
},
|
||||||
|
} as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return exported;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rethrowEstimateRouterError(error, [
|
rethrowEstimateRouterError(error, [
|
||||||
{
|
{
|
||||||
@@ -258,27 +295,6 @@ export async function createEstimateExportRecord(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportedVersion = input.versionId
|
|
||||||
? estimate.versions.find((version) => version.id === input.versionId)
|
|
||||||
: estimate.versions[0];
|
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
|
||||||
data: {
|
|
||||||
entityType: "Estimate",
|
|
||||||
entityId: estimate.id,
|
|
||||||
action: "UPDATE",
|
|
||||||
...withAuditUser(ctx.dbUser?.id),
|
|
||||||
changes: {
|
|
||||||
after: {
|
|
||||||
id: estimate.id,
|
|
||||||
exportFormat: input.format,
|
|
||||||
exportCount: exportedVersion?.exports.length ?? null,
|
|
||||||
versionId: exportedVersion?.id ?? null,
|
|
||||||
},
|
|
||||||
} as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return estimate;
|
return estimate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,10 +304,36 @@ export async function createEstimatePlanningHandoffRecord(
|
|||||||
) {
|
) {
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = await createEstimatePlanningHandoff(
|
result = await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db as unknown as Parameters<typeof createEstimatePlanningHandoff>[0],
|
const handoff = await createEstimatePlanningHandoff(
|
||||||
|
tx as unknown as Parameters<typeof createEstimatePlanningHandoff>[0],
|
||||||
input,
|
input,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tx.auditLog.create({
|
||||||
|
data: {
|
||||||
|
entityType: "Estimate",
|
||||||
|
entityId: handoff.estimateId,
|
||||||
|
action: "UPDATE",
|
||||||
|
...withAuditUser(ctx.dbUser?.id),
|
||||||
|
changes: {
|
||||||
|
after: {
|
||||||
|
planningHandoff: {
|
||||||
|
versionId: handoff.estimateVersionId,
|
||||||
|
versionNumber: handoff.estimateVersionNumber,
|
||||||
|
projectId: handoff.projectId,
|
||||||
|
createdCount: handoff.createdCount,
|
||||||
|
assignedCount: handoff.assignedCount,
|
||||||
|
placeholderCount: handoff.placeholderCount,
|
||||||
|
fallbackPlaceholderCount: handoff.fallbackPlaceholderCount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return handoff;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rethrowEstimateRouterError(error, [
|
rethrowEstimateRouterError(error, [
|
||||||
{
|
{
|
||||||
@@ -319,28 +361,6 @@ export async function createEstimatePlanningHandoffRecord(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
|
||||||
data: {
|
|
||||||
entityType: "Estimate",
|
|
||||||
entityId: result.estimateId,
|
|
||||||
action: "UPDATE",
|
|
||||||
...withAuditUser(ctx.dbUser?.id),
|
|
||||||
changes: {
|
|
||||||
after: {
|
|
||||||
planningHandoff: {
|
|
||||||
versionId: result.estimateVersionId,
|
|
||||||
versionNumber: result.estimateVersionNumber,
|
|
||||||
projectId: result.projectId,
|
|
||||||
createdCount: result.createdCount,
|
|
||||||
assignedCount: result.assignedCount,
|
|
||||||
placeholderCount: result.placeholderCount,
|
|
||||||
fallbackPlaceholderCount: result.fallbackPlaceholderCount,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const allocation of result.allocations) {
|
for (const allocation of result.allocations) {
|
||||||
emitAllocationCreated({
|
emitAllocationCreated({
|
||||||
id: allocation.id,
|
id: allocation.id,
|
||||||
|
|||||||
@@ -21,10 +21,32 @@ export const estimateVersionWorkflowProcedures = {
|
|||||||
|
|
||||||
let estimate;
|
let estimate;
|
||||||
try {
|
try {
|
||||||
estimate = await submitEstimateVersion(
|
estimate = await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db as unknown as Parameters<typeof submitEstimateVersion>[0],
|
const submitted = await submitEstimateVersion(
|
||||||
|
tx as unknown as Parameters<typeof submitEstimateVersion>[0],
|
||||||
input,
|
input,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tx.auditLog.create({
|
||||||
|
data: {
|
||||||
|
entityType: "Estimate",
|
||||||
|
entityId: submitted.id,
|
||||||
|
action: "UPDATE",
|
||||||
|
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||||
|
changes: {
|
||||||
|
after: {
|
||||||
|
id: submitted.id,
|
||||||
|
status: submitted.status,
|
||||||
|
submittedVersionId: submitted.versions.find(
|
||||||
|
(version) => version.status === "SUBMITTED",
|
||||||
|
)?.id,
|
||||||
|
},
|
||||||
|
} as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return submitted;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rethrowEstimateRouterError(error, [
|
rethrowEstimateRouterError(error, [
|
||||||
{
|
{
|
||||||
@@ -41,24 +63,6 @@ export const estimateVersionWorkflowProcedures = {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
|
||||||
data: {
|
|
||||||
entityType: "Estimate",
|
|
||||||
entityId: estimate.id,
|
|
||||||
action: "UPDATE",
|
|
||||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
|
||||||
changes: {
|
|
||||||
after: {
|
|
||||||
id: estimate.id,
|
|
||||||
status: estimate.status,
|
|
||||||
submittedVersionId: estimate.versions.find(
|
|
||||||
(version) => version.status === "SUBMITTED",
|
|
||||||
)?.id,
|
|
||||||
},
|
|
||||||
} as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return estimate;
|
return estimate;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -69,10 +73,32 @@ export const estimateVersionWorkflowProcedures = {
|
|||||||
|
|
||||||
let estimate;
|
let estimate;
|
||||||
try {
|
try {
|
||||||
estimate = await approveEstimateVersion(
|
estimate = await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db as unknown as Parameters<typeof approveEstimateVersion>[0],
|
const approved = await approveEstimateVersion(
|
||||||
|
tx as unknown as Parameters<typeof approveEstimateVersion>[0],
|
||||||
input,
|
input,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tx.auditLog.create({
|
||||||
|
data: {
|
||||||
|
entityType: "Estimate",
|
||||||
|
entityId: approved.id,
|
||||||
|
action: "UPDATE",
|
||||||
|
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||||
|
changes: {
|
||||||
|
after: {
|
||||||
|
id: approved.id,
|
||||||
|
status: approved.status,
|
||||||
|
approvedVersionId: approved.versions.find(
|
||||||
|
(version) => version.status === "APPROVED",
|
||||||
|
)?.id,
|
||||||
|
},
|
||||||
|
} as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return approved;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rethrowEstimateRouterError(error, [
|
rethrowEstimateRouterError(error, [
|
||||||
{
|
{
|
||||||
@@ -89,24 +115,6 @@ export const estimateVersionWorkflowProcedures = {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
|
||||||
data: {
|
|
||||||
entityType: "Estimate",
|
|
||||||
entityId: estimate.id,
|
|
||||||
action: "UPDATE",
|
|
||||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
|
||||||
changes: {
|
|
||||||
after: {
|
|
||||||
id: estimate.id,
|
|
||||||
status: estimate.status,
|
|
||||||
approvedVersionId: estimate.versions.find(
|
|
||||||
(version) => version.status === "APPROVED",
|
|
||||||
)?.id,
|
|
||||||
},
|
|
||||||
} as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return estimate;
|
return estimate;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -117,10 +125,33 @@ export const estimateVersionWorkflowProcedures = {
|
|||||||
|
|
||||||
let estimate;
|
let estimate;
|
||||||
try {
|
try {
|
||||||
estimate = await createEstimateRevision(
|
estimate = await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db as unknown as Parameters<typeof createEstimateRevision>[0],
|
const revision = await createEstimateRevision(
|
||||||
|
tx as unknown as Parameters<typeof createEstimateRevision>[0],
|
||||||
input,
|
input,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tx.auditLog.create({
|
||||||
|
data: {
|
||||||
|
entityType: "Estimate",
|
||||||
|
entityId: revision.id,
|
||||||
|
action: "UPDATE",
|
||||||
|
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||||
|
changes: {
|
||||||
|
after: {
|
||||||
|
id: revision.id,
|
||||||
|
status: revision.status,
|
||||||
|
latestVersionNumber: revision.latestVersionNumber,
|
||||||
|
workingVersionId: revision.versions.find(
|
||||||
|
(version) => version.status === "WORKING",
|
||||||
|
)?.id,
|
||||||
|
},
|
||||||
|
} as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return revision;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rethrowEstimateRouterError(error, [
|
rethrowEstimateRouterError(error, [
|
||||||
{
|
{
|
||||||
@@ -138,25 +169,6 @@ export const estimateVersionWorkflowProcedures = {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
|
||||||
data: {
|
|
||||||
entityType: "Estimate",
|
|
||||||
entityId: estimate.id,
|
|
||||||
action: "UPDATE",
|
|
||||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
|
||||||
changes: {
|
|
||||||
after: {
|
|
||||||
id: estimate.id,
|
|
||||||
status: estimate.status,
|
|
||||||
latestVersionNumber: estimate.latestVersionNumber,
|
|
||||||
workingVersionId: estimate.versions.find(
|
|
||||||
(version) => version.status === "WORKING",
|
|
||||||
)?.id,
|
|
||||||
},
|
|
||||||
} as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return estimate;
|
return estimate;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -275,13 +275,14 @@ export async function applyExperienceMultiplierRules(
|
|||||||
const inputs = demandLines.map((line) => buildExperienceMultiplierInput(line));
|
const inputs = demandLines.map((line) => buildExperienceMultiplierInput(line));
|
||||||
const batch = applyExperienceMultipliersBatch(inputs, engineRules);
|
const batch = applyExperienceMultipliersBatch(inputs, engineRules);
|
||||||
|
|
||||||
let updatedCount = 0;
|
const updatedCount = await ctx.db.$transaction(async (tx) => {
|
||||||
|
let count = 0;
|
||||||
for (let i = 0; i < demandLines.length; i++) {
|
for (let i = 0; i < demandLines.length; i++) {
|
||||||
const line = demandLines[i]!;
|
const line = demandLines[i]!;
|
||||||
const result = batch.results[i]!;
|
const result = batch.results[i]!;
|
||||||
|
|
||||||
if (hasExperienceMultiplierChanges(line, result)) {
|
if (hasExperienceMultiplierChanges(line, result)) {
|
||||||
await ctx.db.estimateDemandLine.update({
|
await tx.estimateDemandLine.update({
|
||||||
where: { id: line.id },
|
where: { id: line.id },
|
||||||
data: buildExperienceMultiplierDemandLineUpdateData({
|
data: buildExperienceMultiplierDemandLineUpdateData({
|
||||||
line,
|
line,
|
||||||
@@ -289,11 +290,11 @@ export async function applyExperienceMultiplierRules(
|
|||||||
multiplierSet,
|
multiplierSet,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
updatedCount++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Estimate",
|
entityType: "Estimate",
|
||||||
entityId: estimate.id,
|
entityId: estimate.id,
|
||||||
@@ -304,7 +305,7 @@ export async function applyExperienceMultiplierRules(
|
|||||||
experienceMultipliersApplied: {
|
experienceMultipliersApplied: {
|
||||||
setId: multiplierSet.id,
|
setId: multiplierSet.id,
|
||||||
setName: multiplierSet.name,
|
setName: multiplierSet.name,
|
||||||
linesUpdated: updatedCount,
|
linesUpdated: count,
|
||||||
totalOriginalHours: batch.totalOriginalHours,
|
totalOriginalHours: batch.totalOriginalHours,
|
||||||
totalAdjustedHours: batch.totalAdjustedHours,
|
totalAdjustedHours: batch.totalAdjustedHours,
|
||||||
},
|
},
|
||||||
@@ -313,6 +314,9 @@ export async function applyExperienceMultiplierRules(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linesUpdated: updatedCount,
|
linesUpdated: updatedCount,
|
||||||
totalOriginalHours: batch.totalOriginalHours,
|
totalOriginalHours: batch.totalOriginalHours,
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export async function importCsv(ctx: ImportExportMutationContext, input: ImportC
|
|||||||
return { ...results, message: `Dry run: ${input.rows.length} rows validated` };
|
return { ...results, message: `Dry run: ${input.rows.length} rows validated` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ctx.db.$transaction(async (tx) => {
|
||||||
for (let index = 0; index < input.rows.length; index += 1) {
|
for (let index = 0; index < input.rows.length; index += 1) {
|
||||||
const row = input.rows[index];
|
const row = input.rows[index];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
@@ -167,7 +168,7 @@ export async function importCsv(ctx: ImportExportMutationContext, input: ImportC
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (input.entityType === "resources") {
|
if (input.entityType === "resources") {
|
||||||
const outcome = await importResourceRow(ctx, row);
|
const outcome = await importResourceRow({ ...ctx, db: tx as unknown as typeof ctx.db }, row);
|
||||||
if (outcome.updated) {
|
if (outcome.updated) {
|
||||||
results.updated += 1;
|
results.updated += 1;
|
||||||
} else if (outcome.error) {
|
} else if (outcome.error) {
|
||||||
@@ -182,7 +183,7 @@ export async function importCsv(ctx: ImportExportMutationContext, input: ImportC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: input.entityType,
|
entityType: input.entityType,
|
||||||
entityId: "bulk-import",
|
entityId: "bulk-import",
|
||||||
@@ -190,6 +191,7 @@ export async function importCsv(ctx: ImportExportMutationContext, input: ImportC
|
|||||||
changes: { summary: results },
|
changes: { summary: results },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,13 +123,14 @@ export function createProjectLifecycleProcedures(
|
|||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||||
const updated = await ctx.db.$transaction(
|
const updated = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const results = await Promise.all(
|
||||||
input.ids.map((id) =>
|
input.ids.map((id) =>
|
||||||
ctx.db.project.update({ where: { id }, data: { status: input.status } }),
|
tx.project.update({ where: { id }, data: { status: input.status } }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Project",
|
entityType: "Project",
|
||||||
entityId: input.ids.join(","),
|
entityId: input.ids.join(","),
|
||||||
@@ -138,6 +139,9 @@ export function createProjectLifecycleProcedures(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
|
||||||
dependencies.invalidateDashboardCacheInBackground();
|
dependencies.invalidateDashboardCacheInBackground();
|
||||||
return { count: updated.length };
|
return { count: updated.length };
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -80,19 +80,23 @@ export function createProjectMutationProcedures(
|
|||||||
target: BlueprintTarget.PROJECT,
|
target: BlueprintTarget.PROJECT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const project = await ctx.db.project.create({
|
const project = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const created = await tx.project.create({
|
||||||
data: buildProjectCreateData(input),
|
data: buildProjectCreateData(input),
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Project",
|
entityType: "Project",
|
||||||
entityId: project.id,
|
entityId: created.id,
|
||||||
action: "CREATE",
|
action: "CREATE",
|
||||||
changes: { after: project },
|
changes: { after: created },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return created;
|
||||||
|
});
|
||||||
|
|
||||||
backgroundEffects.invalidateDashboardCacheInBackground();
|
backgroundEffects.invalidateDashboardCacheInBackground();
|
||||||
backgroundEffects.dispatchProjectWebhookInBackground(ctx.db, "project.created", {
|
backgroundEffects.dispatchProjectWebhookInBackground(ctx.db, "project.created", {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
@@ -124,20 +128,24 @@ export function createProjectMutationProcedures(
|
|||||||
target: BlueprintTarget.PROJECT,
|
target: BlueprintTarget.PROJECT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updated = await ctx.db.project.update({
|
const updated = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const result = await tx.project.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: buildProjectUpdateData(input.data),
|
data: buildProjectUpdateData(input.data),
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Project",
|
entityType: "Project",
|
||||||
entityId: input.id,
|
entityId: input.id,
|
||||||
action: "UPDATE",
|
action: "UPDATE",
|
||||||
changes: { before: existing, after: updated },
|
changes: { before: existing, after: result },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
backgroundEffects.invalidateDashboardCacheInBackground();
|
backgroundEffects.invalidateDashboardCacheInBackground();
|
||||||
return updated;
|
return updated;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ export const resourceMutationProcedures = {
|
|||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "A resource can have at most one primary role" });
|
throw new TRPCError({ code: "BAD_REQUEST", message: "A resource can have at most one primary role" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = await ctx.db.resource.create({
|
const resource = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const created = await tx.resource.create({
|
||||||
data: {
|
data: {
|
||||||
eid: input.eid,
|
eid: input.eid,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
@@ -76,20 +77,23 @@ export const resourceMutationProcedures = {
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
} as unknown as Parameters<typeof ctx.db.resource.create>[0]["data"],
|
} as unknown as Parameters<typeof tx.resource.create>[0]["data"],
|
||||||
include: {
|
include: {
|
||||||
resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } } },
|
resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Resource",
|
entityType: "Resource",
|
||||||
entityId: resource.id,
|
entityId: created.id,
|
||||||
action: "CREATE",
|
action: "CREATE",
|
||||||
userId: ctx.dbUser?.id,
|
userId: ctx.dbUser?.id,
|
||||||
changes: { after: resource },
|
changes: { after: created },
|
||||||
} as unknown as Parameters<typeof ctx.db.auditLog.create>[0]["data"],
|
} as unknown as Parameters<typeof tx.auditLog.create>[0]["data"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return created;
|
||||||
});
|
});
|
||||||
|
|
||||||
return resource;
|
return resource;
|
||||||
@@ -121,7 +125,8 @@ export const resourceMutationProcedures = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await ctx.db.resource.update({
|
const updated = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const result = await tx.resource.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: {
|
data: {
|
||||||
...(input.data.displayName !== undefined ? { displayName: input.data.displayName } : {}),
|
...(input.data.displayName !== undefined ? { displayName: input.data.displayName } : {}),
|
||||||
@@ -156,16 +161,16 @@ export const resourceMutationProcedures = {
|
|||||||
...(input.data.enterpriseId !== undefined ? { enterpriseId: input.data.enterpriseId || null } : {}),
|
...(input.data.enterpriseId !== undefined ? { enterpriseId: input.data.enterpriseId || null } : {}),
|
||||||
...(input.data.clientUnitId !== undefined ? { clientUnitId: input.data.clientUnitId || null } : {}),
|
...(input.data.clientUnitId !== undefined ? { clientUnitId: input.data.clientUnitId || null } : {}),
|
||||||
...(input.data.fte !== undefined ? { fte: input.data.fte } : {}),
|
...(input.data.fte !== undefined ? { fte: input.data.fte } : {}),
|
||||||
} as unknown as Parameters<typeof ctx.db.resource.update>[0]["data"],
|
} as unknown as Parameters<typeof tx.resource.update>[0]["data"],
|
||||||
include: {
|
include: {
|
||||||
resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } } },
|
resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.data.roles !== undefined) {
|
if (input.data.roles !== undefined) {
|
||||||
await ctx.db.resourceRole.deleteMany({ where: { resourceId: input.id } });
|
await tx.resourceRole.deleteMany({ where: { resourceId: input.id } });
|
||||||
if (input.data.roles.length > 0) {
|
if (input.data.roles.length > 0) {
|
||||||
await ctx.db.resourceRole.createMany({
|
await tx.resourceRole.createMany({
|
||||||
data: input.data.roles.map((role) => ({
|
data: input.data.roles.map((role) => ({
|
||||||
resourceId: input.id,
|
resourceId: input.id,
|
||||||
roleId: role.roleId,
|
roleId: role.roleId,
|
||||||
@@ -175,15 +180,18 @@ export const resourceMutationProcedures = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Resource",
|
entityType: "Resource",
|
||||||
entityId: input.id,
|
entityId: input.id,
|
||||||
action: "UPDATE",
|
action: "UPDATE",
|
||||||
changes: { before: existing, after: updated },
|
changes: { before: existing, after: result },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -191,12 +199,13 @@ export const resourceMutationProcedures = {
|
|||||||
.input(z.object({ id: z.string() }))
|
.input(z.object({ id: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
||||||
const resource = await ctx.db.resource.update({
|
const resource = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const result = await tx.resource.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: { isActive: false },
|
data: { isActive: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Resource",
|
entityType: "Resource",
|
||||||
entityId: input.id,
|
entityId: input.id,
|
||||||
@@ -205,6 +214,9 @@ export const resourceMutationProcedures = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
return resource;
|
return resource;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -212,13 +224,14 @@ export const resourceMutationProcedures = {
|
|||||||
.input(z.object({ ids: z.array(z.string()).min(1).max(100) }))
|
.input(z.object({ ids: z.array(z.string()).min(1).max(100) }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
||||||
const updated = await ctx.db.$transaction(
|
const updated = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const results = await Promise.all(
|
||||||
input.ids.map((id) =>
|
input.ids.map((id) =>
|
||||||
ctx.db.resource.update({ where: { id }, data: { isActive: false } }),
|
tx.resource.update({ where: { id }, data: { isActive: false } }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Resource",
|
entityType: "Resource",
|
||||||
entityId: input.ids.join(","),
|
entityId: input.ids.join(","),
|
||||||
@@ -227,6 +240,9 @@ export const resourceMutationProcedures = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
|
||||||
return { count: updated.length };
|
return { count: updated.length };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -238,9 +254,10 @@ export const resourceMutationProcedures = {
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
||||||
|
|
||||||
await ctx.db.$transaction(
|
await ctx.db.$transaction(async (tx) => {
|
||||||
|
await Promise.all(
|
||||||
input.ids.map((id) =>
|
input.ids.map((id) =>
|
||||||
ctx.db.$executeRaw`
|
tx.$executeRaw`
|
||||||
UPDATE "Resource"
|
UPDATE "Resource"
|
||||||
SET "dynamicFields" = "dynamicFields" || ${JSON.stringify(input.fields)}::jsonb
|
SET "dynamicFields" = "dynamicFields" || ${JSON.stringify(input.fields)}::jsonb
|
||||||
WHERE id = ${id}
|
WHERE id = ${id}
|
||||||
@@ -248,7 +265,7 @@ export const resourceMutationProcedures = {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Resource",
|
entityType: "Resource",
|
||||||
entityId: input.ids.join(","),
|
entityId: input.ids.join(","),
|
||||||
@@ -256,6 +273,7 @@ export const resourceMutationProcedures = {
|
|||||||
changes: { after: { dynamicFields: input.fields, ids: input.ids } } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
changes: { after: { dynamicFields: input.fields, ids: input.ids } } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return { updated: input.ids.length };
|
return { updated: input.ids.length };
|
||||||
}),
|
}),
|
||||||
@@ -271,13 +289,12 @@ export const resourceMutationProcedures = {
|
|||||||
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
|
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.$transaction([
|
await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db.assignment.deleteMany({ where: { resourceId: input.id } }),
|
await tx.assignment.deleteMany({ where: { resourceId: input.id } });
|
||||||
ctx.db.vacation.deleteMany({ where: { resourceId: input.id } }),
|
await tx.vacation.deleteMany({ where: { resourceId: input.id } });
|
||||||
ctx.db.resource.delete({ where: { id: input.id } }),
|
await tx.resource.delete({ where: { id: input.id } });
|
||||||
]);
|
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Resource",
|
entityType: "Resource",
|
||||||
entityId: input.id,
|
entityId: input.id,
|
||||||
@@ -286,6 +303,7 @@ export const resourceMutationProcedures = {
|
|||||||
changes: { before: resource } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
changes: { before: resource } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return { deleted: true };
|
return { deleted: true };
|
||||||
}),
|
}),
|
||||||
@@ -298,13 +316,12 @@ export const resourceMutationProcedures = {
|
|||||||
select: { id: true, displayName: true, eid: true },
|
select: { id: true, displayName: true, eid: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.$transaction([
|
await ctx.db.$transaction(async (tx) => {
|
||||||
ctx.db.assignment.deleteMany({ where: { resourceId: { in: input.ids } } }),
|
await tx.assignment.deleteMany({ where: { resourceId: { in: input.ids } } });
|
||||||
ctx.db.vacation.deleteMany({ where: { resourceId: { in: input.ids } } }),
|
await tx.vacation.deleteMany({ where: { resourceId: { in: input.ids } } });
|
||||||
ctx.db.resource.deleteMany({ where: { id: { in: input.ids } } }),
|
await tx.resource.deleteMany({ where: { id: { in: input.ids } } });
|
||||||
]);
|
|
||||||
|
|
||||||
await ctx.db.auditLog.createMany({
|
await tx.auditLog.createMany({
|
||||||
data: resources.map((r) => ({
|
data: resources.map((r) => ({
|
||||||
entityType: "Resource",
|
entityType: "Resource",
|
||||||
entityId: r.id,
|
entityId: r.id,
|
||||||
@@ -313,6 +330,7 @@ export const resourceMutationProcedures = {
|
|||||||
changes: { before: r } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
changes: { before: r } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return { deleted: resources.length };
|
return { deleted: resources.length };
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -135,20 +135,24 @@ export async function createRole(
|
|||||||
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
|
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
|
||||||
await assertRoleNameAvailable(ctx.db, input.name);
|
await assertRoleNameAvailable(ctx.db, input.name);
|
||||||
|
|
||||||
const role = await ctx.db.role.create({
|
const role = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const created = await tx.role.create({
|
||||||
data: buildRoleCreateData(input),
|
data: buildRoleCreateData(input),
|
||||||
include: { _count: { select: { resourceRoles: true } } },
|
include: { _count: { select: { resourceRoles: true } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Role",
|
entityType: "Role",
|
||||||
entityId: role.id,
|
entityId: created.id,
|
||||||
action: "CREATE",
|
action: "CREATE",
|
||||||
changes: { after: role },
|
changes: { after: created },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return created;
|
||||||
|
});
|
||||||
|
|
||||||
emitRoleCreated({ id: role.id, name: role.name });
|
emitRoleCreated({ id: role.id, name: role.name });
|
||||||
|
|
||||||
return appendZeroAllocationCount(role);
|
return appendZeroAllocationCount(role);
|
||||||
@@ -168,21 +172,25 @@ export async function updateRole(
|
|||||||
await assertRoleNameAvailable(ctx.db, input.data.name, input.id);
|
await assertRoleNameAvailable(ctx.db, input.data.name, input.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await ctx.db.role.update({
|
const updated = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const result = await tx.role.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: buildRoleUpdateData(input.data),
|
data: buildRoleUpdateData(input.data),
|
||||||
include: { _count: { select: { resourceRoles: true } } },
|
include: { _count: { select: { resourceRoles: true } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Role",
|
entityType: "Role",
|
||||||
entityId: input.id,
|
entityId: input.id,
|
||||||
action: "UPDATE",
|
action: "UPDATE",
|
||||||
changes: { before: existing, after: updated },
|
changes: { before: existing, after: result },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
emitRoleUpdated({ id: updated.id, name: updated.name });
|
emitRoleUpdated({ id: updated.id, name: updated.name });
|
||||||
|
|
||||||
return attachSingleRolePlanningEntryCount(ctx.db, updated);
|
return attachSingleRolePlanningEntryCount(ctx.db, updated);
|
||||||
@@ -213,9 +221,10 @@ export async function deleteRole(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.role.delete({ where: { id: input.id } });
|
await ctx.db.$transaction(async (tx) => {
|
||||||
|
await tx.role.delete({ where: { id: input.id } });
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Role",
|
entityType: "Role",
|
||||||
entityId: input.id,
|
entityId: input.id,
|
||||||
@@ -223,6 +232,7 @@ export async function deleteRole(
|
|||||||
changes: { before: role },
|
changes: { before: role },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
emitRoleDeleted(input.id);
|
emitRoleDeleted(input.id);
|
||||||
|
|
||||||
@@ -234,13 +244,14 @@ export async function deactivateRole(
|
|||||||
input: z.infer<typeof RoleIdInputSchema>,
|
input: z.infer<typeof RoleIdInputSchema>,
|
||||||
) {
|
) {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
|
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
|
||||||
const role = await ctx.db.role.update({
|
const role = await ctx.db.$transaction(async (tx) => {
|
||||||
|
const result = await tx.role.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: { isActive: false },
|
data: { isActive: false },
|
||||||
include: { _count: { select: { resourceRoles: true } } },
|
include: { _count: { select: { resourceRoles: true } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.auditLog.create({
|
await tx.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entityType: "Role",
|
entityType: "Role",
|
||||||
entityId: input.id,
|
entityId: input.id,
|
||||||
@@ -249,6 +260,9 @@ export async function deactivateRole(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
emitRoleUpdated({ id: role.id, isActive: false });
|
emitRoleUpdated({ id: role.id, isActive: false });
|
||||||
|
|
||||||
return attachSingleRolePlanningEntryCount(ctx.db, role);
|
return attachSingleRolePlanningEntryCount(ctx.db, role);
|
||||||
|
|||||||
Reference in New Issue
Block a user