Files
CapaKraken/packages/api/src/router/assistant-tools.ts
T
Hartmut dfeb4d361e fix(tests): align 20 drifted tests with current source behavior
Tests fell behind source changes: lastTotpAt replay-attack prevention,
activeSession invalidation on password reset, select clauses in
permission updates, UNAUTHORIZED (anti-enumeration) for disabled TOTP,
and password minimum raised from 8 to 12 characters.

Also fix root eslint.config.mjs to ignore packages/ (linted via turbo)
and add --no-warn-ignored to lint-staged to suppress warnings for
ignored files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:41:42 +02:00

2547 lines
79 KiB
TypeScript

/**
* AI Assistant Tool definitions for OpenAI Function Calling.
* Each tool has a JSON schema (for the AI) and an execute function (for the server).
*/
import { Prisma, VacationType } from "@capakraken/db";
import {
CreateAssignmentSchema,
AllocationStatus,
PermissionKey,
SystemRole,
toIsoDateOrNull,
} from "@capakraken/shared";
import type { WeekdayAvailability } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { ZodError } from "zod";
import { fmtEur } from "../lib/format-utils.js";
import { timelineRouter } from "./timeline.js";
import { logger } from "../lib/logger.js";
import { createCallerFactory, type TRPCContext } from "../trpc.js";
import { auditLogRouter } from "./audit-log.js";
import { chargeabilityReportRouter } from "./chargeability-report.js";
import { computationGraphRouter } from "./computation-graph.js";
import { dispoRouter } from "./dispo.js";
import { importExportRouter } from "./import-export.js";
import { resourceRouter } from "./resource.js";
import { settingsRouter } from "./settings.js";
import { systemRoleConfigRouter } from "./system-role-config.js";
import { userRouter } from "./user.js";
import { notificationRouter } from "./notification.js";
import { estimateRouter } from "./estimate.js";
import { webhookRouter } from "./webhook.js";
import { countryRouter } from "./country.js";
import { holidayCalendarRouter } from "./holiday-calendar.js";
import { blueprintRouter } from "./blueprint.js";
import { roleRouter } from "./role.js";
import { clientRouter } from "./client.js";
import { orgUnitRouter } from "./org-unit.js";
import { projectRouter } from "./project.js";
import { rateCardRouter } from "./rate-card.js";
import { reportRouter } from "./report.js";
import { vacationRouter } from "./vacation.js";
import { entitlementRouter } from "./entitlement.js";
import { commentRouter } from "./comment.js";
import { managementLevelRouter } from "./management-level.js";
import { utilizationCategoryRouter } from "./utilization-category.js";
import { calculationRuleRouter } from "./calculation-rules.js";
import { effortRuleRouter } from "./effort-rule.js";
import { experienceMultiplierRouter } from "./experience-multiplier.js";
import { dashboardRouter } from "./dashboard.js";
import { insightsRouter } from "./insights.js";
import { scenarioRouter } from "./scenario.js";
import { allocationRouter } from "./allocation/index.js";
import { staffingRouter } from "./staffing.js";
import {
advancedTimelineToolDefinitions,
createAdvancedTimelineExecutors,
} from "./assistant-tools/advanced-timeline.js";
import {
allocationPlanningMutationToolDefinitions,
allocationPlanningReadToolDefinitions,
createAllocationPlanningExecutors,
} from "./assistant-tools/allocation-planning.js";
import {
settingsAdminToolDefinitions,
createSettingsAdminExecutors,
} from "./assistant-tools/settings-admin.js";
import {
createVacationHolidayExecutors,
vacationHolidayMutationToolDefinitions,
vacationHolidayReadToolDefinitions,
} from "./assistant-tools/vacation-holidays.js";
import {
createRolesAnalyticsExecutors,
rolesAnalyticsMutationToolDefinitions,
rolesAnalyticsReadToolDefinitions,
} from "./assistant-tools/roles-analytics.js";
import {
clientMutationToolDefinitions,
createClientsOrgUnitsExecutors,
orgUnitMutationToolDefinitions,
} from "./assistant-tools/clients-org-units.js";
import {
chargeabilityComputationReadToolDefinitions,
createChargeabilityComputationExecutors,
} from "./assistant-tools/chargeability-computation.js";
import {
configReadmodelToolDefinitions,
createConfigReadmodelExecutors,
} from "./assistant-tools/config-readmodels.js";
import {
countryMetroAdminToolDefinitions,
createCountryMetroAdminExecutors,
} from "./assistant-tools/country-metro-admin.js";
import {
countryReadmodelToolDefinitions,
createCountryReadmodelExecutors,
} from "./assistant-tools/country-readmodels.js";
import {
createUserSelfServiceExecutors,
userSelfServiceToolDefinitions,
} from "./assistant-tools/user-self-service.js";
import {
createUserAdminExecutors,
userAdminToolDefinitions,
} from "./assistant-tools/user-admin.js";
import {
createNotificationsTasksExecutors,
notificationInboxToolDefinitions,
notificationTaskToolDefinitions,
} from "./assistant-tools/notifications-tasks.js";
import {
createEstimateExecutors,
estimateMutationToolDefinitions,
estimateReadToolDefinitions,
} from "./assistant-tools/estimates.js";
import {
createProjectExecutors,
projectMutationToolDefinitions,
projectReadToolDefinitions,
} from "./assistant-tools/projects.js";
import {
createStaffingDemandExecutors,
staffingDemandMutationToolDefinitions,
staffingDemandReadToolDefinitions,
} from "./assistant-tools/staffing-demand.js";
import {
createResourceExecutors,
resourceMutationToolDefinitions,
resourceReadToolDefinitions,
} from "./assistant-tools/resources.js";
import {
blueprintsRateCardsToolDefinitions,
createBlueprintsRateCardsExecutors,
} from "./assistant-tools/blueprints-rate-cards.js";
import {
createDashboardInsightsReportsExecutors,
dashboardInsightsReportsToolDefinitions,
} from "./assistant-tools/dashboard-insights-reports.js";
import {
createScenarioRateAnalysisExecutors,
scenarioRateAnalysisToolDefinitions,
} from "./assistant-tools/scenario-rate-analysis.js";
import {
createImportExportDispoExecutors,
importExportDispoToolDefinitions,
} from "./assistant-tools/import-export-dispo.js";
import {
commentMutationToolDefinitions,
commentReadToolDefinitions,
createCommentExecutors,
} from "./assistant-tools/comments.js";
import {
auditHistoryToolDefinitions,
createAuditHistoryExecutors,
} from "./assistant-tools/audit-history.js";
import {
createPlanningNavigationExecutors,
planningNavigationToolDefinitions,
} from "./assistant-tools/planning-navigation.js";
import {
createVacationEntitlementExecutors,
vacationEntitlementToolDefinitions,
} from "./assistant-tools/vacation-entitlements.js";
import {
withToolAccess,
type ToolAccessRequirements,
type ToolContext,
type ToolDef,
type ToolExecutor,
} from "./assistant-tools/shared.js";
export type { ToolContext } from "./assistant-tools/shared.js";
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
export const MUTATION_TOOLS = new Set([
"import_csv_data",
"update_system_settings",
"clear_stored_runtime_secrets",
"test_ai_connection",
"test_smtp_connection",
"test_gemini_connection",
"update_system_role_config",
"create_webhook",
"update_webhook",
"delete_webhook",
"test_webhook",
"stage_dispo_import_batch",
"cancel_dispo_import_batch",
"resolve_dispo_staged_record",
"commit_dispo_import_batch",
"create_allocation",
"cancel_allocation",
"update_allocation_status",
"update_timeline_allocation_inline",
"apply_timeline_project_shift",
"quick_assign_timeline_resource",
"batch_quick_assign_timeline_resources",
"batch_shift_timeline_allocations",
"update_resource",
"deactivate_resource",
"create_resource",
"update_project",
"create_project",
"delete_project",
"create_vacation",
"approve_vacation",
"reject_vacation",
"cancel_vacation",
"set_entitlement",
"create_demand",
"fill_demand",
"generate_project_cover",
"remove_project_cover",
"create_role",
"update_role",
"delete_role",
"create_client",
"update_client",
"delete_client",
"create_org_unit",
"update_org_unit",
"create_country",
"update_country",
"create_metro_city",
"update_metro_city",
"delete_metro_city",
"create_holiday_calendar",
"update_holiday_calendar",
"delete_holiday_calendar",
"create_holiday_calendar_entry",
"update_holiday_calendar_entry",
"delete_holiday_calendar_entry",
"send_broadcast",
"create_task_for_user",
"create_reminder",
"update_task_status",
"execute_task_action",
"create_comment",
"resolve_comment",
"mark_notification_read",
"save_dashboard_layout",
"toggle_favorite_project",
"set_column_preferences",
"generate_totp_secret",
"verify_and_enable_totp",
"create_user",
"set_user_password",
"update_user_role",
"update_user_name",
"link_user_resource",
"auto_link_users_by_email",
"set_user_permissions",
"reset_user_permissions",
"disable_user_totp",
"create_notification",
"update_reminder",
"delete_reminder",
"delete_notification",
"assign_task",
"clone_estimate",
"update_estimate_draft",
"submit_estimate_version",
"approve_estimate_version",
"create_estimate_revision",
"create_estimate_export",
"create_estimate_planning_handoff",
"generate_estimate_weekly_phasing",
"update_estimate_commercial_terms",
]);
export const ADVANCED_ASSISTANT_TOOLS = new Set([
"find_best_project_resource",
"get_timeline_entries_view",
"get_timeline_holiday_overlays",
"get_project_timeline_context",
"preview_project_shift",
"update_timeline_allocation_inline",
"apply_timeline_project_shift",
"quick_assign_timeline_resource",
"batch_quick_assign_timeline_resources",
"batch_shift_timeline_allocations",
"get_chargeability_report",
"get_resource_computation_graph",
"get_project_computation_graph",
]);
const createChargeabilityReportCaller = createCallerFactory(chargeabilityReportRouter);
const createComputationGraphCaller = createCallerFactory(computationGraphRouter);
const createTimelineCaller = createCallerFactory(timelineRouter);
const createAuditLogCaller = createCallerFactory(auditLogRouter);
const createImportExportCaller = createCallerFactory(importExportRouter);
const createDispoCaller = createCallerFactory(dispoRouter);
const createResourceCaller = createCallerFactory(resourceRouter);
const createSettingsCaller = createCallerFactory(settingsRouter);
const createSystemRoleConfigCaller = createCallerFactory(systemRoleConfigRouter);
const createUserCaller = createCallerFactory(userRouter);
const createNotificationCaller = createCallerFactory(notificationRouter);
const createEstimateCaller = createCallerFactory(estimateRouter);
const createWebhookCaller = createCallerFactory(webhookRouter);
const createCountryCaller = createCallerFactory(countryRouter);
const createHolidayCalendarCaller = createCallerFactory(holidayCalendarRouter);
const createBlueprintCaller = createCallerFactory(blueprintRouter);
const createRoleCaller = createCallerFactory(roleRouter);
const createClientCaller = createCallerFactory(clientRouter);
const createOrgUnitCaller = createCallerFactory(orgUnitRouter);
const createProjectCaller = createCallerFactory(projectRouter);
const createRateCardCaller = createCallerFactory(rateCardRouter);
const createReportCaller = createCallerFactory(reportRouter);
const createVacationCaller = createCallerFactory(vacationRouter);
const createEntitlementCaller = createCallerFactory(entitlementRouter);
const createCommentCaller = createCallerFactory(commentRouter);
const createManagementLevelCaller = createCallerFactory(managementLevelRouter);
const createUtilizationCategoryCaller = createCallerFactory(utilizationCategoryRouter);
const createCalculationRuleCaller = createCallerFactory(calculationRuleRouter);
const createEffortRuleCaller = createCallerFactory(effortRuleRouter);
const createExperienceMultiplierCaller = createCallerFactory(experienceMultiplierRouter);
const createDashboardCaller = createCallerFactory(dashboardRouter);
const createInsightsCaller = createCallerFactory(insightsRouter);
const createScenarioCaller = createCallerFactory(scenarioRouter);
const createAllocationCaller = createCallerFactory(allocationRouter);
const createStaffingCaller = createCallerFactory(staffingRouter);
// ─── Helpers ────────────────────────────────────────────────────────────────
const fmtDate = toIsoDateOrNull;
class AssistantVisibleError extends Error {
constructor(message: string) {
super(message);
this.name = "AssistantVisibleError";
}
}
function assertPermission(ctx: ToolContext, perm: PermissionKey): void {
if (!ctx.permissions.has(perm)) {
throw new AssistantVisibleError(
`Permission denied: you need the "${perm}" permission to perform this action.`,
);
}
}
function assertAdminRole(ctx: ToolContext): void {
if (ctx.userRole !== SystemRole.ADMIN) {
throw new AssistantVisibleError("Admin role required to perform this action.");
}
}
function formatHolidayCalendarEntry(entry: {
id: string;
date: Date;
name: string;
isRecurringAnnual?: boolean | null;
source?: string | null;
}) {
return {
id: entry.id,
date: toIsoDateOrNull(entry.date),
name: entry.name,
isRecurringAnnual: entry.isRecurringAnnual ?? false,
source: entry.source ?? null,
};
}
function formatHolidayCalendar(calendar: {
id: string;
name: string;
scopeType: string;
stateCode?: string | null;
isActive?: boolean | null;
priority?: number | null;
country?: { id: string; code: string; name: string } | null;
metroCity?: { id: string; name: string } | null;
_count?: { entries?: number | null } | null;
entries?: Array<{
id: string;
date: Date;
name: string;
isRecurringAnnual?: boolean | null;
source?: string | null;
}> | null;
}) {
const entries = calendar.entries?.map(formatHolidayCalendarEntry) ?? [];
return {
id: calendar.id,
name: calendar.name,
scopeType: calendar.scopeType,
stateCode: calendar.stateCode ?? null,
isActive: calendar.isActive ?? true,
priority: calendar.priority ?? 0,
country: calendar.country
? {
id: calendar.country.id,
code: calendar.country.code,
name: calendar.country.name,
}
: null,
metroCity: calendar.metroCity
? {
id: calendar.metroCity.id,
name: calendar.metroCity.name,
}
: null,
entryCount: calendar._count?.entries ?? entries.length,
entries,
};
}
function formatCountry(country: {
id: string;
code: string;
name: string;
dailyWorkingHours: number;
scheduleRules?: Prisma.JsonValue | null;
isActive?: boolean | null;
metroCities?: Array<{ id: string; name: string }> | null;
_count?: { resources?: number | null } | null;
}) {
return {
id: country.id,
code: country.code,
name: country.name,
dailyWorkingHours: country.dailyWorkingHours,
scheduleRules: country.scheduleRules ?? null,
isActive: country.isActive ?? true,
resourceCount: country._count?.resources ?? null,
metroCities: (country.metroCities ?? []).map((city) => ({
id: city.id,
name: city.name,
})),
cities: (country.metroCities ?? []).map((city) => city.name),
};
}
function createUtcDate(year: number, monthIndex: number, day: number): Date {
return new Date(Date.UTC(year, monthIndex, day));
}
function resolveHolidayPeriod(input: { year?: number; periodStart?: string; periodEnd?: string }): {
year: number | null;
periodStart: Date;
periodEnd: Date;
} {
if (input.periodStart || input.periodEnd) {
if (!input.periodStart || !input.periodEnd) {
throw new AssistantVisibleError(
"periodStart and periodEnd must both be provided when using a custom holiday range.",
);
}
const periodStart = new Date(`${input.periodStart}T00:00:00.000Z`);
const periodEnd = new Date(`${input.periodEnd}T00:00:00.000Z`);
if (Number.isNaN(periodStart.getTime())) {
throw new AssistantVisibleError(`Invalid periodStart: ${input.periodStart}`);
}
if (Number.isNaN(periodEnd.getTime())) {
throw new AssistantVisibleError(`Invalid periodEnd: ${input.periodEnd}`);
}
if (periodEnd < periodStart) {
throw new AssistantVisibleError("periodEnd must be on or after periodStart.");
}
return { year: null, periodStart, periodEnd };
}
const year = input.year ?? new Date().getUTCFullYear();
return {
year,
periodStart: createUtcDate(year, 0, 1),
periodEnd: createUtcDate(year, 11, 31),
};
}
const CONTROLLER_ASSISTANT_ROLES = [
SystemRole.ADMIN,
SystemRole.MANAGER,
SystemRole.CONTROLLER,
] as const;
const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequirements>> = {
search_projects: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
get_project: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
update_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
create_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
};
const ASSISTANT_VACATION_REQUEST_TYPES = [
VacationType.ANNUAL,
VacationType.SICK,
VacationType.OTHER,
] as const;
function parseAssistantVacationRequestType(input: string): VacationType {
const normalized = input.trim().toUpperCase();
if (normalized === VacationType.PUBLIC_HOLIDAY) {
throw new AssistantVisibleError(
"PUBLIC_HOLIDAY requests cannot be created manually. Manage public holidays through holiday calendars instead.",
);
}
if ((ASSISTANT_VACATION_REQUEST_TYPES as readonly string[]).includes(normalized)) {
return normalized as VacationType;
}
throw new AssistantVisibleError(
`Invalid vacation type: ${input}. Valid types: ${ASSISTANT_VACATION_REQUEST_TYPES.join(", ")}.`,
);
}
function parseIsoDate(value: string, fieldName: string): Date {
const parsed = new Date(`${value}T00:00:00.000Z`);
if (Number.isNaN(parsed.getTime())) {
throw new AssistantVisibleError(`Invalid ${fieldName}: ${value}`);
}
return parsed;
}
function parseOptionalIsoDate(value: string | undefined, fieldName: string): Date | undefined {
return value ? parseIsoDate(value, fieldName) : undefined;
}
function parseDateTime(value: string, fieldName: string): Date {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
throw new AssistantVisibleError(`Invalid ${fieldName}: ${value}`);
}
return parsed;
}
function parseOptionalDateTime(value: string | undefined, fieldName: string): Date | undefined {
return value ? parseDateTime(value, fieldName) : undefined;
}
function toDate(value: Date | string): Date {
return value instanceof Date ? value : new Date(value);
}
type AssistantToolErrorResult = { error: string };
type AssistantIndexedFieldErrorResult = AssistantToolErrorResult & {
field: string;
index: number;
};
type BatchQuickAssignmentInput = {
resourceId: string;
projectId: string;
startDate: Date;
endDate: Date;
hoursPerDay?: number;
role?: string;
status?: AllocationStatus;
};
function toAssistantNotFoundError(
error: unknown,
message: string,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: message };
}
return null;
}
function toAssistantAllocationNotFoundError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "NOT_FOUND") {
return { error: "Allocation not found with the given criteria." };
}
if (error.message === "Record not found" || error.message.includes("Assignment not found")) {
return { error: "Allocation not found with the given criteria." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "Allocation not found with the given criteria." };
}
return null;
}
function toAssistantProjectNotFoundError(
error: unknown,
identifier: string,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, `Project not found: ${identifier}`);
}
function toAssistantTimelineMutationError(
error: unknown,
context: "updateInline" | "applyShift" | "quickAssign" | "batchShift",
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "NOT_FOUND") {
if (error.message.includes("Resource not found")) {
return { error: "Resource not found with the given criteria." };
}
if (error.message.includes("Project not found")) {
return { error: "Project not found with the given criteria." };
}
if (error.message.includes("Demand requirement not found")) {
return { error: "Demand requirement not found with the given criteria." };
}
if (error.message.includes("No allocations found")) {
return { error: "Allocation not found with the given criteria." };
}
}
if (error.code === "BAD_REQUEST" || error.code === "CONFLICT") {
return { error: error.message };
}
}
const allocationNotFound = toAssistantAllocationNotFoundError(error);
if (allocationNotFound && (context === "updateInline" || context === "batchShift")) {
return allocationNotFound;
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
if (errorText.includes("project")) {
return { error: "Project not found with the given criteria." };
}
if (errorText.includes("demand")) {
return { error: "Demand requirement not found with the given criteria." };
}
if (prismaError.code === "P2025" && (context === "updateInline" || context === "batchShift")) {
return { error: "Allocation not found with the given criteria." };
}
return null;
}
function toAssistantVacationNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Vacation not found with the given criteria.");
}
function toAssistantVacationMutationError(
error: unknown,
action: "approve" | "reject" | "cancel",
): AssistantToolErrorResult | null {
const notFound = toAssistantVacationNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (action === "approve") {
return { error: "Vacation cannot be approved in its current status." };
}
if (action === "reject") {
return { error: "Vacation cannot be rejected in its current status." };
}
return { error: "Vacation cannot be cancelled in its current status." };
}
return null;
}
function toAssistantProjectCreationError(
error: unknown,
shortCode: string,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "CONFLICT") {
return { error: `A project with short code "${shortCode}" already exists.` };
}
if (error.code === "NOT_FOUND") {
if (error.message.includes("Blueprint")) {
return { error: "Blueprint not found with the given criteria." };
}
if (error.message.includes("Client")) {
return { error: "Client not found with the given criteria." };
}
}
if (error.code === "BAD_REQUEST" || error.code === "UNPROCESSABLE_CONTENT") {
return { error: error.message };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("blueprint")) {
return { error: "Blueprint not found with the given criteria." };
}
if (errorText.includes("client")) {
return { error: "Client not found with the given criteria." };
}
return null;
}
function toAssistantDemandNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Demand not found with the given criteria.");
}
function toAssistantDemandFillError(error: unknown): AssistantToolErrorResult | null {
const notFound = toAssistantDemandNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return { error: "Demand cannot be filled in its current status." };
}
return null;
}
function toAssistantEstimateNotFoundError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("version")) {
return { error: "Estimate version not found with the given criteria." };
}
return { error: "Estimate not found with the given criteria." };
}
return null;
}
function toAssistantEstimateReadError(
error: unknown,
context: "weeklyPhasing" | "commercialTerms",
): AssistantToolErrorResult | null {
const notFound = toAssistantEstimateNotFoundError(error);
if (notFound) {
return notFound;
}
if (
context === "weeklyPhasing" &&
error instanceof TRPCError &&
error.code === "PRECONDITION_FAILED" &&
error.message === "Estimate has no versions"
) {
return { error: "Estimate version not found with the given criteria." };
}
return null;
}
function toAssistantHolidayCalendarNotFoundError(
error: unknown,
identifier?: string,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
identifier
? `Holiday calendar not found: ${identifier}`
: "Holiday calendar not found with the given criteria.",
);
}
function toAssistantHolidayCalendarMutationError(error: unknown): AssistantToolErrorResult | null {
const notFound = toAssistantHolidayCalendarNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return { error: "Holiday calendar scope is invalid." };
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A holiday calendar for this scope already exists." };
}
return null;
}
function toAssistantHolidayEntryNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Holiday calendar entry not found with the given criteria.",
);
}
function toAssistantHolidayEntryMutationError(error: unknown): AssistantToolErrorResult | null {
const calendarNotFound = toAssistantHolidayCalendarNotFoundError(error);
if (calendarNotFound) {
return calendarNotFound;
}
const entryNotFound = toAssistantHolidayEntryNotFoundError(error);
if (entryNotFound) {
return entryNotFound;
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A holiday entry for this calendar and date already exists." };
}
return null;
}
function toAssistantRoleNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Role not found with the given criteria.");
}
function toAssistantRoleMutationError(
error: unknown,
action: "create" | "update" | "delete",
): AssistantToolErrorResult | null {
const notFound = toAssistantRoleNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A role with this name already exists." };
}
if (action === "delete" && error instanceof TRPCError && error.code === "PRECONDITION_FAILED") {
return { error: "Role cannot be deleted while it is still assigned. Deactivate it instead." };
}
return null;
}
function toAssistantClientMutationError(
error: unknown,
action: "create" | "update" | "delete" = "update",
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Parent client")) {
return { error: "Parent client not found with the given criteria." };
}
return { error: "Client not found with the given criteria." };
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A client with this code already exists." };
}
if (action === "delete" && error instanceof TRPCError && error.code === "PRECONDITION_FAILED") {
if (error.message.includes("project")) {
return {
error: "Client cannot be deleted while it still has projects. Deactivate it instead.",
};
}
if (error.message.includes("child client")) {
return {
error:
"Client cannot be deleted while it still has child clients. Remove or reassign them first.",
};
}
}
return null;
}
function toAssistantOrgUnitNotFoundError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Parent org unit")) {
return { error: "Parent org unit not found with the given criteria." };
}
return { error: "Org unit not found with the given criteria." };
}
return null;
}
function toAssistantOrgUnitMutationError(error: unknown): AssistantToolErrorResult | null {
const notFound = toAssistantOrgUnitNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (error.message.includes("must be greater than parent level")) {
return { error: "Org unit level must be greater than the parent org unit level." };
}
}
return null;
}
function toAssistantCountryNotFoundError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Country not found with the given criteria." };
}
return null;
}
function toAssistantCountryMutationError(error: unknown): AssistantToolErrorResult | null {
const notFound = toAssistantCountryNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A country with this code already exists." };
}
return null;
}
function toAssistantResourceCreationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "CONFLICT") {
return { error: "A resource with this EID or email already exists." };
}
if (error.code === "BAD_REQUEST" || error.code === "UNPROCESSABLE_CONTENT") {
return { error: error.message };
}
if (error.code === "NOT_FOUND") {
if (error.message.includes("Role")) {
return { error: "Role not found with the given criteria." };
}
if (error.message.includes("Country")) {
return { error: "Country not found with the given criteria." };
}
if (error.message.includes("Org unit")) {
return { error: "Org unit not found with the given criteria." };
}
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("country")) {
return { error: "Country not found with the given criteria." };
}
if (
errorText.includes("orgunit") ||
errorText.includes("org_unit") ||
errorText.includes("org unit")
) {
return { error: "Org unit not found with the given criteria." };
}
return { error: "The selected role, country, or org unit no longer exists." };
}
function toAssistantResourceMutationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Resource not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2025") {
return { error: "Resource not found with the given criteria." };
}
if (prismaError.code !== "P2003") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("country")) {
return { error: "Country not found with the given criteria." };
}
if (
errorText.includes("orgunit") ||
errorText.includes("org_unit") ||
errorText.includes("org unit")
) {
return { error: "Org unit not found with the given criteria." };
}
return { error: "Resource not found with the given criteria." };
}
function toAssistantProjectMutationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Project not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2025") {
return { error: "Project not found with the given criteria." };
}
if (prismaError.code !== "P2003") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("blueprint")) {
return { error: "Blueprint not found with the given criteria." };
}
if (errorText.includes("client")) {
return { error: "Client not found with the given criteria." };
}
return { error: "Project not found with the given criteria." };
}
function toAssistantMetroCityMutationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Country")) {
return { error: "Country not found with the given criteria." };
}
return { error: "Metro city not found with the given criteria." };
}
if (error instanceof TRPCError && error.code === "PRECONDITION_FAILED") {
return { error: "Metro city cannot be deleted while it is still assigned to resources." };
}
return null;
}
function toAssistantDemandCreationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Role")) {
return { error: "Role not found with the given criteria." };
}
return { error: "Project not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("project")) {
return { error: "Project not found with the given criteria." };
}
return null;
}
function toAssistantVacationCreationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "FORBIDDEN") {
return { error: "You can only create vacation requests for your own resource." };
}
if (error.code === "BAD_REQUEST") {
return { error: error.message };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2025") {
return { error: "Resource not found with the given criteria." };
}
if (prismaError.code !== "P2003") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
return { error: "Resource not found with the given criteria." };
}
function toAssistantEntitlementMutationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Resource not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2025") {
return { error: "Resource not found with the given criteria." };
}
if (prismaError.code !== "P2003") {
return null;
}
return { error: "Resource not found with the given criteria." };
}
function toAssistantEstimateCreationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Project not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("project")) {
return { error: "Project not found with the given criteria." };
}
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
if (
errorText.includes("scopeitem") ||
errorText.includes("scope_item") ||
errorText.includes("scope item")
) {
return { error: "Estimate scope item not found with the given criteria." };
}
return {
error: "One of the referenced project, role, resource, or scope items no longer exists.",
};
}
function toAssistantEstimateMutationError(
error: unknown,
action:
| "clone"
| "updateDraft"
| "submitVersion"
| "approveVersion"
| "createRevision"
| "createExport"
| "createPlanningHandoff"
| "generateWeeklyPhasing"
| "updateCommercialTerms",
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "NOT_FOUND") {
if (error.message.includes("Linked project")) {
return { error: "Project not found with the given criteria." };
}
if (action === "clone" && error.message === "Source estimate has no versions") {
return { error: "Source estimate has no versions and cannot be cloned." };
}
if (error.message.includes("version") || error.message.includes("versions")) {
return { error: "Estimate version not found with the given criteria." };
}
return { error: "Estimate not found with the given criteria." };
}
if (error.code === "PRECONDITION_FAILED") {
switch (error.message) {
case "Estimate has no working version":
return { error: "Estimate has no working version." };
case "Only working versions can be submitted":
return { error: "Only working versions can be submitted." };
case "Estimate has no submitted version":
return { error: "Estimate has no submitted version." };
case "Only submitted versions can be approved":
return { error: "Only submitted versions can be approved." };
case "Estimate already has a working version":
return { error: "Estimate already has a working version." };
case "Estimate has no locked version to revise":
return { error: "Estimate has no locked version to revise." };
case "Source version must be locked before creating a revision":
return { error: "Source version must be locked before creating a revision." };
case "Estimate has no approved version":
return { error: "Estimate has no approved version." };
case "Only approved versions can be handed off to planning":
return { error: "Only approved versions can be handed off to planning." };
case "Estimate must be linked to a project before planning handoff":
return { error: "Estimate must be linked to a project before planning handoff." };
case "Planning handoff already exists for this approved version":
return { error: "Planning handoff already exists for this approved version." };
case "Linked project has an invalid date range":
return { error: "The linked project has an invalid date range for planning handoff." };
case "Commercial terms can only be edited on working versions":
return { error: "Commercial terms can only be edited on working versions." };
default:
if (error.message.startsWith("Project window has no working days for demand line")) {
return {
error: "The linked project window has no working days for at least one demand line.",
};
}
}
}
if (error.code === "BAD_REQUEST" && action === "updateCommercialTerms") {
return { error: "Commercial terms input is invalid." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2003") {
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("project")) {
return { error: "Project not found with the given criteria." };
}
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
if (
errorText.includes("scopeitem") ||
errorText.includes("scope_item") ||
errorText.includes("scope item")
) {
return { error: "Estimate scope item not found with the given criteria." };
}
return {
error: "One of the referenced project, role, resource, or scope items no longer exists.",
};
}
if (prismaError.code === "P2025") {
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (
errorText.includes("estimatedemandline") ||
errorText.includes("estimate_demand_line") ||
errorText.includes("estimate demand line")
) {
return { error: "Estimate demand line not found with the given criteria." };
}
if (
errorText.includes("estimateversion") ||
errorText.includes("estimate_version") ||
errorText.includes("estimate version")
) {
return { error: "Estimate version not found with the given criteria." };
}
if (errorText.includes("estimate")) {
return { error: "Estimate not found with the given criteria." };
}
switch (action) {
case "generateWeeklyPhasing":
return { error: "Estimate demand line not found with the given criteria." };
case "updateCommercialTerms":
case "submitVersion":
case "approveVersion":
case "createRevision":
case "createExport":
return { error: "Estimate version not found with the given criteria." };
default:
return { error: "Estimate not found with the given criteria." };
}
}
return null;
}
function toAssistantUserMutationError(
error: unknown,
action: "create" | "update" | "password" = "update",
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "CONFLICT" && action === "create") {
return { error: "User with this email already exists." };
}
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "User not found with the given criteria." };
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
const validationIssues = getTrpcValidationIssues(error);
for (const issue of validationIssues) {
const field = issue.path[0];
if (field === "password" && issue.code === "too_small") {
return { error: "Password must be at least 12 characters." };
}
if (field === "name" && issue.code === "too_small") {
return { error: "Name is required." };
}
if (field === "name" && issue.code === "too_big") {
return { error: "Name must be at most 200 characters." };
}
}
if (error.message.includes("Password must be at least 12 characters")) {
return { error: "Password must be at least 12 characters." };
}
if (error.message.includes("Name is required")) {
return { error: "Name is required." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "User not found with the given criteria." };
}
return null;
}
function getTrpcValidationIssues(error: TRPCError): Array<{
code?: string;
path: string[];
}> {
if (error.cause instanceof ZodError) {
return error.cause.issues.map((issue) => ({
code: issue.code,
path: issue.path.map((segment) => String(segment)),
}));
}
try {
const parsed = JSON.parse(error.message);
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.filter(
(issue): issue is { code?: unknown; path?: unknown } =>
issue !== null && typeof issue === "object",
)
.map((issue) =>
typeof issue.code === "string"
? {
code: issue.code,
path: Array.isArray(issue.path) ? issue.path.map((segment) => String(segment)) : [],
}
: {
path: Array.isArray(issue.path) ? issue.path.map((segment) => String(segment)) : [],
},
);
} catch {
return [];
}
}
function toAssistantUserResourceLinkError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "CONFLICT") {
if (error.message.includes("already linked")) {
return { error: "Resource is already linked to another user." };
}
if (error.message.includes("changed during update")) {
return { error: "Resource link changed during update. Please retry." };
}
}
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Resource")) {
return { error: "Resource not found with the given criteria." };
}
return { error: "User not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
const pointsToUser =
errorText.includes("userid") || errorText.includes("user_id") || errorText.includes(" user ");
const pointsToResource =
errorText.includes("resourceid") ||
errorText.includes("resource_id") ||
errorText.includes(" resource ");
if (prismaError.code === "P2025") {
return { error: "Resource not found with the given criteria." };
}
if (prismaError.code === "P2003") {
if (pointsToUser) {
return { error: "User not found with the given criteria." };
}
if (pointsToResource || errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
return { error: "User not found with the given criteria." };
}
return null;
}
function toAssistantTotpEnableError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (error.message.includes("No TOTP secret generated")) {
return { error: "No TOTP secret generated. Call generate_totp_secret first." };
}
if (error.message.includes("already enabled")) {
return { error: "TOTP is already enabled." };
}
if (error.message.includes("Invalid TOTP token")) {
return { error: "Invalid TOTP token." };
}
}
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "User not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "User not found with the given criteria." };
}
return null;
}
function toAssistantWebhookNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Webhook not found with the given criteria.");
}
function toAssistantWebhookMutationError(
error: unknown,
action: "create" | "update" = "update",
): AssistantToolErrorResult | null {
const notFound = toAssistantWebhookNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return {
error: action === "create" ? "Webhook input is invalid." : "Webhook update input is invalid.",
};
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "Webhook not found with the given criteria." };
}
return null;
}
function toAssistantAuditLogEntryNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Audit log entry not found with the given criteria.");
}
function toAssistantTaskNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Task not found with the given criteria.");
}
function toAssistantTaskActionError(error: unknown): AssistantToolErrorResult | null {
const notFound = toAssistantTaskNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "PRECONDITION_FAILED") {
if (error.message.includes("no executable action")) {
return { error: "Task has no executable action." };
}
if (error.message.includes("already completed")) {
return { error: "Task is already completed." };
}
if (error.message.includes("dismissed")) {
return { error: "Task has been dismissed and cannot be executed." };
}
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (
error.message.includes("Invalid taskAction format") ||
error.message.includes("Unknown action")
) {
return { error: "Task action is invalid and cannot be executed." };
}
if (error.message === "Vacation not found") {
return { error: "Vacation not found with the given criteria." };
}
if (error.message.startsWith("Vacation is ") && error.message.includes(", not PENDING")) {
return {
error: "Vacation is not pending and cannot be approved or rejected via this task action.",
};
}
if (error.message === "Assignment not found") {
return { error: "Assignment not found with the given criteria." };
}
if (error.message === "Assignment is already CONFIRMED") {
return { error: "Assignment is already confirmed." };
}
return { error: error.message };
}
if (error instanceof TRPCError && error.code === "FORBIDDEN") {
return { error: "You do not have permission to execute this task action." };
}
return null;
}
function toAssistantTaskAssignmentError(error: unknown): AssistantToolErrorResult | null {
const notFound = toAssistantTaskNotFoundError(error);
if (notFound) {
return notFound;
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError && (prismaError.code === "P2003" || prismaError.code === "P2025")) {
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("assignee")) {
return { error: "Assignee user not found with the given criteria." };
}
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return { error: "Only tasks and approvals can be assigned." };
}
return null;
}
function toAssistantBroadcastNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Broadcast not found with the given criteria.");
}
function toAssistantDispoImportBatchNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Import batch not found with the given criteria.");
}
function toAssistantReminderNotFoundError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotFoundError(error, "Reminder not found with the given criteria.");
}
function toAssistantNotificationNotFoundError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Notification not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "Notification not found with the given criteria." };
}
return null;
}
function toAssistantNotificationReadError(error: unknown): AssistantToolErrorResult | null {
return toAssistantNotificationNotFoundError(error);
}
function toAssistantNotificationDeletionError(error: unknown): AssistantToolErrorResult | null {
const notFound = toAssistantNotificationNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "FORBIDDEN") {
return { error: "Tasks created by other users cannot be deleted." };
}
return null;
}
function toAssistantReminderCreationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return { error: "Reminder input is invalid." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("user")) {
return { error: "Authenticated user not found with the given criteria." };
}
return null;
}
function toAssistantCommentResolveError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Comment not found with the given criteria." };
}
if (error instanceof TRPCError && error.code === "FORBIDDEN") {
return { error: "Only the comment author or an admin can resolve comments." };
}
return null;
}
function toAssistantCommentCreationError(error: unknown): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (error.message.includes("at least 1 character")) {
return { error: "Comment body is required." };
}
if (error.message.includes("at most 10000")) {
return { error: "Comment body must be at most 10000 characters." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("author") || errorText.includes("sender")) {
return { error: "Comment author not found with the given criteria." };
}
if (errorText.includes("user") || errorText.includes("recipient")) {
return { error: "Mentioned user not found with the given criteria." };
}
return null;
}
function getPrismaRequestErrorMetadata(error: unknown): {
code: string;
message: string;
metaText: string;
} | null {
const collectMetaStrings = (value: unknown): string[] => {
if (typeof value === "string") {
return [value];
}
if (Array.isArray(value)) {
return value.flatMap((entry) => collectMetaStrings(entry));
}
if (value && typeof value === "object") {
return Object.values(value).flatMap((entry) => collectMetaStrings(entry));
}
return [];
};
const queue: unknown[] = [error];
const visited = new Set<unknown>();
while (queue.length > 0) {
const current = queue.shift();
if (current === undefined || current === null || visited.has(current)) {
continue;
}
visited.add(current);
if (current instanceof Prisma.PrismaClientKnownRequestError) {
const metaText = Object.values(current.meta ?? {})
.flatMap((value) => collectMetaStrings(value))
.join(" ");
return {
code: current.code,
message: current.message,
metaText,
};
}
if (typeof current !== "object") {
continue;
}
const candidate = current as {
code?: unknown;
message?: unknown;
meta?: Record<string, unknown>;
cause?: unknown;
};
if (typeof candidate.code === "string" && /^P\d{4}$/.test(candidate.code)) {
const metaText = Object.values(candidate.meta ?? {})
.flatMap((value) => collectMetaStrings(value))
.join(" ");
return {
code: candidate.code,
message: typeof candidate.message === "string" ? candidate.message : "",
metaText,
};
}
if ("cause" in candidate) {
queue.push(candidate.cause);
}
}
return null;
}
function getTrpcErrorMetadata(error: unknown): {
code: string;
message: string;
} | null {
const queue: unknown[] = [error];
const visited = new Set<unknown>();
while (queue.length > 0) {
const current = queue.shift();
if (current === undefined || current === null || visited.has(current)) {
continue;
}
visited.add(current);
if (current instanceof TRPCError) {
return {
code: current.code,
message: current.message,
};
}
if (typeof current !== "object") {
continue;
}
const candidate = current as {
code?: unknown;
message?: unknown;
cause?: unknown;
data?: { code?: unknown };
shape?: { code?: unknown; message?: unknown; data?: { cause?: unknown } };
};
const candidateCode =
typeof candidate.code === "string"
? candidate.code
: typeof candidate.data?.code === "string"
? candidate.data.code
: typeof candidate.shape?.code === "string"
? candidate.shape.code
: null;
const candidateMessage =
typeof candidate.message === "string"
? candidate.message
: typeof candidate.shape?.message === "string"
? candidate.shape.message
: "";
if (candidateCode && /^[A-Z_]+$/.test(candidateCode)) {
return {
code: candidateCode,
message: candidateMessage,
};
}
if ("cause" in candidate) {
queue.push(candidate.cause);
}
if (candidate.shape?.data?.cause) {
queue.push(candidate.shape.data.cause);
}
}
return null;
}
function toAssistantNotificationCreationError(
error: unknown,
context: "notification" | "task" | "broadcast",
): AssistantToolErrorResult | null {
const trpcError = getTrpcErrorMetadata(error);
if (
context === "broadcast" &&
trpcError?.code === "BAD_REQUEST" &&
trpcError.message === "No recipients matched the broadcast target."
) {
return { error: "No recipients matched the broadcast target." };
}
if (
context === "broadcast" &&
trpcError?.code === "BAD_REQUEST" &&
trpcError.message === "Scheduled broadcasts with task metadata are not supported yet."
) {
return { error: "Scheduled broadcasts with task metadata are not supported yet." };
}
if (trpcError?.code === "NOT_FOUND") {
if (trpcError.message.includes("broadcast")) {
return { error: "Broadcast not found with the given criteria." };
}
if (trpcError.message.includes("Sender user not found")) {
return { error: "Sender user not found with the given criteria." };
}
if (trpcError.message.includes("Assignee user not found")) {
return { error: "Assignee user not found with the given criteria." };
}
if (trpcError.message.includes("recipient")) {
return context === "broadcast"
? { error: "Broadcast recipient user not found with the given criteria." }
: context === "task"
? { error: "Task recipient user not found with the given criteria." }
: { error: "Notification recipient user not found with the given criteria." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("assignee")) {
return { error: "Assignee user not found with the given criteria." };
}
if (errorText.includes("sender")) {
return { error: "Sender user not found with the given criteria." };
}
if (
context === "broadcast" &&
(errorText.includes("notificationbroadcast") || errorText.includes("broadcast"))
) {
return { error: "Broadcast not found with the given criteria." };
}
if (context === "broadcast" && prismaError.code === "P2025") {
return { error: "Broadcast not found with the given criteria." };
}
if (context === "task") {
return { error: "Task recipient user not found with the given criteria." };
}
if (context === "broadcast") {
return { error: "Broadcast recipient user not found with the given criteria." };
}
return { error: "Notification recipient user not found with the given criteria." };
}
function normalizeAssistantExecutionError(error: unknown): AssistantToolErrorResult {
if (error instanceof AssistantVisibleError) {
return { error: error.message };
}
const trpcError = getTrpcErrorMetadata(error);
if (trpcError) {
if (trpcError.code === "INTERNAL_SERVER_ERROR") {
return {
error: "The tool could not complete due to an internal error.",
};
}
if (trpcError.code === "UNAUTHORIZED") {
return {
error: "Authentication is required to use this tool.",
};
}
if (trpcError.code === "FORBIDDEN") {
return {
error: "You do not have permission to perform this action.",
};
}
return { error: "The tool could not complete due to a request error." };
}
if (error instanceof Error) {
return { error: "The tool could not complete due to an unexpected error." };
}
return { error: "The tool could not complete due to an unexpected error." };
}
function isAssistantToolErrorResult(value: unknown): value is AssistantToolErrorResult {
return value !== null && typeof value === "object" && "error" in value;
}
function toAssistantIndexedFieldError(
index: number,
field: string,
message: string,
): AssistantIndexedFieldErrorResult {
return {
error: `assignments[${index}].${field}: ${message}`,
field: `assignments[${index}].${field}`,
index,
};
}
async function resolveEntityOrAssistantError<T>(
resolve: () => Promise<T>,
notFoundMessage: string,
): Promise<T | AssistantToolErrorResult> {
try {
return await resolve();
} catch (error) {
const mapped = toAssistantNotFoundError(error, notFoundMessage);
if (mapped) {
return mapped;
}
if (error instanceof TRPCError && error.code === "INTERNAL_SERVER_ERROR") {
return normalizeAssistantExecutionError(error);
}
throw error;
}
}
async function resolveProjectIdentifier(ctx: ToolContext, identifier: string) {
const caller = createProjectCaller(createScopedCallerContext(ctx));
return resolveEntityOrAssistantError(
() => caller.resolveByIdentifier({ identifier }),
`Project not found: ${identifier}`,
);
}
async function resolveResourceIdentifier(ctx: ToolContext, identifier: string) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
return resolveEntityOrAssistantError(
() => caller.resolveByIdentifier({ identifier }),
`Resource not found: ${identifier}`,
);
}
function createScopedCallerContext(ctx: ToolContext): TRPCContext {
if (!ctx.session?.user || !ctx.dbUser) {
throw new AssistantVisibleError("Authenticated assistant context is required for this tool.");
}
return {
session: ctx.session,
db: ctx.db,
dbUser: ctx.dbUser,
roleDefaults: ctx.roleDefaults ?? null,
};
}
function sanitizeWebhook<T extends { secret?: string | null }>(webhook: T) {
const { secret: _secret, ...rest } = webhook;
return {
...rest,
hasSecret: Boolean(webhook.secret),
};
}
function sanitizeWebhookList<T extends { secret?: string | null }>(webhooks: T[]) {
return webhooks.map((webhook) => sanitizeWebhook(webhook));
}
// ─── Tool Definitions ───────────────────────────────────────────────────────
export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess(
[
// ── READ TOOLS ──
...resourceReadToolDefinitions,
...projectReadToolDefinitions,
...advancedTimelineToolDefinitions,
...allocationPlanningReadToolDefinitions,
...vacationHolidayReadToolDefinitions,
...vacationHolidayMutationToolDefinitions,
...rolesAnalyticsReadToolDefinitions,
...chargeabilityComputationReadToolDefinitions,
...planningNavigationToolDefinitions,
// ── WRITE TOOLS ──
...allocationPlanningMutationToolDefinitions,
...resourceMutationToolDefinitions,
...projectMutationToolDefinitions,
...vacationEntitlementToolDefinitions,
// ── DEMAND / STAFFING ──
...staffingDemandReadToolDefinitions,
...staffingDemandMutationToolDefinitions,
// ── BLUEPRINT ──
...blueprintsRateCardsToolDefinitions,
// ── ESTIMATES ──
...estimateReadToolDefinitions,
...estimateMutationToolDefinitions,
// ── ROLES ──
...rolesAnalyticsMutationToolDefinitions,
// ── CLIENTS ──
...clientMutationToolDefinitions,
// ── ADMIN / CONFIG READ TOOLS ──
...countryReadmodelToolDefinitions,
...countryMetroAdminToolDefinitions,
...configReadmodelToolDefinitions,
...userAdminToolDefinitions,
...userSelfServiceToolDefinitions,
...notificationInboxToolDefinitions,
...dashboardInsightsReportsToolDefinitions,
// ── ORG UNIT MANAGEMENT ──
...orgUnitMutationToolDefinitions,
// ── TASK MANAGEMENT ──
...notificationTaskToolDefinitions,
...commentReadToolDefinitions,
...scenarioRateAnalysisToolDefinitions,
...commentMutationToolDefinitions,
...auditHistoryToolDefinitions,
...importExportDispoToolDefinitions,
...settingsAdminToolDefinitions,
],
LEGACY_MONOLITHIC_TOOL_ACCESS,
);
const TOOL_DEFINITIONS_BY_NAME = new Map(
TOOL_DEFINITIONS.map((tool) => [tool.function.name, tool]),
);
type AssistantToolAccessEvaluationContext = Pick<ToolContext, "permissions" | "userRole">;
type AssistantToolAccessFailure =
| { type: "role" }
| {
type: "permission";
permission?: PermissionKey;
message?: string;
};
function hasAssistantResourceOverviewAccess(permissions: Set<PermissionKey>): boolean {
return (
permissions.has(PermissionKey.VIEW_ALL_RESOURCES) ||
permissions.has(PermissionKey.MANAGE_RESOURCES)
);
}
function getAssistantToolAccessRequirements(tool: ToolDef): ToolAccessRequirements | undefined {
return tool.access ?? LEGACY_MONOLITHIC_TOOL_ACCESS[tool.function.name];
}
function getAssistantToolAccessFailure(
tool: ToolDef,
ctx: AssistantToolAccessEvaluationContext,
): AssistantToolAccessFailure | null {
const access = getAssistantToolAccessRequirements(tool);
if (!access) {
return null;
}
if (
access.allowedSystemRoles &&
!access.allowedSystemRoles.includes(ctx.userRole as SystemRole)
) {
return { type: "role" };
}
const missingRequiredPermission = access.requiredPermissions?.find(
(permission) => !ctx.permissions.has(permission),
);
if (missingRequiredPermission) {
return {
type: "permission",
permission: missingRequiredPermission,
};
}
if (access.requiresPlanningRead && !ctx.permissions.has(PermissionKey.VIEW_PLANNING)) {
return {
type: "permission",
permission: PermissionKey.VIEW_PLANNING,
};
}
if (access.requiresCostView && !ctx.permissions.has(PermissionKey.VIEW_COSTS)) {
return {
type: "permission",
permission: PermissionKey.VIEW_COSTS,
};
}
if (
access.requiresAdvancedAssistant &&
!ctx.permissions.has(PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS)
) {
return {
type: "permission",
permission: PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS,
};
}
if (access.requiresResourceOverview && !hasAssistantResourceOverviewAccess(ctx.permissions)) {
return {
type: "permission",
message: "Permission denied: you need resource overview access to perform this action.",
};
}
return null;
}
function toAssistantToolAccessError(failure: AssistantToolAccessFailure): AssistantVisibleError {
if (failure.type === "role") {
return new AssistantVisibleError("You do not have permission to perform this action.");
}
if (failure.permission) {
return new AssistantVisibleError(
`Permission denied: you need the "${failure.permission}" permission to perform this action.`,
);
}
return new AssistantVisibleError(
failure.message ?? "You do not have permission to perform this action.",
);
}
export function canAccessAssistantTool(
tool: ToolDef,
ctx: AssistantToolAccessEvaluationContext,
): boolean {
return getAssistantToolAccessFailure(tool, ctx) === null;
}
export function getAvailableAssistantToolsForContext(
permissions: Set<PermissionKey>,
userRole: string,
): ToolDef[] {
return TOOL_DEFINITIONS.filter((tool) => canAccessAssistantTool(tool, { permissions, userRole }));
}
// ─── Helpers ────────────────────────────────────────────────────────────────
/** Resolve a responsible person name against existing resources. Returns the exact displayName or an error object. */
async function resolveResponsiblePerson(
name: string,
ctx: ToolContext,
): Promise<{ displayName: string } | { error: string }> {
const caller = createResourceCaller(createScopedCallerContext(ctx));
const result = await caller.resolveResponsiblePersonName({ name });
if (result.status === "resolved") {
return { displayName: result.displayName };
}
if (result.status === "ambiguous" || result.status === "missing") {
return { error: result.message };
}
return { error: `Unable to resolve responsible person: ${name}` };
}
// ─── Tool Executors ─────────────────────────────────────────────────────────
const executors = {
...createResourceExecutors({
assertPermission,
createResourceCaller,
createRoleCaller,
createCountryCaller,
createOrgUnitCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
toAssistantResourceMutationError,
toAssistantResourceCreationError,
}),
...createProjectExecutors({
assertPermission,
createProjectCaller,
createBlueprintCaller,
createClientCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResponsiblePerson,
resolveEntityOrAssistantError,
toAssistantNotFoundError,
toAssistantProjectMutationError,
toAssistantProjectCreationError,
toAssistantProjectNotFoundError,
}),
...createStaffingDemandExecutors({
assertPermission,
createAllocationCaller,
createStaffingCaller,
createRoleCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
parseIsoDate,
parseOptionalIsoDate,
fmtDate,
toAssistantDemandCreationError,
toAssistantDemandFillError,
}),
...createAdvancedTimelineExecutors({
assertPermission,
createStaffingCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
parseIsoDate,
fmtDate,
isAssistantToolErrorResult,
toAssistantIndexedFieldError,
toAssistantTimelineMutationError,
}),
...createAllocationPlanningExecutors({
assertPermission,
createAllocationCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
parseIsoDate,
parseOptionalIsoDate,
fmtDate,
toAssistantAllocationNotFoundError,
}),
...createVacationHolidayExecutors({
createEntitlementCaller,
createVacationCaller,
createHolidayCalendarCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveHolidayPeriod,
resolveEntityOrAssistantError,
assertAdminRole,
fmtDate,
formatHolidayCalendar,
formatHolidayCalendarEntry,
toAssistantHolidayCalendarMutationError,
toAssistantHolidayCalendarNotFoundError,
toAssistantHolidayEntryMutationError,
toAssistantHolidayEntryNotFoundError,
}),
...createRolesAnalyticsExecutors({
createRoleCaller,
createResourceCaller,
createDashboardCaller,
createScopedCallerContext,
resolveResourceIdentifier,
toAssistantRoleMutationError,
}),
...createClientsOrgUnitsExecutors({
createClientCaller,
createOrgUnitCaller,
createScopedCallerContext,
toAssistantClientMutationError,
toAssistantOrgUnitMutationError,
}),
...createChargeabilityComputationExecutors({
assertPermission,
createChargeabilityReportCaller,
createComputationGraphCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveProjectIdentifier,
}),
...createBlueprintsRateCardsExecutors({
createBlueprintCaller,
createRateCardCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
parseOptionalIsoDate,
fmtDate,
}),
...createDashboardInsightsReportsExecutors({
assertPermission,
createDashboardCaller,
createInsightsCaller,
createReportCaller,
createScopedCallerContext,
}),
...createPlanningNavigationExecutors({
createEstimateCaller,
createClientCaller,
createOrgUnitCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
parseIsoDate,
}),
...createScenarioRateAnalysisExecutors({
assertPermission,
createRateCardCaller,
createScenarioCaller,
createInsightsCaller,
createScopedCallerContext,
fmtEur,
}),
...createCommentExecutors({
createCommentCaller,
createScopedCallerContext,
toAssistantCommentCreationError,
toAssistantCommentResolveError,
}),
...createAuditHistoryExecutors({
createAuditLogCaller,
createScopedCallerContext,
}),
...createVacationEntitlementExecutors({
createVacationCaller,
createEntitlementCaller,
createScopedCallerContext,
resolveResourceIdentifier,
parseIsoDate,
fmtDate,
parseAssistantVacationRequestType,
toAssistantVacationCreationError,
toAssistantVacationMutationError,
toAssistantEntitlementMutationError,
}),
// ── ESTIMATES ──
...createEstimateExecutors({
assertPermission,
createEstimateCaller,
createScopedCallerContext,
resolveProjectIdentifier,
toAssistantEstimateNotFoundError,
toAssistantEstimateReadError,
toAssistantEstimateCreationError,
toAssistantEstimateMutationError,
}),
// ── ROLES ──
// ── CLIENTS ──
// ── ADMIN / CONFIG ──
...createCountryReadmodelExecutors({
createCountryCaller,
createScopedCallerContext,
formatCountry,
toAssistantCountryNotFoundError,
}),
...createCountryMetroAdminExecutors({
createCountryCaller,
createScopedCallerContext,
assertAdminRole,
formatCountry,
toAssistantCountryMutationError,
toAssistantMetroCityMutationError,
}),
...createConfigReadmodelExecutors({
createManagementLevelCaller,
createUtilizationCategoryCaller,
createCalculationRuleCaller,
createEffortRuleCaller,
createExperienceMultiplierCaller,
createScopedCallerContext,
}),
...createUserAdminExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantUserMutationError,
toAssistantUserResourceLinkError,
}),
...createUserSelfServiceExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantCurrentUserError: toAssistantUserMutationError,
toAssistantTotpEnableError,
}),
...createNotificationsTasksExecutors({
createNotificationCaller,
createScopedCallerContext,
parseDateTime,
parseOptionalDateTime,
toAssistantTaskNotFoundError,
toAssistantTaskActionError,
toAssistantTaskAssignmentError,
toAssistantBroadcastNotFoundError,
toAssistantReminderNotFoundError,
toAssistantNotificationReadError,
toAssistantNotificationDeletionError,
toAssistantReminderCreationError,
toAssistantNotificationCreationError,
}),
...createImportExportDispoExecutors({
assertPermission,
createImportExportCaller,
createDispoCaller,
createScopedCallerContext,
toAssistantDispoImportBatchNotFoundError,
}),
...createSettingsAdminExecutors({
createSettingsCaller,
createSystemRoleConfigCaller,
createWebhookCaller,
createAuditLogCaller,
createProjectCaller,
createScopedCallerContext,
parseIsoDate,
resolveProjectIdentifier,
sanitizeWebhook,
sanitizeWebhookList,
toAssistantWebhookNotFoundError,
toAssistantWebhookMutationError,
toAssistantAuditLogEntryNotFoundError,
}),
};
// ─── Executor ───────────────────────────────────────────────────────────────
export interface ToolAction {
type: string;
url?: string;
scope?: string[];
description?: string;
}
export interface ToolResult {
content: string;
action?: ToolAction;
data?: unknown;
}
export async function executeTool(
name: string,
args: string,
ctx: ToolContext,
): Promise<ToolResult> {
const executor = executors[name as keyof typeof executors];
if (!executor) return { content: JSON.stringify({ error: `Unknown tool: ${name}` }) };
try {
const toolDefinition = TOOL_DEFINITIONS_BY_NAME.get(name);
const accessFailure = toolDefinition
? getAssistantToolAccessFailure(toolDefinition, ctx)
: null;
if (accessFailure) {
throw toAssistantToolAccessError(accessFailure);
}
const params = JSON.parse(args);
// Audit-log all mutation tool executions (EGAI 4.1.3.1 / IAAI 3.6.26)
if (MUTATION_TOOLS.has(name)) {
logger.info(
{ tool: name, params, userId: ctx.userId, userRole: ctx.userRole },
"AI assistant mutation tool executed",
);
}
const result = await executor(params, ctx);
// Detect action payloads (e.g. navigation, invalidation)
if (result && typeof result === "object" && "__action" in (result as Record<string, unknown>)) {
const actionResult = result as Record<string, unknown>;
const actionType = actionResult.__action as string;
if (actionType === "navigate") {
const url = actionResult.url as string;
const desc = (actionResult.description as string | undefined) ?? url;
return {
content: JSON.stringify({ description: desc }),
data: { description: desc },
action: { type: "navigate", url, description: desc },
};
}
if (actionType === "invalidate") {
const scope = actionResult.scope as string[];
// Strip __action, scope, and large data from the result sent back to the AI
const { __action: _, scope: _s, coverImageUrl: _img, ...rest } = actionResult;
const content = JSON.stringify(rest);
return {
content: content.length > 4000 ? content.slice(0, 4000) + '..."' : content,
data: rest,
action: { type: "invalidate", scope },
};
}
}
// Cap tool result size to prevent oversized OpenAI conversation payloads
const content = typeof result === "string" ? result : JSON.stringify(result);
return {
content: content.length > 8000 ? content.slice(0, 8000) + '..."' : content,
...(typeof result === "string" ? {} : { data: result }),
};
} catch (err) {
const normalizedError = normalizeAssistantExecutionError(err);
logger.error(
{
tool: name,
userId: ctx.userId,
userRole: ctx.userRole,
error: err instanceof Error ? { message: err.message, stack: err.stack } : err,
},
"AI assistant tool execution failed",
);
return { content: JSON.stringify(normalizedError), data: normalizedError };
}
}