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
+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 ──