refactor(api): extract assistant vacation entitlement slice

This commit is contained in:
2026-03-30 23:09:32 +02:00
parent 45c25b17c1
commit bcfb18393e
3 changed files with 515 additions and 367 deletions
+17 -366
View File
@@ -151,6 +151,10 @@ import {
createPlanningNavigationExecutors,
planningNavigationToolDefinitions,
} from "./assistant-tools/planning-navigation.js";
import {
createVacationEntitlementExecutors,
vacationEntitlementToolDefinitions,
} from "./assistant-tools/vacation-entitlements.js";
import {
withToolAccess,
type ToolAccessRequirements,
@@ -418,23 +422,11 @@ const CONTROLLER_ASSISTANT_ROLES = [
SystemRole.CONTROLLER,
] as const;
const MANAGER_ASSISTANT_ROLES = [
SystemRole.ADMIN,
SystemRole.MANAGER,
] as const;
const ADMIN_ASSISTANT_ROLES = [SystemRole.ADMIN] 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] },
approve_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
reject_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
@@ -2015,136 +2007,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
...allocationPlanningMutationToolDefinitions,
...resourceMutationToolDefinitions,
...projectMutationToolDefinitions,
// ── VACATION MANAGEMENT ──
{
type: "function",
function: {
name: "create_vacation",
description: "Create a vacation/leave request through the real vacation workflow. Any authenticated user can request leave for their own resource; manager/admin can create requests for others. Always confirm with the user.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
type: {
type: "string",
enum: ["ANNUAL", "SICK", "OTHER"],
description: "Vacation type. PUBLIC_HOLIDAY requests are managed through holiday calendars, not manual vacation requests.",
},
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
isHalfDay: { type: "boolean", description: "Half day? Default: false" },
halfDayPart: { type: "string", description: "MORNING or AFTERNOON (if half day)" },
note: { type: "string", description: "Optional note" },
},
required: ["resourceId", "type", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "approve_vacation",
description: "Approve a vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "reject_vacation",
description: "Reject a pending vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
reason: { type: "string", description: "Rejection reason" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "cancel_vacation",
description: "Cancel a vacation request through the real vacation workflow. Users can cancel their own requests; manager/admin can cancel any request. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "get_pending_vacation_approvals",
description: "List vacation requests awaiting approval.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_team_vacation_overlap",
description: "Check if team members have overlapping vacations in a date range.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID to check overlap for" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
},
required: ["resourceId", "startDate", "endDate"],
},
},
},
// ── ENTITLEMENT ──
{
type: "function",
function: {
name: "get_entitlement_summary",
description: "Get vacation entitlement year summary for all resources or a specific resource.",
parameters: {
type: "object",
properties: {
year: { type: "integer", description: "Year. Default: current year" },
resourceName: { type: "string", description: "Filter by resource name (optional)" },
},
},
},
},
{
type: "function",
function: {
name: "set_entitlement",
description: "Set the annual vacation entitlement for a resource/year through the real entitlement workflow. Manager or admin role required. Carryover is computed automatically. Always confirm first.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
year: { type: "integer", description: "Year" },
entitledDays: { type: "number", description: "Number of entitled vacation days" },
},
required: ["resourceId", "year", "entitledDays"],
},
},
},
...vacationEntitlementToolDefinitions,
// ── DEMAND / STAFFING ──
...staffingDemandReadToolDefinitions,
@@ -2360,230 +2223,18 @@ const executors = {
createAuditLogCaller,
createScopedCallerContext,
}),
// ── VACATION MANAGEMENT ──
async create_vacation(params: {
resourceId: string; type: string;
startDate: string; endDate: string;
isHalfDay?: boolean; halfDayPart?: string; note?: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const caller = createVacationCaller(createScopedCallerContext(ctx));
const type = parseAssistantVacationRequestType(params.type);
let vacation;
try {
vacation = await caller.create({
resourceId: resource.id,
type,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
...(params.isHalfDay !== undefined ? { isHalfDay: params.isHalfDay } : {}),
...(params.halfDayPart !== undefined ? { halfDayPart: params.halfDayPart as "MORNING" | "AFTERNOON" } : {}),
...(params.note !== undefined ? { note: params.note } : {}),
});
} catch (error) {
const mapped = toAssistantVacationCreationError(error);
if (mapped) {
return mapped;
}
throw error;
}
const effectiveDays = "effectiveDays" in vacation && typeof vacation.effectiveDays === "number"
? vacation.effectiveDays
: null;
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
message: `Created ${type} for ${resource.displayName}: ${params.startDate} to ${params.endDate} (status: ${vacation.status}${effectiveDays !== null ? `, deducted ${effectiveDays} day(s)` : ""})`,
vacationId: vacation.id,
vacation,
};
},
async approve_vacation(params: { vacationId: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "approve");
if (mapped) {
return mapped;
}
throw error;
}
let approved;
try {
approved = await caller.approve({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "approve");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
warnings: approved.warnings,
vacation: approved,
message: `Approved vacation for ${existing.resource?.displayName ?? params.vacationId}`,
};
},
async reject_vacation(params: { vacationId: string; reason?: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "reject");
if (mapped) {
return mapped;
}
throw error;
}
let rejected;
try {
rejected = await caller.reject({
id: params.vacationId,
...(params.reason !== undefined ? { rejectionReason: params.reason } : {}),
});
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "reject");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
vacation: rejected,
message: `Rejected vacation for ${existing.resource?.displayName ?? params.vacationId}${params.reason ? `: ${params.reason}` : ""}`,
};
},
async cancel_vacation(params: { vacationId: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "cancel");
if (mapped) {
return mapped;
}
throw error;
}
let cancelled;
try {
cancelled = await caller.cancel({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "cancel");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
vacation: cancelled,
message: `Cancelled vacation for ${existing.resource?.displayName ?? params.vacationId}`,
};
},
async get_pending_vacation_approvals(params: { limit?: number }, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 20, 50);
const caller = createVacationCaller(createScopedCallerContext(ctx));
const vacations = await caller.getPendingApprovals();
return vacations.map((v) => ({
id: v.id,
resource: v.resource.displayName,
eid: v.resource.eid,
chapter: v.resource.chapter,
type: v.type,
start: fmtDate(v.startDate),
end: fmtDate(v.endDate),
isHalfDay: v.isHalfDay,
})).slice(0, limit);
},
async get_team_vacation_overlap(params: {
resourceId: string; startDate: string; endDate: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createVacationCaller(createScopedCallerContext(ctx));
return caller.getTeamOverlapDetail({
resourceId: resource.id,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
});
},
// ── ENTITLEMENT ──
async get_entitlement_summary(params: { year?: number; resourceName?: string }, ctx: ToolContext) {
const year = params.year ?? new Date().getFullYear();
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
return caller.getYearSummaryDetail({
year,
...(params.resourceName ? { resourceName: params.resourceName } : {}),
});
},
async set_entitlement(params: {
resourceId: string; year: number; entitledDays: number; carryoverDays?: number;
}, ctx: ToolContext) {
if (params.carryoverDays !== undefined) {
return {
error: "Manual carryoverDays is not supported here. Carryover is computed automatically from prior-year balances.",
};
}
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
let entitlement;
try {
entitlement = await caller.set({
resourceId: resource.id,
year: params.year,
entitledDays: params.entitledDays,
});
} catch (error) {
const mapped = toAssistantEntitlementMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
entitlement,
message: `Set entitlement for ${resource.displayName} (${params.year}): ${params.entitledDays} days`,
};
},
...createVacationEntitlementExecutors({
createVacationCaller,
createEntitlementCaller,
createScopedCallerContext,
resolveResourceIdentifier,
parseIsoDate,
fmtDate,
parseAssistantVacationRequestType,
toAssistantVacationCreationError,
toAssistantVacationMutationError,
toAssistantEntitlementMutationError,
}),
// ── ESTIMATES ──
...createEstimateExecutors({
@@ -0,0 +1,496 @@
import { VacationType } from "@capakraken/db";
import { SystemRole } from "@capakraken/shared";
import type { TRPCContext } from "../../trpc.js";
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
type AssistantToolErrorResult = { error: string };
type ResolvedResource = {
id: string;
displayName: string;
};
type VacationRecord = {
id: string;
status: string;
effectiveDays?: number | null;
warnings?: unknown;
resource?: {
displayName?: string | null;
} | null;
};
type PendingVacationApproval = {
id: string;
resource: {
displayName: string;
eid: string | null;
chapter: string | null;
};
type: string;
startDate: Date;
endDate: Date;
isHalfDay: boolean;
};
type VacationEntitlementDeps = {
createVacationCaller: (ctx: TRPCContext) => {
create: (params: {
resourceId: string;
type: VacationType;
startDate: Date;
endDate: Date;
isHalfDay?: boolean;
halfDayPart?: "MORNING" | "AFTERNOON";
note?: string;
}) => Promise<VacationRecord>;
getById: (params: { id: string }) => Promise<VacationRecord>;
approve: (params: { id: string }) => Promise<VacationRecord>;
reject: (params: { id: string; rejectionReason?: string }) => Promise<VacationRecord>;
cancel: (params: { id: string }) => Promise<VacationRecord>;
getPendingApprovals: () => Promise<PendingVacationApproval[]>;
getTeamOverlapDetail: (params: {
resourceId: string;
startDate: Date;
endDate: Date;
}) => Promise<unknown>;
};
createEntitlementCaller: (ctx: TRPCContext) => {
getYearSummaryDetail: (params: {
year: number;
resourceName?: string;
}) => Promise<unknown>;
set: (params: {
resourceId: string;
year: number;
entitledDays: number;
}) => Promise<unknown>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
resolveResourceIdentifier: (
ctx: ToolContext,
identifier: string,
) => Promise<ResolvedResource | AssistantToolErrorResult>;
parseIsoDate: (value: string, fieldName: string) => Date;
fmtDate: (value: Date | null | undefined) => string | null;
parseAssistantVacationRequestType: (input: string) => VacationType;
toAssistantVacationCreationError: (
error: unknown,
) => AssistantToolErrorResult | null;
toAssistantVacationMutationError: (
error: unknown,
action: "approve" | "reject" | "cancel",
) => AssistantToolErrorResult | null;
toAssistantEntitlementMutationError: (
error: unknown,
) => AssistantToolErrorResult | null;
};
export const vacationEntitlementToolDefinitions: ToolDef[] = withToolAccess([
{
type: "function",
function: {
name: "create_vacation",
description: "Create a vacation/leave request through the real vacation workflow. Any authenticated user can request leave for their own resource; manager/admin can create requests for others. Always confirm with the user.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
type: {
type: "string",
enum: ["ANNUAL", "SICK", "OTHER"],
description: "Vacation type. PUBLIC_HOLIDAY requests are managed through holiday calendars, not manual vacation requests.",
},
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
isHalfDay: { type: "boolean", description: "Half day? Default: false" },
halfDayPart: { type: "string", description: "MORNING or AFTERNOON (if half day)" },
note: { type: "string", description: "Optional note" },
},
required: ["resourceId", "type", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "approve_vacation",
description: "Approve a vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "reject_vacation",
description: "Reject a pending vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
reason: { type: "string", description: "Rejection reason" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "cancel_vacation",
description: "Cancel a vacation request through the real vacation workflow. Users can cancel their own requests; manager/admin can cancel any request. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "get_pending_vacation_approvals",
description: "List vacation requests awaiting approval.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_team_vacation_overlap",
description: "Check if team members have overlapping vacations in a date range.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID to check overlap for" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
},
required: ["resourceId", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "get_entitlement_summary",
description: "Get vacation entitlement year summary for all resources or a specific resource.",
parameters: {
type: "object",
properties: {
year: { type: "integer", description: "Year. Default: current year" },
resourceName: { type: "string", description: "Filter by resource name (optional)" },
},
},
},
},
{
type: "function",
function: {
name: "set_entitlement",
description: "Set the annual vacation entitlement for a resource/year through the real entitlement workflow. Manager or admin role required. Carryover is computed automatically. Always confirm first.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
year: { type: "integer", description: "Year" },
entitledDays: { type: "number", description: "Number of entitled vacation days" },
},
required: ["resourceId", "year", "entitledDays"],
},
},
},
], {
approve_vacation: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
},
reject_vacation: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
},
get_pending_vacation_approvals: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
},
get_entitlement_summary: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
},
set_entitlement: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
},
});
export function createVacationEntitlementExecutors(
deps: VacationEntitlementDeps,
): Record<string, ToolExecutor> {
return {
async create_vacation(
params: {
resourceId: string;
type: string;
startDate: string;
endDate: string;
isHalfDay?: boolean;
halfDayPart?: string;
note?: string;
},
ctx: ToolContext,
) {
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
const type = deps.parseAssistantVacationRequestType(params.type);
let vacation;
try {
vacation = await caller.create({
resourceId: resource.id,
type,
startDate: deps.parseIsoDate(params.startDate, "startDate"),
endDate: deps.parseIsoDate(params.endDate, "endDate"),
...(params.isHalfDay !== undefined ? { isHalfDay: params.isHalfDay } : {}),
...(params.halfDayPart !== undefined ? { halfDayPart: params.halfDayPart as "MORNING" | "AFTERNOON" } : {}),
...(params.note !== undefined ? { note: params.note } : {}),
});
} catch (error) {
const mapped = deps.toAssistantVacationCreationError(error);
if (mapped) {
return mapped;
}
throw error;
}
const effectiveDays = typeof vacation.effectiveDays === "number"
? vacation.effectiveDays
: null;
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
message: `Created ${type} for ${resource.displayName}: ${params.startDate} to ${params.endDate} (status: ${vacation.status}${effectiveDays !== null ? `, deducted ${effectiveDays} day(s)` : ""})`,
vacationId: vacation.id,
vacation,
};
},
async approve_vacation(
params: { vacationId: string },
ctx: ToolContext,
) {
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = deps.toAssistantVacationMutationError(error, "approve");
if (mapped) {
return mapped;
}
throw error;
}
let approved;
try {
approved = await caller.approve({ id: params.vacationId });
} catch (error) {
const mapped = deps.toAssistantVacationMutationError(error, "approve");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
warnings: approved.warnings,
vacation: approved,
message: `Approved vacation for ${existing.resource?.displayName ?? params.vacationId}`,
};
},
async reject_vacation(
params: { vacationId: string; reason?: string },
ctx: ToolContext,
) {
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = deps.toAssistantVacationMutationError(error, "reject");
if (mapped) {
return mapped;
}
throw error;
}
let rejected;
try {
rejected = await caller.reject({
id: params.vacationId,
...(params.reason !== undefined ? { rejectionReason: params.reason } : {}),
});
} catch (error) {
const mapped = deps.toAssistantVacationMutationError(error, "reject");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
vacation: rejected,
message: `Rejected vacation for ${existing.resource?.displayName ?? params.vacationId}${params.reason ? `: ${params.reason}` : ""}`,
};
},
async cancel_vacation(
params: { vacationId: string },
ctx: ToolContext,
) {
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = deps.toAssistantVacationMutationError(error, "cancel");
if (mapped) {
return mapped;
}
throw error;
}
let cancelled;
try {
cancelled = await caller.cancel({ id: params.vacationId });
} catch (error) {
const mapped = deps.toAssistantVacationMutationError(error, "cancel");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
vacation: cancelled,
message: `Cancelled vacation for ${existing.resource?.displayName ?? params.vacationId}`,
};
},
async get_pending_vacation_approvals(
params: { limit?: number },
ctx: ToolContext,
) {
const limit = Math.min(params.limit ?? 20, 50);
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
const vacations = await caller.getPendingApprovals();
return vacations.map((vacation) => ({
id: vacation.id,
resource: vacation.resource.displayName,
eid: vacation.resource.eid,
chapter: vacation.resource.chapter,
type: vacation.type,
start: deps.fmtDate(vacation.startDate),
end: deps.fmtDate(vacation.endDate),
isHalfDay: vacation.isHalfDay,
})).slice(0, limit);
},
async get_team_vacation_overlap(
params: { resourceId: string; startDate: string; endDate: string },
ctx: ToolContext,
) {
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
return caller.getTeamOverlapDetail({
resourceId: resource.id,
startDate: deps.parseIsoDate(params.startDate, "startDate"),
endDate: deps.parseIsoDate(params.endDate, "endDate"),
});
},
async get_entitlement_summary(
params: { year?: number; resourceName?: string },
ctx: ToolContext,
) {
const year = params.year ?? new Date().getFullYear();
const caller = deps.createEntitlementCaller(deps.createScopedCallerContext(ctx));
return caller.getYearSummaryDetail({
year,
...(params.resourceName ? { resourceName: params.resourceName } : {}),
});
},
async set_entitlement(
params: {
resourceId: string;
year: number;
entitledDays: number;
carryoverDays?: number;
},
ctx: ToolContext,
) {
if (params.carryoverDays !== undefined) {
return {
error: "Manual carryoverDays is not supported here. Carryover is computed automatically from prior-year balances.",
};
}
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = deps.createEntitlementCaller(deps.createScopedCallerContext(ctx));
let entitlement;
try {
entitlement = await caller.set({
resourceId: resource.id,
year: params.year,
entitledDays: params.entitledDays,
});
} catch (error) {
const mapped = deps.toAssistantEntitlementMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
entitlement,
message: `Set entitlement for ${resource.displayName} (${params.year}): ${params.entitledDays} days`,
};
},
};
}