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:
@@ -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,
|
||||
|
||||
@@ -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 })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user