Files
CapaKraken/packages/api/src/router/estimate-version-workflow.ts
T

195 lines
5.3 KiB
TypeScript

import {
approveEstimateVersion,
createEstimateRevision,
submitEstimateVersion,
} from "@capakraken/application";
import type { Prisma } from "@capakraken/db";
import {
ApproveEstimateVersionSchema,
CreateEstimateRevisionSchema,
PermissionKey,
SubmitEstimateVersionSchema,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { managerProcedure, requirePermission } from "../trpc.js";
type EstimateRouterErrorCode = "NOT_FOUND" | "PRECONDITION_FAILED";
type EstimateRouterErrorRule = {
code: EstimateRouterErrorCode;
messages?: readonly string[];
predicates?: readonly ((message: string) => boolean)[];
};
function rethrowEstimateRouterError(
error: unknown,
rules: readonly EstimateRouterErrorRule[],
): never {
if (!(error instanceof Error)) {
throw error;
}
const matchingRule = rules.find(
(rule) =>
rule.messages?.includes(error.message) === true ||
rule.predicates?.some((predicate) => predicate(error.message)) === true,
);
if (matchingRule) {
throw new TRPCError({
code: matchingRule.code,
message: error.message,
});
}
throw error;
}
export const estimateVersionWorkflowProcedures = {
submitVersion: managerProcedure
.input(SubmitEstimateVersionSchema)
.mutation(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
let estimate;
try {
estimate = await submitEstimateVersion(
ctx.db as unknown as Parameters<typeof submitEstimateVersion>[0],
input,
);
} catch (error) {
rethrowEstimateRouterError(error, [
{
code: "NOT_FOUND",
messages: ["Estimate not found", "Estimate version not found"],
},
{
code: "PRECONDITION_FAILED",
messages: [
"Estimate has no working version",
"Only working versions can be submitted",
],
},
]);
}
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;
}),
approveVersion: managerProcedure
.input(ApproveEstimateVersionSchema)
.mutation(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
let estimate;
try {
estimate = await approveEstimateVersion(
ctx.db as unknown as Parameters<typeof approveEstimateVersion>[0],
input,
);
} catch (error) {
rethrowEstimateRouterError(error, [
{
code: "NOT_FOUND",
messages: ["Estimate not found", "Estimate version not found"],
},
{
code: "PRECONDITION_FAILED",
messages: [
"Estimate has no submitted version",
"Only submitted versions can be approved",
],
},
]);
}
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;
}),
createRevision: managerProcedure
.input(CreateEstimateRevisionSchema)
.mutation(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
let estimate;
try {
estimate = await createEstimateRevision(
ctx.db as unknown as Parameters<typeof createEstimateRevision>[0],
input,
);
} catch (error) {
rethrowEstimateRouterError(error, [
{
code: "NOT_FOUND",
messages: ["Estimate not found", "Estimate version not found"],
},
{
code: "PRECONDITION_FAILED",
messages: [
"Estimate already has a working version",
"Estimate has no locked version to revise",
"Source version must be locked before creating a revision",
],
},
]);
}
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;
}),
};