import type { CreateEstimateInput, EstimateExportFormat, EstimateStatus, PermissionKey, UpdateEstimateDraftInput, } from "@capakraken/shared"; import type { TRPCContext } from "../../trpc.js"; import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js"; type AssistantToolErrorResult = { error: string }; type EstimateMutationAction = | "clone" | "updateDraft" | "submitVersion" | "approveVersion" | "createRevision" | "createExport" | "createPlanningHandoff" | "generateWeeklyPhasing" | "updateCommercialTerms"; type WeeklyPhasingPattern = "even" | "front_loaded" | "back_loaded" | "custom"; type ResolvedProject = { id: string; }; type EstimateRecord = { id: string; name: string; }; type EstimateToolsDeps = { assertPermission: (ctx: ToolContext, perm: PermissionKey) => void; createEstimateCaller: (ctx: TRPCContext) => { getById: (params: { id: string }) => Promise; listVersions: (params: { estimateId: string }) => Promise; getVersionSnapshot: (params: { estimateId: string; versionId?: string; }) => Promise; create: (params: { name: string; projectId?: string; opportunityId?: string; baseCurrency?: string; status?: EstimateStatus; versionLabel?: string; versionNotes?: string; assumptions?: CreateEstimateInput["assumptions"]; scopeItems?: CreateEstimateInput["scopeItems"]; demandLines?: CreateEstimateInput["demandLines"]; resourceSnapshots?: CreateEstimateInput["resourceSnapshots"]; metrics?: CreateEstimateInput["metrics"]; }) => Promise; clone: (params: { sourceEstimateId: string; name?: string; projectId?: string; }) => Promise; updateDraft: (params: { id: string; projectId?: string; name?: string; opportunityId?: string; baseCurrency?: string; status?: EstimateStatus; versionLabel?: string; versionNotes?: string; assumptions?: UpdateEstimateDraftInput["assumptions"]; scopeItems?: UpdateEstimateDraftInput["scopeItems"]; demandLines?: UpdateEstimateDraftInput["demandLines"]; resourceSnapshots?: UpdateEstimateDraftInput["resourceSnapshots"]; metrics?: UpdateEstimateDraftInput["metrics"]; }) => Promise; submitVersion: (params: { estimateId: string; versionId?: string; }) => Promise; approveVersion: (params: { estimateId: string; versionId?: string; }) => Promise; createRevision: (params: { estimateId: string; sourceVersionId?: string; label?: string; notes?: string; }) => Promise; createExport: (params: { estimateId: string; versionId?: string; format: EstimateExportFormat; }) => Promise; createPlanningHandoff: (params: { estimateId: string; versionId?: string; }) => Promise; generateWeeklyPhasing: (params: { estimateId: string; startDate: string; endDate: string; pattern?: WeeklyPhasingPattern; }) => Promise; getWeeklyPhasing: (params: { estimateId: string }) => Promise; getCommercialTerms: (params: { estimateId: string; versionId?: string; }) => Promise; updateCommercialTerms: (params: { estimateId: string; versionId?: string; terms: Record; }) => Promise; }; createScopedCallerContext: (ctx: ToolContext) => TRPCContext; resolveProjectIdentifier: ( ctx: ToolContext, identifier: string, ) => Promise; toAssistantEstimateNotFoundError: ( error: unknown, ) => AssistantToolErrorResult | null; toAssistantEstimateReadError: ( error: unknown, context: "weeklyPhasing" | "commercialTerms", ) => AssistantToolErrorResult | null; toAssistantEstimateCreationError: ( error: unknown, ) => AssistantToolErrorResult | null; toAssistantEstimateMutationError: ( error: unknown, action: EstimateMutationAction, ) => AssistantToolErrorResult | null; }; async function resolveEstimateProjectId( ctx: ToolContext, deps: EstimateToolsDeps, params: { projectId?: string; projectCode?: string }, ): Promise { if (params.projectId) { return params.projectId; } if (!params.projectCode) { return undefined; } const project = await deps.resolveProjectIdentifier(ctx, params.projectCode); if ("error" in project) { return project; } return project.id; } export const estimateReadToolDefinitions: ToolDef[] = [ { type: "function", function: { name: "get_estimate_detail", description: "Get one estimate via the real estimate router, including versions, demand lines, metrics, assumptions, and linked project data. Controller/manager/admin access and viewCosts required.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID" }, }, required: ["estimateId"], }, }, }, { type: "function", function: { name: "list_estimate_versions", description: "List estimate versions via the real estimate router, including status, timestamps, and artifact counts. Controller/manager/admin access required.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID" }, }, required: ["estimateId"], }, }, }, { type: "function", function: { name: "get_estimate_version_snapshot", description: "Get an estimate version snapshot via the real estimate router, including totals, breakdowns, exports, and resource snapshots. Controller/manager/admin access and viewCosts required.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID" }, versionId: { type: "string", description: "Optional explicit version ID. Defaults to the latest version." }, }, required: ["estimateId"], }, }, }, { type: "function", function: { name: "get_estimate_weekly_phasing", description: "Get generated weekly phasing for an estimate via the real estimate router. Controller/manager/admin access required.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, }, required: ["estimateId"], }, }, }, { type: "function", function: { name: "get_estimate_commercial_terms", description: "Get estimate commercial terms via the real estimate router. Controller/manager/admin access required.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, versionId: { type: "string", description: "Optional explicit version ID." }, }, required: ["estimateId"], }, }, }, ]; export const estimateMutationToolDefinitions: ToolDef[] = [ { type: "function", function: { name: "create_estimate", description: "Create a new estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { projectId: { type: "string", description: "Optional project ID." }, projectCode: { type: "string", description: "Optional project short code convenience alias." }, name: { type: "string", description: "Estimate name." }, opportunityId: { type: "string", description: "Optional opportunity/reference ID." }, baseCurrency: { type: "string", description: "Base currency, e.g. EUR." }, status: { type: "string", enum: ["DRAFT", "IN_REVIEW", "APPROVED", "ARCHIVED"] }, versionLabel: { type: "string", description: "Optional working version label." }, versionNotes: { type: "string", description: "Optional working version notes." }, assumptions: { type: "array", items: { type: "object" }, description: "Estimate assumptions." }, scopeItems: { type: "array", items: { type: "object" }, description: "Estimate scope items." }, demandLines: { type: "array", items: { type: "object" }, description: "Estimate demand lines." }, resourceSnapshots: { type: "array", items: { type: "object" }, description: "Resource cost snapshots." }, metrics: { type: "array", items: { type: "object" }, description: "Optional metric overrides." }, }, required: ["name"], }, }, }, { type: "function", function: { name: "clone_estimate", description: "Clone an existing estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { sourceEstimateId: { type: "string", description: "Source estimate ID." }, name: { type: "string", description: "Optional cloned estimate name." }, projectId: { type: "string", description: "Optional target project ID." }, projectCode: { type: "string", description: "Optional target project short code convenience alias." }, }, required: ["sourceEstimateId"], }, }, }, { type: "function", function: { name: "update_estimate_draft", description: "Update the working draft of an estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { id: { type: "string", description: "Estimate ID." }, projectId: { type: "string", description: "Optional linked project ID." }, projectCode: { type: "string", description: "Optional linked project short code convenience alias." }, name: { type: "string" }, opportunityId: { type: "string" }, baseCurrency: { type: "string" }, status: { type: "string", enum: ["DRAFT", "IN_REVIEW", "APPROVED", "ARCHIVED"] }, versionLabel: { type: "string" }, versionNotes: { type: "string" }, assumptions: { type: "array", items: { type: "object" } }, scopeItems: { type: "array", items: { type: "object" } }, demandLines: { type: "array", items: { type: "object" } }, resourceSnapshots: { type: "array", items: { type: "object" } }, metrics: { type: "array", items: { type: "object" } }, }, required: ["id"], }, }, }, { type: "function", function: { name: "submit_estimate_version", description: "Submit an estimate working version for review via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, versionId: { type: "string", description: "Optional explicit version ID." }, }, required: ["estimateId"], }, }, }, { type: "function", function: { name: "approve_estimate_version", description: "Approve a submitted estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, versionId: { type: "string", description: "Optional explicit version ID." }, }, required: ["estimateId"], }, }, }, { type: "function", function: { name: "create_estimate_revision", description: "Create a new working revision from the latest locked estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, sourceVersionId: { type: "string", description: "Optional source version ID." }, label: { type: "string", description: "Optional revision label." }, notes: { type: "string", description: "Optional revision notes." }, }, required: ["estimateId"], }, }, }, { type: "function", function: { name: "create_estimate_export", description: "Create an estimate export artifact via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, versionId: { type: "string", description: "Optional explicit version ID." }, format: { type: "string", enum: ["XLSX", "CSV", "JSON", "SAP", "MMP"], description: "Export format." }, }, required: ["estimateId", "format"], }, }, }, { type: "function", function: { name: "create_estimate_planning_handoff", description: "Create planning allocations from an approved estimate version via the real estimate router. Manager/admin role and manageAllocations permission required. Always confirm first.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, versionId: { type: "string", description: "Optional explicit approved version ID." }, }, required: ["estimateId"], }, }, }, { type: "function", function: { name: "generate_estimate_weekly_phasing", description: "Generate weekly phasing for the working estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, startDate: { type: "string", description: "Start date in YYYY-MM-DD." }, endDate: { type: "string", description: "End date in YYYY-MM-DD." }, pattern: { type: "string", enum: ["even", "front_loaded", "back_loaded", "custom"], description: "Distribution pattern." }, }, required: ["estimateId", "startDate", "endDate"], }, }, }, { type: "function", function: { name: "update_estimate_commercial_terms", description: "Update estimate commercial terms on a working version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.", parameters: { type: "object", properties: { estimateId: { type: "string", description: "Estimate ID." }, versionId: { type: "string", description: "Optional explicit version ID." }, terms: { type: "object", description: "Commercial terms payload." }, }, required: ["estimateId", "terms"], }, }, }, ]; export function createEstimateExecutors( deps: EstimateToolsDeps, ): Record { return { async get_estimate_detail(params: { estimateId: string }, ctx: ToolContext) { deps.assertPermission(ctx, "viewCosts"); const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); try { return await caller.getById({ id: params.estimateId }); } catch (error) { const mapped = deps.toAssistantEstimateNotFoundError(error); if (mapped) { return mapped; } throw error; } }, async list_estimate_versions(params: { estimateId: string }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); try { return await caller.listVersions({ estimateId: params.estimateId }); } catch (error) { const mapped = deps.toAssistantEstimateNotFoundError(error); if (mapped) { return mapped; } throw error; } }, async get_estimate_version_snapshot(params: { estimateId: string; versionId?: string; }, ctx: ToolContext) { deps.assertPermission(ctx, "viewCosts"); const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); try { return await caller.getVersionSnapshot({ estimateId: params.estimateId, ...(params.versionId !== undefined ? { versionId: params.versionId } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateNotFoundError(error); if (mapped) { return mapped; } throw error; } }, async create_estimate(params: { projectId?: string; projectCode?: string; name: string; opportunityId?: string; baseCurrency?: string; status?: EstimateStatus; versionLabel?: string; versionNotes?: string; assumptions?: CreateEstimateInput["assumptions"]; scopeItems?: CreateEstimateInput["scopeItems"]; demandLines?: CreateEstimateInput["demandLines"]; resourceSnapshots?: CreateEstimateInput["resourceSnapshots"]; metrics?: CreateEstimateInput["metrics"]; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); const projectId = await resolveEstimateProjectId(ctx, deps, params); if (projectId && typeof projectId === "object" && "error" in projectId) { return projectId; } let estimate; try { estimate = await caller.create({ name: params.name, ...(typeof projectId === "string" ? { projectId } : {}), ...(params.opportunityId !== undefined ? { opportunityId: params.opportunityId } : {}), ...(params.baseCurrency !== undefined ? { baseCurrency: params.baseCurrency } : {}), ...(params.status !== undefined ? { status: params.status } : {}), ...(params.versionLabel !== undefined ? { versionLabel: params.versionLabel } : {}), ...(params.versionNotes !== undefined ? { versionNotes: params.versionNotes } : {}), ...(params.assumptions !== undefined ? { assumptions: params.assumptions } : {}), ...(params.scopeItems !== undefined ? { scopeItems: params.scopeItems } : {}), ...(params.demandLines !== undefined ? { demandLines: params.demandLines } : {}), ...(params.resourceSnapshots !== undefined ? { resourceSnapshots: params.resourceSnapshots } : {}), ...(params.metrics !== undefined ? { metrics: params.metrics } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateCreationError(error); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, estimate, estimateId: estimate.id, message: `Created estimate "${estimate.name}".`, }; }, async clone_estimate(params: { sourceEstimateId: string; name?: string; projectId?: string; projectCode?: string; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); const projectId = await resolveEstimateProjectId(ctx, deps, params); if (projectId && typeof projectId === "object" && "error" in projectId) { return projectId; } let estimate; try { estimate = await caller.clone({ sourceEstimateId: params.sourceEstimateId, ...(params.name !== undefined ? { name: params.name } : {}), ...(typeof projectId === "string" ? { projectId } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "clone"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, estimate, estimateId: estimate.id, message: `Cloned estimate "${estimate.name}".`, }; }, async update_estimate_draft(params: { id: string; projectId?: string; projectCode?: string; name?: string; opportunityId?: string; baseCurrency?: string; status?: EstimateStatus; versionLabel?: string; versionNotes?: string; assumptions?: UpdateEstimateDraftInput["assumptions"]; scopeItems?: UpdateEstimateDraftInput["scopeItems"]; demandLines?: UpdateEstimateDraftInput["demandLines"]; resourceSnapshots?: UpdateEstimateDraftInput["resourceSnapshots"]; metrics?: UpdateEstimateDraftInput["metrics"]; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); const projectId = await resolveEstimateProjectId(ctx, deps, params); if (projectId && typeof projectId === "object" && "error" in projectId) { return projectId; } let estimate; try { estimate = await caller.updateDraft({ id: params.id, ...(typeof projectId === "string" ? { projectId } : {}), ...(params.name !== undefined ? { name: params.name } : {}), ...(params.opportunityId !== undefined ? { opportunityId: params.opportunityId } : {}), ...(params.baseCurrency !== undefined ? { baseCurrency: params.baseCurrency } : {}), ...(params.status !== undefined ? { status: params.status } : {}), ...(params.versionLabel !== undefined ? { versionLabel: params.versionLabel } : {}), ...(params.versionNotes !== undefined ? { versionNotes: params.versionNotes } : {}), ...(params.assumptions !== undefined ? { assumptions: params.assumptions } : {}), ...(params.scopeItems !== undefined ? { scopeItems: params.scopeItems } : {}), ...(params.demandLines !== undefined ? { demandLines: params.demandLines } : {}), ...(params.resourceSnapshots !== undefined ? { resourceSnapshots: params.resourceSnapshots } : {}), ...(params.metrics !== undefined ? { metrics: params.metrics } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "updateDraft"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, estimate, estimateId: estimate.id, message: `Updated estimate draft "${estimate.name}".`, }; }, async submit_estimate_version(params: { estimateId: string; versionId?: string; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); let estimate; try { estimate = await caller.submitVersion({ estimateId: params.estimateId, ...(params.versionId !== undefined ? { versionId: params.versionId } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "submitVersion"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, estimate, estimateId: estimate.id, message: `Submitted estimate version for "${estimate.name}".`, }; }, async approve_estimate_version(params: { estimateId: string; versionId?: string; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); let estimate; try { estimate = await caller.approveVersion({ estimateId: params.estimateId, ...(params.versionId !== undefined ? { versionId: params.versionId } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "approveVersion"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, estimate, estimateId: estimate.id, message: `Approved estimate version for "${estimate.name}".`, }; }, async create_estimate_revision(params: { estimateId: string; sourceVersionId?: string; label?: string; notes?: string; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); let estimate; try { estimate = await caller.createRevision({ estimateId: params.estimateId, ...(params.sourceVersionId !== undefined ? { sourceVersionId: params.sourceVersionId } : {}), ...(params.label !== undefined ? { label: params.label } : {}), ...(params.notes !== undefined ? { notes: params.notes } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "createRevision"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, estimate, estimateId: estimate.id, message: `Created a new estimate revision for "${estimate.name}".`, }; }, async create_estimate_export(params: { estimateId: string; versionId?: string; format: EstimateExportFormat; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); let estimate; try { estimate = await caller.createExport({ estimateId: params.estimateId, format: params.format, ...(params.versionId !== undefined ? { versionId: params.versionId } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "createExport"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, estimate, estimateId: estimate.id, message: `Created ${params.format} export for estimate "${estimate.name}".`, }; }, async create_estimate_planning_handoff(params: { estimateId: string; versionId?: string; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); let result; try { result = await caller.createPlanningHandoff({ estimateId: params.estimateId, ...(params.versionId !== undefined ? { versionId: params.versionId } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "createPlanningHandoff"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate", "allocation", "timeline"], success: true, ...result, message: `Created planning handoff for estimate ${params.estimateId}.`, }; }, async generate_estimate_weekly_phasing(params: { estimateId: string; startDate: string; endDate: string; pattern?: WeeklyPhasingPattern; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); let result; try { result = await caller.generateWeeklyPhasing({ estimateId: params.estimateId, startDate: params.startDate, endDate: params.endDate, ...(params.pattern !== undefined ? { pattern: params.pattern } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "generateWeeklyPhasing"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, ...result, message: `Generated weekly phasing for estimate ${params.estimateId}.`, }; }, async get_estimate_weekly_phasing(params: { estimateId: string; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); try { return await caller.getWeeklyPhasing({ estimateId: params.estimateId }); } catch (error) { const mapped = deps.toAssistantEstimateReadError(error, "weeklyPhasing"); if (mapped) { return mapped; } throw error; } }, async get_estimate_commercial_terms(params: { estimateId: string; versionId?: string; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); try { return await caller.getCommercialTerms({ estimateId: params.estimateId, ...(params.versionId !== undefined ? { versionId: params.versionId } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateReadError(error, "commercialTerms"); if (mapped) { return mapped; } throw error; } }, async update_estimate_commercial_terms(params: { estimateId: string; versionId?: string; terms: Record; }, ctx: ToolContext) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); let result; try { result = await caller.updateCommercialTerms({ estimateId: params.estimateId, terms: params.terms, ...(params.versionId !== undefined ? { versionId: params.versionId } : {}), }); } catch (error) { const mapped = deps.toAssistantEstimateMutationError(error, "updateCommercialTerms"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["estimate"], success: true, ...result, message: `Updated commercial terms for estimate ${params.estimateId}.`, }; }, }; }