refactor(api): extract rate card match support

This commit is contained in:
2026-03-31 14:33:38 +02:00
parent aeffb2a069
commit b093a47c1b
3 changed files with 195 additions and 71 deletions
+41 -71
View File
@@ -2,6 +2,10 @@ import type { Prisma } from "@capakraken/db";
import { ROLE_BRIEF_SELECT } from "../db/selects.js";
import { findUniqueOrThrow } from "../db/helpers.js";
import { fmtEur } from "../lib/format-utils.js";
import {
pickBestRateCardLineMatch,
scoreRateCardLineMatch,
} from "./rate-card-match-support.js";
export const rateCardLineSelect = {
id: true,
@@ -43,6 +47,25 @@ type ResolveRateLineCriteria = {
workType?: string | undefined;
};
const lookupBestRateMatchWeights = {
roleId: 4,
chapter: 2,
seniority: 1,
} as const;
const lookupBestRateCaseInsensitiveKeys = [
"chapter",
"seniority",
] as const;
const resolveBestRateLineWeights = {
roleId: 4,
chapter: 2,
location: 1,
seniority: 1,
workType: 1,
} as const;
function buildEffectiveRateCardWhere(effectiveAt: Date): Prisma.RateCardWhereInput {
return {
isActive: true,
@@ -126,30 +149,21 @@ export async function lookupBestRateMatch(
for (const card of rateCards) {
for (const line of card.lines) {
let score = 0;
let mismatch = false;
if (roleId && line.role) {
if (line.role.id === roleId) {
score += 4;
} else {
mismatch = true;
}
}
if (input.chapter && line.chapter) {
if (line.chapter.toLowerCase() === input.chapter.toLowerCase()) {
score += 2;
} else {
mismatch = true;
}
}
if (input.seniority && line.seniority) {
if (line.seniority.toLowerCase() === input.seniority.toLowerCase()) {
score += 1;
} else {
mismatch = true;
}
}
const scored = scoreRateCardLineMatch({
roleId: line.role?.id ?? null,
chapter: line.chapter,
seniority: line.seniority,
}, {
criteria: {
roleId,
chapter: input.chapter,
seniority: input.seniority,
},
weights: lookupBestRateMatchWeights,
caseInsensitiveKeys: lookupBestRateCaseInsensitiveKeys,
});
let score = scored.score;
const mismatch = scored.mismatch;
if (input.clientId && card.client?.id === input.clientId) {
score += 3;
}
@@ -288,52 +302,8 @@ export function resolveBestRateLineMatch<
workType: string | null;
},
>(lines: TLine[], criteria: ResolveRateLineCriteria): TLine | null {
const scored = lines.map((line) => {
let score = 0;
let mismatch = false;
if (criteria.roleId && line.roleId) {
if (line.roleId === criteria.roleId) {
score += 4;
} else {
mismatch = true;
}
}
if (criteria.chapter && line.chapter) {
if (line.chapter === criteria.chapter) {
score += 2;
} else {
mismatch = true;
}
}
if (criteria.location && line.location) {
if (line.location === criteria.location) {
score += 1;
} else {
mismatch = true;
}
}
if (criteria.seniority && line.seniority) {
if (line.seniority === criteria.seniority) {
score += 1;
} else {
mismatch = true;
}
}
if (criteria.workType && line.workType) {
if (line.workType === criteria.workType) {
score += 1;
} else {
mismatch = true;
}
}
return { line, score, mismatch };
return pickBestRateCardLineMatch(lines, {
criteria,
weights: resolveBestRateLineWeights,
});
const candidates = scored
.filter((candidate) => !candidate.mismatch)
.sort((left, right) => right.score - left.score);
return candidates[0]?.line ?? null;
}