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
+141 -120
View File
@@ -13,13 +13,13 @@
*/
export interface RateCardLookupParams {
clientId?: string | null;
chapter?: string | null;
roleId?: string | null;
seniority?: string | null;
location?: string | null;
workType?: string | null;
effectiveDate?: Date | null;
clientId?: string | null | undefined;
chapter?: string | null | undefined;
roleId?: string | null | undefined;
seniority?: string | null | undefined;
location?: string | null | undefined;
workType?: string | null | undefined;
effectiveDate?: Date | null | undefined;
}
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.
* Returns null when no active rate card line matches.
@@ -59,120 +168,32 @@ export async function lookupRate(
params: RateCardLookupParams,
): Promise<RateCardLookupResult | null> {
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({
where: {
rateCard: rateCardWhere,
},
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,
},
},
},
where: { rateCard: buildRateCardWhere(params.clientId, effectiveDate) },
select: RATE_CARD_LINE_SELECT,
})) as RateCardLineRow[];
if (lines.length === 0) return null;
// Score each line. Higher = better match.
type ScoredLine = { line: RateCardLineRow; score: number; mismatch: boolean };
const scored: ScoredLine[] = lines.map((line) => {
let score = 0;
let mismatch = false;
// Client specificity: client-specific cards get a large bonus
if (params.clientId && line.rateCard.clientId === params.clientId) {
score += 100;
} else if (params.clientId && line.rateCard.clientId != null) {
// Different client entirely => disqualify
mismatch = true;
}
// Default card (null client) gets no bonus but is a valid fallback
// Role match
if (params.roleId && line.roleId) {
if (line.roleId === params.roleId) score += 16;
else mismatch = true;
}
// 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,
};
return findBestMatch(lines, params);
}
/**
* Batch-optimized rate lookup: loads rate card lines once, then scores
* each param set against the cached lines. Use this when looking up rates
* for multiple demand lines sharing the same clientId/effectiveDate.
*/
export async function lookupRatesBatch(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
db: any,
clientId: string | null | undefined,
paramsList: RateCardLookupParams[],
): Promise<(RateCardLookupResult | null)[]> {
if (paramsList.length === 0) return [];
const effectiveDate = paramsList[0]?.effectiveDate ?? new Date();
const lines = (await db.rateCardLine.findMany({
where: { rateCard: buildRateCardWhere(clientId, effectiveDate) },
select: RATE_CARD_LINE_SELECT,
})) as RateCardLineRow[];
return paramsList.map((params) => findBestMatch(lines, { ...params, clientId }));
}