feat: migrate SMS/voice to Telnyx (TeXML), keep browser dial pad on Twilio
Twilio now handles only the in-browser dial pad (Access Token + browser-Device outbound TwiML); Telnyx takes over SMS, inbound voicemail, the AI voice conversation loop, and staff-triggered outbound calls via TeXML. Adds TelnyxSettings model, Communication.provider discriminator, new comms/routes modules, and a real Telnyx Settings page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -657,16 +657,17 @@ sudo systemctl start cloudflared
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Variant — Tunnel scoped to Twilio webhooks only
|
### Variant — Tunnel scoped to Twilio/Telnyx webhooks only
|
||||||
|
|
||||||
The example above tunnels the whole app (`service: http://localhost:3000`) to the public
|
The example above tunnels the whole app (`service: http://localhost:3000`) to the public
|
||||||
subdomain. If the only reason you need public access is Twilio's webhooks (inbound SMS/voice —
|
subdomain. If the only reason you need public access is webhooks — Twilio's (dial pad outbound
|
||||||
see [Twilio In-Browser Calling Setup](#twilio-in-browser-calling-setup-dial-pad) below), point
|
calls only — see [Twilio In-Browser Calling Setup](#twilio-in-browser-calling-setup-dial-pad)
|
||||||
the tunnel at nginx's port-80 block instead, which only forwards `/api/twilio/*` and returns
|
below) and Telnyx's (everything else — see [Telnyx Setup](#telnyx-setup) below) — point the
|
||||||
`403` for everything else (see [nginx.conf](nginx.conf)) — the rest of the app stays unreachable
|
tunnel at nginx's port-80 block instead, which only forwards `/api/twilio/*` and `/api/telnyx/*`
|
||||||
from the internet even though the tunnel is live.
|
and returns `403` for everything else (see [nginx.conf](nginx.conf)) — the rest of the app stays
|
||||||
|
unreachable from the internet even though the tunnel is live.
|
||||||
|
|
||||||
Example actually used for Summit Dental Care's Twilio integration — paste this into
|
Example actually used for Summit Dental Care's Twilio/Telnyx integration — paste this into
|
||||||
`/etc/cloudflared/config.yml` (`sudo nano /etc/cloudflared/config.yml`, this is a YAML file, not
|
`/etc/cloudflared/config.yml` (`sudo nano /etc/cloudflared/config.yml`, this is a YAML file, not
|
||||||
the certbot `.ini` credentials file from the LAN HTTPS section above):
|
the certbot `.ini` credentials file from the LAN HTTPS section above):
|
||||||
|
|
||||||
@@ -690,8 +691,8 @@ sudo systemctl start cloudflared
|
|||||||
```
|
```
|
||||||
|
|
||||||
Skip Steps 8–9 (Vite `allowedHosts` / backend CORS) for this variant — the public hostname never
|
Skip Steps 8–9 (Vite `allowedHosts` / backend CORS) for this variant — the public hostname never
|
||||||
reaches the frontend or triggers a login/API CORS check, only the unauthenticated Twilio webhook
|
reaches the frontend or triggers a login/API CORS check, only the unauthenticated Twilio/Telnyx
|
||||||
routes.
|
webhook routes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -960,6 +961,64 @@ Once saved, the dial pad on the Patient Connection page is fully functional. The
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Telnyx Setup
|
||||||
|
|
||||||
|
Telnyx handles everything except the in-browser dial pad above: outbound/inbound SMS (including
|
||||||
|
the AI text conversation), inbound voicemail, and both staff-triggered and AI-driven outbound
|
||||||
|
calls. It uses **TeXML** (Telnyx's TwiML-compatible voice API), which is why the webhook XML looks
|
||||||
|
just like the Twilio webhooks above.
|
||||||
|
|
||||||
|
This reuses the same Cloudflare Tunnel already set up for Twilio above (see
|
||||||
|
[Cloudflare Tunnel Setup](#cloudflare-tunnel-setup-remote-access-per-office)) — no new tunnel or
|
||||||
|
DNS record needed, Telnyx just hits the same public hostname on a different path
|
||||||
|
(`/api/telnyx/*` instead of `/api/twilio/*`). If you're using the
|
||||||
|
[tunnel-scoped-to-webhooks variant](#variant--tunnel-scoped-to-twiliotelnyx-webhooks-only), make
|
||||||
|
sure `nginx.conf`'s port-80 block includes the `/api/telnyx/` `location` block — Telnyx webhooks
|
||||||
|
will silently 403 in production without it.
|
||||||
|
|
||||||
|
### Step 1 — Buy/assign a phone number for this office
|
||||||
|
|
||||||
|
In the [Telnyx Mission Control Portal](https://portal.telnyx.com), buy or port a number for this
|
||||||
|
office (each office needs its own Telnyx number, separate from its Twilio dial-pad number).
|
||||||
|
|
||||||
|
### Step 2 — Create a Messaging Profile (for SMS)
|
||||||
|
|
||||||
|
1. **Messaging → Messaging Profiles → Create Messaging Profile**
|
||||||
|
2. Attach this office's phone number to the profile
|
||||||
|
3. Set the **Inbound Webhook URL** to:
|
||||||
|
```
|
||||||
|
https://<office>.mydentalofficemanagement.com/api/telnyx/webhook/sms
|
||||||
|
```
|
||||||
|
4. Note the **Messaging Profile ID** if your account requires one for sending (some number types don't)
|
||||||
|
|
||||||
|
### Step 3 — Create a TeXML Application (for calls)
|
||||||
|
|
||||||
|
1. **Voice → TeXML Applications → Create TeXML Application**
|
||||||
|
2. Set the **Voice Webhook URL** to:
|
||||||
|
```
|
||||||
|
https://<office>.mydentalofficemanagement.com/api/telnyx/webhook/voice
|
||||||
|
```
|
||||||
|
3. Attach this office's phone number to the application
|
||||||
|
4. Note the **Application ID** (also called Connection ID)
|
||||||
|
|
||||||
|
### Step 4 — Get an API Key and Account SID
|
||||||
|
|
||||||
|
**API Keys & Credentials** in the portal → create an API Key. The **Account SID** (separate from
|
||||||
|
the API Key, needed for placing outbound calls) is shown in your account/organization settings.
|
||||||
|
|
||||||
|
### Step 5 — Enter credentials in the app
|
||||||
|
|
||||||
|
Go to **Settings → Telnyx Settings** and fill in: API Key, Account SID, Phone Number, TeXML
|
||||||
|
Application ID, Messaging Profile ID (if applicable), and an optional Voicemail Greeting → Save.
|
||||||
|
|
||||||
|
### Step 6 — Webhook signature verification (optional, recommended)
|
||||||
|
|
||||||
|
Telnyx signs webhooks with Ed25519. If you want inbound webhook signatures verified (rejecting
|
||||||
|
forged requests to your webhook URLs), copy your account's **Public Key** from the portal into
|
||||||
|
the Webhook Public Key field in Telnyx Settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License Key Generator
|
## License Key Generator
|
||||||
|
|
||||||
The license key generator is a private tool that lives only on your dev PC. Use it to generate a new license key for any office every 3 months.
|
The license key generator is a private tool that lives only on your dev PC. Use it to generate a new license key for any office every 3 months.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
"sharp": "^0.35.3",
|
"sharp": "^0.35.3",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
|
"telnyx": "^7.20.0",
|
||||||
"twilio": "^6.0.0",
|
"twilio": "^6.0.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import twilio from "twilio";
|
|
||||||
import { prisma as db } from "@repo/db/client";
|
import { prisma as db } from "@repo/db/client";
|
||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
|
import { sendTelnyxSms } from "../comms/telnyx-client";
|
||||||
|
|
||||||
const CONFIRMATION_FALLBACKS: Record<string, (dateLabel: string, time: string) => string> = {
|
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.`,
|
English: (d, t) => `Confirmed! Your appointment is on ${d} at ${t}. We look forward to seeing you.`,
|
||||||
@@ -27,20 +27,22 @@ export async function sendAppointmentConfirmationSms(
|
|||||||
lang: string,
|
lang: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const [twilioSettings, patient] = await Promise.all([
|
const [telnyxSettings, patient] = await Promise.all([
|
||||||
storage.getTwilioSettings(userId),
|
storage.getTelnyxSettings(userId),
|
||||||
db.patient.findUnique({ where: { id: patientId }, select: { phone: true } }),
|
db.patient.findUnique({ where: { id: patientId }, select: { phone: true } }),
|
||||||
]);
|
]);
|
||||||
if (!twilioSettings || !patient?.phone) return;
|
if (!telnyxSettings || !patient?.phone) return;
|
||||||
|
|
||||||
const build = CONFIRMATION_FALLBACKS[lang] ?? CONFIRMATION_FALLBACKS["English"]!;
|
const build = CONFIRMATION_FALLBACKS[lang] ?? CONFIRMATION_FALLBACKS["English"]!;
|
||||||
const body = build(dateLabel, time);
|
const body = build(dateLabel, time);
|
||||||
|
|
||||||
const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
|
await sendTelnyxSms({
|
||||||
await client.messages.create({ body, from: twilioSettings.phoneNumber, to: patient.phone });
|
apiKey: telnyxSettings.apiKey, from: telnyxSettings.phoneNumber, to: patient.phone, text: body,
|
||||||
|
messagingProfileId: telnyxSettings.messagingProfileId,
|
||||||
|
});
|
||||||
|
|
||||||
await storage.createCommunication({
|
await storage.createCommunication({
|
||||||
patientId, userId, channel: "sms", direction: "outbound", status: "sent", body,
|
patientId, userId, channel: "sms", direction: "outbound", status: "sent", body, provider: "telnyx",
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Silent — don't crash the booking flow over a notification failure
|
// Silent — don't crash the booking flow over a notification failure
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import twilio from "twilio";
|
|
||||||
import { prisma as db } from "@repo/db/client";
|
import { prisma as db } from "@repo/db/client";
|
||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
|
import { sendTelnyxSms } from "../comms/telnyx-client";
|
||||||
import { runReminderGraph } from "./reminder-graph";
|
import { runReminderGraph } from "./reminder-graph";
|
||||||
import { runRecallGraph, runRecallBookingStep, hasDateOrTimeSignal } from "./recall-graph";
|
import { runRecallGraph, runRecallBookingStep, hasDateOrTimeSignal } from "./recall-graph";
|
||||||
import { runNewPatientStep } from "./new-patient-graph";
|
import { runNewPatientStep } from "./new-patient-graph";
|
||||||
@@ -62,7 +62,7 @@ export function applyOfficeName(template: string, name: string): string {
|
|||||||
/** Save an outbound SMS message (always sms channel — used for async follow-ups). */
|
/** Save an outbound SMS message (always sms channel — used for async follow-ups). */
|
||||||
async function saveOutboundSms(patientId: number, body: string): Promise<void> {
|
async function saveOutboundSms(patientId: number, body: string): Promise<void> {
|
||||||
await storage.createCommunication({
|
await storage.createCommunication({
|
||||||
patientId, channel: "sms", direction: "outbound", status: "sent", body,
|
patientId, channel: "sms", direction: "outbound", status: "sent", body, provider: "telnyx",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,8 +132,8 @@ async function runMassHealthCheckAndNotify(
|
|||||||
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(patient.userId, "MH");
|
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(patient.userId, "MH");
|
||||||
if (!credentials) return;
|
if (!credentials) return;
|
||||||
|
|
||||||
const twilioSettings = await storage.getTwilioSettings(patient.userId);
|
const telnyxSettings = await storage.getTelnyxSettings(patient.userId);
|
||||||
if (!twilioSettings || !patient.phone) return;
|
if (!telnyxSettings || !patient.phone) return;
|
||||||
|
|
||||||
await runEligibilityProcessor({
|
await runEligibilityProcessor({
|
||||||
userId: patient.userId,
|
userId: patient.userId,
|
||||||
@@ -198,11 +198,12 @@ async function runMassHealthCheckAndNotify(
|
|||||||
? "asked_self_pay"
|
? "asked_self_pay"
|
||||||
: "asked_other_insurance_after_inactive";
|
: "asked_other_insurance_after_inactive";
|
||||||
|
|
||||||
const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
|
await sendTelnyxSms({
|
||||||
await client.messages.create({
|
apiKey: telnyxSettings.apiKey,
|
||||||
body: resultText,
|
from: telnyxSettings.phoneNumber,
|
||||||
from: twilioSettings.phoneNumber,
|
|
||||||
to: patient.phone,
|
to: patient.phone,
|
||||||
|
text: resultText,
|
||||||
|
messagingProfileId: telnyxSettings.messagingProfileId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveOutboundSms(patient.id, resultText);
|
await saveOutboundSms(patient.id, resultText);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { errorHandler } from "./middlewares/error.middleware";
|
|||||||
import { apiLogger } from "./middlewares/logger.middleware";
|
import { apiLogger } from "./middlewares/logger.middleware";
|
||||||
import authRoutes from "./routes/auth";
|
import authRoutes from "./routes/auth";
|
||||||
import twilioWebhookRoutes from "./routes/twilio-webhooks";
|
import twilioWebhookRoutes from "./routes/twilio-webhooks";
|
||||||
|
import telnyxWebhookRoutes from "./routes/telnyx-webhooks";
|
||||||
import greetingRoutes from "./routes/greeting";
|
import greetingRoutes from "./routes/greeting";
|
||||||
import { authenticateJWT } from "./middlewares/auth.middleware";
|
import { authenticateJWT } from "./middlewares/auth.middleware";
|
||||||
import networkBackupPublicRoutes from "./routes/network-backup-public";
|
import networkBackupPublicRoutes from "./routes/network-backup-public";
|
||||||
@@ -79,6 +80,11 @@ app.use("/api/auth", authRoutes);
|
|||||||
app.use("/api/greeting", greetingRoutes);
|
app.use("/api/greeting", greetingRoutes);
|
||||||
// Twilio webhooks are public — Twilio sends no JWT token
|
// Twilio webhooks are public — Twilio sends no JWT token
|
||||||
app.use("/api/twilio", express.urlencoded({ extended: false }), twilioWebhookRoutes);
|
app.use("/api/twilio", express.urlencoded({ extended: false }), twilioWebhookRoutes);
|
||||||
|
// Telnyx webhooks are public. The Messaging webhook (/webhook/sms) sends JSON;
|
||||||
|
// the TeXML voice callbacks (/webhook/voice*, /webhook/ai-voice, /webhook/say)
|
||||||
|
// are form-encoded, Twilio-compatible — parse both, each only applies to
|
||||||
|
// requests with the matching Content-Type.
|
||||||
|
app.use("/api/telnyx", express.json(), express.urlencoded({ extended: false }), telnyxWebhookRoutes);
|
||||||
// Network backup endpoints use their own API key auth — must be before authenticateJWT
|
// Network backup endpoints use their own API key auth — must be before authenticateJWT
|
||||||
app.use("/api/database-management", networkBackupPublicRoutes);
|
app.use("/api/database-management", networkBackupPublicRoutes);
|
||||||
// All other API routes require JWT
|
// All other API routes require JWT
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Telnyx from "telnyx";
|
||||||
|
|
||||||
|
export function getTelnyxClient(apiKey: string) {
|
||||||
|
return new Telnyx({ apiKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sends an SMS via Telnyx's Messaging API. Mirrors the shape of Twilio's
|
||||||
|
* `client.messages.create` return so call sites need minimal changes. */
|
||||||
|
export async function sendTelnyxSms(opts: {
|
||||||
|
apiKey: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
text: string;
|
||||||
|
messagingProfileId?: string | null;
|
||||||
|
}): Promise<{ id: string; status: string }> {
|
||||||
|
const client = getTelnyxClient(opts.apiKey);
|
||||||
|
const res = await client.messages.send({
|
||||||
|
from: opts.from,
|
||||||
|
to: opts.to,
|
||||||
|
text: opts.text,
|
||||||
|
...(opts.messagingProfileId ? { messaging_profile_id: opts.messagingProfileId } : {}),
|
||||||
|
});
|
||||||
|
const data = res.data;
|
||||||
|
return { id: data?.id ?? "", status: data?.to?.[0]?.status ?? "queued" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places an outbound call via Telnyx's TeXML product (not Call Control —
|
||||||
|
* TeXML was chosen specifically so the existing hand-built TwiML-style XML
|
||||||
|
* in the webhook handlers ports over largely unchanged): POST
|
||||||
|
* /texml/Accounts/{accountSid}/Calls, which tells Telnyx to fetch TeXML call
|
||||||
|
* instructions from `webhookUrl` once the call connects.
|
||||||
|
*/
|
||||||
|
export async function placeTexmlCall(opts: {
|
||||||
|
apiKey: string;
|
||||||
|
accountSid: string;
|
||||||
|
texmlApplicationId?: string | null;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
webhookUrl: string;
|
||||||
|
}): Promise<{ id: string; status: string }> {
|
||||||
|
const client = getTelnyxClient(opts.apiKey);
|
||||||
|
const res = await client.texml.accounts.calls.calls(opts.accountSid, {
|
||||||
|
body: {
|
||||||
|
Url: opts.webhookUrl,
|
||||||
|
From: opts.from,
|
||||||
|
To: opts.to,
|
||||||
|
...(opts.texmlApplicationId ? { ApplicationSid: opts.texmlApplicationId } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// The SDK's typed CallCallsResponse only declares { from, status, to } — no
|
||||||
|
// call id field. Falling back defensively since a CallSid is very likely
|
||||||
|
// present in the real response; verify the actual field name against a
|
||||||
|
// live test call before relying on this for Communication correlation.
|
||||||
|
return { id: (res as any).sid ?? (res as any).call_sid ?? (res as any).CallSid ?? "", status: res.status ?? "queued" };
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import paymentsReportsRoutes from "./payments-reports";
|
|||||||
import exportPaymentsReportsRoutes from "./export-payments-reports";
|
import exportPaymentsReportsRoutes from "./export-payments-reports";
|
||||||
import jobMonitorRoutes from "./job-monitor";
|
import jobMonitorRoutes from "./job-monitor";
|
||||||
import twilioRoutes from "./twilio";
|
import twilioRoutes from "./twilio";
|
||||||
|
import telnyxRoutes from "./telnyx";
|
||||||
import aiSettingsRoutes from "./ai-settings";
|
import aiSettingsRoutes from "./ai-settings";
|
||||||
import officeHoursRoutes from "./office-hours";
|
import officeHoursRoutes from "./office-hours";
|
||||||
import officeContactRoutes from "./office-contact";
|
import officeContactRoutes from "./office-contact";
|
||||||
@@ -85,6 +86,7 @@ router.use("/export-payments-reports", exportPaymentsReportsRoutes);
|
|||||||
router.use("/job-monitor", jobMonitorRoutes);
|
router.use("/job-monitor", jobMonitorRoutes);
|
||||||
router.use("/type-agent", typeAgentRoutes);
|
router.use("/type-agent", typeAgentRoutes);
|
||||||
router.use("/twilio", twilioRoutes);
|
router.use("/twilio", twilioRoutes);
|
||||||
|
router.use("/telnyx", telnyxRoutes);
|
||||||
router.use("/ai", aiSettingsRoutes);
|
router.use("/ai", aiSettingsRoutes);
|
||||||
router.use("/office-hours", officeHoursRoutes);
|
router.use("/office-hours", officeHoursRoutes);
|
||||||
router.use("/office-contact", officeContactRoutes);
|
router.use("/office-contact", officeContactRoutes);
|
||||||
|
|||||||
@@ -0,0 +1,897 @@
|
|||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import { storage } from "../storage";
|
||||||
|
import { DEFAULT_GREETING, DEFAULT_RECALL_GREETING } from "../storage/twilio-storage";
|
||||||
|
import { prisma as db } from "@repo/db/client";
|
||||||
|
import { sendTelnyxSms } from "../comms/telnyx-client";
|
||||||
|
import { runNewPatientStep } from "../ai/new-patient-graph";
|
||||||
|
import {
|
||||||
|
parseDateOnlyFromMessage,
|
||||||
|
parseTime,
|
||||||
|
isOfficeDayOpen,
|
||||||
|
isWithinOfficeHours,
|
||||||
|
getOfficeHoursDisplay,
|
||||||
|
timeLabel,
|
||||||
|
} from "../ai/reschedule-graph";
|
||||||
|
import { getLlm, resolveAiProvider, type AiProvider } from "../ai/llm-factory";
|
||||||
|
import { soundsLikeGoodbye } from "../ai/voice-assistant";
|
||||||
|
import { getPublicBaseUrl } from "../utils/publicUrl";
|
||||||
|
import {
|
||||||
|
routePatientTurn, getAppointmentDatetime, applyOfficeName,
|
||||||
|
} from "../ai/patient-conversation-router";
|
||||||
|
import {
|
||||||
|
getHandoff, getAfterHoursHandoff,
|
||||||
|
getStage, setStage, resetConversation, startRecallConversation,
|
||||||
|
setPendingReschedule, getPendingReschedule, clearPendingReschedule,
|
||||||
|
type ConversationStage,
|
||||||
|
} from "../ai/aiHandoffStore";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function escapeXml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """).replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ok(res: Response) {
|
||||||
|
return res.status(200).json({ ok: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelnyxSmsSettings = { apiKey: string; phoneNumber: string; messagingProfileId?: string | null };
|
||||||
|
|
||||||
|
/** Telnyx has no synchronous TeXML-style SMS reply — every reply is a
|
||||||
|
* separate outbound API call. Sequential await preserves ordering. */
|
||||||
|
async function sendReplies(settings: TelnyxSmsSettings, to: string, texts: string[]): Promise<void> {
|
||||||
|
for (const text of texts) {
|
||||||
|
await sendTelnyxSms({
|
||||||
|
apiKey: settings.apiKey, from: settings.phoneNumber, to, text,
|
||||||
|
messagingProfileId: settings.messagingProfileId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if right now is outside office hours for the given user. */
|
||||||
|
async function isAfterHours(userId: number): Promise<boolean> {
|
||||||
|
const record = await storage.getOfficeHours(userId);
|
||||||
|
if (!record?.data) return false; // no hours configured → treat as in-hours
|
||||||
|
|
||||||
|
const data = record.data as any;
|
||||||
|
const now = new Date();
|
||||||
|
const days = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];
|
||||||
|
const day = days[now.getDay()];
|
||||||
|
|
||||||
|
const slot = data.doctors?.[day];
|
||||||
|
if (!slot?.enabled) return true; // office closed today
|
||||||
|
|
||||||
|
const hhmm = `${String(now.getHours()).padStart(2,"0")}:${String(now.getMinutes()).padStart(2,"0")}`;
|
||||||
|
if (hhmm >= slot.amStart && hhmm <= slot.amEnd) return false;
|
||||||
|
if (hhmm >= slot.pmStart && hhmm <= slot.pmEnd) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save an outbound message and return the text. */
|
||||||
|
async function saveOutbound(patientId: number, body: string): Promise<void> {
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId, channel: "sms", direction: "outbound", status: "sent", body, provider: "telnyx",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Empathetic one-liner (instant keyword-based, no API latency) ─────────────
|
||||||
|
|
||||||
|
function getEmpatheticAck(message: string, language: string): string {
|
||||||
|
const hasPain = /pain|hurt|ache|toothache|emergency|urgent|asap|bleeding|broke|crack|fell out|swollen|infection|abscess/i.test(message);
|
||||||
|
|
||||||
|
const painMsgs: Record<string, string> = {
|
||||||
|
English: "Sorry to hear that! We are here to help you.",
|
||||||
|
Spanish: "¡Lo sentimos! Estamos aquí para ayudarle.",
|
||||||
|
Portuguese: "Lamentamos isso! Estamos aqui para ajudá-lo.",
|
||||||
|
Mandarin: "很遗憾听到这个消息!我们在这里帮助您。",
|
||||||
|
Cantonese: "很遺憾聽到這個消息!我們在這裡幫助您。",
|
||||||
|
Arabic: "آسف لسماع ذلك! نحن هنا لمساعدتك.",
|
||||||
|
"Haitian Creole": "Nou regrèt tande sa! Nou la pou ede ou.",
|
||||||
|
};
|
||||||
|
const normalMsgs: Record<string, string> = {
|
||||||
|
English: "No problem!",
|
||||||
|
Spanish: "¡Sin problema!",
|
||||||
|
Portuguese: "Sem problema!",
|
||||||
|
Mandarin: "没问题!",
|
||||||
|
Cantonese: "沒問題!",
|
||||||
|
Arabic: "لا مشكلة!",
|
||||||
|
"Haitian Creole": "Pa gen pwoblèm!",
|
||||||
|
};
|
||||||
|
|
||||||
|
return hasPain
|
||||||
|
? (painMsgs[language] ?? painMsgs["English"]!)
|
||||||
|
: (normalMsgs[language] ?? normalMsgs["English"]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stale-conversation handling (SMS) ─────────────────────────────────────────
|
||||||
|
// Unlike a phone call, a long gap between texts doesn't necessarily mean the
|
||||||
|
// patient walked away — replies are often just slow. So instead of hard
|
||||||
|
// resetting on any idle gap, we only second-guess the stored stage once the
|
||||||
|
// gap crosses STALE_SMS_THRESHOLD_MS, and even then we ask the LLM to look at
|
||||||
|
// the recent history before deciding whether the new text is still answering
|
||||||
|
// the last question or reads like a fresh, unrelated conversation.
|
||||||
|
|
||||||
|
const STALE_SMS_THRESHOLD_MS = 3 * 60 * 60 * 1000; // 3 hours
|
||||||
|
|
||||||
|
const STILL_HERE_MSGS: Record<string, string> = {
|
||||||
|
English: "It's been a while since our last message. How can I help you today?",
|
||||||
|
Spanish: "Ha pasado un tiempo desde nuestro último mensaje. ¿Cómo puedo ayudarle hoy?",
|
||||||
|
Portuguese: "Já faz um tempo desde nossa última mensagem. Como posso ajudá-lo hoje?",
|
||||||
|
Mandarin: "距离我们上次的消息已经有一段时间了。今天我能帮您什么?",
|
||||||
|
Cantonese: "距離我們上次嘅消息已經有一段時間喇。今日我可以點幫您?",
|
||||||
|
Arabic: "لقد مر بعض الوقت منذ آخر رسالة لنا. كيف يمكنني مساعدتك اليوم؟",
|
||||||
|
"Haitian Creole": "Sa gen yon ti tan depi dènye mesaj nou. Kijan mwen ka ede ou jodi a?",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After a long idle gap, ask the LLM whether the patient's new text still
|
||||||
|
* answers what was last asked (recent history + stage in context) or reads
|
||||||
|
* like an unrelated, fresh message. Defaults to "continue" on any failure —
|
||||||
|
* an LLM call error shouldn't be the reason a legitimate reply gets bounced.
|
||||||
|
*/
|
||||||
|
async function isLikelyContinuation(
|
||||||
|
recentHistory: { role: "user" | "assistant"; text: string }[],
|
||||||
|
newMessage: string,
|
||||||
|
stage: ConversationStage,
|
||||||
|
apiKey: string,
|
||||||
|
provider: AiProvider,
|
||||||
|
model?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (recentHistory.length === 0) return true;
|
||||||
|
try {
|
||||||
|
const llm = getLlm(provider, apiKey, model);
|
||||||
|
const transcript = recentHistory
|
||||||
|
.map(m => `${m.role === "assistant" ? "Office" : "Patient"}: ${m.text}`)
|
||||||
|
.join("\n");
|
||||||
|
const res = await llm.invoke([
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content:
|
||||||
|
`You are helping a dental office text-messaging assistant resume a conversation with a patient after a long gap of silence. ` +
|
||||||
|
`Here is the recent conversation (oldest first):\n${transcript}\n\n` +
|
||||||
|
`The assistant is currently waiting on stage "${stage}" of a booking flow. ` +
|
||||||
|
`The patient just sent a new message after the long gap: "${newMessage}"\n\n` +
|
||||||
|
`Does this new message still look like it's answering/continuing that same conversation, or does it read like an unrelated, fresh message (e.g. a new question, a greeting, a different topic)? ` +
|
||||||
|
`Reply with ONLY one word: "continue" or "restart".`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return !/restart/i.test(String(res.content));
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POST /api/telnyx/webhook/sms ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
const CONVO_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
// In-memory conversation state for unknown phone numbers (no patient record yet).
|
||||||
|
// Keyed by normalized "From" number.
|
||||||
|
const unknownPhoneConvos = new Map<string, {
|
||||||
|
userId: number;
|
||||||
|
stage: ConversationStage;
|
||||||
|
language: string;
|
||||||
|
lastActivityAt: Date;
|
||||||
|
pendingApptDate?: { date: Date; dateLabel: string };
|
||||||
|
}>();
|
||||||
|
|
||||||
|
router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
// Telnyx webhooks are JSON with a nested event envelope, not Twilio's flat
|
||||||
|
// form-encoded From/To/Body/MessageSid. Ignore any event type other than
|
||||||
|
// an inbound message — Telnyx may fan message.sent/message.finalized into
|
||||||
|
// the same webhook URL depending on portal config.
|
||||||
|
const event = (req.body as any)?.data;
|
||||||
|
if (!event || event.event_type !== "message.received") return ok(res);
|
||||||
|
const payload = event.payload || {};
|
||||||
|
const From = payload?.from?.phone_number || "";
|
||||||
|
const To = payload?.to?.[0]?.phone_number || "";
|
||||||
|
const Body = payload?.text || "";
|
||||||
|
const MessageSid = payload?.id;
|
||||||
|
|
||||||
|
const normalizedFrom = (From || "").replace(/\D/g, "");
|
||||||
|
// Ordered by id so that when duplicate patients share a phone number
|
||||||
|
// (one record per insurance), the earliest-created record is the default.
|
||||||
|
const allPatients = await db.patient.findMany({
|
||||||
|
select: { id: true, phone: true, userId: true, preferredLanguage: true },
|
||||||
|
orderBy: { id: "asc" },
|
||||||
|
});
|
||||||
|
const patient = allPatients.find(
|
||||||
|
(p) => p.phone && p.phone.replace(/\D/g, "") === normalizedFrom
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!patient) {
|
||||||
|
// Unknown number — look up office by the Telnyx "To" number
|
||||||
|
const normalizedTo = (To || "").replace(/\D/g, "");
|
||||||
|
const telnyxRow = await db.telnyxSettings.findFirst({
|
||||||
|
where: { phoneNumber: { contains: normalizedTo.slice(-10) } },
|
||||||
|
});
|
||||||
|
if (!telnyxRow) return ok(res);
|
||||||
|
|
||||||
|
const userId = telnyxRow.userId;
|
||||||
|
const openPhoneReply = await storage.getOpenPhoneReply(userId);
|
||||||
|
if (!openPhoneReply) return ok(res);
|
||||||
|
|
||||||
|
// Fetch required context for this office
|
||||||
|
const aiSettings = await storage.getAiSettings(userId);
|
||||||
|
const activeAiU = resolveAiProvider(aiSettings ?? {});
|
||||||
|
if (!activeAiU) return ok(res);
|
||||||
|
|
||||||
|
const chatTemplates = await storage.getAiChatTemplates(userId);
|
||||||
|
const officeContact = await storage.getOfficeContact(userId);
|
||||||
|
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
||||||
|
const convo = unknownPhoneConvos.get(normalizedFrom);
|
||||||
|
let stage = convo?.stage ?? "initial";
|
||||||
|
const language = convo?.language ?? "English";
|
||||||
|
|
||||||
|
// Reset conversation if idle for more than 5 minutes
|
||||||
|
if (
|
||||||
|
stage !== "initial" && stage !== "done" &&
|
||||||
|
convo?.lastActivityAt &&
|
||||||
|
Date.now() - convo.lastActivityAt.getTime() > CONVO_TIMEOUT_MS
|
||||||
|
) {
|
||||||
|
stage = "initial";
|
||||||
|
unknownPhoneConvos.set(normalizedFrom, { userId, stage: "initial", language, lastActivityAt: new Date() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyUnknown = async (
|
||||||
|
text: string,
|
||||||
|
nextStage: ConversationStage,
|
||||||
|
pendingApptDate?: { date: Date; dateLabel: string },
|
||||||
|
) => {
|
||||||
|
unknownPhoneConvos.set(normalizedFrom, {
|
||||||
|
userId, stage: nextStage, language,
|
||||||
|
lastActivityAt: new Date(),
|
||||||
|
...(pendingApptDate ? { pendingApptDate } : { pendingApptDate: convo?.pendingApptDate }),
|
||||||
|
});
|
||||||
|
await sendReplies(telnyxRow, From, [text]);
|
||||||
|
return ok(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stage === "initial" || stage === "done") {
|
||||||
|
// MSG 1: AI intro
|
||||||
|
const rawGreeting = chatTemplates.newPatientGreeting ||
|
||||||
|
`Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7.`;
|
||||||
|
const introText = applyOfficeName(rawGreeting, officeName);
|
||||||
|
|
||||||
|
// MSG 2: Empathetic acknowledgment
|
||||||
|
const empatheticText = getEmpatheticAck(Body, language);
|
||||||
|
|
||||||
|
// MSG 3: Detect intent from first message
|
||||||
|
const isReschedule = /reschedule|rescheduler|change.*appoint|modify.*appoint|move.*appoint|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
||||||
|
let msg3Unk: string;
|
||||||
|
let nextStage3Unk: ConversationStage;
|
||||||
|
|
||||||
|
if (isReschedule) {
|
||||||
|
const notFoundMsgs: Record<string, string> = {
|
||||||
|
English: "I couldn't find an appointment associated with your number. Please leave your name and a good callback number and our receptionist will assist you as soon as possible.",
|
||||||
|
Spanish: "No encontré una cita asociada a su número. Por favor deje su nombre y un número de contacto y nuestra recepcionista le atenderá lo antes posible.",
|
||||||
|
Portuguese: "Não encontrei uma consulta associada ao seu número. Por favor deixe seu nome e um número de contato e nossa recepcionista lhe atenderá o mais breve possível.",
|
||||||
|
Mandarin: "我找不到与您号码关联的预约。请留下您的姓名和联系电话,我们的前台将尽快与您联系。",
|
||||||
|
Cantonese: "我找不到與您號碼關聯的預約。請留下您的姓名和聯絡電話,我們的接待員將盡快與您聯絡。",
|
||||||
|
Arabic: "لم أجد موعداً مرتبطاً برقمك. يرجى ترك اسمك ورقم هاتف للتواصل وسيساعدك موظف الاستقبال في أقرب وقت.",
|
||||||
|
"Haitian Creole": "Mwen pa jwenn yon randevou ki asosye ak nimewo ou. Tanpri kite non ou ak yon nimewo pou rele epi resepsyonis nou an pral ede ou pi vit posib.",
|
||||||
|
};
|
||||||
|
msg3Unk = notFoundMsgs[language] ?? notFoundMsgs["English"]!;
|
||||||
|
nextStage3Unk = "collecting_contact_info";
|
||||||
|
} else {
|
||||||
|
const newOrExistingMsgs: Record<string, string> = {
|
||||||
|
English: "Can you please tell me if you are a new or existing patient?",
|
||||||
|
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
||||||
|
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
||||||
|
Mandarin: "请问您是新患者还是现有患者?",
|
||||||
|
Cantonese: "請問您是新病人還是現有病人?",
|
||||||
|
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
||||||
|
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
||||||
|
};
|
||||||
|
msg3Unk = newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!;
|
||||||
|
nextStage3Unk = "asked_new_or_existing";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update store then send all 3 in guaranteed order
|
||||||
|
unknownPhoneConvos.set(normalizedFrom, {
|
||||||
|
userId, stage: nextStage3Unk, language, lastActivityAt: new Date(),
|
||||||
|
});
|
||||||
|
await sendReplies(telnyxRow, From, [introText, empatheticText, msg3Unk]);
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unknown: asked_new_or_reschedule (fallback for in-progress conversations) ──
|
||||||
|
if (stage === "asked_new_or_reschedule") {
|
||||||
|
const isReschedule = /reschedule|rescheduler|change|modify|move|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
||||||
|
if (isReschedule) {
|
||||||
|
const notFoundMsgs: Record<string, string> = {
|
||||||
|
English: "I couldn't find an appointment associated with your number. Please leave your name and a good callback number and our receptionist will assist you as soon as possible.",
|
||||||
|
Spanish: "No encontré una cita asociada a su número. Por favor deje su nombre y un número de contacto y nuestra recepcionista le atenderá lo antes posible.",
|
||||||
|
Portuguese: "Não encontrei uma consulta associada ao seu número. Por favor deixe seu nome e um número de contato e nossa recepcionista lhe atenderá o mais breve possível.",
|
||||||
|
Mandarin: "我找不到与您号码关联的预约。请留下您的姓名和联系电话,我们的前台将尽快与您联系。",
|
||||||
|
Cantonese: "我找不到與您號碼關聯的預約。請留下您的姓名和聯絡電話,我們的接待員將盡快與您聯絡。",
|
||||||
|
Arabic: "لم أجد موعداً مرتبطاً برقمك. يرجى ترك اسمك ورقم هاتف للتواصل وسيساعدك موظف الاستقبال في أقرب وقت.",
|
||||||
|
"Haitian Creole": "Mwen pa jwenn yon randevou ki asosye ak nimewo ou. Tanpri kite non ou ak yon nimewo pou rele epi resepsyonis nou an pral ede ou pi vit posib.",
|
||||||
|
};
|
||||||
|
return await replyUnknown(notFoundMsgs[language] ?? notFoundMsgs["English"]!, "collecting_contact_info");
|
||||||
|
}
|
||||||
|
const newOrExistingMsgs: Record<string, string> = {
|
||||||
|
English: "Can you please tell me if you are a new or existing patient?",
|
||||||
|
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
||||||
|
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
||||||
|
Mandarin: "请问您是新患者还是现有患者?",
|
||||||
|
Cantonese: "請問您是新病人還是現有病人?",
|
||||||
|
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
||||||
|
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
||||||
|
};
|
||||||
|
return await replyUnknown(newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!, "asked_new_or_existing");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unknown: asked_appointment_time → parse date, check office hours ──
|
||||||
|
if (stage === "asked_appointment_time") {
|
||||||
|
const parsedDate = await parseDateOnlyFromMessage(Body, activeAiU.key, activeAiU.provider, activeAiU.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 await replyUnknown(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
|
||||||
|
}
|
||||||
|
const { date, dateLabel } = parsedDate;
|
||||||
|
const dayCheck = await isOfficeDayOpen(date, 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 await replyUnknown(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
|
||||||
|
}
|
||||||
|
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 await replyUnknown(
|
||||||
|
askTimeMsgs[language] ?? askTimeMsgs["English"]!,
|
||||||
|
"asked_new_appt_time_for_date",
|
||||||
|
{ date, dateLabel },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unknown: asked_new_appt_time_for_date → parse time, check hours ───
|
||||||
|
if (stage === "asked_new_appt_time_for_date") {
|
||||||
|
const pendingApptDate = convo?.pendingApptDate;
|
||||||
|
if (!pendingApptDate) {
|
||||||
|
return await replyUnknown(
|
||||||
|
"I lost track of the date. What day would you prefer?",
|
||||||
|
"asked_appointment_time",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const startTime = await parseTime(Body, activeAiU.key, activeAiU.provider, activeAiU.model);
|
||||||
|
if (!startTime) {
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: `I didn't catch the time. What time do you prefer on ${pendingApptDate.dateLabel}? For example: '10am' or '2pm'.`,
|
||||||
|
Spanish: `No entendí la hora. ¿Qué hora prefiere el ${pendingApptDate.dateLabel}? Por ejemplo: '10am' o '2pm'.`,
|
||||||
|
Portuguese: `Não entendi o horário. Que hora você prefere em ${pendingApptDate.dateLabel}? Por exemplo: '10h' ou '14h'.`,
|
||||||
|
Mandarin: `我没听清时间。您在 ${pendingApptDate.dateLabel} 几点?例如:上午10点或下午2点。`,
|
||||||
|
Cantonese: `我沒聽清時間。您在 ${pendingApptDate.dateLabel} 幾點?例如:上午10點或下午2點。`,
|
||||||
|
Arabic: `لم أفهم الوقت. ما الوقت الذي تفضله في ${pendingApptDate.dateLabel}؟ مثلاً: 10 صباحاً أو 2 مساءً.`,
|
||||||
|
"Haitian Creole": `Mwen pa konprann lè a. Ki lè ou prefere nan ${pendingApptDate.dateLabel}? Pa egzanp: 10am oswa 2pm.`,
|
||||||
|
};
|
||||||
|
return await replyUnknown(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
|
||||||
|
}
|
||||||
|
const withinHours = await isWithinOfficeHours(pendingApptDate.date, startTime, userId);
|
||||||
|
if (!withinHours) {
|
||||||
|
const hoursDisplay = await getOfficeHoursDisplay(pendingApptDate.date, userId);
|
||||||
|
const timeLbl = timeLabel(startTime);
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: hoursDisplay
|
||||||
|
? `Our office is not available at ${timeLbl} on ${pendingApptDate.dateLabel}. Our hours are ${hoursDisplay}. What other time do you prefer?`
|
||||||
|
: `Our office is not available at ${timeLbl} on ${pendingApptDate.dateLabel}. What other time do you prefer?`,
|
||||||
|
Spanish: hoursDisplay
|
||||||
|
? `Nuestra oficina no está disponible a las ${timeLbl} el ${pendingApptDate.dateLabel}. Nuestro horario es ${hoursDisplay}. ¿Qué otro horario prefiere?`
|
||||||
|
: `Nuestra oficina no está disponible a las ${timeLbl} el ${pendingApptDate.dateLabel}. ¿Qué otro horario prefiere?`,
|
||||||
|
Portuguese: hoursDisplay
|
||||||
|
? `Nosso consultório não está disponível às ${timeLbl} em ${pendingApptDate.dateLabel}. Nosso horário é ${hoursDisplay}. Que outro horário você prefere?`
|
||||||
|
: `Nosso consultório não está disponível às ${timeLbl} em ${pendingApptDate.dateLabel}. Que outro horário você prefere?`,
|
||||||
|
Mandarin: hoursDisplay
|
||||||
|
? `我们诊所在 ${pendingApptDate.dateLabel} ${timeLbl} 不开放。工作时间是 ${hoursDisplay}。您希望改什么时间?`
|
||||||
|
: `我们诊所在 ${pendingApptDate.dateLabel} ${timeLbl} 不开放。您希望改什么时间?`,
|
||||||
|
Cantonese: hoursDisplay
|
||||||
|
? `我們診所在 ${pendingApptDate.dateLabel} ${timeLbl} 不開放。工作時間是 ${hoursDisplay}。您希望改什麼時間?`
|
||||||
|
: `我們診所在 ${pendingApptDate.dateLabel} ${timeLbl} 不開放。您希望改什麼時間?`,
|
||||||
|
Arabic: hoursDisplay
|
||||||
|
? `مكتبنا غير متاح في ${timeLbl} يوم ${pendingApptDate.dateLabel}. ساعات العمل: ${hoursDisplay}. ما وقت آخر تفضله؟`
|
||||||
|
: `مكتبنا غير متاح في ${timeLbl} يوم ${pendingApptDate.dateLabel}. ما وقت آخر تفضله؟`,
|
||||||
|
"Haitian Creole": hoursDisplay
|
||||||
|
? `Biwo nou pa disponib a ${timeLbl} nan ${pendingApptDate.dateLabel}. Orè nou se ${hoursDisplay}. Ki lòt lè ou prefere?`
|
||||||
|
: `Biwo nou pa disponib a ${timeLbl} nan ${pendingApptDate.dateLabel}. Ki lòt lè ou prefere?`,
|
||||||
|
};
|
||||||
|
return await replyUnknown(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
|
||||||
|
}
|
||||||
|
const apptLabel = `${pendingApptDate.dateLabel} at ${timeLabel(startTime)}`;
|
||||||
|
const confirmMsgs: Record<string, string> = {
|
||||||
|
English: `Thank you! Your preferred appointment is ${apptLabel}. Our receptionist will confirm the details with you shortly.`,
|
||||||
|
Spanish: `¡Gracias! Su cita preferida es el ${apptLabel}. Nuestra recepcionista confirmará los detalles con usted en breve.`,
|
||||||
|
Portuguese: `Obrigado! Sua consulta preferida é em ${apptLabel}. Nossa recepcionista confirmará os detalhes em breve.`,
|
||||||
|
Mandarin: `谢谢!您的预约时间为 ${apptLabel}。我们的前台将很快与您确认详情。`,
|
||||||
|
Cantonese: `多謝!您的預約時間為 ${apptLabel}。我們的接待員將很快與您確認詳情。`,
|
||||||
|
Arabic: `شكراً! موعدك المفضل هو ${apptLabel}. ستتواصل معك موظفة الاستقبال قريباً لتأكيد التفاصيل.`,
|
||||||
|
"Haitian Creole": `Mèsi! Randevou ou a se ${apptLabel}. Resepsyonis nou an pral konfime detay yo ak ou byento.`,
|
||||||
|
};
|
||||||
|
return await replyUnknown(confirmMsgs[language] ?? confirmMsgs["English"]!, "done");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-step new patient stages for unknown numbers
|
||||||
|
const unknownNewPatientStages: 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 (unknownNewPatientStages.includes(stage)) {
|
||||||
|
const { reply: aiReply, nextStage } = await runNewPatientStep(
|
||||||
|
Body, stage, language, activeAiU.key, chatTemplates.generalFallback, activeAiU.provider, activeAiU.model
|
||||||
|
);
|
||||||
|
return await replyUnknown(aiReply, nextStage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save inbound message
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: patient.id, channel: "sms", direction: "inbound",
|
||||||
|
status: "delivered", body: Body, twilioSid: MessageSid, provider: "telnyx",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Per-patient handoff toggle must be ON
|
||||||
|
if (!await getHandoff(patient.userId, patient.id)) return ok(res);
|
||||||
|
|
||||||
|
const aiSettings = await storage.getAiSettings(patient.userId);
|
||||||
|
const activeAi = resolveAiProvider(aiSettings ?? {});
|
||||||
|
if (!activeAi) return ok(res);
|
||||||
|
|
||||||
|
const telnyxSettings = await storage.getTelnyxSettings(patient.userId);
|
||||||
|
if (!telnyxSettings) return ok(res);
|
||||||
|
|
||||||
|
const language = patient.preferredLanguage || "English";
|
||||||
|
let stage = await getStage(patient.userId, patient.id);
|
||||||
|
const chatTemplates = await storage.getAiChatTemplates(patient.userId);
|
||||||
|
|
||||||
|
// Long idle gap: don't blindly resume (SMS replies are often just slow),
|
||||||
|
// but don't blindly reset either — check whether the new text still reads
|
||||||
|
// like a continuation before deciding.
|
||||||
|
if (stage !== "initial" && stage !== "done") {
|
||||||
|
const convRow = await db.patientConversation.findUnique({
|
||||||
|
where: { patientId: patient.id },
|
||||||
|
select: { updatedAt: true },
|
||||||
|
});
|
||||||
|
if (convRow?.updatedAt && Date.now() - convRow.updatedAt.getTime() > STALE_SMS_THRESHOLD_MS) {
|
||||||
|
const recentComms = await storage.getCommunicationsByPatient(patient.id);
|
||||||
|
const recentHistory = recentComms.slice(-5).map((c: any) => ({
|
||||||
|
role: (c.direction === "outbound" ? "assistant" : "user") as "assistant" | "user",
|
||||||
|
text: c.body || "",
|
||||||
|
}));
|
||||||
|
const continuing = await isLikelyContinuation(
|
||||||
|
recentHistory, Body, stage, activeAi.key, activeAi.provider, activeAi.model
|
||||||
|
);
|
||||||
|
if (!continuing) {
|
||||||
|
clearPendingReschedule(patient.userId, patient.id);
|
||||||
|
await setStage(patient.userId, patient.id, "initial");
|
||||||
|
stage = "initial";
|
||||||
|
const stillHereText = STILL_HERE_MSGS[language] ?? STILL_HERE_MSGS["English"]!;
|
||||||
|
await saveOutbound(patient.id, stillHereText);
|
||||||
|
await sendReplies(telnyxSettings, From, [stillHereText]);
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
// Looks like a continuation — fall through and resume the stored stage.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const officeContact = await storage.getOfficeContact(patient.userId);
|
||||||
|
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
||||||
|
|
||||||
|
// ── Shared business rules (reminder/reschedule/new-patient graphs, plus the
|
||||||
|
// MassHealth and appointment-booking stages) — the exact same code path the
|
||||||
|
// AI voice call webhook uses, so a patient gets identical guardrails and
|
||||||
|
// outcomes regardless of channel.
|
||||||
|
const routed = await routePatientTurn({
|
||||||
|
patient, stage, message: Body, activeAi, chatTemplates, officeName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (routed.segments.length > 0) {
|
||||||
|
for (const seg of routed.segments) await saveOutbound(patient.id, seg);
|
||||||
|
await sendReplies(telnyxSettings, From, routed.segments);
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stage: initial / done (patient texts in fresh) ───────────────────
|
||||||
|
if (stage === "initial" || stage === "done") {
|
||||||
|
const openPhoneReply = await storage.getOpenPhoneReply(patient.userId);
|
||||||
|
const afterHoursEnabled = await getAfterHoursHandoff(patient.userId);
|
||||||
|
const outsideHours = await isAfterHours(patient.userId);
|
||||||
|
|
||||||
|
if (openPhoneReply || (afterHoursEnabled && outsideHours)) {
|
||||||
|
// MSG 1: AI self-introduction
|
||||||
|
const rawGreeting = chatTemplates.newPatientGreeting ||
|
||||||
|
`Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7.`;
|
||||||
|
const introText = applyOfficeName(rawGreeting, officeName);
|
||||||
|
|
||||||
|
// MSG 2: Empathetic acknowledgment of the patient's message
|
||||||
|
const empatheticText = getEmpatheticAck(Body, language);
|
||||||
|
|
||||||
|
// MSG 3: Detect reschedule vs new appointment from initial text
|
||||||
|
const isRescheduleIntent = /reschedule|rescheduler|change.*appoint|modify.*appoint|move.*appoint|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
||||||
|
|
||||||
|
let msg3: string;
|
||||||
|
let nextStage3: ConversationStage;
|
||||||
|
|
||||||
|
if (isRescheduleIntent) {
|
||||||
|
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è?`,
|
||||||
|
};
|
||||||
|
msg3 = foundMsgs[language] ?? foundMsgs["English"]!;
|
||||||
|
nextStage3 = "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.",
|
||||||
|
};
|
||||||
|
msg3 = notFoundMsgs[language] ?? notFoundMsgs["English"]!;
|
||||||
|
nextStage3 = "done";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New appointment (or any other intent) → ask new or existing patient
|
||||||
|
const newOrExistingMsgs: Record<string, string> = {
|
||||||
|
English: "Can you please tell me if you are a new or existing patient?",
|
||||||
|
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
||||||
|
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
||||||
|
Mandarin: "请问您是新患者还是现有患者?",
|
||||||
|
Cantonese: "請問您是新病人還是現有病人?",
|
||||||
|
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
||||||
|
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
||||||
|
};
|
||||||
|
msg3 = newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!;
|
||||||
|
nextStage3 = "asked_new_or_existing";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all 3 outbound messages and set stage
|
||||||
|
await saveOutbound(patient.id, introText);
|
||||||
|
await saveOutbound(patient.id, empatheticText);
|
||||||
|
await saveOutbound(patient.id, msg3);
|
||||||
|
await setStage(patient.userId, patient.id, nextStage3);
|
||||||
|
|
||||||
|
await sendReplies(telnyxSettings, From, [introText, empatheticText, msg3]);
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(res);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── POST /api/telnyx/webhook/voice ────────────────────────────────────────────
|
||||||
|
|
||||||
|
router.post("/webhook/voice", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const { From, CallSid } = req.body;
|
||||||
|
|
||||||
|
const normalizedFrom = (From || "").replace(/\D/g, "");
|
||||||
|
// Ordered by id — earliest-created record wins when duplicates share a phone.
|
||||||
|
const allPatients = await db.patient.findMany({ select: { id: true, phone: true, userId: true }, orderBy: { id: "asc" } });
|
||||||
|
const patient = allPatients.find(
|
||||||
|
(p: { id: number; phone: string | null; userId: number }) => p.phone && p.phone.replace(/\D/g, "") === normalizedFrom
|
||||||
|
);
|
||||||
|
|
||||||
|
let greeting = "Thank you for calling. Please leave a message after the beep and we will get back to you shortly.";
|
||||||
|
if (patient) {
|
||||||
|
const settings = await storage.getTelnyxSettings(patient.userId);
|
||||||
|
if (settings?.greetingMessage?.trim()) greeting = settings.greetingMessage.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patient) {
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: patient.id, channel: "voice", direction: "inbound",
|
||||||
|
status: "completed", body: "(Inbound call — voicemail below)", twilioSid: CallSid, provider: "telnyx",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordingCallbackUrl = `${getPublicBaseUrl(req)}/api/telnyx/webhook/voice-recording`;
|
||||||
|
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(`<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Response>
|
||||||
|
<Say voice="alice">${greeting}</Say>
|
||||||
|
<Record maxLength="120" action="${recordingCallbackUrl}" transcribeCallback="${recordingCallbackUrl}" playBeep="true"/>
|
||||||
|
<Say voice="alice">We did not receive a recording. Goodbye.</Say>
|
||||||
|
</Response>`);
|
||||||
|
} catch (err) {
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(`<?xml version="1.0" encoding="UTF-8"?><Response><Say>Thank you for calling. Please try again later.</Say></Response>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── POST /api/telnyx/webhook/voice-recording ──────────────────────────────────
|
||||||
|
|
||||||
|
router.post("/webhook/voice-recording", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const { CallSid, RecordingUrl } = req.body;
|
||||||
|
if (RecordingUrl && CallSid) {
|
||||||
|
const comm = await db.communication.findFirst({ where: { twilioSid: CallSid, provider: "telnyx" } });
|
||||||
|
if (comm) {
|
||||||
|
await db.communication.update({
|
||||||
|
where: { id: comm.id },
|
||||||
|
data: { body: `Voicemail: ${RecordingUrl}.mp3` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send("<Response></Response>");
|
||||||
|
} catch (err) {
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send("<Response></Response>");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── POST /api/telnyx/webhook/say ──────────────────────────────────────────────
|
||||||
|
// Minimal helper endpoint for /make-call's one-shot outbound calls — avoids
|
||||||
|
// relying on an unverified "inline TeXML on call creation" SDK feature.
|
||||||
|
router.post("/webhook/say", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
const text = typeof req.query.text === "string" ? req.query.text : "Hello, this is a call from your dental office.";
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(`<?xml version="1.0" encoding="UTF-8"?><Response><Say>${escapeXml(text)}</Say></Response>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── AI phone call (outbound "AI Call" from Patient Connection) ───────────────
|
||||||
|
|
||||||
|
const MAX_AI_CALL_TURNS = 12;
|
||||||
|
|
||||||
|
interface AiCallSession {
|
||||||
|
patientId: number;
|
||||||
|
userId: number;
|
||||||
|
phone: string | null;
|
||||||
|
language: string;
|
||||||
|
officeName: string;
|
||||||
|
chatTemplates: Record<string, string>;
|
||||||
|
history: { role: "assistant" | "user"; text: string }[];
|
||||||
|
turns: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiCallSessions = new Map<string, AiCallSession>();
|
||||||
|
|
||||||
|
interface VoiceLocale { gatherLang: string; sayVoice: string; }
|
||||||
|
|
||||||
|
/** English/Spanish only for now — Telnyx TeXML's native voice/recognition
|
||||||
|
* coverage for the app's other supported languages (Mandarin, Cantonese,
|
||||||
|
* Arabic, Haitian Creole) is inconsistent, so those fall back to English on
|
||||||
|
* calls. NOTE: voice names below (Polly.*) are carried over from the Twilio
|
||||||
|
* implementation — verify Telnyx TeXML's <Say voice="..."> naming convention
|
||||||
|
* against a real test call before relying on this. */
|
||||||
|
function voiceLocaleFor(language: string): VoiceLocale {
|
||||||
|
if (language === "Spanish") return { gatherLang: "es-US", sayVoice: "Polly.Lupe" };
|
||||||
|
return { gatherLang: "en-US", sayVoice: "Polly.Joanna" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spoken greeting is plain text, not LLM-generated, so it can't follow the
|
||||||
|
// patient's language on its own — translate it ourselves when the office
|
||||||
|
// hasn't overridden the default wording. Matches voiceLocaleFor's English/
|
||||||
|
// Spanish-only voice coverage above.
|
||||||
|
const GREETING_TRANSLATIONS: Record<string, string> = {
|
||||||
|
Spanish: "Hola {firstName}, soy Lisa, la asistente de inteligencia artificial de {officeName}. ¿En qué puedo ayudarle hoy?",
|
||||||
|
};
|
||||||
|
const RECALL_GREETING_TRANSLATIONS: Record<string, string> = {
|
||||||
|
Spanish: "Hola {firstName}, soy Lisa de {officeName}. Queríamos avisarle que ya le corresponde un examen y limpieza de rutina, que la mayoría de los seguros cubren sin costo. ¿Le gustaría programar una cita?",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Picks the greeting template to speak: the office's custom wording as-is if
|
||||||
|
* they've edited it (we can't auto-translate free text they wrote), otherwise
|
||||||
|
* the default translated into the patient's language when available. */
|
||||||
|
function resolveGreetingTemplate(rawTemplate: string, defaultTemplate: string, translations: Record<string, string>, language: string): string {
|
||||||
|
if (rawTemplate.trim() !== defaultTemplate) return rawTemplate;
|
||||||
|
return translations[language] ?? defaultTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENGLISH_LOCALE: VoiceLocale = { gatherLang: "en-US", sayVoice: "Polly.Joanna" };
|
||||||
|
|
||||||
|
function aiVoiceTwiml(sayText: string, gatherActionUrl: string, locale: VoiceLocale): string {
|
||||||
|
// action is always set, so the platform posts back to it on both speech and
|
||||||
|
// silence timeout — nothing after </Gather> would ever run.
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Response>
|
||||||
|
<Gather input="speech" language="${locale.gatherLang}" speechTimeout="auto" timeout="15" action="${gatherActionUrl}" method="POST">
|
||||||
|
<Say voice="${locale.sayVoice}">${escapeXml(sayText)}</Say>
|
||||||
|
</Gather>
|
||||||
|
</Response>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aiVoiceHangup(sayText: string, locale: VoiceLocale = ENGLISH_LOCALE): string {
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Response>
|
||||||
|
<Say voice="${locale.sayVoice}">${escapeXml(sayText)}</Say>
|
||||||
|
<Hangup/>
|
||||||
|
</Response>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/telnyx/webhook/ai-voice
|
||||||
|
// Entry point + conversation loop for outbound AI-driven calls placed via
|
||||||
|
// POST /api/telnyx/make-ai-call. TeXML hits this once when the call connects
|
||||||
|
// (no CallSid session yet) and again after every <Gather> with SpeechResult.
|
||||||
|
// Each spoken turn is routed through the same routePatientTurn() logic the
|
||||||
|
// SMS webhook uses, so calls follow identical rules/guardrails, in the
|
||||||
|
// patient's preferred language (English/Spanish supported for voice).
|
||||||
|
router.post("/webhook/ai-voice", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
try {
|
||||||
|
const { CallSid, SpeechResult } = req.body as { CallSid?: string; SpeechResult?: string };
|
||||||
|
const patientIdParam = req.query.patientId as string | undefined;
|
||||||
|
const purposeParam = req.query.purpose as string | undefined;
|
||||||
|
if (!CallSid) return res.send(aiVoiceHangup("Sorry, something went wrong. Goodbye."));
|
||||||
|
|
||||||
|
let session = aiCallSessions.get(CallSid);
|
||||||
|
|
||||||
|
// ── First hit for this call: set up session + greet ──────────────────
|
||||||
|
if (!session) {
|
||||||
|
const patientId = parseInt(patientIdParam || "", 10);
|
||||||
|
const patient = !isNaN(patientId)
|
||||||
|
? await db.patient.findUnique({
|
||||||
|
where: { id: patientId },
|
||||||
|
select: { id: true, userId: true, firstName: true, phone: true, preferredLanguage: true },
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (!patient) return res.send(aiVoiceHangup("Sorry, we could not find your patient record. Goodbye."));
|
||||||
|
|
||||||
|
const language = patient.preferredLanguage || "English";
|
||||||
|
const locale = voiceLocaleFor(language);
|
||||||
|
const isRecallCall = purposeParam === "recall";
|
||||||
|
|
||||||
|
// Resume an in-progress cross-channel conversation (e.g. mid-reschedule
|
||||||
|
// from a prior text) if one exists; otherwise start a fresh check-in call.
|
||||||
|
// A "recall" call (staff-triggered from Patient Connection, offering an
|
||||||
|
// existing patient a routine exam & cleaning) seeds a different stage
|
||||||
|
// than the default reminder-style check-in.
|
||||||
|
// Either kind of call always speaks its own pitch fresh (see greeting
|
||||||
|
// below), so it must always reset the stage — even if a prior call of
|
||||||
|
// either kind left the patient stuck mid-flow (e.g. "recall_asked_datetime"
|
||||||
|
// or "asked_reschedule_datetime"). Otherwise the reply below gets
|
||||||
|
// interpreted against that stale leftover stage instead of the fresh
|
||||||
|
// pitch that was just spoken.
|
||||||
|
if (isRecallCall) {
|
||||||
|
await startRecallConversation(patient.userId, patient.id);
|
||||||
|
} else {
|
||||||
|
await resetConversation(patient.userId, patient.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const officeContact = await storage.getOfficeContact(patient.userId);
|
||||||
|
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
||||||
|
const callTemplates = await storage.getAiCallTemplates(patient.userId);
|
||||||
|
|
||||||
|
session = {
|
||||||
|
patientId: patient.id, userId: patient.userId, phone: patient.phone,
|
||||||
|
language, officeName, chatTemplates: callTemplates, history: [], turns: 0,
|
||||||
|
};
|
||||||
|
aiCallSessions.set(CallSid, session);
|
||||||
|
|
||||||
|
const rawCallTemplate = (isRecallCall ? callTemplates.recallGreeting?.trim() : callTemplates.greeting?.trim()) ||
|
||||||
|
(isRecallCall ? DEFAULT_RECALL_GREETING : DEFAULT_GREETING);
|
||||||
|
const callTemplate = isRecallCall
|
||||||
|
? resolveGreetingTemplate(rawCallTemplate, DEFAULT_RECALL_GREETING, RECALL_GREETING_TRANSLATIONS, language)
|
||||||
|
: resolveGreetingTemplate(rawCallTemplate, DEFAULT_GREETING, GREETING_TRANSLATIONS, language);
|
||||||
|
const greeting = callTemplate
|
||||||
|
.replace(/\{firstName\}/g, patient.firstName || "there")
|
||||||
|
.replace(/\{officeName\}/g, officeName || "our office");
|
||||||
|
|
||||||
|
session.history.push({ role: "assistant", text: greeting });
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: patient.id, userId: patient.userId, channel: "voice",
|
||||||
|
direction: "outbound", status: "completed", body: greeting, twilioSid: CallSid, provider: "telnyx",
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionUrl = `${getPublicBaseUrl(req)}/api/telnyx/webhook/ai-voice?patientId=${patient.id}`;
|
||||||
|
return res.send(aiVoiceTwiml(greeting, actionUrl, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = voiceLocaleFor(session.language);
|
||||||
|
|
||||||
|
// ── Caller stayed silent past the speech timeout ──────────────────────
|
||||||
|
// A call is a single continuous conversation, unlike SMS — if it ends
|
||||||
|
// here, mid-flow or not, there's nothing left to resume, so reset the
|
||||||
|
// stage now instead of leaving the patient stuck for their next call/text.
|
||||||
|
if (!SpeechResult?.trim()) {
|
||||||
|
aiCallSessions.delete(CallSid);
|
||||||
|
await setStage(session.userId, session.patientId, "done");
|
||||||
|
return res.send(aiVoiceHangup("I didn't catch a response. Our staff will follow up with you if needed. Goodbye.", locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
session.history.push({ role: "user", text: SpeechResult.trim() });
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: session.patientId, userId: session.userId, channel: "voice",
|
||||||
|
direction: "inbound", status: "completed", body: SpeechResult.trim(), twilioSid: CallSid, provider: "telnyx",
|
||||||
|
});
|
||||||
|
|
||||||
|
session.turns++;
|
||||||
|
|
||||||
|
// ── Caller said something that sounds like goodbye, or we've hit the cap ──
|
||||||
|
if (soundsLikeGoodbye(SpeechResult) || session.turns >= MAX_AI_CALL_TURNS) {
|
||||||
|
aiCallSessions.delete(CallSid);
|
||||||
|
await setStage(session.userId, session.patientId, "done");
|
||||||
|
return res.send(aiVoiceHangup("Thank you for calling, have a great day. Goodbye.", locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiSettings = await storage.getAiSettings(session.userId);
|
||||||
|
const activeAi = resolveAiProvider(aiSettings ?? {});
|
||||||
|
if (!activeAi) {
|
||||||
|
aiCallSessions.delete(CallSid);
|
||||||
|
await setStage(session.userId, session.patientId, "done");
|
||||||
|
return res.send(aiVoiceHangup("Our AI assistant is not available right now. Our staff will follow up with you. Goodbye.", locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
const stage = await getStage(session.userId, session.patientId);
|
||||||
|
const routed = await routePatientTurn({
|
||||||
|
patient: {
|
||||||
|
id: session.patientId, userId: session.userId,
|
||||||
|
phone: session.phone, preferredLanguage: session.language,
|
||||||
|
},
|
||||||
|
stage, message: SpeechResult.trim(), activeAi,
|
||||||
|
chatTemplates: session.chatTemplates, officeName: session.officeName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply = routed.segments.join(" ").trim() ||
|
||||||
|
"Sorry, I didn't quite catch that — could you say that again?";
|
||||||
|
|
||||||
|
session.history.push({ role: "assistant", text: reply });
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: session.patientId, userId: session.userId, channel: "voice",
|
||||||
|
direction: "outbound", status: "completed", body: reply, twilioSid: CallSid, provider: "telnyx",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (routed.nextStage === "done" || soundsLikeGoodbye(reply)) {
|
||||||
|
aiCallSessions.delete(CallSid);
|
||||||
|
// routePatientTurn() already persisted nextStage — but if we're hanging
|
||||||
|
// up because the reply sounded like a goodbye while nextStage was still
|
||||||
|
// mid-flow, force it to "done" so the next call/text doesn't resume there.
|
||||||
|
if (routed.nextStage !== "done") {
|
||||||
|
await setStage(session.userId, session.patientId, "done");
|
||||||
|
}
|
||||||
|
return res.send(aiVoiceHangup(reply, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionUrl = `${getPublicBaseUrl(req)}/api/telnyx/webhook/ai-voice?patientId=${session.patientId}`;
|
||||||
|
return res.send(aiVoiceTwiml(reply, actionUrl, locale));
|
||||||
|
} catch (err) {
|
||||||
|
return res.send(aiVoiceHangup("Sorry, something went wrong on our end. Goodbye."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import { storage } from "../storage";
|
||||||
|
import { prisma as db } from "@repo/db/client";
|
||||||
|
import { sendTelnyxSms, placeTexmlCall } from "../comms/telnyx-client";
|
||||||
|
import { getHandoff, setHandoff, resetConversation, startNewPatientConversation, startRescheduleConversation, getAfterHoursHandoff, setAfterHoursHandoff } from "../ai/aiHandoffStore";
|
||||||
|
import { resolveAiProvider } from "../ai/llm-factory";
|
||||||
|
import { getPublicTunnelBaseUrl } from "../utils/publicUrl";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// GET /api/telnyx/settings
|
||||||
|
router.get("/settings", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const settings = await storage.getTelnyxSettings(userId);
|
||||||
|
if (!settings) return res.status(200).json(null);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
id: settings.id,
|
||||||
|
apiKey: settings.apiKey,
|
||||||
|
accountSid: settings.accountSid,
|
||||||
|
phoneNumber: settings.phoneNumber,
|
||||||
|
texmlApplicationId: settings.texmlApplicationId,
|
||||||
|
messagingProfileId: settings.messagingProfileId,
|
||||||
|
webhookPublicKey: settings.webhookPublicKey,
|
||||||
|
greetingMessage: settings.greetingMessage,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to fetch Telnyx settings", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/telnyx/settings
|
||||||
|
router.put("/settings", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const { apiKey, accountSid, phoneNumber, texmlApplicationId, messagingProfileId, webhookPublicKey, greetingMessage } = req.body;
|
||||||
|
if (!apiKey?.trim() || !phoneNumber?.trim()) {
|
||||||
|
return res.status(400).json({ message: "apiKey and phoneNumber are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await storage.upsertTelnyxSettings(userId, {
|
||||||
|
apiKey: apiKey.trim(),
|
||||||
|
accountSid: accountSid?.trim() || null,
|
||||||
|
phoneNumber: phoneNumber.trim(),
|
||||||
|
texmlApplicationId: texmlApplicationId?.trim() || null,
|
||||||
|
messagingProfileId: messagingProfileId?.trim() || null,
|
||||||
|
webhookPublicKey: webhookPublicKey?.trim() || null,
|
||||||
|
greetingMessage: greetingMessage?.trim() || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
id: settings.id,
|
||||||
|
apiKey: settings.apiKey,
|
||||||
|
accountSid: settings.accountSid,
|
||||||
|
phoneNumber: settings.phoneNumber,
|
||||||
|
texmlApplicationId: settings.texmlApplicationId,
|
||||||
|
messagingProfileId: settings.messagingProfileId,
|
||||||
|
webhookPublicKey: settings.webhookPublicKey,
|
||||||
|
greetingMessage: settings.greetingMessage,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to save Telnyx settings", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/telnyx/send-sms
|
||||||
|
router.post("/send-sms", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const { to, message, patientId, startFlow } = req.body;
|
||||||
|
if (!to || !message) return res.status(400).json({ message: "to and message are required" });
|
||||||
|
|
||||||
|
const settings = await storage.getTelnyxSettings(userId);
|
||||||
|
if (!settings) {
|
||||||
|
return res.status(400).json({ message: "Telnyx is not configured. Please add your Telnyx credentials in Settings." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sent = await sendTelnyxSms({
|
||||||
|
apiKey: settings.apiKey, from: settings.phoneNumber, to, text: message,
|
||||||
|
messagingProfileId: settings.messagingProfileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (patientId) {
|
||||||
|
const pid = Number(patientId);
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: pid,
|
||||||
|
userId,
|
||||||
|
channel: "sms",
|
||||||
|
direction: "outbound",
|
||||||
|
status: "sent",
|
||||||
|
body: message,
|
||||||
|
twilioSid: sent.id,
|
||||||
|
provider: "telnyx",
|
||||||
|
});
|
||||||
|
// Set conversation stage based on which flow was started
|
||||||
|
if (startFlow === "new_patient") {
|
||||||
|
await startNewPatientConversation(userId, pid);
|
||||||
|
} else if (startFlow === "reschedule") {
|
||||||
|
await startRescheduleConversation(userId, pid);
|
||||||
|
} else if (startFlow === "no_follow_up") {
|
||||||
|
// AI follow-up disabled — leave existing stage untouched
|
||||||
|
} else {
|
||||||
|
// "reminder" or unspecified → start reminder AI flow
|
||||||
|
await resetConversation(userId, pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ sid: sent.id, status: sent.status });
|
||||||
|
} catch (err: any) {
|
||||||
|
return res.status(500).json({ error: err.message || "Failed to send SMS" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/telnyx/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.getTelnyxSettings(userId);
|
||||||
|
if (!settings) {
|
||||||
|
return res.status(400).json({ message: "Telnyx is not configured. Please add your Telnyx credentials in Settings." });
|
||||||
|
}
|
||||||
|
if (!settings.accountSid) {
|
||||||
|
return res.status(400).json({ message: "Telnyx Account SID is not configured. Please add it in Telnyx 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 = getPublicTunnelBaseUrl();
|
||||||
|
} catch (err: any) {
|
||||||
|
return res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
const callPurpose = purpose === "recall" ? "recall" : "reminder";
|
||||||
|
const webhookUrl = `${publicBaseUrl}/api/telnyx/webhook/ai-voice?patientId=${patient.id}&purpose=${callPurpose}`;
|
||||||
|
|
||||||
|
const call = await placeTexmlCall({
|
||||||
|
apiKey: settings.apiKey,
|
||||||
|
accountSid: settings.accountSid,
|
||||||
|
texmlApplicationId: settings.texmlApplicationId,
|
||||||
|
from: settings.phoneNumber,
|
||||||
|
to,
|
||||||
|
webhookUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ sid: call.id, status: call.status });
|
||||||
|
} catch (err: any) {
|
||||||
|
return res.status(500).json({ error: err.message || "Failed to place AI call" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/telnyx/send-reminders-batch
|
||||||
|
router.post("/send-reminders-batch", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const { date, staffIds, aiFollowUp = true } = req.body as {
|
||||||
|
date: string;
|
||||||
|
staffIds: number[];
|
||||||
|
aiFollowUp?: boolean;
|
||||||
|
};
|
||||||
|
if (!date || !Array.isArray(staffIds) || staffIds.length === 0) {
|
||||||
|
return res.status(400).json({ message: "date and staffIds are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await storage.getTelnyxSettings(userId);
|
||||||
|
if (!settings) {
|
||||||
|
return res.status(400).json({ message: "Telnyx is not configured. Please add your Telnyx credentials in Settings." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve office name, address, phone, and reminder SMS template
|
||||||
|
const officeContact = await storage.getOfficeContact(userId);
|
||||||
|
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
||||||
|
const officeAddress = [
|
||||||
|
(officeContact as any)?.streetAddress?.trim(),
|
||||||
|
(officeContact as any)?.city?.trim(),
|
||||||
|
(officeContact as any)?.state?.trim(),
|
||||||
|
(officeContact as any)?.zipCode?.trim(),
|
||||||
|
].filter(Boolean).join(", ");
|
||||||
|
const officePhone = (officeContact as any)?.phoneNumber?.trim() || "";
|
||||||
|
const chatTemplates = await storage.getAiChatTemplates(userId);
|
||||||
|
|
||||||
|
const DEFAULT_REMINDER_SMS =
|
||||||
|
"Hi {firstName}, this is a reminder from {officeName}. You have an appointment on {appointmentDate} at {appointmentTime}. Please come to our office at {officeAddress}. Please reply YES to confirm or NO to reschedule. Thank you!";
|
||||||
|
const templateBody = chatTemplates.reminderSms?.trim() || DEFAULT_REMINDER_SMS;
|
||||||
|
|
||||||
|
// Fetch appointments for the selected staff columns on the given date
|
||||||
|
const dayStart = new Date(date);
|
||||||
|
dayStart.setUTCHours(0, 0, 0, 0);
|
||||||
|
const dayEnd = new Date(date);
|
||||||
|
dayEnd.setUTCHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
const appointments = await db.appointment.findMany({
|
||||||
|
where: {
|
||||||
|
staffId: { in: staffIds },
|
||||||
|
date: { gte: dayStart, lte: dayEnd },
|
||||||
|
status: { not: "cancelled" },
|
||||||
|
patient: { userId },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
patient: { select: { id: true, firstName: true, phone: true } },
|
||||||
|
},
|
||||||
|
orderBy: { startTime: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format a date string as "May 9, 2026"
|
||||||
|
const months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||||||
|
const formatApptDate = (d: Date | string) => {
|
||||||
|
const dt = new Date(d);
|
||||||
|
return `${months[dt.getUTCMonth()]} ${dt.getUTCDate()}, ${dt.getUTCFullYear()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let sent = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
// Deduplicate by patientId — send at most one reminder per patient per run
|
||||||
|
const seen = new Set<number>();
|
||||||
|
|
||||||
|
for (const appt of appointments) {
|
||||||
|
const patient = appt.patient;
|
||||||
|
if (!patient?.phone || seen.has(patient.id)) { skipped++; continue; }
|
||||||
|
seen.add(patient.id);
|
||||||
|
|
||||||
|
const apptDate = formatApptDate(appt.date);
|
||||||
|
const apptTime = typeof appt.startTime === "string"
|
||||||
|
? appt.startTime.substring(0, 5)
|
||||||
|
: String(appt.startTime);
|
||||||
|
|
||||||
|
const message = templateBody
|
||||||
|
.replace(/\{firstName\}/g, patient.firstName ?? "")
|
||||||
|
.replace(/\{officeName\}/g, officeName)
|
||||||
|
.replace(/\{officeAddress\}/g, officeAddress)
|
||||||
|
.replace(/\{officePhone\}/g, officePhone)
|
||||||
|
.replace(/\{twilioPhone\}/g, settings.phoneNumber)
|
||||||
|
.replace(/\{appointmentDate\}/g, apptDate)
|
||||||
|
.replace(/\{appointmentTime\}/g, apptTime)
|
||||||
|
.replace(/\{date\}/g, apptDate)
|
||||||
|
.replace(/\{time\}/g, apptTime);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msg = await sendTelnyxSms({
|
||||||
|
apiKey: settings.apiKey, from: settings.phoneNumber, to: patient.phone, text: message,
|
||||||
|
messagingProfileId: settings.messagingProfileId,
|
||||||
|
});
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: patient.id,
|
||||||
|
userId,
|
||||||
|
channel: "sms",
|
||||||
|
direction: "outbound",
|
||||||
|
status: "sent",
|
||||||
|
body: message,
|
||||||
|
twilioSid: msg.id,
|
||||||
|
provider: "telnyx",
|
||||||
|
});
|
||||||
|
if (aiFollowUp) {
|
||||||
|
await resetConversation(userId, patient.id);
|
||||||
|
}
|
||||||
|
sent++;
|
||||||
|
} catch {
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ sent, skipped });
|
||||||
|
} catch (err: any) {
|
||||||
|
return res.status(500).json({ error: err.message || "Failed to send reminders" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/telnyx/send-reschedule-batch
|
||||||
|
router.post("/send-reschedule-batch", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const { date, staffIds, aiFollowUp = true } = req.body as { date: string; staffIds: number[]; aiFollowUp?: boolean };
|
||||||
|
if (!date || !Array.isArray(staffIds) || staffIds.length === 0) {
|
||||||
|
return res.status(400).json({ message: "date and staffIds are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await storage.getTelnyxSettings(userId);
|
||||||
|
if (!settings) {
|
||||||
|
return res.status(400).json({ message: "Telnyx is not configured. Please add your Telnyx credentials in Settings." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the "Reschedule by office" template from the SMS template list
|
||||||
|
const templateList = await storage.getSmsTemplateList(userId);
|
||||||
|
const rescheduleTemplate = templateList.find((t) =>
|
||||||
|
t.name.toLowerCase().includes("reschedule") && t.name.toLowerCase().includes("office")
|
||||||
|
) || templateList.find((t) => t.name.toLowerCase().includes("reschedule"));
|
||||||
|
|
||||||
|
const DEFAULT_RESCHEDULE_SMS =
|
||||||
|
"Hi {firstName}, this is {officeName}. We need to reschedule your appointment. Please reply to let us know your availability. Thank you!";
|
||||||
|
const templateBody = rescheduleTemplate?.body?.trim() || DEFAULT_RESCHEDULE_SMS;
|
||||||
|
|
||||||
|
const officeContact = await storage.getOfficeContact(userId);
|
||||||
|
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
||||||
|
const officeAddress = [
|
||||||
|
(officeContact as any)?.streetAddress?.trim(),
|
||||||
|
(officeContact as any)?.city?.trim(),
|
||||||
|
(officeContact as any)?.state?.trim(),
|
||||||
|
(officeContact as any)?.zipCode?.trim(),
|
||||||
|
].filter(Boolean).join(", ");
|
||||||
|
const officePhone = (officeContact as any)?.phoneNumber?.trim() || "";
|
||||||
|
|
||||||
|
const dayStart = new Date(date);
|
||||||
|
dayStart.setUTCHours(0, 0, 0, 0);
|
||||||
|
const dayEnd = new Date(date);
|
||||||
|
dayEnd.setUTCHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
const appointments = await db.appointment.findMany({
|
||||||
|
where: {
|
||||||
|
staffId: { in: staffIds },
|
||||||
|
date: { gte: dayStart, lte: dayEnd },
|
||||||
|
status: { not: "cancelled" },
|
||||||
|
patient: { userId },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
patient: { select: { id: true, firstName: true, phone: true } },
|
||||||
|
},
|
||||||
|
orderBy: { startTime: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||||||
|
const formatApptDate = (d: Date | string) => {
|
||||||
|
const dt = new Date(d);
|
||||||
|
return `${months[dt.getUTCMonth()]} ${dt.getUTCDate()}, ${dt.getUTCFullYear()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let sent = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
const seen = new Set<number>();
|
||||||
|
|
||||||
|
for (const appt of appointments) {
|
||||||
|
const patient = appt.patient;
|
||||||
|
if (!patient?.phone || seen.has(patient.id)) { skipped++; continue; }
|
||||||
|
seen.add(patient.id);
|
||||||
|
|
||||||
|
const apptDate = formatApptDate(appt.date);
|
||||||
|
const apptTime = typeof appt.startTime === "string"
|
||||||
|
? appt.startTime.substring(0, 5)
|
||||||
|
: String(appt.startTime);
|
||||||
|
|
||||||
|
const message = templateBody
|
||||||
|
.replace(/\{firstName\}/g, patient.firstName ?? "")
|
||||||
|
.replace(/\{officeName\}/g, officeName)
|
||||||
|
.replace(/\{officeAddress\}/g, officeAddress)
|
||||||
|
.replace(/\{officePhone\}/g, officePhone)
|
||||||
|
.replace(/\{twilioPhone\}/g, settings.phoneNumber)
|
||||||
|
.replace(/\{appointmentDate\}/g, apptDate)
|
||||||
|
.replace(/\{appointmentTime\}/g, apptTime)
|
||||||
|
.replace(/\{date\}/g, apptDate)
|
||||||
|
.replace(/\{time\}/g, apptTime);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msg = await sendTelnyxSms({
|
||||||
|
apiKey: settings.apiKey, from: settings.phoneNumber, to: patient.phone, text: message,
|
||||||
|
messagingProfileId: settings.messagingProfileId,
|
||||||
|
});
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: patient.id,
|
||||||
|
userId,
|
||||||
|
channel: "sms",
|
||||||
|
direction: "outbound",
|
||||||
|
status: "sent",
|
||||||
|
body: message,
|
||||||
|
twilioSid: msg.id,
|
||||||
|
provider: "telnyx",
|
||||||
|
});
|
||||||
|
if (aiFollowUp) {
|
||||||
|
await startRescheduleConversation(userId, patient.id);
|
||||||
|
}
|
||||||
|
sent++;
|
||||||
|
} catch {
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ sent, skipped });
|
||||||
|
} catch (err: any) {
|
||||||
|
return res.status(500).json({ error: err.message || "Failed to send reschedule messages" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/telnyx/sms-template-list
|
||||||
|
router.get("/sms-template-list", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
const list = await storage.getSmsTemplateList(userId);
|
||||||
|
return res.status(200).json(list);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to fetch SMS templates", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/telnyx/sms-template-list
|
||||||
|
router.put("/sms-template-list", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
const templates = req.body;
|
||||||
|
if (!Array.isArray(templates)) return res.status(400).json({ message: "Expected an array of templates" });
|
||||||
|
await storage.saveSmsTemplateList(userId, templates);
|
||||||
|
return res.status(200).json({ ok: true });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to save SMS templates", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/telnyx/templates
|
||||||
|
router.get("/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.getTemplates(userId);
|
||||||
|
return res.status(200).json(templates);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to fetch templates", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/telnyx/templates/:key
|
||||||
|
router.put("/templates/:key", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
const { key } = req.params;
|
||||||
|
const { body } = req.body;
|
||||||
|
if (!key || !body?.trim()) return res.status(400).json({ message: "key and body are required" });
|
||||||
|
await storage.saveTemplate(userId, key, body.trim());
|
||||||
|
return res.status(200).json({ key, body: body.trim() });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to save template", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/telnyx/after-hours-handoff
|
||||||
|
router.get("/after-hours-handoff", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
return res.status(200).json({ enabled: await getAfterHoursHandoff(userId) });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to get after-hours handoff state" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/telnyx/after-hours-handoff
|
||||||
|
router.put("/after-hours-handoff", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
const { enabled } = req.body;
|
||||||
|
if (typeof enabled !== "boolean") return res.status(400).json({ message: "enabled must be a boolean" });
|
||||||
|
await setAfterHoursHandoff(userId, enabled);
|
||||||
|
return res.status(200).json({ enabled });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to set after-hours handoff state" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/telnyx/ai-handoff/:patientId
|
||||||
|
router.get("/ai-handoff/:patientId", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
const patientId = parseInt(req.params.patientId);
|
||||||
|
if (isNaN(patientId)) return res.status(400).json({ message: "Invalid patientId" });
|
||||||
|
return res.status(200).json({ enabled: await getHandoff(userId, patientId) });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to get AI handoff state" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/telnyx/ai-handoff/:patientId
|
||||||
|
router.put("/ai-handoff/:patientId", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
const patientId = parseInt(req.params.patientId);
|
||||||
|
if (isNaN(patientId)) return res.status(400).json({ message: "Invalid patientId" });
|
||||||
|
const { enabled } = req.body;
|
||||||
|
if (typeof enabled !== "boolean") return res.status(400).json({ message: "enabled must be a boolean" });
|
||||||
|
await setHandoff(userId, patientId, enabled);
|
||||||
|
return res.status(200).json({ enabled });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to set AI handoff state" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/telnyx/recent-communications
|
||||||
|
router.get("/recent-communications", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const limit = Math.min(50, parseInt(req.query.limit as string) || 20);
|
||||||
|
const communications = await storage.getRecentCommunicationsByUser(userId, limit);
|
||||||
|
return res.status(200).json(communications);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to fetch recent communications", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/telnyx/make-call
|
||||||
|
router.post("/make-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, message, patientId } = req.body;
|
||||||
|
if (!to) return res.status(400).json({ message: "to is required" });
|
||||||
|
|
||||||
|
const settings = await storage.getTelnyxSettings(userId);
|
||||||
|
if (!settings) {
|
||||||
|
return res.status(400).json({ message: "Telnyx is not configured. Please add your Telnyx credentials in Settings." });
|
||||||
|
}
|
||||||
|
if (!settings.accountSid) {
|
||||||
|
return res.status(400).json({ message: "Telnyx Account SID is not configured. Please add it in Telnyx Settings." });
|
||||||
|
}
|
||||||
|
|
||||||
|
let publicBaseUrl: string;
|
||||||
|
try {
|
||||||
|
publicBaseUrl = getPublicTunnelBaseUrl();
|
||||||
|
} catch (err: any) {
|
||||||
|
return res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
const twimlMessage = message || "Hello, this is a call from your dental office.";
|
||||||
|
const webhookUrl = `${publicBaseUrl}/api/telnyx/webhook/say?text=${encodeURIComponent(twimlMessage)}`;
|
||||||
|
|
||||||
|
const call = await placeTexmlCall({
|
||||||
|
apiKey: settings.apiKey,
|
||||||
|
accountSid: settings.accountSid,
|
||||||
|
texmlApplicationId: settings.texmlApplicationId,
|
||||||
|
from: settings.phoneNumber,
|
||||||
|
to,
|
||||||
|
webhookUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (patientId) {
|
||||||
|
await storage.createCommunication({
|
||||||
|
patientId: Number(patientId),
|
||||||
|
userId,
|
||||||
|
channel: "voice",
|
||||||
|
direction: "outbound",
|
||||||
|
status: "queued",
|
||||||
|
twilioSid: call.id,
|
||||||
|
provider: "telnyx",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ sid: call.id, status: call.status });
|
||||||
|
} catch (err: any) {
|
||||||
|
return res.status(500).json({ error: err.message || "Failed to make call" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,699 +1,17 @@
|
|||||||
|
// NOTE: Twilio is now used only for the in-browser dial pad. This webhook
|
||||||
|
// handles the browser-Device outbound-call leg only — everything else (SMS,
|
||||||
|
// inbound/AI voice) moved to Telnyx, see routes/telnyx-webhooks.ts.
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import twilio from "twilio";
|
|
||||||
import { storage } from "../storage";
|
|
||||||
import { DEFAULT_GREETING, DEFAULT_RECALL_GREETING } from "../storage/twilio-storage";
|
|
||||||
import { prisma as db } from "@repo/db/client";
|
import { prisma as db } from "@repo/db/client";
|
||||||
import { runNewPatientStep } from "../ai/new-patient-graph";
|
|
||||||
import {
|
|
||||||
parseDateOnlyFromMessage,
|
|
||||||
parseTime,
|
|
||||||
isOfficeDayOpen,
|
|
||||||
isWithinOfficeHours,
|
|
||||||
getOfficeHoursDisplay,
|
|
||||||
timeLabel,
|
|
||||||
} from "../ai/reschedule-graph";
|
|
||||||
import { getLlm, resolveAiProvider, type AiProvider } from "../ai/llm-factory";
|
|
||||||
import { soundsLikeGoodbye } from "../ai/voice-assistant";
|
|
||||||
import { getPublicBaseUrl } from "../utils/publicUrl";
|
|
||||||
import {
|
|
||||||
routePatientTurn, getAppointmentDatetime, applyOfficeName,
|
|
||||||
} from "../ai/patient-conversation-router";
|
|
||||||
import {
|
|
||||||
getHandoff, getAfterHoursHandoff,
|
|
||||||
getStage, setStage, resetConversation, startRecallConversation,
|
|
||||||
setPendingReschedule, getPendingReschedule, clearPendingReschedule,
|
|
||||||
type ConversationStage,
|
|
||||||
} from "../ai/aiHandoffStore";
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function escapeXml(text: string): string {
|
function escapeXml(text: string): string {
|
||||||
return text
|
return text
|
||||||
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||||
.replace(/"/g, """).replace(/'/g, "'");
|
.replace(/"/g, """).replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send multiple SMS messages in guaranteed order via a single TwiML response. */
|
|
||||||
function twimlMessages(...texts: string[]): string {
|
|
||||||
const body = texts.map(t => `<Message>${escapeXml(t)}</Message>`).join("");
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?><Response>${body}</Response>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function twimlReply(text: string): string {
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?><Response><Message>${escapeXml(text)}</Message></Response>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function empty(): string {
|
|
||||||
return "<Response></Response>";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if right now is outside office hours for the given user. */
|
|
||||||
async function isAfterHours(userId: number): Promise<boolean> {
|
|
||||||
const record = await storage.getOfficeHours(userId);
|
|
||||||
if (!record?.data) return false; // no hours configured → treat as in-hours
|
|
||||||
|
|
||||||
const data = record.data as any;
|
|
||||||
const now = new Date();
|
|
||||||
const days = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];
|
|
||||||
const day = days[now.getDay()];
|
|
||||||
|
|
||||||
const slot = data.doctors?.[day];
|
|
||||||
if (!slot?.enabled) return true; // office closed today
|
|
||||||
|
|
||||||
const hhmm = `${String(now.getHours()).padStart(2,"0")}:${String(now.getMinutes()).padStart(2,"0")}`;
|
|
||||||
if (hhmm >= slot.amStart && hhmm <= slot.amEnd) return false;
|
|
||||||
if (hhmm >= slot.pmStart && hhmm <= slot.pmEnd) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save an outbound message and return the text. */
|
|
||||||
async function saveOutbound(patientId: number, body: string): Promise<void> {
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId, channel: "sms", direction: "outbound", status: "sent", body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Empathetic one-liner (instant keyword-based, no API latency) ─────────────
|
|
||||||
|
|
||||||
function getEmpatheticAck(message: string, language: string): string {
|
|
||||||
const hasPain = /pain|hurt|ache|toothache|emergency|urgent|asap|bleeding|broke|crack|fell out|swollen|infection|abscess/i.test(message);
|
|
||||||
|
|
||||||
const painMsgs: Record<string, string> = {
|
|
||||||
English: "Sorry to hear that! We are here to help you.",
|
|
||||||
Spanish: "¡Lo sentimos! Estamos aquí para ayudarle.",
|
|
||||||
Portuguese: "Lamentamos isso! Estamos aqui para ajudá-lo.",
|
|
||||||
Mandarin: "很遗憾听到这个消息!我们在这里帮助您。",
|
|
||||||
Cantonese: "很遺憾聽到這個消息!我們在這裡幫助您。",
|
|
||||||
Arabic: "آسف لسماع ذلك! نحن هنا لمساعدتك.",
|
|
||||||
"Haitian Creole": "Nou regrèt tande sa! Nou la pou ede ou.",
|
|
||||||
};
|
|
||||||
const normalMsgs: Record<string, string> = {
|
|
||||||
English: "No problem!",
|
|
||||||
Spanish: "¡Sin problema!",
|
|
||||||
Portuguese: "Sem problema!",
|
|
||||||
Mandarin: "没问题!",
|
|
||||||
Cantonese: "沒問題!",
|
|
||||||
Arabic: "لا مشكلة!",
|
|
||||||
"Haitian Creole": "Pa gen pwoblèm!",
|
|
||||||
};
|
|
||||||
|
|
||||||
return hasPain
|
|
||||||
? (painMsgs[language] ?? painMsgs["English"]!)
|
|
||||||
: (normalMsgs[language] ?? normalMsgs["English"]!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── "New appointment or reschedule?" prompt (multilingual) ────────────────────
|
|
||||||
|
|
||||||
const NEW_OR_RESCHEDULE_Q: Record<string, string> = {
|
|
||||||
English: "Just to confirm — would you like to make a new appointment, or would you like to reschedule your current appointment?",
|
|
||||||
Spanish: "Solo para confirmar — ¿desea hacer una nueva cita o reprogramar su cita actual?",
|
|
||||||
Portuguese: "Só para confirmar — você gostaria de marcar uma nova consulta ou reagendar sua consulta atual?",
|
|
||||||
Mandarin: "请问您是想预约新的就诊,还是想更改现有预约?",
|
|
||||||
Cantonese: "請問您是想預約新的就診,還是想更改現有預約?",
|
|
||||||
Arabic: "فقط للتأكيد — هل تريد تحديد موعد جديد أم تعديل موعدك الحالي؟",
|
|
||||||
"Haitian Creole": "Jis pou konfime — èske ou ta renmen pran yon nouvo randevou, oswa èske ou ta renmen reprogramè randevou aktyèl ou?",
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Stale-conversation handling (SMS) ─────────────────────────────────────────
|
|
||||||
// Unlike a phone call, a long gap between texts doesn't necessarily mean the
|
|
||||||
// patient walked away — replies are often just slow. So instead of hard
|
|
||||||
// resetting on any idle gap, we only second-guess the stored stage once the
|
|
||||||
// gap crosses STALE_SMS_THRESHOLD_MS, and even then we ask the LLM to look at
|
|
||||||
// the recent history before deciding whether the new text is still answering
|
|
||||||
// the last question or reads like a fresh, unrelated conversation.
|
|
||||||
|
|
||||||
const STALE_SMS_THRESHOLD_MS = 3 * 60 * 60 * 1000; // 3 hours
|
|
||||||
|
|
||||||
const STILL_HERE_MSGS: Record<string, string> = {
|
|
||||||
English: "It's been a while since our last message. How can I help you today?",
|
|
||||||
Spanish: "Ha pasado un tiempo desde nuestro último mensaje. ¿Cómo puedo ayudarle hoy?",
|
|
||||||
Portuguese: "Já faz um tempo desde nossa última mensagem. Como posso ajudá-lo hoje?",
|
|
||||||
Mandarin: "距离我们上次的消息已经有一段时间了。今天我能帮您什么?",
|
|
||||||
Cantonese: "距離我們上次嘅消息已經有一段時間喇。今日我可以點幫您?",
|
|
||||||
Arabic: "لقد مر بعض الوقت منذ آخر رسالة لنا. كيف يمكنني مساعدتك اليوم؟",
|
|
||||||
"Haitian Creole": "Sa gen yon ti tan depi dènye mesaj nou. Kijan mwen ka ede ou jodi a?",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After a long idle gap, ask the LLM whether the patient's new text still
|
|
||||||
* answers what was last asked (recent history + stage in context) or reads
|
|
||||||
* like an unrelated, fresh message. Defaults to "continue" on any failure —
|
|
||||||
* an LLM call error shouldn't be the reason a legitimate reply gets bounced.
|
|
||||||
*/
|
|
||||||
async function isLikelyContinuation(
|
|
||||||
recentHistory: { role: "user" | "assistant"; text: string }[],
|
|
||||||
newMessage: string,
|
|
||||||
stage: ConversationStage,
|
|
||||||
apiKey: string,
|
|
||||||
provider: AiProvider,
|
|
||||||
model?: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
if (recentHistory.length === 0) return true;
|
|
||||||
try {
|
|
||||||
const llm = getLlm(provider, apiKey, model);
|
|
||||||
const transcript = recentHistory
|
|
||||||
.map(m => `${m.role === "assistant" ? "Office" : "Patient"}: ${m.text}`)
|
|
||||||
.join("\n");
|
|
||||||
const res = await llm.invoke([
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content:
|
|
||||||
`You are helping a dental office text-messaging assistant resume a conversation with a patient after a long gap of silence. ` +
|
|
||||||
`Here is the recent conversation (oldest first):\n${transcript}\n\n` +
|
|
||||||
`The assistant is currently waiting on stage "${stage}" of a booking flow. ` +
|
|
||||||
`The patient just sent a new message after the long gap: "${newMessage}"\n\n` +
|
|
||||||
`Does this new message still look like it's answering/continuing that same conversation, or does it read like an unrelated, fresh message (e.g. a new question, a greeting, a different topic)? ` +
|
|
||||||
`Reply with ONLY one word: "continue" or "restart".`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return !/restart/i.test(String(res.content));
|
|
||||||
} catch {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── POST /api/twilio/webhook/sms ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
const CONVO_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
// In-memory conversation state for unknown phone numbers (no patient record yet).
|
|
||||||
// Keyed by normalized "From" number.
|
|
||||||
const unknownPhoneConvos = new Map<string, {
|
|
||||||
userId: number;
|
|
||||||
stage: ConversationStage;
|
|
||||||
language: string;
|
|
||||||
lastActivityAt: Date;
|
|
||||||
pendingApptDate?: { date: Date; dateLabel: string };
|
|
||||||
}>();
|
|
||||||
|
|
||||||
router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const { From, To, Body, MessageSid } = req.body;
|
|
||||||
|
|
||||||
const normalizedFrom = (From || "").replace(/\D/g, "");
|
|
||||||
// Ordered by id so that when duplicate patients share a phone number
|
|
||||||
// (one record per insurance), the earliest-created record is the default.
|
|
||||||
const allPatients = await db.patient.findMany({
|
|
||||||
select: { id: true, phone: true, userId: true, preferredLanguage: true },
|
|
||||||
orderBy: { id: "asc" },
|
|
||||||
});
|
|
||||||
const patient = allPatients.find(
|
|
||||||
(p) => p.phone && p.phone.replace(/\D/g, "") === normalizedFrom
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!patient) {
|
|
||||||
// Unknown number — look up office by the Twilio "To" number
|
|
||||||
const normalizedTo = (To || "").replace(/\D/g, "");
|
|
||||||
const twilioRow = await db.twilioSettings.findFirst({
|
|
||||||
where: { phoneNumber: { contains: normalizedTo.slice(-10) } },
|
|
||||||
select: { userId: true },
|
|
||||||
});
|
|
||||||
if (!twilioRow) {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = twilioRow.userId;
|
|
||||||
const openPhoneReply = await storage.getOpenPhoneReply(userId);
|
|
||||||
if (!openPhoneReply) {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch required context for this office
|
|
||||||
const aiSettings = await storage.getAiSettings(userId);
|
|
||||||
const activeAiU = resolveAiProvider(aiSettings ?? {});
|
|
||||||
if (!activeAiU) {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatTemplates = await storage.getAiChatTemplates(userId);
|
|
||||||
const officeContact = await storage.getOfficeContact(userId);
|
|
||||||
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
|
||||||
const convo = unknownPhoneConvos.get(normalizedFrom);
|
|
||||||
let stage = convo?.stage ?? "initial";
|
|
||||||
const language = convo?.language ?? "English";
|
|
||||||
|
|
||||||
// Reset conversation if idle for more than 5 minutes
|
|
||||||
if (
|
|
||||||
stage !== "initial" && stage !== "done" &&
|
|
||||||
convo?.lastActivityAt &&
|
|
||||||
Date.now() - convo.lastActivityAt.getTime() > CONVO_TIMEOUT_MS
|
|
||||||
) {
|
|
||||||
stage = "initial";
|
|
||||||
unknownPhoneConvos.set(normalizedFrom, { userId, stage: "initial", language, lastActivityAt: new Date() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const replyUnknown = (
|
|
||||||
text: string,
|
|
||||||
nextStage: ConversationStage,
|
|
||||||
pendingApptDate?: { date: Date; dateLabel: string },
|
|
||||||
) => {
|
|
||||||
unknownPhoneConvos.set(normalizedFrom, {
|
|
||||||
userId, stage: nextStage, language,
|
|
||||||
lastActivityAt: new Date(),
|
|
||||||
...(pendingApptDate ? { pendingApptDate } : { pendingApptDate: convo?.pendingApptDate }),
|
|
||||||
});
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(twimlReply(text));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (stage === "initial" || stage === "done") {
|
|
||||||
// MSG 1: AI intro
|
|
||||||
const rawGreeting = chatTemplates.newPatientGreeting ||
|
|
||||||
`Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7.`;
|
|
||||||
const introText = applyOfficeName(rawGreeting, officeName);
|
|
||||||
|
|
||||||
// MSG 2: Empathetic acknowledgment
|
|
||||||
const empatheticText = getEmpatheticAck(Body, language);
|
|
||||||
|
|
||||||
// MSG 3: Detect intent from first message
|
|
||||||
const isReschedule = /reschedule|rescheduler|change.*appoint|modify.*appoint|move.*appoint|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
|
||||||
let msg3Unk: string;
|
|
||||||
let nextStage3Unk: ConversationStage;
|
|
||||||
|
|
||||||
if (isReschedule) {
|
|
||||||
const notFoundMsgs: Record<string, string> = {
|
|
||||||
English: "I couldn't find an appointment associated with your number. Please leave your name and a good callback number and our receptionist will assist you as soon as possible.",
|
|
||||||
Spanish: "No encontré una cita asociada a su número. Por favor deje su nombre y un número de contacto y nuestra recepcionista le atenderá lo antes posible.",
|
|
||||||
Portuguese: "Não encontrei uma consulta associada ao seu número. Por favor deixe seu nome e um número de contato e nossa recepcionista lhe atenderá o mais breve possível.",
|
|
||||||
Mandarin: "我找不到与您号码关联的预约。请留下您的姓名和联系电话,我们的前台将尽快与您联系。",
|
|
||||||
Cantonese: "我找不到與您號碼關聯的預約。請留下您的姓名和聯絡電話,我們的接待員將盡快與您聯絡。",
|
|
||||||
Arabic: "لم أجد موعداً مرتبطاً برقمك. يرجى ترك اسمك ورقم هاتف للتواصل وسيساعدك موظف الاستقبال في أقرب وقت.",
|
|
||||||
"Haitian Creole": "Mwen pa jwenn yon randevou ki asosye ak nimewo ou. Tanpri kite non ou ak yon nimewo pou rele epi resepsyonis nou an pral ede ou pi vit posib.",
|
|
||||||
};
|
|
||||||
msg3Unk = notFoundMsgs[language] ?? notFoundMsgs["English"]!;
|
|
||||||
nextStage3Unk = "collecting_contact_info";
|
|
||||||
} else {
|
|
||||||
const newOrExistingMsgs: Record<string, string> = {
|
|
||||||
English: "Can you please tell me if you are a new or existing patient?",
|
|
||||||
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
|
||||||
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
|
||||||
Mandarin: "请问您是新患者还是现有患者?",
|
|
||||||
Cantonese: "請問您是新病人還是現有病人?",
|
|
||||||
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
|
||||||
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
|
||||||
};
|
|
||||||
msg3Unk = newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!;
|
|
||||||
nextStage3Unk = "asked_new_or_existing";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update store then send all 3 in one TwiML response (guaranteed order)
|
|
||||||
unknownPhoneConvos.set(normalizedFrom, {
|
|
||||||
userId, stage: nextStage3Unk, language, lastActivityAt: new Date(),
|
|
||||||
});
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(twimlMessages(introText, empatheticText, msg3Unk));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Unknown: asked_new_or_reschedule (fallback for in-progress conversations) ──
|
|
||||||
if (stage === "asked_new_or_reschedule") {
|
|
||||||
const isReschedule = /reschedule|rescheduler|change|modify|move|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
|
||||||
if (isReschedule) {
|
|
||||||
const notFoundMsgs: Record<string, string> = {
|
|
||||||
English: "I couldn't find an appointment associated with your number. Please leave your name and a good callback number and our receptionist will assist you as soon as possible.",
|
|
||||||
Spanish: "No encontré una cita asociada a su número. Por favor deje su nombre y un número de contacto y nuestra recepcionista le atenderá lo antes posible.",
|
|
||||||
Portuguese: "Não encontrei uma consulta associada ao seu número. Por favor deixe seu nome e um número de contato e nossa recepcionista lhe atenderá o mais breve possível.",
|
|
||||||
Mandarin: "我找不到与您号码关联的预约。请留下您的姓名和联系电话,我们的前台将尽快与您联系。",
|
|
||||||
Cantonese: "我找不到與您號碼關聯的預約。請留下您的姓名和聯絡電話,我們的接待員將盡快與您聯絡。",
|
|
||||||
Arabic: "لم أجد موعداً مرتبطاً برقمك. يرجى ترك اسمك ورقم هاتف للتواصل وسيساعدك موظف الاستقبال في أقرب وقت.",
|
|
||||||
"Haitian Creole": "Mwen pa jwenn yon randevou ki asosye ak nimewo ou. Tanpri kite non ou ak yon nimewo pou rele epi resepsyonis nou an pral ede ou pi vit posib.",
|
|
||||||
};
|
|
||||||
return replyUnknown(notFoundMsgs[language] ?? notFoundMsgs["English"]!, "collecting_contact_info");
|
|
||||||
}
|
|
||||||
const newOrExistingMsgs: Record<string, string> = {
|
|
||||||
English: "Can you please tell me if you are a new or existing patient?",
|
|
||||||
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
|
||||||
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
|
||||||
Mandarin: "请问您是新患者还是现有患者?",
|
|
||||||
Cantonese: "請問您是新病人還是現有病人?",
|
|
||||||
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
|
||||||
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
|
||||||
};
|
|
||||||
return replyUnknown(newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!, "asked_new_or_existing");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Unknown: asked_appointment_time → parse date, check office hours ──
|
|
||||||
if (stage === "asked_appointment_time") {
|
|
||||||
const parsedDate = await parseDateOnlyFromMessage(Body, activeAiU.key, activeAiU.provider, activeAiU.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 replyUnknown(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
|
|
||||||
}
|
|
||||||
const { date, dateLabel } = parsedDate;
|
|
||||||
const dayCheck = await isOfficeDayOpen(date, 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 replyUnknown(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
|
|
||||||
}
|
|
||||||
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 replyUnknown(
|
|
||||||
askTimeMsgs[language] ?? askTimeMsgs["English"]!,
|
|
||||||
"asked_new_appt_time_for_date",
|
|
||||||
{ date, dateLabel },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Unknown: asked_new_appt_time_for_date → parse time, check hours ───
|
|
||||||
if (stage === "asked_new_appt_time_for_date") {
|
|
||||||
const pendingApptDate = convo?.pendingApptDate;
|
|
||||||
if (!pendingApptDate) {
|
|
||||||
return replyUnknown(
|
|
||||||
"I lost track of the date. What day would you prefer?",
|
|
||||||
"asked_appointment_time",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const startTime = await parseTime(Body, activeAiU.key, activeAiU.provider, activeAiU.model);
|
|
||||||
if (!startTime) {
|
|
||||||
const msgs: Record<string, string> = {
|
|
||||||
English: `I didn't catch the time. What time do you prefer on ${pendingApptDate.dateLabel}? For example: '10am' or '2pm'.`,
|
|
||||||
Spanish: `No entendí la hora. ¿Qué hora prefiere el ${pendingApptDate.dateLabel}? Por ejemplo: '10am' o '2pm'.`,
|
|
||||||
Portuguese: `Não entendi o horário. Que hora você prefere em ${pendingApptDate.dateLabel}? Por exemplo: '10h' ou '14h'.`,
|
|
||||||
Mandarin: `我没听清时间。您在 ${pendingApptDate.dateLabel} 几点?例如:上午10点或下午2点。`,
|
|
||||||
Cantonese: `我沒聽清時間。您在 ${pendingApptDate.dateLabel} 幾點?例如:上午10點或下午2點。`,
|
|
||||||
Arabic: `لم أفهم الوقت. ما الوقت الذي تفضله في ${pendingApptDate.dateLabel}؟ مثلاً: 10 صباحاً أو 2 مساءً.`,
|
|
||||||
"Haitian Creole": `Mwen pa konprann lè a. Ki lè ou prefere nan ${pendingApptDate.dateLabel}? Pa egzanp: 10am oswa 2pm.`,
|
|
||||||
};
|
|
||||||
return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
|
|
||||||
}
|
|
||||||
const withinHours = await isWithinOfficeHours(pendingApptDate.date, startTime, userId);
|
|
||||||
if (!withinHours) {
|
|
||||||
const hoursDisplay = await getOfficeHoursDisplay(pendingApptDate.date, userId);
|
|
||||||
const timeLbl = timeLabel(startTime);
|
|
||||||
const msgs: Record<string, string> = {
|
|
||||||
English: hoursDisplay
|
|
||||||
? `Our office is not available at ${timeLbl} on ${pendingApptDate.dateLabel}. Our hours are ${hoursDisplay}. What other time do you prefer?`
|
|
||||||
: `Our office is not available at ${timeLbl} on ${pendingApptDate.dateLabel}. What other time do you prefer?`,
|
|
||||||
Spanish: hoursDisplay
|
|
||||||
? `Nuestra oficina no está disponible a las ${timeLbl} el ${pendingApptDate.dateLabel}. Nuestro horario es ${hoursDisplay}. ¿Qué otro horario prefiere?`
|
|
||||||
: `Nuestra oficina no está disponible a las ${timeLbl} el ${pendingApptDate.dateLabel}. ¿Qué otro horario prefiere?`,
|
|
||||||
Portuguese: hoursDisplay
|
|
||||||
? `Nosso consultório não está disponível às ${timeLbl} em ${pendingApptDate.dateLabel}. Nosso horário é ${hoursDisplay}. Que outro horário você prefere?`
|
|
||||||
: `Nosso consultório não está disponível às ${timeLbl} em ${pendingApptDate.dateLabel}. Que outro horário você prefere?`,
|
|
||||||
Mandarin: hoursDisplay
|
|
||||||
? `我们诊所在 ${pendingApptDate.dateLabel} ${timeLbl} 不开放。工作时间是 ${hoursDisplay}。您希望改什么时间?`
|
|
||||||
: `我们诊所在 ${pendingApptDate.dateLabel} ${timeLbl} 不开放。您希望改什么时间?`,
|
|
||||||
Cantonese: hoursDisplay
|
|
||||||
? `我們診所在 ${pendingApptDate.dateLabel} ${timeLbl} 不開放。工作時間是 ${hoursDisplay}。您希望改什麼時間?`
|
|
||||||
: `我們診所在 ${pendingApptDate.dateLabel} ${timeLbl} 不開放。您希望改什麼時間?`,
|
|
||||||
Arabic: hoursDisplay
|
|
||||||
? `مكتبنا غير متاح في ${timeLbl} يوم ${pendingApptDate.dateLabel}. ساعات العمل: ${hoursDisplay}. ما وقت آخر تفضله؟`
|
|
||||||
: `مكتبنا غير متاح في ${timeLbl} يوم ${pendingApptDate.dateLabel}. ما وقت آخر تفضله؟`,
|
|
||||||
"Haitian Creole": hoursDisplay
|
|
||||||
? `Biwo nou pa disponib a ${timeLbl} nan ${pendingApptDate.dateLabel}. Orè nou se ${hoursDisplay}. Ki lòt lè ou prefere?`
|
|
||||||
: `Biwo nou pa disponib a ${timeLbl} nan ${pendingApptDate.dateLabel}. Ki lòt lè ou prefere?`,
|
|
||||||
};
|
|
||||||
return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
|
|
||||||
}
|
|
||||||
const apptLabel = `${pendingApptDate.dateLabel} at ${timeLabel(startTime)}`;
|
|
||||||
const confirmMsgs: Record<string, string> = {
|
|
||||||
English: `Thank you! Your preferred appointment is ${apptLabel}. Our receptionist will confirm the details with you shortly.`,
|
|
||||||
Spanish: `¡Gracias! Su cita preferida es el ${apptLabel}. Nuestra recepcionista confirmará los detalles con usted en breve.`,
|
|
||||||
Portuguese: `Obrigado! Sua consulta preferida é em ${apptLabel}. Nossa recepcionista confirmará os detalhes em breve.`,
|
|
||||||
Mandarin: `谢谢!您的预约时间为 ${apptLabel}。我们的前台将很快与您确认详情。`,
|
|
||||||
Cantonese: `多謝!您的預約時間為 ${apptLabel}。我們的接待員將很快與您確認詳情。`,
|
|
||||||
Arabic: `شكراً! موعدك المفضل هو ${apptLabel}. ستتواصل معك موظفة الاستقبال قريباً لتأكيد التفاصيل.`,
|
|
||||||
"Haitian Creole": `Mèsi! Randevou ou a se ${apptLabel}. Resepsyonis nou an pral konfime detay yo ak ou byento.`,
|
|
||||||
};
|
|
||||||
return replyUnknown(confirmMsgs[language] ?? confirmMsgs["English"]!, "done");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-step new patient stages for unknown numbers
|
|
||||||
const unknownNewPatientStages: 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 (unknownNewPatientStages.includes(stage)) {
|
|
||||||
const { reply: aiReply, nextStage } = await runNewPatientStep(
|
|
||||||
Body, stage, language, activeAiU.key, chatTemplates.generalFallback, activeAiU.provider, activeAiU.model
|
|
||||||
);
|
|
||||||
return replyUnknown(aiReply, nextStage);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save inbound message
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: patient.id, channel: "sms", direction: "inbound",
|
|
||||||
status: "delivered", body: Body, twilioSid: MessageSid,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Per-patient handoff toggle must be ON
|
|
||||||
if (!await getHandoff(patient.userId, patient.id)) {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
const aiSettings = await storage.getAiSettings(patient.userId);
|
|
||||||
const activeAi = resolveAiProvider(aiSettings ?? {});
|
|
||||||
if (!activeAi) {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
const language = patient.preferredLanguage || "English";
|
|
||||||
let stage = await getStage(patient.userId, patient.id);
|
|
||||||
const chatTemplates = await storage.getAiChatTemplates(patient.userId);
|
|
||||||
|
|
||||||
// Long idle gap: don't blindly resume (SMS replies are often just slow),
|
|
||||||
// but don't blindly reset either — check whether the new text still reads
|
|
||||||
// like a continuation before deciding.
|
|
||||||
if (stage !== "initial" && stage !== "done") {
|
|
||||||
const convRow = await db.patientConversation.findUnique({
|
|
||||||
where: { patientId: patient.id },
|
|
||||||
select: { updatedAt: true },
|
|
||||||
});
|
|
||||||
if (convRow?.updatedAt && Date.now() - convRow.updatedAt.getTime() > STALE_SMS_THRESHOLD_MS) {
|
|
||||||
const recentComms = await storage.getCommunicationsByPatient(patient.id);
|
|
||||||
const recentHistory = recentComms.slice(-5).map((c: any) => ({
|
|
||||||
role: (c.direction === "outbound" ? "assistant" : "user") as "assistant" | "user",
|
|
||||||
text: c.body || "",
|
|
||||||
}));
|
|
||||||
const continuing = await isLikelyContinuation(
|
|
||||||
recentHistory, Body, stage, activeAi.key, activeAi.provider, activeAi.model
|
|
||||||
);
|
|
||||||
if (!continuing) {
|
|
||||||
clearPendingReschedule(patient.userId, patient.id);
|
|
||||||
await setStage(patient.userId, patient.id, "initial");
|
|
||||||
stage = "initial";
|
|
||||||
const stillHereText = STILL_HERE_MSGS[language] ?? STILL_HERE_MSGS["English"]!;
|
|
||||||
await saveOutbound(patient.id, stillHereText);
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(twimlMessages(stillHereText));
|
|
||||||
}
|
|
||||||
// Looks like a continuation — fall through and resume the stored stage.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const officeContact = await storage.getOfficeContact(patient.userId);
|
|
||||||
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
|
||||||
|
|
||||||
// ── Shared business rules (reminder/reschedule/new-patient graphs, plus the
|
|
||||||
// MassHealth and appointment-booking stages) — the exact same code path the
|
|
||||||
// AI voice call webhook uses, so a patient gets identical guardrails and
|
|
||||||
// outcomes regardless of channel.
|
|
||||||
const routed = await routePatientTurn({
|
|
||||||
patient, stage, message: Body, activeAi, chatTemplates, officeName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (routed.segments.length > 0) {
|
|
||||||
for (const seg of routed.segments) await saveOutbound(patient.id, seg);
|
|
||||||
|
|
||||||
// reminder_initial's self-intro (segments[0]) is queued via REST so it's
|
|
||||||
// guaranteed to arrive before the classified reply that follows it.
|
|
||||||
if (stage === "reminder_initial" && routed.segments.length > 1) {
|
|
||||||
const twilioSettings = await storage.getTwilioSettings(patient.userId);
|
|
||||||
if (twilioSettings) {
|
|
||||||
const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
|
|
||||||
await client.messages.create({ body: routed.segments[0]!, from: twilioSettings.phoneNumber, to: From });
|
|
||||||
}
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(twimlMessages(...routed.segments.slice(1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(twimlMessages(...routed.segments));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Stage: initial / done (patient texts in fresh) ───────────────────
|
|
||||||
if (stage === "initial" || stage === "done") {
|
|
||||||
const openPhoneReply = await storage.getOpenPhoneReply(patient.userId);
|
|
||||||
const afterHoursEnabled = await getAfterHoursHandoff(patient.userId);
|
|
||||||
const outsideHours = await isAfterHours(patient.userId);
|
|
||||||
|
|
||||||
if (openPhoneReply || (afterHoursEnabled && outsideHours)) {
|
|
||||||
// MSG 1: AI self-introduction
|
|
||||||
const rawGreeting = chatTemplates.newPatientGreeting ||
|
|
||||||
`Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7.`;
|
|
||||||
const introText = applyOfficeName(rawGreeting, officeName);
|
|
||||||
|
|
||||||
// MSG 2: Empathetic acknowledgment of the patient's message
|
|
||||||
const empatheticText = getEmpatheticAck(Body, language);
|
|
||||||
|
|
||||||
// MSG 3: Detect reschedule vs new appointment from initial text
|
|
||||||
const isRescheduleIntent = /reschedule|rescheduler|change.*appoint|modify.*appoint|move.*appoint|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
|
||||||
|
|
||||||
let msg3: string;
|
|
||||||
let nextStage3: ConversationStage;
|
|
||||||
|
|
||||||
if (isRescheduleIntent) {
|
|
||||||
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è?`,
|
|
||||||
};
|
|
||||||
msg3 = foundMsgs[language] ?? foundMsgs["English"]!;
|
|
||||||
nextStage3 = "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.",
|
|
||||||
};
|
|
||||||
msg3 = notFoundMsgs[language] ?? notFoundMsgs["English"]!;
|
|
||||||
nextStage3 = "done";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// New appointment (or any other intent) → ask new or existing patient
|
|
||||||
const newOrExistingMsgs: Record<string, string> = {
|
|
||||||
English: "Can you please tell me if you are a new or existing patient?",
|
|
||||||
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
|
||||||
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
|
||||||
Mandarin: "请问您是新患者还是现有患者?",
|
|
||||||
Cantonese: "請問您是新病人還是現有病人?",
|
|
||||||
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
|
||||||
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
|
||||||
};
|
|
||||||
msg3 = newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!;
|
|
||||||
nextStage3 = "asked_new_or_existing";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save all 3 outbound messages and set stage
|
|
||||||
await saveOutbound(patient.id, introText);
|
|
||||||
await saveOutbound(patient.id, empatheticText);
|
|
||||||
await saveOutbound(patient.id, msg3);
|
|
||||||
await setStage(patient.userId, patient.id, nextStage3);
|
|
||||||
|
|
||||||
// Send all 3 in one TwiML response — Twilio guarantees delivery order
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(twimlMessages(introText, empatheticText, msg3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── POST /api/twilio/webhook/voice ────────────────────────────────────────────
|
|
||||||
|
|
||||||
router.post("/webhook/voice", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const { From, CallSid } = req.body;
|
|
||||||
|
|
||||||
const normalizedFrom = (From || "").replace(/\D/g, "");
|
|
||||||
// Ordered by id — earliest-created record wins when duplicates share a phone.
|
|
||||||
const allPatients = await db.patient.findMany({ select: { id: true, phone: true, userId: true }, orderBy: { id: "asc" } });
|
|
||||||
const patient = allPatients.find(
|
|
||||||
(p: { id: number; phone: string | null; userId: number }) => p.phone && p.phone.replace(/\D/g, "") === normalizedFrom
|
|
||||||
);
|
|
||||||
|
|
||||||
let greeting = "Thank you for calling. Please leave a message after the beep and we will get back to you shortly.";
|
|
||||||
if (patient) {
|
|
||||||
const settings = await storage.getTwilioSettings(patient.userId);
|
|
||||||
if (settings?.greetingMessage?.trim()) greeting = settings.greetingMessage.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patient) {
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: patient.id, channel: "voice", direction: "inbound",
|
|
||||||
status: "completed", body: "(Inbound call — voicemail below)", twilioSid: CallSid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordingCallbackUrl = `${getPublicBaseUrl(req)}/api/twilio/webhook/voice-recording`;
|
|
||||||
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(`<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Response>
|
|
||||||
<Say voice="alice">${greeting}</Say>
|
|
||||||
<Record maxLength="120" action="${recordingCallbackUrl}" transcribeCallback="${recordingCallbackUrl}" playBeep="true"/>
|
|
||||||
<Say voice="alice">We did not receive a recording. Goodbye.</Say>
|
|
||||||
</Response>`);
|
|
||||||
} catch (err) {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(`<?xml version="1.0" encoding="UTF-8"?><Response><Say>Thank you for calling. Please try again later.</Say></Response>`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── POST /api/twilio/webhook/voice-recording ──────────────────────────────────
|
|
||||||
|
|
||||||
router.post("/webhook/voice-recording", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const { CallSid, RecordingUrl } = req.body;
|
|
||||||
if (RecordingUrl && CallSid) {
|
|
||||||
const comm = await db.communication.findFirst({ where: { twilioSid: CallSid } });
|
|
||||||
if (comm) {
|
|
||||||
await db.communication.update({
|
|
||||||
where: { id: comm.id },
|
|
||||||
data: { body: `Voicemail: ${RecordingUrl}.mp3` },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
} catch (err) {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
return res.send(empty());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/twilio/webhook/voice-browser
|
// POST /api/twilio/webhook/voice-browser
|
||||||
// Called by Twilio when a browser Device (Voice SDK) places an outbound call.
|
// Called by Twilio when a browser Device (Voice SDK) places an outbound call.
|
||||||
// Looks up the office's Twilio phone number by AccountSid and dials the patient.
|
// Looks up the office's Twilio phone number by AccountSid and dials the patient.
|
||||||
@@ -716,221 +34,4 @@ router.post("/webhook/voice-browser", async (req: Request, res: Response): Promi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── AI phone call (outbound "AI Call" from Patient Connection) ───────────────
|
|
||||||
|
|
||||||
const MAX_AI_CALL_TURNS = 12;
|
|
||||||
|
|
||||||
interface AiCallSession {
|
|
||||||
patientId: number;
|
|
||||||
userId: number;
|
|
||||||
phone: string | null;
|
|
||||||
language: string;
|
|
||||||
officeName: string;
|
|
||||||
chatTemplates: Record<string, string>;
|
|
||||||
history: { role: "assistant" | "user"; text: string }[];
|
|
||||||
turns: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aiCallSessions = new Map<string, AiCallSession>();
|
|
||||||
|
|
||||||
interface VoiceLocale { gatherLang: string; sayVoice: string; }
|
|
||||||
|
|
||||||
/** English/Spanish only for now — Twilio's native voice/recognition coverage
|
|
||||||
* for the app's other supported languages (Mandarin, Cantonese, Arabic,
|
|
||||||
* Haitian Creole) is inconsistent, so those fall back to English on calls. */
|
|
||||||
function voiceLocaleFor(language: string): VoiceLocale {
|
|
||||||
if (language === "Spanish") return { gatherLang: "es-US", sayVoice: "Polly.Lupe" };
|
|
||||||
return { gatherLang: "en-US", sayVoice: "Polly.Joanna" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spoken greeting is plain text, not LLM-generated, so it can't follow the
|
|
||||||
// patient's language on its own — translate it ourselves when the office
|
|
||||||
// hasn't overridden the default wording. Matches voiceLocaleFor's English/
|
|
||||||
// Spanish-only voice coverage above.
|
|
||||||
const GREETING_TRANSLATIONS: Record<string, string> = {
|
|
||||||
Spanish: "Hola {firstName}, soy Lisa, la asistente de inteligencia artificial de {officeName}. ¿En qué puedo ayudarle hoy?",
|
|
||||||
};
|
|
||||||
const RECALL_GREETING_TRANSLATIONS: Record<string, string> = {
|
|
||||||
Spanish: "Hola {firstName}, soy Lisa de {officeName}. Queríamos avisarle que ya le corresponde un examen y limpieza de rutina, que la mayoría de los seguros cubren sin costo. ¿Le gustaría programar una cita?",
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Picks the greeting template to speak: the office's custom wording as-is if
|
|
||||||
* they've edited it (we can't auto-translate free text they wrote), otherwise
|
|
||||||
* the default translated into the patient's language when available. */
|
|
||||||
function resolveGreetingTemplate(rawTemplate: string, defaultTemplate: string, translations: Record<string, string>, language: string): string {
|
|
||||||
if (rawTemplate.trim() !== defaultTemplate) return rawTemplate;
|
|
||||||
return translations[language] ?? defaultTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ENGLISH_LOCALE: VoiceLocale = { gatherLang: "en-US", sayVoice: "Polly.Joanna" };
|
|
||||||
|
|
||||||
function aiVoiceTwiml(sayText: string, gatherActionUrl: string, locale: VoiceLocale): string {
|
|
||||||
// action is always set, so Twilio posts back to it on both speech and
|
|
||||||
// silence timeout — nothing after </Gather> would ever run.
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Response>
|
|
||||||
<Gather input="speech" language="${locale.gatherLang}" speechTimeout="auto" timeout="15" action="${gatherActionUrl}" method="POST">
|
|
||||||
<Say voice="${locale.sayVoice}">${escapeXml(sayText)}</Say>
|
|
||||||
</Gather>
|
|
||||||
</Response>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function aiVoiceHangup(sayText: string, locale: VoiceLocale = ENGLISH_LOCALE): string {
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Response>
|
|
||||||
<Say voice="${locale.sayVoice}">${escapeXml(sayText)}</Say>
|
|
||||||
<Hangup/>
|
|
||||||
</Response>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/twilio/webhook/ai-voice
|
|
||||||
// Entry point + conversation loop for outbound AI-driven calls placed via
|
|
||||||
// POST /api/twilio/make-ai-call. Twilio hits this once when the call connects
|
|
||||||
// (no CallSid session yet) and again after every <Gather> with SpeechResult.
|
|
||||||
// Each spoken turn is routed through the same routePatientTurn() logic the
|
|
||||||
// SMS webhook uses, so calls follow identical rules/guardrails, in the
|
|
||||||
// patient's preferred language (English/Spanish supported for voice).
|
|
||||||
router.post("/webhook/ai-voice", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
res.set("Content-Type", "text/xml");
|
|
||||||
try {
|
|
||||||
const { CallSid, SpeechResult } = req.body as { CallSid?: string; SpeechResult?: string };
|
|
||||||
const patientIdParam = req.query.patientId as string | undefined;
|
|
||||||
const purposeParam = req.query.purpose as string | undefined;
|
|
||||||
if (!CallSid) return res.send(aiVoiceHangup("Sorry, something went wrong. Goodbye."));
|
|
||||||
|
|
||||||
let session = aiCallSessions.get(CallSid);
|
|
||||||
|
|
||||||
// ── First hit for this call: set up session + greet ──────────────────
|
|
||||||
if (!session) {
|
|
||||||
const patientId = parseInt(patientIdParam || "", 10);
|
|
||||||
const patient = !isNaN(patientId)
|
|
||||||
? await db.patient.findUnique({
|
|
||||||
where: { id: patientId },
|
|
||||||
select: { id: true, userId: true, firstName: true, phone: true, preferredLanguage: true },
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
if (!patient) return res.send(aiVoiceHangup("Sorry, we could not find your patient record. Goodbye."));
|
|
||||||
|
|
||||||
const language = patient.preferredLanguage || "English";
|
|
||||||
const locale = voiceLocaleFor(language);
|
|
||||||
const isRecallCall = purposeParam === "recall";
|
|
||||||
|
|
||||||
// Resume an in-progress cross-channel conversation (e.g. mid-reschedule
|
|
||||||
// from a prior text) if one exists; otherwise start a fresh check-in call.
|
|
||||||
// A "recall" call (staff-triggered from Patient Connection, offering an
|
|
||||||
// existing patient a routine exam & cleaning) seeds a different stage
|
|
||||||
// than the default reminder-style check-in.
|
|
||||||
// Either kind of call always speaks its own pitch fresh (see greeting
|
|
||||||
// below), so it must always reset the stage — even if a prior call of
|
|
||||||
// either kind left the patient stuck mid-flow (e.g. "recall_asked_datetime"
|
|
||||||
// or "asked_reschedule_datetime"). Otherwise the reply below gets
|
|
||||||
// interpreted against that stale leftover stage instead of the fresh
|
|
||||||
// pitch that was just spoken.
|
|
||||||
if (isRecallCall) {
|
|
||||||
await startRecallConversation(patient.userId, patient.id);
|
|
||||||
} else {
|
|
||||||
await resetConversation(patient.userId, patient.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const officeContact = await storage.getOfficeContact(patient.userId);
|
|
||||||
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
|
||||||
const callTemplates = await storage.getAiCallTemplates(patient.userId);
|
|
||||||
|
|
||||||
session = {
|
|
||||||
patientId: patient.id, userId: patient.userId, phone: patient.phone,
|
|
||||||
language, officeName, chatTemplates: callTemplates, history: [], turns: 0,
|
|
||||||
};
|
|
||||||
aiCallSessions.set(CallSid, session);
|
|
||||||
|
|
||||||
const rawCallTemplate = (isRecallCall ? callTemplates.recallGreeting?.trim() : callTemplates.greeting?.trim()) ||
|
|
||||||
(isRecallCall ? DEFAULT_RECALL_GREETING : DEFAULT_GREETING);
|
|
||||||
const callTemplate = isRecallCall
|
|
||||||
? resolveGreetingTemplate(rawCallTemplate, DEFAULT_RECALL_GREETING, RECALL_GREETING_TRANSLATIONS, language)
|
|
||||||
: resolveGreetingTemplate(rawCallTemplate, DEFAULT_GREETING, GREETING_TRANSLATIONS, language);
|
|
||||||
const greeting = callTemplate
|
|
||||||
.replace(/\{firstName\}/g, patient.firstName || "there")
|
|
||||||
.replace(/\{officeName\}/g, officeName || "our office");
|
|
||||||
|
|
||||||
session.history.push({ role: "assistant", text: greeting });
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: patient.id, userId: patient.userId, channel: "voice",
|
|
||||||
direction: "outbound", status: "completed", body: greeting, twilioSid: CallSid,
|
|
||||||
});
|
|
||||||
|
|
||||||
const actionUrl = `${getPublicBaseUrl(req)}/api/twilio/webhook/ai-voice?patientId=${patient.id}`;
|
|
||||||
return res.send(aiVoiceTwiml(greeting, actionUrl, locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
const locale = voiceLocaleFor(session.language);
|
|
||||||
|
|
||||||
// ── Caller stayed silent past the speech timeout ──────────────────────
|
|
||||||
// A call is a single continuous conversation, unlike SMS — if it ends
|
|
||||||
// here, mid-flow or not, there's nothing left to resume, so reset the
|
|
||||||
// stage now instead of leaving the patient stuck for their next call/text.
|
|
||||||
if (!SpeechResult?.trim()) {
|
|
||||||
aiCallSessions.delete(CallSid);
|
|
||||||
await setStage(session.userId, session.patientId, "done");
|
|
||||||
return res.send(aiVoiceHangup("I didn't catch a response. Our staff will follow up with you if needed. Goodbye.", locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
session.history.push({ role: "user", text: SpeechResult.trim() });
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: session.patientId, userId: session.userId, channel: "voice",
|
|
||||||
direction: "inbound", status: "completed", body: SpeechResult.trim(), twilioSid: CallSid,
|
|
||||||
});
|
|
||||||
|
|
||||||
session.turns++;
|
|
||||||
|
|
||||||
// ── Caller said something that sounds like goodbye, or we've hit the cap ──
|
|
||||||
if (soundsLikeGoodbye(SpeechResult) || session.turns >= MAX_AI_CALL_TURNS) {
|
|
||||||
aiCallSessions.delete(CallSid);
|
|
||||||
await setStage(session.userId, session.patientId, "done");
|
|
||||||
return res.send(aiVoiceHangup("Thank you for calling, have a great day. Goodbye.", locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
const aiSettings = await storage.getAiSettings(session.userId);
|
|
||||||
const activeAi = resolveAiProvider(aiSettings ?? {});
|
|
||||||
if (!activeAi) {
|
|
||||||
aiCallSessions.delete(CallSid);
|
|
||||||
await setStage(session.userId, session.patientId, "done");
|
|
||||||
return res.send(aiVoiceHangup("Our AI assistant is not available right now. Our staff will follow up with you. Goodbye.", locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
const stage = await getStage(session.userId, session.patientId);
|
|
||||||
const routed = await routePatientTurn({
|
|
||||||
patient: {
|
|
||||||
id: session.patientId, userId: session.userId,
|
|
||||||
phone: session.phone, preferredLanguage: session.language,
|
|
||||||
},
|
|
||||||
stage, message: SpeechResult.trim(), activeAi,
|
|
||||||
chatTemplates: session.chatTemplates, officeName: session.officeName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const reply = routed.segments.join(" ").trim() ||
|
|
||||||
"Sorry, I didn't quite catch that — could you say that again?";
|
|
||||||
|
|
||||||
session.history.push({ role: "assistant", text: reply });
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: session.patientId, userId: session.userId, channel: "voice",
|
|
||||||
direction: "outbound", status: "completed", body: reply, twilioSid: CallSid,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (routed.nextStage === "done" || soundsLikeGoodbye(reply)) {
|
|
||||||
aiCallSessions.delete(CallSid);
|
|
||||||
// routePatientTurn() already persisted nextStage — but if we're hanging
|
|
||||||
// up because the reply sounded like a goodbye while nextStage was still
|
|
||||||
// mid-flow, force it to "done" so the next call/text doesn't resume there.
|
|
||||||
if (routed.nextStage !== "done") {
|
|
||||||
await setStage(session.userId, session.patientId, "done");
|
|
||||||
}
|
|
||||||
return res.send(aiVoiceHangup(reply, locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionUrl = `${getPublicBaseUrl(req)}/api/twilio/webhook/ai-voice?patientId=${session.patientId}`;
|
|
||||||
return res.send(aiVoiceTwiml(reply, actionUrl, locale));
|
|
||||||
} catch (err) {
|
|
||||||
return res.send(aiVoiceHangup("Sorry, something went wrong on our end. Goodbye."));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
|
// NOTE: Twilio is now used only for the in-browser dial pad (Access Token
|
||||||
|
// minting + browser-Device outbound-call TwiML). Everything else — SMS,
|
||||||
|
// inbound/AI voice, staff-triggered outbound calls — moved to Telnyx, see
|
||||||
|
// routes/telnyx.ts and routes/telnyx-webhooks.ts.
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import twilio from "twilio";
|
import twilio from "twilio";
|
||||||
import { storage } from "../storage";
|
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();
|
const router = express.Router();
|
||||||
|
|
||||||
function getTwilioClient(accountSid: string, authToken: string) {
|
|
||||||
return twilio(accountSid, authToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/twilio/settings
|
// GET /api/twilio/settings
|
||||||
router.get("/settings", async (req: Request, res: Response): Promise<any> => {
|
router.get("/settings", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
@@ -27,7 +23,6 @@ router.get("/settings", async (req: Request, res: Response): Promise<any> => {
|
|||||||
accountSid: settings.accountSid,
|
accountSid: settings.accountSid,
|
||||||
authToken: settings.authToken,
|
authToken: settings.authToken,
|
||||||
phoneNumber: settings.phoneNumber,
|
phoneNumber: settings.phoneNumber,
|
||||||
greetingMessage: settings.greetingMessage,
|
|
||||||
twimlAppSid: templates["_twiml_app_sid"] || null,
|
twimlAppSid: templates["_twiml_app_sid"] || null,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -41,7 +36,7 @@ router.put("/settings", async (req: Request, res: Response): Promise<any> => {
|
|||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
const { accountSid, authToken, phoneNumber, greetingMessage, twimlAppSid } = req.body;
|
const { accountSid, authToken, phoneNumber, twimlAppSid } = req.body;
|
||||||
if (!accountSid?.trim() || !authToken?.trim() || !phoneNumber?.trim()) {
|
if (!accountSid?.trim() || !authToken?.trim() || !phoneNumber?.trim()) {
|
||||||
return res.status(400).json({ message: "accountSid, authToken, and phoneNumber are required" });
|
return res.status(400).json({ message: "accountSid, authToken, and phoneNumber are required" });
|
||||||
}
|
}
|
||||||
@@ -50,7 +45,6 @@ router.put("/settings", async (req: Request, res: Response): Promise<any> => {
|
|||||||
accountSid: accountSid.trim(),
|
accountSid: accountSid.trim(),
|
||||||
authToken: authToken.trim(),
|
authToken: authToken.trim(),
|
||||||
phoneNumber: phoneNumber.trim(),
|
phoneNumber: phoneNumber.trim(),
|
||||||
greetingMessage: greetingMessage?.trim() || null,
|
|
||||||
twimlAppSid: twimlAppSid?.trim() || null,
|
twimlAppSid: twimlAppSid?.trim() || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +54,6 @@ router.put("/settings", async (req: Request, res: Response): Promise<any> => {
|
|||||||
accountSid: settings.accountSid,
|
accountSid: settings.accountSid,
|
||||||
authToken: settings.authToken,
|
authToken: settings.authToken,
|
||||||
phoneNumber: settings.phoneNumber,
|
phoneNumber: settings.phoneNumber,
|
||||||
greetingMessage: settings.greetingMessage,
|
|
||||||
twimlAppSid: templates["_twiml_app_sid"] || null,
|
twimlAppSid: templates["_twiml_app_sid"] || null,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -68,461 +61,6 @@ router.put("/settings", async (req: Request, res: Response): Promise<any> => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/twilio/send-sms
|
|
||||||
router.post("/send-sms", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
|
|
||||||
const { to, message, patientId, startFlow } = req.body;
|
|
||||||
if (!to || !message) return res.status(400).json({ message: "to and message 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 client = getTwilioClient(settings.accountSid, settings.authToken);
|
|
||||||
const twilioMsg = await client.messages.create({
|
|
||||||
body: message,
|
|
||||||
from: settings.phoneNumber,
|
|
||||||
to,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (patientId) {
|
|
||||||
const pid = Number(patientId);
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: pid,
|
|
||||||
userId,
|
|
||||||
channel: "sms",
|
|
||||||
direction: "outbound",
|
|
||||||
status: "sent",
|
|
||||||
body: message,
|
|
||||||
twilioSid: twilioMsg.sid,
|
|
||||||
});
|
|
||||||
// Set conversation stage based on which flow was started
|
|
||||||
if (startFlow === "new_patient") {
|
|
||||||
await startNewPatientConversation(userId, pid);
|
|
||||||
} else if (startFlow === "reschedule") {
|
|
||||||
await startRescheduleConversation(userId, pid);
|
|
||||||
} else if (startFlow === "no_follow_up") {
|
|
||||||
// AI follow-up disabled — leave existing stage untouched
|
|
||||||
} else {
|
|
||||||
// "reminder" or unspecified → start reminder AI flow
|
|
||||||
await resetConversation(userId, pid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({ sid: twilioMsg.sid, status: twilioMsg.status });
|
|
||||||
} catch (err: any) {
|
|
||||||
return res.status(500).json({ error: err.message || "Failed to send SMS" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
|
|
||||||
const { date, staffIds, aiFollowUp = true } = req.body as {
|
|
||||||
date: string;
|
|
||||||
staffIds: number[];
|
|
||||||
aiFollowUp?: boolean;
|
|
||||||
};
|
|
||||||
if (!date || !Array.isArray(staffIds) || staffIds.length === 0) {
|
|
||||||
return res.status(400).json({ message: "date and staffIds 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." });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve office name, address, phone, and reminder SMS template
|
|
||||||
const officeContact = await storage.getOfficeContact(userId);
|
|
||||||
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
|
||||||
const officeAddress = [
|
|
||||||
(officeContact as any)?.streetAddress?.trim(),
|
|
||||||
(officeContact as any)?.city?.trim(),
|
|
||||||
(officeContact as any)?.state?.trim(),
|
|
||||||
(officeContact as any)?.zipCode?.trim(),
|
|
||||||
].filter(Boolean).join(", ");
|
|
||||||
const officePhone = (officeContact as any)?.phoneNumber?.trim() || "";
|
|
||||||
const chatTemplates = await storage.getAiChatTemplates(userId);
|
|
||||||
|
|
||||||
const DEFAULT_REMINDER_SMS =
|
|
||||||
"Hi {firstName}, this is a reminder from {officeName}. You have an appointment on {appointmentDate} at {appointmentTime}. Please come to our office at {officeAddress}. Please reply YES to confirm or NO to reschedule. Thank you!";
|
|
||||||
const templateBody = chatTemplates.reminderSms?.trim() || DEFAULT_REMINDER_SMS;
|
|
||||||
|
|
||||||
// Fetch appointments for the selected staff columns on the given date
|
|
||||||
const dayStart = new Date(date);
|
|
||||||
dayStart.setUTCHours(0, 0, 0, 0);
|
|
||||||
const dayEnd = new Date(date);
|
|
||||||
dayEnd.setUTCHours(23, 59, 59, 999);
|
|
||||||
|
|
||||||
const appointments = await db.appointment.findMany({
|
|
||||||
where: {
|
|
||||||
staffId: { in: staffIds },
|
|
||||||
date: { gte: dayStart, lte: dayEnd },
|
|
||||||
status: { not: "cancelled" },
|
|
||||||
patient: { userId },
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
patient: { select: { id: true, firstName: true, phone: true } },
|
|
||||||
},
|
|
||||||
orderBy: { startTime: "asc" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Format a date string as "May 9, 2026"
|
|
||||||
const months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
||||||
const formatApptDate = (d: Date | string) => {
|
|
||||||
const dt = new Date(d);
|
|
||||||
return `${months[dt.getUTCMonth()]} ${dt.getUTCDate()}, ${dt.getUTCFullYear()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const client = getTwilioClient(settings.accountSid, settings.authToken);
|
|
||||||
let sent = 0;
|
|
||||||
let skipped = 0;
|
|
||||||
|
|
||||||
// Deduplicate by patientId — send at most one reminder per patient per run
|
|
||||||
const seen = new Set<number>();
|
|
||||||
|
|
||||||
for (const appt of appointments) {
|
|
||||||
const patient = appt.patient;
|
|
||||||
if (!patient?.phone || seen.has(patient.id)) { skipped++; continue; }
|
|
||||||
seen.add(patient.id);
|
|
||||||
|
|
||||||
const apptDate = formatApptDate(appt.date);
|
|
||||||
const apptTime = typeof appt.startTime === "string"
|
|
||||||
? appt.startTime.substring(0, 5)
|
|
||||||
: String(appt.startTime);
|
|
||||||
|
|
||||||
const message = templateBody
|
|
||||||
.replace(/\{firstName\}/g, patient.firstName ?? "")
|
|
||||||
.replace(/\{officeName\}/g, officeName)
|
|
||||||
.replace(/\{officeAddress\}/g, officeAddress)
|
|
||||||
.replace(/\{officePhone\}/g, officePhone)
|
|
||||||
.replace(/\{twilioPhone\}/g, settings.phoneNumber)
|
|
||||||
.replace(/\{appointmentDate\}/g, apptDate)
|
|
||||||
.replace(/\{appointmentTime\}/g, apptTime)
|
|
||||||
.replace(/\{date\}/g, apptDate)
|
|
||||||
.replace(/\{time\}/g, apptTime);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const twilioMsg = await client.messages.create({
|
|
||||||
body: message,
|
|
||||||
from: settings.phoneNumber,
|
|
||||||
to: patient.phone,
|
|
||||||
});
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: patient.id,
|
|
||||||
userId,
|
|
||||||
channel: "sms",
|
|
||||||
direction: "outbound",
|
|
||||||
status: "sent",
|
|
||||||
body: message,
|
|
||||||
twilioSid: twilioMsg.sid,
|
|
||||||
});
|
|
||||||
if (aiFollowUp) {
|
|
||||||
await resetConversation(userId, patient.id);
|
|
||||||
}
|
|
||||||
sent++;
|
|
||||||
} catch {
|
|
||||||
skipped++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({ sent, skipped });
|
|
||||||
} catch (err: any) {
|
|
||||||
return res.status(500).json({ error: err.message || "Failed to send reminders" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/twilio/send-reschedule-batch
|
|
||||||
router.post("/send-reschedule-batch", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
|
|
||||||
const { date, staffIds, aiFollowUp = true } = req.body as { date: string; staffIds: number[]; aiFollowUp?: boolean };
|
|
||||||
if (!date || !Array.isArray(staffIds) || staffIds.length === 0) {
|
|
||||||
return res.status(400).json({ message: "date and staffIds 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." });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the "Reschedule by office" template from the SMS template list
|
|
||||||
const templateList = await storage.getSmsTemplateList(userId);
|
|
||||||
const rescheduleTemplate = templateList.find((t) =>
|
|
||||||
t.name.toLowerCase().includes("reschedule") && t.name.toLowerCase().includes("office")
|
|
||||||
) || templateList.find((t) => t.name.toLowerCase().includes("reschedule"));
|
|
||||||
|
|
||||||
const DEFAULT_RESCHEDULE_SMS =
|
|
||||||
"Hi {firstName}, this is {officeName}. We need to reschedule your appointment. Please reply to let us know your availability. Thank you!";
|
|
||||||
const templateBody = rescheduleTemplate?.body?.trim() || DEFAULT_RESCHEDULE_SMS;
|
|
||||||
|
|
||||||
const officeContact = await storage.getOfficeContact(userId);
|
|
||||||
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
|
||||||
const officeAddress = [
|
|
||||||
(officeContact as any)?.streetAddress?.trim(),
|
|
||||||
(officeContact as any)?.city?.trim(),
|
|
||||||
(officeContact as any)?.state?.trim(),
|
|
||||||
(officeContact as any)?.zipCode?.trim(),
|
|
||||||
].filter(Boolean).join(", ");
|
|
||||||
const officePhone = (officeContact as any)?.phoneNumber?.trim() || "";
|
|
||||||
|
|
||||||
const dayStart = new Date(date);
|
|
||||||
dayStart.setUTCHours(0, 0, 0, 0);
|
|
||||||
const dayEnd = new Date(date);
|
|
||||||
dayEnd.setUTCHours(23, 59, 59, 999);
|
|
||||||
|
|
||||||
const appointments = await db.appointment.findMany({
|
|
||||||
where: {
|
|
||||||
staffId: { in: staffIds },
|
|
||||||
date: { gte: dayStart, lte: dayEnd },
|
|
||||||
status: { not: "cancelled" },
|
|
||||||
patient: { userId },
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
patient: { select: { id: true, firstName: true, phone: true } },
|
|
||||||
},
|
|
||||||
orderBy: { startTime: "asc" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
||||||
const formatApptDate = (d: Date | string) => {
|
|
||||||
const dt = new Date(d);
|
|
||||||
return `${months[dt.getUTCMonth()]} ${dt.getUTCDate()}, ${dt.getUTCFullYear()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const client = getTwilioClient(settings.accountSid, settings.authToken);
|
|
||||||
let sent = 0;
|
|
||||||
let skipped = 0;
|
|
||||||
const seen = new Set<number>();
|
|
||||||
|
|
||||||
for (const appt of appointments) {
|
|
||||||
const patient = appt.patient;
|
|
||||||
if (!patient?.phone || seen.has(patient.id)) { skipped++; continue; }
|
|
||||||
seen.add(patient.id);
|
|
||||||
|
|
||||||
const apptDate = formatApptDate(appt.date);
|
|
||||||
const apptTime = typeof appt.startTime === "string"
|
|
||||||
? appt.startTime.substring(0, 5)
|
|
||||||
: String(appt.startTime);
|
|
||||||
|
|
||||||
const message = templateBody
|
|
||||||
.replace(/\{firstName\}/g, patient.firstName ?? "")
|
|
||||||
.replace(/\{officeName\}/g, officeName)
|
|
||||||
.replace(/\{officeAddress\}/g, officeAddress)
|
|
||||||
.replace(/\{officePhone\}/g, officePhone)
|
|
||||||
.replace(/\{twilioPhone\}/g, settings.phoneNumber)
|
|
||||||
.replace(/\{appointmentDate\}/g, apptDate)
|
|
||||||
.replace(/\{appointmentTime\}/g, apptTime)
|
|
||||||
.replace(/\{date\}/g, apptDate)
|
|
||||||
.replace(/\{time\}/g, apptTime);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const twilioMsg = await client.messages.create({
|
|
||||||
body: message,
|
|
||||||
from: settings.phoneNumber,
|
|
||||||
to: patient.phone,
|
|
||||||
});
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: patient.id,
|
|
||||||
userId,
|
|
||||||
channel: "sms",
|
|
||||||
direction: "outbound",
|
|
||||||
status: "sent",
|
|
||||||
body: message,
|
|
||||||
twilioSid: twilioMsg.sid,
|
|
||||||
});
|
|
||||||
if (aiFollowUp) {
|
|
||||||
await startRescheduleConversation(userId, patient.id);
|
|
||||||
}
|
|
||||||
sent++;
|
|
||||||
} catch {
|
|
||||||
skipped++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({ sent, skipped });
|
|
||||||
} catch (err: any) {
|
|
||||||
return res.status(500).json({ error: err.message || "Failed to send reschedule messages" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/twilio/sms-template-list
|
|
||||||
router.get("/sms-template-list", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
const list = await storage.getSmsTemplateList(userId);
|
|
||||||
return res.status(200).json(list);
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to fetch SMS templates", details: String(err) });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// PUT /api/twilio/sms-template-list
|
|
||||||
router.put("/sms-template-list", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
const templates = req.body;
|
|
||||||
if (!Array.isArray(templates)) return res.status(400).json({ message: "Expected an array of templates" });
|
|
||||||
await storage.saveSmsTemplateList(userId, templates);
|
|
||||||
return res.status(200).json({ ok: true });
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to save SMS templates", details: String(err) });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/twilio/templates
|
|
||||||
router.get("/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.getTemplates(userId);
|
|
||||||
return res.status(200).json(templates);
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to fetch templates", details: String(err) });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// PUT /api/twilio/templates/:key
|
|
||||||
router.put("/templates/:key", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
const { key } = req.params;
|
|
||||||
const { body } = req.body;
|
|
||||||
if (!key || !body?.trim()) return res.status(400).json({ message: "key and body are required" });
|
|
||||||
await storage.saveTemplate(userId, key, body.trim());
|
|
||||||
return res.status(200).json({ key, body: body.trim() });
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to save template", details: String(err) });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/twilio/after-hours-handoff
|
|
||||||
router.get("/after-hours-handoff", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
return res.status(200).json({ enabled: await getAfterHoursHandoff(userId) });
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to get after-hours handoff state" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// PUT /api/twilio/after-hours-handoff
|
|
||||||
router.put("/after-hours-handoff", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
const { enabled } = req.body;
|
|
||||||
if (typeof enabled !== "boolean") return res.status(400).json({ message: "enabled must be a boolean" });
|
|
||||||
await setAfterHoursHandoff(userId, enabled);
|
|
||||||
return res.status(200).json({ enabled });
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to set after-hours handoff state" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/twilio/ai-handoff/:patientId
|
|
||||||
router.get("/ai-handoff/:patientId", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
const patientId = parseInt(req.params.patientId);
|
|
||||||
if (isNaN(patientId)) return res.status(400).json({ message: "Invalid patientId" });
|
|
||||||
return res.status(200).json({ enabled: await getHandoff(userId, patientId) });
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to get AI handoff state" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// PUT /api/twilio/ai-handoff/:patientId
|
|
||||||
router.put("/ai-handoff/:patientId", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
const patientId = parseInt(req.params.patientId);
|
|
||||||
if (isNaN(patientId)) return res.status(400).json({ message: "Invalid patientId" });
|
|
||||||
const { enabled } = req.body;
|
|
||||||
if (typeof enabled !== "boolean") return res.status(400).json({ message: "enabled must be a boolean" });
|
|
||||||
await setHandoff(userId, patientId, enabled);
|
|
||||||
return res.status(200).json({ enabled });
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to set AI handoff state" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/twilio/recent-communications
|
|
||||||
router.get("/recent-communications", async (req: Request, res: Response): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
|
|
||||||
const limit = Math.min(50, parseInt(req.query.limit as string) || 20);
|
|
||||||
const communications = await storage.getRecentCommunicationsByUser(userId, limit);
|
|
||||||
return res.status(200).json(communications);
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: "Failed to fetch recent communications", details: String(err) });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/twilio/voice-token
|
// POST /api/twilio/voice-token
|
||||||
// Returns a short-lived Twilio Access Token so the browser Voice SDK can place calls.
|
// Returns a short-lived Twilio Access Token so the browser Voice SDK can place calls.
|
||||||
router.post("/voice-token", async (req: Request, res: Response): Promise<any> => {
|
router.post("/voice-token", async (req: Request, res: Response): Promise<any> => {
|
||||||
@@ -559,45 +97,4 @@ router.post("/voice-token", async (req: Request, res: Response): Promise<any> =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/twilio/make-call
|
|
||||||
router.post("/make-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, message, patientId } = req.body;
|
|
||||||
if (!to) return res.status(400).json({ message: "to is 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 twimlMessage = message || "Hello, this is a call from your dental office.";
|
|
||||||
const twiml = `<Response><Say>${twimlMessage}</Say></Response>`;
|
|
||||||
|
|
||||||
const client = getTwilioClient(settings.accountSid, settings.authToken);
|
|
||||||
const call = await client.calls.create({
|
|
||||||
twiml,
|
|
||||||
from: settings.phoneNumber,
|
|
||||||
to,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (patientId) {
|
|
||||||
await storage.createCommunication({
|
|
||||||
patientId: Number(patientId),
|
|
||||||
userId,
|
|
||||||
channel: "voice",
|
|
||||||
direction: "outbound",
|
|
||||||
status: "queued",
|
|
||||||
twilioSid: call.sid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({ sid: call.sid, status: call.status });
|
|
||||||
} catch (err: any) {
|
|
||||||
return res.status(500).json({ error: err.message || "Failed to make call" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { patientDocumentsStorage } from './patientDocuments-storage';
|
|||||||
import * as exportPaymentsReportsStorage from "./export-payments-reports-storage";
|
import * as exportPaymentsReportsStorage from "./export-payments-reports-storage";
|
||||||
import { cronJobLogStorage } from "./cron-job-log-storage";
|
import { cronJobLogStorage } from "./cron-job-log-storage";
|
||||||
import { twilioStorage } from "./twilio-storage";
|
import { twilioStorage } from "./twilio-storage";
|
||||||
|
import { telnyxStorage } from "./telnyx-storage";
|
||||||
import { aiSettingsStorage } from "./ai-settings-storage";
|
import { aiSettingsStorage } from "./ai-settings-storage";
|
||||||
import { officeHoursStorage } from "./office-hours-storage";
|
import { officeHoursStorage } from "./office-hours-storage";
|
||||||
import { officeContactStorage } from "./office-contact-storage";
|
import { officeContactStorage } from "./office-contact-storage";
|
||||||
@@ -47,6 +48,7 @@ export const storage = {
|
|||||||
...exportPaymentsReportsStorage,
|
...exportPaymentsReportsStorage,
|
||||||
...cronJobLogStorage,
|
...cronJobLogStorage,
|
||||||
...twilioStorage,
|
...twilioStorage,
|
||||||
|
...telnyxStorage,
|
||||||
...aiSettingsStorage,
|
...aiSettingsStorage,
|
||||||
...officeHoursStorage,
|
...officeHoursStorage,
|
||||||
...officeContactStorage,
|
...officeContactStorage,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { prisma as db } from "@repo/db/client";
|
||||||
|
|
||||||
|
export type TelnyxSettingsData = {
|
||||||
|
apiKey: string;
|
||||||
|
accountSid?: string | null;
|
||||||
|
phoneNumber: string;
|
||||||
|
texmlApplicationId?: string | null;
|
||||||
|
messagingProfileId?: string | null;
|
||||||
|
webhookPublicKey?: string | null;
|
||||||
|
greetingMessage?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const telnyxStorage = {
|
||||||
|
async getTelnyxSettings(userId: number) {
|
||||||
|
return db.telnyxSettings.findUnique({ where: { userId } });
|
||||||
|
},
|
||||||
|
|
||||||
|
async upsertTelnyxSettings(userId: number, data: TelnyxSettingsData) {
|
||||||
|
return db.telnyxSettings.upsert({
|
||||||
|
where: { userId },
|
||||||
|
update: data,
|
||||||
|
create: { userId, ...data },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -24,6 +24,9 @@ export type CommunicationCreateData = {
|
|||||||
body?: string;
|
body?: string;
|
||||||
callDuration?: number;
|
callDuration?: number;
|
||||||
twilioSid?: string;
|
twilioSid?: string;
|
||||||
|
// Which platform produced twilioSid — defaults to "twilio" (dial-pad calls) when
|
||||||
|
// omitted; Telnyx call sites (everything else) pass "telnyx" explicitly.
|
||||||
|
provider?: "twilio" | "telnyx";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const twilioStorage = {
|
export const twilioStorage = {
|
||||||
|
|||||||
@@ -7,26 +7,27 @@ import type { Request } from "express";
|
|||||||
* and Cloudflare/Let's Encrypt terminate TLS in front of it either way.
|
* 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
|
* 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
|
* a Twilio/Telnyx webhook handler). For anything triggered from the staff browser —
|
||||||
* reaches the backend over the LAN-only local-* hostname — use getTwilioPublicBaseUrl()
|
* which reaches the backend over the LAN-only local-* hostname — use
|
||||||
* instead, since Twilio can never reach that LAN hostname to fetch a callback URL.
|
* getPublicTunnelBaseUrl() instead, since neither provider can reach that LAN
|
||||||
|
* hostname to fetch a callback URL.
|
||||||
*/
|
*/
|
||||||
export function getPublicBaseUrl(req: Request): string {
|
export function getPublicBaseUrl(req: Request): string {
|
||||||
return `https://${req.get("host")}`;
|
return `https://${req.get("host")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixed public origin Twilio must use to call back into this office's server
|
* Fixed public origin Twilio/Telnyx must use to call back into this office's server
|
||||||
* (e.g. for outbound-call TwiML webhooks). Comes from CLOUDFLARE_HOST, the
|
* (e.g. for outbound-call webhooks). Comes from CLOUDFLARE_HOST, the per-office env
|
||||||
* per-office env var already reserved for this — not derived from the
|
* var already reserved for this — not derived from the triggering request, since
|
||||||
* triggering request, since that request may have arrived over the LAN-only
|
* that request may have arrived over the LAN-only hostname, which neither provider
|
||||||
* hostname, which Twilio cannot reach.
|
* can reach.
|
||||||
*/
|
*/
|
||||||
export function getTwilioPublicBaseUrl(): string {
|
export function getPublicTunnelBaseUrl(): string {
|
||||||
const host = process.env.CLOUDFLARE_HOST?.trim();
|
const host = process.env.CLOUDFLARE_HOST?.trim();
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new Error(
|
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."
|
"CLOUDFLARE_HOST is not set — required to build a provider-reachable callback URL. Set it in .env to this office's public tunnel hostname."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return `https://${host}`;
|
return `https://${host}`;
|
||||||
|
|||||||
@@ -305,6 +305,11 @@ export function Sidebar() {
|
|||||||
path: "/settings/twilio",
|
path: "/settings/twilio",
|
||||||
icon: <Phone className="h-4 w-4 text-gray-400" />,
|
icon: <Phone className="h-4 w-4 text-gray-400" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Telnyx Settings",
|
||||||
|
path: "/settings/telnyx",
|
||||||
|
icon: <Phone className="h-4 w-4 text-gray-400" />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "AI API Setting",
|
name: "AI API Setting",
|
||||||
path: "/settings/ai",
|
path: "/settings/ai",
|
||||||
|
|||||||
@@ -269,9 +269,9 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea
|
|||||||
const [pendingStartFlow, setPendingStartFlow] = useState<"new_patient" | "reschedule" | null>(null);
|
const [pendingStartFlow, setPendingStartFlow] = useState<"new_patient" | "reschedule" | null>(null);
|
||||||
|
|
||||||
useQuery<{ enabled: boolean }>({
|
useQuery<{ enabled: boolean }>({
|
||||||
queryKey: ["/api/twilio/ai-handoff", patient.id],
|
queryKey: ["/api/telnyx/ai-handoff", patient.id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiRequest("GET", `/api/twilio/ai-handoff/${patient.id}`);
|
const res = await apiRequest("GET", `/api/telnyx/ai-handoff/${patient.id}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
onSuccess: (data: { enabled: boolean }) => setHandOffToAI(data.enabled),
|
onSuccess: (data: { enabled: boolean }) => setHandOffToAI(data.enabled),
|
||||||
@@ -289,7 +289,7 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea
|
|||||||
|
|
||||||
const handoffMutation = useMutation({
|
const handoffMutation = useMutation({
|
||||||
mutationFn: async (enabled: boolean) =>
|
mutationFn: async (enabled: boolean) =>
|
||||||
apiRequest("PUT", `/api/twilio/ai-handoff/${patient.id}`, { enabled }),
|
apiRequest("PUT", `/api/telnyx/ai-handoff/${patient.id}`, { enabled }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleHandoffToggle = (enabled: boolean) => {
|
const handleHandoffToggle = (enabled: boolean) => {
|
||||||
@@ -318,7 +318,7 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea
|
|||||||
|
|
||||||
const sendMessageMutation = useMutation({
|
const sendMessageMutation = useMutation({
|
||||||
mutationFn: async (message: string) =>
|
mutationFn: async (message: string) =>
|
||||||
apiRequest("POST", "/api/twilio/send-sms", {
|
apiRequest("POST", "/api/telnyx/send-sms", {
|
||||||
to: patient.phone,
|
to: patient.phone,
|
||||||
message,
|
message,
|
||||||
patientId: patient.id,
|
patientId: patient.id,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export function SmsTemplateDialog({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data: savedTemplates = {} } = useQuery<Record<string, string>>({
|
const { data: savedTemplates = {} } = useQuery<Record<string, string>>({
|
||||||
queryKey: ["/api/twilio/templates"],
|
queryKey: ["/api/telnyx/templates"],
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export function SmsTemplateDialog({
|
|||||||
|
|
||||||
const sendMutation = useMutation({
|
const sendMutation = useMutation({
|
||||||
mutationFn: async (message: string) => {
|
mutationFn: async (message: string) => {
|
||||||
return apiRequest("POST", "/api/twilio/send-sms", {
|
return apiRequest("POST", "/api/telnyx/send-sms", {
|
||||||
to: patient!.phone,
|
to: patient!.phone,
|
||||||
message,
|
message,
|
||||||
patientId: patient!.id,
|
patientId: patient!.id,
|
||||||
@@ -112,16 +112,16 @@ export function SmsTemplateDialog({
|
|||||||
setMessageText("");
|
setMessageText("");
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: any) => {
|
||||||
toast({ title: "Failed to Send SMS", description: err.message || "Please check your Twilio configuration.", variant: "destructive" });
|
toast({ title: "Failed to Send SMS", description: err.message || "Please check your Telnyx configuration.", variant: "destructive" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: async ({ key, body }: { key: string; body: string }) => {
|
mutationFn: async ({ key, body }: { key: string; body: string }) => {
|
||||||
return apiRequest("PUT", `/api/twilio/templates/${key}`, { body });
|
return apiRequest("PUT", `/api/telnyx/templates/${key}`, { body });
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/twilio/templates"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/telnyx/templates"] });
|
||||||
toast({ title: "Template Updated", description: "This template will be used going forward." });
|
toast({ title: "Template Updated", description: "This template will be used going forward." });
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: any) => {
|
||||||
|
|||||||
@@ -1107,9 +1107,9 @@ export function AiChatSettingsCard() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: smsTemplateListData, isLoading: smsLoading } = useQuery<SmsTemplate[]>({
|
const { data: smsTemplateListData, isLoading: smsLoading } = useQuery<SmsTemplate[]>({
|
||||||
queryKey: ["/api/twilio/sms-template-list"],
|
queryKey: ["/api/telnyx/sms-template-list"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiRequest("GET", "/api/twilio/sms-template-list");
|
const res = await apiRequest("GET", "/api/telnyx/sms-template-list");
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
@@ -1200,7 +1200,7 @@ export function AiChatSettingsCard() {
|
|||||||
|
|
||||||
const saveSmsListMutation = useMutation({
|
const saveSmsListMutation = useMutation({
|
||||||
mutationFn: async (list: SmsTemplate[]) => {
|
mutationFn: async (list: SmsTemplate[]) => {
|
||||||
const res = await apiRequest("PUT", "/api/twilio/sms-template-list", list);
|
const res = await apiRequest("PUT", "/api/telnyx/sms-template-list", list);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => null);
|
const err = await res.json().catch(() => null);
|
||||||
throw new Error(err?.message || "Failed to save SMS templates");
|
throw new Error(err?.message || "Failed to save SMS templates");
|
||||||
@@ -1208,7 +1208,7 @@ export function AiChatSettingsCard() {
|
|||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/twilio/sms-template-list"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/telnyx/sms-template-list"] });
|
||||||
toast({ title: "SMS template saved" });
|
toast({ title: "SMS template saved" });
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: any) => {
|
||||||
@@ -1382,7 +1382,7 @@ export function AiChatSettingsCard() {
|
|||||||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{officeName}"}</code>{" "}
|
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{officeName}"}</code>{" "}
|
||||||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{officeAddress}"}</code>{" "}
|
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{officeAddress}"}</code>{" "}
|
||||||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono" title="Office call-in phone number">{"{officePhone}"}</code>{" "}
|
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono" title="Office call-in phone number">{"{officePhone}"}</code>{" "}
|
||||||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono" title="Twilio SMS number patients text back to">{"{twilioPhone}"}</code>{" "}
|
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono" title="SMS number patients text back to">{"{twilioPhone}"}</code>{" "}
|
||||||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{appointmentDate}"}</code>{" "}
|
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{appointmentDate}"}</code>{" "}
|
||||||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{appointmentTime}"}</code>
|
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{appointmentTime}"}</code>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Eye, EyeOff, CheckCircle } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
|
|
||||||
|
type TelnyxSettings = {
|
||||||
|
id?: number;
|
||||||
|
apiKey: string;
|
||||||
|
accountSid?: string | null;
|
||||||
|
phoneNumber: string;
|
||||||
|
texmlApplicationId?: string | null;
|
||||||
|
messagingProfileId?: string | null;
|
||||||
|
webhookPublicKey?: string | null;
|
||||||
|
greetingMessage?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TelnyxSettingsCard() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [accountSid, setAccountSid] = useState("");
|
||||||
|
const [phoneNumber, setPhoneNumber] = useState("");
|
||||||
|
const [texmlApplicationId, setTexmlApplicationId] = useState("");
|
||||||
|
const [messagingProfileId, setMessagingProfileId] = useState("");
|
||||||
|
const [webhookPublicKey, setWebhookPublicKey] = useState("");
|
||||||
|
const [greetingMessage, setGreetingMessage] = useState("");
|
||||||
|
const [showApiKey, setShowApiKey] = useState(false);
|
||||||
|
|
||||||
|
const { data: settings, isLoading } = useQuery<TelnyxSettings | null>({
|
||||||
|
queryKey: ["/api/telnyx/settings"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/telnyx/settings");
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings) {
|
||||||
|
setApiKey(settings.apiKey ?? "");
|
||||||
|
setAccountSid(settings.accountSid ?? "");
|
||||||
|
setPhoneNumber(settings.phoneNumber ?? "");
|
||||||
|
setTexmlApplicationId(settings.texmlApplicationId ?? "");
|
||||||
|
setMessagingProfileId(settings.messagingProfileId ?? "");
|
||||||
|
setWebhookPublicKey(settings.webhookPublicKey ?? "");
|
||||||
|
setGreetingMessage(settings.greetingMessage ?? "");
|
||||||
|
}
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
const saveMutation = useMutation({
|
||||||
|
mutationFn: async (data: TelnyxSettings) => {
|
||||||
|
const res = await apiRequest("PUT", "/api/telnyx/settings", data);
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => null);
|
||||||
|
throw new Error(err?.message || "Failed to save Telnyx settings");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/telnyx/settings"] });
|
||||||
|
toast({ title: "Telnyx Settings Saved", description: "Your Telnyx credentials have been saved." });
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
toast({ title: "Error", description: err?.message || "Failed to save settings", variant: "destructive" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!apiKey.trim() || !phoneNumber.trim()) return;
|
||||||
|
saveMutation.mutate({
|
||||||
|
apiKey: apiKey.trim(),
|
||||||
|
accountSid: accountSid.trim() || null,
|
||||||
|
phoneNumber: phoneNumber.trim(),
|
||||||
|
texmlApplicationId: texmlApplicationId.trim() || null,
|
||||||
|
messagingProfileId: messagingProfileId.trim() || null,
|
||||||
|
webhookPublicKey: webhookPublicKey.trim() || null,
|
||||||
|
greetingMessage: greetingMessage.trim() || null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isConfigured = !!(settings?.apiKey && settings?.phoneNumber);
|
||||||
|
const webhookOrigin = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="space-y-4 py-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-lg font-semibold">Telnyx Settings</h3>
|
||||||
|
{isConfigured && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-green-600 font-medium">
|
||||||
|
<CheckCircle className="h-3.5 w-3.5" /> Configured
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Telnyx handles SMS, voicemail, and AI phone calls — everything except the in-browser dial pad
|
||||||
|
(see Twilio Settings above). Find these in your{" "}
|
||||||
|
<span className="font-medium">Telnyx Mission Control Portal</span>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-sm text-gray-400">Loading...</p>
|
||||||
|
) : (
|
||||||
|
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">API Key</label>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<input
|
||||||
|
type={showApiKey ? "text" : "password"}
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
className="p-2 border rounded w-full pr-10 font-mono text-sm"
|
||||||
|
placeholder="KEYxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowApiKey((v) => !v)}
|
||||||
|
className="absolute inset-y-0 right-2 flex items-center text-gray-500 hover:text-gray-700"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">Account SID <span className="text-gray-400 font-normal">(for placing calls)</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={accountSid}
|
||||||
|
onChange={(e) => setAccountSid(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full font-mono text-sm"
|
||||||
|
placeholder="Mission Control Portal → Account SID"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Separate from the API Key — required for outbound/AI calls.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">Telnyx Phone Number</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={phoneNumber}
|
||||||
|
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full font-mono text-sm"
|
||||||
|
placeholder="+1xxxxxxxxxx"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Must be in E.164 format, e.g. +16175551234.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">TeXML Application ID <span className="text-gray-400 font-normal">(for calls)</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={texmlApplicationId}
|
||||||
|
onChange={(e) => setTexmlApplicationId(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full font-mono text-sm"
|
||||||
|
placeholder="TeXML Application ID"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
In the Telnyx portal → Voice → TeXML Applications → create one → set the Voice webhook URL to{" "}
|
||||||
|
<code className="bg-gray-100 px-1 rounded text-xs">{webhookOrigin}/api/telnyx/webhook/voice</code>
|
||||||
|
{" "}then paste the Application ID here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">Messaging Profile ID <span className="text-gray-400 font-normal">(optional)</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={messagingProfileId}
|
||||||
|
onChange={(e) => setMessagingProfileId(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full font-mono text-sm"
|
||||||
|
placeholder="Messaging Profile ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">Voicemail Greeting</label>
|
||||||
|
<textarea
|
||||||
|
value={greetingMessage}
|
||||||
|
onChange={(e) => setGreetingMessage(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full text-sm resize-none"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Thank you for calling. Please leave a message after the beep and we will get back to you shortly."
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">This message plays when a patient calls your Telnyx number. Leave blank to use the default.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">Webhook Public Key <span className="text-gray-400 font-normal">(for signature verification)</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={webhookPublicKey}
|
||||||
|
onChange={(e) => setWebhookPublicKey(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full font-mono text-sm"
|
||||||
|
placeholder="Public key from Telnyx portal → Webhooks"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 pt-1">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-teal-600 text-white px-4 py-2 rounded hover:bg-teal-700 text-sm"
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
>
|
||||||
|
{saveMutation.isPending ? "Saving..." : "Save Telnyx Settings"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isConfigured && (
|
||||||
|
<div className="mt-2 p-3 bg-gray-50 rounded border text-xs text-gray-600 space-y-1">
|
||||||
|
<p className="font-medium text-gray-700">Telnyx Webhook URLs</p>
|
||||||
|
<p>SMS: <code className="bg-white border px-1 rounded">{webhookOrigin}/api/telnyx/webhook/sms</code></p>
|
||||||
|
<p>Voice: <code className="bg-white border px-1 rounded">{webhookOrigin}/api/telnyx/webhook/voice</code></p>
|
||||||
|
<p className="text-gray-400">Set SMS on your Messaging Profile, and Voice on your TeXML Application, in the Telnyx portal.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ type TwilioSettings = {
|
|||||||
accountSid: string;
|
accountSid: string;
|
||||||
authToken: string;
|
authToken: string;
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
greetingMessage?: string | null;
|
|
||||||
twimlAppSid?: string | null;
|
twimlAppSid?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,7 +18,6 @@ export function TwilioSettingsCard() {
|
|||||||
const [accountSid, setAccountSid] = useState("");
|
const [accountSid, setAccountSid] = useState("");
|
||||||
const [authToken, setAuthToken] = useState("");
|
const [authToken, setAuthToken] = useState("");
|
||||||
const [phoneNumber, setPhoneNumber] = useState("");
|
const [phoneNumber, setPhoneNumber] = useState("");
|
||||||
const [greetingMessage, setGreetingMessage] = useState("");
|
|
||||||
const [twimlAppSid, setTwimlAppSid] = useState("");
|
const [twimlAppSid, setTwimlAppSid] = useState("");
|
||||||
const [showAuthToken, setShowAuthToken] = useState(false);
|
const [showAuthToken, setShowAuthToken] = useState(false);
|
||||||
|
|
||||||
@@ -37,7 +35,6 @@ export function TwilioSettingsCard() {
|
|||||||
setAccountSid(settings.accountSid ?? "");
|
setAccountSid(settings.accountSid ?? "");
|
||||||
setAuthToken(settings.authToken ?? "");
|
setAuthToken(settings.authToken ?? "");
|
||||||
setPhoneNumber(settings.phoneNumber ?? "");
|
setPhoneNumber(settings.phoneNumber ?? "");
|
||||||
setGreetingMessage(settings.greetingMessage ?? "");
|
|
||||||
setTwimlAppSid(settings.twimlAppSid ?? "");
|
setTwimlAppSid(settings.twimlAppSid ?? "");
|
||||||
}
|
}
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
@@ -63,10 +60,11 @@ export function TwilioSettingsCard() {
|
|||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!accountSid.trim() || !authToken.trim() || !phoneNumber.trim()) return;
|
if (!accountSid.trim() || !authToken.trim() || !phoneNumber.trim()) return;
|
||||||
saveMutation.mutate({ accountSid: accountSid.trim(), authToken: authToken.trim(), phoneNumber: phoneNumber.trim(), greetingMessage: greetingMessage.trim() || null, twimlAppSid: twimlAppSid.trim() || null });
|
saveMutation.mutate({ accountSid: accountSid.trim(), authToken: authToken.trim(), phoneNumber: phoneNumber.trim(), twimlAppSid: twimlAppSid.trim() || null });
|
||||||
};
|
};
|
||||||
|
|
||||||
const isConfigured = !!(settings?.accountSid && settings?.authToken && settings?.phoneNumber);
|
const isConfigured = !!(settings?.accountSid && settings?.authToken && settings?.phoneNumber);
|
||||||
|
const webhookOrigin = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -80,7 +78,8 @@ export function TwilioSettingsCard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Enter your Twilio credentials to enable SMS and calling features. Find these in your{" "}
|
Twilio powers the in-browser dial pad only — SMS, voicemail, and AI calls run through{" "}
|
||||||
|
<span className="font-medium">Telnyx</span> (see Telnyx Settings below). Find these credentials in your{" "}
|
||||||
<span className="font-medium">Twilio Console</span> → Account Info.
|
<span className="font-medium">Twilio Console</span> → Account Info.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -132,19 +131,7 @@ export function TwilioSettingsCard() {
|
|||||||
placeholder="+1xxxxxxxxxx"
|
placeholder="+1xxxxxxxxxx"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Must be in E.164 format, e.g. +16175551234</p>
|
<p className="text-xs text-gray-500 mt-1">Must be in E.164 format, e.g. +16175551234. Used as the caller ID for dial pad calls.</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium">Voicemail Greeting</label>
|
|
||||||
<textarea
|
|
||||||
value={greetingMessage}
|
|
||||||
onChange={(e) => setGreetingMessage(e.target.value)}
|
|
||||||
className="mt-1 p-2 border rounded w-full text-sm resize-none"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Thank you for calling. Please leave a message after the beep and we will get back to you shortly."
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">This message plays when a patient calls your Twilio number. Leave blank to use the default.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -158,7 +145,7 @@ export function TwilioSettingsCard() {
|
|||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
In Twilio Console → TwiML Apps → create one → set Voice URL to{" "}
|
In Twilio Console → TwiML Apps → create one → set Voice URL to{" "}
|
||||||
<code className="bg-gray-100 px-1 rounded text-xs">https://communitydentistsoflowell.mydentalofficemanagement.com/api/twilio/webhook/voice-browser</code>
|
<code className="bg-gray-100 px-1 rounded text-xs">{webhookOrigin}/api/twilio/webhook/voice-browser</code>
|
||||||
{" "}then paste the App SID here. Required for the dial pad.
|
{" "}then paste the App SID here. Required for the dial pad.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,15 +159,6 @@ export function TwilioSettingsCard() {
|
|||||||
{saveMutation.isPending ? "Saving..." : "Save Twilio Settings"}
|
{saveMutation.isPending ? "Saving..." : "Save Twilio Settings"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isConfigured && (
|
|
||||||
<div className="mt-2 p-3 bg-gray-50 rounded border text-xs text-gray-600 space-y-1">
|
|
||||||
<p className="font-medium text-gray-700">Twilio Webhook URLs</p>
|
|
||||||
<p>SMS: <code className="bg-white border px-1 rounded">https://communitydentistsoflowell.mydentalofficemanagement.com/api/twilio/webhook/sms</code></p>
|
|
||||||
<p>Voice: <code className="bg-white border px-1 rounded">https://communitydentistsoflowell.mydentalofficemanagement.com/api/twilio/webhook/voice</code></p>
|
|
||||||
<p className="text-gray-400">Set these in your Twilio Console → Phone Numbers → your number</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1460,7 +1460,7 @@ export default function AppointmentsPage() {
|
|||||||
if (!user || selectedReminderColumns.size === 0) return;
|
if (!user || selectedReminderColumns.size === 0) return;
|
||||||
setIsSendingReminders(true);
|
setIsSendingReminders(true);
|
||||||
try {
|
try {
|
||||||
const res = await apiRequest("POST", "/api/twilio/send-reminders-batch", {
|
const res = await apiRequest("POST", "/api/telnyx/send-reminders-batch", {
|
||||||
date: formattedSelectedDate,
|
date: formattedSelectedDate,
|
||||||
staffIds: Array.from(selectedReminderColumns),
|
staffIds: Array.from(selectedReminderColumns),
|
||||||
aiFollowUp: reminderAiFollowUp,
|
aiFollowUp: reminderAiFollowUp,
|
||||||
@@ -1481,7 +1481,7 @@ export default function AppointmentsPage() {
|
|||||||
if (!user || selectedRescheduleColumns.size === 0) return;
|
if (!user || selectedRescheduleColumns.size === 0) return;
|
||||||
setIsSendingReschedule(true);
|
setIsSendingReschedule(true);
|
||||||
try {
|
try {
|
||||||
const res = await apiRequest("POST", "/api/twilio/send-reschedule-batch", {
|
const res = await apiRequest("POST", "/api/telnyx/send-reschedule-batch", {
|
||||||
date: formattedSelectedDate,
|
date: formattedSelectedDate,
|
||||||
staffIds: Array.from(selectedRescheduleColumns),
|
staffIds: Array.from(selectedRescheduleColumns),
|
||||||
aiFollowUp: rescheduleAiFollowUp,
|
aiFollowUp: rescheduleAiFollowUp,
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export default function PatientConnectionPage() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
useQuery<{ enabled: boolean }>({
|
useQuery<{ enabled: boolean }>({
|
||||||
queryKey: ["/api/twilio/after-hours-handoff"],
|
queryKey: ["/api/telnyx/after-hours-handoff"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiRequest("GET", "/api/twilio/after-hours-handoff");
|
const res = await apiRequest("GET", "/api/telnyx/after-hours-handoff");
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
onSuccess: (data: { enabled: boolean }) => setAfterHoursEnabled(data.enabled),
|
onSuccess: (data: { enabled: boolean }) => setAfterHoursEnabled(data.enabled),
|
||||||
@@ -53,7 +53,7 @@ export default function PatientConnectionPage() {
|
|||||||
|
|
||||||
const afterHoursMutation = useMutation({
|
const afterHoursMutation = useMutation({
|
||||||
mutationFn: async (enabled: boolean) =>
|
mutationFn: async (enabled: boolean) =>
|
||||||
apiRequest("PUT", "/api/twilio/after-hours-handoff", { enabled }),
|
apiRequest("PUT", "/api/telnyx/after-hours-handoff", { enabled }),
|
||||||
onSuccess: (_: any, enabled: boolean) => {
|
onSuccess: (_: any, enabled: boolean) => {
|
||||||
toast({
|
toast({
|
||||||
title: enabled ? "After-hours AI enabled" : "After-hours AI disabled",
|
title: enabled ? "After-hours AI enabled" : "After-hours AI disabled",
|
||||||
@@ -81,7 +81,7 @@ export default function PatientConnectionPage() {
|
|||||||
to: string;
|
to: string;
|
||||||
patientId: number;
|
patientId: number;
|
||||||
}) => {
|
}) => {
|
||||||
return apiRequest("POST", "/api/twilio/make-call", {
|
return apiRequest("POST", "/api/telnyx/make-call", {
|
||||||
to,
|
to,
|
||||||
message:
|
message:
|
||||||
"Hello, this is a call from your dental office. We are calling to connect with you.",
|
"Hello, this is a call from your dental office. We are calling to connect with you.",
|
||||||
@@ -112,7 +112,7 @@ export default function PatientConnectionPage() {
|
|||||||
to: string;
|
to: string;
|
||||||
patientId: number;
|
patientId: number;
|
||||||
}) => {
|
}) => {
|
||||||
return apiRequest("POST", "/api/twilio/make-ai-call", { to, patientId, purpose: "recall" });
|
return apiRequest("POST", "/api/telnyx/make-ai-call", { to, patientId, purpose: "recall" });
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
@@ -150,7 +150,7 @@ export default function PatientConnectionPage() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle calling patient via Twilio
|
// Handle calling patient via Telnyx
|
||||||
const handleCall = (patient: Patient) => {
|
const handleCall = (patient: Patient) => {
|
||||||
if (!patient.phone?.trim()) {
|
if (!patient.phone?.trim()) {
|
||||||
toast({
|
toast({
|
||||||
@@ -166,7 +166,7 @@ export default function PatientConnectionPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle AI-driven call via Twilio
|
// Handle AI-driven call via Telnyx
|
||||||
const handleAiCall = (patient: Patient) => {
|
const handleAiCall = (patient: Patient) => {
|
||||||
if (!patient.phone?.trim()) {
|
if (!patient.phone?.trim()) {
|
||||||
toast({
|
toast({
|
||||||
@@ -201,7 +201,7 @@ export default function PatientConnectionPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { data: recentCommunications = [] } = useQuery<any[]>({
|
const { data: recentCommunications = [] } = useQuery<any[]>({
|
||||||
queryKey: ["/api/twilio/recent-communications"],
|
queryKey: ["/api/telnyx/recent-communications"],
|
||||||
refetchInterval: 10000,
|
refetchInterval: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Staff } from "@repo/db/types";
|
|||||||
import { NpiProviderTable } from "@/components/settings/npiProviderTable";
|
import { NpiProviderTable } from "@/components/settings/npiProviderTable";
|
||||||
import { ProgramBridgeTable } from "@/components/settings/program-bridge-table";
|
import { ProgramBridgeTable } from "@/components/settings/program-bridge-table";
|
||||||
import { TwilioSettingsCard } from "@/components/settings/twilio-settings-card";
|
import { TwilioSettingsCard } from "@/components/settings/twilio-settings-card";
|
||||||
|
import { TelnyxSettingsCard } from "@/components/settings/telnyx-settings-card";
|
||||||
import { AiSettingsCard } from "@/components/settings/ai-settings-card";
|
import { AiSettingsCard } from "@/components/settings/ai-settings-card";
|
||||||
import { OfficeHoursCard } from "@/components/settings/office-hours-card";
|
import { OfficeHoursCard } from "@/components/settings/office-hours-card";
|
||||||
import { OfficeContactCard } from "@/components/settings/office-contact-card";
|
import { OfficeContactCard } from "@/components/settings/office-contact-card";
|
||||||
@@ -30,6 +31,7 @@ type SectionId =
|
|||||||
| "npi"
|
| "npi"
|
||||||
| "programs"
|
| "programs"
|
||||||
| "twilio"
|
| "twilio"
|
||||||
|
| "telnyx"
|
||||||
| "ai"
|
| "ai"
|
||||||
| "aichat"
|
| "aichat"
|
||||||
| "officehours"
|
| "officehours"
|
||||||
@@ -260,6 +262,9 @@ export default function SettingsPage() {
|
|||||||
case "twilio":
|
case "twilio":
|
||||||
return <TwilioSettingsCard />;
|
return <TwilioSettingsCard />;
|
||||||
|
|
||||||
|
case "telnyx":
|
||||||
|
return <TelnyxSettingsCard />;
|
||||||
|
|
||||||
case "ai":
|
case "ai":
|
||||||
return <AiSettingsCard />;
|
return <AiSettingsCard />;
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -45,11 +45,12 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Public Twilio webhooks only ──────────────────────────────────────────
|
# ── Public Twilio/Telnyx webhooks only ───────────────────────────────────
|
||||||
# https://summit.mydentalofficemanagement.com, reached via Cloudflare Tunnel.
|
# https://summit.mydentalofficemanagement.com, reached via Cloudflare Tunnel.
|
||||||
# Cloudflare terminates TLS at its edge and cloudflared forwards plain HTTP
|
# Cloudflare terminates TLS at its edge and cloudflared forwards plain HTTP
|
||||||
# to this block, so no certificate is needed here. Nothing except the
|
# to this block, so no certificate is needed here. Nothing except the
|
||||||
# Twilio webhook path is exposed on this hostname.
|
# Twilio (dial pad only) and Telnyx (everything else) webhook paths is
|
||||||
|
# exposed on this hostname.
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name summit.mydentalofficemanagement.com;
|
server_name summit.mydentalofficemanagement.com;
|
||||||
@@ -64,6 +65,14 @@ server {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /api/telnyx/ {
|
||||||
|
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;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+15
@@ -59,6 +59,7 @@
|
|||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
"sharp": "^0.35.3",
|
"sharp": "^0.35.3",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
|
"telnyx": "^7.20.0",
|
||||||
"twilio": "^6.0.0",
|
"twilio": "^6.0.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
@@ -14003,6 +14004,20 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/telnyx": {
|
||||||
|
"version": "7.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/telnyx/-/telnyx-7.20.0.tgz",
|
||||||
|
"integrity": "sha512-Y+M9h8I9cvd/Zv/RzgrWnQX22VJx8+oClwGtsxvwr3lfkA+Gl0XutwuDmKhuIzTcyrpkf/qCdC2kjPYARqXrqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ws": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-decoder": {
|
"node_modules/text-decoder": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -413,6 +413,7 @@ exports.Prisma.CommunicationScalarFieldEnum = {
|
|||||||
channel: 'channel',
|
channel: 'channel',
|
||||||
direction: 'direction',
|
direction: 'direction',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
|
provider: 'provider',
|
||||||
body: 'body',
|
body: 'body',
|
||||||
callDuration: 'callDuration',
|
callDuration: 'callDuration',
|
||||||
twilioSid: 'twilioSid',
|
twilioSid: 'twilioSid',
|
||||||
@@ -441,6 +442,18 @@ exports.Prisma.TwilioSettingsScalarFieldEnum = {
|
|||||||
templates: 'templates'
|
templates: 'templates'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.TelnyxSettingsScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
apiKey: 'apiKey',
|
||||||
|
accountSid: 'accountSid',
|
||||||
|
phoneNumber: 'phoneNumber',
|
||||||
|
texmlApplicationId: 'texmlApplicationId',
|
||||||
|
messagingProfileId: 'messagingProfileId',
|
||||||
|
webhookPublicKey: 'webhookPublicKey',
|
||||||
|
greetingMessage: 'greetingMessage'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.AiSettingsScalarFieldEnum = {
|
exports.Prisma.AiSettingsScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
@@ -660,6 +673,11 @@ exports.CommunicationStatus = exports.$Enums.CommunicationStatus = {
|
|||||||
no_answer: 'no_answer'
|
no_answer: 'no_answer'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.CommunicationProvider = exports.$Enums.CommunicationProvider = {
|
||||||
|
twilio: 'twilio',
|
||||||
|
telnyx: 'telnyx'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
User: 'User',
|
User: 'User',
|
||||||
Patient: 'Patient',
|
Patient: 'Patient',
|
||||||
@@ -687,6 +705,7 @@ exports.Prisma.ModelName = {
|
|||||||
Communication: 'Communication',
|
Communication: 'Communication',
|
||||||
PatientDocument: 'PatientDocument',
|
PatientDocument: 'PatientDocument',
|
||||||
TwilioSettings: 'TwilioSettings',
|
TwilioSettings: 'TwilioSettings',
|
||||||
|
TelnyxSettings: 'TelnyxSettings',
|
||||||
AiSettings: 'AiSettings',
|
AiSettings: 'AiSettings',
|
||||||
OfficeHours: 'OfficeHours',
|
OfficeHours: 'OfficeHours',
|
||||||
OfficeContact: 'OfficeContact',
|
OfficeContact: 'OfficeContact',
|
||||||
|
|||||||
+1954
-2
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-11a124b84a93d3e63379c8a6769ab83d8729151f8cca035589101564f7ede8ed",
|
"name": "prisma-client-85ee1e66f23d650a3f0c01310fbe1ede142a47bab2aa7d55e25b3a29308a8e32",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "default.js",
|
"browser": "default.js",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ model User {
|
|||||||
cloudFiles CloudFile[]
|
cloudFiles CloudFile[]
|
||||||
communications Communication[]
|
communications Communication[]
|
||||||
twilioSettings TwilioSettings?
|
twilioSettings TwilioSettings?
|
||||||
|
telnyxSettings TelnyxSettings?
|
||||||
aiSettings AiSettings?
|
aiSettings AiSettings?
|
||||||
officeHours OfficeHours?
|
officeHours OfficeHours?
|
||||||
officeContact OfficeContact?
|
officeContact OfficeContact?
|
||||||
@@ -562,6 +563,11 @@ enum CommunicationStatus {
|
|||||||
no_answer
|
no_answer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CommunicationProvider {
|
||||||
|
twilio
|
||||||
|
telnyx
|
||||||
|
}
|
||||||
|
|
||||||
model Communication {
|
model Communication {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
patientId Int
|
patientId Int
|
||||||
@@ -570,9 +576,13 @@ model Communication {
|
|||||||
channel CommunicationChannel
|
channel CommunicationChannel
|
||||||
direction CommunicationDirection
|
direction CommunicationDirection
|
||||||
status CommunicationStatus
|
status CommunicationStatus
|
||||||
|
// Which platform produced twilioSid — twilio for dial-pad-originated calls,
|
||||||
|
// telnyx for everything else (SMS, inbound/AI voice, staff-triggered outbound calls).
|
||||||
|
provider CommunicationProvider @default(twilio)
|
||||||
|
|
||||||
body String?
|
body String?
|
||||||
callDuration Int?
|
callDuration Int?
|
||||||
|
// Provider SID — Twilio MessageSid/CallSid, or Telnyx message/call id. See `provider`.
|
||||||
twilioSid String?
|
twilioSid String?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -606,6 +616,8 @@ model TwilioSettings {
|
|||||||
accountSid String
|
accountSid String
|
||||||
authToken String
|
authToken String
|
||||||
phoneNumber String
|
phoneNumber String
|
||||||
|
// Vestigial — inbound voice moved to Telnyx (TelnyxSettings.greetingMessage). No longer
|
||||||
|
// read/written; kept to avoid a destructive migration for a cosmetic cleanup.
|
||||||
greetingMessage String?
|
greetingMessage String?
|
||||||
templates Json?
|
templates Json?
|
||||||
|
|
||||||
@@ -614,6 +626,24 @@ model TwilioSettings {
|
|||||||
@@map("twilio_settings")
|
@@map("twilio_settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model TelnyxSettings {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int @unique
|
||||||
|
apiKey String
|
||||||
|
// Telnyx account SID (Mission Control Portal), required to place outbound
|
||||||
|
// TeXML calls via POST /texml/Accounts/{accountSid}/Calls — separate from apiKey.
|
||||||
|
accountSid String?
|
||||||
|
phoneNumber String
|
||||||
|
texmlApplicationId String?
|
||||||
|
messagingProfileId String?
|
||||||
|
webhookPublicKey String?
|
||||||
|
greetingMessage String?
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("telnyx_settings")
|
||||||
|
}
|
||||||
|
|
||||||
model AiSettings {
|
model AiSettings {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId Int @unique
|
userId Int @unique
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "CommunicationProvider" AS ENUM ('twilio', 'telnyx');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "communications" ADD COLUMN "provider" "CommunicationProvider" NOT NULL DEFAULT 'twilio';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "telnyx_settings" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"apiKey" TEXT NOT NULL,
|
||||||
|
"phoneNumber" TEXT NOT NULL,
|
||||||
|
"texmlApplicationId" TEXT,
|
||||||
|
"messagingProfileId" TEXT,
|
||||||
|
"webhookPublicKey" TEXT,
|
||||||
|
"greetingMessage" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "telnyx_settings_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "telnyx_settings_userId_key" ON "telnyx_settings"("userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "telnyx_settings" ADD CONSTRAINT "telnyx_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "telnyx_settings" ADD COLUMN "accountSid" TEXT;
|
||||||
@@ -44,6 +44,7 @@ model User {
|
|||||||
cloudFiles CloudFile[]
|
cloudFiles CloudFile[]
|
||||||
communications Communication[]
|
communications Communication[]
|
||||||
twilioSettings TwilioSettings?
|
twilioSettings TwilioSettings?
|
||||||
|
telnyxSettings TelnyxSettings?
|
||||||
aiSettings AiSettings?
|
aiSettings AiSettings?
|
||||||
officeHours OfficeHours?
|
officeHours OfficeHours?
|
||||||
officeContact OfficeContact?
|
officeContact OfficeContact?
|
||||||
@@ -563,6 +564,11 @@ enum CommunicationStatus {
|
|||||||
no_answer
|
no_answer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CommunicationProvider {
|
||||||
|
twilio
|
||||||
|
telnyx
|
||||||
|
}
|
||||||
|
|
||||||
model Communication {
|
model Communication {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
patientId Int
|
patientId Int
|
||||||
@@ -571,9 +577,13 @@ model Communication {
|
|||||||
channel CommunicationChannel
|
channel CommunicationChannel
|
||||||
direction CommunicationDirection
|
direction CommunicationDirection
|
||||||
status CommunicationStatus
|
status CommunicationStatus
|
||||||
|
// Which platform produced twilioSid — twilio for dial-pad-originated calls,
|
||||||
|
// telnyx for everything else (SMS, inbound/AI voice, staff-triggered outbound calls).
|
||||||
|
provider CommunicationProvider @default(twilio)
|
||||||
|
|
||||||
body String?
|
body String?
|
||||||
callDuration Int?
|
callDuration Int?
|
||||||
|
// Provider SID — Twilio MessageSid/CallSid, or Telnyx message/call id. See `provider`.
|
||||||
twilioSid String?
|
twilioSid String?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -607,6 +617,8 @@ model TwilioSettings {
|
|||||||
accountSid String
|
accountSid String
|
||||||
authToken String
|
authToken String
|
||||||
phoneNumber String
|
phoneNumber String
|
||||||
|
// Vestigial — inbound voice moved to Telnyx (TelnyxSettings.greetingMessage). No longer
|
||||||
|
// read/written; kept to avoid a destructive migration for a cosmetic cleanup.
|
||||||
greetingMessage String?
|
greetingMessage String?
|
||||||
templates Json?
|
templates Json?
|
||||||
|
|
||||||
@@ -615,6 +627,24 @@ model TwilioSettings {
|
|||||||
@@map("twilio_settings")
|
@@map("twilio_settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model TelnyxSettings {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int @unique
|
||||||
|
apiKey String
|
||||||
|
// Telnyx account SID (Mission Control Portal), required to place outbound
|
||||||
|
// TeXML calls via POST /texml/Accounts/{accountSid}/Calls — separate from apiKey.
|
||||||
|
accountSid String?
|
||||||
|
phoneNumber String
|
||||||
|
texmlApplicationId String?
|
||||||
|
messagingProfileId String?
|
||||||
|
webhookPublicKey String?
|
||||||
|
greetingMessage String?
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("telnyx_settings")
|
||||||
|
}
|
||||||
|
|
||||||
model AiSettings {
|
model AiSettings {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId Int @unique
|
userId Int @unique
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"generatorVersion": "1.0.0",
|
"generatorVersion": "unknown",
|
||||||
"generatedAt": "2026-09-03T01:38:13.906Z",
|
"generatedAt": "2026-09-03T18:52:59.084Z",
|
||||||
"outputPath": "/home/gg/Desktop/DentalManagementMH07/packages/db/shared",
|
"outputPath": "/home/gg/Desktop/DentalManagementMH07/packages/db/shared",
|
||||||
"files": [
|
"files": [
|
||||||
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"schemas/enums/CommunicationScalarFieldEnum.schema.ts",
|
"schemas/enums/CommunicationScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/PatientDocumentScalarFieldEnum.schema.ts",
|
"schemas/enums/PatientDocumentScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/TwilioSettingsScalarFieldEnum.schema.ts",
|
"schemas/enums/TwilioSettingsScalarFieldEnum.schema.ts",
|
||||||
|
"schemas/enums/TelnyxSettingsScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/AiSettingsScalarFieldEnum.schema.ts",
|
"schemas/enums/AiSettingsScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/OfficeHoursScalarFieldEnum.schema.ts",
|
"schemas/enums/OfficeHoursScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/OfficeContactScalarFieldEnum.schema.ts",
|
"schemas/enums/OfficeContactScalarFieldEnum.schema.ts",
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
"schemas/enums/CommunicationChannel.schema.ts",
|
"schemas/enums/CommunicationChannel.schema.ts",
|
||||||
"schemas/enums/CommunicationDirection.schema.ts",
|
"schemas/enums/CommunicationDirection.schema.ts",
|
||||||
"schemas/enums/CommunicationStatus.schema.ts",
|
"schemas/enums/CommunicationStatus.schema.ts",
|
||||||
|
"schemas/enums/CommunicationProvider.schema.ts",
|
||||||
"schemas/objects/UserWhereInput.schema.ts",
|
"schemas/objects/UserWhereInput.schema.ts",
|
||||||
"schemas/objects/UserOrderByWithRelationInput.schema.ts",
|
"schemas/objects/UserOrderByWithRelationInput.schema.ts",
|
||||||
"schemas/objects/UserWhereUniqueInput.schema.ts",
|
"schemas/objects/UserWhereUniqueInput.schema.ts",
|
||||||
@@ -189,6 +191,11 @@
|
|||||||
"schemas/objects/TwilioSettingsWhereUniqueInput.schema.ts",
|
"schemas/objects/TwilioSettingsWhereUniqueInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsOrderByWithAggregationInput.schema.ts",
|
"schemas/objects/TwilioSettingsOrderByWithAggregationInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsScalarWhereWithAggregatesInput.schema.ts",
|
"schemas/objects/TwilioSettingsScalarWhereWithAggregatesInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsWhereInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsOrderByWithRelationInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsWhereUniqueInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsOrderByWithAggregationInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsScalarWhereWithAggregatesInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsWhereInput.schema.ts",
|
"schemas/objects/AiSettingsWhereInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsOrderByWithRelationInput.schema.ts",
|
"schemas/objects/AiSettingsOrderByWithRelationInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsWhereUniqueInput.schema.ts",
|
"schemas/objects/AiSettingsWhereUniqueInput.schema.ts",
|
||||||
@@ -421,6 +428,13 @@
|
|||||||
"schemas/objects/TwilioSettingsCreateManyInput.schema.ts",
|
"schemas/objects/TwilioSettingsCreateManyInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsUpdateManyMutationInput.schema.ts",
|
"schemas/objects/TwilioSettingsUpdateManyMutationInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsUncheckedUpdateManyInput.schema.ts",
|
"schemas/objects/TwilioSettingsUncheckedUpdateManyInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsCreateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUncheckedCreateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUpdateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUncheckedUpdateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsCreateManyInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUpdateManyMutationInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUncheckedUpdateManyInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsCreateInput.schema.ts",
|
"schemas/objects/AiSettingsCreateInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUncheckedCreateInput.schema.ts",
|
"schemas/objects/AiSettingsUncheckedCreateInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUpdateInput.schema.ts",
|
"schemas/objects/AiSettingsUpdateInput.schema.ts",
|
||||||
@@ -509,6 +523,7 @@
|
|||||||
"schemas/objects/CloudFileListRelationFilter.schema.ts",
|
"schemas/objects/CloudFileListRelationFilter.schema.ts",
|
||||||
"schemas/objects/CommunicationListRelationFilter.schema.ts",
|
"schemas/objects/CommunicationListRelationFilter.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsNullableScalarRelationFilter.schema.ts",
|
"schemas/objects/TwilioSettingsNullableScalarRelationFilter.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsNullableScalarRelationFilter.schema.ts",
|
||||||
"schemas/objects/AiSettingsNullableScalarRelationFilter.schema.ts",
|
"schemas/objects/AiSettingsNullableScalarRelationFilter.schema.ts",
|
||||||
"schemas/objects/OfficeHoursNullableScalarRelationFilter.schema.ts",
|
"schemas/objects/OfficeHoursNullableScalarRelationFilter.schema.ts",
|
||||||
"schemas/objects/OfficeContactNullableScalarRelationFilter.schema.ts",
|
"schemas/objects/OfficeContactNullableScalarRelationFilter.schema.ts",
|
||||||
@@ -735,6 +750,7 @@
|
|||||||
"schemas/objects/EnumCommunicationChannelFilter.schema.ts",
|
"schemas/objects/EnumCommunicationChannelFilter.schema.ts",
|
||||||
"schemas/objects/EnumCommunicationDirectionFilter.schema.ts",
|
"schemas/objects/EnumCommunicationDirectionFilter.schema.ts",
|
||||||
"schemas/objects/EnumCommunicationStatusFilter.schema.ts",
|
"schemas/objects/EnumCommunicationStatusFilter.schema.ts",
|
||||||
|
"schemas/objects/EnumCommunicationProviderFilter.schema.ts",
|
||||||
"schemas/objects/CommunicationCountOrderByAggregateInput.schema.ts",
|
"schemas/objects/CommunicationCountOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommunicationAvgOrderByAggregateInput.schema.ts",
|
"schemas/objects/CommunicationAvgOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommunicationMaxOrderByAggregateInput.schema.ts",
|
"schemas/objects/CommunicationMaxOrderByAggregateInput.schema.ts",
|
||||||
@@ -743,6 +759,7 @@
|
|||||||
"schemas/objects/EnumCommunicationChannelWithAggregatesFilter.schema.ts",
|
"schemas/objects/EnumCommunicationChannelWithAggregatesFilter.schema.ts",
|
||||||
"schemas/objects/EnumCommunicationDirectionWithAggregatesFilter.schema.ts",
|
"schemas/objects/EnumCommunicationDirectionWithAggregatesFilter.schema.ts",
|
||||||
"schemas/objects/EnumCommunicationStatusWithAggregatesFilter.schema.ts",
|
"schemas/objects/EnumCommunicationStatusWithAggregatesFilter.schema.ts",
|
||||||
|
"schemas/objects/EnumCommunicationProviderWithAggregatesFilter.schema.ts",
|
||||||
"schemas/objects/PatientDocumentCountOrderByAggregateInput.schema.ts",
|
"schemas/objects/PatientDocumentCountOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/PatientDocumentAvgOrderByAggregateInput.schema.ts",
|
"schemas/objects/PatientDocumentAvgOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/PatientDocumentMaxOrderByAggregateInput.schema.ts",
|
"schemas/objects/PatientDocumentMaxOrderByAggregateInput.schema.ts",
|
||||||
@@ -753,6 +770,11 @@
|
|||||||
"schemas/objects/TwilioSettingsMaxOrderByAggregateInput.schema.ts",
|
"schemas/objects/TwilioSettingsMaxOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsMinOrderByAggregateInput.schema.ts",
|
"schemas/objects/TwilioSettingsMinOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsSumOrderByAggregateInput.schema.ts",
|
"schemas/objects/TwilioSettingsSumOrderByAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsCountOrderByAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsAvgOrderByAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsMaxOrderByAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsMinOrderByAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsSumOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsCountOrderByAggregateInput.schema.ts",
|
"schemas/objects/AiSettingsCountOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsAvgOrderByAggregateInput.schema.ts",
|
"schemas/objects/AiSettingsAvgOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsMaxOrderByAggregateInput.schema.ts",
|
"schemas/objects/AiSettingsMaxOrderByAggregateInput.schema.ts",
|
||||||
@@ -823,6 +845,7 @@
|
|||||||
"schemas/objects/CloudFileCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/CloudFileCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/CommunicationCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/CommunicationCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/TwilioSettingsCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/AiSettingsCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/OfficeHoursCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/OfficeHoursCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/OfficeContactCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/OfficeContactCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
@@ -846,6 +869,7 @@
|
|||||||
"schemas/objects/CloudFileUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/CloudFileUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/CommunicationUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/CommunicationUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/TwilioSettingsUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/AiSettingsUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/OfficeHoursUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/OfficeHoursUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/OfficeContactUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/OfficeContactUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
@@ -872,6 +896,7 @@
|
|||||||
"schemas/objects/CloudFileUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/CloudFileUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/CommunicationUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/CommunicationUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/TwilioSettingsUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/AiSettingsUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/OfficeHoursUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/OfficeHoursUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/OfficeContactUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/OfficeContactUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
@@ -895,6 +920,7 @@
|
|||||||
"schemas/objects/CloudFileUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/CloudFileUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/CommunicationUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/CommunicationUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/TwilioSettingsUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/AiSettingsUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/OfficeHoursUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/OfficeHoursUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/OfficeContactUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/OfficeContactUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
@@ -1121,12 +1147,15 @@
|
|||||||
"schemas/objects/EnumCommunicationChannelFieldUpdateOperationsInput.schema.ts",
|
"schemas/objects/EnumCommunicationChannelFieldUpdateOperationsInput.schema.ts",
|
||||||
"schemas/objects/EnumCommunicationDirectionFieldUpdateOperationsInput.schema.ts",
|
"schemas/objects/EnumCommunicationDirectionFieldUpdateOperationsInput.schema.ts",
|
||||||
"schemas/objects/EnumCommunicationStatusFieldUpdateOperationsInput.schema.ts",
|
"schemas/objects/EnumCommunicationStatusFieldUpdateOperationsInput.schema.ts",
|
||||||
|
"schemas/objects/EnumCommunicationProviderFieldUpdateOperationsInput.schema.ts",
|
||||||
"schemas/objects/PatientUpdateOneRequiredWithoutCommunicationsNestedInput.schema.ts",
|
"schemas/objects/PatientUpdateOneRequiredWithoutCommunicationsNestedInput.schema.ts",
|
||||||
"schemas/objects/UserUpdateOneWithoutCommunicationsNestedInput.schema.ts",
|
"schemas/objects/UserUpdateOneWithoutCommunicationsNestedInput.schema.ts",
|
||||||
"schemas/objects/PatientCreateNestedOneWithoutDocumentsInput.schema.ts",
|
"schemas/objects/PatientCreateNestedOneWithoutDocumentsInput.schema.ts",
|
||||||
"schemas/objects/PatientUpdateOneRequiredWithoutDocumentsNestedInput.schema.ts",
|
"schemas/objects/PatientUpdateOneRequiredWithoutDocumentsNestedInput.schema.ts",
|
||||||
"schemas/objects/UserCreateNestedOneWithoutTwilioSettingsInput.schema.ts",
|
"schemas/objects/UserCreateNestedOneWithoutTwilioSettingsInput.schema.ts",
|
||||||
"schemas/objects/UserUpdateOneRequiredWithoutTwilioSettingsNestedInput.schema.ts",
|
"schemas/objects/UserUpdateOneRequiredWithoutTwilioSettingsNestedInput.schema.ts",
|
||||||
|
"schemas/objects/UserCreateNestedOneWithoutTelnyxSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserUpdateOneRequiredWithoutTelnyxSettingsNestedInput.schema.ts",
|
||||||
"schemas/objects/UserCreateNestedOneWithoutAiSettingsInput.schema.ts",
|
"schemas/objects/UserCreateNestedOneWithoutAiSettingsInput.schema.ts",
|
||||||
"schemas/objects/UserUpdateOneRequiredWithoutAiSettingsNestedInput.schema.ts",
|
"schemas/objects/UserUpdateOneRequiredWithoutAiSettingsNestedInput.schema.ts",
|
||||||
"schemas/objects/UserCreateNestedOneWithoutOfficeHoursInput.schema.ts",
|
"schemas/objects/UserCreateNestedOneWithoutOfficeHoursInput.schema.ts",
|
||||||
@@ -1201,9 +1230,11 @@
|
|||||||
"schemas/objects/NestedEnumCommunicationChannelFilter.schema.ts",
|
"schemas/objects/NestedEnumCommunicationChannelFilter.schema.ts",
|
||||||
"schemas/objects/NestedEnumCommunicationDirectionFilter.schema.ts",
|
"schemas/objects/NestedEnumCommunicationDirectionFilter.schema.ts",
|
||||||
"schemas/objects/NestedEnumCommunicationStatusFilter.schema.ts",
|
"schemas/objects/NestedEnumCommunicationStatusFilter.schema.ts",
|
||||||
|
"schemas/objects/NestedEnumCommunicationProviderFilter.schema.ts",
|
||||||
"schemas/objects/NestedEnumCommunicationChannelWithAggregatesFilter.schema.ts",
|
"schemas/objects/NestedEnumCommunicationChannelWithAggregatesFilter.schema.ts",
|
||||||
"schemas/objects/NestedEnumCommunicationDirectionWithAggregatesFilter.schema.ts",
|
"schemas/objects/NestedEnumCommunicationDirectionWithAggregatesFilter.schema.ts",
|
||||||
"schemas/objects/NestedEnumCommunicationStatusWithAggregatesFilter.schema.ts",
|
"schemas/objects/NestedEnumCommunicationStatusWithAggregatesFilter.schema.ts",
|
||||||
|
"schemas/objects/NestedEnumCommunicationProviderWithAggregatesFilter.schema.ts",
|
||||||
"schemas/objects/NestedJsonFilter.schema.ts",
|
"schemas/objects/NestedJsonFilter.schema.ts",
|
||||||
"schemas/objects/PatientCreateWithoutUserInput.schema.ts",
|
"schemas/objects/PatientCreateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientUncheckedCreateWithoutUserInput.schema.ts",
|
"schemas/objects/PatientUncheckedCreateWithoutUserInput.schema.ts",
|
||||||
@@ -1264,6 +1295,9 @@
|
|||||||
"schemas/objects/TwilioSettingsCreateWithoutUserInput.schema.ts",
|
"schemas/objects/TwilioSettingsCreateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsUncheckedCreateWithoutUserInput.schema.ts",
|
"schemas/objects/TwilioSettingsUncheckedCreateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsCreateOrConnectWithoutUserInput.schema.ts",
|
"schemas/objects/TwilioSettingsCreateOrConnectWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsCreateWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUncheckedCreateWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsCreateOrConnectWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsCreateWithoutUserInput.schema.ts",
|
"schemas/objects/AiSettingsCreateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUncheckedCreateWithoutUserInput.schema.ts",
|
"schemas/objects/AiSettingsUncheckedCreateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsCreateOrConnectWithoutUserInput.schema.ts",
|
"schemas/objects/AiSettingsCreateOrConnectWithoutUserInput.schema.ts",
|
||||||
@@ -1351,6 +1385,10 @@
|
|||||||
"schemas/objects/TwilioSettingsUpdateToOneWithWhereWithoutUserInput.schema.ts",
|
"schemas/objects/TwilioSettingsUpdateToOneWithWhereWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsUpdateWithoutUserInput.schema.ts",
|
"schemas/objects/TwilioSettingsUpdateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsUncheckedUpdateWithoutUserInput.schema.ts",
|
"schemas/objects/TwilioSettingsUncheckedUpdateWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUpsertWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUpdateToOneWithWhereWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUpdateWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsUncheckedUpdateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUpsertWithoutUserInput.schema.ts",
|
"schemas/objects/AiSettingsUpsertWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUpdateToOneWithWhereWithoutUserInput.schema.ts",
|
"schemas/objects/AiSettingsUpdateToOneWithWhereWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsUpdateWithoutUserInput.schema.ts",
|
"schemas/objects/AiSettingsUpdateWithoutUserInput.schema.ts",
|
||||||
@@ -1898,6 +1936,13 @@
|
|||||||
"schemas/objects/UserUpdateToOneWithWhereWithoutTwilioSettingsInput.schema.ts",
|
"schemas/objects/UserUpdateToOneWithWhereWithoutTwilioSettingsInput.schema.ts",
|
||||||
"schemas/objects/UserUpdateWithoutTwilioSettingsInput.schema.ts",
|
"schemas/objects/UserUpdateWithoutTwilioSettingsInput.schema.ts",
|
||||||
"schemas/objects/UserUncheckedUpdateWithoutTwilioSettingsInput.schema.ts",
|
"schemas/objects/UserUncheckedUpdateWithoutTwilioSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserCreateWithoutTelnyxSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserUncheckedCreateWithoutTelnyxSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserCreateOrConnectWithoutTelnyxSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserUpsertWithoutTelnyxSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserUpdateToOneWithWhereWithoutTelnyxSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserUpdateWithoutTelnyxSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserUncheckedUpdateWithoutTelnyxSettingsInput.schema.ts",
|
||||||
"schemas/objects/UserCreateWithoutAiSettingsInput.schema.ts",
|
"schemas/objects/UserCreateWithoutAiSettingsInput.schema.ts",
|
||||||
"schemas/objects/UserUncheckedCreateWithoutAiSettingsInput.schema.ts",
|
"schemas/objects/UserUncheckedCreateWithoutAiSettingsInput.schema.ts",
|
||||||
"schemas/objects/UserCreateOrConnectWithoutAiSettingsInput.schema.ts",
|
"schemas/objects/UserCreateOrConnectWithoutAiSettingsInput.schema.ts",
|
||||||
@@ -2303,6 +2348,11 @@
|
|||||||
"schemas/objects/TwilioSettingsSumAggregateInput.schema.ts",
|
"schemas/objects/TwilioSettingsSumAggregateInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsMinAggregateInput.schema.ts",
|
"schemas/objects/TwilioSettingsMinAggregateInput.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsMaxAggregateInput.schema.ts",
|
"schemas/objects/TwilioSettingsMaxAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsCountAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsAvgAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsSumAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsMinAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsMaxAggregateInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsCountAggregateInput.schema.ts",
|
"schemas/objects/AiSettingsCountAggregateInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsAvgAggregateInput.schema.ts",
|
"schemas/objects/AiSettingsAvgAggregateInput.schema.ts",
|
||||||
"schemas/objects/AiSettingsSumAggregateInput.schema.ts",
|
"schemas/objects/AiSettingsSumAggregateInput.schema.ts",
|
||||||
@@ -2449,6 +2499,7 @@
|
|||||||
"schemas/objects/CommunicationSelect.schema.ts",
|
"schemas/objects/CommunicationSelect.schema.ts",
|
||||||
"schemas/objects/PatientDocumentSelect.schema.ts",
|
"schemas/objects/PatientDocumentSelect.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsSelect.schema.ts",
|
"schemas/objects/TwilioSettingsSelect.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsSelect.schema.ts",
|
||||||
"schemas/objects/AiSettingsSelect.schema.ts",
|
"schemas/objects/AiSettingsSelect.schema.ts",
|
||||||
"schemas/objects/OfficeHoursSelect.schema.ts",
|
"schemas/objects/OfficeHoursSelect.schema.ts",
|
||||||
"schemas/objects/OfficeContactSelect.schema.ts",
|
"schemas/objects/OfficeContactSelect.schema.ts",
|
||||||
@@ -2485,6 +2536,7 @@
|
|||||||
"schemas/objects/CommunicationArgs.schema.ts",
|
"schemas/objects/CommunicationArgs.schema.ts",
|
||||||
"schemas/objects/PatientDocumentArgs.schema.ts",
|
"schemas/objects/PatientDocumentArgs.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsArgs.schema.ts",
|
"schemas/objects/TwilioSettingsArgs.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsArgs.schema.ts",
|
||||||
"schemas/objects/AiSettingsArgs.schema.ts",
|
"schemas/objects/AiSettingsArgs.schema.ts",
|
||||||
"schemas/objects/OfficeHoursArgs.schema.ts",
|
"schemas/objects/OfficeHoursArgs.schema.ts",
|
||||||
"schemas/objects/OfficeContactArgs.schema.ts",
|
"schemas/objects/OfficeContactArgs.schema.ts",
|
||||||
@@ -2520,6 +2572,7 @@
|
|||||||
"schemas/objects/CommunicationInclude.schema.ts",
|
"schemas/objects/CommunicationInclude.schema.ts",
|
||||||
"schemas/objects/PatientDocumentInclude.schema.ts",
|
"schemas/objects/PatientDocumentInclude.schema.ts",
|
||||||
"schemas/objects/TwilioSettingsInclude.schema.ts",
|
"schemas/objects/TwilioSettingsInclude.schema.ts",
|
||||||
|
"schemas/objects/TelnyxSettingsInclude.schema.ts",
|
||||||
"schemas/objects/AiSettingsInclude.schema.ts",
|
"schemas/objects/AiSettingsInclude.schema.ts",
|
||||||
"schemas/objects/OfficeHoursInclude.schema.ts",
|
"schemas/objects/OfficeHoursInclude.schema.ts",
|
||||||
"schemas/objects/OfficeContactInclude.schema.ts",
|
"schemas/objects/OfficeContactInclude.schema.ts",
|
||||||
@@ -2972,6 +3025,23 @@
|
|||||||
"schemas/upsertOneTwilioSettings.schema.ts",
|
"schemas/upsertOneTwilioSettings.schema.ts",
|
||||||
"schemas/aggregateTwilioSettings.schema.ts",
|
"schemas/aggregateTwilioSettings.schema.ts",
|
||||||
"schemas/groupByTwilioSettings.schema.ts",
|
"schemas/groupByTwilioSettings.schema.ts",
|
||||||
|
"schemas/findUniqueTelnyxSettings.schema.ts",
|
||||||
|
"schemas/findUniqueOrThrowTelnyxSettings.schema.ts",
|
||||||
|
"schemas/findFirstTelnyxSettings.schema.ts",
|
||||||
|
"schemas/findFirstOrThrowTelnyxSettings.schema.ts",
|
||||||
|
"schemas/findManyTelnyxSettings.schema.ts",
|
||||||
|
"schemas/countTelnyxSettings.schema.ts",
|
||||||
|
"schemas/createOneTelnyxSettings.schema.ts",
|
||||||
|
"schemas/createManyTelnyxSettings.schema.ts",
|
||||||
|
"schemas/createManyAndReturnTelnyxSettings.schema.ts",
|
||||||
|
"schemas/deleteOneTelnyxSettings.schema.ts",
|
||||||
|
"schemas/deleteManyTelnyxSettings.schema.ts",
|
||||||
|
"schemas/updateOneTelnyxSettings.schema.ts",
|
||||||
|
"schemas/updateManyTelnyxSettings.schema.ts",
|
||||||
|
"schemas/updateManyAndReturnTelnyxSettings.schema.ts",
|
||||||
|
"schemas/upsertOneTelnyxSettings.schema.ts",
|
||||||
|
"schemas/aggregateTelnyxSettings.schema.ts",
|
||||||
|
"schemas/groupByTelnyxSettings.schema.ts",
|
||||||
"schemas/findUniqueAiSettings.schema.ts",
|
"schemas/findUniqueAiSettings.schema.ts",
|
||||||
"schemas/findUniqueOrThrowAiSettings.schema.ts",
|
"schemas/findUniqueOrThrowAiSettings.schema.ts",
|
||||||
"schemas/findFirstAiSettings.schema.ts",
|
"schemas/findFirstAiSettings.schema.ts",
|
||||||
@@ -3480,6 +3550,19 @@
|
|||||||
"schemas/results/TwilioSettingsAggregateResult.schema.ts",
|
"schemas/results/TwilioSettingsAggregateResult.schema.ts",
|
||||||
"schemas/results/TwilioSettingsGroupByResult.schema.ts",
|
"schemas/results/TwilioSettingsGroupByResult.schema.ts",
|
||||||
"schemas/results/TwilioSettingsCountResult.schema.ts",
|
"schemas/results/TwilioSettingsCountResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsFindUniqueResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsFindFirstResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsFindManyResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsCreateResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsCreateManyResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsUpdateResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsUpdateManyResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsUpsertResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsDeleteResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsDeleteManyResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsAggregateResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsGroupByResult.schema.ts",
|
||||||
|
"schemas/results/TelnyxSettingsCountResult.schema.ts",
|
||||||
"schemas/results/AiSettingsFindUniqueResult.schema.ts",
|
"schemas/results/AiSettingsFindUniqueResult.schema.ts",
|
||||||
"schemas/results/AiSettingsFindFirstResult.schema.ts",
|
"schemas/results/AiSettingsFindFirstResult.schema.ts",
|
||||||
"schemas/results/AiSettingsFindManyResult.schema.ts",
|
"schemas/results/AiSettingsFindManyResult.schema.ts",
|
||||||
@@ -3638,6 +3721,7 @@
|
|||||||
"schemas/variants/pure/Communication.pure.ts",
|
"schemas/variants/pure/Communication.pure.ts",
|
||||||
"schemas/variants/pure/PatientDocument.pure.ts",
|
"schemas/variants/pure/PatientDocument.pure.ts",
|
||||||
"schemas/variants/pure/TwilioSettings.pure.ts",
|
"schemas/variants/pure/TwilioSettings.pure.ts",
|
||||||
|
"schemas/variants/pure/TelnyxSettings.pure.ts",
|
||||||
"schemas/variants/pure/AiSettings.pure.ts",
|
"schemas/variants/pure/AiSettings.pure.ts",
|
||||||
"schemas/variants/pure/OfficeHours.pure.ts",
|
"schemas/variants/pure/OfficeHours.pure.ts",
|
||||||
"schemas/variants/pure/OfficeContact.pure.ts",
|
"schemas/variants/pure/OfficeContact.pure.ts",
|
||||||
@@ -3675,6 +3759,7 @@
|
|||||||
"schemas/variants/input/Communication.input.ts",
|
"schemas/variants/input/Communication.input.ts",
|
||||||
"schemas/variants/input/PatientDocument.input.ts",
|
"schemas/variants/input/PatientDocument.input.ts",
|
||||||
"schemas/variants/input/TwilioSettings.input.ts",
|
"schemas/variants/input/TwilioSettings.input.ts",
|
||||||
|
"schemas/variants/input/TelnyxSettings.input.ts",
|
||||||
"schemas/variants/input/AiSettings.input.ts",
|
"schemas/variants/input/AiSettings.input.ts",
|
||||||
"schemas/variants/input/OfficeHours.input.ts",
|
"schemas/variants/input/OfficeHours.input.ts",
|
||||||
"schemas/variants/input/OfficeContact.input.ts",
|
"schemas/variants/input/OfficeContact.input.ts",
|
||||||
@@ -3712,6 +3797,7 @@
|
|||||||
"schemas/variants/result/Communication.result.ts",
|
"schemas/variants/result/Communication.result.ts",
|
||||||
"schemas/variants/result/PatientDocument.result.ts",
|
"schemas/variants/result/PatientDocument.result.ts",
|
||||||
"schemas/variants/result/TwilioSettings.result.ts",
|
"schemas/variants/result/TwilioSettings.result.ts",
|
||||||
|
"schemas/variants/result/TelnyxSettings.result.ts",
|
||||||
"schemas/variants/result/AiSettings.result.ts",
|
"schemas/variants/result/AiSettings.result.ts",
|
||||||
"schemas/variants/result/OfficeHours.result.ts",
|
"schemas/variants/result/OfficeHours.result.ts",
|
||||||
"schemas/variants/result/OfficeContact.result.ts",
|
"schemas/variants/result/OfficeContact.result.ts",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import type { Prisma } from '../../generated/prisma';
|
import { Prisma } from '../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import Decimal from "decimal.js";
|
|
||||||
|
|
||||||
// DECIMAL HELPERS
|
// DECIMAL HELPERS
|
||||||
//------------------------------------------------------
|
//------------------------------------------------------
|
||||||
@@ -24,7 +23,7 @@ export const isValidDecimalInput = (
|
|||||||
if (v === undefined || v === null) return false;
|
if (v === undefined || v === null) return false;
|
||||||
return (
|
return (
|
||||||
// Explicit instance checks first
|
// Explicit instance checks first
|
||||||
v instanceof Decimal ||
|
v instanceof Prisma.Decimal ||
|
||||||
// If Decimal.js is present and imported by the generator, this symbol exists at runtime
|
// If Decimal.js is present and imported by the generator, this symbol exists at runtime
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore - Decimal may be undefined when not installed; codegen controls the import
|
// @ts-ignore - Decimal may be undefined when not installed; codegen controls the import
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsOrderByWithRelationInputObjectSchema as TelnyxSettingsOrderByWithRelationInputObjectSchema } from './objects/TelnyxSettingsOrderByWithRelationInput.schema';
|
||||||
|
import { TelnyxSettingsWhereInputObjectSchema as TelnyxSettingsWhereInputObjectSchema } from './objects/TelnyxSettingsWhereInput.schema';
|
||||||
|
import { TelnyxSettingsWhereUniqueInputObjectSchema as TelnyxSettingsWhereUniqueInputObjectSchema } from './objects/TelnyxSettingsWhereUniqueInput.schema';
|
||||||
|
import { TelnyxSettingsCountAggregateInputObjectSchema as TelnyxSettingsCountAggregateInputObjectSchema } from './objects/TelnyxSettingsCountAggregateInput.schema';
|
||||||
|
import { TelnyxSettingsMinAggregateInputObjectSchema as TelnyxSettingsMinAggregateInputObjectSchema } from './objects/TelnyxSettingsMinAggregateInput.schema';
|
||||||
|
import { TelnyxSettingsMaxAggregateInputObjectSchema as TelnyxSettingsMaxAggregateInputObjectSchema } from './objects/TelnyxSettingsMaxAggregateInput.schema';
|
||||||
|
import { TelnyxSettingsAvgAggregateInputObjectSchema as TelnyxSettingsAvgAggregateInputObjectSchema } from './objects/TelnyxSettingsAvgAggregateInput.schema';
|
||||||
|
import { TelnyxSettingsSumAggregateInputObjectSchema as TelnyxSettingsSumAggregateInputObjectSchema } from './objects/TelnyxSettingsSumAggregateInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsAggregateSchema: z.ZodType<Prisma.TelnyxSettingsAggregateArgs> = z.object({ orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), _count: z.union([ z.literal(true), TelnyxSettingsCountAggregateInputObjectSchema ]).optional(), _min: TelnyxSettingsMinAggregateInputObjectSchema.optional(), _max: TelnyxSettingsMaxAggregateInputObjectSchema.optional(), _avg: TelnyxSettingsAvgAggregateInputObjectSchema.optional(), _sum: TelnyxSettingsSumAggregateInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsAggregateArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsAggregateZodSchema = z.object({ orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), _count: z.union([ z.literal(true), TelnyxSettingsCountAggregateInputObjectSchema ]).optional(), _min: TelnyxSettingsMinAggregateInputObjectSchema.optional(), _max: TelnyxSettingsMaxAggregateInputObjectSchema.optional(), _avg: TelnyxSettingsAvgAggregateInputObjectSchema.optional(), _sum: TelnyxSettingsSumAggregateInputObjectSchema.optional() }).strict();
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsOrderByWithRelationInputObjectSchema as TelnyxSettingsOrderByWithRelationInputObjectSchema } from './objects/TelnyxSettingsOrderByWithRelationInput.schema';
|
||||||
|
import { TelnyxSettingsWhereInputObjectSchema as TelnyxSettingsWhereInputObjectSchema } from './objects/TelnyxSettingsWhereInput.schema';
|
||||||
|
import { TelnyxSettingsWhereUniqueInputObjectSchema as TelnyxSettingsWhereUniqueInputObjectSchema } from './objects/TelnyxSettingsWhereUniqueInput.schema';
|
||||||
|
import { TelnyxSettingsCountAggregateInputObjectSchema as TelnyxSettingsCountAggregateInputObjectSchema } from './objects/TelnyxSettingsCountAggregateInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsCountSchema: z.ZodType<Prisma.TelnyxSettingsCountArgs> = z.object({ orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), select: z.union([ z.literal(true), TelnyxSettingsCountAggregateInputObjectSchema ]).optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsCountArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsCountZodSchema = z.object({ orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), select: z.union([ z.literal(true), TelnyxSettingsCountAggregateInputObjectSchema ]).optional() }).strict();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsSelectObjectSchema as TelnyxSettingsSelectObjectSchema } from './objects/TelnyxSettingsSelect.schema';
|
||||||
|
import { TelnyxSettingsCreateManyInputObjectSchema as TelnyxSettingsCreateManyInputObjectSchema } from './objects/TelnyxSettingsCreateManyInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsCreateManyAndReturnSchema: z.ZodType<Prisma.TelnyxSettingsCreateManyAndReturnArgs> = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), data: z.union([ TelnyxSettingsCreateManyInputObjectSchema, z.array(TelnyxSettingsCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsCreateManyAndReturnArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsCreateManyAndReturnZodSchema = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), data: z.union([ TelnyxSettingsCreateManyInputObjectSchema, z.array(TelnyxSettingsCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict();
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsCreateManyInputObjectSchema as TelnyxSettingsCreateManyInputObjectSchema } from './objects/TelnyxSettingsCreateManyInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsCreateManySchema: z.ZodType<Prisma.TelnyxSettingsCreateManyArgs> = z.object({ data: z.union([ TelnyxSettingsCreateManyInputObjectSchema, z.array(TelnyxSettingsCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsCreateManyArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsCreateManyZodSchema = z.object({ data: z.union([ TelnyxSettingsCreateManyInputObjectSchema, z.array(TelnyxSettingsCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict();
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsSelectObjectSchema as TelnyxSettingsSelectObjectSchema } from './objects/TelnyxSettingsSelect.schema';
|
||||||
|
import { TelnyxSettingsIncludeObjectSchema as TelnyxSettingsIncludeObjectSchema } from './objects/TelnyxSettingsInclude.schema';
|
||||||
|
import { TelnyxSettingsCreateInputObjectSchema as TelnyxSettingsCreateInputObjectSchema } from './objects/TelnyxSettingsCreateInput.schema';
|
||||||
|
import { TelnyxSettingsUncheckedCreateInputObjectSchema as TelnyxSettingsUncheckedCreateInputObjectSchema } from './objects/TelnyxSettingsUncheckedCreateInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsCreateOneSchema: z.ZodType<Prisma.TelnyxSettingsCreateArgs> = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), include: TelnyxSettingsIncludeObjectSchema.optional(), data: z.union([TelnyxSettingsCreateInputObjectSchema, TelnyxSettingsUncheckedCreateInputObjectSchema]) }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsCreateArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsCreateOneZodSchema = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), include: TelnyxSettingsIncludeObjectSchema.optional(), data: z.union([TelnyxSettingsCreateInputObjectSchema, TelnyxSettingsUncheckedCreateInputObjectSchema]) }).strict();
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsWhereInputObjectSchema as TelnyxSettingsWhereInputObjectSchema } from './objects/TelnyxSettingsWhereInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsDeleteManySchema: z.ZodType<Prisma.TelnyxSettingsDeleteManyArgs> = z.object({ where: TelnyxSettingsWhereInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsDeleteManyArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsDeleteManyZodSchema = z.object({ where: TelnyxSettingsWhereInputObjectSchema.optional() }).strict();
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsSelectObjectSchema as TelnyxSettingsSelectObjectSchema } from './objects/TelnyxSettingsSelect.schema';
|
||||||
|
import { TelnyxSettingsIncludeObjectSchema as TelnyxSettingsIncludeObjectSchema } from './objects/TelnyxSettingsInclude.schema';
|
||||||
|
import { TelnyxSettingsWhereUniqueInputObjectSchema as TelnyxSettingsWhereUniqueInputObjectSchema } from './objects/TelnyxSettingsWhereUniqueInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsDeleteOneSchema: z.ZodType<Prisma.TelnyxSettingsDeleteArgs> = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), include: TelnyxSettingsIncludeObjectSchema.optional(), where: TelnyxSettingsWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsDeleteArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsDeleteOneZodSchema = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), include: TelnyxSettingsIncludeObjectSchema.optional(), where: TelnyxSettingsWhereUniqueInputObjectSchema }).strict();
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
export const CommunicationProviderSchema = z.enum(['twilio', 'telnyx'])
|
||||||
|
|
||||||
|
export type CommunicationProvider = z.infer<typeof CommunicationProviderSchema>;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const CommunicationScalarFieldEnumSchema = z.enum(['id', 'patientId', 'userId', 'channel', 'direction', 'status', 'body', 'callDuration', 'twilioSid', 'createdAt'])
|
export const CommunicationScalarFieldEnumSchema = z.enum(['id', 'patientId', 'userId', 'channel', 'direction', 'status', 'provider', 'body', 'callDuration', 'twilioSid', 'createdAt'])
|
||||||
|
|
||||||
export type CommunicationScalarFieldEnum = z.infer<typeof CommunicationScalarFieldEnumSchema>;
|
export type CommunicationScalarFieldEnum = z.infer<typeof CommunicationScalarFieldEnumSchema>;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
export const TelnyxSettingsScalarFieldEnumSchema = z.enum(['id', 'userId', 'apiKey', 'accountSid', 'phoneNumber', 'texmlApplicationId', 'messagingProfileId', 'webhookPublicKey', 'greetingMessage'])
|
||||||
|
|
||||||
|
export type TelnyxSettingsScalarFieldEnum = z.infer<typeof TelnyxSettingsScalarFieldEnumSchema>;
|
||||||
@@ -16,6 +16,7 @@ export const CommunicationFindFirstSelectSchema: z.ZodType<Prisma.CommunicationS
|
|||||||
channel: z.boolean().optional(),
|
channel: z.boolean().optional(),
|
||||||
direction: z.boolean().optional(),
|
direction: z.boolean().optional(),
|
||||||
status: z.boolean().optional(),
|
status: z.boolean().optional(),
|
||||||
|
provider: z.boolean().optional(),
|
||||||
body: z.boolean().optional(),
|
body: z.boolean().optional(),
|
||||||
callDuration: z.boolean().optional(),
|
callDuration: z.boolean().optional(),
|
||||||
twilioSid: z.boolean().optional(),
|
twilioSid: z.boolean().optional(),
|
||||||
@@ -31,6 +32,7 @@ export const CommunicationFindFirstSelectZodSchema = z.object({
|
|||||||
channel: z.boolean().optional(),
|
channel: z.boolean().optional(),
|
||||||
direction: z.boolean().optional(),
|
direction: z.boolean().optional(),
|
||||||
status: z.boolean().optional(),
|
status: z.boolean().optional(),
|
||||||
|
provider: z.boolean().optional(),
|
||||||
body: z.boolean().optional(),
|
body: z.boolean().optional(),
|
||||||
callDuration: z.boolean().optional(),
|
callDuration: z.boolean().optional(),
|
||||||
twilioSid: z.boolean().optional(),
|
twilioSid: z.boolean().optional(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const CommunicationFindFirstOrThrowSelectSchema: z.ZodType<Prisma.Communi
|
|||||||
channel: z.boolean().optional(),
|
channel: z.boolean().optional(),
|
||||||
direction: z.boolean().optional(),
|
direction: z.boolean().optional(),
|
||||||
status: z.boolean().optional(),
|
status: z.boolean().optional(),
|
||||||
|
provider: z.boolean().optional(),
|
||||||
body: z.boolean().optional(),
|
body: z.boolean().optional(),
|
||||||
callDuration: z.boolean().optional(),
|
callDuration: z.boolean().optional(),
|
||||||
twilioSid: z.boolean().optional(),
|
twilioSid: z.boolean().optional(),
|
||||||
@@ -31,6 +32,7 @@ export const CommunicationFindFirstOrThrowSelectZodSchema = z.object({
|
|||||||
channel: z.boolean().optional(),
|
channel: z.boolean().optional(),
|
||||||
direction: z.boolean().optional(),
|
direction: z.boolean().optional(),
|
||||||
status: z.boolean().optional(),
|
status: z.boolean().optional(),
|
||||||
|
provider: z.boolean().optional(),
|
||||||
body: z.boolean().optional(),
|
body: z.boolean().optional(),
|
||||||
callDuration: z.boolean().optional(),
|
callDuration: z.boolean().optional(),
|
||||||
twilioSid: z.boolean().optional(),
|
twilioSid: z.boolean().optional(),
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsIncludeObjectSchema as TelnyxSettingsIncludeObjectSchema } from './objects/TelnyxSettingsInclude.schema';
|
||||||
|
import { TelnyxSettingsOrderByWithRelationInputObjectSchema as TelnyxSettingsOrderByWithRelationInputObjectSchema } from './objects/TelnyxSettingsOrderByWithRelationInput.schema';
|
||||||
|
import { TelnyxSettingsWhereInputObjectSchema as TelnyxSettingsWhereInputObjectSchema } from './objects/TelnyxSettingsWhereInput.schema';
|
||||||
|
import { TelnyxSettingsWhereUniqueInputObjectSchema as TelnyxSettingsWhereUniqueInputObjectSchema } from './objects/TelnyxSettingsWhereUniqueInput.schema';
|
||||||
|
import { TelnyxSettingsScalarFieldEnumSchema } from './enums/TelnyxSettingsScalarFieldEnum.schema';
|
||||||
|
|
||||||
|
// Select schema needs to be in file to prevent circular imports
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindFirstOrThrowSelectSchema: z.ZodType<Prisma.TelnyxSettingsSelect> = z.object({
|
||||||
|
id: z.boolean().optional(),
|
||||||
|
userId: z.boolean().optional(),
|
||||||
|
apiKey: z.boolean().optional(),
|
||||||
|
accountSid: z.boolean().optional(),
|
||||||
|
phoneNumber: z.boolean().optional(),
|
||||||
|
texmlApplicationId: z.boolean().optional(),
|
||||||
|
messagingProfileId: z.boolean().optional(),
|
||||||
|
webhookPublicKey: z.boolean().optional(),
|
||||||
|
greetingMessage: z.boolean().optional(),
|
||||||
|
user: z.boolean().optional()
|
||||||
|
}).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsSelect>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindFirstOrThrowSelectZodSchema = z.object({
|
||||||
|
id: z.boolean().optional(),
|
||||||
|
userId: z.boolean().optional(),
|
||||||
|
apiKey: z.boolean().optional(),
|
||||||
|
accountSid: z.boolean().optional(),
|
||||||
|
phoneNumber: z.boolean().optional(),
|
||||||
|
texmlApplicationId: z.boolean().optional(),
|
||||||
|
messagingProfileId: z.boolean().optional(),
|
||||||
|
webhookPublicKey: z.boolean().optional(),
|
||||||
|
greetingMessage: z.boolean().optional(),
|
||||||
|
user: z.boolean().optional()
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindFirstOrThrowSchema: z.ZodType<Prisma.TelnyxSettingsFindFirstOrThrowArgs> = z.object({ select: TelnyxSettingsFindFirstOrThrowSelectSchema.optional(), include: z.lazy(() => TelnyxSettingsIncludeObjectSchema.optional()), orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([TelnyxSettingsScalarFieldEnumSchema, TelnyxSettingsScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsFindFirstOrThrowArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindFirstOrThrowZodSchema = z.object({ select: TelnyxSettingsFindFirstOrThrowSelectSchema.optional(), include: z.lazy(() => TelnyxSettingsIncludeObjectSchema.optional()), orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([TelnyxSettingsScalarFieldEnumSchema, TelnyxSettingsScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||||
@@ -35,6 +35,7 @@ export const UserFindFirstOrThrowSelectSchema: z.ZodType<Prisma.UserSelect> = z.
|
|||||||
cloudFiles: z.boolean().optional(),
|
cloudFiles: z.boolean().optional(),
|
||||||
communications: z.boolean().optional(),
|
communications: z.boolean().optional(),
|
||||||
twilioSettings: z.boolean().optional(),
|
twilioSettings: z.boolean().optional(),
|
||||||
|
telnyxSettings: z.boolean().optional(),
|
||||||
aiSettings: z.boolean().optional(),
|
aiSettings: z.boolean().optional(),
|
||||||
officeHours: z.boolean().optional(),
|
officeHours: z.boolean().optional(),
|
||||||
officeContact: z.boolean().optional(),
|
officeContact: z.boolean().optional(),
|
||||||
@@ -72,6 +73,7 @@ export const UserFindFirstOrThrowSelectZodSchema = z.object({
|
|||||||
cloudFiles: z.boolean().optional(),
|
cloudFiles: z.boolean().optional(),
|
||||||
communications: z.boolean().optional(),
|
communications: z.boolean().optional(),
|
||||||
twilioSettings: z.boolean().optional(),
|
twilioSettings: z.boolean().optional(),
|
||||||
|
telnyxSettings: z.boolean().optional(),
|
||||||
aiSettings: z.boolean().optional(),
|
aiSettings: z.boolean().optional(),
|
||||||
officeHours: z.boolean().optional(),
|
officeHours: z.boolean().optional(),
|
||||||
officeContact: z.boolean().optional(),
|
officeContact: z.boolean().optional(),
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsIncludeObjectSchema as TelnyxSettingsIncludeObjectSchema } from './objects/TelnyxSettingsInclude.schema';
|
||||||
|
import { TelnyxSettingsOrderByWithRelationInputObjectSchema as TelnyxSettingsOrderByWithRelationInputObjectSchema } from './objects/TelnyxSettingsOrderByWithRelationInput.schema';
|
||||||
|
import { TelnyxSettingsWhereInputObjectSchema as TelnyxSettingsWhereInputObjectSchema } from './objects/TelnyxSettingsWhereInput.schema';
|
||||||
|
import { TelnyxSettingsWhereUniqueInputObjectSchema as TelnyxSettingsWhereUniqueInputObjectSchema } from './objects/TelnyxSettingsWhereUniqueInput.schema';
|
||||||
|
import { TelnyxSettingsScalarFieldEnumSchema } from './enums/TelnyxSettingsScalarFieldEnum.schema';
|
||||||
|
|
||||||
|
// Select schema needs to be in file to prevent circular imports
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindFirstSelectSchema: z.ZodType<Prisma.TelnyxSettingsSelect> = z.object({
|
||||||
|
id: z.boolean().optional(),
|
||||||
|
userId: z.boolean().optional(),
|
||||||
|
apiKey: z.boolean().optional(),
|
||||||
|
accountSid: z.boolean().optional(),
|
||||||
|
phoneNumber: z.boolean().optional(),
|
||||||
|
texmlApplicationId: z.boolean().optional(),
|
||||||
|
messagingProfileId: z.boolean().optional(),
|
||||||
|
webhookPublicKey: z.boolean().optional(),
|
||||||
|
greetingMessage: z.boolean().optional(),
|
||||||
|
user: z.boolean().optional()
|
||||||
|
}).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsSelect>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindFirstSelectZodSchema = z.object({
|
||||||
|
id: z.boolean().optional(),
|
||||||
|
userId: z.boolean().optional(),
|
||||||
|
apiKey: z.boolean().optional(),
|
||||||
|
accountSid: z.boolean().optional(),
|
||||||
|
phoneNumber: z.boolean().optional(),
|
||||||
|
texmlApplicationId: z.boolean().optional(),
|
||||||
|
messagingProfileId: z.boolean().optional(),
|
||||||
|
webhookPublicKey: z.boolean().optional(),
|
||||||
|
greetingMessage: z.boolean().optional(),
|
||||||
|
user: z.boolean().optional()
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindFirstSchema: z.ZodType<Prisma.TelnyxSettingsFindFirstArgs> = z.object({ select: TelnyxSettingsFindFirstSelectSchema.optional(), include: z.lazy(() => TelnyxSettingsIncludeObjectSchema.optional()), orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([TelnyxSettingsScalarFieldEnumSchema, TelnyxSettingsScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsFindFirstArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindFirstZodSchema = z.object({ select: TelnyxSettingsFindFirstSelectSchema.optional(), include: z.lazy(() => TelnyxSettingsIncludeObjectSchema.optional()), orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([TelnyxSettingsScalarFieldEnumSchema, TelnyxSettingsScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||||
@@ -35,6 +35,7 @@ export const UserFindFirstSelectSchema: z.ZodType<Prisma.UserSelect> = z.object(
|
|||||||
cloudFiles: z.boolean().optional(),
|
cloudFiles: z.boolean().optional(),
|
||||||
communications: z.boolean().optional(),
|
communications: z.boolean().optional(),
|
||||||
twilioSettings: z.boolean().optional(),
|
twilioSettings: z.boolean().optional(),
|
||||||
|
telnyxSettings: z.boolean().optional(),
|
||||||
aiSettings: z.boolean().optional(),
|
aiSettings: z.boolean().optional(),
|
||||||
officeHours: z.boolean().optional(),
|
officeHours: z.boolean().optional(),
|
||||||
officeContact: z.boolean().optional(),
|
officeContact: z.boolean().optional(),
|
||||||
@@ -72,6 +73,7 @@ export const UserFindFirstSelectZodSchema = z.object({
|
|||||||
cloudFiles: z.boolean().optional(),
|
cloudFiles: z.boolean().optional(),
|
||||||
communications: z.boolean().optional(),
|
communications: z.boolean().optional(),
|
||||||
twilioSettings: z.boolean().optional(),
|
twilioSettings: z.boolean().optional(),
|
||||||
|
telnyxSettings: z.boolean().optional(),
|
||||||
aiSettings: z.boolean().optional(),
|
aiSettings: z.boolean().optional(),
|
||||||
officeHours: z.boolean().optional(),
|
officeHours: z.boolean().optional(),
|
||||||
officeContact: z.boolean().optional(),
|
officeContact: z.boolean().optional(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const CommunicationFindManySelectSchema: z.ZodType<Prisma.CommunicationSe
|
|||||||
channel: z.boolean().optional(),
|
channel: z.boolean().optional(),
|
||||||
direction: z.boolean().optional(),
|
direction: z.boolean().optional(),
|
||||||
status: z.boolean().optional(),
|
status: z.boolean().optional(),
|
||||||
|
provider: z.boolean().optional(),
|
||||||
body: z.boolean().optional(),
|
body: z.boolean().optional(),
|
||||||
callDuration: z.boolean().optional(),
|
callDuration: z.boolean().optional(),
|
||||||
twilioSid: z.boolean().optional(),
|
twilioSid: z.boolean().optional(),
|
||||||
@@ -31,6 +32,7 @@ export const CommunicationFindManySelectZodSchema = z.object({
|
|||||||
channel: z.boolean().optional(),
|
channel: z.boolean().optional(),
|
||||||
direction: z.boolean().optional(),
|
direction: z.boolean().optional(),
|
||||||
status: z.boolean().optional(),
|
status: z.boolean().optional(),
|
||||||
|
provider: z.boolean().optional(),
|
||||||
body: z.boolean().optional(),
|
body: z.boolean().optional(),
|
||||||
callDuration: z.boolean().optional(),
|
callDuration: z.boolean().optional(),
|
||||||
twilioSid: z.boolean().optional(),
|
twilioSid: z.boolean().optional(),
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsIncludeObjectSchema as TelnyxSettingsIncludeObjectSchema } from './objects/TelnyxSettingsInclude.schema';
|
||||||
|
import { TelnyxSettingsOrderByWithRelationInputObjectSchema as TelnyxSettingsOrderByWithRelationInputObjectSchema } from './objects/TelnyxSettingsOrderByWithRelationInput.schema';
|
||||||
|
import { TelnyxSettingsWhereInputObjectSchema as TelnyxSettingsWhereInputObjectSchema } from './objects/TelnyxSettingsWhereInput.schema';
|
||||||
|
import { TelnyxSettingsWhereUniqueInputObjectSchema as TelnyxSettingsWhereUniqueInputObjectSchema } from './objects/TelnyxSettingsWhereUniqueInput.schema';
|
||||||
|
import { TelnyxSettingsScalarFieldEnumSchema } from './enums/TelnyxSettingsScalarFieldEnum.schema';
|
||||||
|
|
||||||
|
// Select schema needs to be in file to prevent circular imports
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindManySelectSchema: z.ZodType<Prisma.TelnyxSettingsSelect> = z.object({
|
||||||
|
id: z.boolean().optional(),
|
||||||
|
userId: z.boolean().optional(),
|
||||||
|
apiKey: z.boolean().optional(),
|
||||||
|
accountSid: z.boolean().optional(),
|
||||||
|
phoneNumber: z.boolean().optional(),
|
||||||
|
texmlApplicationId: z.boolean().optional(),
|
||||||
|
messagingProfileId: z.boolean().optional(),
|
||||||
|
webhookPublicKey: z.boolean().optional(),
|
||||||
|
greetingMessage: z.boolean().optional(),
|
||||||
|
user: z.boolean().optional()
|
||||||
|
}).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsSelect>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindManySelectZodSchema = z.object({
|
||||||
|
id: z.boolean().optional(),
|
||||||
|
userId: z.boolean().optional(),
|
||||||
|
apiKey: z.boolean().optional(),
|
||||||
|
accountSid: z.boolean().optional(),
|
||||||
|
phoneNumber: z.boolean().optional(),
|
||||||
|
texmlApplicationId: z.boolean().optional(),
|
||||||
|
messagingProfileId: z.boolean().optional(),
|
||||||
|
webhookPublicKey: z.boolean().optional(),
|
||||||
|
greetingMessage: z.boolean().optional(),
|
||||||
|
user: z.boolean().optional()
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindManySchema: z.ZodType<Prisma.TelnyxSettingsFindManyArgs> = z.object({ select: TelnyxSettingsFindManySelectSchema.optional(), include: z.lazy(() => TelnyxSettingsIncludeObjectSchema.optional()), orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([TelnyxSettingsScalarFieldEnumSchema, TelnyxSettingsScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsFindManyArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindManyZodSchema = z.object({ select: TelnyxSettingsFindManySelectSchema.optional(), include: z.lazy(() => TelnyxSettingsIncludeObjectSchema.optional()), orderBy: z.union([TelnyxSettingsOrderByWithRelationInputObjectSchema, TelnyxSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: TelnyxSettingsWhereInputObjectSchema.optional(), cursor: TelnyxSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([TelnyxSettingsScalarFieldEnumSchema, TelnyxSettingsScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||||
@@ -35,6 +35,7 @@ export const UserFindManySelectSchema: z.ZodType<Prisma.UserSelect> = z.object({
|
|||||||
cloudFiles: z.boolean().optional(),
|
cloudFiles: z.boolean().optional(),
|
||||||
communications: z.boolean().optional(),
|
communications: z.boolean().optional(),
|
||||||
twilioSettings: z.boolean().optional(),
|
twilioSettings: z.boolean().optional(),
|
||||||
|
telnyxSettings: z.boolean().optional(),
|
||||||
aiSettings: z.boolean().optional(),
|
aiSettings: z.boolean().optional(),
|
||||||
officeHours: z.boolean().optional(),
|
officeHours: z.boolean().optional(),
|
||||||
officeContact: z.boolean().optional(),
|
officeContact: z.boolean().optional(),
|
||||||
@@ -72,6 +73,7 @@ export const UserFindManySelectZodSchema = z.object({
|
|||||||
cloudFiles: z.boolean().optional(),
|
cloudFiles: z.boolean().optional(),
|
||||||
communications: z.boolean().optional(),
|
communications: z.boolean().optional(),
|
||||||
twilioSettings: z.boolean().optional(),
|
twilioSettings: z.boolean().optional(),
|
||||||
|
telnyxSettings: z.boolean().optional(),
|
||||||
aiSettings: z.boolean().optional(),
|
aiSettings: z.boolean().optional(),
|
||||||
officeHours: z.boolean().optional(),
|
officeHours: z.boolean().optional(),
|
||||||
officeContact: z.boolean().optional(),
|
officeContact: z.boolean().optional(),
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsSelectObjectSchema as TelnyxSettingsSelectObjectSchema } from './objects/TelnyxSettingsSelect.schema';
|
||||||
|
import { TelnyxSettingsIncludeObjectSchema as TelnyxSettingsIncludeObjectSchema } from './objects/TelnyxSettingsInclude.schema';
|
||||||
|
import { TelnyxSettingsWhereUniqueInputObjectSchema as TelnyxSettingsWhereUniqueInputObjectSchema } from './objects/TelnyxSettingsWhereUniqueInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindUniqueOrThrowSchema: z.ZodType<Prisma.TelnyxSettingsFindUniqueOrThrowArgs> = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), include: TelnyxSettingsIncludeObjectSchema.optional(), where: TelnyxSettingsWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsFindUniqueOrThrowArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindUniqueOrThrowZodSchema = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), include: TelnyxSettingsIncludeObjectSchema.optional(), where: TelnyxSettingsWhereUniqueInputObjectSchema }).strict();
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsSelectObjectSchema as TelnyxSettingsSelectObjectSchema } from './objects/TelnyxSettingsSelect.schema';
|
||||||
|
import { TelnyxSettingsIncludeObjectSchema as TelnyxSettingsIncludeObjectSchema } from './objects/TelnyxSettingsInclude.schema';
|
||||||
|
import { TelnyxSettingsWhereUniqueInputObjectSchema as TelnyxSettingsWhereUniqueInputObjectSchema } from './objects/TelnyxSettingsWhereUniqueInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindUniqueSchema: z.ZodType<Prisma.TelnyxSettingsFindUniqueArgs> = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), include: TelnyxSettingsIncludeObjectSchema.optional(), where: TelnyxSettingsWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsFindUniqueArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsFindUniqueZodSchema = z.object({ select: TelnyxSettingsSelectObjectSchema.optional(), include: TelnyxSettingsIncludeObjectSchema.optional(), where: TelnyxSettingsWhereUniqueInputObjectSchema }).strict();
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Prisma } from '../../generated/prisma';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { TelnyxSettingsWhereInputObjectSchema as TelnyxSettingsWhereInputObjectSchema } from './objects/TelnyxSettingsWhereInput.schema';
|
||||||
|
import { TelnyxSettingsOrderByWithAggregationInputObjectSchema as TelnyxSettingsOrderByWithAggregationInputObjectSchema } from './objects/TelnyxSettingsOrderByWithAggregationInput.schema';
|
||||||
|
import { TelnyxSettingsScalarWhereWithAggregatesInputObjectSchema as TelnyxSettingsScalarWhereWithAggregatesInputObjectSchema } from './objects/TelnyxSettingsScalarWhereWithAggregatesInput.schema';
|
||||||
|
import { TelnyxSettingsScalarFieldEnumSchema } from './enums/TelnyxSettingsScalarFieldEnum.schema';
|
||||||
|
import { TelnyxSettingsCountAggregateInputObjectSchema as TelnyxSettingsCountAggregateInputObjectSchema } from './objects/TelnyxSettingsCountAggregateInput.schema';
|
||||||
|
import { TelnyxSettingsMinAggregateInputObjectSchema as TelnyxSettingsMinAggregateInputObjectSchema } from './objects/TelnyxSettingsMinAggregateInput.schema';
|
||||||
|
import { TelnyxSettingsMaxAggregateInputObjectSchema as TelnyxSettingsMaxAggregateInputObjectSchema } from './objects/TelnyxSettingsMaxAggregateInput.schema';
|
||||||
|
import { TelnyxSettingsAvgAggregateInputObjectSchema as TelnyxSettingsAvgAggregateInputObjectSchema } from './objects/TelnyxSettingsAvgAggregateInput.schema';
|
||||||
|
import { TelnyxSettingsSumAggregateInputObjectSchema as TelnyxSettingsSumAggregateInputObjectSchema } from './objects/TelnyxSettingsSumAggregateInput.schema';
|
||||||
|
|
||||||
|
export const TelnyxSettingsGroupBySchema: z.ZodType<Prisma.TelnyxSettingsGroupByArgs> = z.object({ where: TelnyxSettingsWhereInputObjectSchema.optional(), orderBy: z.union([TelnyxSettingsOrderByWithAggregationInputObjectSchema, TelnyxSettingsOrderByWithAggregationInputObjectSchema.array()]).optional(), having: TelnyxSettingsScalarWhereWithAggregatesInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), by: z.array(TelnyxSettingsScalarFieldEnumSchema), _count: z.union([ z.literal(true), TelnyxSettingsCountAggregateInputObjectSchema ]).optional(), _min: TelnyxSettingsMinAggregateInputObjectSchema.optional(), _max: TelnyxSettingsMaxAggregateInputObjectSchema.optional(), _avg: TelnyxSettingsAvgAggregateInputObjectSchema.optional(), _sum: TelnyxSettingsSumAggregateInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.TelnyxSettingsGroupByArgs>;
|
||||||
|
|
||||||
|
export const TelnyxSettingsGroupByZodSchema = z.object({ where: TelnyxSettingsWhereInputObjectSchema.optional(), orderBy: z.union([TelnyxSettingsOrderByWithAggregationInputObjectSchema, TelnyxSettingsOrderByWithAggregationInputObjectSchema.array()]).optional(), having: TelnyxSettingsScalarWhereWithAggregatesInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), by: z.array(TelnyxSettingsScalarFieldEnumSchema), _count: z.union([ z.literal(true), TelnyxSettingsCountAggregateInputObjectSchema ]).optional(), _min: TelnyxSettingsMinAggregateInputObjectSchema.optional(), _max: TelnyxSettingsMaxAggregateInputObjectSchema.optional(), _avg: TelnyxSettingsAvgAggregateInputObjectSchema.optional(), _sum: TelnyxSettingsSumAggregateInputObjectSchema.optional() }).strict();
|
||||||
@@ -25,6 +25,7 @@ export * from './enums/CloudFileChunkScalarFieldEnum.schema'
|
|||||||
export * from './enums/CommunicationScalarFieldEnum.schema'
|
export * from './enums/CommunicationScalarFieldEnum.schema'
|
||||||
export * from './enums/PatientDocumentScalarFieldEnum.schema'
|
export * from './enums/PatientDocumentScalarFieldEnum.schema'
|
||||||
export * from './enums/TwilioSettingsScalarFieldEnum.schema'
|
export * from './enums/TwilioSettingsScalarFieldEnum.schema'
|
||||||
|
export * from './enums/TelnyxSettingsScalarFieldEnum.schema'
|
||||||
export * from './enums/AiSettingsScalarFieldEnum.schema'
|
export * from './enums/AiSettingsScalarFieldEnum.schema'
|
||||||
export * from './enums/OfficeHoursScalarFieldEnum.schema'
|
export * from './enums/OfficeHoursScalarFieldEnum.schema'
|
||||||
export * from './enums/OfficeContactScalarFieldEnum.schema'
|
export * from './enums/OfficeContactScalarFieldEnum.schema'
|
||||||
@@ -53,6 +54,7 @@ export * from './enums/NotificationTypes.schema'
|
|||||||
export * from './enums/CommunicationChannel.schema'
|
export * from './enums/CommunicationChannel.schema'
|
||||||
export * from './enums/CommunicationDirection.schema'
|
export * from './enums/CommunicationDirection.schema'
|
||||||
export * from './enums/CommunicationStatus.schema'
|
export * from './enums/CommunicationStatus.schema'
|
||||||
|
export * from './enums/CommunicationProvider.schema'
|
||||||
export * from './findUniqueUser.schema'
|
export * from './findUniqueUser.schema'
|
||||||
export * from './findUniqueOrThrowUser.schema'
|
export * from './findUniqueOrThrowUser.schema'
|
||||||
export * from './findFirstUser.schema'
|
export * from './findFirstUser.schema'
|
||||||
@@ -495,6 +497,23 @@ export * from './updateManyAndReturnTwilioSettings.schema'
|
|||||||
export * from './upsertOneTwilioSettings.schema'
|
export * from './upsertOneTwilioSettings.schema'
|
||||||
export * from './aggregateTwilioSettings.schema'
|
export * from './aggregateTwilioSettings.schema'
|
||||||
export * from './groupByTwilioSettings.schema'
|
export * from './groupByTwilioSettings.schema'
|
||||||
|
export * from './findUniqueTelnyxSettings.schema'
|
||||||
|
export * from './findUniqueOrThrowTelnyxSettings.schema'
|
||||||
|
export * from './findFirstTelnyxSettings.schema'
|
||||||
|
export * from './findFirstOrThrowTelnyxSettings.schema'
|
||||||
|
export * from './findManyTelnyxSettings.schema'
|
||||||
|
export * from './countTelnyxSettings.schema'
|
||||||
|
export * from './createOneTelnyxSettings.schema'
|
||||||
|
export * from './createManyTelnyxSettings.schema'
|
||||||
|
export * from './createManyAndReturnTelnyxSettings.schema'
|
||||||
|
export * from './deleteOneTelnyxSettings.schema'
|
||||||
|
export * from './deleteManyTelnyxSettings.schema'
|
||||||
|
export * from './updateOneTelnyxSettings.schema'
|
||||||
|
export * from './updateManyTelnyxSettings.schema'
|
||||||
|
export * from './updateManyAndReturnTelnyxSettings.schema'
|
||||||
|
export * from './upsertOneTelnyxSettings.schema'
|
||||||
|
export * from './aggregateTelnyxSettings.schema'
|
||||||
|
export * from './groupByTelnyxSettings.schema'
|
||||||
export * from './findUniqueAiSettings.schema'
|
export * from './findUniqueAiSettings.schema'
|
||||||
export * from './findUniqueOrThrowAiSettings.schema'
|
export * from './findUniqueOrThrowAiSettings.schema'
|
||||||
export * from './findFirstAiSettings.schema'
|
export * from './findFirstAiSettings.schema'
|
||||||
@@ -1003,6 +1022,19 @@ export * from './results/TwilioSettingsDeleteManyResult.schema'
|
|||||||
export * from './results/TwilioSettingsAggregateResult.schema'
|
export * from './results/TwilioSettingsAggregateResult.schema'
|
||||||
export * from './results/TwilioSettingsGroupByResult.schema'
|
export * from './results/TwilioSettingsGroupByResult.schema'
|
||||||
export * from './results/TwilioSettingsCountResult.schema'
|
export * from './results/TwilioSettingsCountResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsFindUniqueResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsFindFirstResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsFindManyResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsCreateResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsCreateManyResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsUpdateResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsUpdateManyResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsUpsertResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsDeleteResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsDeleteManyResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsAggregateResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsGroupByResult.schema'
|
||||||
|
export * from './results/TelnyxSettingsCountResult.schema'
|
||||||
export * from './results/AiSettingsFindUniqueResult.schema'
|
export * from './results/AiSettingsFindUniqueResult.schema'
|
||||||
export * from './results/AiSettingsFindFirstResult.schema'
|
export * from './results/AiSettingsFindFirstResult.schema'
|
||||||
export * from './results/AiSettingsFindManyResult.schema'
|
export * from './results/AiSettingsFindManyResult.schema'
|
||||||
@@ -1161,6 +1193,7 @@ export * from './variants/pure/CloudFileChunk.pure'
|
|||||||
export * from './variants/pure/Communication.pure'
|
export * from './variants/pure/Communication.pure'
|
||||||
export * from './variants/pure/PatientDocument.pure'
|
export * from './variants/pure/PatientDocument.pure'
|
||||||
export * from './variants/pure/TwilioSettings.pure'
|
export * from './variants/pure/TwilioSettings.pure'
|
||||||
|
export * from './variants/pure/TelnyxSettings.pure'
|
||||||
export * from './variants/pure/AiSettings.pure'
|
export * from './variants/pure/AiSettings.pure'
|
||||||
export * from './variants/pure/OfficeHours.pure'
|
export * from './variants/pure/OfficeHours.pure'
|
||||||
export * from './variants/pure/OfficeContact.pure'
|
export * from './variants/pure/OfficeContact.pure'
|
||||||
@@ -1198,6 +1231,7 @@ export * from './variants/input/CloudFileChunk.input'
|
|||||||
export * from './variants/input/Communication.input'
|
export * from './variants/input/Communication.input'
|
||||||
export * from './variants/input/PatientDocument.input'
|
export * from './variants/input/PatientDocument.input'
|
||||||
export * from './variants/input/TwilioSettings.input'
|
export * from './variants/input/TwilioSettings.input'
|
||||||
|
export * from './variants/input/TelnyxSettings.input'
|
||||||
export * from './variants/input/AiSettings.input'
|
export * from './variants/input/AiSettings.input'
|
||||||
export * from './variants/input/OfficeHours.input'
|
export * from './variants/input/OfficeHours.input'
|
||||||
export * from './variants/input/OfficeContact.input'
|
export * from './variants/input/OfficeContact.input'
|
||||||
@@ -1235,6 +1269,7 @@ export * from './variants/result/CloudFileChunk.result'
|
|||||||
export * from './variants/result/Communication.result'
|
export * from './variants/result/Communication.result'
|
||||||
export * from './variants/result/PatientDocument.result'
|
export * from './variants/result/PatientDocument.result'
|
||||||
export * from './variants/result/TwilioSettings.result'
|
export * from './variants/result/TwilioSettings.result'
|
||||||
|
export * from './variants/result/TelnyxSettings.result'
|
||||||
export * from './variants/result/AiSettings.result'
|
export * from './variants/result/AiSettings.result'
|
||||||
export * from './variants/result/OfficeHours.result'
|
export * from './variants/result/OfficeHours.result'
|
||||||
export * from './variants/result/OfficeContact.result'
|
export * from './variants/result/OfficeContact.result'
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
||||||
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
||||||
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema';
|
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema';
|
||||||
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.string(),
|
procedureCode: z.string(),
|
||||||
procedureLabel: z.string().optional().nullable(),
|
procedureLabel: z.string().optional().nullable(),
|
||||||
@@ -15,7 +14,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
patientId: z.number().int(),
|
patientId: z.number().int(),
|
||||||
@@ -15,7 +14,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
appointmentId: z.number().int(),
|
appointmentId: z.number().int(),
|
||||||
@@ -16,7 +15,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
appointmentId: z.number().int(),
|
appointmentId: z.number().int(),
|
||||||
@@ -15,7 +14,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
appointmentId: z.number().int(),
|
appointmentId: z.number().int(),
|
||||||
@@ -15,7 +14,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,12 +1,11 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
||||||
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema';
|
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema';
|
||||||
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.string(),
|
procedureCode: z.string(),
|
||||||
procedureLabel: z.string().optional().nullable(),
|
procedureLabel: z.string().optional().nullable(),
|
||||||
@@ -14,7 +13,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,12 +1,11 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
||||||
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
||||||
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema'
|
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.string(),
|
procedureCode: z.string(),
|
||||||
procedureLabel: z.string().optional().nullable(),
|
procedureLabel: z.string().optional().nullable(),
|
||||||
@@ -14,7 +13,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,12 +1,11 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
||||||
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
||||||
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.string(),
|
procedureCode: z.string(),
|
||||||
procedureLabel: z.string().optional().nullable(),
|
procedureLabel: z.string().optional().nullable(),
|
||||||
@@ -14,7 +13,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFilterObjectSchema as IntFilterObjectSchema } from './IntFilter.schema';
|
import { IntFilterObjectSchema as IntFilterObjectSchema } from './IntFilter.schema';
|
||||||
import { IntNullableFilterObjectSchema as IntNullableFilterObjectSchema } from './IntNullableFilter.schema';
|
import { IntNullableFilterObjectSchema as IntNullableFilterObjectSchema } from './IntNullableFilter.schema';
|
||||||
import { StringFilterObjectSchema as StringFilterObjectSchema } from './StringFilter.schema';
|
import { StringFilterObjectSchema as StringFilterObjectSchema } from './StringFilter.schema';
|
||||||
@@ -10,8 +11,6 @@ import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
|||||||
import { DateTimeFilterObjectSchema as DateTimeFilterObjectSchema } from './DateTimeFilter.schema'
|
import { DateTimeFilterObjectSchema as DateTimeFilterObjectSchema } from './DateTimeFilter.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const appointmentprocedurescalarwhereinputSchema = z.object({
|
const appointmentprocedurescalarwhereinputSchema = z.object({
|
||||||
AND: z.union([z.lazy(() => AppointmentProcedureScalarWhereInputObjectSchema), z.lazy(() => AppointmentProcedureScalarWhereInputObjectSchema).array()]).optional(),
|
AND: z.union([z.lazy(() => AppointmentProcedureScalarWhereInputObjectSchema), z.lazy(() => AppointmentProcedureScalarWhereInputObjectSchema).array()]).optional(),
|
||||||
OR: z.lazy(() => AppointmentProcedureScalarWhereInputObjectSchema).array().optional(),
|
OR: z.lazy(() => AppointmentProcedureScalarWhereInputObjectSchema).array().optional(),
|
||||||
@@ -26,7 +25,7 @@ const appointmentprocedurescalarwhereinputSchema = z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntWithAggregatesFilterObjectSchema as IntWithAggregatesFilterObjectSchema } from './IntWithAggregatesFilter.schema';
|
import { IntWithAggregatesFilterObjectSchema as IntWithAggregatesFilterObjectSchema } from './IntWithAggregatesFilter.schema';
|
||||||
import { IntNullableWithAggregatesFilterObjectSchema as IntNullableWithAggregatesFilterObjectSchema } from './IntNullableWithAggregatesFilter.schema';
|
import { IntNullableWithAggregatesFilterObjectSchema as IntNullableWithAggregatesFilterObjectSchema } from './IntNullableWithAggregatesFilter.schema';
|
||||||
import { StringWithAggregatesFilterObjectSchema as StringWithAggregatesFilterObjectSchema } from './StringWithAggregatesFilter.schema';
|
import { StringWithAggregatesFilterObjectSchema as StringWithAggregatesFilterObjectSchema } from './StringWithAggregatesFilter.schema';
|
||||||
@@ -10,8 +11,6 @@ import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
|||||||
import { DateTimeWithAggregatesFilterObjectSchema as DateTimeWithAggregatesFilterObjectSchema } from './DateTimeWithAggregatesFilter.schema'
|
import { DateTimeWithAggregatesFilterObjectSchema as DateTimeWithAggregatesFilterObjectSchema } from './DateTimeWithAggregatesFilter.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const appointmentprocedurescalarwherewithaggregatesinputSchema = z.object({
|
const appointmentprocedurescalarwherewithaggregatesinputSchema = z.object({
|
||||||
AND: z.union([z.lazy(() => AppointmentProcedureScalarWhereWithAggregatesInputObjectSchema), z.lazy(() => AppointmentProcedureScalarWhereWithAggregatesInputObjectSchema).array()]).optional(),
|
AND: z.union([z.lazy(() => AppointmentProcedureScalarWhereWithAggregatesInputObjectSchema), z.lazy(() => AppointmentProcedureScalarWhereWithAggregatesInputObjectSchema).array()]).optional(),
|
||||||
OR: z.lazy(() => AppointmentProcedureScalarWhereWithAggregatesInputObjectSchema).array().optional(),
|
OR: z.lazy(() => AppointmentProcedureScalarWhereWithAggregatesInputObjectSchema).array().optional(),
|
||||||
@@ -26,7 +25,7 @@ const appointmentprocedurescalarwherewithaggregatesinputSchema = z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
appointmentId: z.number().int(),
|
appointmentId: z.number().int(),
|
||||||
@@ -16,7 +15,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
patientId: z.number().int(),
|
patientId: z.number().int(),
|
||||||
@@ -15,7 +14,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
appointmentId: z.number().int(),
|
appointmentId: z.number().int(),
|
||||||
@@ -15,7 +14,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
appointmentId: z.number().int(),
|
appointmentId: z.number().int(),
|
||||||
@@ -15,7 +14,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
||||||
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
@@ -23,7 +22,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
||||||
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
@@ -23,7 +22,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
||||||
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
patientId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
patientId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
@@ -22,7 +21,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
||||||
@@ -9,8 +10,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
@@ -21,7 +20,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
||||||
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
@@ -22,7 +21,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
||||||
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
patientId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
patientId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
@@ -22,7 +21,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
||||||
@@ -9,8 +10,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
@@ -21,7 +20,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
import { IntFieldUpdateOperationsInputObjectSchema as IntFieldUpdateOperationsInputObjectSchema } from './IntFieldUpdateOperationsInput.schema';
|
||||||
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
import { NullableIntFieldUpdateOperationsInputObjectSchema as NullableIntFieldUpdateOperationsInputObjectSchema } from './NullableIntFieldUpdateOperationsInput.schema';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
appointmentId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
@@ -22,7 +21,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
||||||
@@ -11,8 +12,6 @@ import { PatientUpdateOneRequiredWithoutProceduresNestedInputObjectSchema as Pat
|
|||||||
import { NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema as NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema } from './NpiProviderUpdateOneWithoutAppointmentProceduresNestedInput.schema'
|
import { NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema as NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema } from './NpiProviderUpdateOneWithoutAppointmentProceduresNestedInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
||||||
@@ -20,7 +19,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
||||||
@@ -8,8 +9,6 @@ import { EnumProcedureSourceFieldUpdateOperationsInputObjectSchema as EnumProced
|
|||||||
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
import { DateTimeFieldUpdateOperationsInputObjectSchema as DateTimeFieldUpdateOperationsInputObjectSchema } from './DateTimeFieldUpdateOperationsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
||||||
@@ -17,7 +16,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { PatientUpdateOneRequiredWithoutProceduresNestedInputObjectSchema as Pat
|
|||||||
import { NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema as NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema } from './NpiProviderUpdateOneWithoutAppointmentProceduresNestedInput.schema'
|
import { NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema as NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema } from './NpiProviderUpdateOneWithoutAppointmentProceduresNestedInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
||||||
@@ -19,7 +18,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { AppointmentUpdateOneRequiredWithoutProceduresNestedInputObjectSchema as
|
|||||||
import { PatientUpdateOneRequiredWithoutProceduresNestedInputObjectSchema as PatientUpdateOneRequiredWithoutProceduresNestedInputObjectSchema } from './PatientUpdateOneRequiredWithoutProceduresNestedInput.schema'
|
import { PatientUpdateOneRequiredWithoutProceduresNestedInputObjectSchema as PatientUpdateOneRequiredWithoutProceduresNestedInputObjectSchema } from './PatientUpdateOneRequiredWithoutProceduresNestedInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
||||||
@@ -19,7 +18,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
import { StringFieldUpdateOperationsInputObjectSchema as StringFieldUpdateOperationsInputObjectSchema } from './StringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
import { NullableStringFieldUpdateOperationsInputObjectSchema as NullableStringFieldUpdateOperationsInputObjectSchema } from './NullableStringFieldUpdateOperationsInput.schema';
|
||||||
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
import { NullableDecimalFieldUpdateOperationsInputObjectSchema as NullableDecimalFieldUpdateOperationsInputObjectSchema } from './NullableDecimalFieldUpdateOperationsInput.schema';
|
||||||
@@ -10,8 +11,6 @@ import { AppointmentUpdateOneRequiredWithoutProceduresNestedInputObjectSchema as
|
|||||||
import { NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema as NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema } from './NpiProviderUpdateOneWithoutAppointmentProceduresNestedInput.schema'
|
import { NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema as NpiProviderUpdateOneWithoutAppointmentProceduresNestedInputObjectSchema } from './NpiProviderUpdateOneWithoutAppointmentProceduresNestedInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
procedureCode: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
procedureLabel: z.union([z.string(), z.lazy(() => NullableStringFieldUpdateOperationsInputObjectSchema)]).optional().nullable(),
|
||||||
@@ -19,7 +18,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFilterObjectSchema as IntFilterObjectSchema } from './IntFilter.schema';
|
import { IntFilterObjectSchema as IntFilterObjectSchema } from './IntFilter.schema';
|
||||||
import { IntNullableFilterObjectSchema as IntNullableFilterObjectSchema } from './IntNullableFilter.schema';
|
import { IntNullableFilterObjectSchema as IntNullableFilterObjectSchema } from './IntNullableFilter.schema';
|
||||||
import { StringFilterObjectSchema as StringFilterObjectSchema } from './StringFilter.schema';
|
import { StringFilterObjectSchema as StringFilterObjectSchema } from './StringFilter.schema';
|
||||||
@@ -16,8 +17,6 @@ import { NpiProviderNullableScalarRelationFilterObjectSchema as NpiProviderNulla
|
|||||||
import { NpiProviderWhereInputObjectSchema as NpiProviderWhereInputObjectSchema } from './NpiProviderWhereInput.schema'
|
import { NpiProviderWhereInputObjectSchema as NpiProviderWhereInputObjectSchema } from './NpiProviderWhereInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const appointmentprocedurewhereinputSchema = z.object({
|
const appointmentprocedurewhereinputSchema = z.object({
|
||||||
AND: z.union([z.lazy(() => AppointmentProcedureWhereInputObjectSchema), z.lazy(() => AppointmentProcedureWhereInputObjectSchema).array()]).optional(),
|
AND: z.union([z.lazy(() => AppointmentProcedureWhereInputObjectSchema), z.lazy(() => AppointmentProcedureWhereInputObjectSchema).array()]).optional(),
|
||||||
OR: z.lazy(() => AppointmentProcedureWhereInputObjectSchema).array().optional(),
|
OR: z.lazy(() => AppointmentProcedureWhereInputObjectSchema).array().optional(),
|
||||||
@@ -32,7 +31,7 @@ const appointmentprocedurewhereinputSchema = z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { NpiProviderCreateNestedOneWithoutCommissionBatchesInputObjectSchema as NpiProviderCreateNestedOneWithoutCommissionBatchesInputObjectSchema } from './NpiProviderCreateNestedOneWithoutCommissionBatchesInput.schema';
|
import { NpiProviderCreateNestedOneWithoutCommissionBatchesInputObjectSchema as NpiProviderCreateNestedOneWithoutCommissionBatchesInputObjectSchema } from './NpiProviderCreateNestedOneWithoutCommissionBatchesInput.schema';
|
||||||
import { CommissionBatchItemCreateNestedManyWithoutCommissionBatchInputObjectSchema as CommissionBatchItemCreateNestedManyWithoutCommissionBatchInputObjectSchema } from './CommissionBatchItemCreateNestedManyWithoutCommissionBatchInput.schema'
|
import { CommissionBatchItemCreateNestedManyWithoutCommissionBatchInputObjectSchema as CommissionBatchItemCreateNestedManyWithoutCommissionBatchInputObjectSchema } from './CommissionBatchItemCreateNestedManyWithoutCommissionBatchInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
totalCollection: z.union([
|
totalCollection: z.union([
|
||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'totalCollection' must be a Decimal",
|
message: "Field 'totalCollection' must be a Decimal",
|
||||||
@@ -20,7 +19,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'commissionAmount' must be a Decimal",
|
message: "Field 'commissionAmount' must be a Decimal",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
npiProviderId: z.number().int(),
|
npiProviderId: z.number().int(),
|
||||||
@@ -11,7 +11,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'totalCollection' must be a Decimal",
|
message: "Field 'totalCollection' must be a Decimal",
|
||||||
@@ -20,7 +20,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'commissionAmount' must be a Decimal",
|
message: "Field 'commissionAmount' must be a Decimal",
|
||||||
|
|||||||
+5
-5
@@ -1,16 +1,16 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
totalCollection: z.union([
|
totalCollection: z.union([
|
||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'totalCollection' must be a Decimal",
|
message: "Field 'totalCollection' must be a Decimal",
|
||||||
@@ -19,7 +19,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'commissionAmount' must be a Decimal",
|
message: "Field 'commissionAmount' must be a Decimal",
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { NpiProviderCreateNestedOneWithoutCommissionBatchesInputObjectSchema as NpiProviderCreateNestedOneWithoutCommissionBatchesInputObjectSchema } from './NpiProviderCreateNestedOneWithoutCommissionBatchesInput.schema'
|
import { NpiProviderCreateNestedOneWithoutCommissionBatchesInputObjectSchema as NpiProviderCreateNestedOneWithoutCommissionBatchesInputObjectSchema } from './NpiProviderCreateNestedOneWithoutCommissionBatchesInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
totalCollection: z.union([
|
totalCollection: z.union([
|
||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'totalCollection' must be a Decimal",
|
message: "Field 'totalCollection' must be a Decimal",
|
||||||
@@ -19,7 +18,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'commissionAmount' must be a Decimal",
|
message: "Field 'commissionAmount' must be a Decimal",
|
||||||
|
|||||||
+4
-5
@@ -1,16 +1,15 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { CommissionBatchItemCreateNestedManyWithoutCommissionBatchInputObjectSchema as CommissionBatchItemCreateNestedManyWithoutCommissionBatchInputObjectSchema } from './CommissionBatchItemCreateNestedManyWithoutCommissionBatchInput.schema'
|
import { CommissionBatchItemCreateNestedManyWithoutCommissionBatchInputObjectSchema as CommissionBatchItemCreateNestedManyWithoutCommissionBatchInputObjectSchema } from './CommissionBatchItemCreateNestedManyWithoutCommissionBatchInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
totalCollection: z.union([
|
totalCollection: z.union([
|
||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'totalCollection' must be a Decimal",
|
message: "Field 'totalCollection' must be a Decimal",
|
||||||
@@ -19,7 +18,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'commissionAmount' must be a Decimal",
|
message: "Field 'commissionAmount' must be a Decimal",
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { CommissionBatchCreateNestedOneWithoutItemsInputObjectSchema as CommissionBatchCreateNestedOneWithoutItemsInputObjectSchema } from './CommissionBatchCreateNestedOneWithoutItemsInput.schema';
|
import { CommissionBatchCreateNestedOneWithoutItemsInputObjectSchema as CommissionBatchCreateNestedOneWithoutItemsInputObjectSchema } from './CommissionBatchCreateNestedOneWithoutItemsInput.schema';
|
||||||
import { PaymentCreateNestedOneWithoutCommissionBatchItemsInputObjectSchema as PaymentCreateNestedOneWithoutCommissionBatchItemsInputObjectSchema } from './PaymentCreateNestedOneWithoutCommissionBatchItemsInput.schema'
|
import { PaymentCreateNestedOneWithoutCommissionBatchItemsInputObjectSchema as PaymentCreateNestedOneWithoutCommissionBatchItemsInputObjectSchema } from './PaymentCreateNestedOneWithoutCommissionBatchItemsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
collectionAmount: z.union([
|
collectionAmount: z.union([
|
||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'collectionAmount' must be a Decimal",
|
message: "Field 'collectionAmount' must be a Decimal",
|
||||||
|
|||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
paymentId: z.number().int(),
|
paymentId: z.number().int(),
|
||||||
@@ -11,7 +11,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'collectionAmount' must be a Decimal",
|
message: "Field 'collectionAmount' must be a Decimal",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
commissionBatchId: z.number().int(),
|
commissionBatchId: z.number().int(),
|
||||||
@@ -12,7 +12,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'collectionAmount' must be a Decimal",
|
message: "Field 'collectionAmount' must be a Decimal",
|
||||||
|
|||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
commissionBatchId: z.number().int(),
|
commissionBatchId: z.number().int(),
|
||||||
@@ -11,7 +11,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'collectionAmount' must be a Decimal",
|
message: "Field 'collectionAmount' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,16 +1,15 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { PaymentCreateNestedOneWithoutCommissionBatchItemsInputObjectSchema as PaymentCreateNestedOneWithoutCommissionBatchItemsInputObjectSchema } from './PaymentCreateNestedOneWithoutCommissionBatchItemsInput.schema'
|
import { PaymentCreateNestedOneWithoutCommissionBatchItemsInputObjectSchema as PaymentCreateNestedOneWithoutCommissionBatchItemsInputObjectSchema } from './PaymentCreateNestedOneWithoutCommissionBatchItemsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
collectionAmount: z.union([
|
collectionAmount: z.union([
|
||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'collectionAmount' must be a Decimal",
|
message: "Field 'collectionAmount' must be a Decimal",
|
||||||
|
|||||||
+3
-4
@@ -1,16 +1,15 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { CommissionBatchCreateNestedOneWithoutItemsInputObjectSchema as CommissionBatchCreateNestedOneWithoutItemsInputObjectSchema } from './CommissionBatchCreateNestedOneWithoutItemsInput.schema'
|
import { CommissionBatchCreateNestedOneWithoutItemsInputObjectSchema as CommissionBatchCreateNestedOneWithoutItemsInputObjectSchema } from './CommissionBatchCreateNestedOneWithoutItemsInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
collectionAmount: z.union([
|
collectionAmount: z.union([
|
||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'collectionAmount' must be a Decimal",
|
message: "Field 'collectionAmount' must be a Decimal",
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { Prisma } from '../../../generated/prisma';
|
import { Prisma } from '../../../generated/prisma';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
import { IntFilterObjectSchema as IntFilterObjectSchema } from './IntFilter.schema';
|
import { IntFilterObjectSchema as IntFilterObjectSchema } from './IntFilter.schema';
|
||||||
import { DecimalFilterObjectSchema as DecimalFilterObjectSchema } from './DecimalFilter.schema'
|
import { DecimalFilterObjectSchema as DecimalFilterObjectSchema } from './DecimalFilter.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
import Decimal from "decimal.js";
|
|
||||||
const commissionbatchitemscalarwhereinputSchema = z.object({
|
const commissionbatchitemscalarwhereinputSchema = z.object({
|
||||||
AND: z.union([z.lazy(() => CommissionBatchItemScalarWhereInputObjectSchema), z.lazy(() => CommissionBatchItemScalarWhereInputObjectSchema).array()]).optional(),
|
AND: z.union([z.lazy(() => CommissionBatchItemScalarWhereInputObjectSchema), z.lazy(() => CommissionBatchItemScalarWhereInputObjectSchema).array()]).optional(),
|
||||||
OR: z.lazy(() => CommissionBatchItemScalarWhereInputObjectSchema).array().optional(),
|
OR: z.lazy(() => CommissionBatchItemScalarWhereInputObjectSchema).array().optional(),
|
||||||
@@ -17,7 +16,7 @@ const commissionbatchitemscalarwhereinputSchema = z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Prisma.Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'collectionAmount' must be a Decimal",
|
message: "Field 'collectionAmount' must be a Decimal",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user