Files
DentalManagement09/apps/Backend/src/storage/appointment-procedures-storage.ts
T
ffandClaude Sonnet 4.6 9d0cfe5dba feat: appointment type inference, procedure codes on cards, claim attachment fixes
- Add appointment type categories matching insurance claim form (recall, filling, pedo, dentures, implant, endo, crown, perio, extraction, ortho, consultation, emergency, other)
- Auto-infer appointment type from CDT codes with priority rules (endo > implant > crown > ...)
- typeLocked flag prevents auto-overwrite when user manually sets type
- Show appointment type label and procedure codes on schedule cards
- Background sync on /day route retroactively fixes stale appointment types
- Fix PUT /api/claims/:id to save claimFiles (previously silently dropped)
- Auto-link AppointmentFile records to ClaimFile when claim is created or updated
- Fix D5750 (denture reline) CDT range to map correctly to dentures category
- Fix typeLocked Zod rejection in appointment update route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:18:10 -04:00

205 lines
5.9 KiB
TypeScript
Executable File

import {
Appointment,
AppointmentProcedure,
InsertAppointmentProcedure,
Patient,
UpdateAppointmentProcedure,
} from "@repo/db/types";
import { prisma as db } from "@repo/db/client";
export interface AppointmentFileMeta {
filename: string;
mimeType?: string | null;
filePath?: string | null;
}
export interface IAppointmentProceduresStorage {
getByAppointmentId(appointmentId: number): Promise<AppointmentProcedure[]>;
getPrefillDataByAppointmentId(appointmentId: number): Promise<{
appointment: Appointment;
patient: Patient;
procedures: AppointmentProcedure[];
npiProviderId: number | null;
appointmentFiles: AppointmentFileMeta[];
} | null>;
saveForAppointment(params: {
appointmentId: number;
patientId: number;
npiProviderId: number | null;
procedures: Array<{
procedureCode: string;
fee?: number | null;
toothNumber?: string | null;
toothSurface?: string | null;
}>;
attachments?: AppointmentFileMeta[];
}): Promise<number>;
createProcedure(
data: InsertAppointmentProcedure
): Promise<AppointmentProcedure>;
createProceduresBulk(data: InsertAppointmentProcedure[]): Promise<number>;
updateProcedure(
id: number,
data: UpdateAppointmentProcedure
): Promise<AppointmentProcedure>;
deleteProcedure(id: number): Promise<void>;
clearByAppointmentId(appointmentId: number): Promise<void>;
getAppointmentFiles(appointmentId: number): Promise<AppointmentFileMeta[]>;
getAppointmentIdsWithProcedures(ids: number[]): Promise<Set<number>>;
getProcedureCodesByAppointmentIds(ids: number[]): Promise<Map<number, string[]>>;
}
export const appointmentProceduresStorage: IAppointmentProceduresStorage = {
async getByAppointmentId(
appointmentId: number
): Promise<AppointmentProcedure[]> {
return db.appointmentProcedure.findMany({
where: { appointmentId },
orderBy: { createdAt: "asc" },
});
},
async getPrefillDataByAppointmentId(appointmentId: number) {
const appointment = await db.appointment.findUnique({
where: { id: appointmentId },
include: {
patient: true,
procedures: {
orderBy: { createdAt: "asc" },
},
files: {
orderBy: { id: "asc" },
},
},
});
if (!appointment) {
return null;
}
const npiProviderId = appointment.procedures[0]?.npiProviderId ?? null;
return {
appointment,
patient: appointment.patient,
procedures: appointment.procedures,
npiProviderId,
appointmentFiles: (appointment.files as any[]).map((f) => ({
id: f.id,
filename: f.filename,
mimeType: f.mimeType,
filePath: f.filePath,
})),
};
},
async saveForAppointment({ appointmentId, patientId, npiProviderId, procedures, attachments }) {
await db.appointmentProcedure.deleteMany({ where: { appointmentId } });
if (attachments?.length) {
await db.appointmentFile.deleteMany({ where: { appointmentId } });
await db.appointmentFile.createMany({
data: attachments.map((a) => ({
appointmentId,
filename: a.filename,
mimeType: a.mimeType ?? null,
filePath: a.filePath ?? null,
})),
});
}
if (!procedures.length) return 0;
const result = await db.appointmentProcedure.createMany({
data: procedures.map((p) => ({
appointmentId,
patientId,
npiProviderId: npiProviderId ?? null,
procedureCode: p.procedureCode,
fee: p.fee != null ? p.fee : null,
toothNumber: p.toothNumber || null,
toothSurface: p.toothSurface || null,
source: "MANUAL" as const,
})),
});
return result.count;
},
async createProcedure(
data: InsertAppointmentProcedure
): Promise<AppointmentProcedure> {
return db.appointmentProcedure.create({
data: data as AppointmentProcedure,
});
},
async createProceduresBulk(
data: InsertAppointmentProcedure[]
): Promise<number> {
const result = await db.appointmentProcedure.createMany({
data: data as any[],
});
return result.count;
},
async updateProcedure(
id: number,
data: UpdateAppointmentProcedure
): Promise<AppointmentProcedure> {
return db.appointmentProcedure.update({
where: { id },
data: data as any,
});
},
async deleteProcedure(id: number): Promise<void> {
await db.appointmentProcedure.delete({
where: { id },
});
},
async clearByAppointmentId(appointmentId: number): Promise<void> {
await db.appointmentProcedure.deleteMany({
where: { appointmentId },
});
},
async getAppointmentIdsWithProcedures(ids: number[]): Promise<Set<number>> {
if (!ids.length) return new Set();
const rows = await db.appointmentProcedure.findMany({
where: { appointmentId: { in: ids } },
select: { appointmentId: true },
distinct: ["appointmentId"],
});
return new Set(rows.map((r: any) => r.appointmentId));
},
async getProcedureCodesByAppointmentIds(ids: number[]): Promise<Map<number, string[]>> {
if (!ids.length) return new Map();
const rows = await db.appointmentProcedure.findMany({
where: { appointmentId: { in: ids } },
select: { appointmentId: true, procedureCode: true },
orderBy: { createdAt: "asc" },
});
const map = new Map<number, string[]>();
for (const r of rows as any[]) {
if (!r.procedureCode) continue;
const list = map.get(r.appointmentId) ?? [];
list.push(r.procedureCode);
map.set(r.appointmentId, list);
}
return map;
},
async getAppointmentFiles(appointmentId: number): Promise<AppointmentFileMeta[]> {
const rows = await db.appointmentFile.findMany({
where: { appointmentId },
orderBy: { id: "asc" },
});
return rows.map((f: any) => ({
id: f.id,
filename: f.filename,
mimeType: f.mimeType,
filePath: f.filePath,
}));
},
};