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, rateCardId: true, roleId: true, chapter: true, location: true, seniority: true, workType: true, serviceGroup: true, costRateCents: true, billRateCents: true, machineRateCents: true, attributes: true, role: { select: ROLE_BRIEF_SELECT }, createdAt: true, updatedAt: true, } as const; type RateCardBestMatchInput = { clientId?: string | undefined; chapter?: string | undefined; managementLevelId?: string | undefined; roleName?: string | undefined; seniority?: string | undefined; }; type ResolveBestRateInput = { resourceId?: string | undefined; roleName?: string | undefined; date?: Date | undefined; }; type ResolveRateLineCriteria = { roleId?: string | undefined; chapter?: string | undefined; location?: string | undefined; seniority?: string | undefined; 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, OR: [ { effectiveFrom: null }, { effectiveFrom: { lte: effectiveAt } }, ], AND: [ { OR: [ { effectiveTo: null }, { effectiveTo: { gte: effectiveAt } }, ], }, ], }; } export async function lookupBestRateMatch( db: Pick, input: RateCardBestMatchInput, ) { const rateCardWhere: Prisma.RateCardWhereInput = { isActive: true }; if (input.clientId) { rateCardWhere.OR = [ { clientId: input.clientId }, { clientId: null }, ]; } const rateCards = await db.rateCard.findMany({ where: rateCardWhere, include: { lines: { select: { id: true, chapter: true, seniority: true, costRateCents: true, billRateCents: true, role: { select: { id: true, name: true } }, }, }, client: { select: { id: true, name: true } }, }, orderBy: [{ effectiveFrom: "desc" }], }); if (rateCards.length === 0) { return { bestMatch: null, alternatives: [], totalCandidates: 0, message: "No active rate cards found.", }; } let roleId: string | undefined; if (input.roleName) { const role = await db.role.findFirst({ where: { name: { contains: input.roleName, mode: "insensitive" } }, select: { id: true }, }); if (role) { roleId = role.id; } } const scoredLines: Array<{ rateCardName: string; clientId: string | null; clientName: string | null; lineId: string; chapter: string | null; seniority: string | null; roleName: string | null; costRateCents: number; billRateCents: number | null; score: number; }> = []; for (const card of rateCards) { for (const line of card.lines) { 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; } if (!mismatch) { scoredLines.push({ rateCardName: card.name, clientId: card.client?.id ?? null, clientName: card.client?.name ?? null, lineId: line.id, chapter: line.chapter, seniority: line.seniority, roleName: line.role?.name ?? null, costRateCents: line.costRateCents, billRateCents: line.billRateCents ?? null, score, }); } } } scoredLines.sort((left, right) => right.score - left.score); return { bestMatch: scoredLines[0] ?? null, alternatives: scoredLines.slice(1, 4), totalCandidates: scoredLines.length, }; } export async function resolveBestRate( db: Pick, input: ResolveBestRateInput, ) { const effectiveAt = input.date ?? new Date(); if (input.resourceId) { const resource = await findUniqueOrThrow( db.resource.findUnique({ where: { id: input.resourceId }, select: { id: true, displayName: true, chapter: true, areaRole: { select: { name: true } }, }, }), "Resource", ); const resolved = await lookupBestRateMatch(db, { ...(resource.chapter ? { chapter: resource.chapter } : {}), ...(resource.areaRole?.name ? { roleName: resource.areaRole.name } : {}), }); if (resolved.bestMatch) { return { rateCard: resolved.bestMatch.rateCardName, resource: resource.displayName, rate: fmtEur(resolved.bestMatch.costRateCents), rateCents: resolved.bestMatch.costRateCents, matchedBy: resolved.bestMatch.roleName ? `role: ${resolved.bestMatch.roleName}` : "best_match", }; } } if (input.roleName) { const match = await lookupBestRateMatch(db, { roleName: input.roleName }); if (match.bestMatch) { return { rateCard: match.bestMatch.rateCardName, rate: fmtEur(match.bestMatch.costRateCents), rateCents: match.bestMatch.costRateCents, matchedBy: match.bestMatch.roleName ? `role: ${match.bestMatch.roleName}` : "best_match", alternatives: match.alternatives.map((alternative) => ({ rateCard: alternative.rateCardName, role: alternative.roleName, chapter: alternative.chapter, seniority: alternative.seniority, costRate: fmtEur(alternative.costRateCents), billRate: alternative.billRateCents != null ? fmtEur(alternative.billRateCents) : null, })), }; } if (match.totalCandidates === 0) { return { error: "No matching rate card line found." }; } } const cards = await db.rateCard.findMany({ where: buildEffectiveRateCardWhere(effectiveAt), include: { _count: { select: { lines: true } }, client: { select: { id: true, name: true, code: true } }, }, orderBy: [{ isActive: "desc" }, { effectiveFrom: "desc" }, { name: "asc" }], }); const card = cards[0]; if (!card) { return { error: "No active rate card found for the given date." }; } const detail = await findUniqueOrThrow( db.rateCard.findUnique({ where: { id: card.id }, include: { client: { select: { id: true, name: true, code: true } }, lines: { select: rateCardLineSelect, orderBy: [{ chapter: "asc" }, { seniority: "asc" }, { createdAt: "asc" }], }, }, }), "Rate card", ); return { rateCard: detail.name, lines: detail.lines.map((line) => ({ role: line.role?.name ?? null, seniority: line.seniority, chapter: line.chapter, location: line.location, costRate: fmtEur(line.costRateCents), billRate: line.billRateCents != null ? fmtEur(line.billRateCents) : null, })), }; } export function resolveBestRateLineMatch< TLine extends { roleId: string | null; chapter: string | null; location: string | null; seniority: string | null; workType: string | null; }, >(lines: TLine[], criteria: ResolveRateLineCriteria): TLine | null { return pickBestRateCardLineMatch(lines, { criteria, weights: resolveBestRateLineWeights, }); }