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