From dc039741cad545ebdac9df2e852f657a07bae054 Mon Sep 17 00:00:00 2001 From: ff Date: Mon, 15 Jun 2026 23:54:05 -0400 Subject: [PATCH] fix: Tufts SCO + UnitedDH pre-auth file upload, tooth fill, PDF and pre-auth number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Frontend: upload attachments to disk before sending pre-auth payload (same pattern as claims) - cloud-storage: finalizeFileUpload returns diskPath so Python workers get real file paths - upload-to-cloud route: return diskPath instead of API URL - TuftsSCO preAuth worker: skip 'Add a file' button click; send_keys directly to hidden react-aria-Input - TuftsSCO preAuth worker: JS focus() on tooth field to bypass warning-banner overlay - TuftsSCO preAuth worker: 1.5s wait after procedure code for layout shift to settle - TuftsSCO preAuth worker: step8 waits for 'thank' in page_source then extracts via 'submitted pre-authorization' regex - helpers_tuftssco_preauth: convert pdf_path → pdf_url (http://localhost:5002/downloads/...) - tuftsSCOPreAuthProcessor: use pdf_url (not pdf_path), save preAuthNumber to preAuthNumber field - unitedDHPreAuthProcessor: save preAuthNumber to preAuthNumber field (not claimNumber) Co-Authored-By: Claude Sonnet 4.6 --- .../processors/tuftsSCOPreAuthProcessor.ts | 22 ++- .../processors/unitedDHPreAuthProcessor.ts | 2 +- apps/Backend/src/routes/claims.ts | 4 +- .../routes/insuranceStatusTuftsSCOPreAuth.ts | 3 +- .../services/seleniumTuftsSCOPreAuthClient.ts | 2 +- .../src/storage/cloudStorage-storage.ts | 4 +- .../src/components/claims/claim-form.tsx | 16 ++- .../helpers_tuftssco_preauth.py | 29 ++-- .../selenium_TuftsSCO_preAuthWorker.py | 130 ++++++++---------- 9 files changed, 107 insertions(+), 105 deletions(-) diff --git a/apps/Backend/src/queue/processors/tuftsSCOPreAuthProcessor.ts b/apps/Backend/src/queue/processors/tuftsSCOPreAuthProcessor.ts index 08eae67b..70406449 100644 --- a/apps/Backend/src/queue/processors/tuftsSCOPreAuthProcessor.ts +++ b/apps/Backend/src/queue/processors/tuftsSCOPreAuthProcessor.ts @@ -88,12 +88,10 @@ async function pollUntilDone( throw new Error(`Tufts SCO preauth polling exhausted all attempts for session ${sessionId}`); } -async function savePdfFromSelenium(pdf_path: string, patientId: number) { +async function savePdfFromSelenium(pdf_url: string, patientId: number) { try { - const filename = path.basename(pdf_path); - const seleniumPort = process.env.SELENIUM_PORT || "5002"; - const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`; - const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 }); + const filename = path.basename(new URL(pdf_url).pathname); + const resp = await axios.get(pdf_url, { responseType: "arraybuffer", timeout: 30000 }); let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH"); if (!group) { @@ -116,7 +114,7 @@ export interface TuftsSCOPreAuthProcessorInput { export async function runTuftsSCOPreAuthProcessor( input: TuftsSCOPreAuthProcessorInput, jobId: string -): Promise<{ status: string; pdf_path?: string; preAuthNumber?: string }> { +): Promise<{ status: string; pdf_url?: string; preAuthNumber?: string }> { const { enrichedPayload, userId, claimId, socketId } = input; log("tuftssco-preauth-processor", "starting Python agent session", { claimId }); @@ -138,12 +136,12 @@ export async function runTuftsSCOPreAuthProcessor( } const preAuthNumber: string | undefined = seleniumResult.preAuthNumber ?? undefined; - const pdf_path: string | undefined = seleniumResult.pdf_path ?? undefined; + const pdf_url: string | undefined = seleniumResult.pdf_url ?? undefined; if (claimId) { try { const updates: Record = { status: "PREAUTH" }; - if (preAuthNumber) updates.claimNumber = preAuthNumber; + if (preAuthNumber) updates.preAuthNumber = preAuthNumber; await storage.updateClaim(claimId, updates); log("tuftssco-preauth-processor", "claim record updated", { claimId, preAuthNumber }); @@ -157,22 +155,22 @@ export async function runTuftsSCOPreAuthProcessor( } } - if (pdf_path && !socketId) { + if (pdf_url && !socketId) { const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null; const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId; - if (patientId) await savePdfFromSelenium(pdf_path, Number(patientId)); + if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId)); } emitToSocket(socketId, "selenium:tuftssco_preauth_completed", { jobId, claimId, preAuthNumber, - pdf_path, + pdf_url, message: preAuthNumber ? `Tufts SCO pre-authorization submitted — PreAuth #: ${preAuthNumber}` : (seleniumResult?.message ?? "Tufts SCO pre-authorization submitted successfully"), }); log("tuftssco-preauth-processor", "done", { claimId, preAuthNumber }); - return { status: "success", pdf_path, preAuthNumber }; + return { status: "success", pdf_url, preAuthNumber }; } diff --git a/apps/Backend/src/queue/processors/unitedDHPreAuthProcessor.ts b/apps/Backend/src/queue/processors/unitedDHPreAuthProcessor.ts index d16efe2a..c52bd9ae 100644 --- a/apps/Backend/src/queue/processors/unitedDHPreAuthProcessor.ts +++ b/apps/Backend/src/queue/processors/unitedDHPreAuthProcessor.ts @@ -153,7 +153,7 @@ export async function runUnitedDHPreAuthProcessor( if (claimId) { try { const updates: Record = { status: "PREAUTH" }; - if (preAuthNumber) updates.claimNumber = preAuthNumber; + if (preAuthNumber) updates.preAuthNumber = preAuthNumber; await storage.updateClaim(claimId, updates); log("uniteddh-preauth-processor", "claim record updated", { claimId, preAuthNumber }); diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 27fc6a75..160f19f1 100755 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -136,12 +136,12 @@ router.post( (folder as any).id ); await storage.appendFileChunk((cloudFile as any).id, 0, file.buffer); - await storage.finalizeFileUpload((cloudFile as any).id); + const finalized = await storage.finalizeFileUpload((cloudFile as any).id); result.push({ filename: file.originalname, mimeType: file.mimetype, - filePath: `/api/cloud-storage/files/${(cloudFile as any).id}/content`, + filePath: finalized.diskPath, }); } diff --git a/apps/Backend/src/routes/insuranceStatusTuftsSCOPreAuth.ts b/apps/Backend/src/routes/insuranceStatusTuftsSCOPreAuth.ts index fc1d927a..4521a1e8 100644 --- a/apps/Backend/src/routes/insuranceStatusTuftsSCOPreAuth.ts +++ b/apps/Backend/src/routes/insuranceStatusTuftsSCOPreAuth.ts @@ -10,13 +10,12 @@ const router = Router(); * POST /tuftssco-preauth * * Enqueues a Tufts SCO (DentaQuest) pre-authorization submission job. + * Uses persistent session + OTP handling, same pattern as UnitedDH preauth. * * Body fields (JSON): * data — preauth payload (memberId, dateOfBirth, serviceDate, serviceLines, patientName, etc.) * socketId — socket.io client id * claimId — existing claim DB id (optional) - * - * Response: { status: "queued", jobId: "…" } */ router.post("/tuftssco-preauth", async (req: Request, res: Response): Promise => { if (!req.user?.id) { diff --git a/apps/Backend/src/services/seleniumTuftsSCOPreAuthClient.ts b/apps/Backend/src/services/seleniumTuftsSCOPreAuthClient.ts index 8375a7e1..aeee3a5e 100644 --- a/apps/Backend/src/services/seleniumTuftsSCOPreAuthClient.ts +++ b/apps/Backend/src/services/seleniumTuftsSCOPreAuthClient.ts @@ -1,6 +1,6 @@ import axios from "axios"; -const SELENIUM_BASE = process.env.SELENIUM_SERVICE_URL || "http://localhost:8000"; +const SELENIUM_BASE = process.env.SELENIUM_SERVICE_URL ?? "http://localhost:5002"; export async function forwardToSeleniumTuftsSCOPreAuthAgent(data: any) { const response = await axios.post(`${SELENIUM_BASE}/tuftssco-preauth`, data, { diff --git a/apps/Backend/src/storage/cloudStorage-storage.ts b/apps/Backend/src/storage/cloudStorage-storage.ts index a5dd3993..d58054b7 100755 --- a/apps/Backend/src/storage/cloudStorage-storage.ts +++ b/apps/Backend/src/storage/cloudStorage-storage.ts @@ -106,7 +106,7 @@ export interface IStorage { folderId?: number | null ): Promise; appendFileChunk(fileId: number, seq: number, data: Buffer): Promise; - finalizeFileUpload(fileId: number): Promise<{ ok: true; size: string }>; + finalizeFileUpload(fileId: number): Promise<{ ok: true; size: string; diskPath: string }>; deleteFile(fileId: number): Promise; updateFile( id: number, @@ -354,7 +354,7 @@ export const cloudStorageStorage: IStorage = { }); await updateFolderTimestampsRecursively(file.folderId); - return { ok: true, size: BigInt(total).toString() }; + return { ok: true, size: BigInt(total).toString(), diskPath }; }, async deleteFile(fileId: number) { diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 35f5e0b4..0ffdf964 100755 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -1463,13 +1463,19 @@ export function ClaimForm({ return; } + const { uploadedFiles: udPreAuthFiles, ...udPreAuthRestForm } = form; + const udPreAuthFilesMeta: ClaimFileMeta[] = udPreAuthFiles?.length + ? await uploadAttachmentsToLocalFolder(udPreAuthFiles) + : []; + onHandleForUnitedDHSeleniumPreAuth({ - ...form, + ...udPreAuthRestForm, serviceLines: filteredServiceLines, staffId: appointmentStaffId ?? Number(staff?.id), patientId, insuranceProvider: "United/DentalHub", insuranceSiteKey: "UNITED_SCO", + claimFiles: udPreAuthFilesMeta, }); onClose(); @@ -1501,13 +1507,19 @@ export function ClaimForm({ return; } + const { uploadedFiles: preAuthUploadedFiles, ...preAuthRestForm } = form; + const preAuthFilesMeta: ClaimFileMeta[] = preAuthUploadedFiles?.length + ? await uploadAttachmentsToLocalFolder(preAuthUploadedFiles) + : []; + onHandleForTuftsSCOSeleniumPreAuth({ - ...form, + ...preAuthRestForm, serviceLines: filteredServiceLines, staffId: appointmentStaffId ?? Number(staff?.id), patientId, insuranceProvider: "Tufts SCO", insuranceSiteKey: "TUFTS_SCO", + claimFiles: preAuthFilesMeta, }); onClose(); diff --git a/apps/SeleniumService/helpers_tuftssco_preauth.py b/apps/SeleniumService/helpers_tuftssco_preauth.py index 721d4b6c..f417fe33 100644 --- a/apps/SeleniumService/helpers_tuftssco_preauth.py +++ b/apps/SeleniumService/helpers_tuftssco_preauth.py @@ -159,14 +159,18 @@ async def start_tuftssco_preauth_run(sid: str, data: dict, url: str): otp_input.send_keys(otp_value) try: verify_btn = driver.find_element(By.XPATH, - "//button[@type='submit'] | " - "//button[contains(text(),'Verify') or contains(text(),'Submit') or contains(text(),'Confirm')]" - ) + "//button[@type='button' and @aria-label='Verify']") verify_btn.click() - print("[TuftsSCO PreAuth OTP] Clicked verify button") - except: - otp_input.send_keys("\n") - print("[TuftsSCO PreAuth OTP] Pressed Enter as fallback") + print("[TuftsSCO PreAuth OTP] Clicked Verify button (aria-label)") + except Exception: + try: + verify_btn = driver.find_element(By.XPATH, + "//button[contains(text(),'Verify') or contains(text(),'Submit') or @type='submit']") + verify_btn.click() + print("[TuftsSCO PreAuth OTP] Clicked verify button (text fallback)") + except Exception: + otp_input.send_keys("\n") + print("[TuftsSCO PreAuth OTP] Pressed Enter as fallback") print("[TuftsSCO PreAuth OTP] OTP typed and submitted via app") s["otp_value"] = None await asyncio.sleep(3) @@ -288,11 +292,20 @@ async def start_tuftssco_preauth_run(sid: str, data: dict, url: str): pdf_path = step8_result.get("pdf_path") if isinstance(step8_result, dict) else None preauth_number = step8_result.get("preAuthNumber") if isinstance(step8_result, dict) else None + pdf_url = None + if pdf_path: + import os as _os + filename = _os.path.basename(pdf_path) + port = _os.getenv("PORT", "5002") + url_host = _os.getenv("HOST", "localhost") + pdf_url = f"http://{url_host}:{port}/downloads/{filename}" + print(f"[TuftsSCO PreAuth] pdf_url: {pdf_url}") + result = { "status": "success", "message": "Tufts SCO pre-authorization submitted successfully", "preAuthNumber": preauth_number, - "pdf_path": pdf_path, + "pdf_url": pdf_url, } s["status"] = "completed" s["result"] = result diff --git a/apps/SeleniumService/selenium_TuftsSCO_preAuthWorker.py b/apps/SeleniumService/selenium_TuftsSCO_preAuthWorker.py index 5fea455e..a296f92e 100644 --- a/apps/SeleniumService/selenium_TuftsSCO_preAuthWorker.py +++ b/apps/SeleniumService/selenium_TuftsSCO_preAuthWorker.py @@ -572,17 +572,17 @@ class AutomationTuftsSCOPreAuth: inp.send_keys(Keys.CONTROL + "a") inp.send_keys(Keys.DELETE) inp.send_keys(str(value)) - time.sleep(0.5) + time.sleep(1.5) # give portal time to load autocomplete results listbox_id = inp.get_attribute("aria-controls") or "" try: if listbox_id: - option = WebDriverWait(self.driver, 4).until( + option = WebDriverWait(self.driver, 6).until( EC.element_to_be_clickable((By.XPATH, f"//*[@id='{listbox_id}']//*[@role='option'][1]" )) ) else: - option = WebDriverWait(self.driver, 4).until( + option = WebDriverWait(self.driver, 6).until( EC.element_to_be_clickable((By.XPATH, f"//*[@role='listbox']//*[@role='option' and contains(normalize-space(.),'{value}')]" f" | //*[@role='listbox']//*[@role='option'][1]" @@ -592,7 +592,7 @@ class AutomationTuftsSCOPreAuth: print(f"[TuftsSCO PreAuth step4] {label}: selected '{value}'") except TimeoutException: inp.send_keys(Keys.TAB) - print(f"[TuftsSCO PreAuth step4] {label}: typed '{value}' (no dropdown)") + print(f"[TuftsSCO PreAuth step4] {label}: typed '{value}' (no dropdown — pressed TAB)") except Exception as e: print(f"[TuftsSCO PreAuth step4] Warning: could not fill {label}: {e}") @@ -608,38 +608,8 @@ class AutomationTuftsSCOPreAuth: print(f"[TuftsSCO PreAuth step4] Warning: could not fill {label}: {e}") def step4_fill_preauth_form(self): - """Fill service date then all procedure line fields.""" + """Fill all procedure line fields. Preauth form has no date field — skip it.""" try: - month, day, year = self._parse_service_date() - - if month and day and year: - print(f"[TuftsSCO PreAuth step4] Filling service date: {month}/{day}/{year}") - try: - dos_container = WebDriverWait(self.driver, 8).until( - EC.presence_of_element_located((By.XPATH, - "//*[@data-testid and contains(@data-testid,'date-of-service')] | " - "//*[contains(@aria-label,'Select date of service')]/ancestor::div[1]" - )) - ) - month_el = dos_container.find_element(By.XPATH, ".//span[@data-type='month' and @contenteditable='true']") - day_el = dos_container.find_element(By.XPATH, ".//span[@data-type='day' and @contenteditable='true']") - year_el = dos_container.find_element(By.XPATH, ".//span[@data-type='year' and @contenteditable='true']") - for elem, val in [(month_el, month), (day_el, day), (year_el, year)]: - elem.click() - elem.send_keys(Keys.CONTROL, "a") - elem.send_keys(Keys.BACKSPACE) - elem.send_keys(val) - time.sleep(0.05) - print("[TuftsSCO PreAuth step4] Service date filled") - except Exception: - self._fill_spinbutton("month", month) - self._fill_spinbutton("day", day) - self._fill_spinbutton("year", year) - else: - print(f"[TuftsSCO PreAuth step4] No valid service date: {self.serviceDate!r}") - - time.sleep(0.3) - active_lines = [ln for ln in self.serviceLines if str(ln.get("procedureCode") or "").strip()] print(f"[TuftsSCO PreAuth step4] {len(active_lines)} service line(s)") @@ -678,8 +648,8 @@ class AutomationTuftsSCOPreAuth: ) proc_inp = proc_inputs[idx] if idx < len(proc_inputs) else proc_inputs[-1] self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", proc_inp) - self._fill_combobox(proc_inp, code, f"procedureCode[{idx}]") - time.sleep(0.5) + self._fill_text_input(proc_inp, code, f"procedureCode[{idx}]") + time.sleep(1.5) # wait for warning banner layout shift to settle except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not fill procedure code: {e}") @@ -687,7 +657,14 @@ class AutomationTuftsSCOPreAuth: try: tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']") if idx < len(tooth_inputs): - self._fill_combobox(tooth_inputs[idx], tooth, f"tooth[{idx}]") + tooth_inp = tooth_inputs[idx] + self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", tooth_inp) + self.driver.execute_script("arguments[0].focus();", tooth_inp) + time.sleep(0.2) + tooth_inp.send_keys(Keys.CONTROL + "a") + tooth_inp.send_keys(Keys.DELETE) + tooth_inp.send_keys(str(tooth)) + print(f"[TuftsSCO PreAuth step4] tooth[{idx}]: typed '{tooth}'") time.sleep(0.3) except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not fill tooth: {e}") @@ -766,24 +743,30 @@ class AutomationTuftsSCOPreAuth: print(f"[TuftsSCO PreAuth step5] Attaching: {abs_path}") try: - add_file_btn = WebDriverWait(self.driver, 10).until( - EC.element_to_be_clickable((By.XPATH, - "//button[.//span[contains(text(),'Add a file')]] | " - "//button[contains(normalize-space(text()),'Add a file')] | " - "//*[contains(text(),'Add a file') and (@role='button' or self::label)]" - )) + # The react-aria-Input file input is always in the DOM (hidden). + # Do NOT click "Add a file" — that opens the OS dialog and blocks WebDriver. + file_input = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.XPATH, "//input[@type='file' and contains(@class,'react-aria-Input')]")) ) - self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_file_btn) - ActionChains(self.driver).move_to_element(add_file_btn).click().perform() - time.sleep(1) - - file_input = WebDriverWait(self.driver, 8).until( - EC.presence_of_element_located((By.XPATH, "//input[@type='file']")) + self.driver.execute_script( + "arguments[0].style.display='block'; arguments[0].style.visibility='visible';", + file_input ) - self.driver.execute_script("arguments[0].style.display='block';", file_input) file_input.send_keys(abs_path) - time.sleep(1.5) - print(f"[TuftsSCO PreAuth step5] Attached: {os.path.basename(abs_path)}") + + # Wait until the filename appears on the page confirming upload completed + filename = os.path.basename(abs_path) + try: + WebDriverWait(self.driver, 15).until( + lambda d: filename.lower() in d.page_source.lower() + or filename.rsplit("_", 1)[-1].lower() in d.page_source.lower() + ) + print(f"[TuftsSCO PreAuth step5] Upload confirmed: {filename}") + except Exception: + time.sleep(3) + print(f"[TuftsSCO PreAuth step5] Upload wait timed out — proceeding: {filename}") + + print(f"[TuftsSCO PreAuth step5] Attached: {filename}") attached += 1 except Exception as e: print(f"[TuftsSCO PreAuth step5] Could not attach {abs_path}: {e}") @@ -847,20 +830,24 @@ class AutomationTuftsSCOPreAuth: print("[TuftsSCO PreAuth step7] Checked acknowledgement checkbox") time.sleep(0.5) - # Try pre-auth submit button first, fall back to generic submit + all_btns = self.driver.find_elements(By.XPATH, "//button") + print(f"[TuftsSCO PreAuth step7] Buttons: {[b.get_attribute('aria-label') or b.text[:40] for b in all_btns]}") + submit_btn = None for xpath in [ - "//button[.//span[contains(text(),'Submit prior authorization')]] | //button[contains(normalize-space(text()),'Submit prior authorization')] | //button[@aria-label='Submit prior authorization']", + "//button[.//span[normalize-space(text())='pre-authorization']]", + "//button[.//span[contains(text(),'Submit prior authorization')]] | //button[@aria-label='Submit prior authorization']", "//button[.//span[contains(text(),'Submit pre-authorization')]] | //button[contains(normalize-space(text()),'Submit pre-authorization')]", - "//button[.//span[contains(text(),'Submit claim')]] | //button[contains(normalize-space(text()),'Submit claim')] | //button[@aria-label='Submit claim']", + "//button[.//span[contains(text(),'Submit claim')]] | //button[@aria-label='Submit claim']", + "//button[.//span[contains(text(),'Submit')]] | //button[contains(normalize-space(text()),'Submit')]", ]: try: submit_btn = WebDriverWait(self.driver, 5).until( - EC.element_to_be_clickable((By.XPATH, xpath)) + EC.presence_of_element_located((By.XPATH, xpath)) ) - print(f"[TuftsSCO PreAuth step7] Found submit button") + print(f"[TuftsSCO PreAuth step7] Found submit button: {submit_btn.get_attribute('aria-label') or submit_btn.text[:40]!r}") break - except TimeoutException: + except Exception: continue if submit_btn is None: @@ -889,13 +876,8 @@ class AutomationTuftsSCOPreAuth: def step8_save_confirmation_pdf(self): """Wait for the confirmation page, extract the pre-auth number, save page as PDF.""" try: - WebDriverWait(self.driver, 30).until( - lambda d: ( - "thank" in d.page_source.lower() - or "submitted" in d.page_source.lower() - or "prior auth" in d.page_source.lower() - or "pre-auth" in d.page_source.lower() - ) + WebDriverWait(self.driver, 60).until( + lambda d: "thank" in d.page_source.lower() or "submitted pre-authorization" in d.page_source.lower() ) time.sleep(2) print(f"[TuftsSCO PreAuth step8] Confirmation page URL: {self.driver.current_url}") @@ -903,17 +885,15 @@ class AutomationTuftsSCOPreAuth: preauth_number = None try: body_text = self.driver.find_element(By.TAG_NAME, "body").text - for pattern in [ - r'prior auth(?:orization)?\s+(?:number\s+)?(\d{8,})', - r'pre-auth(?:orization)?\s+(?:number\s+)?(\d{8,})', - r'submitted\s+(?:prior auth|pre-auth|authorization)\s+(\d{8,})', - r'\b(\d{12,})\b', - ]: - match = re.search(pattern, body_text, re.IGNORECASE) + match = re.search(r'submitted pre-authorization\s+(\d{10,})', body_text, re.IGNORECASE) + if match: + preauth_number = match.group(1) + print(f"[TuftsSCO PreAuth step8] Extracted pre-auth number: {preauth_number}") + else: + match = re.search(r'\b(\d{12,})\b', body_text) if match: preauth_number = match.group(1) - print(f"[TuftsSCO PreAuth step8] Extracted pre-auth number: {preauth_number}") - break + print(f"[TuftsSCO PreAuth step8] Extracted pre-auth number (fallback): {preauth_number}") except Exception as e: print(f"[TuftsSCO PreAuth step8] Could not extract pre-auth number: {e}")