Files
DentalManagement09/apps/SeleniumService/selenium_BCBS_MA_eligibilityCheckWorker.py
T
GiteadandClaude Opus 5.5 13be6e97d5 feat: BCBS MA persistent session, Expand All fix, address/gender extraction
- Persistent Chrome profile (bcbs_ma_browser_manager) reused across patients;
  login probes /myportal/ to skip login + OTP while the session is active
- Expand All: JS-click the expandCollapseViews span, verify sections opened,
  and unclip fixed-height result tables so the PDF includes every row
- Extract patient address and gender from the results page and save to patient
- Uppercase member IDs (XXA...), matching older lowercase records
- Add /clear-bcbs-ma-session endpoint

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:27:46 -04:00

657 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import re
import time
import base64
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium import webdriver
from bcbs_ma_browser_manager import get_browser_manager
DOWNLOAD_DIR = get_browser_manager().download_dir
class AutomationBCBSMAEligibilityCheck:
"""
BCBS MA Provider Central eligibility check.
Uses the persistent BCBS MA browser (bcbs_ma_browser_manager) — one Chrome
with a saved profile, reused across patients. While the portal login is
still active, login() returns SUCCESS and no OTP is needed.
Flow: login(url) → OTP_REQUIRED | SUCCESS → [caller polls for OTP] → submit_otp_step(otp)
→ step1() [search member] → step2() [extract + PDF]
"""
# Public portal — always shows the login form, even when already logged in
LOGIN_URL = "https://provider.bluecrossma.com/ProviderHome/portal/"
# Logged-in portal — only loads with an active session; otherwise BCBS
# redirects away from /myportal/
MYPORTAL_URL = "https://provider.bluecrossma.com/ProviderHome/myportal/"
def __init__(self, data: dict):
raw = data.get("data", data)
self.member_id = raw.get("memberId", "")
self.dob = raw.get("dateOfBirth", "") # YYYY-MM-DD from frontend
self.first_name = raw.get("firstName", "")
self.last_name = raw.get("lastName", "")
self.username = raw.get("bcbsMaUsername", "")
self.password = raw.get("bcbsMaPassword", "")
self.provider_npi = raw.get("providerNpi", "")
self.driver: webdriver.Chrome | None = None
# ── Driver lifecycle ──────────────────────────────────────────────────────
def config_driver(self):
# Persistent browser from manager (keeps the BCBS MA login between patients)
headless = os.getenv("SELENIUM_HEADLESS", "false").lower() == "true"
self.driver = get_browser_manager().get_driver(headless)
self._close_extra_tabs()
def _close_extra_tabs(self):
"""Close ConnectCenter tabs left over from the previous patient, keep the first tab."""
try:
handles = self.driver.window_handles
for handle in handles[1:]:
self.driver.switch_to.window(handle)
self.driver.close()
self.driver.switch_to.window(handles[0])
if len(handles) > 1:
print(f"[BCBS MA] Closed {len(handles) - 1} leftover tab(s)")
except Exception as e:
print(f"[BCBS MA] Could not close leftover tabs: {e}")
def _force_logout(self):
"""Clear all BCBS MA / ConnectCenter cookies when the saved username changes."""
try:
print("[BCBS MA login] Forcing logout due to credential change...")
self.driver.execute_cdp_cmd("Network.clearBrowserCookies", {})
get_browser_manager().clear_credentials_hash()
print("[BCBS MA login] Cookies cleared")
except Exception as e:
print(f"[BCBS MA login] Error during forced logout: {e}")
# ── Helpers ───────────────────────────────────────────────────────────────
def _wait(self, timeout=15):
return WebDriverWait(self.driver, timeout)
def _is_logged_in_page(self) -> bool:
"""On the logged-in portal (/myportal/) with no login form showing."""
return ("/myportal/" in self.driver.current_url.lower()
and not self.driver.find_elements(By.ID, "txtUsername0"))
def _dob_mmddyyyy(self) -> str:
"""Convert YYYY-MM-DD → MM/DD/YYYY."""
try:
parts = self.dob.split("-")
return f"{parts[1]}/{parts[2]}/{parts[0]}"
except Exception:
return self.dob
# ── Step: Login ───────────────────────────────────────────────────────────
def login(self, url: str = "") -> str:
"""
Check for an active session on the logged-in portal (/myportal/); if
there is one, return SUCCESS straight away. Otherwise open the public
portal, fill credentials and click Log in.
Returns:
"OTP_REQUIRED" – redirected to MFA challenge page (/mga/sps/authsvc)
"SUCCESS" – on the logged-in portal (existing session, or login without OTP)
"ERROR:..." – something went wrong
"""
target = url or self.LOGIN_URL
try:
if self.username and get_browser_manager().credentials_changed(self.username):
self._force_logout()
# 1. Existing session? The public portal shows the login form even when
# logged in, so probe /myportal/, which only loads with a live session.
print(f"[BCBS MA login] Checking for active session at {self.MYPORTAL_URL}")
self.driver.get(self.MYPORTAL_URL)
self._wait(20).until(lambda d: d.execute_script("return document.readyState") == "complete")
time.sleep(2) # let any session-expired redirect finish
print(f"[BCBS MA login] Landed on {self.driver.current_url[:80]}")
if self._is_logged_in_page():
print("[BCBS MA login] Session still active — skipping login and OTP")
return "SUCCESS"
if "authsvc" in self.driver.current_url.lower():
print("[BCBS MA login] Landed on OTP page")
return "OTP_REQUIRED"
# 2. Not logged in — log in from the public portal
print(f"[BCBS MA login] Not logged in — navigating to {target}")
self.driver.get(target)
username_input = self._wait(20).until(
EC.presence_of_element_located((By.ID, "txtUsername0"))
)
username_input.clear()
username_input.send_keys(self.username)
print("[BCBS MA login] Username filled")
# Password field: id="txtPassword"
password_input = self._wait(10).until(
EC.presence_of_element_located((By.ID, "txtPassword"))
)
password_input.clear()
password_input.send_keys(self.password)
print("[BCBS MA login] Password filled")
# Log in button: id="ns_Z7_09ME1282N8N3B0QGV9ND6N20G2_loginSubmit"
login_btn = self._wait(10).until(
EC.element_to_be_clickable((By.ID, "ns_Z7_09ME1282N8N3B0QGV9ND6N20G2_loginSubmit"))
)
login_btn.click()
print("[BCBS MA login] Log in clicked, waiting for response...")
time.sleep(3)
return self._detect_post_login_state()
except Exception as e:
print(f"[BCBS MA login] Error: {e}")
return f"ERROR: login failed: {e}"
def _detect_post_login_state(self) -> str:
"""Check current URL/DOM to decide what happened after login."""
for _ in range(6):
time.sleep(1)
url = self.driver.current_url.lower()
print(f"[BCBS MA] post-login URL: {url[:80]}")
if "authsvc" in url or "/mga/sps/" in url:
print("[BCBS MA] OTP page detected")
return "OTP_REQUIRED"
# eTools shows on the public page too — only /myportal/ proves login
if self._is_logged_in_page():
print("[BCBS MA] Logged in without OTP")
return "SUCCESS"
print("[BCBS MA] Could not determine post-login state")
return "OTP_REQUIRED" # default assumption for BCBS MA
# ── Step: Submit OTP ──────────────────────────────────────────────────────
def submit_otp_step(self, otp: str) -> str:
"""
Enter the 6-digit OTP into the BCBS MA verification page and click Submit.
OTP input: id="otppswd", name="otp.user.otp"
Submit btn: id="submitButton" (starts disabled, enables after OTP typed)
Returns "SUCCESS" or "ERROR:..."
"""
try:
print(f"[BCBS MA OTP] Submitting OTP: {otp}")
# OTP input field: id="otppswd"
otp_input = self._wait(15).until(
EC.presence_of_element_located((By.ID, "otppswd"))
)
otp_input.clear()
otp_input.send_keys(otp)
print("[BCBS MA OTP] OTP entered")
# Submit button starts disabled — wait for it to become clickable
submit_btn = self._wait(10).until(
EC.element_to_be_clickable((By.ID, "submitButton"))
)
submit_btn.click()
print("[BCBS MA OTP] Submit clicked, waiting for dashboard...")
time.sleep(4)
# Wait for dashboard
for _ in range(15):
time.sleep(1)
url = self.driver.current_url.lower()
print(f"[BCBS MA OTP] URL: {url[:80]}")
if "authsvc" not in url and "/mga/sps/" not in url:
print("[BCBS MA OTP] Left OTP page — login successful")
return "SUCCESS"
return "ERROR: OTP page still visible after submission"
except Exception as e:
print(f"[BCBS MA OTP] Error: {e}")
return f"ERROR: OTP submission failed: {e}"
# ── Step 1: Navigate to ConnectCenter → New Eligibility Request ──────────
def step1(self) -> str:
"""
After login, on the Provider Central dashboard:
1. Click eTools menu
2. Click ConnectCenter in the dropdown
3. Click Go Now on the ConnectCenter launch page
4. Click Continue in the popup (opens ConnectCenter in new tab)
5. Switch to new tab, click Verification
6. Click New Eligibility Request in dropdown
Returns "SUCCESS" (on Eligibility Identifier page) or "ERROR:..."
"""
try:
from selenium.webdriver.common.keys import Keys
# 1. Click eTools
print("[BCBS MA step1] Clicking eTools...")
etools = self._wait(15).until(
EC.element_to_be_clickable((By.XPATH,
"//span[text()='eTools'] | //a[.//span[text()='eTools']]"
))
)
etools.click()
print("[BCBS MA step1] eTools clicked, waiting for dropdown...")
time.sleep(2)
# 2. Click ConnectCenter link in the dropdown
print("[BCBS MA step1] Clicking ConnectCenter...")
connect_center = self._wait(15).until(
EC.element_to_be_clickable((By.XPATH,
"//a[contains(@href,'connectcenter')]"
))
)
connect_center.click()
print("[BCBS MA step1] ConnectCenter clicked, waiting for page to load...")
time.sleep(3)
# 3. Click Go Now on the ConnectCenter page
print("[BCBS MA step1] Clicking Go Now...")
go_now = self._wait(20).until(
EC.element_to_be_clickable((By.ID, "GoNow-2c338de9-6a2f-4d71-b5fb-a6e4ae14be80"))
)
go_now.click()
time.sleep(2)
print("[BCBS MA step1] Go Now clicked — popup opened, pressing Enter to continue...")
# 4. Press Enter to auto-confirm the popup
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(self.driver).send_keys(Keys.ENTER).perform()
time.sleep(4)
print("[BCBS MA step1] Enter pressed — continuing to ConnectCenter")
# 5. Switch to the new tab that ConnectCenter opened
self._wait(10).until(lambda d: len(d.window_handles) > 1)
self.driver.switch_to.window(self.driver.window_handles[-1])
print(f"[BCBS MA step1] Switched to new tab: {self.driver.current_url[:60]}")
time.sleep(3)
# 6. Click Verification menu
print("[BCBS MA step1] Clicking Verification...")
verification = self._wait(15).until(
EC.element_to_be_clickable((By.XPATH,
"//a[text()='Verification' or normalize-space(text())='Verification']"
))
)
verification.click()
time.sleep(2)
print("[BCBS MA step1] Verification clicked")
# 7. Click New Eligibility Request in dropdown
print("[BCBS MA step1] Clicking New Eligibility Request...")
NEW_ELIG_XPATH = (
"//a[contains(@ng-click,'newEligibility')] | "
"//a[contains(normalize-space(text()),'New Eligibility Request')] | "
"//a[contains(normalize-space(text()),'New Eligibility')]"
)
try:
new_elig = self._wait(8).until(
EC.element_to_be_clickable((By.XPATH, NEW_ELIG_XPATH))
)
except TimeoutException:
# Dropdown may have closed — re-open Verification and try again
print("[BCBS MA step1] New Eligibility Request not found, re-clicking Verification...")
verification.click()
time.sleep(2)
new_elig = self._wait(8).until(
EC.element_to_be_clickable((By.XPATH, NEW_ELIG_XPATH))
)
try:
new_elig.click()
except Exception:
self.driver.execute_script("arguments[0].click();", new_elig)
time.sleep(3)
print("[BCBS MA step1] New Eligibility Request clicked — on Eligibility Identifier page")
return "SUCCESS"
except Exception as e:
print(f"[BCBS MA step1] Error: {e}")
return f"ERROR: step1 failed: {e}"
# ── Step 2: Fill Eligibility Identifier form and get results ─────────────
def _expand_all_sections(self):
"""
Click "Expand All" on the results page and confirm the sections opened.
The label may be nested inside an h6/a/span, so match the innermost
element whose visible text is "Expand All", then fall back to a JS
click on it and its clickable ancestors until the page height grows.
"""
label_xpath = (
"//*[normalize-space(.)='Expand All' and "
"not(*[normalize-space(.)='Expand All'])]"
)
try:
label = self._wait(15).until(
EC.presence_of_element_located((By.XPATH, label_xpath))
)
except TimeoutException:
print("[BCBS MA step2] Expand All not found — PDF will have collapsed sections")
return
# Each benefit section (Copay, Coinsurance, ...) is an ng-grid table that
# only becomes visible once expanded — count them to verify the click
def visible_grids():
return self.driver.execute_script(
"return [...document.querySelectorAll('.ngViewport')]"
".filter(v => v.offsetParent && v.clientHeight > 0).length;"
)
# Let the benefit sections finish rendering before expanding them
time.sleep(2)
before = visible_grids()
# Markup: <span ng-click="expandCollapseViews(false)"><h6>Expand All</h6></span>.
# JS-click the span (an overlapping View Options panel intercepts native
# clicks); fall back to the label itself. Only re-click while nothing has
# expanded, so a click can never collapse sections that already opened.
for attempt in range(1, 4):
try:
targets = self.driver.find_elements(By.XPATH,
label_xpath + "/ancestor::*[starts-with(@ng-click,'expandCollapseViews')][1]"
) or [label]
self.driver.execute_script("arguments[0].click();", targets[0])
except Exception as e:
print(f"[BCBS MA step2] Expand All click attempt {attempt} failed: {e}")
for _ in range(10):
time.sleep(0.5)
if visible_grids() > before:
time.sleep(1.5) # let the rows inside each section render
print(f"[BCBS MA step2] Expand All worked (attempt {attempt}, "
f"visible sections {before} → {visible_grids()})")
self._unclip_grids()
return
print(f"[BCBS MA step2] Expand All attempt {attempt} did not expand sections "
f"(visible sections still {visible_grids()})")
print("[BCBS MA step2] Expand All failed — PDF will have collapsed sections")
def _unclip_grids(self):
"""
Each expanded section's table is a fixed-height (368px) scroll box, so a
PDF only shows its first rows. All rows are already in the DOM — grow each
box (and its ng-grid wrapper) to full height so the PDF includes them.
"""
try:
count = self.driver.execute_script("""
let n = 0;
document.querySelectorAll('.ngViewport').forEach(v => {
if (!v.offsetParent || v.scrollHeight <= v.clientHeight + 2) return;
const grow = v.scrollHeight - v.clientHeight;
v.style.height = v.scrollHeight + 'px';
v.style.overflow = 'visible';
const grid = v.closest('.ngGrid');
if (grid) {
grid.style.height = (grid.offsetHeight + grow) + 'px';
grid.style.overflow = 'visible';
}
n++;
});
return n;
""")
print(f"[BCBS MA step2] Unclipped {count} scrolling table(s) for the PDF")
except Exception as e:
print(f"[BCBS MA step2] Could not unclip tables: {e}")
def step2(self) -> dict:
"""
On the Eligibility Identifier page:
1. Enter provider NPI → click Find Provider
2. Select payer search option: Member ID, Subscriber Date Of Birth (value=2)
3. Select service type: Dental Care [35] (value=33)
4. Select place of service: OFFICE [11] (value=30)
5. Enter member ID
6. Enter date of birth (MM/DD/YYYY)
7. Click Submit → wait for results page → PDF
"""
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
try:
# New Eligibility Request opens in a new tab — switch to the latest one
self._wait(10).until(lambda d: len(d.window_handles) >= 2)
self.driver.switch_to.window(self.driver.window_handles[-1])
print(f"[BCBS MA step2] Switched to tab ({len(self.driver.window_handles)} open): {self.driver.current_url[:70]}")
time.sleep(2)
print("[BCBS MA step2] Filling Eligibility Identifier form...")
# 1. Enter provider NPI
provider_id_input = self._wait(15).until(
EC.presence_of_element_located((By.ID, "providerID"))
)
provider_id_input.clear()
provider_id_input.send_keys(self.provider_npi)
print(f"[BCBS MA step2] Provider NPI entered: {self.provider_npi}")
# Wait for spinner cycle to complete, then click Find Provider
try:
self._wait(2).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "div.ccProgressBar"))
)
except TimeoutException:
pass # spinner never appeared — page loaded instantly
self._wait(10).until(
EC.invisibility_of_element_located((By.CSS_SELECTOR, "div.ccProgressBar"))
)
find_provider = self._wait(10).until(
EC.element_to_be_clickable((By.XPATH,
"//button[@ng-click='searchProviders()']"
))
)
try:
find_provider.click()
except Exception:
self.driver.execute_script("arguments[0].click();", find_provider)
print("[BCBS MA step2] Find Provider clicked, waiting...")
time.sleep(3)
# 2. Payer search options → value="2": Member ID, Subscriber Date Of Birth
payer_select = self._wait(10).until(
EC.presence_of_element_located((By.ID, "payerSearchOptions"))
)
Select(payer_select).select_by_value("2")
print("[BCBS MA step2] Payer search option selected: Member ID + DOB")
time.sleep(1)
# 3. Service type → value="33": Dental Care [35]
service_select = self._wait(10).until(
EC.presence_of_element_located((By.ID, "serviceType"))
)
Select(service_select).select_by_value("33")
print("[BCBS MA step2] Service type selected: Dental Care [35]")
# 4. Place of service → value="30": OFFICE [11]
pos_select = self._wait(10).until(
EC.presence_of_element_located((By.ID, "placeOfService"))
)
Select(pos_select).select_by_value("30")
print("[BCBS MA step2] Place of service selected: OFFICE [11]")
# 5. Member ID — field name is subscriberMedicaidID
member_input = self._wait(10).until(
EC.presence_of_element_located((By.XPATH,
"//input[@name='subscriberMedicaidID' or @id='subscriberMedicaidID']"
))
)
member_input.clear()
member_input.send_keys(self.member_id)
print(f"[BCBS MA step2] Member ID entered: {self.member_id}")
# 6. Date of birth — id="subscriberDateOfBirth", placeholder="mm/dd/yyyy"
dob_formatted = self._dob_mmddyyyy()
dob_input = self._wait(10).until(
EC.presence_of_element_located((By.ID, "subscriberDateOfBirth"))
)
# Double-click to focus, then type directly via ActionChains
ActionChains(self.driver).double_click(dob_input).perform()
time.sleep(0.3)
ActionChains(self.driver).send_keys(dob_formatted).perform()
print(f"[BCBS MA step2] DOB typed: {dob_formatted}")
time.sleep(1)
# 7. Click Submit
submit_btn = self._wait(10).until(
EC.element_to_be_clickable((By.XPATH,
"//button[@ng-click='submit()' and @type='submit']"
))
)
submit_btn.click()
print("[BCBS MA step2] Submit clicked, waiting for results...")
time.sleep(5)
# Wait for results page to load
self._wait(20).until(
EC.presence_of_element_located((By.XPATH,
"//*[contains(text(),'Eligible') or contains(text(),'Not Eligible') or "
"contains(text(),'Active') or contains(text(),'Inactive') or "
"contains(text(),'Coverage') or contains(text(),'Benefit')]"
))
)
print("[BCBS MA step2] Results page loaded")
# Click Expand All to expand all eligibility sections before PDFing
self._expand_all_sections()
# Extract eligibility status and patient name from live page text
page_text = self.driver.find_element(By.TAG_NAME, "body").text
text_lower = page_text.lower()
if "not eligible" in text_lower or "inactive" in text_lower or "terminated" in text_lower:
eligibility = "Not Eligible"
elif "eligible" in text_lower or "active" in text_lower or "covered" in text_lower:
eligibility = "Eligible"
else:
eligibility = "Unknown"
# Extract first/last name from Patient Information column only.
# "Relationship:" is unique to the Patient column (not in Subscriber column).
# Use it as anchor: grab text from "Relationship:" up to "Member ID:" (Subscriber starts there).
import re
first_name = self.first_name
last_name = self.last_name
street = city = state = zip_code = gender = ""
try:
# Slice out just the Patient Information section
patient_match = re.search(
r"Relationship:(.+?)(?=Member ID:|Subscriber Information|Plan Name:)",
page_text,
re.DOTALL
)
if patient_match:
patient_text = patient_match.group(1)
print(f"[BCBS MA step2] Patient section:\n{patient_text[:200]}")
lines = patient_text.split("\n")
capturing_last = False
for line in lines:
stripped = line.strip()
if "First Name:" in stripped and not first_name:
val = stripped.split("First Name:", 1)[1].strip()
val = val.split("Middle Name:")[0].split("Last Name:")[0].strip()
if val:
first_name = val
elif "Last Name:" in stripped and not last_name:
val = stripped.split("Last Name:", 1)[1].strip()
val = val.split("SSN:")[0].split("Date of Birth:")[0].strip()
if val:
last_name = val
capturing_last = True
elif capturing_last:
if stripped and ":" not in stripped and stripped == stripped.upper():
last_name += " " + stripped
capturing_last = False
if first_name and last_name and not capturing_last:
break
# Address — "Street: 1041 MADISON PLACE" / "City State Zip: SOUTHBOROUGH MA 01772"
street_match = re.search(r"Street:[ \t]*([^\n]*)", patient_text)
if street_match:
street = street_match.group(1).strip()
csz_match = re.search(
r"City State Zip:\s*(.+?)\s+([A-Z]{2})\s+(\d{5}(?:-?\d{4})?)",
patient_text
)
if csz_match:
city = csz_match.group(1).strip()
state = csz_match.group(2)
zip_code = csz_match.group(3)
print(f"[BCBS MA step2] Extracted address — Street: '{street}', "
f"City: '{city}', State: '{state}', Zip: '{zip_code}'")
# Gender — "Gender: M"
gender_match = re.search(r"Gender:[ \t]*([^\n]*)", patient_text)
if gender_match:
gender = gender_match.group(1).strip()
print(f"[BCBS MA step2] Extracted gender: '{gender}'")
else:
print("[BCBS MA step2] Patient section not found in page text")
print(f"[BCBS MA step2] Extracted — First: '{first_name}', Last: '{last_name}'")
except Exception as e:
print(f"[BCBS MA step2] Name extraction failed: {e}")
# PDF the results page via CDP
pdf_base64 = ""
pdf_path = None
try:
result = self.driver.execute_cdp_cmd("Page.printToPDF", {
"printBackground": True,
"paperWidth": 8.5,
"paperHeight": 11,
"marginTop": 0.5,
"marginBottom": 0.5,
"marginLeft": 0.5,
"marginRight": 0.5,
})
pdf_data = result.get("data", "")
if pdf_data:
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
filename = f"bcbs_ma_eligibility_{self.member_id}_{int(time.time())}.pdf"
pdf_path = os.path.join(DOWNLOAD_DIR, filename)
with open(pdf_path, "wb") as f:
f.write(base64.b64decode(pdf_data))
pdf_base64 = pdf_data
print(f"[BCBS MA step2] PDF saved: {pdf_path}")
except Exception as e:
print(f"[BCBS MA step2] PDF generation failed: {e}")
patient_name = f"{first_name} {last_name}".strip()
print(f"[BCBS MA step2] Eligibility: {eligibility}, Patient: {patient_name}")
return {
"status": "success",
"eligibility": eligibility,
"patientName": patient_name,
"firstName": first_name,
"lastName": last_name,
"memberId": self.member_id,
"address": street,
"city": city,
"state": state,
"zipCode": zip_code,
"gender": gender,
"insurerName": "BCBS MA",
"pdfBase64": pdf_base64,
"pdf_path": pdf_path,
}
except Exception as e:
print(f"[BCBS MA step2] Error: {e}")
return {"status": "error", "message": f"step2 failed: {e}"}