fix: BCBS MA — identify patient by member ID + DOB, prevent overwriting subscriber
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -63,10 +63,45 @@ async function processBcbsMaResult(
|
|||||||
? seleniumResult.lastName.trim()
|
? seleniumResult.lastName.trim()
|
||||||
: (formLastName ?? "");
|
: (formLastName ?? "");
|
||||||
|
|
||||||
await createOrUpdatePatientByInsuranceId({ insuranceId, firstName, lastName, dob: formDob, userId });
|
|
||||||
|
|
||||||
const normalizedInsuranceId = insuranceId.replace(/\s+/g, "");
|
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) {
|
if (!patient?.id) {
|
||||||
output.patientUpdateStatus = "Patient not found; no update performed";
|
output.patientUpdateStatus = "Patient not found; no update performed";
|
||||||
return output;
|
return output;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface IStorage {
|
|||||||
// Patient methods
|
// Patient methods
|
||||||
getPatient(id: number): Promise<Patient | undefined>;
|
getPatient(id: number): Promise<Patient | undefined>;
|
||||||
getPatientByInsuranceId(insuranceId: string): Promise<Patient | null>;
|
getPatientByInsuranceId(insuranceId: string): Promise<Patient | null>;
|
||||||
|
getPatientByInsuranceIdAndDob(insuranceId: string, dob: Date): Promise<Patient | null>;
|
||||||
getAllPatients(): Promise<Patient[]>;
|
getAllPatients(): Promise<Patient[]>;
|
||||||
getRecentPatients(limit: number, offset: number): Promise<Patient[]>;
|
getRecentPatients(limit: number, offset: number): Promise<Patient[]>;
|
||||||
getPatientsByIds(ids: number[]): Promise<Patient[]>;
|
getPatientsByIds(ids: number[]): Promise<Patient[]>;
|
||||||
@@ -60,6 +61,19 @@ export const patientsStorage: IStorage = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getPatientByInsuranceIdAndDob(insuranceId: string, dob: Date): Promise<Patient | null> {
|
||||||
|
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<Patient[]> {
|
async getRecentPatients(limit: number, offset: number): Promise<Patient[]> {
|
||||||
return db.patient.findMany({
|
return db.patient.findMany({
|
||||||
skip: offset,
|
skip: offset,
|
||||||
|
|||||||
@@ -424,19 +424,24 @@ class AutomationBCBSMAEligibilityCheck:
|
|||||||
else:
|
else:
|
||||||
eligibility = "Unknown"
|
eligibility = "Unknown"
|
||||||
|
|
||||||
# Extract first/last name from DOM — scope to "Patient Information" column only
|
# Extract first/last name from Patient Information column only.
|
||||||
# to avoid picking up the duplicate values in the Subscriber Information column.
|
# "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
|
first_name = self.first_name
|
||||||
last_name = self.last_name
|
last_name = self.last_name
|
||||||
try:
|
try:
|
||||||
# Find the "Patient Information" header, then search within its parent container
|
# Slice out just the Patient Information section
|
||||||
patient_section = self.driver.find_element(By.XPATH,
|
patient_match = re.search(
|
||||||
"//*[normalize-space(text())='Patient Information']/ancestor::*[3]"
|
r"Relationship:(.+?)(?=Member ID:|Subscriber Information|Plan Name:)",
|
||||||
|
page_text,
|
||||||
|
re.DOTALL
|
||||||
)
|
)
|
||||||
section_text = patient_section.text
|
if patient_match:
|
||||||
print(f"[BCBS MA step2] Patient section text:\n{section_text[:300]}")
|
patient_text = patient_match.group(1)
|
||||||
|
print(f"[BCBS MA step2] Patient section:\n{patient_text[:200]}")
|
||||||
|
|
||||||
lines = section_text.split("\n")
|
lines = patient_text.split("\n")
|
||||||
capturing_last = False
|
capturing_last = False
|
||||||
for line in lines:
|
for line in lines:
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
@@ -447,7 +452,7 @@ class AutomationBCBSMAEligibilityCheck:
|
|||||||
first_name = val
|
first_name = val
|
||||||
elif "Last Name:" in stripped and not last_name:
|
elif "Last Name:" in stripped and not last_name:
|
||||||
val = stripped.split("Last Name:", 1)[1].strip()
|
val = stripped.split("Last Name:", 1)[1].strip()
|
||||||
val = val.split("SSN:")[0].split("Date of Birth:")[0].split("Member ID:")[0].strip()
|
val = val.split("SSN:")[0].split("Date of Birth:")[0].strip()
|
||||||
if val:
|
if val:
|
||||||
last_name = val
|
last_name = val
|
||||||
capturing_last = True
|
capturing_last = True
|
||||||
@@ -457,6 +462,8 @@ class AutomationBCBSMAEligibilityCheck:
|
|||||||
capturing_last = False
|
capturing_last = False
|
||||||
if first_name and last_name and not capturing_last:
|
if first_name and last_name and not capturing_last:
|
||||||
break
|
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}'")
|
print(f"[BCBS MA step2] Extracted — First: '{first_name}', Last: '{last_name}'")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user