diff --git a/apps/Backend/src/ai/cdt-lookup.ts b/apps/Backend/src/ai/cdt-lookup.ts index 58602a7d..095442e3 100644 --- a/apps/Backend/src/ai/cdt-lookup.ts +++ b/apps/Backend/src/ai/cdt-lookup.ts @@ -17,6 +17,8 @@ interface CdtMatch { code: string; description: string; input: string; + toothNumber?: string; + toothSurface?: string; } // Load once at module init @@ -215,6 +217,23 @@ const DIRECT_CODE_MAP: Record = { "implant crown": "D6058", }; +/** + * Extract tooth number and surface from any input containing "#NN" (1-32). + * e.g. "#29 OB" → { toothNumber: "29", toothSurface: "OB" } + * e.g. "extraction #14" → { toothNumber: "14" } + */ +function extractToothSurface(input: string): { toothNumber?: string; toothSurface?: string } { + 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 } : {}), + }; +} + // Composite filling tooth classification const FRONT_TEETH = new Set([6, 7, 8, 9, 10, 11, 22, 23, 24, 25, 26, 27]); const BACK_TEETH = new Set([1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 28, 29, 30, 31, 32]); @@ -249,7 +268,7 @@ function parseCompositeCode(input: string): CdtMatch | null { } const row = ALL_CODES.find((r) => r["Procedure Code"] === code); - return { code, description: row?.Description ?? code, input }; + return { code, description: row?.Description ?? code, input, toothNumber: String(toothNum), toothSurface: surfaces.toUpperCase() }; } /** @@ -326,28 +345,30 @@ export function lookupCdtCodes( return procedureNames.map((name) => { const cleaned = name.trim().toLowerCase(); + // Extract tooth# from "#NN" notation — applies to any procedure + const toothInfo = extractToothSurface(name); // 1. Custom alias exact match (highest priority) if (customMap[cleaned]) { const code = customMap[cleaned]!; const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code); - return { code, description: row?.Description ?? code, input: name }; + return { code, description: row?.Description ?? code, input: name, ...toothInfo }; } - // 2. Composite filling by tooth# and surfaces (e.g. "#29 OB", "composite #8 MO") + // 2. Composite filling by tooth# and surfaces — also determines CDT code from tooth position const compositeMatch = parseCompositeCode(name); - if (compositeMatch) return compositeMatch; + if (compositeMatch) return compositeMatch; // already carries toothNumber/toothSurface // 3. Hardcoded direct code map (short abbreviations / codes not in MH schedule) if (DIRECT_CODE_MAP[cleaned]) { const code = DIRECT_CODE_MAP[cleaned]!; const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code); - return { code, description: row?.Description ?? code, input: name }; + return { code, description: row?.Description ?? code, input: name, ...toothInfo }; } - // 3. Hardcoded alias + keyword search + // 4. Hardcoded alias + keyword search const match = matchOne(name); - if (match) return match; + if (match) return { ...match, ...toothInfo }; return { code: null, diff --git a/apps/Backend/src/ai/internal-chat-workflow.ts b/apps/Backend/src/ai/internal-chat-workflow.ts index e5b9d354..323bd2a3 100644 --- a/apps/Backend/src/ai/internal-chat-workflow.ts +++ b/apps/Backend/src/ai/internal-chat-workflow.ts @@ -436,7 +436,7 @@ async function handleCheckAndClaim( siteKey, autoCheck: siteKeyToAutoCheck(siteKey), cdtResults, - matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description })), + matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface })), }, }; } @@ -518,7 +518,7 @@ async function handleClaimOnly( actionData: { patient, siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH", - matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description })), + matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface })), options: [ { label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: isoUTC(sorted[0]) }, { label: fmtUTC(sorted[1]), appointmentId: sorted[1].id, serviceDate: isoUTC(sorted[1]) }, @@ -561,7 +561,7 @@ async function handleClaimOnly( siteKey, serviceDate, appointmentId, - matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description })), + matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface })), }, }; } diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 74f67252..e9c196a1 100755 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -496,7 +496,7 @@ export function ClaimForm({ if (!raw) return; try { const { codes, serviceDate } = JSON.parse(raw) as { - codes: { code: string; description: string }[]; + codes: { code: string; description: string; toothNumber?: string; toothSurface?: string }[]; serviceDate?: string; }; sessionStorage.removeItem("chatbot_claim_prefill"); @@ -515,7 +515,13 @@ export function ClaimForm({ const updatedLines = [...prev.serviceLines]; codes.forEach((c, i) => { if (i < updatedLines.length) { - updatedLines[i] = { ...updatedLines[i]!, procedureCode: c.code, procedureDate: date }; + updatedLines[i] = { + ...updatedLines[i]!, + procedureCode: c.code, + procedureDate: date, + ...(c.toothNumber ? { toothNumber: c.toothNumber } : {}), + ...(c.toothSurface ? { toothSurface: c.toothSurface } : {}), + }; } }); return { ...prev, serviceLines: updatedLines };