diff --git a/apps/Backend/src/ai/cdt-lookup.ts b/apps/Backend/src/ai/cdt-lookup.ts index e30f269a..1dda2a43 100644 --- a/apps/Backend/src/ai/cdt-lookup.ts +++ b/apps/Backend/src/ai/cdt-lookup.ts @@ -224,15 +224,22 @@ const DIRECT_CODE_MAP: Record = { * e.g. "extraction #14" → { toothNumber: "14" } */ function extractToothSurface(input: string): { toothNumber?: string; toothSurface?: string } { + // Numeric tooth #1-32 (e.g. "#29 OB") const m = input.match(/#(\d{1,2})(?:\s+([A-Za-z]{1,5}))?/); - if (!m) return {}; - const toothNum = parseInt(m[1]!, 10); - if (toothNum < 1 || toothNum > 32) return {}; - const surfaces = (m[2] ?? "").toUpperCase(); - return { - toothNumber: String(toothNum), - ...(surfaces && /^[OMDBLFIV]+$/.test(surfaces) ? { toothSurface: surfaces } : {}), - }; + if (m) { + const toothNum = parseInt(m[1]!, 10); + if (toothNum >= 1 && toothNum <= 32) { + const surfaces = (m[2] ?? "").toUpperCase(); + return { + toothNumber: String(toothNum), + ...(surfaces && /^[OMDBLFIV]+$/.test(surfaces) ? { toothSurface: surfaces } : {}), + }; + } + } + // Primary/baby tooth letter A-T (e.g. "# H", "#H") + const pm = input.match(/#\s*([A-Ta-t])\b/); + if (pm) return { toothNumber: pm[1]!.toUpperCase() }; + return {}; } // Composite filling tooth classification @@ -336,14 +343,13 @@ function score(queryTokens: string[], entry: { tokens: Set; description: function matchOne(input: string): CdtMatch | null { const cleaned = input.trim().toLowerCase(); - // Direct CDT code pass-through (e.g. "D0120") + // Direct CDT code pass-through (e.g. "D0120", "D7111") if (/^d\d{4}$/i.test(cleaned)) { const row = ALL_CODES.find( (r) => r["Procedure Code"].toLowerCase() === cleaned.toLowerCase() ); - return row - ? { code: row["Procedure Code"], description: row.Description, input } - : null; + const code = row?.["Procedure Code"] ?? cleaned.toUpperCase(); + return { code, description: row?.Description ?? code, input }; } // Apply alias before tokenizing @@ -390,9 +396,13 @@ export function lookupCdtCodes( const cleaned = name.trim().toLowerCase(); // Extract tooth# from "#NN" notation — applies to any procedure const toothInfo = extractToothSurface(name); - // Version of cleaned with tooth notation stripped, for alias matching - // e.g. "1 pa, #3" → "1 pa" so it hits the "1 pa" alias - const strippedCleaned = cleaned.replace(/,?\s*#\d{1,2}\b/g, "").replace(/\s+/g, " ").trim(); + // Strip tooth notation for alias matching: + // numeric (#29), numeric with space (# 3), primary letter (#H, # H) + const strippedCleaned = cleaned + .replace(/,?\s*#\s*\d{1,2}\b/g, "") + .replace(/,?\s*#\s*[a-t]\b/gi, "") + .replace(/\s+/g, " ") + .trim(); // 1. Custom alias exact match (highest priority) const customKey = customMap[cleaned] ? cleaned : customMap[strippedCleaned] ? strippedCleaned : null;