fix: extract appointmentDate for multi_claim/batch_claim/preauth intents

The classifier prompt only told the AI that appointmentDate applies to
schedule_appointment/claim_only/check_and_claim, so a trailing date like
"all on 6/23/26" was dropped for multi-patient claims even though the
workflow already reads c.appointmentDate for those intents and silently
defaulted to today.

Also add Prisma config/seed scripts and shopping-vendor types/schema updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 22:52:48 -04:00
parent cdda91f2b4
commit 26da3394aa
10 changed files with 351 additions and 3 deletions

View File

@@ -1,4 +1,4 @@
{ {
"is_task_list_visible": true, "is_task_list_visible": true,
"active_task": "frontend#dev" "active_task": "backend#dev"
} }

View File

@@ -102,8 +102,12 @@ Intents:
e.g. "claim #13 crown for flor and claim d5212 for bian" e.g. "claim #13 crown for flor and claim d5212 for bian"
e.g. "claim perio exam for Maria and claim adult prophy for John" e.g. "claim perio exam for Maria and claim adult prophy for John"
e.g. "claim D0120 for Jane and D1110 for Bob" e.g. "claim D0120 for Jane and D1110 for Bob"
e.g. "claim post #6 for A and comp #10 DL for B, all on 6/23/26"
→ claimGroups: [{patientName:"A", procedureNames:["post #6"]}, {patientName:"B", procedureNames:["comp #10 DL"]}], appointmentDate: "2026-06-23"
Use this when each patient has their OWN distinct set of procedures. Use this when each patient has their OWN distinct set of procedures.
Put each patient+procedures group into the "claimGroups" array. Put each patient+procedures group into the "claimGroups" array.
A trailing date phrase like "all on <date>" or "on <date>" at the end of the message sets
appointmentDate for EVERY group — do not skip it just because it comes after multiple patients.
Do NOT use batch_claim for this — batch_claim is ONLY for the SAME procedures applied to ALL patients. Do NOT use batch_claim for this — batch_claim is ONLY for the SAME procedures applied to ALL patients.
- batch_check_and_claim : user provides MULTIPLE member IDs with DOBs AND wants to claim PROCEDURES for all of them - batch_check_and_claim : user provides MULTIPLE member IDs with DOBs AND wants to claim PROCEDURES for all of them
e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000, and claim perio exam and adult prophy" e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000, and claim perio exam and adult prophy"
@@ -192,9 +196,13 @@ Rules:
Extract just the name (without "Dr." prefix unless it's part of the name), omit if not mentioned Extract just the name (without "Dr." prefix unless it's part of the name), omit if not mentioned
- Keep fallbackReply to 1-2 sentences - Keep fallbackReply to 1-2 sentences
- For navigate intents, fallbackReply = "Opening the [page] page..." (e.g. "Opening the eligibility page...") - For navigate intents, fallbackReply = "Opening the [page] page..." (e.g. "Opening the eligibility page...")
- appointmentDate applies to BOTH schedule_appointment AND claim_only/check_and_claim: - appointmentDate applies to ALL claim/scheduling intents — schedule_appointment, claim_only,
check_and_claim, batch_claim, multi_claim, batch_check_and_claim, and preauth:
always set it to today's date (${today}) when the user says "today", "this visit", or similar always set it to today's date (${today}) when the user says "today", "this visit", or similar
set it to the specified date when the user mentions a date (e.g. "05/15/2026") set it to the specified date when the user mentions a date (e.g. "05/15/2026")
This applies even when the date is mentioned once at the END of the message and covers
multiple patients/groups (e.g. "...claim X for A and Y for B, all on 6/23/26" → appointmentDate
applies to every group, not just the last one mentioned)
omit it only when no date is mentioned at all (the backend will find the last appointment) omit it only when no date is mentioned at all (the backend will find the last appointment)
- IMPORTANT: Users type dates in American M/D/YYYY format (month first, then day). - IMPORTANT: Users type dates in American M/D/YYYY format (month first, then day).
e.g. "6/12/2026" means June 12 2026 (NOT December 6). "1/5/2026" means January 5 2026. e.g. "6/12/2026" means June 12 2026 (NOT December 6). "1/5/2026" means January 5 2026.

View File

