From 5f559e613d6734327d19ae7533157290904a629a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 09:21:43 +0200 Subject: [PATCH] refactor(api): extract estimate commercial procedures --- .../api/src/router/estimate-commercial.ts | 88 ++++++++++++++++++ packages/api/src/router/estimate.ts | 89 +------------------ 2 files changed, 90 insertions(+), 87 deletions(-) create mode 100644 packages/api/src/router/estimate-commercial.ts diff --git a/packages/api/src/router/estimate-commercial.ts b/packages/api/src/router/estimate-commercial.ts new file mode 100644 index 0000000..db88951 --- /dev/null +++ b/packages/api/src/router/estimate-commercial.ts @@ -0,0 +1,88 @@ +import type { Prisma } from "@capakraken/db"; +import { CommercialTermsSchema, PermissionKey, UpdateCommercialTermsSchema } from "@capakraken/shared"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { controllerProcedure, managerProcedure, requirePermission } from "../trpc.js"; + +export const estimateCommercialProcedures = { + getCommercialTerms: controllerProcedure + .input(z.object({ estimateId: z.string(), versionId: z.string().optional() })) + .query(async ({ ctx, input }) => { + const estimate = await ctx.db.estimate.findUnique({ + where: { id: input.estimateId }, + include: { + versions: { + ...(input.versionId + ? { where: { id: input.versionId } } + : { orderBy: { versionNumber: "desc" as const }, take: 1 }), + select: { id: true, commercialTerms: true }, + }, + }, + }); + + if (!estimate || estimate.versions.length === 0) { + throw new TRPCError({ code: "NOT_FOUND", message: "Estimate version not found" }); + } + + const version = estimate.versions[0]!; + const raw = version.commercialTerms; + const terms = raw + ? CommercialTermsSchema.parse(raw) + : CommercialTermsSchema.parse({}); + + return { versionId: version.id, terms }; + }), + + updateCommercialTerms: managerProcedure + .input(UpdateCommercialTermsSchema) + .mutation(async ({ ctx, input }) => { + requirePermission(ctx, PermissionKey.MANAGE_PROJECTS); + + const estimate = await ctx.db.estimate.findUnique({ + where: { id: input.estimateId }, + include: { + versions: { + ...(input.versionId + ? { where: { id: input.versionId } } + : { where: { status: "WORKING" }, take: 1 }), + select: { id: true, status: true }, + }, + }, + }); + + if (!estimate || estimate.versions.length === 0) { + throw new TRPCError({ code: "NOT_FOUND", message: "Estimate version not found" }); + } + + const version = estimate.versions[0]!; + + if (version.status !== "WORKING") { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "Commercial terms can only be edited on working versions", + }); + } + + const validated = CommercialTermsSchema.parse(input.terms); + + await ctx.db.estimateVersion.update({ + where: { id: version.id }, + data: { commercialTerms: validated as unknown as Prisma.InputJsonValue }, + }); + + await ctx.db.auditLog.create({ + data: { + entityType: "Estimate", + entityId: estimate.id, + action: "UPDATE", + ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}), + changes: { + field: "commercialTerms", + after: validated, + } as Prisma.InputJsonValue, + }, + }); + + return { versionId: version.id, terms: validated }; + }), +}; diff --git a/packages/api/src/router/estimate.ts b/packages/api/src/router/estimate.ts index 524a110..86f589b 100644 --- a/packages/api/src/router/estimate.ts +++ b/packages/api/src/router/estimate.ts @@ -21,7 +21,6 @@ import { import { ApproveEstimateVersionSchema, CloneEstimateSchema, - CommercialTermsSchema, CreateEstimateExportSchema, CreateEstimatePlanningHandoffSchema, CreateEstimateSchema, @@ -29,7 +28,6 @@ import { GenerateWeeklyPhasingSchema, PermissionKey, SubmitEstimateVersionSchema, - UpdateCommercialTermsSchema, UpdateEstimateDraftSchema, } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; @@ -43,6 +41,7 @@ import { requirePermission, } from "../trpc.js"; import { emitAllocationCreated } from "../sse/event-bus.js"; +import { estimateCommercialProcedures } from "./estimate-commercial.js"; import { estimateReadProcedures } from "./estimate-read.js"; type EstimateRouterErrorCode = "NOT_FOUND" | "PRECONDITION_FAILED"; @@ -244,6 +243,7 @@ async function autoFillDemandLineRates( export const estimateRouter = createTRPCRouter({ ...estimateReadProcedures, + ...estimateCommercialProcedures, create: managerProcedure .input(CreateEstimateSchema) @@ -834,91 +834,6 @@ export const estimateRouter = createTRPCRouter({ }; }), - // ─── Commercial Terms ─────────────────────────────────────────────────── - - getCommercialTerms: controllerProcedure - .input(z.object({ estimateId: z.string(), versionId: z.string().optional() })) - .query(async ({ ctx, input }) => { - const estimate = await ctx.db.estimate.findUnique({ - where: { id: input.estimateId }, - include: { - versions: { - ...(input.versionId - ? { where: { id: input.versionId } } - : { orderBy: { versionNumber: "desc" as const }, take: 1 }), - select: { id: true, commercialTerms: true }, - }, - }, - }); - - if (!estimate || estimate.versions.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Estimate version not found" }); - } - - const version = estimate.versions[0]!; - const raw = version.commercialTerms; - - // Parse stored JSON through Zod for type safety, fall back to defaults - const terms = raw - ? CommercialTermsSchema.parse(raw) - : CommercialTermsSchema.parse({}); - - return { versionId: version.id, terms }; - }), - - updateCommercialTerms: managerProcedure - .input(UpdateCommercialTermsSchema) - .mutation(async ({ ctx, input }) => { - requirePermission(ctx, PermissionKey.MANAGE_PROJECTS); - - const estimate = await ctx.db.estimate.findUnique({ - where: { id: input.estimateId }, - include: { - versions: { - ...(input.versionId - ? { where: { id: input.versionId } } - : { where: { status: "WORKING" }, take: 1 }), - select: { id: true, status: true }, - }, - }, - }); - - if (!estimate || estimate.versions.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Estimate version not found" }); - } - - const version = estimate.versions[0]!; - - if (version.status !== "WORKING") { - throw new TRPCError({ - code: "PRECONDITION_FAILED", - message: "Commercial terms can only be edited on working versions", - }); - } - - const validated = CommercialTermsSchema.parse(input.terms); - - await ctx.db.estimateVersion.update({ - where: { id: version.id }, - data: { commercialTerms: validated as unknown as Prisma.InputJsonValue }, - }); - - await ctx.db.auditLog.create({ - data: { - entityType: "Estimate", - entityId: estimate.id, - action: "UPDATE", - ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}), - changes: { - field: "commercialTerms", - after: validated, - } as Prisma.InputJsonValue, - }, - }); - - return { versionId: version.id, terms: validated }; - }), - // ─── Rate Card Lookup for Demand Lines ────────────────────────────────── lookupDemandLineRate: controllerProcedure