import { normalizeEstimateDemandLine, summarizeEstimateDemandLines } from "@capakraken/engine"; import { CreateEstimateSchema } from "@capakraken/shared"; import { z } from "zod"; import { lookupRate } 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; } const autoFilledIndices: number[] = []; const enriched = await Promise.all( demandLines.map(async (line, index) => { const isDefaultRate = line.costRateCents === 0 && line.billRateCents === 0; const hasExplicitSource = line.rateSource != null && line.rateSource.length > 0; if (!isDefaultRate || hasExplicitSource) { return line; } const result = await lookupRate(db, { clientId, chapter: line.chapter ?? null, roleId: line.roleId ?? null, }); if (!result) { return line; } autoFilledIndices.push(index); const existingMetadata = (line.metadata ?? {}) as Record; return { ...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 }; }