@@ -0,0 +1,15 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const dotenv_1 = __importDefault(require("dotenv"));
const path_1 = __importDefault(require("path"));
const config_1 = require("prisma/config");
dotenv_1.default.config({ path: path_1.default.resolve(__dirname, ".env") });
exports.default = (0, config_1.defineConfig)({
schema: "prisma/schema.prisma",
datasource: {
url: (0, config_1.env)("DATABASE_URL"),
},
});

View File

@@ -0,0 +1,18 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const dotenv_1 = __importDefault(require("dotenv"));
const path_1 = __importDefault(require("path"));
const config_1 = require("prisma/config");
dotenv_1.default.config({ path: path_1.default.resolve(__dirname, ".env") });
exports.default = (0, config_1.defineConfig)({
schema: "schema.prisma",
datasource: {
url: (0, config_1.env)("DATABASE_URL"),
},
migrations: {
seed: "ts-node prisma/seed.ts",
},
});

View File

@@ -0,0 +1,54 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const dotenv_1 = __importDefault(require("dotenv"));
const path_1 = __importDefault(require("path"));
dotenv_1.default.config({ path: path_1.default.resolve(__dirname, ".env") });
const prisma_1 = require("../generated/prisma");
const adapter_pg_1 = require("@prisma/adapter-pg");
const bcrypt_1 = __importDefault(require("bcrypt"));
const adapter = new adapter_pg_1.PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new prisma_1.PrismaClient({ adapter });
function main() {
return __awaiter(this, void 0, void 0, function* () {
const hashedPassword = yield bcrypt_1.default.hash("123456", 10);
const admin = yield prisma.user.upsert({
where: { username: "admin" },
update: {},
create: { username: "admin", password: hashedPassword },
});
console.log("Seed complete: admin user created (username: admin, password: 123456)");
// Seed 5 default staff members — rename these to real staff names in Settings
const defaultStaff = ["A", "B", "C", "D", "E"];
for (const name of defaultStaff) {
const existing = yield prisma.staff.findFirst({
where: { userId: admin.id, name },
});
if (!existing) {
yield prisma.staff.create({
data: { userId: admin.id, name, role: "Staff" },
});
}
}
console.log("Seed complete: 5 default staff members created (A, B, C, D, E)");
});
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => __awaiter(void 0, void 0, void 0, function* () {
yield prisma.$disconnect();
}));

View File

