From 6d0838446b09cc853515f4df24943db34e224903 Mon Sep 17 00:00:00 2001 From: Gitead Date: Wed, 29 Jul 2026 22:57:21 -0400 Subject: [PATCH] fix: cascade-delete stale ClaimFile rows and sync claim attachments Deleting/replacing an appointment attachment left an orphaned ClaimFile copy behind (auto-linked earlier by autoLinkAppointmentFiles), causing the Selenium claim automation to upload extra files beyond what's currently attached to the appointment. Claim PUT was also append-only for claimFiles, so removing an attachment in the UI never persisted. Co-Authored-By: Claude Sonnet 5 --- apps/Backend/src/routes/claims.ts | 25 ++++++++++++++++--- .../storage/appointment-procedures-storage.ts | 6 +++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 2bdefb2a..ba01515f 100755 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -1403,15 +1403,32 @@ router.put("/:id", async (req: Request, res: Response): Promise => { await syncAppointmentType(existingClaim.appointmentId, updatedCodes); } - // If new claimFiles are provided, append only ones not already attached to this claim - if (Array.isArray(req.body.claimFiles) && req.body.claimFiles.length > 0) { + // If claimFiles are provided, sync ClaimFile rows to match the submitted list exactly: + // delete rows no longer present (so removing an attachment in the UI actually persists) + // and add rows not yet attached. Any AppointmentFile still on the appointment gets + // re-linked right after by autoLinkAppointmentFiles, so current attachments aren't lost. + if (Array.isArray(req.body.claimFiles)) { const { prisma: db } = await import("@repo/db/client"); const existingFiles = await db.claimFile.findMany({ where: { claimId }, - select: { filename: true, filePath: true }, + select: { id: true, filename: true, filePath: true }, }); + const submittedKeys = new Set( + req.body.claimFiles.map( + (f: any) => `${String(f.filename || "")}::${String(f.filePath || "")}` + ) + ); + const staleIds = existingFiles + .filter((f: any) => !submittedKeys.has(`${f.filename}::${f.filePath ?? ""}`)) + .map((f: any) => f.id); + if (staleIds.length > 0) { + await db.claimFile.deleteMany({ where: { id: { in: staleIds } } }); + } + const existingKeys = new Set( - existingFiles.map((f: any) => `${f.filename}::${f.filePath ?? ""}`) + existingFiles + .filter((f: any) => !staleIds.includes(f.id)) + .map((f: any) => `${f.filename}::${f.filePath ?? ""}`) ); const seenKeys = new Set(); const newFiles = req.body.claimFiles.filter((f: any) => { diff --git a/apps/Backend/src/storage/appointment-procedures-storage.ts b/apps/Backend/src/storage/appointment-procedures-storage.ts index 9f0a7811..10e7e1e0 100755 --- a/apps/Backend/src/storage/appointment-procedures-storage.ts +++ b/apps/Backend/src/storage/appointment-procedures-storage.ts @@ -237,6 +237,12 @@ export const appointmentProceduresStorage: IAppointmentProceduresStorage = { await db.appointmentFile.delete({ where: { id: fileId } }); + // Remove any ClaimFile copies previously auto-linked from this appointment file + // so a deleted/replaced attachment doesn't linger on the claim and get re-uploaded. + await db.claimFile.deleteMany({ + where: { filename: file.filename, claim: { appointmentId } }, + }); + // Best-effort cleanup of the underlying cloud-storage file/disk copy. if (file.filePath) { const cloudFile = await db.cloudFile.findFirst({