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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -24,9 +24,10 @@ router.get("/recent", async (req: Request, res: Response) => {
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
const offset = parseInt(req.query.offset as string) || 0;
|
||||
|
||||
const userId = req.user!.id;
|
||||
const [patients, totalCount] = await Promise.all([
|
||||
storage.getRecentPatients(limit, offset),
|
||||
storage.getTotalPatientCount(),
|
||||
storage.getRecentPatients(limit, offset, userId),
|
||||
storage.getTotalPatientCount(userId),
|
||||
]);
|
||||
|
||||
res.json({ patients, totalCount });
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface IStorage {
|
||||
getPatient(id: number): Promise<Patient | undefined>;
|
||||
getPatientByInsuranceId(insuranceId: string): Promise<Patient | null>;
|
||||
getPatientsByUserId(userId: number): Promise<Patient[]>;
|
||||
getRecentPatients(limit: number, offset: number): Promise<Patient[]>;
|
||||
getRecentPatients(limit: number, offset: number, userId: number): Promise<Patient[]>;
|
||||
getPatientsByIds(ids: number[]): Promise<Patient[]>;
|
||||
createPatient(patient: InsertPatient): Promise<Patient>;
|
||||
updatePatient(id: number, patient: UpdatePatient): Promise<Patient>;
|
||||
@@ -33,7 +33,7 @@ export interface IStorage {
|
||||
status: string;
|
||||
}[]
|
||||
>;
|
||||
getTotalPatientCount(): Promise<number>;
|
||||
getTotalPatientCount(userId: number): Promise<number>;
|
||||
countPatients(filters: any): Promise<number>; // optional but useful
|
||||
getPatientFinancialRows(
|
||||
patientId: number,
|
||||
@@ -59,11 +59,12 @@ export const patientsStorage: IStorage = {
|
||||
});
|
||||
},
|
||||
|
||||
async getRecentPatients(limit: number, offset: number): Promise<Patient[]> {
|
||||
async getRecentPatients(limit: number, offset: number, userId: number): Promise<Patient[]> {
|
||||
return db.patient.findMany({
|
||||
where: { userId },
|
||||
skip: offset,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -85,6 +86,7 @@ export const patientsStorage: IStorage = {
|
||||
status: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -124,7 +126,7 @@ export const patientsStorage: IStorage = {
|
||||
}) {
|
||||
return db.patient.findMany({
|
||||
where: filters,
|
||||
orderBy: { createdAt: "desc" },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
select: {
|
||||
@@ -141,8 +143,8 @@ export const patientsStorage: IStorage = {
|
||||
});
|
||||
},
|
||||
|
||||
async getTotalPatientCount(): Promise<number> {
|
||||
return db.patient.count();
|
||||
async getTotalPatientCount(userId: number): Promise<number> {
|
||||
return db.patient.count({ where: { userId } });
|
||||
},
|
||||
|
||||
async countPatients(filters: any) {
|
||||
|
||||
Reference in New Issue
Block a user