fix: extract tooth number and surface from #NN notation for all DDMA procedures

Added extractToothSurface() helper that parses "#NN [SURFACES]" from any
procedure input (tooth 1-32, surface letters O/M/D/B/L/F/I/V). Applied to
all lookup paths so any procedure with #14 or #29 OB carries toothNumber and
toothSurface through to the DDMA Selenium worker.

Also propagate toothNumber/toothSurface in matchedCodes and chatbot_claim_prefill
so the claim-form prefill sets these fields on the service lines before auto-submit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-06 23:31:22 -04:00
parent 905e236166
commit d02e8c8dcb
3 changed files with 39 additions and 12 deletions

View File

@@ -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<string, string> = {
"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,

View File

@@ -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 })),
},
};
}

View File

@@ -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 };