From 3ac185b0ec56b7e3aa26decd0a60ba041951f7f6 Mon Sep 17 00:00:00 2001 From: ff Date: Mon, 1 Jun 2026 23:51:11 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20BCBS=20MA=20=E2=80=94=20identify=20patie?= =?UTF-8?q?nt=20by=20member=20ID=20+=20DOB,=20prevent=20overwriting=20subs?= =?UTF-8?q?criber?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added getPatientByInsuranceIdAndDob to storage - Processor uses insuranceId + DOB as unique key instead of insuranceId alone - Dependent with same subscriber ID but different DOB gets a new patient record (bypasses createOrUpdatePatientByInsuranceId which would overwrite subscriber) - Name extraction anchored to "Relationship:" to scope Patient Info column only Co-Authored-By: Claude Sonnet 4.6 --- .../processors/bcbsMaEligibilityProcessor.ts | 41 +++++++++++- apps/Backend/src/storage/patients-storage.ts | 14 +++++ ...selenium_BCBS_MA_eligibilityCheckWorker.py | 63 ++++++++++--------- 3 files changed, 87 insertions(+), 31 deletions(-) diff --git a/apps/Backend/src/queue/processors/bcbsMaEligibilityProcessor.ts b/apps/Backend/src/queue/processors/bcbsMaEligibilityProcessor.ts index 99128d9c..cf736274 100644 --- a/apps/Backend/src/queue/processors/bcbsMaEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/bcbsMaEligibilityProcessor.ts @@ -63,10 +63,45 @@ async function processBcbsMaResult( ? seleniumResult.lastName.trim() : (formLastName ?? ""); - await createOrUpdatePatientByInsuranceId({ insuranceId, firstName, lastName, dob: formDob, userId }); - const normalizedInsuranceId = insuranceId.replace(/\s+/g, ""); - const patient = await storage.getPatientByInsuranceId(normalizedInsuranceId); + + // Identify patient by member ID + DOB — not member ID alone. + // Dependents (e.g. Maria) share the subscriber's member ID but have a different DOB. + const dobDate = formDob ? new Date(formDob) : null; + + let patient = dobDate + ? await storage.getPatientByInsuranceIdAndDob(normalizedInsuranceId, dobDate) + : await storage.getPatientByInsuranceId(normalizedInsuranceId); + + if (patient) { + // Existing patient found by insuranceId + DOB — update name + log("bcbs-ma-processor", `Patient matched by insuranceId+DOB id=${patient.id}`); + await storage.updatePatient(patient.id, { firstName, lastName }); + } else { + // No match — create a brand new patient record. + // Do NOT use createOrUpdatePatientByInsuranceId here because it looks up by + // insuranceId alone and would overwrite the subscriber (e.g. Hugo) instead of + // creating a new record for the dependent (e.g. Maria). + log("bcbs-ma-processor", "No patient matched by insuranceId+DOB — creating new patient record"); + try { + await storage.createPatient({ + firstName, + lastName, + insuranceId: normalizedInsuranceId, + dateOfBirth: dobDate ?? undefined, + gender: "", + phone: "", + userId, + } as any); + } catch (createErr: any) { + log("bcbs-ma-processor", `createPatient failed: ${createErr?.message}`, createErr); + throw createErr; + } + patient = dobDate + ? await storage.getPatientByInsuranceIdAndDob(normalizedInsuranceId, dobDate) + : await storage.getPatientByInsuranceId(normalizedInsuranceId); + } + if (!patient?.id) { output.patientUpdateStatus = "Patient not found; no update performed"; return output; diff --git a/apps/Backend/src/storage/patients-storage.ts b/apps/Backend/src/storage/patients-storage.ts index afc973e5..82a49aa4 100755 --- a/apps/Backend/src/storage/patients-storage.ts +++ b/apps/Backend/src/storage/patients-storage.ts @@ -10,6 +10,7 @@ export interface IStorage { // Patient methods getPatient(id: number): Promise; getPatientByInsuranceId(insuranceId: string): Promise; + getPatientByInsuranceIdAndDob(insuranceId: string, dob: Date): Promise; getAllPatients(): Promise; getRecentPatients(limit: number, offset: number): Promise; getPatientsByIds(ids: number[]): Promise; @@ -60,6 +61,19 @@ export const patientsStorage: IStorage = { }); }, + async getPatientByInsuranceIdAndDob(insuranceId: string, dob: Date): Promise { + const startOfDay = new Date(dob); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(dob); + endOfDay.setHours(23, 59, 59, 999); + return db.patient.findFirst({ + where: { + insuranceId, + dateOfBirth: { gte: startOfDay, lte: endOfDay }, + }, + }); + }, + async getRecentPatients(limit: number, offset: number): Promise { return db.patient.findMany({ skip: offset, diff --git a/apps/SeleniumService/selenium_BCBS_MA_eligibilityCheckWorker.py b/apps/SeleniumService/selenium_BCBS_MA_eligibilityCheckWorker.py index 68277833..3c7cc314 100644 --- a/apps/SeleniumService/selenium_BCBS_MA_eligibilityCheckWorker.py +++ b/apps/SeleniumService/selenium_BCBS_MA_eligibilityCheckWorker.py @@ -424,39 +424,46 @@ class AutomationBCBSMAEligibilityCheck: else: eligibility = "Unknown" - # Extract first/last name from DOM — scope to "Patient Information" column only - # to avoid picking up the duplicate values in the Subscriber Information column. + # 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 try: - # Find the "Patient Information" header, then search within its parent container - patient_section = self.driver.find_element(By.XPATH, - "//*[normalize-space(text())='Patient Information']/ancestor::*[3]" + # Slice out just the Patient Information section + patient_match = re.search( + r"Relationship:(.+?)(?=Member ID:|Subscriber Information|Plan Name:)", + page_text, + re.DOTALL ) - section_text = patient_section.text - print(f"[BCBS MA step2] Patient section text:\n{section_text[:300]}") + if patient_match: + patient_text = patient_match.group(1) + print(f"[BCBS MA step2] Patient section:\n{patient_text[:200]}") - lines = section_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].split("Member ID:")[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 + 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 + 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: