feat: batch eligibility, batch claim, and batch check+claim from AI chat
- Add batch_eligibility, batch_claim, and batch_check_and_claim intents to AI classifier so multiple patients can be processed one by one - Add queue processing on insurance-status and claims pages to auto-start the next patient after each check/claim completes - Make patient schema firstName, lastName, phone optional so patients can be created with just member ID + DOB from eligibility checks - Cancel buttons now preserve chat history instead of clearing it - Patient-found card shows Check Eligibility, Eligibility & Appointment Today, and Cancel buttons - Claim service date asks user to pick between latest appointment and today when they differ - Login page subtitle styled with animated gradient Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@ import { getLlm, type AiProvider } from "./llm-factory";
|
||||
export type InternalChatIntent =
|
||||
| "check_eligibility" // by patient name → look up in DB
|
||||
| "eligibility_by_id" // by explicit memberId + dob (no name)
|
||||
| "batch_eligibility" // multiple patients by memberId + dob
|
||||
| "batch_claim" // claim same procedures for multiple patients by name
|
||||
| "batch_check_and_claim" // eligibility + claim for multiple patients by memberId+dob
|
||||
| "check_and_claim" // eligibility + claim procedures
|
||||
| "find_patient" // look up patient record only
|
||||
| "schedule_appointment" // add patient to today's (or specified) schedule
|
||||
@@ -21,6 +24,10 @@ export interface ChatClassification {
|
||||
patientName?: string; // for check_eligibility / find_patient / schedule_appointment
|
||||
memberId?: string; // for eligibility_by_id / check_and_claim
|
||||
dob?: string; // for eligibility_by_id / check_and_claim (MM/DD/YYYY)
|
||||
// --- batch eligibility (multiple patients) ---
|
||||
patients?: { memberId: string; dob: string }[]; // for batch_eligibility
|
||||
// --- batch claim (same procedures for multiple patients by name) ---
|
||||
patientNames?: string[]; // for batch_claim
|
||||
// --- insurance hint (only if explicitly stated in the message) ---
|
||||
insuranceHint?: string; // raw text, e.g. "masshealth", "BCBS", "CCA"
|
||||
// --- rendering/treating provider (only if explicitly stated, e.g. "with provider Kai Gao") ---
|
||||
@@ -48,6 +55,8 @@ Respond ONLY with valid JSON (no markdown fences):
|
||||
"patientName": "<full name if mentioned by name>",
|
||||
"memberId": "<member/insurance ID if given explicitly or found in history>",
|
||||
"dob": "<date of birth in MM/DD/YYYY if given explicitly or found in history>",
|
||||
"patients": [{"memberId": "<id>", "dob": "<MM/DD/YYYY>"}, ...],
|
||||
"patientNames": ["<name1>", "<name2>", ...],
|
||||
"insuranceHint": "<insurance name only if explicitly stated in the message, e.g. 'masshealth', 'BCBS MA', 'CCA'>",
|
||||
"renderingProvider": "<provider/doctor name only if explicitly stated, e.g. 'Kai Gao', 'Dr. Smith' — omit if not mentioned>",
|
||||
"procedureNames": ["<raw procedure name>", ...],
|
||||
@@ -61,12 +70,27 @@ Omit any field that is not present in the message or history.
|
||||
Intents:
|
||||
- check_eligibility : user wants to check insurance for a patient identified by NAME only
|
||||
e.g. "check Maria Jesus", "verify insurance for John Smith"
|
||||
- eligibility_by_id : user provides a member ID and date of birth (no patient name)
|
||||
- eligibility_by_id : user provides a SINGLE member ID and date of birth (no patient name)
|
||||
e.g. "check masshealth for 100xxxx, 10/10/1988"
|
||||
ALSO use this when user wants to check eligibility AND schedule/add an appointment on a date
|
||||
e.g. "check mh for 100xxxx, 10/10/1988 and schedule on 4/10/2026"
|
||||
e.g. "check mh for 100xxxx, 10/10/1988 and make appointment on 5/1/2026"
|
||||
In these cases set appointmentDate to the mentioned date (YYYY-MM-DD)
|
||||
- batch_eligibility : user provides MULTIPLE member IDs with dates of birth in one message
|
||||
e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000"
|
||||
e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000"
|
||||
Use this ONLY when TWO OR MORE distinct memberId+dob pairs are given.
|
||||
Put each pair into the "patients" array. Also set insuranceHint if stated.
|
||||
- batch_claim : user wants to claim the SAME procedures for MULTIPLE patients identified by NAME
|
||||
e.g. "claim perio exam and adult prophy for Jackaline and Keioson"
|
||||
e.g. "perio exam, adult cleaning for Maria and John"
|
||||
Use this ONLY when procedures AND two or more patient names are given.
|
||||
Put each patient name into the "patientNames" array. Put procedure names in "procedureNames".
|
||||
- batch_check_and_claim : user provides MULTIPLE member IDs with DOBs AND wants to claim PROCEDURES for all of them
|
||||
e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000, and claim perio exam and adult prophy"
|
||||
e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000 and claim D0120 D1110 for them"
|
||||
Use this when TWO OR MORE memberId+dob pairs are given WITH procedures.
|
||||
Put each pair into "patients" array. Put procedure names in "procedureNames".
|
||||
- check_and_claim : user wants to check eligibility AND submit PROCEDURES/BILLING as claims
|
||||
e.g. "check masshealth for 100xxxx, 10/10/1988 and claim perio exam and adult cleaning"
|
||||
e.g. "check Maria Jesus and claim D0120 D1110"
|
||||
|
||||
@@ -45,6 +45,9 @@ export interface ChatResponse {
|
||||
| "show_patient"
|
||||
| "check_eligibility_prefill"
|
||||
| "eligibility_id_ready"
|
||||
| "batch_eligibility_ready"
|
||||
| "batch_claim_ready"
|
||||
| "batch_check_and_claim_ready"
|
||||
| "check_and_claim_ready"
|
||||
| "need_insurance_clarification"
|
||||
| "appointment_created"
|
||||
@@ -314,12 +317,30 @@ export async function runInternalChatWorkflow(
|
||||
return await handleEligibilityById(classification, storage);
|
||||
}
|
||||
|
||||
// ── Batch eligibility (multiple patients) ─────────────────────────────────
|
||||
|
||||
if (intent === "batch_eligibility") {
|
||||
return await handleBatchEligibility(classification, storage);
|
||||
}
|
||||
|
||||
// ── Check eligibility + claim procedures ──────────────────────────────────
|
||||
|
||||
if (intent === "check_and_claim") {
|
||||
return await handleCheckAndClaim(classification, storage, customAliases);
|
||||
}
|
||||
|
||||
// ── Batch check & claim (eligibility + claim for multiple patients) ────────
|
||||
|
||||
if (intent === "batch_check_and_claim") {
|
||||
return await handleBatchCheckAndClaim(classification, storage, customAliases);
|
||||
}
|
||||
|
||||
// ── Batch claim (same procedures for multiple patients) ───────────────────
|
||||
|
||||
if (intent === "batch_claim") {
|
||||
return await handleBatchClaim(classification, storage, customAliases);
|
||||
}
|
||||
|
||||
// ── Claim only (no eligibility check) ─────────────────────────────────────
|
||||
|
||||
if (intent === "claim_only") {
|
||||
@@ -436,6 +457,266 @@ async function handleEligibilityById(
|
||||
};
|
||||
}
|
||||
|
||||
// ─── batch_eligibility ───────────────────────────────────────────────────────
|
||||
|
||||
async function handleBatchEligibility(
|
||||
c: ChatClassification,
|
||||
storage: StorageLike
|
||||
): Promise<ChatResponse> {
|
||||
const pairs = c.patients ?? [];
|
||||
if (pairs.length < 2) {
|
||||
// Fallback to single if somehow only 1 pair
|
||||
if (pairs.length === 1) {
|
||||
return await handleEligibilityById(
|
||||
{ ...c, memberId: pairs[0]!.memberId, dob: pairs[0]!.dob, intent: "eligibility_by_id" },
|
||||
storage
|
||||
);
|
||||
}
|
||||
return { reply: "Please provide at least two member ID + DOB pairs to batch-check." };
|
||||
}
|
||||
|
||||
const resolved: {
|
||||
memberId: string;
|
||||
dob: string;
|
||||
siteKey: string;
|
||||
autoCheck: string;
|
||||
patient: ResolvedPatient | null;
|
||||
}[] = [];
|
||||
|
||||
for (const { memberId, dob } of pairs) {
|
||||
const id = memberId?.trim();
|
||||
const d = dob?.trim();
|
||||
if (!id || !d) continue;
|
||||
|
||||
const existing = await findPatientByMemberId(id, d, storage);
|
||||
const patient: ResolvedPatient | null = existing ? patientToResult(existing) : null;
|
||||
const resolvedDob = d ?? patient?.dateOfBirth ?? null;
|
||||
if (!resolvedDob) continue;
|
||||
|
||||
const siteKey = resolveSiteKey(
|
||||
patient?.insuranceProvider ?? null,
|
||||
c.insuranceHint ?? null
|
||||
) ?? "MH";
|
||||
|
||||
resolved.push({
|
||||
memberId: id,
|
||||
dob: resolvedDob,
|
||||
siteKey,
|
||||
autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
|
||||
patient,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolved.length === 0) {
|
||||
return { reply: "Could not parse any valid member ID + DOB pairs." };
|
||||
}
|
||||
|
||||
const labels = resolved.map((r) => {
|
||||
const name = r.patient
|
||||
? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
|
||||
: `ID ${r.memberId}`;
|
||||
return name;
|
||||
});
|
||||
|
||||
return {
|
||||
reply: `Ready to check eligibility for ${resolved.length} patients: ${labels.join(", ")}.`,
|
||||
action: "batch_eligibility_ready",
|
||||
actionData: { queue: resolved },
|
||||
};
|
||||
}
|
||||
|
||||
// ─── batch_check_and_claim ────────────────────────────────────────────────────
|
||||
|
||||
async function handleBatchCheckAndClaim(
|
||||
c: ChatClassification,
|
||||
storage: StorageLike,
|
||||
customAliases: { phrase: string; cdtCode: string }[]
|
||||
): Promise<ChatResponse> {
|
||||
const pairs = c.patients ?? [];
|
||||
if (pairs.length < 2) {
|
||||
if (pairs.length === 1) {
|
||||
return await handleCheckAndClaim(
|
||||
{ ...c, memberId: pairs[0]!.memberId, dob: pairs[0]!.dob, intent: "check_and_claim" },
|
||||
storage,
|
||||
customAliases
|
||||
);
|
||||
}
|
||||
return { reply: "Please provide at least two member ID + DOB pairs." };
|
||||
}
|
||||
|
||||
const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
|
||||
if (procedureNames.length === 0) {
|
||||
return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
|
||||
}
|
||||
|
||||
const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
|
||||
const matched = cdtResults.filter((r) => r.code !== null);
|
||||
const unmatched = cdtResults.filter((r) => r.code === null);
|
||||
|
||||
if (unmatched.length > 0) {
|
||||
const phrases = unmatched.map((r) => r.input);
|
||||
return {
|
||||
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
|
||||
action: "need_cdt_clarification",
|
||||
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
|
||||
};
|
||||
}
|
||||
|
||||
const resolved: {
|
||||
memberId: string;
|
||||
dob: string;
|
||||
siteKey: string;
|
||||
autoCheck: string;
|
||||
patient: ResolvedPatient | null;
|
||||
}[] = [];
|
||||
|
||||
for (const { memberId, dob } of pairs) {
|
||||
const id = memberId?.trim();
|
||||
const d = dob?.trim();
|
||||
if (!id || !d) continue;
|
||||
|
||||
const existing = await findPatientByMemberId(id, d, storage);
|
||||
const patient: ResolvedPatient | null = existing ? patientToResult(existing) : null;
|
||||
const resolvedDob = d ?? patient?.dateOfBirth ?? null;
|
||||
if (!resolvedDob) continue;
|
||||
|
||||
const siteKey = resolveSiteKey(
|
||||
patient?.insuranceProvider ?? null,
|
||||
c.insuranceHint ?? null
|
||||
) ?? "MH";
|
||||
|
||||
resolved.push({
|
||||
memberId: id,
|
||||
dob: resolvedDob,
|
||||
siteKey,
|
||||
autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
|
||||
patient,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolved.length === 0) {
|
||||
return { reply: "Could not parse any valid member ID + DOB pairs." };
|
||||
}
|
||||
|
||||
const labels = resolved.map((r) => {
|
||||
const name = r.patient
|
||||
? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
|
||||
: `ID ${r.memberId}`;
|
||||
return name;
|
||||
});
|
||||
const codeList = matched.map((r) => `${r.code} (${r.description})`).join(", ");
|
||||
|
||||
return {
|
||||
reply: `Ready to check eligibility and claim ${codeList} for ${resolved.length} patients: ${labels.join(", ")}.`,
|
||||
action: "batch_check_and_claim_ready",
|
||||
actionData: {
|
||||
queue: resolved,
|
||||
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
|
||||
renderingProvider: c.renderingProvider ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── batch_claim ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleBatchClaim(
|
||||
c: ChatClassification,
|
||||
storage: StorageLike,
|
||||
customAliases: { phrase: string; cdtCode: string }[]
|
||||
): Promise<ChatResponse> {
|
||||
const names = c.patientNames ?? [];
|
||||
if (names.length < 2) {
|
||||
if (names.length === 1) {
|
||||
return await handleClaimOnly(
|
||||
{ ...c, patientName: names[0], intent: "claim_only" },
|
||||
storage,
|
||||
customAliases
|
||||
);
|
||||
}
|
||||
return { reply: "Please include at least two patient names to batch-claim." };
|
||||
}
|
||||
|
||||
const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
|
||||
if (procedureNames.length === 0) {
|
||||
return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
|
||||
}
|
||||
|
||||
const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
|
||||
const matched = cdtResults.filter((r) => r.code !== null);
|
||||
const unmatched = cdtResults.filter((r) => r.code === null);
|
||||
|
||||
if (unmatched.length > 0) {
|
||||
const phrases = unmatched.map((r) => r.input);
|
||||
return {
|
||||
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
|
||||
action: "need_cdt_clarification",
|
||||
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
|
||||
};
|
||||
}
|
||||
|
||||
const resolved: {
|
||||
patient: ResolvedPatient;
|
||||
siteKey: string;
|
||||
serviceDate: string | null;
|
||||
appointmentId: number | null;
|
||||
}[] = [];
|
||||
const notFound: string[] = [];
|
||||
|
||||
for (const name of names) {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) continue;
|
||||
const raw = await findPatientByName(trimmed, storage);
|
||||
if (!raw) {
|
||||
notFound.push(trimmed);
|
||||
continue;
|
||||
}
|
||||
const patient = patientToResult(raw);
|
||||
const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
|
||||
|
||||
const d1120Warning = checkD1120Age(matched, fullName, patient.dateOfBirth, c.appointmentDate);
|
||||
if (d1120Warning) return d1120Warning;
|
||||
|
||||
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
|
||||
|
||||
let serviceDate: string | null = c.appointmentDate ?? null;
|
||||
let appointmentId: number | null = null;
|
||||
if (!serviceDate) {
|
||||
const now = new Date();
|
||||
serviceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
resolved.push({ patient, siteKey, serviceDate, appointmentId });
|
||||
}
|
||||
|
||||
if (notFound.length > 0 && resolved.length === 0) {
|
||||
return { reply: `Could not find any patients matching: ${notFound.join(", ")}. Please check the spelling.` };
|
||||
}
|
||||
|
||||
const labels = resolved.map((r) =>
|
||||
`${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
|
||||
);
|
||||
const codeList = matched.map((r) => `${r.code} (${r.description})`).join(", ");
|
||||
let reply = `Ready to claim ${codeList} for ${resolved.length} patients: ${labels.join(", ")}.`;
|
||||
if (notFound.length > 0) {
|
||||
reply += ` Could not find: ${notFound.join(", ")}.`;
|
||||
}
|
||||
|
||||
return {
|
||||
reply,
|
||||
action: "batch_claim_ready",
|
||||
actionData: {
|
||||
queue: resolved.map((r) => ({
|
||||
patient: r.patient,
|
||||
siteKey: r.siteKey,
|
||||
serviceDate: r.serviceDate,
|
||||
appointmentId: r.appointmentId,
|
||||
})),
|
||||
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
|
||||
renderingProvider: c.renderingProvider ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── check_and_claim ─────────────────────────────────────────────────────────
|
||||
|
||||
async function handleCheckAndClaim(
|
||||
@@ -624,53 +905,44 @@ async function handleClaimOnly(
|
||||
let appointmentId: number | null = null;
|
||||
|
||||
if (!serviceDate) {
|
||||
const now = new Date();
|
||||
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||
|
||||
const appts = await storage.getAppointmentsByPatientId(patient.id);
|
||||
const sorted = appts.sort((a: any, b: any) =>
|
||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
if (sorted.length >= 2) {
|
||||
const d1 = new Date(sorted[0].date).getTime();
|
||||
const d2 = new Date(sorted[1].date).getTime();
|
||||
const diffDays = Math.abs(d1 - d2) / (1000 * 60 * 60 * 24);
|
||||
if (sorted.length > 0) {
|
||||
const rawDate = new Date(sorted[0].date);
|
||||
const latestStr = `${rawDate.getUTCFullYear()}-${String(rawDate.getUTCMonth() + 1).padStart(2, "0")}-${String(rawDate.getUTCDate()).padStart(2, "0")}`;
|
||||
|
||||
if (diffDays < 7) {
|
||||
// Use UTC methods to avoid local-timezone day-shift on midnight UTC dates
|
||||
if (latestStr === todayStr) {
|
||||
serviceDate = todayStr;
|
||||
appointmentId = sorted[0].id ?? null;
|
||||
} else {
|
||||
const fmtUTC = (a: any) => {
|
||||
const d = new Date(a.date);
|
||||
return `${String(d.getUTCMonth() + 1).padStart(2, "0")}/${String(d.getUTCDate()).padStart(2, "0")}/${d.getUTCFullYear()}`;
|
||||
};
|
||||
const isoUTC = (a: any) => {
|
||||
const d = new Date(a.date);
|
||||
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
||||
};
|
||||
const todayLabel = `${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}/${now.getFullYear()}`;
|
||||
return {
|
||||
reply: `Found two appointments close together for ${fullName}: ${fmtUTC(sorted[0])} and ${fmtUTC(sorted[1])}. Which date should I use for the claim?`,
|
||||
reply: `Which service date for ${fullName}?`,
|
||||
action: "need_appointment_selection",
|
||||
actionData: {
|
||||
patient,
|
||||
siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH",
|
||||
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
|
||||
options: [
|
||||
{ label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: isoUTC(sorted[0]) },
|
||||
{ label: fmtUTC(sorted[1]), appointmentId: sorted[1].id, serviceDate: isoUTC(sorted[1]) },
|
||||
{ label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: latestStr },
|
||||
{ label: `${todayLabel} (Today)`, appointmentId: null, serviceDate: todayStr },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
serviceDate = todayStr;
|
||||
}
|
||||
|
||||
if (sorted.length > 0) {
|
||||
const rawDate = new Date(sorted[0].date);
|
||||
serviceDate = `${rawDate.getUTCFullYear()}-${String(rawDate.getUTCMonth() + 1).padStart(2, "0")}-${String(rawDate.getUTCDate()).padStart(2, "0")}`;
|
||||
appointmentId = sorted[0].id ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!serviceDate) {
|
||||
// No appointment on file and no date in message — default to today
|
||||
const now = new Date();
|
||||
serviceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
|
||||
|
||||
@@ -137,7 +137,6 @@ export async function createOrUpdatePatientByInsuranceId(options: {
|
||||
updates.firstName = incomingFirst;
|
||||
if (incomingLast && String(patient.lastName ?? "").trim() !== incomingLast)
|
||||
updates.lastName = incomingLast;
|
||||
// Store DOB if not already set
|
||||
if (dobDate && !patient.dateOfBirth) updates.dateOfBirth = dobDate;
|
||||
if (Object.keys(updates).length > 0) {
|
||||
console.log(`[createOrUpdatePatient] updating patient id=${patient.id} with`, updates);
|
||||
@@ -163,15 +162,8 @@ export async function createOrUpdatePatientByInsuranceId(options: {
|
||||
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;
|
||||
}
|
||||
console.warn(`[createOrUpdatePatient] schema parse failed:`, e1);
|
||||
patientData = createPayload as InsertPatient;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user