feat(anamnesis): soft-delete AUDIT-C in its route, admit both AUDIT-C verbs at manage level, and settle the guards

This commit is contained in:
Marc-André Bombeck committed 2026-10-16 01:24:56 +02:00
1 parent 28a8737124
commit 73bb13a982
26 files changed
+275 -154

No files matched your search

+12 -3
View File
@@ -10,8 +10,9 @@ import { NextRequest } from "next/server";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { annotate } from "@/lib/logging/context";
import { removeAuditC } from "@/lib/profile/life-circumstances";
import { invalidateHealthProfileFactConsumers } from "@/lib/profile/health-facts";
type RouteParams = { params: Promise<{ id: string }> };
@@ -20,12 +21,20 @@ export const DELETE = apiHandler(
const { user } = await requireRecordAuth("manage", "profile");
const { id } = await params;
const removed = await removeAuditC(user.id, id);
if (!removed) {
// Soft delete, scoped to a live AUDIT-C entry of the record: a foreign
// id, a screener's id or an entry already removed is the same 404.
const now = new Date();
const result = await prisma.mentalHealthAssessment.updateMany({
where: { id, userId: user.id, instrument: "AUDIT_C", deletedAt: null },
data: { deletedAt: now, syncVersion: { increment: 1 } },
});
if (result.count !== 1) {
return apiError("AUDIT-C entry not found", 404, {
errorCode: "anamnesis.auditC.notFound",
});
}
await invalidateHealthProfileFactConsumers(user.id, prisma);
const removed = { id, removedAt: now.toISOString() };
await auditLog("anamnesis.audit_c.delete", {
userId: user.id,
@@ -106,6 +106,8 @@ describe("anamnesis facts API", () => {
"user-1",
"SMOKING_STATUS",
"FORMER",
expect.any(Date),
undefined,
);
expect(mocks.audit).toHaveBeenCalledOnce();
});
@@ -135,7 +137,13 @@ describe("anamnesis facts API", () => {
where: expect.objectContaining({ id: "fact-1", userId: "user-1" }),
}),
);
expect(mocks.correct).toHaveBeenCalledWith("user-1", "fact-1", "NEVER");
expect(mocks.correct).toHaveBeenCalledWith(
"user-1",
"fact-1",
"NEVER",
expect.any(Date),
undefined,
);
expect(mocks.audit).toHaveBeenCalledOnce();
});
@@ -36,6 +36,8 @@ vi.mock("@/lib/db", () => ({
familyHistoryEntry: { findMany: vi.fn() },
userHealthProfile: { findUnique: vi.fn() },
healthProfileFactRevision: { findMany: vi.fn() },
// v1.43 — the anamnesis leaf also carries the newest AUDIT-C entry.
mentalHealthAssessment: { findFirst: vi.fn(async () => null) },
user: { findUnique: vi.fn(), update: vi.fn() },
// The whole-record gate reads the session's own stamps: a sign-in a
// moment ago is the proof, on an account without a second factor.
@@ -250,7 +250,12 @@ describe("POST /api/mental-health/assessments", () => {
expect(body.data.assessment.id).toBe("mha_dup");
expect(prisma.mentalHealthAssessment.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: { userId: "user-1", externalId: "outbox-7f3c" },
// Screeners only: an AUDIT-C entry (v1.43) is never a dedup match.
where: {
userId: "user-1",
externalId: "outbox-7f3c",
instrument: { in: ["PHQ9", "GAD7", "WHO5", "SCI"] },
},
}),
);
expect(prisma.mentalHealthAssessment.create).not.toHaveBeenCalled();