Files
DentalManagementMH07/apps/Backend/src/queue/processors/_shared.ts
Gitead e26ebf7fd5 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>
2026-05-01 21:40:04 -04:00

161 lines
5.3 KiB
TypeScript

/**
* Shared utilities used by all job processors.
* Avoids duplicating helpers that currently live inside route files.
*/
import axios from "axios";
import PDFDocument from "pdfkit";
import fsSync from "fs";
import { storage } from "../../storage";
import {
InsertPatient,
insertPatientSchema,
} from "../../../../../packages/db/types/patient-types";
const SELENIUM_BASE_URL =
process.env.SELENIUM_AGENT_BASE_URL || "http://localhost:5002";
// ---------------------------------------------------------------------------
// Python service helpers
// ---------------------------------------------------------------------------
/**
* Call a synchronous Python service endpoint and return its result.
* The Python service runs the selenium job inline and returns the result directly.
* Timeout is long (8 min) to accommodate slow selenium flows.
*/
export async function callPythonSync(
endpoint: string,
payload: any,
timeoutMs = 8 * 60 * 1000
): Promise<any> {
const resp = await axios.post(
`${SELENIUM_BASE_URL}${endpoint}`,
payload,
{ timeout: timeoutMs }
);
const data = resp.data;
if (data?.status === "error") {
const msg =
typeof data.message === "string"
? data.message
: data.message?.msg ?? "Selenium returned error status";
throw new Error(msg);
}
return data;
}
// ---------------------------------------------------------------------------
// General utilities
// ---------------------------------------------------------------------------
export function sleep(ms: number) {
return new Promise<void>((r) => setTimeout(r, ms));
}
export function splitName(fullName?: string | null) {
if (!fullName) return { firstName: "", lastName: "" };
const parts = fullName.trim().split(/\s+/).filter(Boolean);
const firstName = parts.shift() ?? "";
const lastName = parts.join(" ") ?? "";
return { firstName, lastName };
}
export async function imageToPdfBuffer(imagePath: string): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
try {
const doc = new PDFDocument({ autoFirstPage: false });
const chunks: Uint8Array[] = [];
doc.on("data", (c: any) => chunks.push(c));
doc.on("end", () => resolve(Buffer.concat(chunks)));
doc.on("error", reject);
const A4_W = 595.28;
const A4_H = 841.89;
doc.addPage({ size: [A4_W, A4_H] });
doc.image(imagePath, 0, 0, { fit: [A4_W, A4_H], align: "center", valign: "center" });
doc.end();
} catch (e) {
reject(e);
}
});
}
// ---------------------------------------------------------------------------
// Patient DB helpers
// ---------------------------------------------------------------------------
export async function createOrUpdatePatientByInsuranceId(options: {
insuranceId: string;
firstName?: string | null;
lastName?: string | null;
dob?: string | Date | null;
userId: number;
}) {
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();
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 = {};
if (incomingFirst && String(patient.firstName ?? "").trim() !== incomingFirst)
updates.firstName = incomingFirst;
if (incomingLast && String(patient.lastName ?? "").trim() !== incomingLast)
updates.lastName = incomingLast;
if (dob && !patient.dateOfBirth) {
const parsed = new Date(dob);
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(normalizedId);
}
return patient;
}
const createPayload: any = {
firstName: incomingFirst,
lastName: incomingLast,
dateOfBirth: dob,
gender: "",
phone: "",
userId,
insuranceId: normalizedId,
};
let patientData: InsertPatient;
try {
patientData = insertPatientSchema.parse(createPayload);
} catch (e1) {
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 1):`, e1);
const safePayload = { ...createPayload };
delete safePayload.dateOfBirth;
try {
patientData = insertPatientSchema.parse(safePayload);
} catch (e2) {
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 2):`, e2);
patientData = safePayload as InsertPatient;
}
}
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);
}