From d24dfd89a904e51ed1fffb11c38d1ed21353baf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 8 Aug 2026 09:19:06 +0200 Subject: [PATCH] Drive each new visit mutation through the sharing matrix The v1.37 matrix demands an executable driver per admitted mutating handler, not just a line in the inventory: it runs the real route as a delegate, asserts the effect landed once, and asserts the audit row names the acting delegate rather than the owner. Eight admissions came with the visit API, so eight drivers land with them. The visit fixture is deliberately in the past. A planned future visit mints an appointment reminder, and a driver that asserted one effect while producing two would be checking the wrong thing. --- .../sharing-manage-handler-matrix.test.ts | 218 +++++++++++++++++- 1 file changed, 215 insertions(+), 3 deletions(-) diff --git a/tests/integration/sharing-manage-handler-matrix.test.ts b/tests/integration/sharing-manage-handler-matrix.test.ts index e5b4f70b0..b687abd98 100644 --- a/tests/integration/sharing-manage-handler-matrix.test.ts +++ b/tests/integration/sharing-manage-handler-matrix.test.ts @@ -281,7 +281,7 @@ const ROUTE_IMPORTS = import.meta.glob>( describe("the complete MANAGE handler matrix", () => { it("starts with a non-empty, exact handler inventory", () => { - expect(ADMITTED_MUTATING_HANDLERS.length).toBe(62); + expect(ADMITTED_MUTATING_HANDLERS.length).toBe(70); }); }); @@ -512,6 +512,31 @@ async function makeEpisode(userId: string) { }); } +async function makePractitioner(userId: string) { + return getPrismaClient().practitioner.create({ + data: { userId, name: "Praxis Nord", specialty: "Internal medicine" }, + }); +} + +/** + * A visit that already happened. + * + * Past rather than booked on purpose: a PLANNED future visit mints an + * appointment reminder, and the drivers below assert one effect each. Keeping + * the fixture in the past leaves the edit and delete verbs saying only what + * they are here to say. + */ +async function makeVisit(userId: string) { + return getPrismaClient().encounter.create({ + data: { + userId, + occurredAt: new Date(ISO), + status: "DONE", + kind: "ROUTINE", + }, + }); +} + async function makeFamilyHistory(userId: string) { return getPrismaClient().familyHistoryEntry.create({ data: { userId, relationship: "MOTHER", condition: "Hypertension" }, @@ -1227,6 +1252,193 @@ manageContract("POST /api/illness/episodes/[id]/restore", { )?.deletedAt === null, }); +writeEffectContract("POST /api/encounters", { + contract: ADMITTED_MUTATING_HANDLERS.find( + (entry) => entry.route === "/api/encounters" && entry.action === "POST", + )!, + prepare: async (ownerId) => ({ ownerId }), + act: async () => { + const { POST } = await import("@/app/api/encounters/route"); + return call(POST as Handler, "POST", "/api/encounters", { + occurredAt: ISO, + status: "DONE", + }); + }, + ok: 201, + auditAction: "encounter.visit.create", + applied: async ({ ownerId }) => + (await getPrismaClient().encounter.count({ + where: { userId: ownerId }, + })) === 1, +}); + +manageContract("PATCH /api/encounters/[id]", { + contract: ADMITTED_MUTATING_HANDLERS.find( + (entry) => + entry.route === "/api/encounters/[id]" && entry.action === "PATCH", + )!, + prepare: (ownerId) => makeVisit(ownerId), + act: async (row) => { + const { PATCH } = await import("@/app/api/encounters/[id]/route"); + return call( + PATCH as Handler, + "PATCH", + `/api/encounters/${row.id}`, + { kind: "SPECIALIST" }, + { id: row.id }, + ); + }, + ok: 200, + auditAction: "encounter.visit.update", + applied: async (row) => + (await getPrismaClient().encounter.findUnique({ where: { id: row.id } })) + ?.kind === "SPECIALIST", +}); + +manageContract("DELETE /api/encounters/[id]", { + contract: ADMITTED_MUTATING_HANDLERS.find( + (entry) => + entry.route === "/api/encounters/[id]" && entry.action === "DELETE", + )!, + prepare: (ownerId) => makeVisit(ownerId), + act: async (row) => { + const { DELETE } = await import("@/app/api/encounters/[id]/route"); + return call( + DELETE as Handler, + "DELETE", + `/api/encounters/${row.id}`, + undefined, + { id: row.id }, + ); + }, + ok: 200, + auditAction: "encounter.visit.delete", + applied: async (row) => + (await getPrismaClient().encounter.findUnique({ where: { id: row.id } })) + ?.deletedAt !== null, +}); + +manageContract("POST /api/encounters/[id]/restore", { + contract: ADMITTED_MUTATING_HANDLERS.find( + (entry) => entry.route === "/api/encounters/[id]/restore", + )!, + prepare: async (ownerId) => { + const row = await makeVisit(ownerId); + return getPrismaClient().encounter.update({ + where: { id: row.id }, + data: { deletedAt: new Date() }, + }); + }, + act: async (row) => { + const { POST } = await import("@/app/api/encounters/[id]/restore/route"); + return call( + POST as Handler, + "POST", + `/api/encounters/${row.id}/restore`, + {}, + { id: row.id }, + ); + }, + ok: 200, + auditAction: "encounter.visit.restore", + applied: async (row) => + (await getPrismaClient().encounter.findUnique({ where: { id: row.id } })) + ?.deletedAt === null, +}); + +writeEffectContract("POST /api/practitioners", { + contract: ADMITTED_MUTATING_HANDLERS.find( + (entry) => entry.route === "/api/practitioners" && entry.action === "POST", + )!, + prepare: async (ownerId) => ({ ownerId }), + act: async () => { + const { POST } = await import("@/app/api/practitioners/route"); + return call(POST as Handler, "POST", "/api/practitioners", { + name: "Praxis Nord", + }); + }, + ok: 201, + auditAction: "practitioner.contact.create", + applied: async ({ ownerId }) => + (await getPrismaClient().practitioner.count({ + where: { userId: ownerId }, + })) === 1, +}); + +manageContract("PATCH /api/practitioners/[id]", { + contract: ADMITTED_MUTATING_HANDLERS.find( + (entry) => + entry.route === "/api/practitioners/[id]" && entry.action === "PATCH", + )!, + prepare: (ownerId) => makePractitioner(ownerId), + act: async (row) => { + const { PATCH } = await import("@/app/api/practitioners/[id]/route"); + return call( + PATCH as Handler, + "PATCH", + `/api/practitioners/${row.id}`, + { name: "Praxis Süd" }, + { id: row.id }, + ); + }, + ok: 200, + auditAction: "practitioner.contact.update", + applied: async (row) => + (await getPrismaClient().practitioner.findUnique({ where: { id: row.id } })) + ?.name === "Praxis Süd", +}); + +manageContract("DELETE /api/practitioners/[id]", { + contract: ADMITTED_MUTATING_HANDLERS.find( + (entry) => + entry.route === "/api/practitioners/[id]" && entry.action === "DELETE", + )!, + prepare: (ownerId) => makePractitioner(ownerId), + act: async (row) => { + const { DELETE } = await import("@/app/api/practitioners/[id]/route"); + return call( + DELETE as Handler, + "DELETE", + `/api/practitioners/${row.id}`, + undefined, + { id: row.id }, + ); + }, + ok: 200, + auditAction: "practitioner.contact.delete", + applied: async (row) => + (await getPrismaClient().practitioner.findUnique({ where: { id: row.id } })) + ?.deletedAt !== null, +}); + +manageContract("POST /api/practitioners/[id]/restore", { + contract: ADMITTED_MUTATING_HANDLERS.find( + (entry) => entry.route === "/api/practitioners/[id]/restore", + )!, + prepare: async (ownerId) => { + const row = await makePractitioner(ownerId); + return getPrismaClient().practitioner.update({ + where: { id: row.id }, + data: { deletedAt: new Date() }, + }); + }, + act: async (row) => { + const { POST } = await import("@/app/api/practitioners/[id]/restore/route"); + return call( + POST as Handler, + "POST", + `/api/practitioners/${row.id}/restore`, + {}, + { id: row.id }, + ); + }, + ok: 200, + auditAction: "practitioner.contact.restore", + applied: async (row) => + (await getPrismaClient().practitioner.findUnique({ where: { id: row.id } })) + ?.deletedAt === null, +}); + manageContract("POST /api/illness/episodes/[id]/day-logs", { contract: ADMITTED_MUTATING_HANDLERS.find( (entry) => @@ -2167,13 +2379,13 @@ describe("the conditions the admissions were granted on", () => { describe("the complete MANAGE handler matrix", () => { it("registers one strict actor-and-effect driver for every admission", () => { const expected = ADMITTED_MUTATING_HANDLERS.map(matrixKey).sort(); - expect(STRICT_DRIVER_KEYS.size).toBe(62); + expect(STRICT_DRIVER_KEYS.size).toBe(70); expect([...STRICT_DRIVER_KEYS].sort()).toEqual(expected); }); it("executes every registered driver through its owned effect and actor audit", () => { const expected = ADMITTED_MUTATING_HANDLERS.map(matrixKey).sort(); - expect(REAL_EFFECT_KEYS.size).toBe(62); + expect(REAL_EFFECT_KEYS.size).toBe(70); expect([...REAL_EFFECT_KEYS].sort()).toEqual(expected); }); });