diff --git a/apps/Backend/src/ai/cdt-lookup.ts b/apps/Backend/src/ai/cdt-lookup.ts index bd66d3fc..308b873a 100644 --- a/apps/Backend/src/ai/cdt-lookup.ts +++ b/apps/Backend/src/ai/cdt-lookup.ts @@ -214,6 +214,14 @@ const DIRECT_CODE_MAP: Record = { "rct primary molar": "D3240", "baby tooth rct": "D3230", // Extractions + "ext": "D7140", + "exts": "D7140", + "extraction": "D7140", + "extractions": "D7140", + "simple ext": "D7140", + "simple exts": "D7140", + "simple extraction": "D7140", + "simple extractions": "D7140", "baby tooth ext": "D7111", "baby tooth extraction": "D7111", "primary tooth ext": "D7111", @@ -631,6 +639,24 @@ function expandCompositeLists(procedureNames: string[]): string[] { return out; } +/** + * Clean a user-entered alias code down to the bare CDT code: + * "D7140 #25" → "D7140", "7140" → "D7140". Returns null if there is no code. + */ +export function normalizeAliasCode(code: string): string | null { + const m = code.trim().match(/^d?\s*(\d{4})\b/i); + return m ? `D${m[1]}` : null; +} + +/** Drop tooth numbers from an alias phrase: "simple ext #25" → "simple ext". */ +export function normalizeAliasPhrase(phrase: string): string { + return phrase + .toLowerCase() + .replace(/,?\s*#\s*(\d{1,2}|[a-t])\b/gi, "") + .replace(/\s+/g, " ") + .trim(); +} + // Explicit code + tooth list with no surfaces, e.g. "D6010 #5, 9 , 11, 12" or "D6057 #5 #9 and 11" const CODE_TOOTH_LIST_RE = /\b(D\d{4})\s*#\s*(\d{1,2}(?:\s*(?:,|&|\band\b)?\s*#?\s*\d{1,2})*)\b(?!\s*[OMDBLFIV]{1,5}\b)/gi; @@ -664,7 +690,8 @@ export function lookupCdtCodes( // Build a lookup map from custom aliases (phrase → cdtCode), lowercased const customMap: Record = {}; for (const { phrase, cdtCode } of customAliases) { - if (phrase && cdtCode) customMap[phrase.toLowerCase().trim()] = cdtCode.toUpperCase().trim(); + const code = cdtCode ? normalizeAliasCode(cdtCode) : null; + if (phrase && code) customMap[phrase.toLowerCase().trim()] = code; } // Pontics with no stated material — re-coded below to match the crowns @@ -693,7 +720,9 @@ export function lookupCdtCodes( .replace(/,?\s*#\s*[a-t]\b/gi, "") .replace(/\(\s*\)/g, "") .replace(/\s+/g, " ") - .trim(); + .trim() + // dangling connector left after removing the tooth, e.g. "simple ext on #25" + .replace(/\s+(on|of|for|at)$/, ""); // 1. Custom alias exact match (highest priority) const customKey = customMap[cleaned] ? cleaned : customMap[strippedCleaned] ? strippedCleaned : null; diff --git a/apps/Backend/src/routes/ai-settings.ts b/apps/Backend/src/routes/ai-settings.ts index 95f6db38..20c0257f 100644 --- a/apps/Backend/src/routes/ai-settings.ts +++ b/apps/Backend/src/routes/ai-settings.ts @@ -7,6 +7,7 @@ import { storage } from "../storage"; import { classifyInternalChat } from "../ai/internal-chat-graph"; import { runInternalChatWorkflow, createAppointmentToday } from "../ai/internal-chat-workflow"; import { resolveAiProvider, getLlm } from "../ai/llm-factory"; +import { normalizeAliasCode, normalizeAliasPhrase } from "../ai/cdt-lookup"; import { backupScreenshots } from "../utils/screenshotBackup"; const eligibilityImageUpload = multer({ storage: multer.memoryStorage() }); @@ -299,9 +300,10 @@ router.put("/cdt-aliases", async (req: Request, res: Response): Promise => const cleaned = aliases .filter((a: any) => typeof a?.phrase === "string" && typeof a?.cdtCode === "string") .map((a: any) => ({ - phrase: a.phrase.trim().toLowerCase(), - cdtCode: a.cdtCode.trim().toUpperCase(), - })); + phrase: normalizeAliasPhrase(a.phrase), + cdtCode: normalizeAliasCode(a.cdtCode), + })) + .filter((a): a is { phrase: string; cdtCode: string } => !!a.phrase && !!a.cdtCode); await storage.saveCdtAliases(userId, cleaned); return res.status(200).json(cleaned); } catch (err) { @@ -319,7 +321,11 @@ router.post("/cdt-aliases/add", async (req: Request, res: Response): Promise a.phrase !== newEntry.phrase), newEntry]; await storage.saveCdtAliases(userId, updated); return res.status(200).json(newEntry); diff --git a/apps/SeleniumService/selenium_TuftsSCO_claimSubmitWorker.py b/apps/SeleniumService/selenium_TuftsSCO_claimSubmitWorker.py index 70a93a9f..543fff2f 100644 --- a/apps/SeleniumService/selenium_TuftsSCO_claimSubmitWorker.py +++ b/apps/SeleniumService/selenium_TuftsSCO_claimSubmitWorker.py @@ -245,6 +245,93 @@ class AutomationTuftsSCOClaimSubmit: # ── Step 1: Search patient (mirrors DentaQuest eligibility step1) ────────── + def _select_location(self, tag): + """Open the Location dropdown on member search and pick the first option.""" + btn_xpath = '//button[@data-testid="member-search_location_select-btn"]' + option_xpaths = [ + "//*[@role='listbox']//*[@role='option']", + "//li[@role='option']", + "//*[@role='option']", + "//*[@role='listbox']//li", + "//*[@role='menu']//*[@role='menuitem' or @role='menuitemradio']", + ] + + def js_click(el): + self.driver.execute_script("arguments[0].click();", el) + + for attempt in range(3): + try: + trigger = WebDriverWait(self.driver, 15).until( + EC.element_to_be_clickable((By.XPATH, btn_xpath)) + ) + except TimeoutException: + print(f"[{tag}] Warning: Location dropdown button not found") + return False + + before = (trigger.text or "").strip() + try: + self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", trigger) + if attempt == 0: + trigger.click() + else: + js_click(trigger) + except Exception: + js_click(trigger) + print(f"[{tag}] Clicked location dropdown (attempt {attempt + 1})") + + # Wait for any visible option + option = None + deadline = time.time() + 5 + while option is None and time.time() < deadline: + for xp in option_xpaths: + visible = [e for e in self.driver.find_elements(By.XPATH, xp) if e.is_displayed()] + if visible: + option = visible[0] + break + if option is None: + time.sleep(0.3) + + if option is not None: + opt_text = option.get_attribute("aria-label") or option.text.strip() + try: + option.click() + except Exception: + js_click(option) + time.sleep(0.5) + print(f"[{tag}] Selected location: {opt_text[:60]}") + return True + + # Keyboard fallback: open/highlight the first option and confirm + try: + trigger.send_keys(Keys.ARROW_DOWN) + time.sleep(0.3) + ActionChains(self.driver).send_keys(Keys.ENTER).perform() + time.sleep(0.5) + after = (self.driver.find_element(By.XPATH, btn_xpath).text or "").strip() + if after and after != before: + print(f"[{tag}] Selected location via keyboard: {after[:60]}") + return True + except Exception: + pass + + try: + ActionChains(self.driver).send_keys(Keys.ESCAPE).perform() + except Exception: + pass + time.sleep(1) + + # Dump dropdown markup so selector changes on the portal can be diagnosed + try: + html = self.driver.execute_script( + "var lb=document.querySelector('[role=listbox]');" + "var b=document.querySelector('[data-testid=\"member-search_location_select-btn\"]');" + "return (lb||(b&&b.parentElement.parentElement)||{}).outerHTML||'';" + ) + print(f"[{tag}] Location dropdown HTML: {html[:2000]}") + except Exception: + pass + return False + def step1_search_patient(self): """Navigate to member search and find the patient by Member ID + DOB.""" wait = WebDriverWait(self.driver, 30) @@ -276,25 +363,8 @@ class AutomationTuftsSCOClaimSubmit: return f"ERROR: step1 DOB parse failed: {e}" # 1. Select Location from dropdown (required field on DentaQuest portal) - try: - trigger = WebDriverWait(self.driver, 5).until( - EC.element_to_be_clickable((By.XPATH, - '//button[@data-testid="member-search_location_select-btn"]')) - ) - trigger.click() - print("[TuftsSCO Claim step1] Clicked location dropdown") - time.sleep(0.5) - first_option = WebDriverWait(self.driver, 5).until( - EC.element_to_be_clickable((By.XPATH, "(//li[@role='option'])[1]")) - ) - opt_text = first_option.get_attribute("aria-label") or first_option.text.strip() - first_option.click() - print(f"[TuftsSCO Claim step1] Selected location: {opt_text[:60]}") - time.sleep(0.3) - except TimeoutException: - print("[TuftsSCO Claim step1] Warning: Location dropdown not found (continuing)") - except Exception as e: - print(f"[TuftsSCO Claim step1] Warning: Location select failed: {e}") + if not self._select_location("TuftsSCO Claim step1"): + print("[TuftsSCO Claim step1] Warning: Location not selected (continuing)") # 2. Fill DOB try: diff --git a/apps/SeleniumService/selenium_TuftsSCO_preAuthWorker.py b/apps/SeleniumService/selenium_TuftsSCO_preAuthWorker.py index ac244ed0..fa4bb1d8 100644 --- a/apps/SeleniumService/selenium_TuftsSCO_preAuthWorker.py +++ b/apps/SeleniumService/selenium_TuftsSCO_preAuthWorker.py @@ -250,6 +250,93 @@ class AutomationTuftsSCOPreAuth: # ── Step 1: Search patient ───────────────────────────────────────────────── + def _select_location(self, tag): + """Open the Location dropdown on member search and pick the first option.""" + btn_xpath = '//button[@data-testid="member-search_location_select-btn"]' + option_xpaths = [ + "//*[@role='listbox']//*[@role='option']", + "//li[@role='option']", + "//*[@role='option']", + "//*[@role='listbox']//li", + "//*[@role='menu']//*[@role='menuitem' or @role='menuitemradio']", + ] + + def js_click(el): + self.driver.execute_script("arguments[0].click();", el) + + for attempt in range(3): + try: + trigger = WebDriverWait(self.driver, 15).until( + EC.element_to_be_clickable((By.XPATH, btn_xpath)) + ) + except TimeoutException: + print(f"[{tag}] Warning: Location dropdown button not found") + return False + + before = (trigger.text or "").strip() + try: + self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", trigger) + if attempt == 0: + trigger.click() + else: + js_click(trigger) + except Exception: + js_click(trigger) + print(f"[{tag}] Clicked location dropdown (attempt {attempt + 1})") + + # Wait for any visible option + option = None + deadline = time.time() + 5 + while option is None and time.time() < deadline: + for xp in option_xpaths: + visible = [e for e in self.driver.find_elements(By.XPATH, xp) if e.is_displayed()] + if visible: + option = visible[0] + break + if option is None: + time.sleep(0.3) + + if option is not None: + opt_text = option.get_attribute("aria-label") or option.text.strip() + try: + option.click() + except Exception: + js_click(option) + time.sleep(0.5) + print(f"[{tag}] Selected location: {opt_text[:60]}") + return True + + # Keyboard fallback: open/highlight the first option and confirm + try: + trigger.send_keys(Keys.ARROW_DOWN) + time.sleep(0.3) + ActionChains(self.driver).send_keys(Keys.ENTER).perform() + time.sleep(0.5) + after = (self.driver.find_element(By.XPATH, btn_xpath).text or "").strip() + if after and after != before: + print(f"[{tag}] Selected location via keyboard: {after[:60]}") + return True + except Exception: + pass + + try: + ActionChains(self.driver).send_keys(Keys.ESCAPE).perform() + except Exception: + pass + time.sleep(1) + + # Dump dropdown markup so selector changes on the portal can be diagnosed + try: + html = self.driver.execute_script( + "var lb=document.querySelector('[role=listbox]');" + "var b=document.querySelector('[data-testid=\"member-search_location_select-btn\"]');" + "return (lb||(b&&b.parentElement.parentElement)||{}).outerHTML||'';" + ) + print(f"[{tag}] Location dropdown HTML: {html[:2000]}") + except Exception: + pass + return False + def step1_search_patient(self): """Navigate to member search and find the patient by Member ID + DOB.""" wait = WebDriverWait(self.driver, 30) @@ -280,25 +367,8 @@ class AutomationTuftsSCOPreAuth: return f"ERROR: step1 DOB parse failed: {e}" # Select Location from dropdown - try: - trigger = WebDriverWait(self.driver, 5).until( - EC.element_to_be_clickable((By.XPATH, - '//button[@data-testid="member-search_location_select-btn"]')) - ) - trigger.click() - print("[TuftsSCO PreAuth step1] Clicked location dropdown") - time.sleep(0.5) - first_option = WebDriverWait(self.driver, 5).until( - EC.element_to_be_clickable((By.XPATH, "(//li[@role='option'])[1]")) - ) - opt_text = first_option.get_attribute("aria-label") or first_option.text.strip() - first_option.click() - print(f"[TuftsSCO PreAuth step1] Selected location: {opt_text[:60]}") - time.sleep(0.3) - except TimeoutException: - print("[TuftsSCO PreAuth step1] Warning: Location dropdown not found (continuing)") - except Exception as e: - print(f"[TuftsSCO PreAuth step1] Warning: Location select failed: {e}") + if not self._select_location("TuftsSCO PreAuth step1"): + print("[TuftsSCO PreAuth step1] Warning: Location not selected (continuing)") # Fill DOB try: