diff --git a/packages/api/src/router/estimate-version-workflow.ts b/packages/api/src/router/estimate-version-workflow.ts new file mode 100644 index 0000000..be9d2f4 --- /dev/null +++ b/packages/api/src/router/estimate-version-workflow.ts @@ -0,0 +1,194 @@ +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[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[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[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; + }), +}; diff --git a/packages/api/src/router/estimate.ts b/packages/api/src/router/estimate.ts index 86f589b..7bc8483 100644 --- a/packages/api/src/router/estimate.ts +++ b/packages/api/src/router/estimate.ts @@ -1,12 +1,9 @@ import { - approveEstimateVersion, cloneEstimate, createEstimateExport, createEstimate, createEstimatePlanningHandoff, - createEstimateRevision, getEstimateById, - submitEstimateVersion, updateEstimateDraft, } from "@capakraken/application"; import type { Prisma } from "@capakraken/db"; @@ -19,15 +16,12 @@ import { aggregateWeeklyByChapter, } from "@capakraken/engine"; import { - ApproveEstimateVersionSchema, CloneEstimateSchema, CreateEstimateExportSchema, CreateEstimatePlanningHandoffSchema, CreateEstimateSchema, - CreateEstimateRevisionSchema, GenerateWeeklyPhasingSchema, PermissionKey, - SubmitEstimateVersionSchema, UpdateEstimateDraftSchema, } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; @@ -43,6 +37,7 @@ import { import { emitAllocationCreated } from "../sse/event-bus.js"; import { estimateCommercialProcedures } from "./estimate-commercial.js"; import { estimateReadProcedures } from "./estimate-read.js"; +import { estimateVersionWorkflowProcedures } from "./estimate-version-workflow.js"; type EstimateRouterErrorCode = "NOT_FOUND" | "PRECONDITION_FAILED"; @@ -244,6 +239,7 @@ async function autoFillDemandLineRates( export const estimateRouter = createTRPCRouter({ ...estimateReadProcedures, ...estimateCommercialProcedures, + ...estimateVersionWorkflowProcedures, create: managerProcedure .input(CreateEstimateSchema) @@ -403,152 +399,6 @@ export const estimateRouter = createTRPCRouter({ return estimate; }), - 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[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[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[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; - }), - createExport: managerProcedure .input(CreateEstimateExportSchema) .mutation(async ({ ctx, input }) => {