Compare commits

..
19 Commits
Author SHA1 Message Date
Gitead b899659e13 docs: add ngrok vs Cloudflare Tunnel comparison for Twilio tunneling 2026-09-02 22:59:25 -04:00
GiteadandClaude Sonnet 5 52ddfd9f57 feat: add Remaining/Third Party columns, toggle PDF viewer, retire CMSP as separate insurance type
- Add remainingBenefit and thirdPartyInsurance columns to Patient; extract both
  from the MassHealth accumulator/member-detail pages during the eligibility
  check and surface them in the eligibility table
- Add an updated-date display and a toggle to suppress the post-check PDF
  viewer popup (default off) on the insurance eligibility page
- Rename CMSP-prefixed PDF filenames and UI copy to MH, since CMSP and MH now
  share the same worker
- Fold CMSP into MassHealth/MH across patient/claim insurance-type dropdowns
  and the chatbot's eligibility routing, since they're the same insurance
- Remove auto-download of saved eligibility PDFs; downloading is now manual

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 22:32:43 -04:00
GiteadandClaude Sonnet 5 f8235787db fix: handle DDMA site redesign in eligibility check flow
DDMA now often skips the family-members results table and navigates
straight to the single-patient detail page, and labels the patient's
name "Subscriber name:" instead of "Name:". Update step1/step2 to
detect the detail-page URL correctly (it's nested under the search
path, so a plain substring check couldn't tell them apart), extract
the name from the new label, and fall back to parsing "Coverage is:"
for eligibility status when there's no results row to read it from.
Also fix a status-text substring bug ("active" matching inside
"inactive") and stop the backend from collapsing an unknown
eligibility result into INACTIVE. Skip name extraction entirely when
the caller already knows the patient (existing-patient recheck).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 21:20:38 -04:00
GiteadandClaude Sonnet 5 b860fd213f fix: allow chatbot auto-scheduling when eligibility status is UNKNOWN
Only block appointment creation on explicit INACTIVE status, since
scrapes for some plan types (e.g. MassHealth Child Medicaid) can land
on UNKNOWN without actually finding an ineligible record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 20:39:57 -04:00
Gitead 116889dd4e perf: reduce fixed sleeps in MassHealth click_plus_button
Cuts unconditional delay when adding a service line from ~7s to ~3s
as a first pass at speeding up MassHealth claim submission.
2026-08-20 00:37:14 -04:00
GiteadandClaude Sonnet 5 ad9d311a99 fix: tolerate single-letter typos in CDT code text matching
Adds Levenshtein-based fuzzy word matching to lookupCdtCodes so
misspellings like "comprehensiv exam" resolve the same as
"comprehensive exam". Both the AI chat bot and the AI claim column
share this matcher, so the fix applies to both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 00:01:41 -04:00
GiteadandClaude Sonnet 5 374f96399e docs: add fallback keyring-disable steps for when apt purge isn't enough
D-Bus service activation can respawn gnome-keyring-daemon even after
the package is removed and PAM/autostart hooks are disabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:45:06 -04:00
GiteadandClaude Sonnet 5 e934d6bce5 chore: gitignore GCP service account key in PaymentOCRService
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:43:15 -04:00
GiteadandClaude Sonnet 5 304441d95a fix: nginx proxy_pass uses 127.0.0.1 instead of localhost
localhost resolves to ::1 first; backend/Vite only bind IPv4, causing
intermittent 502s when nginx tried the IPv6 loopback address.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:41:10 -04:00
GiteadandClaude Fable 5 5044fecb07 fix: UnitedDH selenium duplicated first attachment (multi-file input appends on send_keys)
DentalHub's upload input is a multi-file input; chromedriver send_keys
appends to the existing selection instead of replacing it, so the second
iteration re-uploaded the first file (2 attachments became 3). Clear the
input value before each send_keys and skip duplicate filePaths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 00:08:30 -04:00
GiteadandClaude Fable 5 efb85d0b92 feat: auto-confirm option for AI column claims (manual toggle + implied by Auto Claim)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 00:07:30 -04:00
ff 41bed21302 feat: send appointment confirmation SMS after AI-booked appointments
Voice-call bookings previously only spoke the confirmation during the
live call, leaving nothing for the patient afterward. Now every
successful booking (recall, reschedule, and new-patient) also sends a
standalone confirmation text, regardless of which channel the
conversation happened on.
2026-08-07 00:28:56 -04:00
ff 67e71a02e4 fix: AI recall follow-up message repeats "Great" and buries office hours
When the AI rejects a requested time in the recall booking flow, it reused
the same "Great, what time..." template used for a fresh ask, producing an
awkward double-greeting right after a rejection, and appended office hours
as a bare fragment instead of framing them as a sentence.
2026-08-07 00:07:10 -04:00
GiteadandClaude Fable 5 ff79cd1643 fix: earliest patient record wins when duplicates share a phone (inbound SMS/voice)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 23:11:55 -04:00
ffandClaude Sonnet 5 0cd095330a fix: AI recall-call flow, speech-to-text time parsing, and stale conversation state
- Reset conversation stage to a fresh start on every new voice call
  (recall and reminder alike), and on every hangup/timeout/goodbye,
  so an abandoned or completed call never leaves a patient stuck
  mid-flow for their next call or text.
- Add a staleness check to the SMS flow: after a long idle gap,
  check whether the patient's reply still reads as a continuation
  before resuming the stored stage, falling back to a friendly
  "it's been a while" re-greeting when unsure.
- Fix parseTime() to handle Twilio's speech-to-text output, which
  renders "pm"/"am" with periods and often drops the colon in a
  spoken time (e.g. "100 p.m." -> 1:00 PM) — this was silently
  failing and causing the AI to loop asking for a time it had
  already been given.
- Extend the voice Gather timeout so the AI doesn't hang up on a
  patient who just needs a few seconds to respond.
- Reword the recall-call opening line and rework the post-"yes"
  prompt to ask for day and time together.
- Fix rclone WebDAV server to serve only the backups folder instead
  of the entire backend app directory (was exposing .env and source).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 09:35:48 -04:00
ff 55ce52f276 fix: patient language field silently dropped by stale browser-side validation schema
packages/db/usedSchemas/browser.ts (the browser-resolved copy of the Patient
zod schema, kept separate from the Node copy since it can't import
Prisma-generated code) was missing several fields added to the real schema
over time — preferredLanguage, gender, insuranceProvider, insuranceId,
groupNumber, policyHolder, and the PLAN_NOT_ACCEPTED status. Since the schema
isn't strict, those fields were silently stripped from every patient
create/update in the browser with no validation error, so edits to them
never reached the server.

Also fixes the AI call greeting always playing in English regardless of the
patient's preferred language — the greeting text was a single hardcoded
string, unlike every later turn in the conversation which already branched
by language.
2026-08-05 21:40:53 -04:00
ffandClaude Sonnet 5 cef928be97 feat: add AI recall calling — books existing patients for routine cleanings
Adds a new recall conversation flow (recall-graph.ts) triggered from the
Patient Connection "AI Call" button: offers an existing patient a routine
exam & cleaning, classifies their yes/no reply, then books directly onto
the schedule (Column A, 30-minute slots, real conflict checking against
office hours and existing appointments) instead of only capturing a
preference for staff to follow up on.

Also adds a "Voice call" label in the message thread to distinguish call
transcripts from SMS, and updates the Call Templates settings UI with an
editable recall greeting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 23:13:09 -04:00
ffandClaude Sonnet 5 06dc63c53e feat: unify AI voice calls with SMS conversation rules, add Call Templates
AI phone calls now route through the same routePatientTurn() rule engine
as SMS (office-hours checks, slot-conflict checks, MassHealth eligibility,
staff escalation) instead of a freeform LLM chat, and speak in the
patient's preferred language (English/Spanish). Adds a separate "Call
Templates" settings section so spoken wording can be customized
independently from SMS wording.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 23:39:31 -04:00
ffandClaude Sonnet 5 089e94ba88 feat: add AI-driven phone calls to patient connection
Adds an "AI Call" option alongside Call/SMS/Chat that places an outbound
call where Lisa converses live with the patient via Twilio speech
gather + TTS, using the same conversational AI as chat. Includes a
separate, user-editable Call Template (Settings > AI Chat/Call Settings)
so the call greeting can be customized independently of SMS templates.

Also fixes the outbound-call webhook URL to always use the fixed public
Twilio hostname (CLOUDFLARE_HOST) instead of deriving it from the
triggering request, since staff-browser requests arrive over the
LAN-only hostname, which Twilio can never reach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 20:14:09 -04:00
122 changed files with 2910 additions and 864 deletions
+59
View File
@@ -81,6 +81,43 @@ Reboot to confirm the popup no longer appears.
> Trade-off: Chrome saved passwords/cookie encryption falls back to its weaker "Basic" store, and NetworkManager Wi-Fi passwords may need to be re-entered once. If a future setup needs the keyring kept, unlock it silently instead by opening **Passwords and Keys** (`seahorse`) → right-click **Login** → **Change Password** → set both new-password fields blank.
#### If the popup (or a "choose a new password for keyring" prompt) still appears after a reboot
The `apt purge` above removes the package, but if something reinstalls `gnome-keyring`/`libpam-gnome-keyring` as a dependency later (e.g. installing another desktop app), it comes back. Deleting just the keyring files under `~/.local/share/keyrings/` doesn't fix it either — a new keyring gets created on the next login/app request and prompts for a password again.
Do this instead, which disables it at every level (PAM login hook, session autostart, and D-Bus on-demand activation) without needing to uninstall the package:
```sh
# 1. Disable PAM hooks (login + password-sync)
sudo sed -i \
-e 's/^-auth optional pam_gnome_keyring.so/#-auth optional pam_gnome_keyring.so/' \
-e 's/^-session optional pam_gnome_keyring.so auto_start/#-session optional pam_gnome_keyring.so auto_start/' \
/etc/pam.d/sddm
sudo sed -i 's/^password\toptional\tpam_gnome_keyring.so/#&/' /etc/pam.d/common-password
# 2. Stop the keyring daemon components from autostarting in the session
mkdir -p ~/.config/autostart
for f in gnome-keyring-pkcs11 gnome-keyring-secrets gnome-keyring-ssh; do
cp /etc/xdg/autostart/$f.desktop ~/.config/autostart/$f.desktop 2>/dev/null
echo "Hidden=true" >> ~/.config/autostart/$f.desktop
done
# 3. Mask D-Bus service activation (the part that keeps bringing it back —
# any app calling the Secret Service API, e.g. Chrome, spawns the daemon
# on demand even with PAM and autostart disabled)
sudo mkdir -p /usr/share/dbus-1/services-disabled
sudo mv /usr/share/dbus-1/services/org.freedesktop.secrets.service \
/usr/share/dbus-1/services/org.gnome.keyring.service \
/usr/share/dbus-1/services/org.freedesktop.impl.portal.Secret.service \
/usr/share/dbus-1/services-disabled/ 2>/dev/null
# 4. Kill the running daemon and clear existing keyring files
pkill -f gnome-keyring-daemon
rm -f ~/.local/share/keyrings/*
```
Reboot to confirm. Same trade-off as above: Chrome falls back to storing saved passwords unencrypted rather than via the Secret Service.
### Step 2 — Install Git
```sh
@@ -1064,4 +1101,26 @@ mkdir -p /home/ff/.claude/projects/-home-ff-Desktop-DentalManagementMH06/memory/
cp -r /media/usb/claude-memory-backup/* /home/ff/.claude/projects/-home-ff-Desktop-DentalManagementMH06/memory/
```
---
## ngrok vs. Cloudflare Tunnel (for exposing Twilio webhooks/streams)
Twilio just needs a public HTTPS/WSS endpoint to reach — either tunnel provider works, and neither
is required by name. Trade-offs when choosing between them for the public tunnel (`CLOUDFLARE_HOST`
today; the code doesn't actually validate it's a Cloudflare domain, it's just named that way):
| | Cloudflare Tunnel (current setup) | ngrok |
|---|---|---|
| **Fixed hostname** | Yes — same subdomain forever, survives reboots | Only on a paid static domain; **free tier assigns a new random subdomain every time the `ngrok` process restarts** (machine reboot, crash, manual relaunch) |
| **Auto-start on boot** | Already set up via `systemctl enable cloudflared` | Possible (systemd service), but only fixes the tunnel coming back up — it does **not** stop the free-tier URL from changing on each restart |
| **After a URL change** | N/A — doesn't happen | Must update `CLOUDFLARE_HOST` in `.env`, restart the backend, and update the URL(s) configured in the Twilio Console |
| **Setup cost** | One-time per office (already documented above) | Quick to spin up for local dev/testing |
| **Latency to Twilio** | No reliable general winner — depends on actual network path (nearest PoP, peering) for this specific office, not the provider's name. Cloudflare's edge network is larger/more distributed in general, but the only way to know for a given office is to measure real round-trip time, not assume | |
| **Best for** | Production / long-running office deployments | Local dev/testing, or short-lived debugging sessions |
**Bottom line:** ngrok is fine for local development, but for an office's live Twilio integration
(webhooks and any Media Stream WebSocket), the Cloudflare Tunnel setup above is preferred because
its hostname doesn't change across reboots — avoiding the reconfigure-`.env`-and-Twilio-Console
cycle that free-tier ngrok requires every time the tunnel restarts.
The memory is plain markdown files and can also be copied manually via a USB drive or file manager. Enable "show hidden files" (Ctrl+H) in the file manager to see the `.claude` folder.
+9
View File
@@ -3,6 +3,9 @@ import { prisma as db } from "@repo/db/client";
export type ConversationStage =
| "initial"
| "reminder_initial"
| "recall_initial"
| "recall_asked_datetime"
| "recall_asked_time_for_date"
| "greeted"
| "done"
| "new_patient_greeted"
@@ -81,6 +84,12 @@ export async function startRescheduleConversation(userId: number, patientId: num
await setStage(userId, patientId, "asked_reschedule_confirm");
}
// Recall: office proactively calls/texts an existing patient to offer a
// routine exam & cleaning (typically covered at no cost by insurance).
export async function startRecallConversation(userId: number, patientId: number): Promise<void> {
await setStage(userId, patientId, "recall_initial");
}
// ── Pending reschedule (in-memory — seconds-lived within a single exchange) ───
interface PendingReschedule {
+52 -3
View File
@@ -386,6 +386,48 @@ function parseRctCode(input: string): CdtMatch | null {
return { code, description: row?.Description ?? code, input, toothNumber: String(toothNum) };
}
/**
* Levenshtein edit distance between two strings.
*/
function editDistance(a: string, b: string): number {
const dp: number[][] = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
for (let i = 0; i <= a.length; i++) dp[i]![0] = i;
for (let j = 0; j <= b.length; j++) dp[0]![j] = j;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
dp[i]![j] = a[i - 1] === b[j - 1]
? dp[i - 1]![j - 1]!
: 1 + Math.min(dp[i - 1]![j]!, dp[i]![j - 1]!, dp[i - 1]![j - 1]!);
}
}
return dp[a.length]![b.length]!;
}
/**
* True if two words are the same or a plausible typo of each other
* (one edit, only for words long enough that a 1-char slip is unambiguous).
*/
function wordsMatch(a: string, b: string): boolean {
if (a === b) return true;
if (a.length < 5 || b.length < 5) return false;
return editDistance(a, b) <= 1;
}
/**
* Fuzzy lookup into ALIAS_MAP: tolerates a single typo per word
* (e.g. "comprehensiv exam" → "comprehensive exam").
*/
function fuzzyAliasLookup(cleaned: string): string | undefined {
if (ALIAS_MAP[cleaned]) return ALIAS_MAP[cleaned];
const queryWords = cleaned.split(/\s+/).filter(Boolean);
for (const key of Object.keys(ALIAS_MAP)) {
const keyWords = key.split(/\s+/).filter(Boolean);
if (keyWords.length !== queryWords.length) continue;
if (keyWords.every((w, i) => wordsMatch(w, queryWords[i]!))) return ALIAS_MAP[key];
}
return undefined;
}
/**
* Score how well a set of query tokens matches a code's description tokens.
* Each matched token contributes 1 point; shorter descriptions get a bonus
@@ -394,7 +436,14 @@ function parseRctCode(input: string): CdtMatch | null {
function score(queryTokens: string[], entry: { tokens: Set<string>; description: string }): number {
let hits = 0;
for (const t of queryTokens) {
if (entry.tokens.has(t)) hits++;
if (entry.tokens.has(t)) {
hits++;
} else if (t.length >= 5) {
// Tolerate a single typo (e.g. "comprehensiv" → "comprehensive")
for (const dt of entry.tokens) {
if (wordsMatch(t, dt)) { hits++; break; }
}
}
}
if (hits === 0) return 0;
// Tie-break: prefer shorter descriptions (more specific match)
@@ -425,8 +474,8 @@ function matchOne(input: string): CdtMatch | null {
return { code, description: row?.Description ?? code, input };
}
// Apply alias before tokenizing
const normalized = ALIAS_MAP[cleaned] ?? cleaned;
// Apply alias before tokenizing (fuzzy-tolerant of single-letter typos)
const normalized = fuzzyAliasLookup(cleaned) ?? cleaned;
const queryTokens = normalized
.replace(/[^a-z0-9\s]/g, " ")
.split(/\s+/)
+48
View File
@@ -0,0 +1,48 @@
import twilio from "twilio";
import { prisma as db } from "@repo/db/client";
import { storage } from "../storage";
const CONFIRMATION_FALLBACKS: Record<string, (dateLabel: string, time: string) => string> = {
English: (d, t) => `Confirmed! Your appointment is on ${d} at ${t}. We look forward to seeing you.`,
Spanish: (d, t) => `¡Confirmado! Su cita es el ${d} a las ${t}. Le esperamos.`,
Portuguese: (d, t) => `Confirmado! Sua consulta é em ${d} às ${t}. Aguardamos sua visita.`,
Mandarin: (d, t) => `已确认!您的预约在 ${d} ${t}。期待您的光临。`,
Cantonese: (d, t) => `已確認!您的預約喺 ${d} ${t}。期待見到您。`,
Arabic: (d, t) => `تم التأكيد! موعدك في ${d} الساعة ${t}. نتطلع لرؤيتك.`,
"Haitian Creole": (d, t) => `Konfime! Randevou ou se ${d} a ${t}. N'ap tann ou.`,
};
/**
* Sends a standalone appointment-confirmation text, independent of whichever
* channel (SMS chat or AI phone call) the booking conversation happened on —
* a voice-booked appointment would otherwise leave the patient with nothing
* to refer back to once the call ends. Swallows all errors so a notification
* failure never breaks the booking flow that already succeeded.
*/
export async function sendAppointmentConfirmationSms(
userId: number,
patientId: number,
dateLabel: string,
time: string,
lang: string,
): Promise<void> {
try {
const [twilioSettings, patient] = await Promise.all([
storage.getTwilioSettings(userId),
db.patient.findUnique({ where: { id: patientId }, select: { phone: true } }),
]);
if (!twilioSettings || !patient?.phone) return;
const build = CONFIRMATION_FALLBACKS[lang] ?? CONFIRMATION_FALLBACKS["English"]!;
const body = build(dateLabel, time);
const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
await client.messages.create({ body, from: twilioSettings.phoneNumber, to: patient.phone });
await storage.createCommunication({
patientId, userId, channel: "sms", direction: "outbound", status: "sent", body,
});
} catch {
// Silent — don't crash the booking flow over a notification failure
}
}
@@ -0,0 +1,689 @@
import twilio from "twilio";
import { prisma as db } from "@repo/db/client";
import { storage } from "../storage";
import { runReminderGraph } from "./reminder-graph";
import { runRecallGraph, runRecallBookingStep, hasDateOrTimeSignal } from "./recall-graph";
import { runNewPatientStep } from "./new-patient-graph";
import {
runRescheduleStep,
parseDateOnlyFromMessage,
parseTime,
isOfficeDayOpen,
isWithinOfficeHours,
getOfficeHoursDisplay,
timeLabel,
} from "./reschedule-graph";
import { getLlm, type AiProvider } from "./llm-factory";
import { runEligibilityProcessor } from "../queue/processors/eligibilityProcessor";
import { sendAppointmentConfirmationSms } from "./confirmationSms";
import {
getPendingReschedule, setPendingReschedule, clearPendingReschedule,
setStage, type ConversationStage,
} from "./aiHandoffStore";
// Shared, channel-agnostic implementation of the patient conversation rules
// (reminder / reschedule / new-patient graphs, plus the inline MassHealth and
// appointment-booking stages) used by both the SMS webhook and the AI voice
// call webhook, so a patient gets identical guardrails and outcomes
// regardless of channel.
export interface RoutedPatient {
id: number;
userId: number;
phone: string | null;
preferredLanguage: string | null;
}
export interface RouteTurnResult {
segments: string[];
nextStage: ConversationStage;
}
/** Get the patient's next scheduled appointment as a human-readable string. */
export async function getAppointmentDatetime(patientId: number): Promise<string> {
const today = new Date();
today.setHours(0, 0, 0, 0);
const appt = await db.appointment.findFirst({
where: { patientId, status: "scheduled", date: { gte: today } },
orderBy: { date: "asc" },
});
if (!appt) return "";
const months = ["January","February","March","April","May","June",
"July","August","September","October","November","December"];
const d = new Date(appt.date);
return `${months[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()} at ${appt.startTime}`;
}
/** Substitute {officeName} in a template string. */
export function applyOfficeName(template: string, name: string): string {
return template.replace(/\{officeName\}/g, name || "our dental office");
}
/** Save an outbound SMS message (always sms channel — used for async follow-ups). */
async function saveOutboundSms(patientId: number, body: string): Promise<void> {
await storage.createCommunication({
patientId, channel: "sms", direction: "outbound", status: "sent", body,
});
}
/** Normalize a DOB string to zero-padded MM/DD/YYYY required by MassHealth. */
function normalizeDob(raw: string): string {
const parts = raw.split(/[\/\-\.]/);
if (parts.length !== 3) return raw;
const [m, d, y] = parts;
const mm = String(parseInt(m!, 10)).padStart(2, "0");
const dd = String(parseInt(d!, 10)).padStart(2, "0");
const yyyy = y!.length === 2 ? `20${y}` : y!;
return `${mm}/${dd}/${yyyy}`;
}
/**
* Extract MassHealth Member ID and date of birth from free text.
* Tries regex first, falls back to LLM extraction.
*/
async function parseMassHealthInfo(
message: string,
apiKey: string,
provider: AiProvider = "google",
model?: string
): Promise<{ memberId: string | null; dob: string | null }> {
const idMatch = message.match(/\b(\d{8,12})\b/);
const dobMatch = message.match(/\b(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{2,4})\b/);
if (idMatch && dobMatch) {
const [, m, d, y] = dobMatch;
const year = y!.length === 2 ? `20${y}` : y;
return { memberId: idMatch[1]!, dob: normalizeDob(`${m}/${d}/${year}`) };
}
try {
const llm = getLlm(provider, apiKey, model);
const res = await llm.invoke([
{
role: "system",
content:
'Extract the insurance member ID and date of birth from the patient message. ' +
'Return ONLY valid JSON: {"memberId":"...","dob":"MM/DD/YYYY"}. Use null for missing fields.',
},
{ role: "user", content: message },
]);
const raw = String(res.content).replace(/```json|```/g, "").trim();
const json = JSON.parse(raw);
const dob = json.dob ? normalizeDob(String(json.dob)) : null;
return { memberId: json.memberId ?? null, dob };
} catch {
return { memberId: null, dob: null };
}
}
/**
* Run MassHealth eligibility check in the background (after replying to the
* patient) and send the result as a follow-up SMS, regardless of which
* channel (SMS or voice call) triggered the check.
*/
async function runMassHealthCheckAndNotify(
patient: RoutedPatient,
memberId: string,
dob: string,
apiKey: string,
isExistingPatient = false
): Promise<void> {
try {
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(patient.userId, "MH");
if (!credentials) return;
const twilioSettings = await storage.getTwilioSettings(patient.userId);
if (!twilioSettings || !patient.phone) return;
await runEligibilityProcessor({
userId: patient.userId,
insuranceId: memberId,
formDob: dob,
enrichedPayload: {
memberId,
dateOfBirth: dob,
insuranceSiteKey: "MH",
massdhpUsername: credentials.username,
massdhpPassword: credentials.password,
},
});
const updated = await db.patient.findUnique({
where: { id: patient.id },
select: { status: true, firstName: true },
});
const lang = patient.preferredLanguage || "English";
const active = updated?.status === "ACTIVE";
const activeMessages: Record<string, string> = {
English: "Great news! Your MassHealth coverage is active. We can schedule an appointment for you! What date and time would you prefer?",
Spanish: "¡Buenas noticias! Su cobertura de MassHealth está activa. ¡Podemos programar una cita para usted! ¿Qué fecha y hora prefiere?",
Portuguese: "Ótimas notícias! Sua cobertura MassHealth está ativa. Podemos agendar uma consulta para você! Qual data e horário prefere?",
Mandarin: "好消息!您的MassHealth保险有效。我们可以为您安排预约!您希望什么日期和时间?",
Cantonese: "好消息!您的MassHealth保險有效。我們可以為您安排預約!您希望什麼日期和時間?",
Arabic: "أخبار رائعة! تغطيتك من MassHealth نشطة. يمكننا تحديد موعد لك! ما التاريخ والوقت المفضل لديك؟",
"Haitian Creole": "Bon nouvèl! Asirans MassHealth ou aktif. Nou ka planifye yon randevou pou ou! Ki dat ak lè ou prefere?",
};
const inactiveMessagesNew: Record<string, string> = {
English: "Unfortunately, your MassHealth coverage appears to be inactive. Do you have any other insurance?",
Spanish: "Lamentablemente, su cobertura de MassHealth parece estar inactiva. ¿Tiene algún otro seguro?",
Portuguese: "Infelizmente, sua cobertura MassHealth parece estar inativa. Você tem algum outro plano de saúde?",
Mandarin: "很遗憾,您的MassHealth保险似乎无效。您还有其他保险吗?",
Cantonese: "很遺憾,您的MassHealth保險似乎無效。您還有其他保險嗎?",
Arabic: "للأسف، تغطيتك من MassHealth تبدو غير نشطة. هل لديك أي تأمين آخر؟",
"Haitian Creole": "Malerezman, kouvèti MassHealth ou parèt inaktif. Èske ou gen yon lòt asirans?",
};
const inactiveMessagesExisting: Record<string, string> = {
English: "We checked your MassHealth coverage. Unfortunately the plan appears inactive or could not be verified. Would you still like to schedule an examination appointment as a self-pay patient?",
Spanish: "Verificamos su cobertura de MassHealth. Lamentablemente el plan aparece inactivo o no pudo ser verificado. ¿Le gustaría programar una cita de examen como paciente de pago particular?",
Portuguese: "Verificamos sua cobertura MassHealth. Infelizmente o plano parece inativo ou não pôde ser verificado. Gostaria de agendar uma consulta de exame como paciente particular?",
Mandarin: "我们查看了您的MassHealth保险。遗憾的是,保险似乎无效或无法验证。您仍然希望以自费方式预约检查吗?",
Cantonese: "我們查看了您的MassHealth保險。遺憾地,保險似乎無效或無法核實。您仍然希望以自費方式預約檢查嗎?",
Arabic: "تحققنا من تغطيتك من MassHealth. للأسف يبدو أن الخطة غير نشطة أو لا يمكن التحقق منها. هل تودّ تحديد موعد فحص كمريض يدفع من حسابه الخاص؟",
"Haitian Creole": "Nou te verifye kouvèti MassHealth ou. Malerezman plan an sanble inaktif oswa pa ka verifye. Èske ou ta renmen pran yon randevou egzamen kòm pasyan ki peye poukont li?",
};
const resultText = active
? (activeMessages[lang] ?? activeMessages["English"]!)
: isExistingPatient
? (inactiveMessagesExisting[lang] ?? inactiveMessagesExisting["English"]!)
: (inactiveMessagesNew[lang] ?? inactiveMessagesNew["English"]!);
const nextStage: ConversationStage = active
? "asked_appointment_time"
: isExistingPatient
? "asked_self_pay"
: "asked_other_insurance_after_inactive";
const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
await client.messages.create({
body: resultText,
from: twilioSettings.phoneNumber,
to: patient.phone,
});
await saveOutboundSms(patient.id, resultText);
await setStage(patient.userId, patient.id, nextStage);
} catch {
// Silent — don't crash the caller
}
}
async function finish(
userId: number, patientId: number,
segments: string[], nextStage: ConversationStage
): Promise<RouteTurnResult> {
await setStage(userId, patientId, nextStage);
return { segments, nextStage };
}
/**
* Process one patient message (SMS body or a transcribed spoken turn) against
* the current ConversationStage and return what to say next plus the stage
* to advance to. Persists the resulting stage itself. Does NOT log the
* Communication row for the reply — callers do that per-channel.
*/
export async function routePatientTurn(params: {
patient: RoutedPatient;
stage: ConversationStage;
message: string;
activeAi: { provider: AiProvider; key: string; model: string };
chatTemplates: Record<string, string>;
officeName: string;
}): Promise<RouteTurnResult> {
const { patient, stage, message, activeAi, chatTemplates, officeName } = params;
const language = patient.preferredLanguage || "English";
// ── Stage: reminder_initial → self-intro + classify yes/no/other ────────
if (stage === "reminder_initial") {
const rawGreeting = chatTemplates.reminderGreeting ||
`Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can confirm or reschedule your appointment and answer general questions 24/7.`;
const introText = applyOfficeName(rawGreeting, officeName);
const apptDatetime = await getAppointmentDatetime(patient.id);
const { reply: intentReply, intent } = await runReminderGraph(
message, activeAi.key, language, apptDatetime,
chatTemplates.rescheduleGreeting, chatTemplates.generalFallback, activeAi.provider, activeAi.model
);
if (intentReply) {
let nextStage: ConversationStage;
if (intent === "no") nextStage = "asked_reschedule_datetime";
else if (intent === "wants_appointment") nextStage = "asked_new_or_existing";
else nextStage = "done";
if (intent === "no") {
const hasDateInMessage =
/\b\d{1,2}[\/\-]\d{1,2}\b/.test(message) ||
/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow|next week)\b/i.test(message);
if (hasDateInMessage) {
const { reply: rescheduleReply, nextStage: rescheduleNextStage } = await runRescheduleStep(
message, "asked_reschedule_datetime", language, patient.id, activeAi.key, patient.userId, activeAi.provider, activeAi.model
);
return finish(patient.userId, patient.id, [introText, rescheduleReply], rescheduleNextStage);
}
}
return finish(patient.userId, patient.id, [introText, intentReply], nextStage);
}
// No clear intent detected — send only the intro and wait for next reply
return finish(patient.userId, patient.id, [introText], "greeted");
}
// ── Stage: recall_initial → office proactively offered an existing patient
// a routine exam & cleaning (usually covered at no cost by insurance); the
// pitch itself was already spoken/sent as the opening greeting, so this
// just classifies the patient's yes/no. On yes, it books directly onto the
// schedule (Column A, 30-minute slots) via runRecallBookingStep — unlike
// reschedule, a recall patient has no existing appointment to move, so this
// is a real create-appointment path, not a shortcut into the reschedule flow.
if (stage === "recall_initial") {
const { reply: intentReply, intent } = await runRecallGraph(
message, activeAi.key, language, chatTemplates.generalFallback, activeAi.provider, activeAi.model
);
if (intentReply) {
if (intent === "yes") {
// Patient may have already given a date (or date + time) in this same
// reply ("Tuesday 1pm") — try to book right away instead of asking again.
// A bare "yes"/"ok" has nothing to parse, so skip straight to asking for
// day + time instead of wasting a parse call that used to come back
// empty and send a mismatched "Sorry, I didn't catch a day" reply.
if (hasDateOrTimeSignal(message)) {
const { reply: bookingReply, nextStage: bookingNextStage } = await runRecallBookingStep(
message, "recall_asked_datetime", language, patient.id, patient.userId, activeAi.key, activeAi.provider, activeAi.model
);
return finish(patient.userId, patient.id, [bookingReply], bookingNextStage);
}
return finish(patient.userId, patient.id, [intentReply], "recall_asked_datetime");
}
if (intent === "no") {
return finish(patient.userId, patient.id, [intentReply], "done");
}
return finish(patient.userId, patient.id, [intentReply], "recall_initial");
}
return finish(patient.userId, patient.id, [chatTemplates.generalFallback || "Thank you for your message!"], "recall_initial");
}
// ── Recall booking flow — collecting/confirming the day & time, then
// creating the appointment (see recall-graph.ts: runRecallBookingStep) ──
const recallBookingStages: ConversationStage[] = ["recall_asked_datetime", "recall_asked_time_for_date"];
if (recallBookingStages.includes(stage)) {
const { reply: aiReply, nextStage } = await runRecallBookingStep(
message, stage, language, patient.id, patient.userId, activeAi.key, activeAi.provider, activeAi.model
);
return finish(patient.userId, patient.id, [aiReply], nextStage);
}
// ── Stage: greeted → classify yes/no for appointment reminder ──────────
if (stage === "greeted") {
const apptDatetime = await getAppointmentDatetime(patient.id);
const { reply: aiReply, intent } = await runReminderGraph(
message, activeAi.key, language, apptDatetime,
chatTemplates.rescheduleGreeting, chatTemplates.generalFallback, activeAi.provider, activeAi.model
);
if (aiReply) {
let nextStage: ConversationStage;
if (intent === "no") nextStage = "asked_reschedule_datetime";
else if (intent === "wants_appointment") nextStage = "asked_new_or_existing";
else nextStage = "done";
if (intent === "no") {
const hasDateInMessage =
/\b\d{1,2}[\/\-]\d{1,2}\b/.test(message) ||
/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow|next week)\b/i.test(message);
if (hasDateInMessage) {
const { reply: rescheduleReply, nextStage: rescheduleNextStage } = await runRescheduleStep(
message, "asked_reschedule_datetime", language, patient.id, activeAi.key, patient.userId, activeAi.provider, activeAi.model
);
return finish(patient.userId, patient.id, [rescheduleReply], rescheduleNextStage);
}
}
return finish(patient.userId, patient.id, [aiReply], nextStage);
}
}
// ── Rescheduling flow stages ────────────────────────────────────────────
const rescheduleStages: ConversationStage[] = [
"asked_reschedule_confirm", "asked_reschedule_preference",
"asked_reschedule_asap", "asked_reschedule_next_week",
"asked_reschedule_time", "asked_reschedule_datetime",
"asked_reschedule_time_for_date", "asked_reschedule_confirm_datetime",
];
if (rescheduleStages.includes(stage)) {
const { reply: aiReply, nextStage } = await runRescheduleStep(
message, stage, language, patient.id, activeAi.key, patient.userId, activeAi.provider, activeAi.model
);
return finish(patient.userId, patient.id, [aiReply], nextStage);
}
// ── Stage: awaiting MassHealth member ID + DOB ─────────────────────────
if (stage === "awaiting_masshealth_info") {
const { memberId, dob } = await parseMassHealthInfo(message, activeAi.key, activeAi.provider, activeAi.model);
if (!memberId || !dob) {
const retryMessages: Record<string, string> = {
English: "I couldn't read your Member ID and date of birth. Please reply in this format: Member ID: 12345678 DOB: 01/01/1990",
Spanish: "No pude leer su número de miembro y fecha de nacimiento. Por favor responda así: ID: 12345678 Fecha: 01/01/1990",
Portuguese: "Não consegui ler seu número de membro e data de nascimento. Por favor responda assim: ID: 12345678 Data: 01/01/1990",
Mandarin: "我无法读取您的会员ID和出生日期。请按以下格式回复:ID: 12345678 生日: 01/01/1990",
Cantonese: "我無法讀取您的會員ID和出生日期。請按以下格式回覆:ID: 12345678 生日: 01/01/1990",
Arabic: "لم أتمكن من قراءة رقم العضوية وتاريخ الميلاد. يرجى الرد بالصيغة التالية: ID: 12345678 DOB: 01/01/1990",
"Haitian Creole": "Mwen pa t ka li ID manm ou ak dat nesans. Tanpri reponn konsa: ID: 12345678 DOB: 01/01/1990",
};
return finish(patient.userId, patient.id, [retryMessages[language] ?? retryMessages["English"]!], "awaiting_masshealth_info");
}
const checkingMessages: Record<string, string> = {
English: "Thank you! I'm checking your MassHealth eligibility now. I'll send you the result in a moment.",
Spanish: "¡Gracias! Estoy verificando su elegibilidad de MassHealth ahora. Le enviaré el resultado en un momento.",
Portuguese: "Obrigado! Estou verificando sua elegibilidade MassHealth agora. Enviarei o resultado em instantes.",
Mandarin: "谢谢!我正在查询您的MassHealth资格。稍后我会发送结果给您。",
Cantonese: "多謝!我正在查詢您的MassHealth資格。稍後我會發送結果給您。",
Arabic: "شكراً! أقوم بالتحقق من أهليتك في MassHealth الآن. سأرسل لك النتيجة قريباً.",
"Haitian Creole": "Mèsi! Mwen ap verifye kalifikasyon MassHealth ou kounye a. M ap voye rezilta a nan yon ti moman.",
};
const checkingMsg = checkingMessages[language] ?? checkingMessages["English"]!;
runMassHealthCheckAndNotify(patient, memberId, dob, activeAi.key).catch(() => {});
return finish(patient.userId, patient.id, [checkingMsg], "done");
}
// ── Stage: existing patient said YES to same insurance ──────────────────
if (stage === "asked_existing_insurance") {
const saysYes = /yes|same|still have|haven't changed|no change|yep|yeah|sí|si|sim|好的|نعم|wi/i.test(message);
if (saysYes) {
const patientRecord = await db.patient.findUnique({
where: { id: patient.id },
select: { insuranceProvider: true, insuranceId: true, dateOfBirth: true },
});
const isMassHealth = /masshealth|mass health|masscare|medicaid/i.test(
patientRecord?.insuranceProvider ?? ""
);
if (isMassHealth && patientRecord?.insuranceId) {
let dobStr = "";
if (patientRecord.dateOfBirth) {
const d = new Date(patientRecord.dateOfBirth);
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
const dd = String(d.getUTCDate()).padStart(2, "0");
const yy = d.getUTCFullYear();
dobStr = `${mm}/${dd}/${yy}`;
}
const checkingMessages: Record<string, string> = {
English: "Please wait about 30-60 seconds! I'm double-checking your MassHealth coverage right now.",
Spanish: "¡Por favor espere unos 30-60 segundos! Estoy verificando su cobertura de MassHealth ahora mismo.",
Portuguese: "Por favor aguarde cerca de 30-60 segundos! Estou verificando sua cobertura MassHealth agora.",
Mandarin: "请等待约30-60秒!我现在正在为您核查MassHealth保险。",
Cantonese: "請等待約30-60秒!我現在正在為您核查MassHealth保險。",
Arabic: "يرجى الانتظار حوالي 30-60 ثانية! أقوم الآن بالتحقق من تغطيتك في MassHealth.",
"Haitian Creole": "Tanpri tann anviwon 30-60 segonn! Mwen ap verifye kouvèti MassHealth ou kounye a.",
};
const checkingMsg = checkingMessages[language] ?? checkingMessages["English"]!;
runMassHealthCheckAndNotify(patient, patientRecord.insuranceId, dobStr, activeAi.key, true).catch(() => {});
return finish(patient.userId, patient.id, [checkingMsg], "done");
}
}
// Not MassHealth or said NO — fall through to normal graph handling
}
// ── Stage: asked_new_or_reschedule ───────────────────────────────────────
if (stage === "asked_new_or_reschedule") {
const isReschedule = /reschedule|rescheduler|change|modify|move|different|reprogramar|reagendar|cambiar|mudar|\b2\b/i.test(message);
const isNew = /new.*appoint|make.*appoint|book.*appoint|new patient|first.*time|nueva cita|nova consulta|\b1\b/i.test(message);
if (isReschedule) {
const apptDatetime = await getAppointmentDatetime(patient.id);
if (apptDatetime) {
const foundMsgs: Record<string, string> = {
English: `Ok. Just to confirm, your current appointment is on ${apptDatetime}. When would you like to reschedule?`,
Spanish: `De acuerdo. Solo para confirmar, su cita actual es el ${apptDatetime}. ¿Cuándo le gustaría reprogramarla?`,
Portuguese: `Ok. Só para confirmar, sua consulta atual é em ${apptDatetime}. Quando você gostaria de reagendar?`,
Mandarin: `好的。请确认一下,您当前的预约是 ${apptDatetime}。您想重新安排到什么时候?`,
Cantonese: `好的。請確認一下,您目前的預約是 ${apptDatetime}。您想重新安排到什麼時候?`,
Arabic: `حسناً. فقط للتأكيد، موعدك الحالي في ${apptDatetime}. متى تريد إعادة الجدولة؟`,
"Haitian Creole": `Oke. Jis pou konfime, randevou aktyèl ou a se ${apptDatetime}. Ki lè ou ta renmen reprogramè?`,
};
return finish(patient.userId, patient.id, [foundMsgs[language] ?? foundMsgs["English"]!], "asked_reschedule_datetime");
} else {
const notFoundMsgs: Record<string, string> = {
English: "Ok. I could not find your current appointment. Please tell me when you'd like to reschedule and our receptionist will contact you as soon as possible.",
Spanish: "De acuerdo. No pude encontrar su cita actual. Por favor díganos cuándo le gustaría reprogramar y nuestra recepcionista le contactará lo antes posible.",
Portuguese: "Ok. Não consegui encontrar sua consulta atual. Por favor diga-nos quando você gostaria de reagendar e nossa recepcionista entrará em contato o mais breve possível.",
Mandarin: "好的。我找不到您当前的预约。请告诉我您希望重新安排的时间,我们的前台将尽快与您联系。",
Cantonese: "好的。我找不到您目前的預約。請告訴我您希望重新安排的時間,我們的接待員將盡快與您聯絡。",
Arabic: "حسناً. لم أجد موعدك الحالي. يرجى إخبارنا بالوقت الذي تريد إعادة الجدولة إليه وسيتصل بك موظف الاستقبال في أقرب وقت ممكن.",
"Haitian Creole": "Oke. Mwen pa t jwenn randevou aktyèl ou. Tanpri di nou ki lè ou ta renmen reprogramè epi resepsyonis nou an pral kontakte ou pi vit posib.",
};
return finish(patient.userId, patient.id, [notFoundMsgs[language] ?? notFoundMsgs["English"]!], "done");
}
}
void isNew; // ambiguous or explicit "new" both fall through to the same prompt
const newApptMsgs: Record<string, string> = {
English: "No problem! To get started, are you an existing patient or a new patient?",
Spanish: "¡Sin problema! Para comenzar, ¿es usted un paciente existente o un paciente nuevo?",
Portuguese: "Sem problema! Para começar, você é um paciente existente ou um novo paciente?",
Mandarin: "没问题!请问您是现有患者还是新患者?",
Cantonese: "沒問題!請問您是現有病人還是新病人?",
Arabic: "لا مشكلة! للبدء، هل أنت مريض حالي أم مريض جديد؟",
"Haitian Creole": "Pa gen pwoblèm! Pou kòmanse, èske ou se yon pasyan egzistan oswa yon nouvo pasyan?",
};
return finish(patient.userId, patient.id, [newApptMsgs[language] ?? newApptMsgs["English"]!], "asked_new_or_existing");
}
// ── Stage: asked_appointment_time → parse date, check office hours ─────
if (stage === "asked_appointment_time") {
const parsedDate = await parseDateOnlyFromMessage(message, activeAi.key, activeAi.provider, activeAi.model);
if (!parsedDate) {
const msgs: Record<string, string> = {
English: "I didn't catch that. What day would you prefer? For example: 'May 28', 'next Monday', or '5/28'.",
Spanish: "No entendí. ¿Qué día prefiere? Por ejemplo: '28 de mayo', 'próximo lunes' o '28/5'.",
Portuguese: "Não entendi. Que dia você prefere? Por exemplo: '28 de maio', 'próxima segunda' ou '28/5'.",
Mandarin: "我没听清。您希望哪天?例如:'5月28日'、'下周一'或'5/28'。",
Cantonese: "我沒聽清。您希望哪天?例如:'5月28日'、'下週一'或'5/28'。",
Arabic: "لم أفهم. ما اليوم الذي تفضله؟ مثلاً: '28 مايو' أو '5/28'.",
"Haitian Creole": "Mwen pa konprann. Ki jou ou prefere? Pa egzanp: '28 me', 'Lendi pwochèn', oswa '5/28'.",
};
return finish(patient.userId, patient.id, [msgs[language] ?? msgs["English"]!], "asked_appointment_time");
}
const { date, dateLabel } = parsedDate;
const dayCheck = await isOfficeDayOpen(date, patient.userId);
if (!dayCheck.open) {
const msgs: Record<string, string> = {
English: `Our office is closed on ${dateLabel} (${dayCheck.displayDay}). Can you please choose another day?`,
Spanish: `Nuestra oficina está cerrada el ${dateLabel} (${dayCheck.displayDay}). ¿Puede elegir otro día?`,
Portuguese: `Nosso consultório está fechado em ${dateLabel} (${dayCheck.displayDay}). Pode escolher outro dia?`,
Mandarin: `我们诊所在 ${dateLabel}(${dayCheck.displayDay})不开放。请您选择另一天好吗?`,
Cantonese: `我們診所在 ${dateLabel}(${dayCheck.displayDay})不開放。請您選擇另一天好嗎?`,
Arabic: `مكتبنا مغلق في ${dateLabel} (${dayCheck.displayDay}). هل يمكنك اختيار يوم آخر؟`,
"Haitian Creole": `Biwo nou fèmen nan ${dateLabel} (${dayCheck.displayDay}). Tanpri chwazi yon lòt jou?`,
};
return finish(patient.userId, patient.id, [msgs[language] ?? msgs["English"]!], "asked_appointment_time");
}
setPendingReschedule(patient.userId, patient.id, { newDate: date, dayLabel: dateLabel });
const askTimeMsgs: Record<string, string> = {
English: `What time do you prefer on ${dateLabel}?`,
Spanish: `¿A qué hora prefiere el ${dateLabel}?`,
Portuguese: `Que horário você prefere em ${dateLabel}?`,
Mandarin: `您希望在 ${dateLabel} 几点?`,
Cantonese: `您希望在 ${dateLabel} 幾點?`,
Arabic: `ما الوقت الذي تفضله في ${dateLabel}؟`,
"Haitian Creole": `Ki lè ou prefere nan ${dateLabel}?`,
};
return finish(patient.userId, patient.id, [askTimeMsgs[language] ?? askTimeMsgs["English"]!], "asked_new_appt_time_for_date");
}
// ── Stage: asked_new_appt_time_for_date → parse time, check hours ──────
if (stage === "asked_new_appt_time_for_date") {
const pending = getPendingReschedule(patient.userId, patient.id);
if (!pending) {
return finish(patient.userId, patient.id, ["I lost track of the date. What day would you prefer?"], "asked_appointment_time");
}
const startTime = await parseTime(message, activeAi.key, activeAi.provider, activeAi.model);
if (!startTime) {
const msgs: Record<string, string> = {
English: `I didn't catch the time. What time do you prefer on ${pending.dayLabel}? For example: '10am' or '2pm'.`,
Spanish: `No entendí la hora. ¿Qué hora prefiere el ${pending.dayLabel}? Por ejemplo: '10am' o '2pm'.`,
Portuguese: `Não entendi o horário. Que hora você prefere em ${pending.dayLabel}? Por exemplo: '10h' ou '14h'.`,
Mandarin: `我没听清时间。您在 ${pending.dayLabel} 几点?例如:上午10点或下午2点。`,
Cantonese: `我沒聽清時間。您在 ${pending.dayLabel} 幾點?例如:上午10點或下午2點。`,
Arabic: `لم أفهم الوقت. ما الوقت الذي تفضله في ${pending.dayLabel}؟ مثلاً: 10 صباحاً أو 2 مساءً.`,
"Haitian Creole": `Mwen pa konprann lè a. Ki lè ou prefere nan ${pending.dayLabel}? Pa egzanp: 10am oswa 2pm.`,
};
return finish(patient.userId, patient.id, [msgs[language] ?? msgs["English"]!], "asked_new_appt_time_for_date");
}
const withinHours = await isWithinOfficeHours(pending.newDate, startTime, patient.userId);
if (!withinHours) {
const hoursDisplay = await getOfficeHoursDisplay(pending.newDate, patient.userId);
const timeLbl = timeLabel(startTime);
const msgs: Record<string, string> = {
English: hoursDisplay
? `Our office is not available at ${timeLbl} on ${pending.dayLabel}. Our hours are ${hoursDisplay}. What other time do you prefer?`
: `Our office is not available at ${timeLbl} on ${pending.dayLabel}. What other time do you prefer?`,
Spanish: hoursDisplay
? `Nuestra oficina no está disponible a las ${timeLbl} el ${pending.dayLabel}. Nuestro horario es ${hoursDisplay}. ¿Qué otro horario prefiere?`
: `Nuestra oficina no está disponible a las ${timeLbl} el ${pending.dayLabel}. ¿Qué otro horario prefiere?`,
Portuguese: hoursDisplay
? `Nosso consultório não está disponível às ${timeLbl} em ${pending.dayLabel}. Nosso horário é ${hoursDisplay}. Que outro horário você prefere?`
: `Nosso consultório não está disponível às ${timeLbl} em ${pending.dayLabel}. Que outro horário você prefere?`,
Mandarin: hoursDisplay
? `我们诊所在 ${pending.dayLabel} ${timeLbl} 不开放。工作时间是 ${hoursDisplay}。您希望改什么时间?`
: `我们诊所在 ${pending.dayLabel} ${timeLbl} 不开放。您希望改什么时间?`,
Cantonese: hoursDisplay
? `我們診所在 ${pending.dayLabel} ${timeLbl} 不開放。工作時間是 ${hoursDisplay}。您希望改什麼時間?`
: `我們診所在 ${pending.dayLabel} ${timeLbl} 不開放。您希望改什麼時間?`,
Arabic: hoursDisplay
? `مكتبنا غير متاح في ${timeLbl} يوم ${pending.dayLabel}. ساعات العمل: ${hoursDisplay}. ما وقت آخر تفضله؟`
: `مكتبنا غير متاح في ${timeLbl} يوم ${pending.dayLabel}. ما وقت آخر تفضله؟`,
"Haitian Creole": hoursDisplay
? `Biwo nou pa disponib a ${timeLbl} nan ${pending.dayLabel}. Orè nou se ${hoursDisplay}. Ki lòt lè ou prefere?`
: `Biwo nou pa disponib a ${timeLbl} nan ${pending.dayLabel}. Ki lòt lè ou prefere?`,
};
return finish(patient.userId, patient.id, [msgs[language] ?? msgs["English"]!], "asked_new_appt_time_for_date");
}
clearPendingReschedule(patient.userId, patient.id);
const apptLabel = `${pending.dayLabel} at ${timeLabel(startTime)}`;
const [nh, nm] = startTime.split(":").map(Number);
const endTotal = nh! * 60 + nm! + 60;
const endTime = `${String(Math.floor(endTotal / 60)).padStart(2, "0")}:${String(endTotal % 60).padStart(2, "0")}`;
const [patientRecord, firstStaff] = await Promise.all([
db.patient.findUnique({
where: { id: patient.id },
select: { firstName: true, lastName: true },
}),
db.staff.findFirst({
where: { userId: patient.userId },
orderBy: { id: "asc" },
}),
]);
if (firstStaff) {
try {
await storage.createAppointment({
patientId: patient.id,
userId: patient.userId,
staffId: firstStaff.id,
title: `AI Scheduled - ${(patientRecord?.firstName ?? "") + " " + (patientRecord?.lastName ?? "")}`.trim(),
date: pending.newDate,
startTime,
endTime,
type: "checkup",
status: "scheduled",
movedByAi: true,
} as any);
await sendAppointmentConfirmationSms(patient.userId, patient.id, pending.dayLabel, timeLabel(startTime), language);
} catch { /* silent — message still sent, staff can create manually */ }
}
const confirmMsgs: Record<string, string> = {
English: `Thank you! Your preferred appointment at ${apptLabel} was scheduled. Our receptionist will confirm it with you shortly.`,
Spanish: `¡Gracias! Su cita preferida el ${apptLabel} fue programada. Nuestra recepcionista lo confirmará con usted en breve.`,
Portuguese: `Obrigado! Sua consulta preferida em ${apptLabel} foi agendada. Nossa recepcionista confirmará com você em breve.`,
Mandarin: `谢谢!您在 ${apptLabel} 的预约已安排。我们的前台将很快与您确认。`,
Cantonese: `多謝!您在 ${apptLabel} 的預約已安排。我們的接待員將很快與您確認。`,
Arabic: `شكراً! تم جدولة موعدك في ${apptLabel}. ستتواصل معك موظفة الاستقبال قريباً لتأكيده.`,
"Haitian Creole": `Mèsi! Randevou ou a nan ${apptLabel} pwograme. Resepsyonis nou an pral konfime li ak ou byento.`,
};
return finish(patient.userId, patient.id, [confirmMsgs[language] ?? confirmMsgs["English"]!], "done");
}
// ── Stage: new_patient_greeted + multi-step new patient stages ──────────
const newPatientStages: ConversationStage[] = [
"new_patient_greeted", "asked_new_or_existing",
"asked_new_patient_insurance", "asked_insurance_type",
"asked_masshealth_check_consent", "asked_existing_insurance",
"asked_appointment_preference",
"asked_self_pay", "asked_other_insurance_after_inactive",
"collecting_contact_info",
];
if (newPatientStages.includes(stage)) {
const { reply: aiReply, nextStage } = await runNewPatientStep(
message, stage, language, activeAi.key, chatTemplates.generalFallback, activeAi.provider, activeAi.model
);
return finish(patient.userId, patient.id, [aiReply], nextStage);
}
// ── Stage: done → closing thank-you reply ────────────────────────────────
if (stage === "done") {
const isThanks = /\b(thank|thanks|thank you|ty|ok|okay|great|perfect|sounds good|got it|understood|alright|appreciate|wonderful|excellent|awesome|cool|nice|good)\b/i.test(message);
if (isThanks) {
const apptDatetime = await getAppointmentDatetime(patient.id);
const CLOSING: Record<string, string> = {
English: apptDatetime
? `Thank you for choosing our office! We look forward to seeing you on ${apptDatetime}.`
: `Thank you for choosing our office! We look forward to seeing you soon.`,
Spanish: apptDatetime
? `¡Gracias por elegirnos! Le esperamos el ${apptDatetime}.`
: `¡Gracias por elegirnos! Le esperamos pronto.`,
Portuguese: apptDatetime
? `Obrigado por nos escolher! Aguardamos sua visita em ${apptDatetime}.`
: `Obrigado por nos escolher! Aguardamos sua visita em breve.`,
Mandarin: apptDatetime
? `感谢您选择我们!期待在 ${apptDatetime} 见到您。`
: `感谢您选择我们!期待很快见到您。`,
Cantonese: apptDatetime
? `感謝您選擇我們!期待在 ${apptDatetime} 見到您。`
: `感謝您選擇我們!期待很快見到您。`,
Arabic: apptDatetime
? `شكراً لاختيارك عيادتنا! نتطلع إلى رؤيتك في ${apptDatetime}.`
: `شكراً لاختيارك عيادتنا! نتطلع إلى رؤيتك قريباً.`,
"Haitian Creole": apptDatetime
? `Mèsi dèske ou chwazi nou! N'ap tann ou ${apptDatetime}.`
: `Mèsi dèske ou chwazi nou! N'ap tann ou byento.`,
};
const fallback = CLOSING[language] ?? CLOSING["English"]!;
if (apptDatetime) {
try {
const llm = getLlm(activeAi.provider, activeAi.key, activeAi.model);
const res = await llm.invoke([
{
role: "system",
content: `You are a friendly dental office AI assistant. The patient just said "${message}" after completing a conversation. Reply warmly in ${language}, thanking them for choosing the office and reminding them of their upcoming appointment on ${apptDatetime}. 1-2 sentences, no formatting.`,
},
{ role: "user", content: message },
]);
const aiMsg = String(res.content).trim();
if (aiMsg) return finish(patient.userId, patient.id, [aiMsg], "done");
} catch { /* fall through to fallback */ }
}
return finish(patient.userId, patient.id, [fallback], "done");
}
}
// No matching stage/branch — hold stage steady with no reply.
return { segments: [], nextStage: stage };
}
+486
View File
@@ -0,0 +1,486 @@
import { StateGraph, END, START, Annotation } from "@langchain/langgraph";
import { getLlm, type AiProvider } from "./llm-factory";
import { prisma as db } from "@repo/db/client";
import { storage } from "../storage";
import {
type ConversationStage,
setPendingReschedule, getPendingReschedule, clearPendingReschedule,
} from "./aiHandoffStore";
import {
parseDateOnlyFromMessage, parseTime, isOfficeDayOpen, isWithinOfficeHours,
getOfficeHoursDisplay, timeLabel,
} from "./reschedule-graph";
import { sendAppointmentConfirmationSms } from "./confirmationSms";
const GraphState = Annotation.Root({
message: Annotation<string>(),
intent: Annotation<string>(),
reply: Annotation<string>(),
language: Annotation<string>(),
generalFallback: Annotation<string>(),
});
type GraphStateType = typeof GraphState.State;
// ── Intent classifier — multilingual yes/no keywords (shared shape with reminder-graph) ──
/** True if `text` mentions a concrete day/date/time (e.g. "Tuesday", "3/10", "2pm").
* Shared by classifyNode (a bare "yes" vs. "yes, Tuesday at 1pm" both count as
* intent "yes") and by the caller in patient-conversation-router.ts, which uses
* it to decide whether to attempt an immediate booking parse or just ask for a
* day/time first. */
export function hasDateOrTimeSignal(text: string): boolean {
return (
/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday|today|tomorrow|next week)\b/i.test(text) ||
/\b\d{1,2}[\/\-]\d{1,2}\b/.test(text) ||
/\b\d{1,2}(:\d{2})?\s*(am|pm|a\.m\.|p\.m\.)\b/i.test(text)
);
}
function classifyNode(state: GraphStateType) {
const text = state.message.toLowerCase().trim();
const yesPatterns = /\b(yes|yeah|yep|yup|sure|ok|okay|sounds good|i'?d like (that|to)|let'?s do it|book it|schedule it|i want (an? )?appointment|i want to (come in|schedule|book)|great|perfect|absolutely|definitely|sí|si|claro|por supuesto|de acuerdo|seguro|sim|com certeza|好的|可以|好|明白|نعم|حسنا|موافق|wi|dakò|oke)\b/;
const noPatterns = /\b(no|nope|not interested|not right now|no thanks|can't|cannot|won't|not now|not available|busy|sorry|unable|no puedo|no gracias|ahora no|não posso|agora não|não|占线|无法|不用了|不需要|لا|لا أستطيع|pa kapab|pa ka)\b/;
// A specific day/date/time, or an appointment-request phrase, is itself a
// strong signal the patient wants to book — real replies rarely say a bare
// "yes", they jump straight to "Tuesday at 1pm" or "I want an appointment".
const mentionsDateOrTime = hasDateOrTimeSignal(text);
const mentionsAppointment = /\b(appointment|schedule|book|come in|check.?up|cleaning)\b/i.test(text);
if (noPatterns.test(text) && !mentionsDateOrTime) return { intent: "no" };
if (yesPatterns.test(text) || mentionsDateOrTime || mentionsAppointment) return { intent: "yes" };
return { intent: "other" };
}
function routeByIntent(state: GraphStateType): string {
if (state.intent === "yes") return "wantsBooking";
if (state.intent === "no") return "declines";
return "other";
}
// ── Fallbacks ──────────────────────────────────────────────────────────────
const WANTS_BOOKING_FALLBACKS: Record<string, string> = {
English: "Great! What day and time work best for you?",
Spanish: "¡Genial! ¿Qué día y hora le vienen mejor?",
Portuguese: "Ótimo! Qual dia e horário são melhores para você?",
Mandarin: "太好了!您哪天、几点方便?",
Cantonese: "太好喇!您邊日、幾點方便?",
Arabic: "رائع! ما هو اليوم والوقت الأنسب لك؟",
"Haitian Creole": "Bon! Ki jou ak ki lè ki pi bon pou ou?",
};
const DECLINES_FALLBACKS: Record<string, string> = {
English: "No problem at all! Feel free to reach out whenever you're ready. Have a great day!",
Spanish: "¡No hay problema! No dude en contactarnos cuando esté listo. ¡Que tenga un buen día!",
Portuguese: "Sem problemas! Entre em contato sempre que estiver pronto. Tenha um ótimo dia!",
Mandarin: "没问题!您准备好了随时联系我们。祝您愉快!",
Cantonese: "冇問題!您準備好隨時聯絡我們。祝您愉快!",
Arabic: "لا مشكلة على الإطلاق! لا تتردد في التواصل معنا عندما تكون مستعداً. أتمنى لك يوماً سعيداً!",
"Haitian Creole": "Pa gen pwoblèm! Ou ka kontakte nou nenpòt lè ou pare. Pase yon bon jounen!",
};
const GENERAL_FALLBACKS: Record<string, string> = {
English: "Thank you for your message! Our office staff will be happy to assist you shortly.",
Spanish: "¡Gracias por su mensaje! El personal de nuestra oficina estará encantado de ayudarle en breve.",
Portuguese: "Obrigado pela sua mensagem! Nossa equipe terá prazer em ajudá-lo em breve.",
Mandarin: "感谢您的留言!我们的办公室工作人员将很快为您提供帮助。",
Cantonese: "感謝您的留言!我們的辦公室工作人員將很快為您提供幫助。",
Arabic: "شكراً على رسالتك! سيسعد فريق مكتبنا بمساعدتك قريباً.",
"Haitian Creole": "Mèsi pou mesaj ou! Ekip biwo nou an pral kontan ede ou byento.",
};
// ── LangGraph nodes ───────────────────────────────────────────────────────────
async function wantsBookingNode(state: GraphStateType, config: any) {
const apiKey: string | undefined = config?.configurable?.apiKey;
const lang = state.language || "English";
const fallback = WANTS_BOOKING_FALLBACKS[lang] ?? WANTS_BOOKING_FALLBACKS["English"]!;
if (!apiKey) return { reply: fallback };
try {
const provider: AiProvider = config?.configurable?.provider ?? "google";
const model: string | undefined = config?.configurable?.model;
const llm = getLlm(provider, apiKey, model);
const response = await llm.invoke([
{
role: "system",
content:
`You are a friendly dental office assistant. The patient agreed to come in for a routine exam and cleaning. Write a short, warm reply (1-2 sentences max) and ask what day AND time work best for them. Do NOT introduce yourself or say your name — the introduction has already been sent in a separate message. You MUST reply in ${lang}. No formatting, no extra text.`,
},
{ role: "user", content: `Patient replied: "${state.message}"` },
]);
return { reply: String(response.content) || fallback };
} catch {
return { reply: fallback };
}
}
async function declinesNode(state: GraphStateType, config: any) {
const apiKey: string | undefined = config?.configurable?.apiKey;
const lang = state.language || "English";
const fallback = DECLINES_FALLBACKS[lang] ?? DECLINES_FALLBACKS["English"]!;
if (!apiKey) return { reply: fallback };
try {
const provider: AiProvider = config?.configurable?.provider ?? "google";
const model: string | undefined = config?.configurable?.model;
const llm = getLlm(provider, apiKey, model);
const response = await llm.invoke([
{
role: "system",
content:
`You are a friendly dental office assistant. The patient does not want to schedule a checkup right now. Write a short, polite reply (1-2 sentences max) letting them know that's no problem and they can reach out whenever they're ready. Do NOT introduce yourself or say your name. You MUST reply in ${lang}. No formatting, no extra text.`,
},
{ role: "user", content: `Patient replied: "${state.message}"` },
]);
return { reply: String(response.content) || fallback };
} catch {
return { reply: fallback };
}
}
async function otherNode(state: GraphStateType, config: any) {
const apiKey: string | undefined = config?.configurable?.apiKey;
const lang = state.language || "English";
const fallback = state.generalFallback || (GENERAL_FALLBACKS[lang] ?? GENERAL_FALLBACKS["English"]!);
if (!apiKey) return { reply: fallback };
try {
const provider: AiProvider = config?.configurable?.provider ?? "google";
const model: string | undefined = config?.configurable?.model;
const llm = getLlm(provider, apiKey, model);
const response = await llm.invoke([
{
role: "system",
content: `You are a friendly dental office AI assistant. Respond helpfully to the patient's message in ${lang}. Keep it to 1-2 sentences, no formatting. For non-dental questions, let them know our office staff can assist.`,
},
{ role: "user", content: `Patient said: "${state.message}"` },
]);
return { reply: String(response.content) || fallback };
} catch {
return { reply: fallback };
}
}
// ── Graph ─────────────────────────────────────────────────────────────────────
const graph = new StateGraph(GraphState)
.addNode("classify", classifyNode)
.addNode("wantsBooking", wantsBookingNode)
.addNode("declines", declinesNode)
.addNode("other", otherNode)
.addEdge(START, "classify")
.addConditionalEdges("classify", routeByIntent, {
wantsBooking: "wantsBooking",
declines: "declines",
other: "other",
})
.addEdge("wantsBooking", END)
.addEdge("declines", END)
.addEdge("other", END)
.compile();
export async function runRecallGraph(
patientMessage: string,
apiKey: string,
language = "English",
generalFallback = "",
provider: AiProvider = "google",
model?: string
): Promise<{ reply: string | null; intent: string | null }> {
const result = await graph.invoke(
{ message: patientMessage, intent: "", reply: "", language, generalFallback },
{ configurable: { apiKey, provider, model } }
);
return {
reply: result.reply || null,
intent: result.intent || null,
};
}
// ── Recall booking — actually reserves a slot, unlike the reschedule flow ──
// (which only MOVES an existing appointment). A recall patient has none, so
// this creates a new one directly on "Column A" (the office's first staff
// column, matching the convention already used in internal-chat-workflow.ts),
// in fixed 30-minute slots.
const RECALL_SLOT_MINUTES = 30;
function toMinutes(t: string): number {
const [h, m] = t.split(":").map(Number);
return (h ?? 0) * 60 + (m ?? 0);
}
function fromMinutes(m: number): string {
return `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
}
/** "Column A" = the office's first staff record by id, per the same convention
* used for AI-booked appointments elsewhere (see internal-chat-workflow.ts). */
async function getColumnAStaffId(userId: number): Promise<number | null> {
const staff = await db.staff.findFirst({ where: { userId }, orderBy: { id: "asc" } });
return staff?.id ?? null;
}
/**
* Finds a free RECALL_SLOT_MINUTES slot for `staffId` on `date`. Prefers
* `requestedStartTime` if it's free; otherwise falls back to the earliest
* open slot that day. Returns null if the whole day is fully booked.
*/
async function findRecallSlot(
userId: number, staffId: number, date: Date, requestedStartTime?: string | null,
): Promise<{ time: string; matchedRequest: boolean } | null> {
const officeHours = await storage.getOfficeHours(userId);
const data = (officeHours as any)?.data;
const dayNames = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];
const dayName = dayNames[date.getUTCDay()]!;
// Union of doctors' + hygienists' windows that day — matches the "open if
// within either group's hours" tolerance isWithinOfficeHours() already uses.
const windows: [number, number][] = [];
for (const group of [data?.doctors, data?.hygienists]) {
const slot = group?.[dayName];
if (slot?.enabled) {
windows.push([toMinutes(slot.amStart), toMinutes(slot.amEnd)]);
windows.push([toMinutes(slot.pmStart), toMinutes(slot.pmEnd)]);
}
}
if (windows.length === 0) {
windows.push([toMinutes("09:00"), toMinutes("12:00")], [toMinutes("13:00"), toMinutes("17:00")]);
}
const allSlots = new Set<number>();
for (const [start, end] of windows) {
for (let t = start; t + RECALL_SLOT_MINUTES <= end; t += RECALL_SLOT_MINUTES) allSlots.add(t);
}
const dateStr = date.toISOString().split("T")[0]!;
const existing = await storage.getAppointmentsByDateForUser(dateStr, userId);
const booked = existing
.filter((a: any) => a.staffId === staffId && a.status !== "cancelled")
.map((a: any) => ({ start: toMinutes(a.startTime), end: toMinutes(a.endTime) }));
const isFree = (slotStart: number) => {
const slotEnd = slotStart + RECALL_SLOT_MINUTES;
return !booked.some((b) => slotStart < b.end && slotEnd > b.start);
};
if (requestedStartTime) {
const reqStart = toMinutes(requestedStartTime);
if (allSlots.has(reqStart) && isFree(reqStart)) return { time: requestedStartTime, matchedRequest: true };
}
const sorted = Array.from(allSlots).sort((a, b) => a - b);
const fallback = sorted.find(isFree);
return fallback === undefined ? null : { time: fromMinutes(fallback), matchedRequest: false };
}
const DAY_FULL_FALLBACKS: Record<string, (dateLabel: string) => string> = {
English: (d) => `I'm sorry, ${d} is fully booked. Could you please choose another day?`,
Spanish: (d) => `Lo sentimos, el ${d} ya está completo. ¿Podría elegir otro día?`,
Portuguese: (d) => `Sinto muito, ${d} está totalmente reservado. Poderia escolher outro dia?`,
Mandarin: (d) => `很抱歉,${d} 的预约已满。您能选择其他日期吗?`,
Cantonese: (d) => `好抱歉,${d} 已經約滿。您可以選擇其他日子嗎?`,
Arabic: (d) => `عذراً، ${d} محجوز بالكامل. هل يمكنك اختيار يوم آخر؟`,
"Haitian Creole": (d) => `Padon, ${d} konplè. Èske ou ka chwazi yon lòt jou?`,
};
const NO_STAFF_FALLBACK: Record<string, string> = {
English: "Thanks! Our office staff will reach out shortly to confirm a time that works.",
Spanish: "¡Gracias! Nuestro personal se pondrá en contacto en breve para confirmar un horario.",
Portuguese: "Obrigado! Nossa equipe entrará em contato em breve para confirmar um horário.",
Mandarin: "谢谢!我们的工作人员将很快与您联系以确认时间。",
Cantonese: "多謝!我們的職員好快會聯絡您確認時間。",
Arabic: "شكراً! سيتواصل معك فريقنا قريباً لتأكيد الموعد.",
"Haitian Creole": "Mèsi! Ekip biwo nou an pral kontakte ou byento pou konfime yon lè.",
};
const BOOKED_EXACT_FALLBACKS: Record<string, (dateLabel: string, time: string) => string> = {
English: (d, t) => `You're all set for ${d} at ${t}! We look forward to seeing you.`,
Spanish: (d, t) => `¡Todo listo para el ${d} a las ${t}! Le esperamos.`,
Portuguese: (d, t) => `Tudo pronto para ${d} às ${t}! Aguardamos sua visita.`,
Mandarin: (d, t) => `已为您安排在 ${d} ${t}!期待您的光临。`,
Cantonese: (d, t) => `已幫您安排喺 ${d} ${t}!期待見到您。`,
Arabic: (d, t) => `تم الحجز في ${d} الساعة ${t}! نتطلع لرؤيتك.`,
"Haitian Creole": (d, t) => `Ou pare pou ${d} a ${t}! N'ap tann ou.`,
};
const BOOKED_ALT_FALLBACKS: Record<string, (dateLabel: string, time: string) => string> = {
English: (d, t) => `That exact time was just taken, so I booked you for ${t} on ${d} instead. We look forward to seeing you!`,
Spanish: (d, t) => `Esa hora ya no estaba disponible, así que le reservé a las ${t} el ${d}. ¡Le esperamos!`,
Portuguese: (d, t) => `Esse horário exato já foi reservado, então marquei ${t} em ${d}. Aguardamos sua visita!`,
Mandarin: (d, t) => `那个时间刚被预订了,我已为您安排在 ${d} ${t}。期待您的光临!`,
Cantonese: (d, t) => `嗰個時間啱啱被人訂咗,我已經幫您安排喺 ${d} ${t}。期待見到您!`,
Arabic: (d, t) => `تم حجز ذلك الوقت للتو، لذا حجزت لك موعداً في ${d} الساعة ${t}. نتطلع لرؤيتك!`,
"Haitian Creole": (d, t) => `Lè sa a fèk pran, kidonk mwen bwouke ou pou ${t} nan ${d}. N'ap tann ou!`,
};
const ASK_TIME_FALLBACKS: Record<string, (dateLabel: string) => string> = {
English: (d) => `Great, what time on ${d} works for you?`,
Spanish: (d) => `Genial, ¿qué hora le viene bien el ${d}?`,
Portuguese: (d) => `Ótimo, que horário funciona para você em ${d}?`,
Mandarin: (d) => `太好了,您 ${d} 哪个时间方便?`,
Cantonese: (d) => `太好喇,您 ${d} 邊個時間方便?`,
Arabic: (d) => `رائع، ما الوقت المناسب لك في ${d}؟`,
"Haitian Creole": (d) => `Bon, ki lè nan ${d} ki bon pou ou?`,
};
// Same follow-up question as ASK_TIME_FALLBACKS, without the leading "Great,"
// greeting — used when it directly follows a rejection sentence, where the
// greeting would read as redundant filler.
const ASK_TIME_PLAIN_FALLBACKS: Record<string, (dateLabel: string) => string> = {
English: (d) => `What time on ${d} works for you?`,
Spanish: (d) => `¿Qué hora le viene bien el ${d}?`,
Portuguese: (d) => `Que horário funciona para você em ${d}?`,
Mandarin: (d) => `您 ${d} 哪个时间方便?`,
Cantonese: (d) => `您 ${d} 邊個時間方便?`,
Arabic: (d) => `ما الوقت المناسب لك في ${d}؟`,
"Haitian Creole": (d) => `Ki lè nan ${d} ki bon pou ou?`,
};
const OFFICE_HOURS_ARE_FALLBACKS: Record<string, string> = {
English: "Our office hours are",
Spanish: "Nuestro horario de atención es",
Portuguese: "Nosso horário de atendimento é",
Mandarin: "我们的营业时间是",
Cantonese: "我們嘅營業時間係",
Arabic: "ساعات عملنا هي",
"Haitian Creole": "Orè biwo nou se",
};
const CLOSED_DAY_FALLBACKS: Record<string, (day: string) => string> = {
English: (day) => `We're closed on ${day}s. What other day works for you?`,
Spanish: (day) => `Estamos cerrados los ${day}. ¿Qué otro día le viene bien?`,
Portuguese: (day) => `Estamos fechados aos ${day}. Que outro dia funciona para você?`,
Mandarin: (day) => `我们${day}休息。您方便其他哪天?`,
Cantonese: (day) => `我們${day}休息。您方便其他邊日?`,
Arabic: (day) => `نحن مغلقون أيام ${day}. ما يوم آخر يناسبك؟`,
"Haitian Creole": (day) => `Nou fèmen jou ${day}. Ki lòt jou ki bon pou ou?`,
};
const NO_DATE_FALLBACKS: Record<string, string> = {
English: "Sorry, I didn't catch a day — what day would you like to come in?",
Spanish: "Disculpe, no entendí el día — ¿qué día le gustaría venir?",
Portuguese: "Desculpe, não entendi o dia — que dia você gostaria de vir?",
Mandarin: "抱歉,我没听清日期——您想哪天过来?",
Cantonese: "抱歉,我冇聽清楚日期——您想邊日嚟?",
Arabic: "عذراً، لم أفهم اليوم — ما اليوم الذي تود الحضور فيه؟",
"Haitian Creole": "Padon, mwen pa konprann jou a — ki jou ou ta renmen vini?",
};
/** Books the recall appointment (or reports the day is full) and returns the reply + next stage. */
async function bookRecallSlot(
patientId: number, userId: number, date: Date, dateLabel: string, requestedStartTime: string | null, lang: string,
): Promise<{ reply: string; nextStage: ConversationStage }> {
const staffId = await getColumnAStaffId(userId);
if (staffId === null) {
return { reply: NO_STAFF_FALLBACK[lang] ?? NO_STAFF_FALLBACK["English"]!, nextStage: "done" };
}
const slot = await findRecallSlot(userId, staffId, date, requestedStartTime);
if (!slot) {
const build = DAY_FULL_FALLBACKS[lang] ?? DAY_FULL_FALLBACKS["English"]!;
return { reply: build(dateLabel), nextStage: "recall_asked_datetime" };
}
const endTime = fromMinutes(toMinutes(slot.time) + RECALL_SLOT_MINUTES);
await storage.createAppointment({
patientId, userId, staffId,
title: "Routine Exam & Cleaning",
date, startTime: slot.time, endTime,
type: "recall",
status: "scheduled",
movedByAi: true,
} as any);
const timeLbl = timeLabel(slot.time);
const build = slot.matchedRequest
? (BOOKED_EXACT_FALLBACKS[lang] ?? BOOKED_EXACT_FALLBACKS["English"]!)
: (BOOKED_ALT_FALLBACKS[lang] ?? BOOKED_ALT_FALLBACKS["English"]!);
await sendAppointmentConfirmationSms(userId, patientId, dateLabel, timeLbl, lang);
return { reply: build(dateLabel, timeLbl), nextStage: "done" };
}
/**
* Drives the recall_asked_datetime / recall_asked_time_for_date stages:
* parses a date (and optionally a time) out of the patient's reply, checks
* office hours, then books via bookRecallSlot() once both pieces are known.
*/
export async function runRecallBookingStep(
message: string,
stage: ConversationStage,
language: string,
patientId: number,
userId: number,
apiKey: string,
provider: AiProvider = "google",
model?: string
): Promise<{ reply: string; nextStage: ConversationStage }> {
const lang = language || "English";
// ── Waiting on a time only (date was already captured last turn) ─────────
if (stage === "recall_asked_time_for_date") {
const pending = getPendingReschedule(userId, patientId);
if (!pending) {
return { reply: NO_DATE_FALLBACKS[lang] ?? NO_DATE_FALLBACKS["English"]!, nextStage: "recall_asked_datetime" };
}
const time = await parseTime(message, apiKey, provider, model);
if (!time) {
const build = ASK_TIME_FALLBACKS[lang] ?? ASK_TIME_FALLBACKS["English"]!;
return { reply: build(pending.dayLabel), nextStage: "recall_asked_time_for_date" };
}
return await resolveDateTime(patientId, userId, pending.newDate, pending.dayLabel, time, lang);
}
// ── recall_initial (yes, with a date already in the same message) or
// recall_asked_datetime — parse a date out of the message ────────────────
const parsedDate = await parseDateOnlyFromMessage(message, apiKey, provider, model);
if (!parsedDate) {
return { reply: NO_DATE_FALLBACKS[lang] ?? NO_DATE_FALLBACKS["English"]!, nextStage: "recall_asked_datetime" };
}
const dayCheck = await isOfficeDayOpen(parsedDate.date, userId);
if (!dayCheck.open) {
const build = CLOSED_DAY_FALLBACKS[lang] ?? CLOSED_DAY_FALLBACKS["English"]!;
return { reply: build(dayCheck.displayDay), nextStage: "recall_asked_datetime" };
}
const time = await parseTime(message, apiKey, provider, model);
if (!time) {
setPendingReschedule(userId, patientId, { newDate: parsedDate.date, dayLabel: parsedDate.dateLabel });
const build = ASK_TIME_FALLBACKS[lang] ?? ASK_TIME_FALLBACKS["English"]!;
return { reply: build(parsedDate.dateLabel), nextStage: "recall_asked_time_for_date" };
}
return await resolveDateTime(patientId, userId, parsedDate.date, parsedDate.dateLabel, time, lang);
}
async function resolveDateTime(
patientId: number, userId: number, date: Date, dateLabel: string, time: string, lang: string,
): Promise<{ reply: string; nextStage: ConversationStage }> {
const withinHours = await isWithinOfficeHours(date, time, userId);
if (!withinHours) {
const hoursDisplay = await getOfficeHoursDisplay(date, userId);
setPendingReschedule(userId, patientId, { newDate: date, dayLabel: dateLabel });
const base: Record<string, string> = {
English: `We're not available at that time on ${dateLabel}.`,
Spanish: `No estamos disponibles a esa hora el ${dateLabel}.`,
Portuguese: `Não estamos disponíveis nesse horário em ${dateLabel}.`,
Mandarin: `我们在 ${dateLabel} 的那个时间不营业。`,
Cantonese: `我們喺 ${dateLabel} 嗰個時間唔開放。`,
Arabic: `نحن غير متاحين في ذلك الوقت يوم ${dateLabel}.`,
"Haitian Creole": `Nou pa disponib nan lè sa a nan ${dateLabel}.`,
};
const askTime = (ASK_TIME_PLAIN_FALLBACKS[lang] ?? ASK_TIME_PLAIN_FALLBACKS["English"]!)(dateLabel);
const officeHoursAre = OFFICE_HOURS_ARE_FALLBACKS[lang] ?? OFFICE_HOURS_ARE_FALLBACKS["English"]!;
const hours = hoursDisplay ? ` ${officeHoursAre} ${hoursDisplay}.` : "";
return { reply: `${base[lang] ?? base["English"]}${hours} ${askTime}`, nextStage: "recall_asked_time_for_date" };
}
clearPendingReschedule(userId, patientId);
return await bookRecallSlot(patientId, userId, date, dateLabel, time, lang);
}
+24 -3
View File
@@ -7,6 +7,7 @@ import {
getPendingReschedule,
clearPendingReschedule,
} from "./aiHandoffStore";
import { sendAppointmentConfirmationSms } from "./confirmationSms";
// ── Date helpers ──────────────────────────────────────────────────────────────
@@ -392,18 +393,35 @@ async function isSlotAvailable(
// ── Time parsing (legacy) ─────────────────────────────────────────────────────
export async function parseTime(message: string, apiKey: string, provider: AiProvider = "google", model?: string): Promise<string | null> {
const t = message.toLowerCase();
// Twilio speech-to-text usually renders "pm"/"am" with periods ("p.m."),
// and often drops the colon in a spoken time too ("1:00pm" -> "100 pm",
// "12:30pm" -> "1230 pm") — normalize both before matching.
const t = message.toLowerCase()
.replace(/\ba\.m\.?/g, "am")
.replace(/\bp\.m\.?/g, "pm");
if (/\bmorning\b|mañana|manhã|上午|صباح|maten/i.test(t)) return "09:00";
if (/\bafternoon\b|tarde|après-midi|下午|مساء|aprèmidi/i.test(t)) return "13:00";
const ampm = t.match(/\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/);
const ampm = t.match(/\b(\d{1,2}):([0-5]\d)\s*(am|pm)\b/);
const glued = !ampm ? t.match(/\b(\d{1,4})\s*(am|pm)\b/) : null;
const clock = t.match(/\b([01]?\d|2[0-3]):([0-5]\d)\b/);
if (ampm) {
let h = parseInt(ampm[1]!);
const m = ampm[2] ? parseInt(ampm[2]) : 0;
const m = parseInt(ampm[2]!);
if (ampm[3] === "pm" && h < 12) h += 12;
if (ampm[3] === "am" && h === 12) h = 0;
return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}`;
}
if (glued) {
const digits = glued[1]!;
let h: number, m: number;
if (digits.length <= 2) { h = parseInt(digits); m = 0; }
else { h = parseInt(digits.slice(0, -2)); m = parseInt(digits.slice(-2)); }
if (h >= 1 && h <= 12 && m <= 59) {
if (glued[2] === "pm" && h < 12) h += 12;
if (glued[2] === "am" && h === 12) h = 0;
return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}`;
}
}
if (clock) return clock[0]!;
try {
const llm = getLlm(provider, apiKey, model);
@@ -820,6 +838,8 @@ export async function runRescheduleStep(
fallback,
apiKey, provider, model
);
const datePart = displayLabel.split(" at ")[0] ?? displayLabel;
await sendAppointmentConfirmationSms(userId, patientId, datePart, timeLabel(startTime), lang);
return { reply, nextStage: "done" };
}
@@ -1109,6 +1129,7 @@ export async function runRescheduleStep(
`You are a friendly dental office assistant. The patient's appointment has been rescheduled to ${apptLabel}. Confirm in ${lang}: say the appointment is moved to ${apptLabel} and that our dental receptionist will confirm it with them tomorrow. 2 sentences, no formatting.`,
`Appointment moved to ${apptLabel}.`, fallback, apiKey, provider, model
);
await sendAppointmentConfirmationSms(userId, patientId, pending.dayLabel, timeLabel(startTime), lang);
return { reply, nextStage: "done" };
}
+6
View File
@@ -0,0 +1,6 @@
const CLOSING_PATTERNS =
/\b(bye|goodbye|good bye|that'?s all|nothing else|no thanks|no thank you|i'?m good|that'?s it|hang up)\b/i;
export function soundsLikeGoodbye(text: string): boolean {
return CLOSING_PATTERNS.test(text.trim());
}
@@ -34,6 +34,8 @@ export interface CmspEligibilityHistoryRemainingProcessorResult {
historyPdfFilename?: string | null;
accumulatorPdfFileId?: number | string | null;
accumulatorPdfFilename?: string | null;
remainingBenefit?: string | null;
thirdPartyInsurance?: string | null;
}
export async function runCmspEligibilityHistoryRemainingProcessor(
@@ -105,8 +107,21 @@ export async function runCmspEligibilityHistoryRemainingProcessor(
const updates: any = { status: newStatus };
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
const remainingBenefit = seleniumResult.remaining_amount?.trim() || null;
if (remainingBenefit) updates.remainingBenefit = remainingBenefit;
// third_party_insurance is present (even as null) whenever the Third Party
// Liability table was found — null means the member has no 2nd insurance,
// so we still write it to clear any stale value from a prior check.
if (Object.prototype.hasOwnProperty.call(seleniumResult, "third_party_insurance")) {
updates.thirdPartyInsurance = seleniumResult.third_party_insurance?.trim() || null;
}
await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
outputResult.remainingBenefit = remainingBenefit;
outputResult.thirdPartyInsurance = updates.thirdPartyInsurance ?? null;
// Helper: save a single PDF to the patient's Cloud Storage Eligibility folder
const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`;
@@ -138,8 +138,9 @@ async function processDdmaResult(
// 4) Determine and update eligibility status + insurance provider name
const eligStatus = (seleniumResult?.eligibility ?? "").toLowerCase();
const newStatus =
eligStatus === "active" || eligStatus === "y" ? "ACTIVE" : "INACTIVE";
let newStatus = "UNKNOWN";
if (eligStatus === "active" || eligStatus === "y") newStatus = "ACTIVE";
else if (eligStatus === "inactive" || eligStatus === "n" || eligStatus === "termed") newStatus = "INACTIVE";
await storage.updatePatient(patient.id, {
status: newStatus,
insuranceProvider: "Delta Dental MA",
+26
View File
@@ -223,6 +223,32 @@ router.put("/chat-templates", async (req: Request, res: Response): Promise<any>
}
});
// GET /api/ai/call-templates
router.get("/call-templates", async (req: Request, res: Response): Promise<any> => {
try {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ message: "Unauthorized" });
const templates = await storage.getAiCallTemplates(userId);
return res.status(200).json(templates);
} catch (err) {
return res.status(500).json({ error: "Failed to fetch AI call templates", details: String(err) });
}
});
// PUT /api/ai/call-templates
router.put("/call-templates", async (req: Request, res: Response): Promise<any> => {
try {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ message: "Unauthorized" });
const { greeting, reminderGreeting, newPatientGreeting, generalFallback, recallGreeting } = req.body;
await storage.saveAiCallTemplates(userId, { greeting, reminderGreeting, newPatientGreeting, generalFallback, recallGreeting });
const updated = await storage.getAiCallTemplates(userId);
return res.status(200).json(updated);
} catch (err) {
return res.status(500).json({ error: "Failed to save AI call templates", details: String(err) });
}
});
// GET /api/ai/internal-chat-settings
router.get("/internal-chat-settings", async (req: Request, res: Response): Promise<any> => {
try {
File diff suppressed because it is too large Load Diff
+49
View File
@@ -3,6 +3,8 @@ import twilio from "twilio";
import { storage } from "../storage";
import { prisma as db } from "@repo/db/client";
import { getHandoff, setHandoff, resetConversation, startNewPatientConversation, startRescheduleConversation, getAfterHoursHandoff, setAfterHoursHandoff } from "../ai/aiHandoffStore";
import { resolveAiProvider } from "../ai/llm-factory";
import { getTwilioPublicBaseUrl } from "../utils/publicUrl";
const router = express.Router();
@@ -117,6 +119,53 @@ router.post("/send-sms", async (req: Request, res: Response): Promise<any> => {
}
});
// POST /api/twilio/make-ai-call
// Places an outbound call where Lisa (the AI assistant) conducts the whole
// conversation live over the phone — see /webhook/ai-voice for the loop.
router.post("/make-ai-call", async (req: Request, res: Response): Promise<any> => {
try {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ message: "Unauthorized" });
const { to, patientId, purpose } = req.body;
if (!to || !patientId) return res.status(400).json({ message: "to and patientId are required" });
const settings = await storage.getTwilioSettings(userId);
if (!settings) {
return res.status(400).json({ message: "Twilio is not configured. Please add your Twilio credentials in Settings." });
}
const aiSettings = await storage.getAiSettings(userId);
if (!resolveAiProvider(aiSettings ?? {})) {
return res.status(400).json({ message: "AI is not configured. Please add an AI provider key in Settings before placing an AI call." });
}
const patient = await db.patient.findFirst({ where: { id: Number(patientId), userId } });
if (!patient) return res.status(404).json({ message: "Patient not found" });
let publicBaseUrl: string;
try {
publicBaseUrl = getTwilioPublicBaseUrl();
} catch (err: any) {
return res.status(500).json({ message: err.message });
}
const callPurpose = purpose === "recall" ? "recall" : "reminder";
const webhookUrl = `${publicBaseUrl}/api/twilio/webhook/ai-voice?patientId=${patient.id}&purpose=${callPurpose}`;
const client = getTwilioClient(settings.accountSid, settings.authToken);
const call = await client.calls.create({
url: webhookUrl,
method: "POST",
from: settings.phoneNumber,
to,
});
return res.status(200).json({ sid: call.sid, status: call.status });
} catch (err: any) {
return res.status(500).json({ error: err.message || "Failed to place AI call" });
}
});
// POST /api/twilio/send-reminders-batch
router.post("/send-reminders-batch", async (req: Request, res: Response): Promise<any> => {
try {
+8 -1
View File
@@ -46,9 +46,16 @@ export async function startWebDavServer(): Promise<void> {
const config = readRcloneConfig();
// Serve only the backups folder — never the app root, which contains
// .env (DB/Twilio/AI credentials) and the full source tree.
const backupsDir = path.join(APP_ROOT, "backups");
if (!fs.existsSync(backupsDir)) {
fs.mkdirSync(backupsDir, { recursive: true });
}
const args = [
"serve", "webdav",
APP_ROOT,
backupsDir,
"--addr", `:${config.serverPort}`,
"--user", RCLONE_USER,
"--pass", RCLONE_PASS,
@@ -1,5 +1,12 @@
import { prisma as db } from "@repo/db/client";
// Exported so twilio-webhooks.ts can detect an admin-customized template
// (vs. this untouched default) and swap in a translated greeting instead.
export const DEFAULT_GREETING =
"Hi {firstName}, this is Lisa, the AI assistant at {officeName}. How can I help you today?";
export const DEFAULT_RECALL_GREETING =
"Hi {firstName}, this is Lisa from {officeName}. We wanted to check in — you're due for a routine exam and cleaning. Would you like to make an appointment because your insurance covers it?";
export type TwilioSettingsData = {
accountSid: string;
authToken: string;
@@ -96,6 +103,38 @@ export const twilioStorage = {
});
},
// ── Call Templates — wording spoken during live AI phone calls, kept
// separate from the SMS wording above (routePatientTurn() consumes
// whichever set the caller passes in). ────────────────────────────────────
async getAiCallTemplates(userId: number): Promise<Record<string, string>> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const all = (settings?.templates as Record<string, string>) || {};
return {
greeting: all["_ai_call_greeting"] ?? all["_ai_chat_call_template"] ?? DEFAULT_GREETING,
reminderGreeting: all["_ai_call_reminder_greeting"] ?? "Sure, I can help with that.",
newPatientGreeting: all["_ai_call_new_patient_greeting"] ?? "No problem, let's get you set up.",
generalFallback: all["_ai_call_general_fallback"] ?? "Sure, how can I help?",
recallGreeting: all["_ai_call_recall_greeting"] ?? DEFAULT_RECALL_GREETING,
};
},
async saveAiCallTemplates(userId: number, templates: { greeting?: string; reminderGreeting?: string; newPatientGreeting?: string; generalFallback?: string; recallGreeting?: string }) {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const existing = (settings?.templates as Record<string, string>) || {};
const updated: Record<string, string> = { ...existing };
if (templates.greeting !== undefined) updated["_ai_call_greeting"] = templates.greeting;
if (templates.reminderGreeting !== undefined) updated["_ai_call_reminder_greeting"] = templates.reminderGreeting;
if (templates.newPatientGreeting !== undefined) updated["_ai_call_new_patient_greeting"] = templates.newPatientGreeting;
if (templates.generalFallback !== undefined) updated["_ai_call_general_fallback"] = templates.generalFallback;
if (templates.recallGreeting !== undefined) updated["_ai_call_recall_greeting"] = templates.recallGreeting;
return db.twilioSettings.upsert({
where: { userId },
update: { templates: updated },
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
});
},
async getSmsTemplateList(userId: number): Promise<{ id: string; name: string; body: string }[]> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const all = (settings?.templates as Record<string, string>) || {};
+33
View File
@@ -0,0 +1,33 @@
import type { Request } from "express";
/**
* Public HTTPS origin this request arrived on (e.g. https://broadwaydental.mydentalofficemanagement.com).
* Derived from the Host header rather than an env var so it's automatically correct
* per-office — each office's nginx forwards its own public hostname in Host,
* and Cloudflare/Let's Encrypt terminate TLS in front of it either way.
*
* Only valid for requests that actually arrived via the public tunnel (i.e. inside
* a Twilio webhook handler). For anything triggered from the staff browser — which
* reaches the backend over the LAN-only local-* hostname — use getTwilioPublicBaseUrl()
* instead, since Twilio can never reach that LAN hostname to fetch a callback URL.
*/
export function getPublicBaseUrl(req: Request): string {
return `https://${req.get("host")}`;
}
/**
* Fixed public origin Twilio must use to call back into this office's server
* (e.g. for outbound-call TwiML webhooks). Comes from CLOUDFLARE_HOST, the
* per-office env var already reserved for this — not derived from the
* triggering request, since that request may have arrived over the LAN-only
* hostname, which Twilio cannot reach.
*/
export function getTwilioPublicBaseUrl(): string {
const host = process.env.CLOUDFLARE_HOST?.trim();
if (!host) {
throw new Error(
"CLOUDFLARE_HOST is not set — required to build a Twilio-reachable callback URL. Set it in .env to this office's public tunnel hostname."
);
}
return `https://${host}`;
}
@@ -791,13 +791,12 @@ export function ClaimForm({
const deriveInsuranceSiteKey = (provider: string | null | undefined): string => {
const p = (provider || "").toLowerCase().trim();
if (!p) return "";
if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MH";
if (p.includes("masshealth") || p === "mh" || p === "mass health" || p.includes("cmsp")) return "MH";
if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
if (p.includes("ddma") || p.includes("delta dental ma")) return "DDMA";
if (p.includes("delta ins") || p === "deltains") return "DeltaIns";
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO";
if ((p.includes("united") && p.includes("sco")) || p === "unitedsco") return "UnitedSCO";
if (p.includes("cmsp")) return "CMSP";
if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS";
if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR";
if (p.includes("aetna")) return "Aetna";
@@ -2008,7 +2007,6 @@ export function ClaimForm({
<SelectItem value="DeltaIns">Delta Ins</SelectItem>
<SelectItem value="TuftsSCO">Tufts SCO</SelectItem>
<SelectItem value="UnitedSCO">United SCO</SelectItem>
<SelectItem value="CMSP">CMSP</SelectItem>
<SelectItem value="BCBS">BCBS</SelectItem>
<SelectItem value="UnitedAAPR">United AAPR</SelectItem>
<SelectItem value="Aetna">Aetna</SelectItem>
@@ -2596,7 +2594,6 @@ export function ClaimForm({
<SelectItem value="DeltaIns">Delta Ins</SelectItem>
<SelectItem value="TuftsSCO">Tufts SCO</SelectItem>
<SelectItem value="UnitedSCO">United SCO</SelectItem>
<SelectItem value="CMSP">CMSP</SelectItem>
<SelectItem value="BCBS">BCBS</SelectItem>
<SelectItem value="UnitedAAPR">United AAPR</SelectItem>
<SelectItem value="Aetna">Aetna</SelectItem>
@@ -135,7 +135,6 @@ function mdyToISO(mdy: string): string {
const INSURANCE_TYPE_OPTIONS: { value: string; label: string }[] = [
{ value: "mh", label: "MassHealth" },
{ value: "cmsp", label: "CMSP" },
{ value: "ddma", label: "DDMA" },
{ value: "delta-ins", label: "Delta Dental" },
{ value: "united-sco", label: "United SCO" },
@@ -143,13 +142,10 @@ const INSURANCE_TYPE_OPTIONS: { value: string; label: string }[] = [
{ value: "cca", label: "CCA" },
];
function getAutoCheck(dobISO: string): "mh" | "cmsp" {
const [y, m, d] = dobISO.split("-").map(Number);
const today = new Date();
let age = today.getFullYear() - (y ?? 0);
const monthDiff = today.getMonth() + 1 - (m ?? 0);
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < (d ?? 0))) age--;
return age >= 21 ? "mh" : "cmsp";
function getAutoCheck(_dobISO: string): "mh" {
// CMSP and MassHealth eligibility checks now run through the same MH worker,
// so there's no longer an age-based split — everyone is "mh".
return "mh";
}
function parseEligibilityInput(
@@ -311,7 +311,7 @@ export function Sidebar() {
icon: <Bot className="h-4 w-4 text-gray-400" />,
},
{
name: "AI Chat Settings",
name: "AI Chat/Call Settings",
path: "/settings/aichat",
icon: <Bot className="h-4 w-4 text-gray-400" />,
},
@@ -11,7 +11,7 @@ import {
} from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { Send, ArrowLeft, FileText, Globe, Bot, UserPlus, CalendarX } from "lucide-react";
import { Send, ArrowLeft, FileText, Globe, Bot, UserPlus, CalendarX, Phone } from "lucide-react";
import { Switch } from "@/components/ui/switch";
import type { Patient, Communication } from "@repo/db/types";
import { format, isToday, isYesterday, parseISO } from "date-fns";
@@ -534,6 +534,12 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea
{patient.firstName[0]}{patient.lastName[0]}
</div>
<div>
{comm.channel === "voice" && (
<div className="flex items-center gap-1 mb-1 text-[10px] font-medium text-muted-foreground">
<Phone className="h-3 w-3" />
Voice call
</div>
)}
<div className="p-3 rounded-2xl bg-gray-100 text-gray-900 rounded-tl-md">
<p className="text-sm whitespace-pre-wrap break-words">{comm.body}</p>
</div>
@@ -545,6 +551,12 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea
)}
{comm.direction === "outbound" && (
<div>
{comm.channel === "voice" && (
<div className="flex items-center justify-end gap-1 mb-1 text-[10px] font-medium text-muted-foreground">
<Phone className="h-3 w-3" />
Voice call
</div>
)}
<div className="p-3 rounded-2xl bg-primary text-primary-foreground rounded-tr-md">
<p className="text-sm whitespace-pre-wrap break-words">{comm.body}</p>
</div>
@@ -1,6 +1,7 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAuth } from "@/hooks/use-auth";
import { useToast } from "@/hooks/use-toast";
import {
Form,
FormControl,
@@ -50,6 +51,7 @@ export type PatientFormRef = {
export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
({ patient, extractedInfo, onSubmit }, ref) => {
const { user } = useAuth();
const { toast } = useToast();
const isEditing = !!patient;
const schema = useMemo(
@@ -62,13 +64,12 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
const normalizeInsuranceProvider = (val: string | null | undefined): string => {
const p = (val || "").toLowerCase().trim();
if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MassHealth";
if (p.includes("masshealth") || p === "mh" || p === "mass health" || p.includes("cmsp")) return "MassHealth";
if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
if (p.includes("ddma")) return "DDMA";
if (p.includes("delta dental") || p.includes("delta ins") || p === "deltains") return "DeltaIns";
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO";
if (p.includes("united sco") || p === "unitedsco") return "UnitedSCO";
if (p.includes("cmsp")) return "CMSP";
if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS";
if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR";
if (p.includes("aetna")) return "Aetna";
@@ -83,7 +84,7 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
const computedDefaultValues = useMemo(() => {
if (isEditing && patient) {
const { id, userId, createdAt, ...sanitizedPatient } = patient;
const { id, userId, createdAt, updatedAt, ...sanitizedPatient } = patient;
return {
...sanitizedPatient,
dateOfBirth: patient.dateOfBirth
@@ -137,7 +138,7 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
useEffect(() => {
if (patient) {
const { id, userId, createdAt, ...sanitizedPatient } = patient;
const { id, userId, createdAt, updatedAt, ...sanitizedPatient } = patient;
const normalized = normalizeInsuranceProvider(patient.insuranceProvider);
const resetValues: Partial<Patient> = {
...sanitizedPatient,
@@ -164,9 +165,19 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
id="patient-form"
key={patient?.id || "new"}
noValidate
onSubmit={form.handleSubmit((data) => {
handleSubmit2(data);
})}
onSubmit={form.handleSubmit(
(data) => {
handleSubmit2(data);
},
(errors) => {
console.log("❌ Form validation errors:", errors);
toast({
title: "Could not save patient",
description: "Please check the highlighted fields and try again.",
variant: "destructive",
});
},
)}
className="space-y-6"
>
{/* Personal Information */}
@@ -439,7 +450,6 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
<SelectItem value="DeltaIns">Delta Ins</SelectItem>
<SelectItem value="TuftsSCO">Tufts SCO</SelectItem>
<SelectItem value="UnitedSCO">United SCO</SelectItem>
<SelectItem value="CMSP">CMSP</SelectItem>
<SelectItem value="BCBS">BCBS</SelectItem>
<SelectItem value="UnitedAAPR">United AAPR</SelectItem>
<SelectItem value="Aetna">Aetna</SelectItem>
@@ -63,6 +63,7 @@ interface PatientTableProps {
allowCheckbox?: boolean;
allowNewClaim?: boolean;
allowFinancial?: boolean;
showEligibilityColumns?: boolean;
onNewClaim?: (patientId: number) => void;
onSelectPatient?: (patient: Patient | null) => void;
onPageChange?: (page: number) => void;
@@ -82,6 +83,7 @@ export function PatientTable({
allowCheckbox,
allowNewClaim,
allowFinancial,
showEligibilityColumns,
onNewClaim,
onSelectPatient,
onPageChange,
@@ -976,6 +978,12 @@ export function PatientTable({
{/*<TableHead>Contact</TableHead>*/}
<TableHead>Insurance</TableHead>
<TableHead>Status</TableHead>
{showEligibilityColumns && (
<>
<TableHead>Remaining</TableHead>
<TableHead>Third Party/2nd Insurance</TableHead>
</>
)}
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -983,7 +991,7 @@ export function PatientTable({
{isLoading ? (
<TableRow>
<TableCell
colSpan={6}
colSpan={showEligibilityColumns ? 8 : 6}
className="text-center py-8 text-muted-foreground"
>
<LoadingScreen />
@@ -992,7 +1000,7 @@ export function PatientTable({
) : isError ? (
<TableRow>
<TableCell
colSpan={6}
colSpan={showEligibilityColumns ? 8 : 6}
className="text-center py-8 text-red-500"
>
Error loading patients.
@@ -1001,7 +1009,7 @@ export function PatientTable({
) : patientsData?.patients.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
colSpan={showEligibilityColumns ? 8 : 6}
className="text-center py-8 text-muted-foreground"
>
No patients found.
@@ -1088,8 +1096,28 @@ export function PatientTable({
</span>
)}
</div>
{patient.updatedAt && (
<div className="text-xs text-gray-500 mt-1">
Updated: {formatDateToHumanReadable(patient.updatedAt)}
</div>
)}
</TableCell>
{showEligibilityColumns && (
<>
<TableCell>
<div className="text-sm text-gray-900">
{patient.remainingBenefit || "—"}
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
{patient.thirdPartyInsurance || "—"}
</div>
</TableCell>
</>
)}
<TableCell className="text-right">
<div className="flex justify-end">
{allowDelete && isAdmin && (
@@ -7,7 +7,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus, Zap, SlidersHorizontal, BookMarked, ChevronDown, ChevronUp } from "lucide-react";
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus, Zap, SlidersHorizontal, BookMarked, ChevronDown, ChevronUp, PhoneCall, Stethoscope } from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -20,6 +20,14 @@ type AiChatTemplates = {
generalFallback: string;
};
type AiCallTemplates = {
greeting: string;
reminderGreeting: string;
newPatientGreeting: string;
generalFallback: string;
recallGreeting: string;
};
type OfficeContact = {
officeName?: string | null;
};
@@ -38,6 +46,16 @@ const DEFAULTS = {
"Hi! My name is Lisa, the dedicated AI assistant at {officeName}. How can I help you today?",
};
const CALL_DEFAULTS = {
greeting:
"Hi {firstName}, this is Lisa, the AI assistant at {officeName}. How can I help you today?",
reminderGreeting: "Sure, I can help with that.",
newPatientGreeting: "No problem, let's get you set up.",
generalFallback: "Sure, how can I help?",
recallGreeting:
"Hi {firstName}, this is Lisa from {officeName}. We wanted to check in — you're due for a routine exam and cleaning. Would you like to make an appointment because your insurance covers it?",
};
const DEFAULT_SMS_TEMPLATES = [
{
id: "default-appt-reminder",
@@ -67,18 +85,19 @@ function LangGraphFlow() {
const nW = 210;
const nx = cx - nW / 2; // 245
// ── Dual entry nodes ──────────────────────────────────────────────────────
const entryW = 192;
const lEntryCx = 155;
const rEntryCx = 545;
const e1y = 14, e1h = 56;
// ── Triple entry nodes ─────────────────────────────────────────────────────
const entryW = 150;
const lEntryCx = 130;
const mEntryCx = cx; // 350
const rEntryCx = 570;
const e1y = 14, e1h = 70;
// Merge connector (horizontal line where both branches meet)
const mergeY = e1y + e1h + 14; // 84
// Merge connector (horizontal line where all three entries meet)
const mergeY = e1y + e1h + 14; // 98
// ── Center sequence ───────────────────────────────────────────────────────
const n2y = mergeY + 14, n2h = 52; // Patient replies y=98
const n3y = n2y + n2h + 14, n3h = 84; // AI classifies y=164
const n2y = mergeY + 14, n2h = 52; // Patient replies
const n3y = n2y + n2h + 14, n3h = 98; // AI classifies
const forkHY = n3y + n3h + 22; // 270
@@ -88,7 +107,7 @@ function LangGraphFlow() {
// ── YES branch ────────────────────────────────────────────────────────────
const yesW = 195;
const yes1y = forkHY + 50, yes1h = 88; // y=320
const yes1y = forkHY + 50, yes1h = 102; // y=320
const yes2y = yes1y + yes1h + 12, yes2h = 52; // y=460
// ── NO / Reschedule branch ────────────────────────────────────────────────
@@ -113,7 +132,7 @@ function LangGraphFlow() {
viewBox={`0 0 ${W} ${totalH}`}
className="w-full max-w-2xl mx-auto"
role="img"
aria-label="LangGraph Reminder and Reschedule conversation flow"
aria-label="LangGraph Reminder, Recall, and Reschedule conversation flow"
>
<defs>
<marker id="rg2ah" markerWidth="10" markerHeight="7"
@@ -130,20 +149,29 @@ function LangGraphFlow() {
</marker>
</defs>
{/* ══ DUAL ENTRY NODES ════════════════════════════════════════════ */}
{/* ══ TRIPLE ENTRY NODES ════════════════════════════════════════════ */}
{/* Left: Reminder SMS */}
<rect x={lEntryCx-entryW/2} y={e1y} width={entryW} height={e1h} rx={8} fill="#EFF6FF" stroke="#3B82F6" strokeWidth={1.5} />
<text x={lEntryCx} y={e1y+22} textAnchor="middle" fontSize={12} fontWeight="600" fill="#1D4ED8">Office sends Reminder SMS</text>
<text x={lEntryCx} y={e1y+40} textAnchor="middle" fontSize={9} fill="#93C5FD">Staff triggers batch send</text>
<text x={lEntryCx} y={e1y+22} textAnchor="middle" fontSize={12} fontWeight="600" fill="#1D4ED8">Office sends Reminder</text>
<text x={lEntryCx} y={e1y+38} textAnchor="middle" fontSize={12} fontWeight="600" fill="#1D4ED8">SMS or Call</text>
<text x={lEntryCx} y={e1y+56} textAnchor="middle" fontSize={9} fill="#93C5FD">Staff triggers batch send</text>
{/* Middle: Recall call/SMS for an existing patient */}
<rect x={mEntryCx-entryW/2} y={e1y} width={entryW} height={e1h} rx={8} fill="#F5F3FF" stroke="#8B5CF6" strokeWidth={1.5} />
<text x={mEntryCx} y={e1y+22} textAnchor="middle" fontSize={12} fontWeight="600" fill="#5B21B6">Office call or SMS for</text>
<text x={mEntryCx} y={e1y+38} textAnchor="middle" fontSize={12} fontWeight="600" fill="#5B21B6">an existing patient</text>
<text x={mEntryCx} y={e1y+56} textAnchor="middle" fontSize={9} fill="#C4B5FD">Recall: free exam &amp; cleaning</text>
{/* Right: Reschedule by Office SMS */}
<rect x={rEntryCx-entryW/2} y={e1y} width={entryW} height={e1h} rx={8} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5} />
<text x={rEntryCx} y={e1y+22} textAnchor="middle" fontSize={12} fontWeight="600" fill="#C2410C">Office sends Reschedule SMS</text>
<text x={rEntryCx} y={e1y+40} textAnchor="middle" fontSize={9} fill="#FDBA74">"Reschedule by Office" template</text>
<text x={rEntryCx} y={e1y+22} textAnchor="middle" fontSize={12} fontWeight="600" fill="#C2410C">Office sends Reschedule</text>
<text x={rEntryCx} y={e1y+38} textAnchor="middle" fontSize={12} fontWeight="600" fill="#C2410C">SMS</text>
<text x={rEntryCx} y={e1y+56} textAnchor="middle" fontSize={9} fill="#FDBA74">"Reschedule by Office" template</text>
{/* Converging lines → horizontal merge → Patient replies */}
<line x1={lEntryCx} y1={e1y+e1h} x2={lEntryCx} y2={mergeY} stroke="#9CA3AF" strokeWidth={1.5} />
<line x1={mEntryCx} y1={e1y+e1h} x2={mEntryCx} y2={mergeY} stroke="#9CA3AF" strokeWidth={1.5} />
<line x1={rEntryCx} y1={e1y+e1h} x2={rEntryCx} y2={mergeY} stroke="#9CA3AF" strokeWidth={1.5} />
<line x1={lEntryCx} y1={mergeY} x2={rEntryCx} y2={mergeY} stroke="#9CA3AF" strokeWidth={1.5} />
<line x1={cx} y1={mergeY} x2={cx} y2={n2y-2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)" />
@@ -158,10 +186,11 @@ function LangGraphFlow() {
{/* N3: AI classifies */}
<rect x={nx} y={n3y} width={nW} height={n3h} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5} />
<text x={cx} y={n3y+20} textAnchor="middle" fontSize={12} fontWeight="700" fill="#065F46">Google AI classifies YES / NO</text>
<text x={cx} y={n3y+36} textAnchor="middle" fontSize={10} fontWeight="600" fill="#1D4ED8">Reminder: intro (MSG 1) + reply (MSG 2)</text>
<text x={cx} y={n3y+52} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Reschedule: reply sent directly</text>
<text x={cx} y={n3y+70} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Date in reply → AI skips to Day Check</text>
<text x={cx} y={n3y+18} textAnchor="middle" fontSize={12} fontWeight="700" fill="#065F46">Google AI classifies YES / NO</text>
<text x={cx} y={n3y+33} textAnchor="middle" fontSize={10} fontWeight="600" fill="#1D4ED8">Reminder: intro (MSG 1) + reply (MSG 2)</text>
<text x={cx} y={n3y+48} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Reschedule: reply sent directly</text>
<text x={cx} y={n3y+63} textAnchor="middle" fontSize={10} fontWeight="600" fill="#5B21B6">Recall: same as Reschedule flow</text>
<text x={cx} y={n3y+82} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Date in reply → AI skips to Day Check</text>
{/* Fork lines */}
<line x1={cx} y1={n3y+n3h} x2={cx} y2={forkHY} stroke="#9CA3AF" strokeWidth={1.5} />
@@ -178,10 +207,11 @@ function LangGraphFlow() {
{/* ══ YES BRANCH ══════════════════════════════════════════════════ */}
<rect x={lcx-yesW/2} y={yes1y} width={yesW} height={yes1h} rx={8} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5} />
<text x={lcx} y={yes1y+18} textAnchor="middle" fontSize={10} fontWeight="600" fill="#15803D">Reminder: Thank you for confirming!</text>
<text x={lcx} y={yes1y+34} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Reschedule: What day &amp; time?</text>
<text x={lcx} y={yes1y+52} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"See you on [date &amp; time]"</text>
<text x={lcx} y={yes1y+70} textAnchor="middle" fontSize={8} fill="#047857">date in reply → Day Check ↘</text>
<text x={lcx} y={yes1y+16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#15803D">Reminder: Thank you for confirming!</text>
<text x={lcx} y={yes1y+31} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Reschedule: What day &amp; time?</text>
<text x={lcx} y={yes1y+46} textAnchor="middle" fontSize={10} fontWeight="600" fill="#5B21B6">Recall: What day &amp; time?</text>
<text x={lcx} y={yes1y+64} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"See you on [date &amp; time]"</text>
<text x={lcx} y={yes1y+80} textAnchor="middle" fontSize={8} fill="#047857">date in reply → Day Check ↘</text>
<line x1={lcx} y1={yes1y+yes1h} x2={lcx} y2={yes2y-2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)" />
{/* Yes2: Patient thanks */}
@@ -191,7 +221,7 @@ function LangGraphFlow() {
{/* Dashed shortcut: YES with date → Day Check */}
<line
x1={lcx + yesW/2} y1={yes1y + 52}
x1={lcx + yesW/2} y1={yes1y + 64}
x2={rcx - noW/2 - 2} y2={no2y + 34}
stroke="#047857" strokeWidth={1} strokeDasharray="4 3"
markerEnd="url(#rg2short)"
@@ -1011,6 +1041,13 @@ export function AiChatSettingsCard() {
const [generalFallback, setGeneralFallback] = useState(DEFAULTS.generalFallback);
const initialized = useRef(false);
const [callGreeting, setCallGreeting] = useState(CALL_DEFAULTS.greeting);
const [callReminderGreeting, setCallReminderGreeting] = useState(CALL_DEFAULTS.reminderGreeting);
const [callNewPatientGreeting, setCallNewPatientGreeting] = useState(CALL_DEFAULTS.newPatientGreeting);
const [callGeneralFallback, setCallGeneralFallback] = useState(CALL_DEFAULTS.generalFallback);
const [callRecallGreeting, setCallRecallGreeting] = useState(CALL_DEFAULTS.recallGreeting);
const callInitialized = useRef(false);
const [openPhoneReply, setOpenPhoneReply] = useState(false);
const { data: advancedSettings } = useQuery<{ openPhoneReply: boolean }>({
@@ -1090,6 +1127,29 @@ export function AiChatSettingsCard() {
}
}, [templates]);
const { data: callTemplatesData, isLoading: callTemplatesLoading } = useQuery<AiCallTemplates>({
queryKey: ["/api/ai/call-templates"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/ai/call-templates");
if (!res.ok) throw new Error("Failed to load call templates");
return res.json();
},
staleTime: Infinity,
refetchOnWindowFocus: false,
});
// Seed AI call templates
useEffect(() => {
if (callTemplatesData && !callInitialized.current) {
callInitialized.current = true;
setCallGreeting(callTemplatesData.greeting || CALL_DEFAULTS.greeting);
setCallReminderGreeting(callTemplatesData.reminderGreeting || CALL_DEFAULTS.reminderGreeting);
setCallNewPatientGreeting(callTemplatesData.newPatientGreeting || CALL_DEFAULTS.newPatientGreeting);
setCallGeneralFallback(callTemplatesData.generalFallback || CALL_DEFAULTS.generalFallback);
setCallRecallGreeting(callTemplatesData.recallGreeting || CALL_DEFAULTS.recallGreeting);
}
}, [callTemplatesData]);
// Seed SMS template list — fall back to the saved reminderSms if list is empty
useEffect(() => {
if (smsTemplateListData && !smsInitialized.current) {
@@ -1120,6 +1180,24 @@ export function AiChatSettingsCard() {
},
});
const saveCallMutation = useMutation({
mutationFn: async (data: AiCallTemplates) => {
const res = await apiRequest("PUT", "/api/ai/call-templates", data);
if (!res.ok) {
const err = await res.json().catch(() => null);
throw new Error(err?.message || "Failed to save call templates");
}
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/ai/call-templates"] });
toast({ title: "Templates saved", description: "AI call templates have been updated." });
},
onError: (err: any) => {
toast({ title: "Error", description: err?.message, variant: "destructive" });
},
});
const saveSmsListMutation = useMutation({
mutationFn: async (list: SmsTemplate[]) => {
const res = await apiRequest("PUT", "/api/twilio/sms-template-list", list);
@@ -1147,6 +1225,17 @@ export function AiChatSettingsCard() {
});
};
const handleCallSubmit = (e: React.FormEvent) => {
e.preventDefault();
saveCallMutation.mutate({
greeting: callGreeting.trim() || CALL_DEFAULTS.greeting,
reminderGreeting: callReminderGreeting.trim() || CALL_DEFAULTS.reminderGreeting,
newPatientGreeting: callNewPatientGreeting.trim() || CALL_DEFAULTS.newPatientGreeting,
generalFallback: callGeneralFallback.trim() || CALL_DEFAULTS.generalFallback,
recallGreeting: callRecallGreeting.trim() || CALL_DEFAULTS.recallGreeting,
});
};
const updateSmsTemplate = (idx: number, field: "name" | "body", value: string) => {
setSmsTemplates((prev) => prev.map((t, i) => i === idx ? { ...t, [field]: value } : t));
};
@@ -1193,6 +1282,54 @@ export function AiChatSettingsCard() {
},
];
const callTemplateFields = [
{
key: "callGreeting",
icon: <PhoneCall className="h-4 w-4 text-primary" />,
label: "Call an existing patient for an appointment",
description: "Lisa's opening line when placing an AI Call to a patient — use {firstName} and {officeName} as placeholders.",
value: callGreeting,
onChange: setCallGreeting,
placeholder: CALL_DEFAULTS.greeting,
},
{
key: "callRecall",
icon: <Stethoscope className="h-4 w-4 text-primary" />,
label: "Recall Call: Existing Patient Check-Up Offer",
description: "Lisa's opening line for a Patient Connection \"AI Call\" — offers an existing patient a routine exam & cleaning (most insurance covers it at no cost) and asks if they'd like to book. Use {firstName} and {officeName} as placeholders.",
value: callRecallGreeting,
onChange: setCallRecallGreeting,
placeholder: CALL_DEFAULTS.recallGreeting,
},
{
key: "callReminder",
icon: <CalendarCheck className="h-4 w-4 text-primary" />,
label: "Reminder Call Reply",
description: "Spoken after the call's opening greeting when the AI is following up on an appointment reminder — keep it short, it's read aloud, not texted.",
value: callReminderGreeting,
onChange: setCallReminderGreeting,
placeholder: CALL_DEFAULTS.reminderGreeting,
},
{
key: "callNewPatient",
icon: <UserPlus className="h-4 w-4 text-primary" />,
label: "New Patient Call Reply",
description: "Spoken when the AI is walking a new patient through scheduling on a live call.",
value: callNewPatientGreeting,
onChange: setCallNewPatientGreeting,
placeholder: CALL_DEFAULTS.newPatientGreeting,
},
{
key: "callGeneral",
icon: <MessageCircle className="h-4 w-4 text-primary" />,
label: "General Call Fallback",
description: "Spoken when the AI cannot determine the context of what the patient said on a call.",
value: callGeneralFallback,
onChange: setCallGeneralFallback,
placeholder: CALL_DEFAULTS.generalFallback,
},
];
return (
<div className="space-y-6">
@@ -1403,6 +1540,86 @@ export function AiChatSettingsCard() {
</CardContent>
</Card>
{/* ── Section 1b: Call Templates ───────────────────────────── */}
<Card>
<CardContent className="py-6 space-y-5">
<div className="flex items-center gap-2">
<PhoneCall className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">Call Templates</h3>
</div>
<p className="text-sm text-muted-foreground">
Customize what Lisa says on live AI phone calls — kept separate from the SMS wording
above since spoken conversation reads differently than a text message. Use{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{officeName}"}</code>{" "}
and{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{firstName}"}</code>{" "}
as placeholders.
</p>
{officeName && (
<div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 rounded px-3 py-2">
<Info className="h-3.5 w-3.5 flex-shrink-0" />
<span>
<span className="font-medium">{"{officeName}"}</span> will display as{" "}
<span className="font-medium text-foreground">"{officeName}"</span>
</span>
</div>
)}
{callTemplatesLoading ? (
<p className="text-sm text-muted-foreground">Loading templates...</p>
) : (
<form onSubmit={handleCallSubmit} className="space-y-5">
{callTemplateFields.map((f) => (
<div key={f.key} className="space-y-1.5">
<div className="flex items-center gap-2">
{f.icon}
<span className="text-sm font-medium">{f.label}</span>
</div>
<p className="text-xs text-muted-foreground">{f.description}</p>
<Textarea
value={f.value}
onChange={(e) => f.onChange(e.target.value)}
placeholder={f.placeholder}
rows={3}
className="text-sm resize-none"
/>
{officeName && f.value.includes("{officeName}") && (
<p className="text-xs text-muted-foreground italic pl-1">
Preview: {previewTemplate(f.value, officeName)}
</p>
)}
</div>
))}
<div className="flex items-center gap-3 pt-1">
<Button
type="submit"
disabled={saveCallMutation.isPending}
className="bg-teal-600 hover:bg-teal-700 text-white"
>
{saveCallMutation.isPending ? "Saving..." : "Save Templates"}
</Button>
<Button
type="button"
variant="ghost"
className="text-xs text-muted-foreground"
onClick={() => {
setCallGreeting(CALL_DEFAULTS.greeting);
setCallReminderGreeting(CALL_DEFAULTS.reminderGreeting);
setCallNewPatientGreeting(CALL_DEFAULTS.newPatientGreeting);
setCallGeneralFallback(CALL_DEFAULTS.generalFallback);
setCallRecallGreeting(CALL_DEFAULTS.recallGreeting);
}}
>
Reset to defaults
</Button>
</div>
</form>
)}
</CardContent>
</Card>
{/* ── Section 2: LangGraph Flow ────────────────────────────── */}
<Card>
<CardContent className="py-6 space-y-5">
+49 -2
View File
@@ -114,13 +114,12 @@ interface ScheduledAppointment {
function deriveInsuranceSiteKey(provider: string | null | undefined): string {
const p = (provider || "").toLowerCase().trim();
if (!p) return "";
if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MH";
if (p.includes("masshealth") || p === "mh" || p === "mass health" || p.includes("cmsp")) return "MH";
if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
if (p.includes("ddma") || p.includes("delta dental ma")) return "DDMA";
if (p.includes("delta ins") || p === "deltains") return "DeltaIns";
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO";
if ((p.includes("united") && p.includes("sco")) || p === "unitedsco") return "UnitedSCO";
if (p.includes("cmsp")) return "CMSP";
if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS";
if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR";
if (p.includes("aetna")) return "Aetna";
@@ -199,6 +198,11 @@ export default function AppointmentsPage() {
const [isClaimingAiColumn, setIsClaimingAiColumn] = useState(false);
const [autoClaimEnabled, setAutoClaimEnabled] = useState(false);
const [autoClaimTime, setAutoClaimTime] = useState("19:00");
const [autoConfirmEnabled, setAutoConfirmEnabled] = useState(
() => localStorage.getItem("ai_claim_auto_confirm") === "1"
);
// True while the current AI claim run should advance without manual Confirm clicks
const [aiClaimAutoRun, setAiClaimAutoRun] = useState(false);
const autoClaimFiredHourRef = useRef<number | null>(null);
const [aiClaimModalOpen, setAiClaimModalOpen] = useState(false);
const [aiClaimQueue, setAiClaimQueue] = useState<ScheduledAppointment[]>([]);
@@ -488,6 +492,7 @@ export default function AppointmentsPage() {
sessionStorage.setItem("ai_claim_queue", JSON.stringify({ ...parsed, pendingResume: false }));
setAiClaimQueue(queue);
setAiClaimCurrentIndex(0);
setAiClaimAutoRun(!!parsed.autoRun);
setAiClaimModalOpen(true);
processAiClaimAtIndex(queue, 0);
} catch {
@@ -1652,13 +1657,16 @@ export default function AppointmentsPage() {
toast({ title: "No unclaimed appointments", description: "All appointments in the selected columns are already claimed." });
return;
}
const autoRun = autoConfirmEnabled || autoClaimEnabled;
sessionStorage.setItem("ai_claim_queue", JSON.stringify({
appointments: unclaimed,
date: formattedSelectedDate,
pendingResume: false,
autoRun,
}));
setAiClaimQueue(unclaimed);
setAiClaimCurrentIndex(0);
setAiClaimAutoRun(autoRun);
setAiClaimModalOpen(true);
await processAiClaimAtIndex(unclaimed, 0);
};
@@ -1673,6 +1681,7 @@ export default function AppointmentsPage() {
appointments: fromCurrent,
date: formattedSelectedDate,
pendingResume: true,
autoRun: aiClaimAutoRun,
}));
} else {
sessionStorage.removeItem("ai_claim_queue");
@@ -1760,6 +1769,28 @@ export default function AppointmentsPage() {
}
};
// Auto mode: advance the AI claim queue without manual Confirm clicks.
// Confirms when codes matched, skips otherwise (incl. CDT clarifications, which need human input).
useEffect(() => {
if (!aiClaimModalOpen || !aiClaimAutoRun || isAiClaimProcessing) return;
if (aiClaimCdtClarification) {
const t = setTimeout(() => {
setAiClaimCdtClarification(null);
handleAiClaimSkip();
}, 1500);
return () => clearTimeout(t);
}
if (!aiClaimCurrentData) return;
const t = setTimeout(() => {
if (aiClaimCurrentData.matchedCodes.length > 0) {
handleAiClaimConfirm();
} else {
handleAiClaimSkip();
}
}, 1500);
return () => clearTimeout(t);
}, [aiClaimModalOpen, aiClaimAutoRun, isAiClaimProcessing, aiClaimCurrentData, aiClaimCdtClarification]);
return (
<div>
<SeleniumTaskBanner
@@ -1907,6 +1938,19 @@ export default function AppointmentsPage() {
</span>
</label>
))}
<div className="flex items-center gap-2 border-l pl-3 ml-1">
<Switch
id="auto-confirm-toggle"
checked={autoConfirmEnabled}
onCheckedChange={(v) => {
setAutoConfirmEnabled(v);
localStorage.setItem("ai_claim_auto_confirm", v ? "1" : "0");
}}
/>
<Label htmlFor="auto-confirm-toggle" className="text-sm font-medium cursor-pointer whitespace-nowrap">
Auto Confirm
</Label>
</div>
<div className="flex items-center gap-2 border-l pl-3 ml-1">
<Switch
id="auto-claim-toggle"
@@ -2485,6 +2529,9 @@ export default function AppointmentsPage() {
AI Claim Queue
</h3>
<span className="text-xs text-gray-500 font-medium">
{aiClaimAutoRun && (
<span className="text-teal-600 mr-2">Auto</span>
)}
{aiClaimCurrentIndex + 1} / {aiClaimQueue.length}
</span>
</div>
@@ -10,6 +10,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
@@ -122,6 +123,7 @@ export default function InsuranceStatusPage() {
const [isCheckingEligibilityHistory, setIsCheckingEligibilityHistory] =
useState(false);
const [isCheckingCMSP, setIsCheckingCMSP] = useState(false);
const [showPdfViewerOnComplete, setShowPdfViewerOnComplete] = useState(false);
// AI Call Insurance section
const [aiCallOpen, setAiCallOpen] = useState(false);
@@ -710,7 +712,7 @@ export default function InsuranceStatusPage() {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: msg }))
);
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "CMSP PDFs saved to Documents." }));
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "MH PDFs saved to Documents." }));
toast({
title: "Done",
description: "Eligibility, history, and accumulator PDFs saved.",
@@ -727,14 +729,14 @@ export default function InsuranceStatusPage() {
// Open 4-panel modal
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
setCmspEligibilityPdfId(jobResult.pdfFileId ?? null);
setCmspEligibilityFilename(jobResult.pdfFilename ?? `cmsp_eligibility_${memberId}.pdf`);
setCmspEligibilityFilename(jobResult.pdfFilename ?? `mh_eligibility_${memberId}.pdf`);
setCmspMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ?? null);
setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `cmsp_member_details_${memberId}.pdf`);
setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `mh_member_details_${memberId}.pdf`);
setCmspHistoryPdfId(jobResult.historyPdfFileId ?? null);
setCmspHistoryFilename(jobResult.historyPdfFilename ?? `cmsp_history_${memberId}.pdf`);
setCmspHistoryFilename(jobResult.historyPdfFilename ?? `mh_history_${memberId}.pdf`);
setCmspAccumulatorPdfId(jobResult.accumulatorPdfFileId ?? null);
setCmspAccumulatorFilename(jobResult.accumulatorPdfFilename ?? `cmsp_accumulator_${memberId}.pdf`);
setCmspPreviewOpen(true);
setCmspAccumulatorFilename(jobResult.accumulatorPdfFilename ?? `mh_accumulator_${memberId}.pdf`);
if (showPdfViewerOnComplete) setCmspPreviewOpen(true);
}
} catch (error: any) {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
@@ -750,11 +752,9 @@ export default function InsuranceStatusPage() {
const check = pendingAutoCheck.current;
pendingAutoCheck.current = null;
if (check === "mh" || check === "mh-history") {
if (isUnder14(dateOfBirth)) {
handleCMSPButton();
} else {
handleMHEligibilityHistoryButton();
}
// Temporarily disabled: selenium_MH_eligibilityHistoryCheckWorker.py is no
// longer used — all MH eligibility checks route through the CMSP/MH worker.
handleCMSPButton();
} else if (check === "cmsp") {
handleCMSPButton();
} else {
@@ -842,12 +842,13 @@ export default function InsuranceStatusPage() {
const patient = await lookupRes.json();
if (!patient?.id) return;
// The eligibility check that just ran saved the result to patient.status — only book
// the appointment when insurance came back ACTIVE.
if (patient.status !== "ACTIVE") {
// The eligibility check that just ran saved the result to patient.status — only skip
// booking when insurance came back explicitly INACTIVE. ACTIVE and UNKNOWN (e.g. a
// scrape that couldn't confirm status but didn't find an ineligible record) still book.
if (patient.status === "INACTIVE") {
toast({
title: "No appointment created",
description: `${patient.firstName ?? ""} ${patient.lastName ?? ""} insurance is ${patient.status === "INACTIVE" ? "inactive" : "not active"} — please add manually if needed.`.trim(),
description: `${patient.firstName ?? ""} ${patient.lastName ?? ""} insurance is inactive — please add manually if needed.`.trim(),
variant: "destructive",
});
return;
@@ -1020,82 +1021,7 @@ export default function InsuranceStatusPage() {
</div>
</div>
<div className="flex flex-col-2 gap-4">
<Button
onClick={() => handleMHEligibilityButton()}
className="w-full"
disabled={isCheckingEligibilityStatus}
>
{isCheckingEligibilityStatus ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
MH Eligibility
</>
)}
</Button>
<Button
onClick={() => handleMHEligibilityAppointmentButton()}
className="w-full"
disabled={isCheckingEligibilityAppointment}
>
{isCheckingEligibilityAppointment ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
MH Eligibility&Appointment
</>
)}
</Button>
<Button
onClick={() => handleMHEligibilityClaimsPreAuthButton()}
className="w-full"
disabled={isCheckingEligibilityClaimsPreAuth}
>
{isCheckingEligibilityClaimsPreAuth ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
MH Eligibility&Claims/PreAuth
</>
)}
</Button>
</div>
<div className="flex flex-col-2 gap-4 mt-4">
<Button
id="eligibility-mh-history"
className="w-full"
disabled={isCheckingEligibilityHistory}
onClick={() => handleMHEligibilityHistoryButton()}
>
{isCheckingEligibilityHistory ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
MH Eligibility &amp; History
</>
)}
</Button>
<Button
id="eligibility-cmsp"
className="w-full"
@@ -1110,12 +1036,23 @@ export default function InsuranceStatusPage() {
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
CMSP Eligibility &amp; History &amp; Remaining
MH Eligibility &amp; Remaining &amp; History
</>
)}
</Button>
</div>
<div className="flex items-center gap-2 mt-2">
<Switch
id="mh-show-pdf-viewer"
checked={showPdfViewerOnComplete}
onCheckedChange={setShowPdfViewerOnComplete}
/>
<Label htmlFor="mh-show-pdf-viewer" className="text-sm text-muted-foreground font-normal">
Show PDF viewer after check completes
</Label>
</div>
{/* TEMP PROVIDER BUTTONS */}
<div id="eligibility-other-providers" className="space-y-4 mt-6">
<h3 className="text-sm font-medium text-muted-foreground">
@@ -1463,6 +1400,7 @@ export default function InsuranceStatusPage() {
allowDelete={true}
allowCheckbox={true}
allowEdit={true}
showEligibilityColumns={true}
onSelectPatient={setSelectedPatient}
/>
</CardContent>
@@ -1479,7 +1417,6 @@ export default function InsuranceStatusPage() {
}}
pdfId={previewPdfId ?? undefined}
fallbackFilename={previewFallbackFilename ?? undefined}
autoDownload
/>
{/* 4-panel modal for CMSP — eligibility, member details, history, accumulator */}
@@ -1496,13 +1433,12 @@ export default function InsuranceStatusPage() {
setCmspAccumulatorPdfId(null);
setCmspAccumulatorFilename(null);
}}
title="CMSP Eligibility, History &amp; Remaining"
title="MH Eligibility, Remaining &amp; History"
panels={[
{
pdfId: cmspEligibilityPdfId,
fallbackFilename: cmspEligibilityFilename,
label: "Eligibility",
autoDownload: true,
},
{
pdfId: cmspMemberDetailsPdfId,
@@ -1540,7 +1476,6 @@ export default function InsuranceStatusPage() {
pdfId: dualEligibilityPdfId,
fallbackFilename: dualEligibilityFilename,
label: "Eligibility",
autoDownload: true,
},
{
pdfId: dualMemberDetailsPdfId,
@@ -24,6 +24,7 @@ import {
Send,
X,
MoonStar,
Bot,
} from "lucide-react";
import { SmsTemplateDialog } from "@/components/patient-connection/sms-template-diaog";
import { MessageThread } from "@/components/patient-connection/message-thread";
@@ -103,6 +104,32 @@ export default function PatientConnectionPage() {
},
});
const makeAiCallMutation = useMutation({
mutationFn: async ({
to,
patientId,
}: {
to: string;
patientId: number;
}) => {
return apiRequest("POST", "/api/twilio/make-ai-call", { to, patientId, purpose: "recall" });
},
onSuccess: () => {
toast({
title: "AI Call Started",
description: "Lisa is calling the patient now.",
});
},
onError: (error: any) => {
toast({
title: "AI Call Failed",
description:
error.message || "Unable to place the AI call. Please try again.",
variant: "destructive",
});
},
});
// Fetch all patients from database
const { data: patients = [], isLoading } = useQuery<Patient[]>({
queryKey: ["/api/patients"],
@@ -139,6 +166,22 @@ export default function PatientConnectionPage() {
});
};
// Handle AI-driven call via Twilio
const handleAiCall = (patient: Patient) => {
if (!patient.phone?.trim()) {
toast({
title: "No Phone Number",
description: "This patient does not have a phone number on file.",
variant: "destructive",
});
return;
}
makeAiCallMutation.mutate({
to: patient.phone,
patientId: Number(patient.id),
});
};
// Handle sending SMS
const handleSMS = (patient: Patient) => {
setSelectedPatient(patient);
@@ -343,6 +386,15 @@ export default function PatientConnectionPage() {
<Phone className="h-4 w-4 mr-1" />
Call
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleAiCall(patient)}
data-testid={`button-ai-call-${patient.id}`}
>
<Bot className="h-4 w-4 mr-1" />
AI Call
</Button>
<Button
variant="outline"
size="sm"
+1
View File
@@ -1,3 +1,4 @@
google_credentials.json
astute-office-469222-k3-ba714473050d.json
__pycache__/
@@ -234,7 +234,7 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
print("No new tab for eligibility; printing current page directly")
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
pdf_filename = f"cmsp_eligibility_{safe_member}.pdf"
pdf_filename = f"mh_eligibility_{safe_member}.pdf"
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
pdf_bytes = base64.b64decode(pdf_data["data"])
pdf_path = os.path.join(self.download_dir, pdf_filename)
@@ -279,14 +279,57 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
# ── step 3b — print member details page as PDF via CDP ───────────────────────
def extract_third_party_insurance(self):
"""Reads the "Third Party Liability" table (Policy Number | Coverage Type |
Effective Date | Termination Date | Insurer Name | Insurer Payment Order)
and returns the Insurer Name(s), or None if the table has no rows
(i.e. the member has no third-party/2nd insurance).
"""
try:
tables = self.driver.find_elements(By.TAG_NAME, "table")
for table in tables:
rows = table.find_elements(By.TAG_NAME, "tr")
if not rows:
continue
header_cells = [
c.text.strip().lower()
for c in (rows[0].find_elements(By.TAG_NAME, "th") or rows[0].find_elements(By.TAG_NAME, "td"))
]
if "insurer name" not in header_cells:
continue
insurer_idx = header_cells.index("insurer name")
insurers = []
for row in rows[1:]:
cells = row.find_elements(By.TAG_NAME, "td")
if len(cells) <= insurer_idx:
continue
name = cells[insurer_idx].text.strip()
if name:
insurers.append(name)
third_party = ", ".join(insurers) if insurers else None
self.extracted_data["third_party_insurance"] = third_party
print(f"[extract_third_party_insurance] {third_party!r}")
return third_party
print("[extract_third_party_insurance] No 'Third Party Liability' table found")
return None
except Exception as e:
print(f"[extract_third_party_insurance] failed: {e}")
return None
def step3b_member_details_pdf(self):
wait = WebDriverWait(self.driver, 30)
try:
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
time.sleep(2)
self.extract_third_party_insurance()
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
pdf_filename = f"cmsp_member_details_{safe_member}.pdf"
pdf_filename = f"mh_member_details_{safe_member}.pdf"
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
pdf_bytes = base64.b64decode(pdf_data["data"])
pdf_path = os.path.join(self.download_dir, pdf_filename)
@@ -341,7 +384,7 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
time.sleep(3)
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
pdf_filename = f"cmsp_history_{safe_member}.pdf"
pdf_filename = f"mh_history_{safe_member}.pdf"
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
pdf_bytes = base64.b64decode(pdf_data["data"])
pdf_path = os.path.join(self.download_dir, pdf_filename)
@@ -353,7 +396,68 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
print(f"[step5_service_history_pdf] failed: {e}")
return None
# ── step 6 — Tab B: accumulator → wait for data → CDP print ─────────────────
# ── step 6 — Tab B: accumulator → extract remaining → CDP print ─────────────
def extract_remaining_amount(self):
"""Reads the accumulator benefits table and returns the Remaining amount.
Layout (see sample PDF "Accumulator Details"):
Type | Level | Network | Benefit Period | Period | Limit | Applied | Remaining
Prefers the Type="Maximum", Level="Individual" row; falls back to the
first data row with a non-empty Remaining cell if that combination
isn't found (payer may only return one row, or reorder columns).
"""
try:
wait = WebDriverWait(self.driver, 15)
wait.until(EC.presence_of_element_located((By.TAG_NAME, "table")))
tables = self.driver.find_elements(By.TAG_NAME, "table")
for table in tables:
rows = table.find_elements(By.TAG_NAME, "tr")
if not rows:
continue
header_cells = [
c.text.strip().lower()
for c in (rows[0].find_elements(By.TAG_NAME, "th") or rows[0].find_elements(By.TAG_NAME, "td"))
]
if "remaining" not in header_cells:
continue
remaining_idx = header_cells.index("remaining")
type_idx = header_cells.index("type") if "type" in header_cells else None
level_idx = header_cells.index("level") if "level" in header_cells else None
fallback_value = None
best_value = None
for row in rows[1:]:
cells = row.find_elements(By.TAG_NAME, "td")
if len(cells) <= remaining_idx:
continue
cell_texts = [c.text.strip() for c in cells]
value = cell_texts[remaining_idx]
if not value:
continue
if fallback_value is None:
fallback_value = value
type_ok = type_idx is None or "maximum" in cell_texts[type_idx].lower()
level_ok = level_idx is None or "individual" in cell_texts[level_idx].lower()
if type_ok and level_ok:
best_value = value
break
remaining_value = best_value or fallback_value
if remaining_value:
self.extracted_data["remaining_amount"] = remaining_value
print(f"[extract_remaining_amount] Remaining: {remaining_value}")
return remaining_value
print("[extract_remaining_amount] No accumulator table with a 'Remaining' column found")
return None
except Exception as e:
print(f"[extract_remaining_amount] failed: {e}")
return None
def step6_accumulator_pdf(self):
wait = WebDriverWait(self.driver, 30)
@@ -379,8 +483,10 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
time.sleep(4)
self.extract_remaining_amount()
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
pdf_filename = f"cmsp_accumulator_{safe_member}.pdf"
pdf_filename = f"mh_accumulator_{safe_member}.pdf"
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
pdf_bytes = base64.b64decode(pdf_data["data"])
pdf_path = os.path.join(self.download_dir, pdf_filename)
@@ -366,16 +366,26 @@ class AutomationDeltaDentalMAEligibilityCheck:
search_btn.click()
print("[DDMA step1] Clicked Search button")
# Wait for the Members results table to render
# Wait for either the Members results table to render, or (when the
# Subscriber ID + DOB match exactly one family member) DDMA to skip
# the results list and navigate straight to the patient detail page.
# Note: the detail page URL is nested UNDER the search page path
# (".../check-patient-benefits-search/check-patient-benefit-details"),
# so "check-patient-benefits-search" alone can't distinguish them —
# only the detail page's own "-benefit-details" segment can.
try:
WebDriverWait(self.driver, 15).until(
EC.presence_of_element_located((By.XPATH, "//table//tbody//tr"))
lambda d: d.find_elements(By.XPATH, "//table//tbody//tr")
or "check-patient-benefit-details" in d.current_url.lower()
)
time.sleep(2)
except TimeoutException:
print("[DDMA step1] Warning: No results table appeared within timeout")
return "ERROR: INVALID SEARCH CRITERIA"
if "check-patient-benefit-details" in self.driver.current_url.lower():
print(f"[DDMA step1] Search navigated directly to detail page: {self.driver.current_url}")
print("[DDMA step1] Search completed successfully")
return "Success"
@@ -390,26 +400,40 @@ class AutomationDeltaDentalMAEligibilityCheck:
try:
import re
# Wait for results table, then pause for full render
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
)
time.sleep(2) # Let the row content fully render after table appears
except TimeoutException:
print("[DDMA step2] Warning: Results table not found within timeout")
eligibilityText = "unknown"
foundMemberId = self.memberId or ""
patientName = ""
# If the caller already knows this patient (e.g. an existing patient record
# being rechecked), use that name directly — no need to scrape/parse it off
# the DDMA page, only the eligibility status is actually needed.
patientName = f"{self.firstName} {self.lastName}".strip() if self.firstName and self.lastName else ""
known_patient_name = bool(patientName)
# The Members table lists the whole family sharing this subscriber ID —
# find the row matching this patient's DOB (falls back to last name, then first row).
rows = self.driver.find_elements(By.XPATH, "//table//tbody//tr")
print(f"[DDMA step2] Found {len(rows)} family member row(s)")
# DDMA now sometimes skips the family-members results table entirely and
# navigates straight to the single-patient detail page (when Subscriber ID +
# DOB match exactly one person). In that case there's no members table to
# parse here — skip straight to the detail-page fallback extraction below.
already_on_detail_page = "check-patient-benefit-details" in self.driver.current_url.lower()
rows = []
target_row = None
if not already_on_detail_page:
# Wait for results table, then pause for full render
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
)
time.sleep(2) # Let the row content fully render after table appears
except TimeoutException:
print("[DDMA step2] Warning: Results table not found within timeout")
# The Members table lists the whole family sharing this subscriber ID —
# find the row matching this patient's DOB (falls back to last name, then first row).
rows = self.driver.find_elements(By.XPATH, "//table//tbody//tr")
print(f"[DDMA step2] Found {len(rows)} family member row(s)")
else:
print(f"[DDMA step2] Already on detail page: {self.driver.current_url}")
if self.dateOfBirth and rows:
try:
dob_parts = self.dateOfBirth.split("-")
@@ -436,17 +460,17 @@ class AutomationDeltaDentalMAEligibilityCheck:
if target_row is not None:
try:
cells = target_row.find_elements(By.XPATH, "./td")
if cells:
if cells and not known_patient_name:
patientName = cells[0].text.strip()
print(f"[DDMA step2] Extracted patient name from row: '{patientName}'")
if len(cells) >= 2 and cells[1].text.strip():
foundMemberId = cells[1].text.strip()
if len(cells) >= 7:
status_text = cells[6].text.strip().lower()
if "active" in status_text:
eligibilityText = "active"
elif "inactive" in status_text or "termed" in status_text:
if "inactive" in status_text or "termed" in status_text:
eligibilityText = "inactive"
elif "active" in status_text:
eligibilityText = "active"
else:
eligibilityText = status_text
print(f"[DDMA step2] Found eligibility status: {eligibilityText}")
@@ -463,10 +487,10 @@ class AutomationDeltaDentalMAEligibilityCheck:
else:
print("[DDMA step2] Warning: No family member rows found in search results")
# Wait for the detail page to load (URL moves away from the search page)
# Wait for the detail page to load (URL gains the "-benefit-details" segment)
try:
WebDriverWait(self.driver, 15).until(
lambda d: "check-patient-benefits-search" not in d.current_url
lambda d: "check-patient-benefit-details" in d.current_url.lower()
)
print(f"[DDMA step2] Navigated to detail page: {self.driver.current_url}")
except Exception:
@@ -522,11 +546,13 @@ class AutomationDeltaDentalMAEligibilityCheck:
# forward past accessibility text to the first plausible name value.
for i, line in enumerate(page_lines):
# Case 1: "Name : Value" or "Name: Value" on the same line
# (DDMA's detail page labels this "Subscriber name:", so that
# prefix must be accepted rather than excluded)
same_line = re.match(
r'^(?:member\s+)?name\s*[:\-]\s*(.+)$', line, re.IGNORECASE
r'^(?:member\s+|subscriber\s+)?name\s*[:\-]\s*(.+)$', line, re.IGNORECASE
)
if same_line and not re.search(
r'(provider|group|subscriber|plan)\s+name', line, re.IGNORECASE
r'(provider|group|plan)\s+name', line, re.IGNORECASE
):
candidate = same_line.group(1).strip()
if looks_like_name(candidate):
@@ -535,7 +561,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
break
# Case 2: "Name" or "Name:" alone on a line — scan forward for value
elif re.match(r'^(?:member\s+)?name\s*:?\s*$', line, re.IGNORECASE):
elif re.match(r'^(?:member\s+|subscriber\s+)?name\s*:?\s*$', line, re.IGNORECASE):
for j in range(i + 1, min(i + 6, len(page_lines))):
candidate = page_lines[j].strip()
if looks_like_name(candidate):
@@ -554,6 +580,27 @@ class AutomationDeltaDentalMAEligibilityCheck:
if not patientName:
print("[DDMA step2] Could not extract patient name from detail page")
# Fallback: refine eligibility status from the detail page's "Coverage is:"
# label if the family-table row extraction didn't set it (e.g. DDMA
# navigated straight to the detail page, skipping the members table).
if eligibilityText == "unknown":
try:
page_text = self.driver.execute_script("return document.body.innerText;")
coverage_match = re.search(
r'coverage\s+is\s*[:\-]?\s*([A-Za-z]+)', page_text, re.IGNORECASE
)
if coverage_match:
status_text = coverage_match.group(1).strip().lower()
if "inactive" in status_text or "termed" in status_text:
eligibilityText = "inactive"
elif "active" in status_text:
eligibilityText = "active"
else:
eligibilityText = status_text
print(f"[DDMA step2] Extracted eligibility from 'Coverage is:' label: '{eligibilityText}'")
except Exception as e:
print(f"[DDMA step2] Could not extract eligibility from 'Coverage is:' label: {e}")
# Wait for the page to be fully ready before capturing PDF
try:
WebDriverWait(self.driver, 30).until(
@@ -1096,11 +1096,16 @@ class AutomationUnitedDHClaimSubmit:
print(f"[UnitedDH Claim] step7: Could not click caret (section may already be open): {e}")
attached = 0
seen_paths = set()
for cf in self.claimFiles:
relative_path = cf.get("filePath") or ""
if not relative_path:
print(f"[UnitedDH Claim] step7: Skipping file with no filePath: {cf}")
continue
if relative_path in seen_paths:
print(f"[UnitedDH Claim] step7: Skipping duplicate file: {relative_path}")
continue
seen_paths.add(relative_path)
abs_path = os.path.normpath(os.path.join(_BACKEND_CWD, relative_path.lstrip("/")))
if not os.path.isfile(abs_path):
@@ -1113,6 +1118,10 @@ class AutomationUnitedDHClaimSubmit:
EC.presence_of_element_located((By.XPATH, "//input[@type='file']"))
)
self.driver.execute_script("arguments[0].removeAttribute('class');", file_input)
# The input is a multi-file input and chromedriver APPENDS on send_keys,
# so a leftover selection from the previous iteration re-uploads the
# earlier file (2 attachments became 3). Clear it first.
self.driver.execute_script("arguments[0].value = '';", file_input)
file_input.send_keys(abs_path)
WebDriverWait(self.driver, 60).until(
EC.element_to_be_clickable((By.XPATH,
@@ -1035,11 +1035,16 @@ class AutomationUnitedDHPreAuth:
print(f"[UnitedDH PreAuth] step7: Could not click caret (section may already be open): {e}")
attached = 0
seen_paths = set()
for cf in self.claimFiles:
relative_path = cf.get("filePath") or ""
if not relative_path:
print(f"[UnitedDH PreAuth] step7: Skipping file with no filePath: {cf}")
continue
if relative_path in seen_paths:
print(f"[UnitedDH PreAuth] step7: Skipping duplicate file: {relative_path}")
continue
seen_paths.add(relative_path)
abs_path = os.path.normpath(os.path.join(_BACKEND_CWD, relative_path.lstrip("/")))
if not os.path.isfile(abs_path):
@@ -1052,6 +1057,10 @@ class AutomationUnitedDHPreAuth:
EC.presence_of_element_located((By.XPATH, "//input[@type='file']"))
)
self.driver.execute_script("arguments[0].removeAttribute('class');", file_input)
# The input is a multi-file input and chromedriver APPENDS on send_keys,
# so a leftover selection from the previous iteration re-uploads the
# earlier file (2 attachments became 3). Clear it first.
self.driver.execute_script("arguments[0].value = '';", file_input)
file_input.send_keys(abs_path)
WebDriverWait(self.driver, 60).until(
EC.element_to_be_clickable((By.XPATH,
@@ -694,7 +694,7 @@ class AutomationMassHealthClaimsLogin:
print("DEBUG: Clicking plus button to add new service line")
# Step 1: Wait a moment to ensure the previous service line is fully processed
time.sleep(3)
time.sleep(1)
# Step 2: Try multiple approaches to find the plus button
plus_button = None
@@ -768,11 +768,11 @@ class AutomationMassHealthClaimsLogin:
print(f"DEBUG: JavaScript approach failed: {e}")
raise Exception("Could not find plus button with any method")
time.sleep(1)
time.sleep(0.5)
# Step 3: Scroll into view
self.driver.execute_script("arguments[0].scrollIntoView(true);", plus_button)
time.sleep(1)
time.sleep(0.5)
# Step 4: Check if button is enabled
is_disabled = plus_button.get_attribute("disabled")
@@ -804,8 +804,8 @@ class AutomationMassHealthClaimsLogin:
actions = ActionChains(self.driver)
actions.move_to_element(plus_button).click().perform()
time.sleep(2) # Wait for new row to be created
time.sleep(1) # Wait for new row to be created
print("DEBUG: Successfully clicked plus button")
return "Success"
except Exception as e:
+4 -4
View File
@@ -17,7 +17,7 @@ server {
# API requests → backend (Authorization header must be explicit or it gets stripped)
location /api/ {
proxy_pass http://localhost:5000;
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -27,7 +27,7 @@ server {
# Socket.IO → backend (WebSocket upgrade)
location /socket.io/ {
proxy_pass http://localhost:5000;
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -37,7 +37,7 @@ server {
# Everything else → Vite dev server
location / {
proxy_pass http://localhost:3000;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -57,7 +57,7 @@ server {
client_max_body_size 50m;
location /api/twilio/ {
proxy_pass http://localhost:5000;
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
File diff suppressed because one or more lines are too long
@@ -152,6 +152,8 @@ exports.Prisma.PatientScalarFieldEnum = {
medicalConditions: 'medicalConditions',
preferredLanguage: 'preferredLanguage',
status: 'status',
remainingBenefit: 'remainingBenefit',
thirdPartyInsurance: 'thirdPartyInsurance',
userId: 'userId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
+145 -1
View File
@@ -6714,6 +6714,8 @@ export namespace Prisma {
medicalConditions: string | null
preferredLanguage: string | null
status: $Enums.PatientStatus | null
remainingBenefit: string | null
thirdPartyInsurance: string | null
userId: number | null
createdAt: Date | null
updatedAt: Date | null
@@ -6738,6 +6740,8 @@ export namespace Prisma {
medicalConditions: string | null
preferredLanguage: string | null
status: $Enums.PatientStatus | null
remainingBenefit: string | null
thirdPartyInsurance: string | null
userId: number | null
createdAt: Date | null
updatedAt: Date | null
@@ -6762,6 +6766,8 @@ export namespace Prisma {
medicalConditions: number
preferredLanguage: number
status: number
remainingBenefit: number
thirdPartyInsurance: number
userId: number
createdAt: number
updatedAt: number
@@ -6798,6 +6804,8 @@ export namespace Prisma {
medicalConditions?: true
preferredLanguage?: true
status?: true
remainingBenefit?: true
thirdPartyInsurance?: true
userId?: true
createdAt?: true
updatedAt?: true
@@ -6822,6 +6830,8 @@ export namespace Prisma {
medicalConditions?: true
preferredLanguage?: true
status?: true
remainingBenefit?: true
thirdPartyInsurance?: true
userId?: true
createdAt?: true
updatedAt?: true
@@ -6846,6 +6856,8 @@ export namespace Prisma {
medicalConditions?: true
preferredLanguage?: true
status?: true
remainingBenefit?: true
thirdPartyInsurance?: true
userId?: true
createdAt?: true
updatedAt?: true
@@ -6957,6 +6969,8 @@ export namespace Prisma {
medicalConditions: string | null
preferredLanguage: string | null
status: $Enums.PatientStatus
remainingBenefit: string | null
thirdPartyInsurance: string | null
userId: number
createdAt: Date
updatedAt: Date
@@ -7000,6 +7014,8 @@ export namespace Prisma {
medicalConditions?: boolean
preferredLanguage?: boolean
status?: boolean
remainingBenefit?: boolean
thirdPartyInsurance?: boolean
userId?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -7035,6 +7051,8 @@ export namespace Prisma {
medicalConditions?: boolean
preferredLanguage?: boolean
status?: boolean
remainingBenefit?: boolean
thirdPartyInsurance?: boolean
userId?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -7060,6 +7078,8 @@ export namespace Prisma {
medicalConditions?: boolean
preferredLanguage?: boolean
status?: boolean
remainingBenefit?: boolean
thirdPartyInsurance?: boolean
userId?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -7085,12 +7105,14 @@ export namespace Prisma {
medicalConditions?: boolean
preferredLanguage?: boolean
status?: boolean
remainingBenefit?: boolean
thirdPartyInsurance?: boolean
userId?: boolean
createdAt?: boolean
updatedAt?: boolean
}
export type PatientOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "firstName" | "lastName" | "dateOfBirth" | "gender" | "phone" | "email" | "address" | "city" | "zipCode" | "insuranceProvider" | "insuranceId" | "groupNumber" | "policyHolder" | "allergies" | "medicalConditions" | "preferredLanguage" | "status" | "userId" | "createdAt" | "updatedAt", ExtArgs["result"]["patient"]>
export type PatientOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "firstName" | "lastName" | "dateOfBirth" | "gender" | "phone" | "email" | "address" | "city" | "zipCode" | "insuranceProvider" | "insuranceId" | "groupNumber" | "policyHolder" | "allergies" | "medicalConditions" | "preferredLanguage" | "status" | "remainingBenefit" | "thirdPartyInsurance" | "userId" | "createdAt" | "updatedAt", ExtArgs["result"]["patient"]>
export type PatientInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
user?: boolean | UserDefaultArgs<ExtArgs>
appointments?: boolean | Patient$appointmentsArgs<ExtArgs>
@@ -7144,6 +7166,8 @@ export namespace Prisma {
medicalConditions: string | null
preferredLanguage: string | null
status: $Enums.PatientStatus
remainingBenefit: string | null
thirdPartyInsurance: string | null
userId: number
createdAt: Date
updatedAt: Date
@@ -7598,6 +7622,8 @@ export namespace Prisma {
readonly medicalConditions: FieldRef<"Patient", 'String'>
readonly preferredLanguage: FieldRef<"Patient", 'String'>
readonly status: FieldRef<"Patient", 'PatientStatus'>
readonly remainingBenefit: FieldRef<"Patient", 'String'>
readonly thirdPartyInsurance: FieldRef<"Patient", 'String'>
readonly userId: FieldRef<"Patient", 'Int'>
readonly createdAt: FieldRef<"Patient", 'DateTime'>
readonly updatedAt: FieldRef<"Patient", 'DateTime'>
@@ -48204,6 +48230,8 @@ export namespace Prisma {
medicalConditions: 'medicalConditions',
preferredLanguage: 'preferredLanguage',
status: 'status',
remainingBenefit: 'remainingBenefit',
thirdPartyInsurance: 'thirdPartyInsurance',
userId: 'userId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
@@ -49210,6 +49238,8 @@ export namespace Prisma {
medicalConditions?: StringNullableFilter<"Patient"> | string | null
preferredLanguage?: StringNullableFilter<"Patient"> | string | null
status?: EnumPatientStatusFilter<"Patient"> | $Enums.PatientStatus
remainingBenefit?: StringNullableFilter<"Patient"> | string | null
thirdPartyInsurance?: StringNullableFilter<"Patient"> | string | null
userId?: IntFilter<"Patient"> | number
createdAt?: DateTimeFilter<"Patient"> | Date | string
updatedAt?: DateTimeFilter<"Patient"> | Date | string
@@ -49244,6 +49274,8 @@ export namespace Prisma {
medicalConditions?: SortOrderInput | SortOrder
preferredLanguage?: SortOrderInput | SortOrder
status?: SortOrder
remainingBenefit?: SortOrderInput | SortOrder
thirdPartyInsurance?: SortOrderInput | SortOrder
userId?: SortOrder
createdAt?: SortOrder
updatedAt?: SortOrder
@@ -49281,6 +49313,8 @@ export namespace Prisma {
medicalConditions?: StringNullableFilter<"Patient"> | string | null
preferredLanguage?: StringNullableFilter<"Patient"> | string | null
status?: EnumPatientStatusFilter<"Patient"> | $Enums.PatientStatus
remainingBenefit?: StringNullableFilter<"Patient"> | string | null
thirdPartyInsurance?: StringNullableFilter<"Patient"> | string | null
userId?: IntFilter<"Patient"> | number
createdAt?: DateTimeFilter<"Patient"> | Date | string
updatedAt?: DateTimeFilter<"Patient"> | Date | string
@@ -49315,6 +49349,8 @@ export namespace Prisma {
medicalConditions?: SortOrderInput | SortOrder
preferredLanguage?: SortOrderInput | SortOrder
status?: SortOrder
remainingBenefit?: SortOrderInput | SortOrder
thirdPartyInsurance?: SortOrderInput | SortOrder
userId?: SortOrder
createdAt?: SortOrder
updatedAt?: SortOrder
@@ -49347,6 +49383,8 @@ export namespace Prisma {
medicalConditions?: StringNullableWithAggregatesFilter<"Patient"> | string | null
preferredLanguage?: StringNullableWithAggregatesFilter<"Patient"> | string | null
status?: EnumPatientStatusWithAggregatesFilter<"Patient"> | $Enums.PatientStatus
remainingBenefit?: StringNullableWithAggregatesFilter<"Patient"> | string | null
thirdPartyInsurance?: StringNullableWithAggregatesFilter<"Patient"> | string | null
userId?: IntWithAggregatesFilter<"Patient"> | number
createdAt?: DateTimeWithAggregatesFilter<"Patient"> | Date | string
updatedAt?: DateTimeWithAggregatesFilter<"Patient"> | Date | string
@@ -52171,6 +52209,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -52204,6 +52244,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -52236,6 +52278,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -52269,6 +52313,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -52302,6 +52348,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -52325,6 +52373,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -52348,6 +52398,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -55403,6 +55455,8 @@ export namespace Prisma {
medicalConditions?: SortOrder
preferredLanguage?: SortOrder
status?: SortOrder
remainingBenefit?: SortOrder
thirdPartyInsurance?: SortOrder
userId?: SortOrder
createdAt?: SortOrder
updatedAt?: SortOrder
@@ -55432,6 +55486,8 @@ export namespace Prisma {
medicalConditions?: SortOrder
preferredLanguage?: SortOrder
status?: SortOrder
remainingBenefit?: SortOrder
thirdPartyInsurance?: SortOrder
userId?: SortOrder
createdAt?: SortOrder
updatedAt?: SortOrder
@@ -55456,6 +55512,8 @@ export namespace Prisma {
medicalConditions?: SortOrder
preferredLanguage?: SortOrder
status?: SortOrder
remainingBenefit?: SortOrder
thirdPartyInsurance?: SortOrder
userId?: SortOrder
createdAt?: SortOrder
updatedAt?: SortOrder
@@ -61353,6 +61411,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
appointments?: AppointmentCreateNestedManyWithoutPatientInput
@@ -61385,6 +61445,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
appointments?: AppointmentUncheckedCreateNestedManyWithoutPatientInput
@@ -62116,6 +62178,8 @@ export namespace Prisma {
medicalConditions?: StringNullableFilter<"Patient"> | string | null
preferredLanguage?: StringNullableFilter<"Patient"> | string | null
status?: EnumPatientStatusFilter<"Patient"> | $Enums.PatientStatus
remainingBenefit?: StringNullableFilter<"Patient"> | string | null
thirdPartyInsurance?: StringNullableFilter<"Patient"> | string | null
userId?: IntFilter<"Patient"> | number
createdAt?: DateTimeFilter<"Patient"> | Date | string
updatedAt?: DateTimeFilter<"Patient"> | Date | string
@@ -63506,6 +63570,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -63538,6 +63604,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -63836,6 +63904,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -63868,6 +63938,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -65031,6 +65103,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -65063,6 +65137,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -65194,6 +65270,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -65226,6 +65304,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -65293,6 +65373,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -65325,6 +65407,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -65672,6 +65756,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -65704,6 +65790,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -66761,6 +66849,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -66793,6 +66883,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -66863,6 +66955,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -66895,6 +66989,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -67051,6 +67147,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -67083,6 +67181,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -67397,6 +67497,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -67429,6 +67531,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -68427,6 +68531,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -68459,6 +68565,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -68671,6 +68779,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -68703,6 +68813,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -69082,6 +69194,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -69114,6 +69228,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -69235,6 +69351,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -69267,6 +69385,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -69378,6 +69498,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -69410,6 +69532,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -69457,6 +69581,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -69489,6 +69615,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -70444,6 +70572,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
user: UserCreateNestedOneWithoutPatientsInput
@@ -70476,6 +70606,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
userId: number
createdAt?: Date | string
updatedAt?: Date | string
@@ -70597,6 +70729,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
user?: UserUpdateOneRequiredWithoutPatientsNestedInput
@@ -70629,6 +70763,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
userId?: IntFieldUpdateOperationsInput | number
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
@@ -71308,6 +71444,8 @@ export namespace Prisma {
medicalConditions?: string | null
preferredLanguage?: string | null
status?: $Enums.PatientStatus
remainingBenefit?: string | null
thirdPartyInsurance?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -71507,6 +71645,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
appointments?: AppointmentUpdateManyWithoutPatientNestedInput
@@ -71539,6 +71679,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
appointments?: AppointmentUncheckedUpdateManyWithoutPatientNestedInput
@@ -71571,6 +71713,8 @@ export namespace Prisma {
medicalConditions?: NullableStringFieldUpdateOperationsInput | string | null
preferredLanguage?: NullableStringFieldUpdateOperationsInput | string | null
status?: EnumPatientStatusFieldUpdateOperationsInput | $Enums.PatientStatus
remainingBenefit?: NullableStringFieldUpdateOperationsInput | string | null
thirdPartyInsurance?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "prisma-client-06444e98f8328a30442a56cbded54a265df02594192db5e428350bbd93cc975b",
"name": "prisma-client-11a124b84a93d3e63379c8a6769ab83d8729151f8cca035589101564f7ede8ed",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
+33 -31
View File
@@ -55,37 +55,39 @@ model User {
}
model Patient {
id Int @id @default(autoincrement())
firstName String
lastName String
dateOfBirth DateTime? @db.Date
gender String
phone String
email String?
address String?
city String?
zipCode String?
insuranceProvider String?
insuranceId String?
groupNumber String?
policyHolder String?
allergies String?
medicalConditions String?
preferredLanguage String? @default("English")
status PatientStatus @default(UNKNOWN)
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
appointments Appointment[]
procedures AppointmentProcedure[]
claims Claim[]
groups PdfGroup[]
payment Payment[]
communications Communication[]
documents PatientDocument[]
conversation PatientConversation?
cloudFolders CloudFolder[] @relation("PatientCloudFolder")
id Int @id @default(autoincrement())
firstName String
lastName String
dateOfBirth DateTime? @db.Date
gender String
phone String
email String?
address String?
city String?
zipCode String?
insuranceProvider String?
insuranceId String?
groupNumber String?
policyHolder String?
allergies String?
medicalConditions String?
preferredLanguage String? @default("English")
status PatientStatus @default(UNKNOWN)
remainingBenefit String?
thirdPartyInsurance String?
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
appointments Appointment[]
procedures AppointmentProcedure[]
claims Claim[]
groups PdfGroup[]
payment Payment[]
communications Communication[]
documents PatientDocument[]
conversation PatientConversation?
cloudFolders CloudFolder[] @relation("PatientCloudFolder")
@@index([insuranceId])
@@index([createdAt])
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Patient" ADD COLUMN "remainingBenefit" TEXT;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Patient" ADD COLUMN "thirdPartyInsurance" TEXT;
+2
View File
@@ -73,6 +73,8 @@ model Patient {
medicalConditions String?
preferredLanguage String? @default("English")
status PatientStatus @default(UNKNOWN)
remainingBenefit String?
thirdPartyInsurance String?
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1,7 +1,7 @@
{
"version": "1.0",
"generatorVersion": "1.0.0",
"generatedAt": "2026-07-21T03:13:42.342Z",
"generatedAt": "2026-09-03T01:38:13.906Z",
"outputPath": "/home/gg/Desktop/DentalManagementMH07/packages/db/shared",
"files": [
"schemas/enums/TransactionIsolationLevel.schema.ts",
@@ -1,5 +1,5 @@
import * as z from 'zod';
export const PatientScalarFieldEnumSchema = z.enum(['id', 'firstName', 'lastName', 'dateOfBirth', 'gender', 'phone', 'email', 'address', 'city', 'zipCode', 'insuranceProvider', 'insuranceId', 'groupNumber', 'policyHolder', 'allergies', 'medicalConditions', 'preferredLanguage', 'status', 'userId', 'createdAt', 'updatedAt'])
export const PatientScalarFieldEnumSchema = z.enum(['id', 'firstName', 'lastName', 'dateOfBirth', 'gender', 'phone', 'email', 'address', 'city', 'zipCode', 'insuranceProvider', 'insuranceId', 'groupNumber', 'policyHolder', 'allergies', 'medicalConditions', 'preferredLanguage', 'status', 'remainingBenefit', 'thirdPartyInsurance', 'userId', 'createdAt', 'updatedAt'])
export type PatientScalarFieldEnum = z.infer<typeof PatientScalarFieldEnumSchema>;
@@ -28,6 +28,8 @@ export const PatientFindFirstOrThrowSelectSchema: z.ZodType<Prisma.PatientSelect
medicalConditions: z.boolean().optional(),
preferredLanguage: z.boolean().optional(),
status: z.boolean().optional(),
remainingBenefit: z.boolean().optional(),
thirdPartyInsurance: z.boolean().optional(),
userId: z.boolean().optional(),
createdAt: z.boolean().optional(),
updatedAt: z.boolean().optional(),
@@ -63,6 +65,8 @@ export const PatientFindFirstOrThrowSelectZodSchema = z.object({
medicalConditions: z.boolean().optional(),
preferredLanguage: z.boolean().optional(),
status: z.boolean().optional(),
remainingBenefit: z.boolean().optional(),
thirdPartyInsurance: z.boolean().optional(),
userId: z.boolean().optional(),
createdAt: z.boolean().optional(),
updatedAt: z.boolean().optional(),
@@ -28,6 +28,8 @@ export const PatientFindFirstSelectSchema: z.ZodType<Prisma.PatientSelect> = z.o
medicalConditions: z.boolean().optional(),
preferredLanguage: z.boolean().optional(),
status: z.boolean().optional(),
remainingBenefit: z.boolean().optional(),
thirdPartyInsurance: z.boolean().optional(),
userId: z.boolean().optional(),
createdAt: z.boolean().optional(),
updatedAt: z.boolean().optional(),
@@ -63,6 +65,8 @@ export const PatientFindFirstSelectZodSchema = z.object({
medicalConditions: z.boolean().optional(),
preferredLanguage: z.boolean().optional(),
status: z.boolean().optional(),
remainingBenefit: z.boolean().optional(),
thirdPartyInsurance: z.boolean().optional(),
userId: z.boolean().optional(),
createdAt: z.boolean().optional(),
updatedAt: z.boolean().optional(),
@@ -28,6 +28,8 @@ export const PatientFindManySelectSchema: z.ZodType<Prisma.PatientSelect> = z.ob
medicalConditions: z.boolean().optional(),
preferredLanguage: z.boolean().optional(),
status: z.boolean().optional(),
remainingBenefit: z.boolean().optional(),
thirdPartyInsurance: z.boolean().optional(),
userId: z.boolean().optional(),
createdAt: z.boolean().optional(),
updatedAt: z.boolean().optional(),
@@ -63,6 +65,8 @@ export const PatientFindManySelectZodSchema = z.object({
medicalConditions: z.boolean().optional(),
preferredLanguage: z.boolean().optional(),
status: z.boolean().optional(),
remainingBenefit: z.boolean().optional(),
thirdPartyInsurance: z.boolean().optional(),
userId: z.boolean().optional(),
createdAt: z.boolean().optional(),
updatedAt: z.boolean().optional(),
@@ -21,6 +21,8 @@ const makeSchema = () => z.object({
medicalConditions: z.literal(true).optional(),
preferredLanguage: z.literal(true).optional(),
status: z.literal(true).optional(),
remainingBenefit: z.literal(true).optional(),
thirdPartyInsurance: z.literal(true).optional(),
userId: z.literal(true).optional(),
createdAt: z.literal(true).optional(),
updatedAt: z.literal(true).optional(),
@@ -21,6 +21,8 @@ const makeSchema = () => z.object({
medicalConditions: SortOrderSchema.optional(),
preferredLanguage: SortOrderSchema.optional(),
status: SortOrderSchema.optional(),
remainingBenefit: SortOrderSchema.optional(),
thirdPartyInsurance: SortOrderSchema.optional(),
userId: SortOrderSchema.optional(),
createdAt: SortOrderSchema.optional(),
updatedAt: SortOrderSchema.optional()
@@ -30,6 +30,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
appointments: z.lazy(() => AppointmentCreateNestedManyWithoutPatientInputObjectSchema).optional(),
@@ -21,6 +21,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional()
@@ -21,6 +21,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional()
}).strict();
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
user: z.lazy(() => UserCreateNestedOneWithoutPatientsInputObjectSchema),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
appointments: z.lazy(() => AppointmentCreateNestedManyWithoutPatientInputObjectSchema).optional(),
@@ -21,6 +21,8 @@ const makeSchema = () => z.object({
medicalConditions: z.literal(true).optional(),
preferredLanguage: z.literal(true).optional(),
status: z.literal(true).optional(),
remainingBenefit: z.literal(true).optional(),
thirdPartyInsurance: z.literal(true).optional(),
userId: z.literal(true).optional(),
createdAt: z.literal(true).optional(),
updatedAt: z.literal(true).optional()
@@ -21,6 +21,8 @@ const makeSchema = () => z.object({
medicalConditions: SortOrderSchema.optional(),
preferredLanguage: SortOrderSchema.optional(),
status: SortOrderSchema.optional(),
remainingBenefit: SortOrderSchema.optional(),
thirdPartyInsurance: SortOrderSchema.optional(),
userId: SortOrderSchema.optional(),
createdAt: SortOrderSchema.optional(),
updatedAt: SortOrderSchema.optional()
@@ -21,6 +21,8 @@ const makeSchema = () => z.object({
medicalConditions: z.literal(true).optional(),
preferredLanguage: z.literal(true).optional(),
status: z.literal(true).optional(),
remainingBenefit: z.literal(true).optional(),
thirdPartyInsurance: z.literal(true).optional(),
userId: z.literal(true).optional(),
createdAt: z.literal(true).optional(),
updatedAt: z.literal(true).optional()
@@ -21,6 +21,8 @@ const makeSchema = () => z.object({
medicalConditions: SortOrderSchema.optional(),
preferredLanguage: SortOrderSchema.optional(),
status: SortOrderSchema.optional(),
remainingBenefit: SortOrderSchema.optional(),
thirdPartyInsurance: SortOrderSchema.optional(),
userId: SortOrderSchema.optional(),
createdAt: SortOrderSchema.optional(),
updatedAt: SortOrderSchema.optional()
@@ -27,6 +27,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([SortOrderSchema, z.lazy(() => SortOrderInputObjectSchema)]).optional(),
preferredLanguage: z.union([SortOrderSchema, z.lazy(() => SortOrderInputObjectSchema)]).optional(),
status: SortOrderSchema.optional(),
remainingBenefit: z.union([SortOrderSchema, z.lazy(() => SortOrderInputObjectSchema)]).optional(),
thirdPartyInsurance: z.union([SortOrderSchema, z.lazy(() => SortOrderInputObjectSchema)]).optional(),
userId: SortOrderSchema.optional(),
createdAt: SortOrderSchema.optional(),
updatedAt: SortOrderSchema.optional(),
@@ -32,6 +32,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([SortOrderSchema, z.lazy(() => SortOrderInputObjectSchema)]).optional(),
preferredLanguage: z.union([SortOrderSchema, z.lazy(() => SortOrderInputObjectSchema)]).optional(),
status: SortOrderSchema.optional(),
remainingBenefit: z.union([SortOrderSchema, z.lazy(() => SortOrderInputObjectSchema)]).optional(),
thirdPartyInsurance: z.union([SortOrderSchema, z.lazy(() => SortOrderInputObjectSchema)]).optional(),
userId: SortOrderSchema.optional(),
createdAt: SortOrderSchema.optional(),
updatedAt: SortOrderSchema.optional(),
@@ -30,6 +30,8 @@ const patientscalarwhereinputSchema = z.object({
medicalConditions: z.union([z.lazy(() => StringNullableFilterObjectSchema), z.string()]).optional().nullable(),
preferredLanguage: z.union([z.lazy(() => StringNullableFilterObjectSchema), z.string()]).optional().nullable(),
status: z.union([z.lazy(() => EnumPatientStatusFilterObjectSchema), PatientStatusSchema]).optional(),
remainingBenefit: z.union([z.lazy(() => StringNullableFilterObjectSchema), z.string()]).optional().nullable(),
thirdPartyInsurance: z.union([z.lazy(() => StringNullableFilterObjectSchema), z.string()]).optional().nullable(),
userId: z.union([z.lazy(() => IntFilterObjectSchema), z.number().int()]).optional(),
createdAt: z.union([z.lazy(() => DateTimeFilterObjectSchema), z.coerce.date()]).optional(),
updatedAt: z.union([z.lazy(() => DateTimeFilterObjectSchema), z.coerce.date()]).optional()
@@ -30,6 +30,8 @@ const patientscalarwherewithaggregatesinputSchema = z.object({
medicalConditions: z.union([z.lazy(() => StringNullableWithAggregatesFilterObjectSchema), z.string()]).optional().nullable(),
preferredLanguage: z.union([z.lazy(() => StringNullableWithAggregatesFilterObjectSchema), z.string()]).optional().nullable(),
status: z.union([z.lazy(() => EnumPatientStatusWithAggregatesFilterObjectSchema), PatientStatusSchema]).optional(),
remainingBenefit: z.union([z.lazy(() => StringNullableWithAggregatesFilterObjectSchema), z.string()]).optional().nullable(),
thirdPartyInsurance: z.union([z.lazy(() => StringNullableWithAggregatesFilterObjectSchema), z.string()]).optional().nullable(),
userId: z.union([z.lazy(() => IntWithAggregatesFilterObjectSchema), z.number().int()]).optional(),
createdAt: z.union([z.lazy(() => DateTimeWithAggregatesFilterObjectSchema), z.coerce.date()]).optional(),
updatedAt: z.union([z.lazy(() => DateTimeWithAggregatesFilterObjectSchema), z.coerce.date()]).optional()
@@ -31,6 +31,8 @@ const makeSchema = () => z.object({
medicalConditions: z.boolean().optional(),
preferredLanguage: z.boolean().optional(),
status: z.boolean().optional(),
remainingBenefit: z.boolean().optional(),
thirdPartyInsurance: z.boolean().optional(),
userId: z.boolean().optional(),
createdAt: z.boolean().optional(),
updatedAt: z.boolean().optional(),
@@ -30,6 +30,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
appointments: z.lazy(() => AppointmentUncheckedCreateNestedManyWithoutPatientInputObjectSchema).optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -29,6 +29,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
userId: z.number().int(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
@@ -30,6 +30,8 @@ const makeSchema = () => z.object({
medicalConditions: z.string().optional().nullable(),
preferredLanguage: z.string().optional().nullable(),
status: PatientStatusSchema.optional(),
remainingBenefit: z.string().optional().nullable(),
thirdPartyInsurance: z.string().optional().nullable(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
appointments: z.lazy(() => AppointmentUncheckedCreateNestedManyWithoutPatientInputObjectSchema).optional(),
@@ -36,6 +36,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -27,6 +27,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional()
@@ -27,6 +27,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional()
}).strict();
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
@@ -36,6 +36,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
appointments: z.lazy(() => AppointmentUncheckedUpdateManyWithoutPatientNestedInputObjectSchema).optional(),
@@ -35,6 +35,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
user: z.lazy(() => UserUpdateOneRequiredWithoutPatientsNestedInputObjectSchema).optional(),
@@ -25,6 +25,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional()
}).strict();
@@ -34,6 +34,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
user: z.lazy(() => UserUpdateOneRequiredWithoutPatientsNestedInputObjectSchema).optional(),
@@ -34,6 +34,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
user: z.lazy(() => UserUpdateOneRequiredWithoutPatientsNestedInputObjectSchema).optional(),
@@ -34,6 +34,8 @@ const makeSchema = () => z.object({
medicalConditions: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
preferredLanguage: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
status: z.union([PatientStatusSchema, z.lazy(() => EnumPatientStatusFieldUpdateOperationsInputObjectSchema)]).optional(),
remainingBenefit: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
thirdPartyInsurance: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
createdAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
updatedAt: z.union([z.coerce.date(), z.lazy(() => DateTimeFieldUpdateOperationsInputObjectSchema)]).optional(),
user: z.lazy(() => UserUpdateOneRequiredWithoutPatientsNestedInputObjectSchema).optional(),

Some files were not shown because too many files have changed in this diff Show More