@@ -0,0 +1,223 @@
#!/usr/bin/env ts-node
"use strict";
/**
* patch-prisma-imports.ts (SAFE)
*
* - Converts value-level imports/exports of `Prisma` -> type-only imports/exports
* (splits mixed imports).
* - Replaces runtime usages of `Prisma.Decimal` -> `Decimal`.
* - Ensures exactly one `import Decimal from "decimal.js";` per file.
* - DEDICATED: only modifies TypeScript source files (.ts/.tsx).
* - SKIPS: files under packages/db/generated/prisma (the Prisma runtime package).
*
* Usage:
* npx ts-node packages/db/scripts/patch-prisma-imports.ts
*
* Run after `prisma generate` (and make sure generated runtime .js are restored
* if they were modified — see notes below).
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const fast_glob_1 = __importDefault(require("fast-glob"));
const repoRoot = process.cwd();
const GENERATED_FRAGMENT = path_1.default.join("packages", "db", "generated", "prisma");
// Only operate on TS sources (do NOT touch .js)
const GLOBS = [
"packages/db/shared/**/*.ts",
"packages/db/shared/**/*.tsx",
"packages/db/generated/**/*.ts",
"packages/db/generated/**/*.tsx",
];
// -------------------- helpers --------------------
function isFromGeneratedPrisma(fromPath) {
// match relative imports that include generated/prisma
return (fromPath.includes("generated/prisma") ||
fromPath.includes("/generated/prisma") ||
fromPath.includes("\\generated\\prisma"));
}
function splitSpecifiers(list) {
return list
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
function buildNamedImport(specs) {
return `{ ${specs.join(", ")} }`;
}
function extractDecimalLines(src) {
const lines = src.split(/\r?\n/);
const matches = [];
const regexes = [
/^import\s+Decimal\s+from\s+['"]decimal\.js['"]\s*;?/,
/^import\s+\{\s*Decimal\s*\}\s+from\s+['"]decimal\.js['"]\s*;?/,
/^import\s+\*\s+as\s+Decimal\s+from\s+['"]decimal\.js['"]\s*;?/,
/^(const|let|var)\s+Decimal\s*=\s*require\(\s*['"]decimal\.js['"]\s*\)\s*;?/,
/^(const|let|var)\s+Decimal\s*=\s*require\(\s*['"]decimal\.js['"]\s*\)\.default\s*;?/,
];
lines.forEach((line, i) => {
for (const re of regexes) {
if (re.test(line)) {
matches.push(i);
break;
}
}
});
return { lines, matches };
}
function ensureSingleDecimalImport(src) {
const { lines, matches } = extractDecimalLines(src);
if (matches.length === 0)
return src;
// remove all matched import/require lines
// do in reverse index order to keep indices valid
matches
.slice()
.sort((a, b) => b - a)
.forEach((idx) => lines.splice(idx, 1));
let result = lines.join("\n");
// insert single canonical import if missing
if (!/import\s+Decimal\s+from\s+['"]decimal\.js['"]/.test(result)) {
const importBlockMatch = result.match(/^(?:\s*import[\s\S]*?;\r?\n)+/);
if (importBlockMatch && importBlockMatch.index !== undefined) {
const idx = importBlockMatch[0].length;
result =
result.slice(0, idx) +
`\nimport Decimal from "decimal.js";\n` +
result.slice(idx);
}
else {
result = `import Decimal from "decimal.js";\n` + result;
}
}
// collapse excessive blank lines
result = result.replace(/\n{3,}/g, "\n\n");
return result;
}
function replacePrismaDecimalRuntime(src) {
if (!/\bPrisma\.Decimal\b/.test(src))
return { out: src, changed: false };
// mask import/export-from lines so we don't accidentally change them
const placeholder = "__MASK_IMPORT_EXPORT__" + Math.random().toString(36).slice(2);
const saved = [];
const masked = src.replace(/(^\s*(?:import|export)\s+[\s\S]*?from\s+['"][^'"]+['"]\s*;?)/gm, (m) => {
saved.push(m);
return `${placeholder}${saved.length - 1}__\n`;
});
const replaced = masked.replace(/\bPrisma\.Decimal\b/g, "Decimal");
const restored = replaced.replace(new RegExp(`${placeholder}(\\d+)__\\n`, "g"), (_m, i) => saved[Number(i)] || "");
return { out: restored, changed: true };
}
// -------------------- patching logic --------------------
function patchFileContent(src, filePath) {
// safety: do not edit runtime prisma package files
const normalized = path_1.default.normalize(filePath);
if (normalized.includes(path_1.default.normalize(GENERATED_FRAGMENT))) {
// skip any files inside packages/db/generated/prisma
return { out: src, changed: false, skipped: true };
}
let out = src;
let changed = false;
// 1) Named imports
out = out.replace(/import\s+(?!type)(\{[^}]+\})\s+from\s+(['"])([^'"]+)\2\s*;?/gm, (match, specBlock, q, fromPath) => {
if (!isFromGeneratedPrisma(fromPath))
return match;
const specList = specBlock.replace(/^\{|\}$/g, "").trim();
const specs = splitSpecifiers(specList);
const prismaEntries = specs.filter((s) => /^\s*Prisma(\s+as\s+\w+)?\s*$/.test(s));
const otherEntries = specs.filter((s) => !/^\s*Prisma(\s+as\s+\w+)?\s*$/.test(s));
if (prismaEntries.length === 0)
return match;
changed = true;
let replacement = `import type ${buildNamedImport(prismaEntries)} from ${q}${fromPath}${q};`;
if (otherEntries.length > 0) {
replacement += `\nimport ${buildNamedImport(otherEntries)} from ${q}${fromPath}${q};`;
}
return replacement;
});
// 2) Named exports
out = out.replace(/export\s+(?!type)(\{[^}]+\})\s+from\s+(['"])([^'"]+)\2\s*;?/gm, (match, specBlock, q, fromPath) => {
if (!isFromGeneratedPrisma(fromPath))
return match;
const specList = specBlock.replace(/^\{|\}$/g, "").trim();
const specs = splitSpecifiers(specList);
const prismaEntries = specs.filter((s) => /^\s*Prisma(\s+as\s+\w+)?\s*$/.test(s));
const otherEntries = specs.filter((s) => !/^\s*Prisma(\s+as\s+\w+)?\s*$/.test(s));
if (prismaEntries.length === 0)
return match;
changed = true;
let replacement = `export type ${buildNamedImport(prismaEntries)} from ${q}${fromPath}${q};`;
if (otherEntries.length > 0) {
replacement += `\nexport ${buildNamedImport(otherEntries)} from ${q}${fromPath}${q};`;
}
return replacement;
});
// 3) Namespace imports
out = out.replace(/import\s+\*\s+as\s+([A-Za-z0-9_$]+)\s+from\s+(['"])([^'"]+)\2\s*;?/gm, (match, ns, q, fromPath) => {
if (!isFromGeneratedPrisma(fromPath))
return match;
changed = true;
return `import type * as ${ns} from ${q}${fromPath}${q};`;
});
// 4) Default imports
out = out.replace(/import\s+(?!type)([A-Za-z0-9_$]+)\s+from\s+(['"])([^'"]+)\2\s*;?/gm, (match, binding, q, fromPath) => {
if (!isFromGeneratedPrisma(fromPath))
return match;
changed = true;
return `import type ${binding} from ${q}${fromPath}${q};`;
});
// 5) Replace Prisma.Decimal -> Decimal safely
if (/\bPrisma\.Decimal\b/.test(out)) {
const { out: decimalOut, changed: decimalChanged } = replacePrismaDecimalRuntime(out);
out = decimalOut;
if (decimalChanged)
changed = true;
// Ensure a single Decimal import exists
out = ensureSingleDecimalImport(out);
}
return { out, changed, skipped: false };
}
// -------------------- runner --------------------
function run() {
return __awaiter(this, void 0, void 0, function* () {
const files = yield (0, fast_glob_1.default)(GLOBS, { absolute: true, cwd: repoRoot, dot: true });
if (!files || files.length === 0) {
console.warn("No files matched. Check the GLOBS patterns and run from repo root.");
return;
}
for (const file of files) {
try {
const src = fs_1.default.readFileSync(file, "utf8");
const { out, changed, skipped } = patchFileContent(src, file);
if (skipped) {
// intentionally skipped runtime-prisma files
continue;
}
if (changed && out !== src) {
fs_1.default.writeFileSync(file, out, "utf8");
console.log("patched:", path_1.default.relative(repoRoot, file));
}
}
catch (err) {
console.error("failed patching", file, err);
}
}
console.log("done.");
});
}
run().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,19 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const dir = path_1.default.resolve(__dirname, '../shared/schemas/objects');
fs_1.default.readdirSync(dir).forEach(file => {
if (!file.endsWith('.schema.ts'))
return;
const full = path_1.default.join(dir, file);
let content = fs_1.default.readFileSync(full, 'utf8');
if (content.includes('z.instanceof(Buffer)') && !content.includes("import { Buffer")) {
content = `import { Buffer } from 'buffer';\n` + content;
fs_1.default.writeFileSync(full, content);
console.log('Patched:', file);
}
});

View File

@@ -30,3 +30,4 @@ __exportStar(require("./payments-reports-types"), exports);
__exportStar(require("./patientConnection-types"), exports); __exportStar(require("./patientConnection-types"), exports);
__exportStar(require("./npiProviders-types"), exports); __exportStar(require("./npiProviders-types"), exports);
__exportStar(require("./patientDocument-types"), exports); __exportStar(require("./patientDocument-types"), exports);
__exportStar(require("./shopping-vendor-types"), exports);

View File

@@ -35,7 +35,12 @@ exports.insertPatientSchema = usedSchemas_1.PatientUncheckedCreateInputObjectSch
createdAt: true, createdAt: true,
}) })
.extend({ .extend({
insuranceId: exports.insuranceIdSchema, // enforce numeric insuranceId firstName: zod_1.z.string().optional().default(""),
lastName: zod_1.z.string().optional().default(""),
dateOfBirth: zod_1.z.preprocess((val) => (val === null || val === undefined || val === "" ? undefined : val), zod_1.z.coerce.date({ required_error: "Date of birth is required" })),
gender: zod_1.z.string().optional().nullable(),
phone: zod_1.z.string().optional().nullable(),
insuranceId: exports.insuranceIdSchema,
}); });
exports.updatePatientSchema = usedSchemas_1.PatientUncheckedCreateInputObjectSchema exports.updatePatientSchema = usedSchemas_1.PatientUncheckedCreateInputObjectSchema
.omit({ .omit({

View File

@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.insertShoppingVendorSchema = void 0;
const usedSchemas_1 = require("@repo/db/usedSchemas");
exports.insertShoppingVendorSchema = usedSchemas_1.ShoppingVendorUncheckedCreateInputObjectSchema.omit({ id: true });