feat: enhance new-patient AI chat flow with full scheduling and eligibility

- Add 3-message intro (self-intro → empathetic ack → new/existing question) via single TwiML response to guarantee delivery order
- Detect reschedule intent from first message; look up existing appointment date
- New patient flow: ask insurance type → MassHealth consent → member ID + DOB → Selenium eligibility check
- Post-eligibility: active → ask appointment date/time with office-hours validation; inactive → ask other insurance or collect contact info
- Date/time collection mirrors reschedule flow: check office day open, ask time, validate against office hours
- Auto-create appointment in schedule for known patients on confirmation; use first available staff member
- Add openPhoneReply toggle (Settings → AI Chat) to respond to any number at any time
- Add 5-minute inactivity timeout: reset conversation to initial stage and clear pending state
- Normalize MassHealth DOB to zero-padded MM/DD/YYYY before Selenium submission
- Expand isExistingPatient classifier to recognize "old patient", "old", "previous", "prior"
- Existing patient confirmation message now acknowledges patient type before asking about insurance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-15 00:00:56 -04:00
co-authored by Claude Sonnet 4.6
parent c1f55778ca
commit c71624f7e7
53 changed files with 1078 additions and 124 deletions
+6 -6
View File
@@ -58,7 +58,7 @@ function getNextWeekDateObjects() {
}
/** Format "HH:MM" (24-h) → "H:MM am/pm" */
function timeLabel(hhmm: string): string {
export function timeLabel(hhmm: string): string {
const [h, m] = hhmm.split(":").map(Number);
const h12 = (h! % 12) || 12;
const ampm = (h! >= 12) ? "pm" : "am";
@@ -80,7 +80,7 @@ function messageHasTime(msg: string): boolean {
* Parse only a date (no time) from a message like "5/18" or "next Monday".
* Returns a UTC-midnight Date and a date-only display label, or null.
*/
async function parseDateOnlyFromMessage(
export async function parseDateOnlyFromMessage(
message: string,
apiKey: string,
): Promise<{ date: Date; dateLabel: string } | null> {
@@ -249,7 +249,7 @@ Rules:
* Returns true if the given date+time is within the office's configured hours.
* Falls back to true (unrestricted) if no hours are configured.
*/
async function isWithinOfficeHours(
export async function isWithinOfficeHours(
date: Date,
time: string,
userId: number,
@@ -285,7 +285,7 @@ async function isWithinOfficeHours(
* Returns whether the office is open at all on a given date (day-level check).
* Also returns the display day name (e.g. "Sunday") for use in error messages.
*/
async function isOfficeDayOpen(
export async function isOfficeDayOpen(
date: Date,
userId: number,
): Promise<{ open: boolean; displayDay: string }> {
@@ -319,7 +319,7 @@ async function isOfficeDayOpen(
* e.g. "9:00 am – 12:00 pm and 1:00 pm – 5:00 pm".
* Returns null if no hours are configured or the day is closed.
*/
async function getOfficeHoursDisplay(date: Date, userId: number): Promise<string | null> {
export async function getOfficeHoursDisplay(date: Date, userId: number): Promise<string | null> {
try {
const record = await storage.getOfficeHours(userId);
if (!record?.data) return null;
@@ -387,7 +387,7 @@ async function isSlotAvailable(
// ── Time parsing (legacy) ─────────────────────────────────────────────────────
async function parseTime(message: string, apiKey: string): Promise<string | null> {
export async function parseTime(message: string, apiKey: string): Promise<string | null> {
const t = message.toLowerCase();
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";