feat: fix DDMA eligibility — patient list, name extraction, PDF page, OTP session

- Filter patient list by userId so each user sees only their own patients
- Sort patients by updatedAt DESC so recently checked patients appear first
- Add updatedAt field to Patient model (DB migration via raw SQL + db:generate)
- Fix DDMA name extraction: read from detail page "Name:" label, not search
  results row text which included appended dates
- Fix PDF capture: use driver.get() instead of click() to avoid race condition
  that was saving the search results page instead of the patient detail page
- Strip trailing bare dates from extracted names (e.g. "Rodriguez 04/27/2026")
- Handle "Last, First" comma format and single-word last names in splitName
- Normalize insuranceId consistently in createOrUpdatePatientByInsuranceId
- Fix OTP persistent session: stop clearing LocalStorage/IndexedDB on startup
  (these hold the DDMA device trust token that skips OTP on subsequent logins)
- Increase post-navigation wait time for full page render before PDF generation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-01 21:40:04 -04:00
parent 24bbaed6ab
commit e26ebf7fd5
213 changed files with 1698 additions and 1425 deletions

View File

@@ -94,10 +94,16 @@ export async function createOrUpdatePatientByInsuranceId(options: {
const { insuranceId, firstName, lastName, dob, userId } = options;
if (!insuranceId) throw new Error("Missing insuranceId");
// Normalize insuranceId the same way insertPatientSchema does (strip spaces)
const normalizedId = insuranceId.replace(/\s+/g, "");
const incomingFirst = (firstName || "").trim();
const incomingLast = (lastName || "").trim();
let patient = await storage.getPatientByInsuranceId(insuranceId);
console.log(`[createOrUpdatePatient] insuranceId="${normalizedId}" firstName="${incomingFirst}" lastName="${incomingLast}" userId=${userId}`);
let patient = await storage.getPatientByInsuranceId(normalizedId);
console.log(`[createOrUpdatePatient] existing patient lookup: ${patient ? `found id=${patient.id}` : "not found"}`);
if (patient && patient.id) {
const updates: any = {};
@@ -110,8 +116,9 @@ export async function createOrUpdatePatientByInsuranceId(options: {
if (!isNaN(parsed.getTime())) updates.dateOfBirth = parsed;
}
if (Object.keys(updates).length > 0) {
console.log(`[createOrUpdatePatient] updating patient id=${patient.id} with`, updates);
await storage.updatePatient(patient.id, updates);
patient = await storage.getPatientByInsuranceId(insuranceId);
patient = await storage.getPatientByInsuranceId(normalizedId);
}
return patient;
}
@@ -123,24 +130,31 @@ export async function createOrUpdatePatientByInsuranceId(options: {
gender: "",
phone: "",
userId,
insuranceId,
insuranceId: normalizedId,
};
let patientData: InsertPatient;
try {
patientData = insertPatientSchema.parse(createPayload);
} catch {
// Remove fields that may fail validation (invalid date or alphanumeric insuranceId)
} catch (e1) {
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 1):`, e1);
const safePayload = { ...createPayload };
delete safePayload.dateOfBirth;
try {
patientData = insertPatientSchema.parse(safePayload);
} catch {
// Last resort: skip schema validation and cast directly
} catch (e2) {
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 2):`, e2);
patientData = safePayload as InsertPatient;
}
}
await storage.createPatient(patientData);
return storage.getPatientByInsuranceId(insuranceId);
try {
await storage.createPatient(patientData);
console.log(`[createOrUpdatePatient] patient created successfully for insuranceId="${normalizedId}"`);
} catch (dbErr: any) {
console.error(`[createOrUpdatePatient] DB create failed:`, dbErr?.message ?? dbErr);
throw dbErr;
}
return storage.getPatientByInsuranceId(normalizedId);
}

View File

@@ -90,9 +90,33 @@ async function processDdmaResult(
? seleniumResult.patientName.trim()
: null;
const { firstName, lastName } = rawName
? splitName(rawName)
: { firstName: formFirstName ?? "", lastName: formLastName ?? "" };
let firstName: string;
let lastName: string;
if (rawName) {
// Strip trailing bare dates DDMA appends to names e.g. "Christian Rodriguez 04/27/2026"
const cleanName = rawName.replace(/\s+\d{1,2}\/\d{1,2}\/\d{2,4}$/, "").trim();
if (cleanName.includes(",")) {
// "LAST, FIRST" format common on insurance portals
const [last, ...firstParts] = cleanName.split(",").map((s: string) => s.trim());
lastName = last || formLastName || "";
firstName = firstParts.join(" ").trim() || formFirstName || "";
} else {
const parsed = splitName(cleanName);
if (!parsed.lastName) {
// Single word — treat as last name, pull first name from form
lastName = parsed.firstName || formLastName || "";
firstName = formFirstName || "";
} else {
firstName = parsed.firstName || formFirstName || "";
lastName = parsed.lastName || formLastName || "";
}
}
} else {
firstName = formFirstName ?? "";
lastName = formLastName ?? "";
}
// 2) Create / update patient
await createOrUpdatePatientByInsuranceId({
@@ -104,7 +128,9 @@ async function processDdmaResult(
});
// 3) Fetch patient (needed for ID)
const patient = await storage.getPatientByInsuranceId(insuranceId);
const normalizedInsuranceId = insuranceId.replace(/\s+/g, "");
const patient = await storage.getPatientByInsuranceId(normalizedInsuranceId);
log("ddma-processor", `patient lookup after create: ${patient ? `id=${patient.id}` : "NOT FOUND"} for insuranceId="${normalizedInsuranceId}"`);
if (!patient?.id) {
output.patientUpdateStatus = "Patient not found; no update performed";
return output;
@@ -177,6 +203,7 @@ async function processDdmaResult(
output.pdfFileId = createdPdfFileId;
return output;
} catch (err: any) {
log("ddma-processor", `processDdmaResult ERROR: ${err?.message ?? String(err)}`, err);
return {
...output,
pdfUploadStatus: