perf(api): eliminate 3 N+1 query patterns

- timeline-holiday-load-support: deduplicate getResolvedCalendarHolidays
  by location key so resources sharing the same country/state/city resolve
  holidays once instead of per-resource
- rate-card-lookup: add lookupRatesBatch that loads rate card lines once
  and scores locally per demand line, replacing per-line DB round-trips
  in estimate-demand-lines autoFillDemandLineRates
- config-readmodels: include _count in utilization-category list query
  instead of calling getById per category for project counts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 22:59:45 +02:00
parent dd2c9c0f88
commit 5a4836d292
8 changed files with 727 additions and 571 deletions
@@ -43,17 +43,9 @@ describe("assistant master data management and utilization read tools", () => {
description: "Client work", description: "Client work",
isActive: true, isActive: true,
sortOrder: 1, sortOrder: 1,
_count: { projects: 3 },
}, },
]), ]),
findUnique: vi.fn().mockResolvedValue({
id: "util_billable",
code: "BILLABLE",
name: "Billable",
description: "Client work",
isActive: true,
sortOrder: 1,
_count: { projects: 3 },
}),
}, },
}; };
const ctx = createToolContext(db, { const ctx = createToolContext(db, {
@@ -71,9 +63,6 @@ describe("assistant master data management and utilization read tools", () => {
expect(db.utilizationCategory.findMany).toHaveBeenCalledWith({ expect(db.utilizationCategory.findMany).toHaveBeenCalledWith({
where: {}, where: {},
orderBy: { sortOrder: "asc" }, orderBy: { sortOrder: "asc" },
});
expect(db.utilizationCategory.findUnique).toHaveBeenCalledWith({
where: { id: "util_billable" },
include: { _count: { select: { projects: true } } }, include: { _count: { select: { projects: true } } },
}); });
@@ -37,11 +37,13 @@ function createUnauthenticatedContext(db: Record<string, unknown>) {
describe("master-data router authorization", () => { describe("master-data router authorization", () => {
it("requires planning read access for blueprint summaries with project counts", async () => { it("requires planning read access for blueprint summaries with project counts", async () => {
const findMany = vi.fn(); const findMany = vi.fn();
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({ const caller = createCallerFactory(blueprintRouter)(
blueprint: { createProtectedContext({
findMany, blueprint: {
}, findMany,
})); },
}),
);
await expect(caller.listSummaries()).rejects.toMatchObject({ await expect(caller.listSummaries()).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
@@ -59,13 +61,18 @@ describe("master-data router authorization", () => {
_count: { projects: 4 }, _count: { projects: 4 },
}, },
]); ]);
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({ const caller = createCallerFactory(blueprintRouter)(
blueprint: { createProtectedContext(
findMany, {
}, blueprint: {
}, { findMany,
granted: [PermissionKey.VIEW_PLANNING], },
})); },
{
granted: [PermissionKey.VIEW_PLANNING],
},
),
);
const result = await caller.listSummaries(); const result = await caller.listSummaries();
@@ -85,19 +92,23 @@ describe("master-data router authorization", () => {
const findMany = vi.fn(); const findMany = vi.fn();
const findUnique = vi.fn(); const findUnique = vi.fn();
const findFirst = vi.fn(); const findFirst = vi.fn();
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({ const caller = createCallerFactory(blueprintRouter)(
blueprint: { createProtectedContext({
findMany, blueprint: {
findUnique, findMany,
findFirst, findUnique,
}, findFirst,
})); },
}),
);
await expect(caller.list({ isActive: true })).rejects.toMatchObject({ await expect(caller.list({ isActive: true })).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
message: "Planning read access required", message: "Planning read access required",
}); });
await expect(caller.getByIdentifier({ identifier: "Consulting Blueprint" })).rejects.toMatchObject({ await expect(
caller.getByIdentifier({ identifier: "Consulting Blueprint" }),
).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
message: "Planning read access required", message: "Planning read access required",
}); });
@@ -121,7 +132,8 @@ describe("master-data router authorization", () => {
isActive: true, isActive: true,
}, },
]); ]);
const getByIdFindUnique = vi.fn() const getByIdFindUnique = vi
.fn()
.mockResolvedValueOnce({ .mockResolvedValueOnce({
id: "bp_1", id: "bp_1",
name: "Consulting Blueprint", name: "Consulting Blueprint",
@@ -145,15 +157,20 @@ describe("master-data router authorization", () => {
rolePresets: [], rolePresets: [],
isActive: true, isActive: true,
}); });
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({ const caller = createCallerFactory(blueprintRouter)(
blueprint: { createProtectedContext(
findMany: listFindMany, {
findUnique: getByIdFindUnique, blueprint: {
findFirst: getByIdentifierFindFirst, findMany: listFindMany,
}, findUnique: getByIdFindUnique,
}, { findFirst: getByIdentifierFindFirst,
granted: [PermissionKey.VIEW_PLANNING], },
})); },
{
granted: [PermissionKey.VIEW_PLANNING],
},
),
);
const listResult = await caller.list({ target: BlueprintTarget.PROJECT, isActive: true }); const listResult = await caller.list({ target: BlueprintTarget.PROJECT, isActive: true });
const byIdResult = await caller.getById({ id: "bp_1" }); const byIdResult = await caller.getById({ id: "bp_1" });
@@ -177,22 +194,30 @@ describe("master-data router authorization", () => {
it("requires authenticated planning access for global blueprint field definitions", async () => { it("requires authenticated planning access for global blueprint field definitions", async () => {
const findMany = vi.fn(); const findMany = vi.fn();
const unauthenticatedCaller = createCallerFactory(blueprintRouter)(createUnauthenticatedContext({ const unauthenticatedCaller = createCallerFactory(blueprintRouter)(
blueprint: { createUnauthenticatedContext({
findMany, blueprint: {
}, findMany,
})); },
const authenticatedCaller = createCallerFactory(blueprintRouter)(createProtectedContext({ }),
blueprint: { );
findMany, const authenticatedCaller = createCallerFactory(blueprintRouter)(
}, createProtectedContext({
})); blueprint: {
findMany,
},
}),
);
await expect(unauthenticatedCaller.getGlobalFieldDefs({ target: BlueprintTarget.PROJECT })).rejects.toMatchObject({ await expect(
unauthenticatedCaller.getGlobalFieldDefs({ target: BlueprintTarget.PROJECT }),
).rejects.toMatchObject({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Authentication required", message: "Authentication required",
}); });
await expect(authenticatedCaller.getGlobalFieldDefs({ target: BlueprintTarget.PROJECT })).rejects.toMatchObject({ await expect(
authenticatedCaller.getGlobalFieldDefs({ target: BlueprintTarget.PROJECT }),
).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
message: "Planning read access required", message: "Planning read access required",
}); });
@@ -211,13 +236,18 @@ describe("master-data router authorization", () => {
], ],
}, },
]); ]);
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({ const caller = createCallerFactory(blueprintRouter)(
blueprint: { createProtectedContext(
findMany, {
}, blueprint: {
}, { findMany,
granted: [PermissionKey.VIEW_PLANNING], },
})); },
{
granted: [PermissionKey.VIEW_PLANNING],
},
),
);
const result = await caller.getGlobalFieldDefs({ target: BlueprintTarget.PROJECT }); const result = await caller.getGlobalFieldDefs({ target: BlueprintTarget.PROJECT });
@@ -254,20 +284,24 @@ describe("master-data router authorization", () => {
metroCities: [{ id: "city_ber", name: "Berlin", countryId: "country_de" }], metroCities: [{ id: "city_ber", name: "Berlin", countryId: "country_de" }],
}, },
]); ]);
const caller = createCallerFactory(countryRouter)(createProtectedContext({ const caller = createCallerFactory(countryRouter)(
country: { createProtectedContext({
findMany, country: {
}, findMany,
})); },
}),
);
const result = await caller.list({ isActive: true }); const result = await caller.list({ isActive: true });
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ expect(findMany).toHaveBeenCalledWith(
where: { isActive: true }, expect.objectContaining({
include: { metroCities: { orderBy: { name: "asc" } } }, where: { isActive: true },
orderBy: { name: "asc" }, include: { metroCities: { orderBy: { name: "asc" } } },
})); orderBy: { name: "asc" },
}),
);
}); });
it("keeps minimal country lookups available to authenticated users", async () => { it("keeps minimal country lookups available to authenticated users", async () => {
@@ -278,12 +312,14 @@ describe("master-data router authorization", () => {
isActive: true, isActive: true,
dailyWorkingHours: 8, dailyWorkingHours: 8,
}); });
const caller = createCallerFactory(countryRouter)(createProtectedContext({ const caller = createCallerFactory(countryRouter)(
country: { createProtectedContext({
findUnique: vi.fn().mockResolvedValue(null), country: {
findFirst, findUnique: vi.fn().mockResolvedValue(null),
}, findFirst,
})); },
}),
);
const result = await caller.resolveByIdentifier({ identifier: "de" }); const result = await caller.resolveByIdentifier({ identifier: "de" });
@@ -303,11 +339,13 @@ describe("master-data router authorization", () => {
name: "Munich", name: "Munich",
countryId: "country_de", countryId: "country_de",
}); });
const caller = createCallerFactory(countryRouter)(createProtectedContext({ const caller = createCallerFactory(countryRouter)(
metroCity: { createProtectedContext({
findUnique, metroCity: {
}, findUnique,
})); },
}),
);
const result = await caller.getCityById({ id: "city_muc" }); const result = await caller.getCityById({ id: "city_muc" });
@@ -323,12 +361,14 @@ describe("master-data router authorization", () => {
}); });
it("requires resource overview access for detailed country reads with resource counts", async () => { it("requires resource overview access for detailed country reads with resource counts", async () => {
const caller = createCallerFactory(countryRouter)(createProtectedContext({ const caller = createCallerFactory(countryRouter)(
country: { createProtectedContext({
findFirst: vi.fn(), country: {
findUnique: vi.fn(), findFirst: vi.fn(),
}, findUnique: vi.fn(),
})); },
}),
);
await expect(caller.getByIdentifier({ identifier: "DE" })).rejects.toMatchObject({ await expect(caller.getByIdentifier({ identifier: "DE" })).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
@@ -351,24 +391,31 @@ describe("master-data router authorization", () => {
metroCities: [{ id: "city_muc", name: "Munich", countryId: "country_de" }], metroCities: [{ id: "city_muc", name: "Munich", countryId: "country_de" }],
_count: { resources: 12 }, _count: { resources: 12 },
}); });
const caller = createCallerFactory(countryRouter)(createProtectedContext({ const caller = createCallerFactory(countryRouter)(
country: { createProtectedContext(
findUnique: vi.fn().mockResolvedValue(null), {
findFirst, country: {
}, findUnique: vi.fn().mockResolvedValue(null),
}, { findFirst,
granted: [PermissionKey.VIEW_ALL_RESOURCES], },
})); },
{
granted: [PermissionKey.VIEW_ALL_RESOURCES],
},
),
);
const result = await caller.getByIdentifier({ identifier: "DE" }); const result = await caller.getByIdentifier({ identifier: "DE" });
expect(result._count.resources).toBe(12); expect(result._count.resources).toBe(12);
expect(findFirst).toHaveBeenCalledWith(expect.objectContaining({ expect(findFirst).toHaveBeenCalledWith(
include: expect.objectContaining({ expect.objectContaining({
metroCities: expect.any(Object), include: expect.objectContaining({
_count: expect.any(Object), metroCities: expect.any(Object),
_count: expect.any(Object),
}),
}), }),
})); );
}); });
it("allows detailed country reads for users with manage-resources access", async () => { it("allows detailed country reads for users with manage-resources access", async () => {
@@ -382,13 +429,18 @@ describe("master-data router authorization", () => {
metroCities: [], metroCities: [],
_count: { resources: 4 }, _count: { resources: 4 },
}); });
const caller = createCallerFactory(countryRouter)(createProtectedContext({ const caller = createCallerFactory(countryRouter)(
country: { createProtectedContext(
findUnique, {
}, country: {
}, { findUnique,
granted: [PermissionKey.MANAGE_RESOURCES], },
})); },
{
granted: [PermissionKey.MANAGE_RESOURCES],
},
),
);
const result = await caller.getById({ id: "country_de" }); const result = await caller.getById({ id: "country_de" });
@@ -397,21 +449,21 @@ describe("master-data router authorization", () => {
}); });
it("keeps minimal org-unit lookups available to authenticated users", async () => { it("keeps minimal org-unit lookups available to authenticated users", async () => {
const findFirst = vi.fn() const findFirst = vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce({
.mockResolvedValueOnce(null) id: "ou_1",
.mockResolvedValueOnce({ name: "Delivery",
id: "ou_1", shortName: "DEL",
name: "Delivery", level: 5,
shortName: "DEL", isActive: true,
level: 5, });
isActive: true, const caller = createCallerFactory(orgUnitRouter)(
}); createProtectedContext({
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({ orgUnit: {
orgUnit: { findUnique: vi.fn().mockResolvedValue(null),
findUnique: vi.fn().mockResolvedValue(null), findFirst,
findFirst, },
}, }),
})); );
const result = await caller.resolveByIdentifier({ identifier: "DEL" }); const result = await caller.resolveByIdentifier({ identifier: "DEL" });
@@ -426,11 +478,13 @@ describe("master-data router authorization", () => {
it("requires resource overview access for org-unit list and tree reads", async () => { it("requires resource overview access for org-unit list and tree reads", async () => {
const findMany = vi.fn(); const findMany = vi.fn();
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({ const caller = createCallerFactory(orgUnitRouter)(
orgUnit: { createProtectedContext({
findMany, orgUnit: {
}, findMany,
})); },
}),
);
await expect(caller.list({ level: 5, isActive: true })).rejects.toMatchObject({ await expect(caller.list({ level: 5, isActive: true })).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
@@ -445,12 +499,14 @@ describe("master-data router authorization", () => {
}); });
it("requires resource overview access for detailed org-unit reads with staffing counts", async () => { it("requires resource overview access for detailed org-unit reads with staffing counts", async () => {
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({ const caller = createCallerFactory(orgUnitRouter)(
orgUnit: { createProtectedContext({
findFirst: vi.fn(), orgUnit: {
findUnique: vi.fn(), findFirst: vi.fn(),
}, findUnique: vi.fn(),
})); },
}),
);
await expect(caller.getByIdentifier({ identifier: "Delivery" })).rejects.toMatchObject({ await expect(caller.getByIdentifier({ identifier: "Delivery" })).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
@@ -473,21 +529,28 @@ describe("master-data router authorization", () => {
isActive: true, isActive: true,
_count: { resources: 7 }, _count: { resources: 7 },
}); });
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({ const caller = createCallerFactory(orgUnitRouter)(
orgUnit: { createProtectedContext(
findUnique: vi.fn().mockResolvedValue(null), {
findFirst, orgUnit: {
}, findUnique: vi.fn().mockResolvedValue(null),
}, { findFirst,
granted: [PermissionKey.VIEW_ALL_RESOURCES], },
})); },
{
granted: [PermissionKey.VIEW_ALL_RESOURCES],
},
),
);
const result = await caller.getByIdentifier({ identifier: "Delivery" }); const result = await caller.getByIdentifier({ identifier: "Delivery" });
expect(result._count.resources).toBe(7); expect(result._count.resources).toBe(7);
expect(findFirst).toHaveBeenCalledWith(expect.objectContaining({ expect(findFirst).toHaveBeenCalledWith(
include: { _count: { select: { resources: true } } }, expect.objectContaining({
})); include: { _count: { select: { resources: true } } },
}),
);
}); });
it("allows org-unit list and tree reads for users with resource overview access", async () => { it("allows org-unit list and tree reads for users with resource overview access", async () => {
@@ -517,15 +580,21 @@ describe("master-data router authorization", () => {
updatedAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"),
}, },
]); ]);
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({ const caller = createCallerFactory(orgUnitRouter)(
orgUnit: { createProtectedContext(
findMany: vi.fn() {
.mockImplementationOnce(listFindMany) orgUnit: {
.mockImplementationOnce(treeFindMany), findMany: vi
}, .fn()
}, { .mockImplementationOnce(listFindMany)
granted: [PermissionKey.MANAGE_RESOURCES], .mockImplementationOnce(treeFindMany),
})); },
},
{
granted: [PermissionKey.MANAGE_RESOURCES],
},
),
);
const listResult = await caller.list({ level: 5, isActive: true }); const listResult = await caller.list({ level: 5, isActive: true });
const treeResult = await caller.getTree({ isActive: true }); const treeResult = await caller.getTree({ isActive: true });
@@ -549,21 +618,21 @@ describe("master-data router authorization", () => {
}); });
it("keeps minimal client lookups available to authenticated users", async () => { it("keeps minimal client lookups available to authenticated users", async () => {
const findUnique = vi.fn() const findUnique = vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce({
.mockResolvedValueOnce(null) id: "client_1",
.mockResolvedValueOnce({ name: "Acme Studios",
id: "client_1", code: "ACME",
name: "Acme Studios", parentId: null,
code: "ACME", isActive: true,
parentId: null, });
isActive: true, const caller = createCallerFactory(clientRouter)(
}); createProtectedContext({
const caller = createCallerFactory(clientRouter)(createProtectedContext({ client: {
client: { findUnique,
findUnique, findFirst: vi.fn(),
findFirst: vi.fn(), },
}, }),
})); );
const result = await caller.resolveByIdentifier({ identifier: "ACME" }); const result = await caller.resolveByIdentifier({ identifier: "ACME" });
@@ -578,11 +647,13 @@ describe("master-data router authorization", () => {
it("requires planning read access for client list and tree reads", async () => { it("requires planning read access for client list and tree reads", async () => {
const findMany = vi.fn(); const findMany = vi.fn();
const caller = createCallerFactory(clientRouter)(createProtectedContext({ const caller = createCallerFactory(clientRouter)(
client: { createProtectedContext({
findMany, client: {
}, findMany,
})); },
}),
);
await expect(caller.list({ isActive: true })).rejects.toMatchObject({ await expect(caller.list({ isActive: true })).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
@@ -597,12 +668,14 @@ describe("master-data router authorization", () => {
}); });
it("requires planning read access for detailed client reads with project counts", async () => { it("requires planning read access for detailed client reads with project counts", async () => {
const caller = createCallerFactory(clientRouter)(createProtectedContext({ const caller = createCallerFactory(clientRouter)(
client: { createProtectedContext({
findFirst: vi.fn(), client: {
findUnique: vi.fn(), findFirst: vi.fn(),
}, findUnique: vi.fn(),
})); },
}),
);
await expect(caller.getByIdentifier({ identifier: "Acme" })).rejects.toMatchObject({ await expect(caller.getByIdentifier({ identifier: "Acme" })).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
@@ -642,7 +715,8 @@ describe("master-data router authorization", () => {
updatedAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"),
}, },
]); ]);
const getByIdFindUnique = vi.fn() const getByIdFindUnique = vi
.fn()
.mockResolvedValueOnce({ .mockResolvedValueOnce({
id: "client_1", id: "client_1",
name: "Acme Studios", name: "Acme Studios",
@@ -670,17 +744,23 @@ describe("master-data router authorization", () => {
updatedAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"),
_count: { children: 1, projects: 4 }, _count: { children: 1, projects: 4 },
}); });
const caller = createCallerFactory(clientRouter)(createProtectedContext({ const caller = createCallerFactory(clientRouter)(
client: { createProtectedContext(
findMany: vi.fn() {
.mockImplementationOnce(listFindMany) client: {
.mockImplementationOnce(treeFindMany), findMany: vi
findUnique: getByIdFindUnique, .fn()
findFirst: getByIdentifierFindFirst, .mockImplementationOnce(listFindMany)
}, .mockImplementationOnce(treeFindMany),
}, { findUnique: getByIdFindUnique,
granted: [PermissionKey.VIEW_PLANNING], findFirst: getByIdentifierFindFirst,
})); },
},
{
granted: [PermissionKey.VIEW_PLANNING],
},
),
);
const listResult = await caller.list({ isActive: true, search: "Acme" }); const listResult = await caller.list({ isActive: true, search: "Acme" });
const treeResult = await caller.getTree({ isActive: true }); const treeResult = await caller.getTree({ isActive: true });
@@ -720,21 +800,25 @@ describe("master-data router authorization", () => {
_count: { select: { projects: true, children: true } }, _count: { select: { projects: true, children: true } },
}, },
}); });
expect(getByIdentifierFindFirst).toHaveBeenCalledWith(expect.objectContaining({ expect(getByIdentifierFindFirst).toHaveBeenCalledWith(
where: { name: { equals: "Acme Studios", mode: "insensitive" } }, expect.objectContaining({
include: { _count: { select: { projects: true, children: true } } }, where: { name: { equals: "Acme Studios", mode: "insensitive" } },
})); include: { _count: { select: { projects: true, children: true } } },
}),
);
}); });
it("requires planning read access for utilization-category reads with project counts", async () => { it("requires planning read access for utilization-category reads with project counts", async () => {
const listFindMany = vi.fn(); const listFindMany = vi.fn();
const getByIdFindUnique = vi.fn(); const getByIdFindUnique = vi.fn();
const caller = createCallerFactory(utilizationCategoryRouter)(createProtectedContext({ const caller = createCallerFactory(utilizationCategoryRouter)(
utilizationCategory: { createProtectedContext({
findMany: listFindMany, utilizationCategory: {
findUnique: getByIdFindUnique, findMany: listFindMany,
}, findUnique: getByIdFindUnique,
})); },
}),
);
await expect(caller.list({ isActive: true })).rejects.toMatchObject({ await expect(caller.list({ isActive: true })).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
@@ -771,14 +855,19 @@ describe("master-data router authorization", () => {
isActive: true, isActive: true,
_count: { projects: 12 }, _count: { projects: 12 },
}); });
const caller = createCallerFactory(utilizationCategoryRouter)(createProtectedContext({ const caller = createCallerFactory(utilizationCategoryRouter)(
utilizationCategory: { createProtectedContext(
findMany: listFindMany, {
findUnique: getByIdFindUnique, utilizationCategory: {
}, findMany: listFindMany,
}, { findUnique: getByIdFindUnique,
granted: [PermissionKey.VIEW_PLANNING], },
})); },
{
granted: [PermissionKey.VIEW_PLANNING],
},
),
);
const listResult = await caller.list({ isActive: true }); const listResult = await caller.list({ isActive: true });
const byIdResult = await caller.getById({ id: "util_chargeable" }); const byIdResult = await caller.getById({ id: "util_chargeable" });
@@ -788,6 +877,7 @@ describe("master-data router authorization", () => {
expect(listFindMany).toHaveBeenCalledWith({ expect(listFindMany).toHaveBeenCalledWith({
where: { isActive: true }, where: { isActive: true },
orderBy: { sortOrder: "asc" }, orderBy: { sortOrder: "asc" },
include: { _count: { select: { projects: true } } },
}); });
expect(getByIdFindUnique).toHaveBeenCalledWith({ expect(getByIdFindUnique).toHaveBeenCalledWith({
where: { id: "util_chargeable" }, where: { id: "util_chargeable" },
@@ -798,12 +888,14 @@ describe("master-data router authorization", () => {
it("requires planning read access for management-level reads", async () => { it("requires planning read access for management-level reads", async () => {
const listFindMany = vi.fn(); const listFindMany = vi.fn();
const getByIdFindUnique = vi.fn(); const getByIdFindUnique = vi.fn();
const caller = createCallerFactory(managementLevelRouter)(createProtectedContext({ const caller = createCallerFactory(managementLevelRouter)(
managementLevelGroup: { createProtectedContext({
findMany: listFindMany, managementLevelGroup: {
findUnique: getByIdFindUnique, findMany: listFindMany,
}, findUnique: getByIdFindUnique,
})); },
}),
);
await expect(caller.listGroups()).rejects.toMatchObject({ await expect(caller.listGroups()).rejects.toMatchObject({
code: "FORBIDDEN", code: "FORBIDDEN",
@@ -836,14 +928,19 @@ describe("master-data router authorization", () => {
levels: [{ id: "mgmt_level_1", name: "Senior Team Lead" }], levels: [{ id: "mgmt_level_1", name: "Senior Team Lead" }],
_count: { resources: 6 }, _count: { resources: 6 },
}); });
const caller = createCallerFactory(managementLevelRouter)(createProtectedContext({ const caller = createCallerFactory(managementLevelRouter)(
managementLevelGroup: { createProtectedContext(
findMany: listFindMany, {
findUnique: getByIdFindUnique, managementLevelGroup: {
}, findMany: listFindMany,
}, { findUnique: getByIdFindUnique,
granted: [PermissionKey.VIEW_PLANNING], },
})); },
{
granted: [PermissionKey.VIEW_PLANNING],
},
),
);
const listResult = await caller.listGroups(); const listResult = await caller.listGroups();
const detailResult = await caller.getGroupById({ id: "mgmt_group_1" }); const detailResult = await caller.getGroupById({ id: "mgmt_group_1" });
@@ -48,6 +48,7 @@ describe("utilization category router", () => {
expect(findMany).toHaveBeenCalledWith({ expect(findMany).toHaveBeenCalledWith({
where: { isActive: true }, where: { isActive: true },
orderBy: { sortOrder: "asc" }, orderBy: { sortOrder: "asc" },
include: { _count: { select: { projects: true } } },
}); });
}); });
+141 -120
View File
@@ -13,13 +13,13 @@
*/ */
export interface RateCardLookupParams { export interface RateCardLookupParams {
clientId?: string | null; clientId?: string | null | undefined;
chapter?: string | null; chapter?: string | null | undefined;
roleId?: string | null; roleId?: string | null | undefined;
seniority?: string | null; seniority?: string | null | undefined;
location?: string | null; location?: string | null | undefined;
workType?: string | null; workType?: string | null | undefined;
effectiveDate?: Date | null; effectiveDate?: Date | null | undefined;
} }
export interface RateCardLookupResult { export interface RateCardLookupResult {
@@ -49,6 +49,115 @@ interface RateCardLineRow {
}; };
} }
const RATE_CARD_LINE_SELECT = {
id: true,
rateCardId: true,
roleId: true,
chapter: true,
location: true,
seniority: true,
workType: true,
costRateCents: true,
billRateCents: true,
rateCard: {
select: {
id: true,
name: true,
currency: true,
clientId: true,
},
},
} as const;
function scoreLine(
line: RateCardLineRow,
params: RateCardLookupParams,
): { score: number; mismatch: boolean } {
let score = 0;
let mismatch = false;
if (params.clientId && line.rateCard.clientId === params.clientId) {
score += 100;
} else if (params.clientId && line.rateCard.clientId != null) {
mismatch = true;
}
if (params.roleId && line.roleId) {
if (line.roleId === params.roleId) score += 16;
else mismatch = true;
}
if (params.chapter && line.chapter) {
if (line.chapter === params.chapter) score += 8;
else mismatch = true;
}
if (params.seniority && line.seniority) {
if (line.seniority === params.seniority) score += 4;
else mismatch = true;
}
if (params.location && line.location) {
if (line.location === params.location) score += 2;
else mismatch = true;
}
if (params.workType && line.workType) {
if (line.workType === params.workType) score += 1;
else mismatch = true;
}
return { score, mismatch };
}
function findBestMatch(
lines: RateCardLineRow[],
params: RateCardLookupParams,
): RateCardLookupResult | null {
if (lines.length === 0) return null;
let bestLine: RateCardLineRow | null = null;
let bestScore = -1;
for (const line of lines) {
const { score, mismatch } = scoreLine(line, params);
if (!mismatch && score > bestScore) {
bestScore = score;
bestLine = line;
}
}
if (!bestLine) return null;
return {
costRateCents: bestLine.costRateCents,
billRateCents: bestLine.billRateCents ?? 0,
currency: bestLine.rateCard.currency,
rateCardId: bestLine.rateCard.id,
rateCardLineId: bestLine.id,
rateCardName: bestLine.rateCard.name,
};
}
function buildRateCardWhere(
clientId: string | null | undefined,
effectiveDate: Date,
): Record<string, unknown> {
const where: Record<string, unknown> = {
isActive: true,
OR: [{ effectiveFrom: null }, { effectiveFrom: { lte: effectiveDate } }],
AND: [
{
OR: [{ effectiveTo: null }, { effectiveTo: { gte: effectiveDate } }],
},
],
};
if (clientId) {
where.clientId = { in: [clientId, null] };
}
return where;
}
/** /**
* Look up the best-matching rate card line for a given set of criteria. * Look up the best-matching rate card line for a given set of criteria.
* Returns null when no active rate card line matches. * Returns null when no active rate card line matches.
@@ -59,120 +168,32 @@ export async function lookupRate(
params: RateCardLookupParams, params: RateCardLookupParams,
): Promise<RateCardLookupResult | null> { ): Promise<RateCardLookupResult | null> {
const effectiveDate = params.effectiveDate ?? new Date(); const effectiveDate = params.effectiveDate ?? new Date();
// Build rate card filter: active cards, within effective date range
const rateCardWhere: Record<string, unknown> = {
isActive: true,
OR: [
{ effectiveFrom: null },
{ effectiveFrom: { lte: effectiveDate } },
],
AND: [
{
OR: [
{ effectiveTo: null },
{ effectiveTo: { gte: effectiveDate } },
],
},
],
};
// If we have a clientId, look for both client-specific and default (null client) cards
if (params.clientId) {
rateCardWhere.clientId = { in: [params.clientId, null] };
}
// If no clientId, only look at default (null client) cards
// (don't pass clientId filter at all to keep the OR above valid)
const lines = (await db.rateCardLine.findMany({ const lines = (await db.rateCardLine.findMany({
where: { where: { rateCard: buildRateCardWhere(params.clientId, effectiveDate) },
rateCard: rateCardWhere, select: RATE_CARD_LINE_SELECT,
},
select: {
id: true,
rateCardId: true,
roleId: true,
chapter: true,
location: true,
seniority: true,
workType: true,
costRateCents: true,
billRateCents: true,
rateCard: {
select: {
id: true,
name: true,
currency: true,
clientId: true,
},
},
},
})) as RateCardLineRow[]; })) as RateCardLineRow[];
if (lines.length === 0) return null; return findBestMatch(lines, params);
}
// Score each line. Higher = better match.
type ScoredLine = { line: RateCardLineRow; score: number; mismatch: boolean }; /**
const scored: ScoredLine[] = lines.map((line) => { * Batch-optimized rate lookup: loads rate card lines once, then scores
let score = 0; * each param set against the cached lines. Use this when looking up rates
let mismatch = false; * for multiple demand lines sharing the same clientId/effectiveDate.
*/
// Client specificity: client-specific cards get a large bonus export async function lookupRatesBatch(
if (params.clientId && line.rateCard.clientId === params.clientId) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
score += 100; db: any,
} else if (params.clientId && line.rateCard.clientId != null) { clientId: string | null | undefined,
// Different client entirely => disqualify paramsList: RateCardLookupParams[],
mismatch = true; ): Promise<(RateCardLookupResult | null)[]> {
} if (paramsList.length === 0) return [];
// Default card (null client) gets no bonus but is a valid fallback
const effectiveDate = paramsList[0]?.effectiveDate ?? new Date();
// Role match const lines = (await db.rateCardLine.findMany({
if (params.roleId && line.roleId) { where: { rateCard: buildRateCardWhere(clientId, effectiveDate) },
if (line.roleId === params.roleId) score += 16; select: RATE_CARD_LINE_SELECT,
else mismatch = true; })) as RateCardLineRow[];
}
return paramsList.map((params) => findBestMatch(lines, { ...params, clientId }));
// Chapter match
if (params.chapter && line.chapter) {
if (line.chapter === params.chapter) score += 8;
else mismatch = true;
}
// Seniority match
if (params.seniority && line.seniority) {
if (line.seniority === params.seniority) score += 4;
else mismatch = true;
}
// Location match
if (params.location && line.location) {
if (line.location === params.location) score += 2;
else mismatch = true;
}
// Work type match
if (params.workType && line.workType) {
if (line.workType === params.workType) score += 1;
else mismatch = true;
}
return { line, score, mismatch };
});
// Filter out mismatched lines and sort by score descending
const candidates = scored
.filter((s) => !s.mismatch)
.sort((a, b) => b.score - a.score);
const best = candidates[0];
if (!best) return null;
return {
costRateCents: best.line.costRateCents,
billRateCents: best.line.billRateCents ?? 0,
currency: best.line.rateCard.currency,
rateCardId: best.line.rateCard.id,
rateCardLineId: best.line.id,
rateCardName: best.line.rateCard.name,
};
} }
@@ -4,138 +4,149 @@ import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } fro
type ConfigReadmodelsDeps = { type ConfigReadmodelsDeps = {
createManagementLevelCaller: (ctx: TRPCContext) => { createManagementLevelCaller: (ctx: TRPCContext) => {
listGroups: () => Promise<Array<{ listGroups: () => Promise<
id: string; Array<{
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; id: string;
name: string; name: string;
shortCode: string; targetPercentage: number | null;
} | null; levels: Array<{ id: string; name: string }>;
}>>; }>
>;
};
createUtilizationCategoryCaller: (ctx: TRPCContext) => {
list: () => Promise<
Array<{
id: string;
code: string;
name: string;
description: string | null;
_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) => { createEffortRuleCaller: (ctx: TRPCContext) => {
list: () => Promise<Array<{ list: () => Promise<
name: string; Array<{
isDefault: boolean; name: string;
rules: Array<{ isDefault: boolean;
id: string; rules: Array<{
description: string | null; id: string;
scopeType: string; description: string | null;
discipline: string; scopeType: string;
chapter: string | null; discipline: string;
unitMode: string; chapter: string | null;
hoursPerUnit: number; unitMode: string;
sortOrder: number; hoursPerUnit: number;
}>; sortOrder: number;
}>>; }>;
}>
>;
}; };
createExperienceMultiplierCaller: (ctx: TRPCContext) => { createExperienceMultiplierCaller: (ctx: TRPCContext) => {
list: () => Promise<Array<{ list: () => Promise<
name: string; Array<{
isDefault: boolean; name: string;
rules: Array<{ isDefault: boolean;
id: string; rules: Array<{
description: string | null; id: string;
chapter: string | null; description: string | null;
location: string | null; chapter: string | null;
level: string | null; location: string | null;
costMultiplier: number; level: string | null;
billMultiplier: number; costMultiplier: number;
shoringRatio: number | null; billMultiplier: number;
additionalEffortRatio: number | null; shoringRatio: number | null;
sortOrder: number; additionalEffortRatio: number | null;
}>; sortOrder: number;
}>>; }>;
}>
>;
}; };
createScopedCallerContext: (ctx: ToolContext) => TRPCContext; createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
}; };
export const configReadmodelToolDefinitions: ToolDef[] = withToolAccess([ export const configReadmodelToolDefinitions: ToolDef[] = withToolAccess(
[
{
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", list_management_levels: {
function: { requiresPlanningRead: true,
name: "list_management_levels", },
description: "List management level groups and their levels with target percentages.", list_utilization_categories: {
parameters: { type: "object", properties: {} }, requiresPlanningRead: true,
},
list_calculation_rules: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
list_effort_rules: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
list_experience_multipliers: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
}, },
}, },
{ );
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: {} },
},
},
], {
list_management_levels: {
requiresPlanningRead: true,
},
list_utilization_categories: {
requiresPlanningRead: true,
},
list_calculation_rules: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
list_effort_rules: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
list_experience_multipliers: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
});
export function createConfigReadmodelExecutors( export function createConfigReadmodelExecutors(
deps: ConfigReadmodelsDeps, deps: ConfigReadmodelsDeps,
@@ -155,19 +166,13 @@ export function createConfigReadmodelExecutors(
async list_utilization_categories(_params: Record<string, never>, ctx: ToolContext) { async list_utilization_categories(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUtilizationCategoryCaller(deps.createScopedCallerContext(ctx)); const caller = deps.createUtilizationCategoryCaller(deps.createScopedCallerContext(ctx));
const categories = await caller.list(); 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 }) => ({ return categories.map((category) => ({
id: category.id, id: category.id,
code: category.code, code: category.code,
name: category.name, name: category.name,
description: category.description, description: category.description,
projectCount, projectCount: category._count.projects,
})); }));
}, },
@@ -198,41 +203,45 @@ export function createConfigReadmodelExecutors(
async list_effort_rules(_params: Record<string, never>, ctx: ToolContext) { async list_effort_rules(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createEffortRuleCaller(deps.createScopedCallerContext(ctx)); const caller = deps.createEffortRuleCaller(deps.createScopedCallerContext(ctx));
const ruleSets = await caller.list(); const ruleSets = await caller.list();
return ruleSets.flatMap((ruleSet) => ruleSet.rules.map((rule) => ({ return ruleSets.flatMap((ruleSet) =>
id: rule.id, ruleSet.rules.map((rule) => ({
description: rule.description, id: rule.id,
scopeType: rule.scopeType, description: rule.description,
discipline: rule.discipline, scopeType: rule.scopeType,
chapter: rule.chapter, discipline: rule.discipline,
unitMode: rule.unitMode, chapter: rule.chapter,
hoursPerUnit: rule.hoursPerUnit, unitMode: rule.unitMode,
sortOrder: rule.sortOrder, hoursPerUnit: rule.hoursPerUnit,
ruleSet: { sortOrder: rule.sortOrder,
name: ruleSet.name, ruleSet: {
isDefault: ruleSet.isDefault, name: ruleSet.name,
}, isDefault: ruleSet.isDefault,
}))); },
})),
);
}, },
async list_experience_multipliers(_params: Record<string, never>, ctx: ToolContext) { async list_experience_multipliers(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createExperienceMultiplierCaller(deps.createScopedCallerContext(ctx)); const caller = deps.createExperienceMultiplierCaller(deps.createScopedCallerContext(ctx));
const multiplierSets = await caller.list(); const multiplierSets = await caller.list();
return multiplierSets.flatMap((multiplierSet) => multiplierSet.rules.map((rule) => ({ return multiplierSets.flatMap((multiplierSet) =>
id: rule.id, multiplierSet.rules.map((rule) => ({
description: rule.description, id: rule.id,
chapter: rule.chapter, description: rule.description,
location: rule.location, chapter: rule.chapter,
level: rule.level, location: rule.location,
costMultiplier: rule.costMultiplier, level: rule.level,
billMultiplier: rule.billMultiplier, costMultiplier: rule.costMultiplier,
shoringRatio: rule.shoringRatio, billMultiplier: rule.billMultiplier,
additionalEffortRatio: rule.additionalEffortRatio, shoringRatio: rule.shoringRatio,
sortOrder: rule.sortOrder, additionalEffortRatio: rule.additionalEffortRatio,
multiplierSet: { sortOrder: rule.sortOrder,
name: multiplierSet.name, multiplierSet: {
isDefault: multiplierSet.isDefault, name: multiplierSet.name,
}, isDefault: multiplierSet.isDefault,
}))); },
})),
);
}, },
}; };
} }
@@ -1,11 +1,9 @@
import { normalizeEstimateDemandLine, summarizeEstimateDemandLines } from "@capakraken/engine"; import { normalizeEstimateDemandLine, summarizeEstimateDemandLines } from "@capakraken/engine";
import { CreateEstimateSchema } from "@capakraken/shared"; import { CreateEstimateSchema } from "@capakraken/shared";
import { z } from "zod"; import { z } from "zod";
import { lookupRate } from "../lib/rate-card-lookup.js"; import { lookupRatesBatch } from "../lib/rate-card-lookup.js";
function buildComputedMetrics( function buildComputedMetrics(demandLines: z.infer<typeof CreateEstimateSchema>["demandLines"]) {
demandLines: z.infer<typeof CreateEstimateSchema>["demandLines"],
) {
const summary = summarizeEstimateDemandLines(demandLines); const summary = summarizeEstimateDemandLines(demandLines);
return [ return [
@@ -62,7 +60,9 @@ function normalizeDemandLines<
const snapshotsByResourceId = new Map( const snapshotsByResourceId = new Map(
input.resourceSnapshots input.resourceSnapshots
.filter( .filter(
(snapshot): snapshot is (typeof input.resourceSnapshots)[number] & { (
snapshot,
): snapshot is (typeof input.resourceSnapshots)[number] & {
resourceId: string; resourceId: string;
} => typeof snapshot.resourceId === "string" && snapshot.resourceId.length > 0, } => typeof snapshot.resourceId === "string" && snapshot.resourceId.length > 0,
) )
@@ -71,8 +71,7 @@ function normalizeDemandLines<
return input.demandLines.map((line) => return input.demandLines.map((line) =>
normalizeEstimateDemandLine(line, { normalizeEstimateDemandLine(line, {
resourceSnapshot: resourceSnapshot: line.resourceId != null ? snapshotsByResourceId.get(line.resourceId) : null,
line.resourceId != null ? snapshotsByResourceId.get(line.resourceId) : null,
defaultCurrency: baseCurrency, defaultCurrency: baseCurrency,
}), }),
); );
@@ -117,44 +116,60 @@ export async function autoFillDemandLineRates(
clientId = project?.clientId ?? null; clientId = project?.clientId ?? null;
} }
const autoFilledIndices: number[] = []; // Identify which lines need auto-fill and collect their lookup params
const enriched = await Promise.all( const needsLookup: {
demandLines.map(async (line, index) => { index: number;
const isDefaultRate = line.costRateCents === 0 && line.billRateCents === 0; params: { chapter: string | null; roleId: string | null };
const hasExplicitSource = line.rateSource != null && line.rateSource.length > 0; }[] = [];
if (!isDefaultRate || hasExplicitSource) { for (let i = 0; i < demandLines.length; i++) {
return line; const line = demandLines[i]!;
} const isDefaultRate = line.costRateCents === 0 && line.billRateCents === 0;
const hasExplicitSource = line.rateSource != null && line.rateSource.length > 0;
const result = await lookupRate(db, { if (isDefaultRate && !hasExplicitSource) {
clientId, needsLookup.push({
chapter: line.chapter ?? null, index: i,
roleId: line.roleId ?? null, params: { chapter: line.chapter ?? null, roleId: line.roleId ?? null },
}); });
if (!result) { }
return line; }
}
autoFilledIndices.push(index); if (needsLookup.length === 0) {
const existingMetadata = (line.metadata ?? {}) as Record<string, unknown>; return { demandLines, autoFilledIndices: [] };
return { }
...line,
costRateCents: result.costRateCents, // Single DB query for all rate card lines, scored locally per demand line
billRateCents: result.billRateCents, const results = await lookupRatesBatch(
currency: result.currency, db,
rateSource: `rate-card:${result.rateCardId}`, clientId,
metadata: { needsLookup.map((entry) => entry.params),
...existingMetadata,
autoAppliedRateCard: {
rateCardId: result.rateCardId,
rateCardLineId: result.rateCardLineId,
rateCardName: result.rateCardName,
appliedAt: new Date().toISOString(),
},
},
};
}),
); );
const autoFilledIndices: number[] = [];
const enriched = [...demandLines];
for (let i = 0; i < needsLookup.length; i++) {
const result = results[i];
if (!result) continue;
const { index } = needsLookup[i]!;
autoFilledIndices.push(index);
const line = demandLines[index]!;
const existingMetadata = (line.metadata ?? {}) as Record<string, unknown>;
enriched[index] = {
...line,
costRateCents: result.costRateCents,
billRateCents: result.billRateCents,
currency: result.currency,
rateSource: `rate-card:${result.rateCardId}`,
metadata: {
...existingMetadata,
autoAppliedRateCard: {
rateCardId: result.rateCardId,
rateCardLineId: result.rateCardLineId,
rateCardName: result.rateCardName,
appliedAt: new Date().toISOString(),
},
},
};
}
return { demandLines: enriched, autoFilledIndices }; return { demandLines: enriched, autoFilledIndices };
} }
@@ -57,36 +57,57 @@ export async function loadTimelineHolidayOverlaysForReadModel(
}, },
}); });
// Group resources by location key to deduplicate holiday resolution.
// Resources sharing the same (countryId, federalState, metroCityId) get
// identical holidays, so we resolve once per location instead of once per resource.
const locationGroups = new Map<
string,
{ locationResource: (typeof resources)[0]; resourceIds: string[] }
>();
for (const resource of resources) {
const key = `${resource.countryId ?? ""}:${resource.federalState ?? ""}:${resource.metroCityId ?? ""}`;
const existing = locationGroups.get(key);
if (existing) {
existing.resourceIds.push(resource.id);
} else {
locationGroups.set(key, { locationResource: resource, resourceIds: [resource.id] });
}
}
const resolverDb = asHolidayResolverDb(db);
const overlays = await Promise.all( const overlays = await Promise.all(
resources.map(async (resource) => { [...locationGroups.values()].map(async ({ locationResource, resourceIds }) => {
const holidays = await getResolvedCalendarHolidays(asHolidayResolverDb(db), { const holidays = await getResolvedCalendarHolidays(resolverDb, {
periodStart: input.startDate, periodStart: input.startDate,
periodEnd: input.endDate, periodEnd: input.endDate,
countryId: resource.countryId, countryId: locationResource.countryId,
countryCode: resource.country?.code ?? null, countryCode: locationResource.country?.code ?? null,
federalState: resource.federalState, federalState: locationResource.federalState,
metroCityId: resource.metroCityId, metroCityId: locationResource.metroCityId,
metroCityName: resource.metroCity?.name ?? null, metroCityName: locationResource.metroCity?.name ?? null,
}); });
return holidays.map((holiday) => { return resourceIds.flatMap((resourceId) => {
const holidayDate = new Date(`${holiday.date}T00:00:00.000Z`); const resource = resources.find((r) => r.id === resourceId)!;
return { return holidays.map((holiday) => {
id: `calendar-holiday:${resource.id}:${holiday.date}`, const holidayDate = new Date(`${holiday.date}T00:00:00.000Z`);
resourceId: resource.id, return {
type: VacationType.PUBLIC_HOLIDAY, id: `calendar-holiday:${resourceId}:${holiday.date}`,
status: "APPROVED" as const, resourceId,
startDate: holidayDate, type: VacationType.PUBLIC_HOLIDAY,
endDate: holidayDate, status: "APPROVED" as const,
note: holiday.name, startDate: holidayDate,
scope: holiday.scope, endDate: holidayDate,
calendarName: holiday.calendarName, note: holiday.name,
sourceType: holiday.sourceType, scope: holiday.scope,
countryCode: resource.country?.code ?? null, calendarName: holiday.calendarName,
countryName: resource.country?.name ?? null, sourceType: holiday.sourceType,
federalState: resource.federalState ?? null, countryCode: resource.country?.code ?? null,
metroCityName: resource.metroCity?.name ?? null, countryName: resource.country?.name ?? null,
}; federalState: resource.federalState ?? null,
metroCityName: resource.metroCity?.name ?? null,
};
});
}); });
}), }),
); );
@@ -14,9 +14,11 @@ import {
unsetDefaultUtilizationCategory, unsetDefaultUtilizationCategory,
} from "./utilization-category-support.js"; } from "./utilization-category-support.js";
export const UtilizationCategoryListInputSchema = z.object({ export const UtilizationCategoryListInputSchema = z
isActive: z.boolean().optional(), .object({
}).optional(); isActive: z.boolean().optional(),
})
.optional();
export const UtilizationCategoryByIdInputSchema = z.object({ export const UtilizationCategoryByIdInputSchema = z.object({
id: z.string(), id: z.string(),
@@ -38,6 +40,7 @@ export async function listUtilizationCategories(
return ctx.db.utilizationCategory.findMany({ return ctx.db.utilizationCategory.findMany({
where: buildUtilizationCategoryListWhere(input ?? {}), where: buildUtilizationCategoryListWhere(input ?? {}),
orderBy: { sortOrder: "asc" }, orderBy: { sortOrder: "asc" },
include: { _count: { select: { projects: true } } },
}); });
} }