/** * Rate card lookup logic for auto-filling demand line rates. * * Match priority (highest specificity wins): * 1. Exact client + chapter + role * 2. Client + chapter (any role) * 3. Client + role (any chapter) * 4. Client only (fallback) * 5. Default rate card (no client) + best match * * Within each priority tier, additional criteria (seniority, location, * workType) increase the score. */ export interface RateCardLookupParams { clientId?: string | null; chapter?: string | null; roleId?: string | null; seniority?: string | null; location?: string | null; workType?: string | null; effectiveDate?: Date | null; } export interface RateCardLookupResult { costRateCents: number; billRateCents: number; currency: string; rateCardId: string; rateCardLineId: string; rateCardName: string; } interface RateCardLineRow { id: string; rateCardId: string; roleId: string | null; chapter: string | null; location: string | null; seniority: string | null; workType: string | null; costRateCents: number; billRateCents: number | null; rateCard: { id: string; name: string; currency: string; clientId: string | null; }; } /** * Look up the best-matching rate card line for a given set of criteria. * Returns null when no active rate card line matches. */ export async function lookupRate( // eslint-disable-next-line @typescript-eslint/no-explicit-any db: any, params: RateCardLookupParams, ): Promise { const effectiveDate = params.effectiveDate ?? new Date(); // Build rate card filter: active cards, within effective date range const rateCardWhere: Record = { 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, }, }, }, })) 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, }; }