feat: AI chatbot preauth intent, UnitedDH pre-auth improvements

- Add preauth intent to AI chatbot (classifier, workflow, frontend UI card)
- Auto-prefill preauth form with CDT codes, service date, and mapped prices
- Auto-trigger preauth Selenium handler by insurance siteKey (MH/Tufts/CCA/UnitedDH)
- Default tentative service date to today+3 for preauth (user didn't pick a date)
- Fix #number always means tooth number, distributes to all procedures in comma list
- Fix bare "post"/"pos" → D2954 (was matching D2955 via keyword scorer)
- UnitedDH pre-auth: fill procedure date with Ctrl+A to overwrite prefilled value
- UnitedDH pre-auth: select Location "Summit Dental Care" in step1 (same as billing entity)
- UnitedDH pre-auth: remove page refresh in step9 to preserve pre-auth number
- UnitedDH pre-auth: wait for table rows before extracting pre-auth number
- UnitedDH pre-auth/claim: explicit wait for Submit button after file upload (no sleep)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-17 01:21:51 -04:00
parent 43340ab39d
commit 8e011c5a29
8 changed files with 311 additions and 19 deletions

View File

@@ -167,6 +167,8 @@ const DIRECT_CODE_MAP: Record<string, string> = {
// Core / post
"core bu": "D2950",
"p/c": "D2954",
"post": "D2954",
"pos": "D2954",
"post core": "D2954",
// Crowns
"recement": "D2920",

View File

@@ -9,6 +9,7 @@ export type InternalChatIntent =
| "find_patient" // look up patient record only
| "schedule_appointment" // add patient to today's (or specified) schedule
| "claim_only" // submit claim for procedures (no eligibility check)
| "preauth" // submit pre-authorization for procedures
| "navigate_claims"
| "navigate_schedule"
| "navigate_eligibility"
@@ -83,6 +84,11 @@ Intents:
e.g. "claim perio exam, 2BW for John Smith"
Use this when no eligibility check is requested — just billing/claiming services
Always extract appointmentDate when a date or "today" is mentioned
- preauth : submit a pre-authorization request for procedures
e.g. "preauth rct, post, crown for John Smith"
e.g. "pre auth #20 rct, post, crown for Zhiyuan Chen"
e.g. "pre-auth D3320, D2952, D2740 for Maria"
Use this when the user says "preauth", "pre auth", "pre-auth", or "prior auth"
- navigate_claims : open the claims page
- navigate_schedule : open the appointments/schedule page
- navigate_eligibility : open the insurance eligibility page
@@ -99,8 +105,12 @@ Rules:
Only include actual clinical procedures in procedureNames.
- For composite fillings with a tooth number, preserve the EXACT notation including tooth# and surfaces:
e.g. "composite #29 O", "#8 MO", "composite #11 MOD" — keep the #number and surface letters together as one entry
- #number always means a TOOTH number (never a case or pre-auth reference). When a single #number appears before a comma-separated list of procedures, apply it to EVERY procedure in the list.
e.g. "#20 rct, post, crown" → ["#20 rct", "#20 post", "#20 crown"]
e.g. "preauth #20 rct, pos, crown" → ["#20 rct", "#20 pos", "#20 crown"]
e.g. "#14 rct, buildup, crown" → ["#14 rct", "#14 buildup", "#14 crown"]
- For RCT/root canal with a tooth number, preserve the tooth# in the entry:
e.g. "rct #29", "#14 root canal", "rct #6" — keep the #number with the procedure so the correct code can be selected
e.g. "rct #29", "#14 root canal", "rct #6", "#20 rct" — keep the #number with the procedure so the correct code can be selected
- For SRP with a quadrant abbreviation (UL, UR, LL, LR), keep the code and quadrant together as one entry:
e.g. "D4341 UL", "4341 LR", "D4342 UR" — the quadrant always travels with the SRP code
- For multiple PA X-rays with tooth numbers, expand each PA into its own entry:

View File

@@ -49,6 +49,7 @@ export interface ChatResponse {
| "need_insurance_clarification"
| "appointment_created"
| "claim_only_ready"
| "preauth_ready"
| "need_appointment_selection"
| "need_cdt_clarification";
actionData?: Record<string, any>;
@@ -325,6 +326,12 @@ export async function runInternalChatWorkflow(
return await handleClaimOnly(classification, storage, customAliases);
}
// ── Pre-authorization ──────────────────────────────────────────────────────
if (intent === "preauth") {
return await handlePreauth(classification, storage, customAliases);
}
// ── Schedule appointment ───────────────────────────────────────────────────
if (intent === "schedule_appointment") {
@@ -692,6 +699,70 @@ async function handleClaimOnly(
};
}
// ─── preauth ──────────────────────────────────────────────────────────────────
async function handlePreauth(
c: ChatClassification,
storage: StorageLike,
customAliases: { phrase: string; cdtCode: string }[]
): Promise<ChatResponse> {
let patient: ResolvedPatient | null = null;
if (c.memberId?.trim()) {
const existing = await findPatientByMemberId(c.memberId.trim(), c.dob, storage);
if (existing) patient = patientToResult(existing);
} else if (c.patientName?.trim()) {
const raw = await findPatientByName(c.patientName.trim(), storage);
if (raw) patient = patientToResult(raw);
}
if (!patient) {
return { reply: "Please include a patient name or Member ID so I can look them up." };
}
const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
if (procedureNames.length === 0) {
return { reply: "Please specify which procedures to pre-authorize (e.g. rct, post, crown)." };
}
const cdtResults = lookupCdtCodes(procedureNames, customAliases);
const matched = cdtResults.filter((r) => r.code !== null);
const unmatched = cdtResults.filter((r) => r.code === null);
if (unmatched.length > 0) {
const phrases = unmatched.map((r) => r.input);
return {
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D3320)`,
action: "need_cdt_clarification",
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
};
}
// Use explicit date from message; otherwise today+3 (pre-auth is for a future appointment)
let serviceDate: string = c.appointmentDate ?? (() => {
const d = new Date();
d.setDate(d.getDate() + 3);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
})();
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
const [sy, sm, sd] = serviceDate.split("-");
const dateLabel = `${sm}/${sd}/${sy}`;
return {
reply: `Opening pre-auth for ${fullName} (tentative date ${dateLabel}): ${matched.map((r) => `${r.code} (${r.description})`).join(", ")}.`,
action: "preauth_ready",
actionData: {
patient,
siteKey,
serviceDate,
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
renderingProvider: c.renderingProvider ?? null,
},
};
}
// ─── schedule_appointment ─────────────────────────────────────────────────────
const DEFAULT_STAFF_ID = 1; // Column A