Files
DentalManagement09/apps/SeleniumService/helpers_deltains_claim.py
T
Gitead dcf2ff536a fix: close browser window after Deltains claim completes
Session still persists via the Chrome profile/saved cookies, so
closing the window doesn't force a re-login or OTP next time.
2026-09-18 14:51:56 -04:00

351 lines
14 KiB
Python

import os
import time
import asyncio
from typing import Dict, Any
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium_DeltaIns_claimSubmitWorker import AutomationDeltaInsClaimSubmit
from deltains_browser_manager import get_browser_manager
sessions: Dict[str, Dict[str, Any]] = {}
SESSION_OTP_TIMEOUT = int(os.getenv("SESSION_OTP_TIMEOUT", "240"))
def make_session_entry() -> str:
import uuid
sid = str(uuid.uuid4())
sessions[sid] = {
"status": "created",
"created_at": time.time(),
"last_activity": time.time(),
"bot": None,
"driver": None,
"otp_event": asyncio.Event(),
"otp_value": None,
"result": None,
"message": None,
}
return sid
async def cleanup_session(sid: str, message: str | None = None):
s = sessions.get(sid)
if not s:
return
try:
if s.get("status") not in ("completed", "error", "not_found"):
s["status"] = "error"
if message:
s["message"] = message
ev = s.get("otp_event")
if ev and not ev.is_set():
ev.set()
finally:
sessions.pop(sid, None)
print(f"[helpers_deltains_claim] cleaned session {sid}")
async def _remove_session_later(sid: str, delay: int = 30):
await asyncio.sleep(delay)
await cleanup_session(sid)
def _close_browser(bot):
"""Save cookies and close the browser after task completion."""
try:
bm = get_browser_manager()
try:
bm.save_cookies()
except Exception:
pass
try:
bm.quit_driver()
print("[Deltains Claim] Browser closed")
except Exception:
pass
except Exception as e:
print(f"[Deltains Claim] Could not close browser: {e}")
async def start_deltains_claim_run(sid: str, data: dict, url: str):
"""
Run the Deltains claim workflow — reuses the Okta login/OTP handling
proven in helpers_deltains_eligibility.py, then continues into
claim-specific steps mirroring helpers_ddma_claim.py's structure.
"""
s = sessions.get(sid)
if not s:
return {"status": "error", "message": "session not found"}
s["status"] = "running"
s["last_activity"] = time.time()
bot = None
try:
bot = AutomationDeltaInsClaimSubmit(data)
bot.config_driver()
s["bot"] = bot
s["driver"] = bot.driver
s["last_activity"] = time.time()
try:
bot.driver.maximize_window()
except Exception:
pass
try:
login_result = bot.login(url)
except WebDriverException as wde:
s["status"] = "error"
s["message"] = f"Selenium driver error during login: {wde}"
await cleanup_session(sid, s["message"])
_close_browser(bot)
return {"status": "error", "message": s["message"]}
except Exception as e:
s["status"] = "error"
s["message"] = f"Unexpected error during login: {e}"
await cleanup_session(sid, s["message"])
_close_browser(bot)
return {"status": "error", "message": s["message"]}
# ── Already logged in (persistent session) ────────────────────────────
if isinstance(login_result, str) and login_result == "ALREADY_LOGGED_IN":
print("[Deltains Claim] Session persisted — skipping OTP")
s["status"] = "running"
s["message"] = "Session persisted"
get_browser_manager().save_cookies()
# ── OTP required ──────────────────────────────────────────────────────
elif isinstance(login_result, str) and login_result == "OTP_REQUIRED":
s["status"] = "waiting_for_otp"
s["message"] = "OTP required — please enter the code sent to your email"
s["last_activity"] = time.time()
driver = s["driver"]
max_polls = SESSION_OTP_TIMEOUT
login_success = False
print(f"[Deltains Claim OTP] Polling for OTP completion (up to {SESSION_OTP_TIMEOUT}s)...")
for poll in range(max_polls):
await asyncio.sleep(1)
s["last_activity"] = time.time()
try:
otp_value = s.get("otp_value")
if otp_value:
print(f"[Deltains Claim OTP poll {poll+1}] OTP received from app, typing it in...")
try:
otp_input = driver.find_element(By.XPATH,
"//input[@name='credentials.passcode' and @type='text'] | "
"//input[contains(@name,'passcode')]")
otp_input.clear()
otp_input.send_keys(otp_value)
try:
verify_btn = driver.find_element(By.XPATH,
"//input[@type='submit'] | "
"//button[@type='submit']")
verify_btn.click()
except Exception:
otp_input.send_keys(Keys.RETURN)
print("[Deltains Claim OTP] OTP typed and submitted via app")
s["otp_value"] = None
await asyncio.sleep(8)
except Exception as type_err:
print(f"[Deltains Claim OTP] Failed to type OTP from app: {type_err}")
current_url = driver.current_url.lower()
if poll % 10 == 0:
print(f"[Deltains Claim OTP poll {poll+1}/{max_polls}] URL: {current_url[:80]}")
if "provider-tools" in current_url and "login" not in current_url and "ciam" not in current_url:
print("[Deltains Claim OTP] Login successful!")
login_success = True
break
except Exception as poll_err:
if poll % 10 == 0:
print(f"[Deltains Claim OTP poll {poll+1}] Error: {poll_err}")
if not login_success:
try:
current_url = driver.current_url.lower()
if "provider-tools" in current_url and "login" not in current_url and "ciam" not in current_url:
login_success = True
else:
s["status"] = "error"
s["message"] = "OTP timeout — login not completed"
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": "OTP not completed in time"}
except Exception as final_err:
s["status"] = "error"
s["message"] = f"OTP verification failed: {final_err}"
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": s["message"]}
if login_success:
s["status"] = "running"
s["message"] = "Login successful after OTP"
print("[Deltains Claim OTP] Proceeding to claim steps...")
get_browser_manager().save_cookies()
# ── Login succeeded without OTP ───────────────────────────────────────
elif isinstance(login_result, str) and login_result == "SUCCESS":
print("[Deltains Claim] Login succeeded without OTP")
s["status"] = "running"
s["message"] = "Login succeeded"
get_browser_manager().save_cookies()
# ── Login error ───────────────────────────────────────────────────────
elif isinstance(login_result, str) and login_result.startswith("ERROR"):
s["status"] = "error"
s["message"] = login_result
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": login_result}
# --- Step 1: Search patient ---
step1_result = bot.step1_search_patient()
print(f"[Deltains Claim] step1 result: {step1_result}")
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
s["status"] = "error"
s["message"] = step1_result
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": step1_result}
# --- Step 2: Select transaction type (search lands directly on the ---
# --- "Submit a claim" page, so no separate member-page/create-claim ---
# --- click is needed here, unlike the DDMA flow) ---
step2_result = bot.step2_select_transaction_type()
print(f"[Deltains Claim] step2 result: {step2_result}")
if isinstance(step2_result, str) and step2_result.startswith("ERROR"):
s["status"] = "error"
s["message"] = step2_result
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": step2_result}
# --- Step 4: Fill service date + procedure lines ---
step4_result = bot.step4_fill_claim_form()
print(f"[Deltains Claim] step4 result: {step4_result}")
if isinstance(step4_result, str) and step4_result.startswith("ERROR"):
s["status"] = "error"
s["message"] = step4_result
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": step4_result}
# --- Step 5: Attach files ---
step5_result = bot.step5_attach_files()
print(f"[Deltains Claim] step5 result: {step5_result}")
if isinstance(step5_result, str) and step5_result.startswith("ERROR"):
s["status"] = "error"
s["message"] = step5_result
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": step5_result}
# --- Step 6: Click Next step ---
step6_result = bot.step6_click_next()
print(f"[Deltains Claim] step6 result: {step6_result}")
if isinstance(step6_result, str) and step6_result.startswith("ERROR"):
s["status"] = "error"
s["message"] = step6_result
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": step6_result}
# --- Step 7: Acknowledge + Submit claim ---
step7_result = bot.step7_submit_claim()
print(f"[Deltains Claim] step7 result: {step7_result}")
if isinstance(step7_result, str) and step7_result.startswith("ERROR"):
s["status"] = "error"
s["message"] = step7_result
await cleanup_session(sid)
_close_browser(bot)
return {"status": "error", "message": step7_result}
# --- Step 8: Extract claim number + save confirmation PDF ---
step8_result = bot.step8_save_confirmation_pdf()
print(f"[Deltains Claim] step8 result: {step8_result}")
if isinstance(step8_result, str) and step8_result.startswith("ERROR"):
# Non-fatal: claim was submitted; log but don't abort
print(f"[Deltains Claim] step8 warning (non-fatal): {step8_result}")
step8_result = {}
pdf_path = step8_result.get("pdf_path") 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"[Deltains Claim] pdf_url: {pdf_url}")
claim_number = step8_result.get("claimNumber") if isinstance(step8_result, dict) else None
result = {
"status": "success",
"message": "Deltains claim submitted successfully",
"claimNumber": claim_number,
"pdf_url": pdf_url,
}
s["status"] = "completed"
s["result"] = result
s["message"] = "completed"
# Close the browser window after claim (session preserved in profile/cookies)
_close_browser(bot)
print("[Deltains Claim] Browser closed - session preserved in profile")
asyncio.create_task(_remove_session_later(sid, 60))
return result
except Exception as e:
if s:
s["status"] = "error"
s["message"] = f"worker exception: {e}"
if bot:
_close_browser(bot)
await cleanup_session(sid)
return {"status": "error", "message": f"worker exception: {e}"}
def submit_otp(sid: str, otp: str) -> Dict[str, Any]:
s = sessions.get(sid)
if not s:
return {"status": "error", "message": "session not found"}
if s.get("status") != "waiting_for_otp":
return {"status": "error", "message": f"session not waiting for otp (state={s.get('status')})"}
s["otp_value"] = otp
s["last_activity"] = time.time()
try:
s["otp_event"].set()
except Exception:
pass
return {"status": "ok", "message": "otp accepted"}
def get_session_status(sid: str) -> Dict[str, Any]:
s = sessions.get(sid)
if not s:
return {"status": "not_found"}
return {
"session_id": sid,
"status": s.get("status"),
"message": s.get("message"),
"created_at": s.get("created_at"),
"last_activity": s.get("last_activity"),
"result": s.get("result") if s.get("status") in ("completed", "error") else None,
}