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
@@ -0,0 +1,78 @@
type MatchKey = "roleId" | "chapter" | "location" | "seniority" | "workType";
type MatchableRateCardLine = Partial<Record<MatchKey, string | null>>;
type RateCardLineMatchCriteria = Partial<Record<MatchKey, string | undefined>>;
type RateCardLineMatchWeights = Partial<Record<MatchKey, number>>;
type RateCardLineMatchResult<TLine> = {
line: TLine;
score: number;
mismatch: boolean;
};
function normalizeRateCardMatchValue(
value: string,
caseInsensitive: boolean,
): string {
return caseInsensitive ? value.toLowerCase() : value;
}
function rateCardMatchValuesEqual(input: {
lineValue: string;
criteriaValue: string;
caseInsensitive: boolean;
}): boolean {
return normalizeRateCardMatchValue(input.lineValue, input.caseInsensitive) ===
normalizeRateCardMatchValue(input.criteriaValue, input.caseInsensitive);
}
export function scoreRateCardLineMatch<TLine extends MatchableRateCardLine>(
line: TLine,
input: {
criteria: RateCardLineMatchCriteria;
weights: RateCardLineMatchWeights;
caseInsensitiveKeys?: readonly MatchKey[];
},
): RateCardLineMatchResult<TLine> {
const caseInsensitiveKeys = new Set(input.caseInsensitiveKeys ?? []);
let score = 0;
let mismatch = false;
for (const [key, criteriaValue] of Object.entries(input.criteria) as Array<[MatchKey, string | undefined]>) {
const lineValue = line[key];
if (!criteriaValue || !lineValue) {
continue;
}
if (rateCardMatchValuesEqual({
lineValue,
criteriaValue,
caseInsensitive: caseInsensitiveKeys.has(key),
})) {
score += input.weights[key] ?? 0;
continue;
}
mismatch = true;
}
return { line, score, mismatch };
}
export function pickBestRateCardLineMatch<
TLine extends MatchableRateCardLine,
>(
lines: TLine[],
input: {
criteria: RateCardLineMatchCriteria;
weights: RateCardLineMatchWeights;
caseInsensitiveKeys?: readonly MatchKey[];
},
): TLine | null {
const candidates = lines
.map((line) => scoreRateCardLineMatch(line, input))
.filter((candidate) => !candidate.mismatch)
.sort((left, right) => right.score - left.score);
return candidates[0]?.line ?? null;
}