import { normalizeEstimateDemandLine, summarizeEstimateDemandLines } from "@capakraken/engine"; import { CreateEstimateSchema } from "@capakraken/shared"; import { z } from "zod"; import { lookupRatesBatch } from "../lib/rate-card-lookup.js"; function buildComputedMetrics(demandLines: z.infer["demandLines"]) { const summary = summarizeEstimateDemandLines(demandLines); return [ { key: "total_hours", label: "Total Hours", metricGroup: "summary", valueDecimal: summary.totalHours, metadata: {}, }, { key: "total_cost", label: "Total Cost", metricGroup: "summary", valueDecimal: summary.totalCostCents / 100, valueCents: summary.totalCostCents, currency: demandLines[0]?.currency ?? "EUR", metadata: {}, }, { key: "total_price", label: "Total Price", metricGroup: "summary", valueDecimal: summary.totalPriceCents / 100, valueCents: summary.totalPriceCents, currency: demandLines[0]?.currency ?? "EUR", metadata: {}, }, { key: "margin", label: "Margin", metricGroup: "summary", valueDecimal: summary.marginCents / 100, valueCents: summary.marginCents, currency: demandLines[0]?.currency ?? "EUR", metadata: {}, }, { key: "margin_percent", label: "Margin %", metricGroup: "summary", valueDecimal: summary.marginPercent, metadata: {}, }, ]; } function normalizeDemandLines< T extends { demandLines: z.infer["demandLines"]; resourceSnapshots: z.infer["resourceSnapshots"]; }, >(input: T, baseCurrency: string) { const snapshotsByResourceId = new Map( input.resourceSnapshots .filter( ( snapshot, ): snapshot is (typeof input.resourceSnapshots)[number] & { resourceId: string; } => typeof snapshot.resourceId === "string" && snapshot.resourceId.length > 0, ) .map((snapshot) => [snapshot.resourceId, snapshot]), ); return input.demandLines.map((line) => normalizeEstimateDemandLine(line, { resourceSnapshot: line.resourceId != null ? snapshotsByResourceId.get(line.resourceId) : null, defaultCurrency: baseCurrency, }), ); } export function withComputedMetrics< T extends { demandLines: z.infer["demandLines"]; resourceSnapshots: z.infer["resourceSnapshots"]; metrics: z.infer["metrics"]; }, >(input: T, baseCurrency: string) { const normalizedDemandLines = normalizeDemandLines(input, baseCurrency); const computedMetrics = buildComputedMetrics(normalizedDemandLines); const computedKeys = new Set(computedMetrics.map((metric) => metric.key)); return { ...input, demandLines: normalizedDemandLines, metrics: [ ...input.metrics.filter((metric) => !computedKeys.has(metric.key)), ...computedMetrics, ], }; } export async function autoFillDemandLineRates( // eslint-disable-next-line @typescript-eslint/no-explicit-any db: any, demandLines: z.infer["demandLines"], projectId?: string | null, ): Promise<{ demandLines: z.infer["demandLines"]; autoFilledIndices: number[]; }> { let clientId: string | null = null; if (projectId) { const project = await db.project.findUnique({ where: { id: projectId }, select: { clientId: true }, }); clientId = project?.clientId ?? null; } // Identify which lines need auto-fill and collect their lookup params const needsLookup: { index: number; params: { chapter: string | null; roleId: string | null }; }[] = []; for (let i = 0; i < demandLines.length; i++) { const line = demandLines[i]!; const isDefaultRate = line.costRateCents === 0 && line.billRateCents === 0; const hasExplicitSource = line.rateSource != null && line.rateSource.length > 0; if (isDefaultRate && !hasExplicitSource) { needsLookup.push({ index: i, params: { chapter: line.chapter ?? null, roleId: line.roleId ?? null }, }); } } if (needsLookup.length === 0) { return { demandLines, autoFilledIndices: [] }; } // Single DB query for all rate card lines, scored locally per demand line const results = await lookupRatesBatch( db, clientId, needsLookup.map((entry) => entry.params), ); const autoFilledIndices: number[] = []; const enriched = [...demandLines]; for (let i = 0; i < needsLookup.length; i++) { const result = results[i]; if (!result) continue; const { index } = needsLookup[i]!; autoFilledIndices.push(index); const line = demandLines[index]!; const existingMetadata = (line.metadata ?? {}) as Record; enriched[index] = { ...line, costRateCents: result.costRateCents, billRateCents: result.billRateCents, currency: result.currency, rateSource: `rate-card:${result.rateCardId}`, metadata: { ...existingMetadata, autoAppliedRateCard: { rateCardId: result.rateCardId, rateCardLineId: result.rateCardLineId, rateCardName: result.rateCardName, appliedAt: new Date().toISOString(), }, }, }; } return { demandLines: enriched, autoFilledIndices }; }