refactor(api): extract estimate read procedures

This commit is contained in:
2026-03-31 09:16:46 +02:00
parent 71b94d0ad1
commit aa47e4cb79
3 changed files with 409 additions and 217 deletions
+2 -217
View File
@@ -6,7 +6,6 @@ import {
createEstimatePlanningHandoff,
createEstimateRevision,
getEstimateById,
listEstimates,
submitEstimateVersion,
updateEstimateDraft,
} from "@capakraken/application";
@@ -27,7 +26,6 @@ import {
CreateEstimatePlanningHandoffSchema,
CreateEstimateSchema,
CreateEstimateRevisionSchema,
EstimateListFiltersSchema,
GenerateWeeklyPhasingSchema,
PermissionKey,
SubmitEstimateVersionSchema,
@@ -42,10 +40,10 @@ import {
controllerProcedure,
createTRPCRouter,
managerProcedure,
protectedProcedure,
requirePermission,
} from "../trpc.js";
import { emitAllocationCreated } from "../sse/event-bus.js";
import { estimateReadProcedures } from "./estimate-read.js";
type EstimateRouterErrorCode = "NOT_FOUND" | "PRECONDITION_FAILED";
@@ -245,220 +243,7 @@ async function autoFillDemandLineRates(
}
export const estimateRouter = createTRPCRouter({
list: controllerProcedure
.input(EstimateListFiltersSchema.default({}))
.query(async ({ ctx, input }) =>
listEstimates(
ctx.db as unknown as Parameters<typeof listEstimates>[0],
input,
)),
getById: controllerProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const estimate = await findUniqueOrThrow(
getEstimateById(
ctx.db as unknown as Parameters<typeof getEstimateById>[0],
input.id,
),
"Estimate",
);
return estimate;
}),
listVersions: controllerProcedure
.input(z.object({ estimateId: z.string() }))
.query(async ({ ctx, input }) => {
const estimate = await findUniqueOrThrow(
ctx.db.estimate.findUnique({
where: { id: input.estimateId },
select: {
id: true,
name: true,
status: true,
latestVersionNumber: true,
versions: {
orderBy: { versionNumber: "desc" },
select: {
id: true,
versionNumber: true,
label: true,
status: true,
notes: true,
lockedAt: true,
createdAt: true,
updatedAt: true,
_count: {
select: {
assumptions: true,
scopeItems: true,
demandLines: true,
resourceSnapshots: true,
exports: true,
},
},
},
},
},
}),
"Estimate",
);
return estimate;
}),
getVersionSnapshot: 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 },
select: {
id: true,
name: true,
status: true,
baseCurrency: true,
versions: {
...(input.versionId
? { where: { id: input.versionId } }
: { orderBy: { versionNumber: "desc" as const }, take: 1 }),
select: {
id: true,
versionNumber: true,
label: true,
status: true,
notes: true,
lockedAt: true,
createdAt: true,
updatedAt: true,
assumptions: {
select: { id: true, category: true, key: true, label: true },
},
scopeItems: {
select: { id: true, scopeType: true, sequenceNo: true, name: true },
orderBy: [{ sequenceNo: "asc" }, { name: "asc" }],
},
demandLines: {
select: {
id: true,
name: true,
chapter: true,
hours: true,
costTotalCents: true,
priceTotalCents: true,
currency: true,
},
},
resourceSnapshots: {
select: {
id: true,
displayName: true,
chapter: true,
currency: true,
lcrCents: true,
ucrCents: true,
},
},
exports: {
select: {
id: true,
format: true,
fileName: true,
createdAt: true,
},
orderBy: { createdAt: "desc" },
},
},
},
},
});
if (!estimate || estimate.versions.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "Estimate version not found" });
}
const version = estimate.versions[0]!;
const demandSummary = summarizeEstimateDemandLines(version.demandLines);
const chapterTotals = version.demandLines.reduce<Record<string, {
lineCount: number;
hours: number;
costTotalCents: number;
priceTotalCents: number;
currency: string;
}>>((acc, line) => {
const key = line.chapter ?? "Unassigned";
const current = acc[key] ?? {
lineCount: 0,
hours: 0,
costTotalCents: 0,
priceTotalCents: 0,
currency: line.currency,
};
current.lineCount += 1;
current.hours += line.hours;
current.costTotalCents += line.costTotalCents;
current.priceTotalCents += line.priceTotalCents;
acc[key] = current;
return acc;
}, {});
const scopeTypeTotals = version.scopeItems.reduce<Record<string, number>>((acc, item) => {
acc[item.scopeType] = (acc[item.scopeType] ?? 0) + 1;
return acc;
}, {});
const assumptionCategoryTotals = version.assumptions.reduce<Record<string, number>>((acc, assumption) => {
acc[assumption.category] = (acc[assumption.category] ?? 0) + 1;
return acc;
}, {});
return {
estimate: {
id: estimate.id,
name: estimate.name,
status: estimate.status,
baseCurrency: estimate.baseCurrency,
},
version: {
id: version.id,
versionNumber: version.versionNumber,
label: version.label,
status: version.status,
notes: version.notes,
lockedAt: version.lockedAt,
createdAt: version.createdAt,
updatedAt: version.updatedAt,
},
counts: {
assumptions: version.assumptions.length,
scopeItems: version.scopeItems.length,
demandLines: version.demandLines.length,
resourceSnapshots: version.resourceSnapshots.length,
exports: version.exports.length,
},
totals: {
hours: demandSummary.totalHours,
costTotalCents: demandSummary.totalCostCents,
priceTotalCents: demandSummary.totalPriceCents,
marginCents: demandSummary.marginCents,
marginPercent: demandSummary.marginPercent,
},
chapterBreakdown: Object.entries(chapterTotals)
.sort((left, right) => right[1].hours - left[1].hours)
.map(([chapter, totals]) => ({
chapter,
...totals,
})),
scopeTypeBreakdown: Object.entries(scopeTypeTotals)
.sort((left, right) => right[1] - left[1])
.map(([scopeType, count]) => ({ scopeType, count })),
assumptionCategoryBreakdown: Object.entries(assumptionCategoryTotals)
.sort((left, right) => right[1] - left[1])
.map(([category, count]) => ({ category, count })),
exports: version.exports,
};
}),
...estimateReadProcedures,
create: managerProcedure
.input(CreateEstimateSchema)