refactor(api): extract assistant estimate slice

This commit is contained in:
2026-03-30 21:57:16 +02:00
parent 18ba6fff9a
commit 91ab7898e9
3 changed files with 882 additions and 704 deletions
+2 -1
View File
@@ -34,12 +34,13 @@
- the remaining assistant user-admin helper cluster now lives in its own domain module, covering admin listing, user lifecycle mutations, permission overrides, resource linking, and MFA overrides without changing the assistant contract
- the authenticated user self-service assistant helpers now live in their own domain module, covering assignable users, dashboard preferences, favorites, column preferences, and MFA self-service without changing the assistant contract
- the embedded notification, task, reminder, and broadcast assistant helpers now live in their own domain module, keeping the collaboration workflow wiring out of the monolithic router without changing the assistant contract
- the estimate read and mutation helpers now live in their own domain module, keeping estimate lifecycle orchestration out of the monolithic assistant router without changing the assistant contract
## Next Up
Pin the next structural cleanup on the API side:
continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract.
The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the remaining estimate helpers or the project admin/helper clusters that are still in the monolithic router.
The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the remaining project admin/helper clusters that are still in the monolithic router.
## Remaining Major Themes
+17 -703
View File
@@ -6,17 +6,14 @@
import { Prisma, ImportBatchStatus, StagedRecordStatus, DispoStagedRecordType, VacationType } from "@capakraken/db";
import {
CreateAssignmentSchema,
type CreateEstimateInput,
CreateProjectSchema,
CreateResourceSchema,
AllocationStatus,
EstimateExportFormat,
EstimateStatus,
type CommentEntityType,
COMMENT_ENTITY_TYPE_VALUES,
PermissionKey,
SystemRole,
type UpdateEstimateDraftInput,
UpdateProjectSchema,
UpdateResourceSchema,
} from "@capakraken/shared";
@@ -108,6 +105,11 @@ import {
notificationInboxToolDefinitions,
notificationTaskToolDefinitions,
} from "./assistant-tools/notifications-tasks.js";
import {
createEstimateExecutors,
estimateMutationToolDefinitions,
estimateReadToolDefinitions,
} from "./assistant-tools/estimates.js";
import type { ToolContext, ToolDef, ToolExecutor } from "./assistant-tools/shared.js";
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../lib/comment-entity-registry.js";
@@ -2503,259 +2505,8 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
},
// ── ESTIMATES ──
{
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: "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: "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"],
},
},
},
{
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"],
},
},
},
...estimateReadToolDefinitions,
...estimateMutationToolDefinitions,
// ── ROLES ──
...rolesAnalyticsMutationToolDefinitions,
@@ -4383,453 +4134,16 @@ const executors = {
},
// ── ESTIMATES ──
async get_estimate_detail(params: { estimateId: string }, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.getById({ id: params.estimateId });
} catch (error) {
const mapped = toAssistantEstimateNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async list_estimate_versions(params: { estimateId: string }, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.listVersions({ estimateId: params.estimateId });
} catch (error) {
const mapped = toAssistantEstimateNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async get_estimate_version_snapshot(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.getVersionSnapshot({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = 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 = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
let estimate;
try {
estimate = await caller.create({
name: params.name,
...(projectId ? { 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 = 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 = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
let estimate;
try {
estimate = await caller.clone({
sourceEstimateId: params.sourceEstimateId,
...(params.name !== undefined ? { name: params.name } : {}),
...(projectId ? { projectId } : {}),
});
} catch (error) {
const mapped = 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 = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
let estimate;
try {
estimate = await caller.updateDraft({
id: params.id,
...(projectId ? { 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 = 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 = createEstimateCaller(createScopedCallerContext(ctx));
let estimate;
try {
estimate = await caller.submitVersion({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = 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 = createEstimateCaller(createScopedCallerContext(ctx));
let estimate;
try {
estimate = await caller.approveVersion({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = 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 = createEstimateCaller(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 = 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 = createEstimateCaller(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 = 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 = createEstimateCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.createPlanningHandoff({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = 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?: "even" | "front_loaded" | "back_loaded" | "custom";
}, ctx: ToolContext) {
const caller = createEstimateCaller(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 = 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 = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.getWeeklyPhasing({ estimateId: params.estimateId });
} catch (error) {
const mapped = toAssistantEstimateReadError(error, "weeklyPhasing");
if (mapped) {
return mapped;
}
throw error;
}
},
async get_estimate_commercial_terms(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.getCommercialTerms({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateReadError(error, "commercialTerms");
if (mapped) {
return mapped;
}
throw error;
}
},
async update_estimate_commercial_terms(params: {
estimateId: string;
versionId?: string;
terms: Record<string, unknown>;
}, ctx: ToolContext) {
const caller = createEstimateCaller(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 = 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}.`,
};
},
...createEstimateExecutors({
assertPermission,
createEstimateCaller,
createScopedCallerContext,
resolveProjectIdentifier,
toAssistantEstimateNotFoundError,
toAssistantEstimateReadError,
toAssistantEstimateCreationError,
toAssistantEstimateMutationError,
}),
// ── ROLES ──
@@ -0,0 +1,863 @@
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<unknown>;
listVersions: (params: { estimateId: string }) => Promise<unknown>;
getVersionSnapshot: (params: {
estimateId: string;
versionId?: string;
}) => Promise<unknown>;
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<EstimateRecord>;
clone: (params: {
sourceEstimateId: string;
name?: string;
projectId?: string;
}) => Promise<EstimateRecord>;
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<EstimateRecord>;
submitVersion: (params: {
estimateId: string;
versionId?: string;
}) => Promise<EstimateRecord>;
approveVersion: (params: {
estimateId: string;
versionId?: string;
}) => Promise<EstimateRecord>;
createRevision: (params: {
estimateId: string;
sourceVersionId?: string;
label?: string;
notes?: string;
}) => Promise<EstimateRecord>;
createExport: (params: {
estimateId: string;
versionId?: string;
format: EstimateExportFormat;
}) => Promise<EstimateRecord>;
createPlanningHandoff: (params: {
estimateId: string;
versionId?: string;
}) => Promise<object>;
generateWeeklyPhasing: (params: {
estimateId: string;
startDate: string;
endDate: string;
pattern?: WeeklyPhasingPattern;
}) => Promise<object>;
getWeeklyPhasing: (params: { estimateId: string }) => Promise<unknown>;
getCommercialTerms: (params: {
estimateId: string;
versionId?: string;
}) => Promise<unknown>;
updateCommercialTerms: (params: {
estimateId: string;
versionId?: string;
terms: Record<string, unknown>;
}) => Promise<object>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
resolveProjectIdentifier: (
ctx: ToolContext,
identifier: string,
) => Promise<ResolvedProject | AssistantToolErrorResult>;
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<string | AssistantToolErrorResult | undefined> {
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<string, ToolExecutor> {
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<string, unknown>;
}, 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}.`,
};
},
};
}