refactor(api): extract assistant config readmodels
This commit is contained in:
@@ -30,12 +30,13 @@
|
|||||||
- the neighboring client and org-unit admin mutations now live in their own domain module, keeping more CRUD wiring out of the monolithic router without changing the assistant contract
|
- the neighboring client and org-unit admin mutations now live in their own domain module, keeping more CRUD wiring out of the monolithic router without changing the assistant contract
|
||||||
- the adjacent chargeability/computation read helpers now live in their own domain module, keeping the advanced financial transparency read models out of the monolithic router without changing the assistant contract
|
- the adjacent chargeability/computation read helpers now live in their own domain module, keeping the advanced financial transparency read models out of the monolithic router without changing the assistant contract
|
||||||
- the neighboring country and metro-city admin mutations now live in their own domain module, keeping more settings-side CRUD wiring out of the monolithic router without changing the assistant contract
|
- the neighboring country and metro-city admin mutations now live in their own domain module, keeping more settings-side CRUD wiring out of the monolithic router without changing the assistant contract
|
||||||
|
- the adjacent management-level, utilization, calculation-rule, effort-rule, and experience-multiplier read helpers now live in their own domain module, further shrinking the monolithic assistant router without changing the assistant contract
|
||||||
|
|
||||||
## Next Up
|
## Next Up
|
||||||
|
|
||||||
Pin the next structural cleanup on the API side:
|
Pin the next structural cleanup on the API side:
|
||||||
continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract.
|
continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract.
|
||||||
The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as management-level plus utilization-rule configuration reads, or the remaining estimate and project admin helper clusters that are still embedded in the monolithic router.
|
The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the authenticated user/self-service assistant tools, or the remaining estimate and project admin helper clusters that are still embedded in the monolithic router.
|
||||||
|
|
||||||
## Remaining Major Themes
|
## Remaining Major Themes
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ import {
|
|||||||
chargeabilityComputationReadToolDefinitions,
|
chargeabilityComputationReadToolDefinitions,
|
||||||
createChargeabilityComputationExecutors,
|
createChargeabilityComputationExecutors,
|
||||||
} from "./assistant-tools/chargeability-computation.js";
|
} from "./assistant-tools/chargeability-computation.js";
|
||||||
|
import {
|
||||||
|
configReadmodelToolDefinitions,
|
||||||
|
createConfigReadmodelExecutors,
|
||||||
|
} from "./assistant-tools/config-readmodels.js";
|
||||||
import {
|
import {
|
||||||
countryMetroAdminToolDefinitions,
|
countryMetroAdminToolDefinitions,
|
||||||
createCountryMetroAdminExecutors,
|
createCountryMetroAdminExecutors,
|
||||||
@@ -2776,46 +2780,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
...countryMetroAdminToolDefinitions,
|
...countryMetroAdminToolDefinitions,
|
||||||
{
|
...configReadmodelToolDefinitions,
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_management_levels",
|
|
||||||
description: "List management level groups and their levels with target percentages.",
|
|
||||||
parameters: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_utilization_categories",
|
|
||||||
description: "List utilization categories (cost classification for projects).",
|
|
||||||
parameters: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_calculation_rules",
|
|
||||||
description: "List calculation rules for cost attribution and chargeability.",
|
|
||||||
parameters: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_effort_rules",
|
|
||||||
description: "List effort estimation rules with their formulas and conditions.",
|
|
||||||
parameters: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_experience_multipliers",
|
|
||||||
description: "List experience multipliers that adjust effort estimates based on seniority.",
|
|
||||||
parameters: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
@@ -5503,102 +5468,14 @@ const executors = {
|
|||||||
toAssistantCountryMutationError,
|
toAssistantCountryMutationError,
|
||||||
toAssistantMetroCityMutationError,
|
toAssistantMetroCityMutationError,
|
||||||
}),
|
}),
|
||||||
|
...createConfigReadmodelExecutors({
|
||||||
async list_management_levels(_params: Record<string, never>, ctx: ToolContext) {
|
createManagementLevelCaller,
|
||||||
const caller = createManagementLevelCaller(createScopedCallerContext(ctx));
|
createUtilizationCategoryCaller,
|
||||||
const groups = await caller.listGroups();
|
createCalculationRuleCaller,
|
||||||
return groups.map((g) => ({
|
createEffortRuleCaller,
|
||||||
id: g.id,
|
createExperienceMultiplierCaller,
|
||||||
name: g.name,
|
createScopedCallerContext,
|
||||||
target: g.targetPercentage ? `${g.targetPercentage}%` : null,
|
}),
|
||||||
levels: g.levels.map((l: { id: string; name: string }) => ({ id: l.id, name: l.name })),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
async list_utilization_categories(_params: Record<string, never>, ctx: ToolContext) {
|
|
||||||
const caller = createUtilizationCategoryCaller(createScopedCallerContext(ctx));
|
|
||||||
const categories = await caller.list();
|
|
||||||
const categoriesWithCounts = await Promise.all(
|
|
||||||
categories.map(async (category) => {
|
|
||||||
const detailed = await caller.getById({ id: category.id });
|
|
||||||
return {
|
|
||||||
category,
|
|
||||||
projectCount: detailed._count.projects,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return categoriesWithCounts.map(({ category, projectCount }) => ({
|
|
||||||
id: category.id,
|
|
||||||
code: category.code,
|
|
||||||
name: category.name,
|
|
||||||
description: category.description,
|
|
||||||
projectCount,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
async list_calculation_rules(_params: Record<string, never>, ctx: ToolContext) {
|
|
||||||
const caller = createCalculationRuleCaller(createScopedCallerContext(ctx));
|
|
||||||
const rules = await caller.list();
|
|
||||||
return rules.map((rule) => ({
|
|
||||||
id: rule.id,
|
|
||||||
name: rule.name,
|
|
||||||
description: rule.description,
|
|
||||||
isActive: rule.isActive,
|
|
||||||
triggerType: rule.triggerType,
|
|
||||||
orderType: rule.orderType,
|
|
||||||
costEffect: rule.costEffect,
|
|
||||||
costReductionPercent: rule.costReductionPercent,
|
|
||||||
chargeabilityEffect: rule.chargeabilityEffect,
|
|
||||||
priority: rule.priority,
|
|
||||||
project: rule.project
|
|
||||||
? {
|
|
||||||
id: rule.project.id,
|
|
||||||
name: rule.project.name,
|
|
||||||
shortCode: rule.project.shortCode,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
async list_effort_rules(_params: Record<string, never>, ctx: ToolContext) {
|
|
||||||
const caller = createEffortRuleCaller(createScopedCallerContext(ctx));
|
|
||||||
const ruleSets = await caller.list();
|
|
||||||
return ruleSets.flatMap((ruleSet) => ruleSet.rules.map((rule) => ({
|
|
||||||
id: rule.id,
|
|
||||||
description: rule.description,
|
|
||||||
scopeType: rule.scopeType,
|
|
||||||
discipline: rule.discipline,
|
|
||||||
chapter: rule.chapter,
|
|
||||||
unitMode: rule.unitMode,
|
|
||||||
hoursPerUnit: rule.hoursPerUnit,
|
|
||||||
sortOrder: rule.sortOrder,
|
|
||||||
ruleSet: {
|
|
||||||
name: ruleSet.name,
|
|
||||||
isDefault: ruleSet.isDefault,
|
|
||||||
},
|
|
||||||
})));
|
|
||||||
},
|
|
||||||
|
|
||||||
async list_experience_multipliers(_params: Record<string, never>, ctx: ToolContext) {
|
|
||||||
const caller = createExperienceMultiplierCaller(createScopedCallerContext(ctx));
|
|
||||||
const multiplierSets = await caller.list();
|
|
||||||
return multiplierSets.flatMap((multiplierSet) => multiplierSet.rules.map((rule) => ({
|
|
||||||
id: rule.id,
|
|
||||||
description: rule.description,
|
|
||||||
chapter: rule.chapter,
|
|
||||||
location: rule.location,
|
|
||||||
level: rule.level,
|
|
||||||
costMultiplier: rule.costMultiplier,
|
|
||||||
billMultiplier: rule.billMultiplier,
|
|
||||||
shoringRatio: rule.shoringRatio,
|
|
||||||
additionalEffortRatio: rule.additionalEffortRatio,
|
|
||||||
sortOrder: rule.sortOrder,
|
|
||||||
multiplierSet: {
|
|
||||||
name: multiplierSet.name,
|
|
||||||
isDefault: multiplierSet.isDefault,
|
|
||||||
},
|
|
||||||
})));
|
|
||||||
},
|
|
||||||
|
|
||||||
async list_users(params: { limit?: number }, ctx: ToolContext) {
|
async list_users(params: { limit?: number }, ctx: ToolContext) {
|
||||||
const caller = createUserCaller(createScopedCallerContext(ctx));
|
const caller = createUserCaller(createScopedCallerContext(ctx));
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import type { TRPCContext } from "../../trpc.js";
|
||||||
|
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||||
|
|
||||||
|
type ConfigReadmodelsDeps = {
|
||||||
|
createManagementLevelCaller: (ctx: TRPCContext) => {
|
||||||
|
listGroups: () => Promise<Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
targetPercentage: number | null;
|
||||||
|
levels: Array<{ id: string; name: string }>;
|
||||||
|
}>>;
|
||||||
|
};
|
||||||
|
createUtilizationCategoryCaller: (ctx: TRPCContext) => {
|
||||||
|
list: () => Promise<Array<{
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
}>>;
|
||||||
|
getById: (params: { id: string }) => Promise<{
|
||||||
|
_count: { projects: number };
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
createCalculationRuleCaller: (ctx: TRPCContext) => {
|
||||||
|
list: () => Promise<Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
triggerType: string;
|
||||||
|
orderType: string | null;
|
||||||
|
costEffect: string;
|
||||||
|
costReductionPercent: number | null;
|
||||||
|
chargeabilityEffect: string;
|
||||||
|
priority: number;
|
||||||
|
project: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
shortCode: string;
|
||||||
|
} | null;
|
||||||
|
}>>;
|
||||||
|
};
|
||||||
|
createEffortRuleCaller: (ctx: TRPCContext) => {
|
||||||
|
list: () => Promise<Array<{
|
||||||
|
name: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
rules: Array<{
|
||||||
|
id: string;
|
||||||
|
description: string | null;
|
||||||
|
scopeType: string;
|
||||||
|
discipline: string;
|
||||||
|
chapter: string | null;
|
||||||
|
unitMode: string;
|
||||||
|
hoursPerUnit: number;
|
||||||
|
sortOrder: number;
|
||||||
|
}>;
|
||||||
|
}>>;
|
||||||
|
};
|
||||||
|
createExperienceMultiplierCaller: (ctx: TRPCContext) => {
|
||||||
|
list: () => Promise<Array<{
|
||||||
|
name: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
rules: Array<{
|
||||||
|
id: string;
|
||||||
|
description: string | null;
|
||||||
|
chapter: string | null;
|
||||||
|
location: string | null;
|
||||||
|
level: string | null;
|
||||||
|
costMultiplier: number;
|
||||||
|
billMultiplier: number;
|
||||||
|
shoringRatio: number | null;
|
||||||
|
additionalEffortRatio: number | null;
|
||||||
|
sortOrder: number;
|
||||||
|
}>;
|
||||||
|
}>>;
|
||||||
|
};
|
||||||
|
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const configReadmodelToolDefinitions: ToolDef[] = [
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_management_levels",
|
||||||
|
description: "List management level groups and their levels with target percentages.",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_utilization_categories",
|
||||||
|
description: "List utilization categories (cost classification for projects).",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_calculation_rules",
|
||||||
|
description: "List calculation rules for cost attribution and chargeability.",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_effort_rules",
|
||||||
|
description: "List effort estimation rules with their formulas and conditions.",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_experience_multipliers",
|
||||||
|
description: "List experience multipliers that adjust effort estimates based on seniority.",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function createConfigReadmodelExecutors(
|
||||||
|
deps: ConfigReadmodelsDeps,
|
||||||
|
): Record<string, ToolExecutor> {
|
||||||
|
return {
|
||||||
|
async list_management_levels(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
|
const caller = deps.createManagementLevelCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const groups = await caller.listGroups();
|
||||||
|
return groups.map((group) => ({
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
target: group.targetPercentage ? `${group.targetPercentage}%` : null,
|
||||||
|
levels: group.levels.map((level) => ({ id: level.id, name: level.name })),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
async list_utilization_categories(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
|
const caller = deps.createUtilizationCategoryCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const categories = await caller.list();
|
||||||
|
const categoriesWithCounts = await Promise.all(
|
||||||
|
categories.map(async (category) => ({
|
||||||
|
category,
|
||||||
|
projectCount: (await caller.getById({ id: category.id }))._count.projects,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return categoriesWithCounts.map(({ category, projectCount }) => ({
|
||||||
|
id: category.id,
|
||||||
|
code: category.code,
|
||||||
|
name: category.name,
|
||||||
|
description: category.description,
|
||||||
|
projectCount,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
async list_calculation_rules(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
|
const caller = deps.createCalculationRuleCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const rules = await caller.list();
|
||||||
|
return rules.map((rule) => ({
|
||||||
|
id: rule.id,
|
||||||
|
name: rule.name,
|
||||||
|
description: rule.description,
|
||||||
|
isActive: rule.isActive,
|
||||||
|
triggerType: rule.triggerType,
|
||||||
|
orderType: rule.orderType,
|
||||||
|
costEffect: rule.costEffect,
|
||||||
|
costReductionPercent: rule.costReductionPercent,
|
||||||
|
chargeabilityEffect: rule.chargeabilityEffect,
|
||||||
|
priority: rule.priority,
|
||||||
|
project: rule.project
|
||||||
|
? {
|
||||||
|
id: rule.project.id,
|
||||||
|
name: rule.project.name,
|
||||||
|
shortCode: rule.project.shortCode,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
async list_effort_rules(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
|
const caller = deps.createEffortRuleCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const ruleSets = await caller.list();
|
||||||
|
return ruleSets.flatMap((ruleSet) => ruleSet.rules.map((rule) => ({
|
||||||
|
id: rule.id,
|
||||||
|
description: rule.description,
|
||||||
|
scopeType: rule.scopeType,
|
||||||
|
discipline: rule.discipline,
|
||||||
|
chapter: rule.chapter,
|
||||||
|
unitMode: rule.unitMode,
|
||||||
|
hoursPerUnit: rule.hoursPerUnit,
|
||||||
|
sortOrder: rule.sortOrder,
|
||||||
|
ruleSet: {
|
||||||
|
name: ruleSet.name,
|
||||||
|
isDefault: ruleSet.isDefault,
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
},
|
||||||
|
|
||||||
|
async list_experience_multipliers(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
|
const caller = deps.createExperienceMultiplierCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const multiplierSets = await caller.list();
|
||||||
|
return multiplierSets.flatMap((multiplierSet) => multiplierSet.rules.map((rule) => ({
|
||||||
|
id: rule.id,
|
||||||
|
description: rule.description,
|
||||||
|
chapter: rule.chapter,
|
||||||
|
location: rule.location,
|
||||||
|
level: rule.level,
|
||||||
|
costMultiplier: rule.costMultiplier,
|
||||||
|
billMultiplier: rule.billMultiplier,
|
||||||
|
shoringRatio: rule.shoringRatio,
|
||||||
|
additionalEffortRatio: rule.additionalEffortRatio,
|
||||||
|
sortOrder: rule.sortOrder,
|
||||||
|
multiplierSet: {
|
||||||
|
name: multiplierSet.name,
|
||||||
|
isDefault: multiplierSet.isDefault,
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user