mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
A calendar feed is a standing credential whose contents end up in a
third party's calendar cloud. Putting a record's cycle dates there is
the decision of the person they are about, or of the guardian of
somebody who cannot make it, and no delegate of an adult's record,
MANAGE included.
The subscription routes resolve through requireGuardianAuth instead of
requireRecordAuth("manage", "cycle"): the owner on their own record, or
a MANAGE grant on a record carrying the managed-profile marker. Every
verb follows, the revoke too. The feed re-reads the marker on each fetch
as well as the guardian's grant, so a row minted for somebody else on an
adult's record is a 404 whatever grant stands behind it. The settings
card is offered only where the routes answer.
The routes move from the delegable and MANAGE literals to the guardian
list, and the MANAGE handler inventory is back to 93.
3038 lines
93 KiB
TypeScript
3038 lines
93 KiB
TypeScript
/**
|
|
* v1.37.0 — the MANAGE perimeter, driven as routes against real Postgres.
|
|
*
|
|
* MANAGE is the level where a delegate stops adding to somebody's record and
|
|
* starts changing it. Seventy modules answer at it; forty-three of them can
|
|
* destroy or rewrite something. The structural guard freezes WHICH routes say
|
|
* `"manage"` and, since this release, freezes the conditions each admission
|
|
* was granted on — but a frozen tag is a promise about code, not about
|
|
* behaviour, and the promises here are the kind that fail quietly: an audit
|
|
* row that names an id and nothing else, a rate bucket keyed on the wrong
|
|
* person, a navigation that spends the owner's AI budget.
|
|
*
|
|
* So this file drives the shipped exports. Same construction as
|
|
* `sharing-delegable-writes.test.ts`: the real `apiHandler`, the real
|
|
* `requireRecordAuth`, the real grant table, the real Prisma writes. The grant
|
|
* is minted through `inviteGrant` + `acceptGrant` because "a MANAGE grant
|
|
* exists" and "a MANAGE grant can be created by the flow this release ships"
|
|
* are different claims.
|
|
*
|
|
* Four legs per verb, then the conditions:
|
|
*
|
|
* 1. a manager with a live MANAGE grant changes the OWNER's record, and the
|
|
* audit row carries `userId = owner` AND `actorUserId = manager`;
|
|
* 2. the same person under a WRITE grant is refused, and nothing moved —
|
|
* asserted by re-reading the row, not by the status alone;
|
|
* 3. a caller who was never granted anything is refused the same way;
|
|
* 4. a caller who has not switched gets exactly what they always got.
|
|
*
|
|
* The conditions each get their own test, and each is written so that removing
|
|
* the implementation turns it red rather than leaving it green with a smaller
|
|
* claim: the reconstruction legs read the audit `details` and assert the
|
|
* VALUES, the scope leg reads the rate-limit table and asserts the key, and
|
|
* the generation leg asserts both halves — nothing enqueued for the manager,
|
|
* something enqueued for the owner doing the identical thing.
|
|
*/
|
|
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { NextRequest } from "next/server";
|
|
|
|
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
|
import {
|
|
ADMITTED_MUTATING_HANDLERS,
|
|
type MutatingHandlerContract,
|
|
} from "../fixtures/v137/sharing-matrix";
|
|
import { cookieJar, headerJar } from "./mock-next-headers";
|
|
import { getPrismaClient, truncateAllTables, switchSessionTo } from "./setup";
|
|
|
|
vi.mock("next/headers", async () => {
|
|
const { cookieJar, headerJar } = await import("./mock-next-headers");
|
|
return {
|
|
headers: vi.fn(async () => ({
|
|
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
|
|
})),
|
|
cookies: vi.fn(async () => ({
|
|
get: (name: string) => {
|
|
const value = cookieJar.get(name);
|
|
return value ? { name, value } : undefined;
|
|
},
|
|
set: (name: string, value: string) => {
|
|
cookieJar.set(name, value);
|
|
},
|
|
delete: (name: string) => {
|
|
cookieJar.delete(name);
|
|
},
|
|
})),
|
|
};
|
|
});
|
|
|
|
// Import admissions have a durable database effect plus an asynchronous queue
|
|
// hand-off. Keep the hand-off deterministic here so each route test can prove
|
|
// the persisted job and actor audit without requiring a running worker.
|
|
vi.mock("@/lib/jobs/boss-instance", () => ({
|
|
getGlobalBoss: () => ({ send: vi.fn().mockResolvedValue("matrix-job") }),
|
|
}));
|
|
|
|
/**
|
|
* The narrative warm, stubbed at the seam the condition is about.
|
|
*
|
|
* C5 is the condition that costs the owner money when it is wrong, and what it
|
|
* suppresses is an ENQUEUE — a pg-boss insert whose absence is invisible from
|
|
* the response. Counting the calls is the only way to see it, and the count
|
|
* has to be taken on both sides: zero for the manager is worth nothing unless
|
|
* the same request by the owner is one.
|
|
*/
|
|
const enqueueNarrativeWarm = vi.fn(async (_payload: unknown) => {});
|
|
vi.mock("@/lib/jobs/period-narrative-shared", async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<typeof import("@/lib/jobs/period-narrative-shared")>();
|
|
return {
|
|
...actual,
|
|
enqueueNarrativeWarm: (payload: unknown) => enqueueNarrativeWarm(payload),
|
|
};
|
|
});
|
|
|
|
let counter = 0;
|
|
|
|
async function makeUser(label: string) {
|
|
const suffix = `${label}-${counter++}`;
|
|
return getPrismaClient().user.create({
|
|
data: {
|
|
username: `mg-${suffix}`,
|
|
email: `mg-${suffix}@example.test`,
|
|
displayName: `MG ${suffix}`,
|
|
role: "USER",
|
|
timezone: "Europe/Berlin",
|
|
locale: "en",
|
|
},
|
|
});
|
|
}
|
|
|
|
async function signIn(userId: string) {
|
|
const session = await getPrismaClient().session.create({
|
|
data: { userId, expiresAt: new Date(Date.now() + 60_000) },
|
|
});
|
|
cookieJar.set("healthlog_session", session.id);
|
|
return session;
|
|
}
|
|
|
|
/** A live grant at the named level, accepted, the caller switched into it. */
|
|
async function switchInto(
|
|
ownerId: string,
|
|
delegateId: string,
|
|
access: "READ" | "WRITE" | "MANAGE",
|
|
) {
|
|
const { inviteGrant, acceptGrant } = await import("@/lib/sharing/grants");
|
|
const invited = await inviteGrant({
|
|
grantorId: ownerId,
|
|
granteeId: delegateId,
|
|
access,
|
|
scope: null,
|
|
});
|
|
const grant = await acceptGrant({
|
|
grantId: invited.id,
|
|
granteeId: delegateId,
|
|
});
|
|
expect(grant.access).toBe(access);
|
|
expect(grant.acceptedAt).not.toBeNull();
|
|
|
|
const session = await signIn(delegateId);
|
|
await switchSessionTo(session.id, ownerId);
|
|
return { grant, session };
|
|
}
|
|
|
|
/** A session inside a record nobody ever shared with this caller. */
|
|
async function claimWithoutGrant(ownerId: string, callerId: string) {
|
|
const session = await signIn(callerId);
|
|
await switchSessionTo(session.id, ownerId);
|
|
}
|
|
|
|
type Handler = (
|
|
request: NextRequest,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
context: { params: Promise<any> },
|
|
) => Promise<Response>;
|
|
|
|
function request(method: string, url: string, body?: unknown): NextRequest {
|
|
return new NextRequest(`http://localhost${url}`, {
|
|
method,
|
|
headers: { "content-type": "application/json" },
|
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
});
|
|
}
|
|
|
|
async function call(
|
|
handler: Handler,
|
|
method: string,
|
|
url: string,
|
|
body?: unknown,
|
|
params: Record<string, string> = {},
|
|
): Promise<Response> {
|
|
return handler(request(method, url, body), {
|
|
params: Promise.resolve(params),
|
|
});
|
|
}
|
|
|
|
function matrixKey({ handlerModule, action, level }: MutatingHandlerContract) {
|
|
return `${handlerModule}:${action}:${level}`;
|
|
}
|
|
|
|
function admittedContract(
|
|
route: string,
|
|
action: MutatingHandlerContract["action"] = "POST",
|
|
) {
|
|
const match = ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === route && entry.action === action,
|
|
);
|
|
expect(
|
|
match,
|
|
`missing frozen admission for ${action} ${route}`,
|
|
).toBeDefined();
|
|
return match!;
|
|
}
|
|
|
|
async function matrixHandler(
|
|
contract: MutatingHandlerContract,
|
|
): Promise<Handler> {
|
|
const importer = ROUTE_IMPORTS[`/src/${contract.handlerModule}`];
|
|
expect(
|
|
importer,
|
|
`missing route import for ${contract.handlerModule}`,
|
|
).toBeTypeOf("function");
|
|
|
|
const route = await (importer as () => Promise<Record<string, unknown>>)();
|
|
const handler = route[contract.action];
|
|
expect(
|
|
handler,
|
|
`${contract.handlerModule} does not export ${contract.action}`,
|
|
).toBeTypeOf("function");
|
|
return handler as Handler;
|
|
}
|
|
|
|
async function driveAdmission(
|
|
contract: MutatingHandlerContract,
|
|
params: Record<string, string> = {},
|
|
body?: unknown,
|
|
): Promise<Response> {
|
|
let url: string = contract.route;
|
|
for (const [name, value] of Object.entries(params)) {
|
|
url = url.replace(`[${name}]`, value);
|
|
}
|
|
return call(
|
|
await matrixHandler(contract),
|
|
contract.action,
|
|
url,
|
|
body,
|
|
params,
|
|
);
|
|
}
|
|
|
|
async function payload(response: Response): Promise<{
|
|
data: unknown;
|
|
error: unknown;
|
|
meta?: { errorCode?: string };
|
|
}> {
|
|
return response.json();
|
|
}
|
|
|
|
/** The stable refusal, asserted by code rather than by status alone. */
|
|
async function expectDenied(response: Response) {
|
|
expect(response.status).toBe(403);
|
|
expect((await payload(response)).meta?.errorCode).toBe(
|
|
"sharing.access.denied",
|
|
);
|
|
}
|
|
|
|
/** The audit row a verb filed, with its details already parsed. */
|
|
async function auditRow(action: string) {
|
|
const row = await getPrismaClient().auditLog.findFirst({
|
|
where: { action },
|
|
orderBy: { createdAt: "desc" },
|
|
});
|
|
expect(row, `no ${action} row was written`).not.toBeNull();
|
|
return {
|
|
row: row!,
|
|
details: JSON.parse(row!.details ?? "{}") as Record<string, unknown>,
|
|
};
|
|
}
|
|
|
|
const ISO = "2026-07-15T08:00:00.000Z";
|
|
|
|
declare global {
|
|
interface ImportMeta {
|
|
glob<T>(pattern: string): Record<string, () => Promise<T>>;
|
|
}
|
|
}
|
|
|
|
/** Every registered driver must name one frozen admission exactly once. */
|
|
const STRICT_DRIVER_KEYS = new Set<string>();
|
|
/**
|
|
* Unlike the import-smoke inventory below, this set is filled only after a
|
|
* handler has changed an owned row and filed the delegated actor in audit.
|
|
* It is the executable answer to "did this route actually do its work?".
|
|
*/
|
|
const REAL_EFFECT_KEYS = new Set<string>();
|
|
/** The drivers whose effect case started in this run, filtered or not. */
|
|
const EFFECT_CASES_STARTED = new Set<string>();
|
|
const ROUTE_IMPORTS = import.meta.glob<Record<string, unknown>>(
|
|
"/src/app/api/**/route.ts",
|
|
);
|
|
|
|
describe("the complete MANAGE handler matrix", () => {
|
|
it("starts with a non-empty, exact handler inventory", () => {
|
|
expect(ADMITTED_MUTATING_HANDLERS.length).toBe(93);
|
|
});
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* The shape of a MANAGE case */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
interface ManageCase<C> {
|
|
/** The frozen admission that this driver proves end to end. */
|
|
contract: MutatingHandlerContract;
|
|
/** Rows the verb acts on, created under the record it will change. */
|
|
prepare: (recordOwnerId: string) => Promise<C>;
|
|
/** Drive the shipped export against the current session. */
|
|
act: (ctx: C) => Promise<Response>;
|
|
/** The status a successful call answers with. */
|
|
ok: number;
|
|
/** The action the happy path files through `auditLog`. */
|
|
auditAction: string;
|
|
/** True once the verb has taken effect on the prepared rows. */
|
|
applied: (ctx: C) => Promise<boolean>;
|
|
}
|
|
|
|
function manageContract<C>(name: string, c: ManageCase<C>) {
|
|
STRICT_DRIVER_KEYS.add(matrixKey(c.contract));
|
|
describe(name, () => {
|
|
it("changes the owner's record and names the manager as actor", async () => {
|
|
EFFECT_CASES_STARTED.add(matrixKey(c.contract));
|
|
const owner = await makeUser("owner");
|
|
const manager = await makeUser("manager");
|
|
const ctx = await c.prepare(owner.id);
|
|
|
|
await switchInto(owner.id, manager.id, "MANAGE");
|
|
const response = await c.act(ctx);
|
|
expect(response.status).toBe(c.ok);
|
|
|
|
// The change landed…
|
|
expect(await c.applied(ctx)).toBe(true);
|
|
// …and the trail says who made it, under whose record.
|
|
const { row } = await auditRow(c.auditAction);
|
|
expect(row.userId).toBe(owner.id);
|
|
expect(row.actorUserId).toBe(manager.id);
|
|
REAL_EFFECT_KEYS.add(matrixKey(c.contract));
|
|
});
|
|
|
|
it("refuses a WRITE grant, and nothing moves", async () => {
|
|
const owner = await makeUser("owner");
|
|
const helper = await makeUser("helper");
|
|
const ctx = await c.prepare(owner.id);
|
|
|
|
await switchInto(owner.id, helper.id, "WRITE");
|
|
await expectDenied(await c.act(ctx));
|
|
|
|
// The mutation-check rule: assert it did NOT apply, by re-reading.
|
|
expect(await c.applied(ctx)).toBe(false);
|
|
});
|
|
|
|
it("refuses a caller who names a record they were never granted", async () => {
|
|
const owner = await makeUser("owner");
|
|
const stranger = await makeUser("stranger");
|
|
const ctx = await c.prepare(owner.id);
|
|
|
|
await claimWithoutGrant(owner.id, stranger.id);
|
|
await expectDenied(await c.act(ctx));
|
|
expect(await c.applied(ctx)).toBe(false);
|
|
});
|
|
|
|
it("is unchanged for a caller who has not switched", async () => {
|
|
const plain = await makeUser("plain");
|
|
const ctx = await c.prepare(plain.id);
|
|
await signIn(plain.id);
|
|
|
|
expect((await c.act(ctx)).status).toBe(c.ok);
|
|
expect(await c.applied(ctx)).toBe(true);
|
|
|
|
const { row } = await auditRow(c.auditAction);
|
|
expect(row.userId).toBe(plain.id);
|
|
expect(row.actorUserId).toBeNull();
|
|
});
|
|
});
|
|
}
|
|
|
|
/** The same proof shape for the small WRITE admission at the edge of MANAGE. */
|
|
function writeEffectContract<C>(name: string, c: ManageCase<C>) {
|
|
STRICT_DRIVER_KEYS.add(matrixKey(c.contract));
|
|
describe(name, () => {
|
|
it("keeps the owner's write effective", async () => {
|
|
const owner = await makeUser("owner");
|
|
const ctx = await c.prepare(owner.id);
|
|
await signIn(owner.id);
|
|
expect((await c.act(ctx)).status).toBe(c.ok);
|
|
expect(await c.applied(ctx)).toBe(true);
|
|
});
|
|
|
|
it("refuses the lower sharing level without applying it", async () => {
|
|
const owner = await makeUser("owner");
|
|
const delegate = await makeUser("denied");
|
|
const ctx = await c.prepare(owner.id);
|
|
await switchInto(owner.id, delegate.id, "READ");
|
|
await expectDenied(await c.act(ctx));
|
|
expect(await c.applied(ctx)).toBe(false);
|
|
});
|
|
|
|
it("writes for the owner and audits the WRITE actor", async () => {
|
|
EFFECT_CASES_STARTED.add(matrixKey(c.contract));
|
|
const owner = await makeUser("owner");
|
|
const delegate = await makeUser("writer");
|
|
const ctx = await c.prepare(owner.id);
|
|
await switchInto(owner.id, delegate.id, "WRITE");
|
|
expect((await c.act(ctx)).status).toBe(c.ok);
|
|
expect(await c.applied(ctx)).toBe(true);
|
|
const { row } = await auditRow(c.auditAction);
|
|
expect(row.userId).toBe(owner.id);
|
|
expect(row.actorUserId).toBe(delegate.id);
|
|
REAL_EFFECT_KEYS.add(matrixKey(c.contract));
|
|
});
|
|
|
|
it("refuses an unsanctioned acting session without applying it", async () => {
|
|
const owner = await makeUser("owner");
|
|
const stranger = await makeUser("stranger");
|
|
const ctx = await c.prepare(owner.id);
|
|
await claimWithoutGrant(owner.id, stranger.id);
|
|
await expectDenied(await c.act(ctx));
|
|
expect(await c.applied(ctx)).toBe(false);
|
|
});
|
|
});
|
|
}
|
|
|
|
type RouteDriver<C> = Omit<ManageCase<C>, "contract" | "act"> & {
|
|
route: string;
|
|
action?: MutatingHandlerContract["action"];
|
|
params?: (ctx: C) => Record<string, string>;
|
|
body?: (ctx: C) => unknown;
|
|
};
|
|
|
|
function proveManageRoute<C>(name: string, driver: RouteDriver<C>) {
|
|
const contract = admittedContract(driver.route, driver.action);
|
|
manageContract(name, {
|
|
...driver,
|
|
contract,
|
|
act: (ctx) =>
|
|
driveAdmission(contract, driver.params?.(ctx), driver.body?.(ctx)),
|
|
});
|
|
}
|
|
|
|
function proveWriteRoute<C>(name: string, driver: RouteDriver<C>) {
|
|
const contract = admittedContract(driver.route, driver.action);
|
|
writeEffectContract(name, {
|
|
...driver,
|
|
contract,
|
|
act: (ctx) =>
|
|
driveAdmission(contract, driver.params?.(ctx), driver.body?.(ctx)),
|
|
});
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
cookieJar.clear();
|
|
headerJar.clear();
|
|
enqueueNarrativeWarm.mockClear();
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Fixtures */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
async function makeMeasurement(userId: string, value = 80) {
|
|
// The timestamp moves per row: `(userId, type, measuredAt, source,
|
|
// sleepStage)` is unique, so two fixtures on the same instant collide.
|
|
const measuredAt = new Date(Date.parse(ISO) + counter++ * 60_000);
|
|
return getPrismaClient().measurement.create({
|
|
data: {
|
|
userId,
|
|
type: "WEIGHT",
|
|
value,
|
|
unit: "kg",
|
|
measuredAt,
|
|
source: "MANUAL",
|
|
},
|
|
});
|
|
}
|
|
|
|
async function makeLabResult(userId: string) {
|
|
return getPrismaClient().labResult.create({
|
|
data: {
|
|
userId,
|
|
analyte: "HbA1c",
|
|
value: 5.2,
|
|
unit: "%",
|
|
takenAt: new Date(ISO),
|
|
},
|
|
});
|
|
}
|
|
|
|
async function makeAllergy(userId: string) {
|
|
return getPrismaClient().allergy.create({
|
|
data: {
|
|
userId,
|
|
substance: "Penicillin",
|
|
category: "MEDICATION",
|
|
type: "ALLERGY",
|
|
severity: "SEVERE",
|
|
status: "ACTIVE",
|
|
onsetAt: new Date("2020-03-01T00:00:00.000Z"),
|
|
},
|
|
});
|
|
}
|
|
|
|
async function makeReminder(userId: string) {
|
|
return getPrismaClient().measurementReminder.create({
|
|
data: {
|
|
userId,
|
|
label: "Colonoscopy recall",
|
|
measurementType: null,
|
|
intervalDays: 365,
|
|
notifyHour: 9,
|
|
nextDueAt: new Date("2027-03-01T08:00:00.000Z"),
|
|
enabled: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function makeEpisode(userId: string) {
|
|
return getPrismaClient().illnessEpisode.create({
|
|
data: {
|
|
userId,
|
|
label: "Influenza",
|
|
type: "INFECTION",
|
|
onsetAt: new Date(ISO),
|
|
},
|
|
});
|
|
}
|
|
|
|
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",
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* One logged dose, catalogue-identified.
|
|
*
|
|
* `tetanus` rather than a combination on purpose: the drivers below assert one
|
|
* effect each, and a combination would satisfy several booster reminders at
|
|
* once if the fixture ever grew one. There is none here, so no reminder moves
|
|
* and the edit and delete verbs say only what they are here to say.
|
|
*/
|
|
async function makeVaccination(userId: string) {
|
|
return getPrismaClient().vaccinationRecord.create({
|
|
data: { userId, occurredAt: new Date(ISO), antigenSlug: "tetanus" },
|
|
});
|
|
}
|
|
|
|
async function makeCustomVaccine(userId: string) {
|
|
return getPrismaClient().customVaccine.create({
|
|
data: { userId, name: "Travel combo", components: ["typhoid"] },
|
|
});
|
|
}
|
|
|
|
async function makeFamilyHistory(userId: string) {
|
|
return getPrismaClient().familyHistoryEntry.create({
|
|
data: { userId, relationship: "MOTHER", condition: "Hypertension" },
|
|
});
|
|
}
|
|
|
|
async function makeMedication(userId: string, name = "Statin") {
|
|
return getPrismaClient().medication.create({
|
|
data: { userId, name, dose: "10mg" },
|
|
});
|
|
}
|
|
|
|
async function makeMedicationIntake(userId: string) {
|
|
const medication = await makeMedication(userId, "Insulin");
|
|
const event = await getPrismaClient().medicationIntakeEvent.create({
|
|
data: { userId, medicationId: medication.id, scheduledFor: new Date(ISO) },
|
|
});
|
|
return { medication, event };
|
|
}
|
|
|
|
async function makeInventoryItem(userId: string) {
|
|
const medication = await makeMedication(userId);
|
|
const item = await getPrismaClient().medicationInventoryItem.create({
|
|
data: {
|
|
userId,
|
|
medicationId: medication.id,
|
|
unitsTotal: 20,
|
|
unitsRemaining: 20,
|
|
},
|
|
});
|
|
return { medication, item };
|
|
}
|
|
|
|
async function makeScheduleRevision(userId: string) {
|
|
const medication = await makeMedication(userId);
|
|
const revision = await getPrismaClient().medicationScheduleRevision.create({
|
|
data: {
|
|
medicationId: medication.id,
|
|
validFrom: new Date(Date.now() - 3 * 86_400_000),
|
|
validUntil: new Date(Date.now() - 2 * 86_400_000),
|
|
source: "MANUAL",
|
|
payload: [{ timesOfDay: ["08:00"] }],
|
|
},
|
|
});
|
|
return { medication, revision };
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 1 — the four legs, one verb per domain family */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
manageContract("PUT /api/measurements/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/measurements/[id]" && entry.action === "PUT",
|
|
)!,
|
|
prepare: (ownerId) => makeMeasurement(ownerId),
|
|
act: async (row) => {
|
|
const { PUT } = await import("@/app/api/measurements/[id]/route");
|
|
return call(
|
|
PUT as Handler,
|
|
"PUT",
|
|
`/api/measurements/${row.id}`,
|
|
{
|
|
value: 74.5,
|
|
},
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurement.update",
|
|
applied: async (row) => {
|
|
const after = await getPrismaClient().measurement.findUnique({
|
|
where: { id: row.id },
|
|
});
|
|
return after?.value === 74.5;
|
|
},
|
|
});
|
|
|
|
manageContract("DELETE /api/measurements/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/measurements/[id]" && entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeMeasurement(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/measurements/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/measurements/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurement.delete",
|
|
applied: async (row) => {
|
|
const after = await getPrismaClient().measurement.findUnique({
|
|
where: { id: row.id },
|
|
});
|
|
return after?.deletedAt !== null;
|
|
},
|
|
});
|
|
|
|
manageContract("PUT /api/labs/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/labs/[id]" && entry.action === "PUT",
|
|
)!,
|
|
prepare: (ownerId) => makeLabResult(ownerId),
|
|
act: async (row) => {
|
|
const { PUT } = await import("@/app/api/labs/[id]/route");
|
|
return call(
|
|
PUT as Handler,
|
|
"PUT",
|
|
`/api/labs/${row.id}`,
|
|
{ value: 6.1 },
|
|
{
|
|
id: row.id,
|
|
},
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "labResult.update",
|
|
applied: async (row) => {
|
|
const after = await getPrismaClient().labResult.findUnique({
|
|
where: { id: row.id },
|
|
});
|
|
return after?.value === 6.1;
|
|
},
|
|
});
|
|
|
|
manageContract("PATCH /api/allergies/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/allergies/[id]" && entry.action === "PATCH",
|
|
)!,
|
|
prepare: (ownerId) => makeAllergy(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } = await import("@/app/api/allergies/[id]/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/allergies/${row.id}`,
|
|
{ severity: "MILD" },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "allergy.update",
|
|
applied: async (row) => {
|
|
const after = await getPrismaClient().allergy.findUnique({
|
|
where: { id: row.id },
|
|
});
|
|
return after?.severity === "MILD";
|
|
},
|
|
});
|
|
|
|
manageContract("DELETE /api/measurement-reminders/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/measurement-reminders/[id]" &&
|
|
entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeReminder(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } =
|
|
await import("@/app/api/measurement-reminders/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/measurement-reminders/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurementReminder.delete",
|
|
applied: async (row) => {
|
|
const after = await getPrismaClient().measurementReminder.findUnique({
|
|
where: { id: row.id },
|
|
});
|
|
return after === null;
|
|
},
|
|
});
|
|
|
|
manageContract("POST /api/measurement-reminders/[id]/skip", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/measurement-reminders/[id]/skip" &&
|
|
entry.action === "POST",
|
|
)!,
|
|
prepare: (ownerId) => makeReminder(ownerId),
|
|
act: async (row) => {
|
|
const { POST } =
|
|
await import("@/app/api/measurement-reminders/[id]/skip/route");
|
|
return call(
|
|
POST as Handler,
|
|
"POST",
|
|
`/api/measurement-reminders/${row.id}/skip`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurementReminder.skip",
|
|
applied: async (row) => {
|
|
const after = await getPrismaClient().measurementReminder.findUnique({
|
|
where: { id: row.id },
|
|
});
|
|
const ledger = await getPrismaClient().measurementReminderEvent.count({
|
|
where: { reminderId: row.id, kind: "SKIPPED" },
|
|
});
|
|
// A skip stamps its own column and ledger row and never completes the
|
|
// reminder: lastSatisfiedAt must stay exactly as prepared (null).
|
|
return (
|
|
after?.lastSkippedAt !== null &&
|
|
after?.lastSatisfiedAt === null &&
|
|
ledger === 1
|
|
);
|
|
},
|
|
});
|
|
|
|
manageContract("POST /api/measurement-reminders/[id]/snooze", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/measurement-reminders/[id]/snooze" &&
|
|
entry.action === "POST",
|
|
)!,
|
|
prepare: (ownerId) => makeReminder(ownerId),
|
|
act: async (row) => {
|
|
const { POST } =
|
|
await import("@/app/api/measurement-reminders/[id]/snooze/route");
|
|
return call(
|
|
POST as Handler,
|
|
"POST",
|
|
`/api/measurement-reminders/${row.id}/snooze`,
|
|
// A snooze must end in the future; a fixed date stops being one.
|
|
{
|
|
until: new Date(Date.now() + 90 * 86_400_000)
|
|
.toISOString()
|
|
.slice(0, 10),
|
|
},
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurementReminder.snooze",
|
|
applied: async (row) => {
|
|
const after = await getPrismaClient().measurementReminder.findUnique({
|
|
where: { id: row.id },
|
|
});
|
|
return after?.snoozedUntil !== null && after?.lastSatisfiedAt === null;
|
|
},
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 1a — measurements and their reminder schedules */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
writeEffectContract("POST /api/measurements", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/measurements" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId, measuredAt: new Date(ISO) }),
|
|
act: async ({ measuredAt }) => {
|
|
const { POST } = await import("@/app/api/measurements/route");
|
|
return call(POST as Handler, "POST", "/api/measurements", {
|
|
type: "WEIGHT",
|
|
value: 81,
|
|
measuredAt: measuredAt.toISOString(),
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "measurement.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().measurement.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("POST /api/measurements/bulk-delete", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/measurements/bulk-delete",
|
|
)!,
|
|
prepare: async (ownerId) => makeMeasurement(ownerId),
|
|
act: async (row) => {
|
|
const { POST } = await import("@/app/api/measurements/bulk-delete/route");
|
|
return call(POST as Handler, "POST", "/api/measurements/bulk-delete", {
|
|
ids: [row.id],
|
|
});
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurement.delete.bulk",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().measurement.findUnique({ where: { id: row.id } }))
|
|
?.deletedAt !== null,
|
|
});
|
|
|
|
manageContract("POST /api/measurements/restore", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/measurements/restore",
|
|
)!,
|
|
prepare: async (ownerId) => {
|
|
const row = await makeMeasurement(ownerId);
|
|
return getPrismaClient().measurement.update({
|
|
where: { id: row.id },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
},
|
|
act: async (row) => {
|
|
const { POST } = await import("@/app/api/measurements/restore/route");
|
|
return call(POST as Handler, "POST", "/api/measurements/restore", {
|
|
ids: [row.id],
|
|
});
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurement.restore",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().measurement.findUnique({ where: { id: row.id } }))
|
|
?.deletedAt === null,
|
|
});
|
|
|
|
manageContract("POST /api/measurement-reminders", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/measurement-reminders" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/measurement-reminders/route");
|
|
return call(POST as Handler, "POST", "/api/measurement-reminders", {
|
|
label: "Annual review",
|
|
intervalDays: 365,
|
|
notifyHour: 9,
|
|
enabled: true,
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "measurementReminder.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().measurementReminder.count({
|
|
where: { userId: ownerId, label: "Annual review" },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("PATCH /api/measurement-reminders/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/measurement-reminders/[id]" &&
|
|
entry.action === "PATCH",
|
|
)!,
|
|
prepare: (ownerId) => makeReminder(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } =
|
|
await import("@/app/api/measurement-reminders/[id]/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/measurement-reminders/${row.id}`,
|
|
{ label: "Annual review updated" },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurementReminder.update",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().measurementReminder.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.label === "Annual review updated",
|
|
});
|
|
|
|
manageContract("POST /api/measurement-reminders/[id]/complete", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/measurement-reminders/[id]/complete",
|
|
)!,
|
|
prepare: (ownerId) => makeReminder(ownerId),
|
|
act: async (row) => {
|
|
const { POST } =
|
|
await import("@/app/api/measurement-reminders/[id]/complete/route");
|
|
return call(
|
|
POST as Handler,
|
|
"POST",
|
|
`/api/measurement-reminders/${row.id}/complete`,
|
|
{},
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurementReminder.complete",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().measurementReminder.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.lastSatisfiedAt !== null,
|
|
});
|
|
|
|
manageContract("POST /api/measurement-reminders/[id]/satisfy", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/measurement-reminders/[id]/satisfy",
|
|
)!,
|
|
prepare: (ownerId) => makeReminder(ownerId),
|
|
act: async (row) => {
|
|
const { POST } =
|
|
await import("@/app/api/measurement-reminders/[id]/satisfy/route");
|
|
return call(
|
|
POST as Handler,
|
|
"POST",
|
|
`/api/measurement-reminders/${row.id}/satisfy`,
|
|
{},
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "measurementReminder.satisfy",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().measurementReminder.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.lastSatisfiedAt !== null,
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 1b — labs, catalog entries, allergies, family, and illness */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
writeEffectContract("POST /api/labs", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/labs" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/labs/route");
|
|
return call(POST as Handler, "POST", "/api/labs", {
|
|
analyte: "HbA1c",
|
|
value: 5.2,
|
|
unit: "%",
|
|
takenAt: ISO,
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "labResult.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().labResult.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("DELETE /api/labs/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/labs/[id]" && entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeLabResult(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/labs/[id]/route");
|
|
return call(DELETE as Handler, "DELETE", `/api/labs/${row.id}`, undefined, {
|
|
id: row.id,
|
|
});
|
|
},
|
|
ok: 200,
|
|
auditAction: "labResult.delete",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().labResult.findUnique({ where: { id: row.id } }))
|
|
?.deletedAt !== null,
|
|
});
|
|
|
|
manageContract("POST /api/labs/restore", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/labs/restore",
|
|
)!,
|
|
prepare: async (ownerId) => {
|
|
const row = await makeLabResult(ownerId);
|
|
return getPrismaClient().labResult.update({
|
|
where: { id: row.id },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
},
|
|
act: async (row) => {
|
|
const { POST } = await import("@/app/api/labs/restore/route");
|
|
return call(POST as Handler, "POST", "/api/labs/restore", {
|
|
ids: [row.id],
|
|
});
|
|
},
|
|
ok: 200,
|
|
auditAction: "labResult.restore",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().labResult.findUnique({ where: { id: row.id } }))
|
|
?.deletedAt === null,
|
|
});
|
|
|
|
writeEffectContract("POST /api/biomarkers", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/biomarkers",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/biomarkers/route");
|
|
return call(POST as Handler, "POST", "/api/biomarkers", {
|
|
name: "Ferritin",
|
|
unit: "µg/L",
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "biomarker.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().biomarker.count({
|
|
where: { userId: ownerId, name: "Ferritin" },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("PUT /api/biomarkers/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/biomarkers/[id]" && entry.action === "PUT",
|
|
)!,
|
|
prepare: async (userId) =>
|
|
getPrismaClient().biomarker.create({
|
|
data: { userId, name: "Ferritin", unit: "µg/L" },
|
|
}),
|
|
act: async (row) => {
|
|
const { PUT } = await import("@/app/api/biomarkers/[id]/route");
|
|
return call(
|
|
PUT as Handler,
|
|
"PUT",
|
|
`/api/biomarkers/${row.id}`,
|
|
{ unit: "ng/mL" },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "biomarker.update",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().biomarker.findUnique({ where: { id: row.id } }))
|
|
?.unit === "ng/mL",
|
|
});
|
|
|
|
manageContract("DELETE /api/biomarkers/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/biomarkers/[id]" && entry.action === "DELETE",
|
|
)!,
|
|
prepare: async (userId) =>
|
|
getPrismaClient().biomarker.create({
|
|
data: { userId, name: "Transferrin", unit: "g/L" },
|
|
}),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/biomarkers/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/biomarkers/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "biomarker.delete",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().biomarker.findUnique({
|
|
where: { id: row.id },
|
|
})) === null,
|
|
});
|
|
|
|
manageContract("POST /api/biomarkers/[id]/merge", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/biomarkers/[id]/merge",
|
|
)!,
|
|
prepare: async (userId) => {
|
|
const prisma = getPrismaClient();
|
|
const target = await prisma.biomarker.create({
|
|
data: { userId, name: "Alkaline Phosphatase", unit: "U/L" },
|
|
});
|
|
const source = await prisma.biomarker.create({
|
|
data: { userId, name: "Alk.P", unit: "U/L" },
|
|
});
|
|
await prisma.labResult.create({
|
|
data: {
|
|
userId,
|
|
biomarkerId: source.id,
|
|
analyte: "Alk.P",
|
|
value: 81,
|
|
unit: "U/L",
|
|
takenAt: new Date("2026-05-01T08:00:00.000Z"),
|
|
},
|
|
});
|
|
return { id: target.id, sourceId: source.id };
|
|
},
|
|
act: async (row) => {
|
|
const { POST } = await import("@/app/api/biomarkers/[id]/merge/route");
|
|
return call(
|
|
POST as Handler,
|
|
"POST",
|
|
`/api/biomarkers/${row.id}/merge`,
|
|
{ sourceIds: [row.sourceId] },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "labs.biomarker.merge",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().biomarker.findUnique({
|
|
where: { id: row.sourceId },
|
|
})) === null &&
|
|
(await getPrismaClient().labResult.count({
|
|
where: { biomarkerId: row.id },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("POST /api/anamnesis/audit-c", {
|
|
contract: admittedContract("/api/anamnesis/audit-c", "POST"),
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/anamnesis/audit-c/route");
|
|
return call(POST as Handler, "POST", "/api/anamnesis/audit-c", {
|
|
items: [1, 1, 0],
|
|
source: "WEB",
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "anamnesis.audit_c.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().mentalHealthAssessment.count({
|
|
where: { userId: ownerId, instrument: "AUDIT_C", deletedAt: null },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("DELETE /api/anamnesis/audit-c/[id]", {
|
|
contract: admittedContract("/api/anamnesis/audit-c/[id]", "DELETE"),
|
|
prepare: async (ownerId) =>
|
|
getPrismaClient().mentalHealthAssessment.create({
|
|
data: {
|
|
userId: ownerId,
|
|
instrument: "AUDIT_C",
|
|
locale: "en",
|
|
responsesEncrypted: Buffer.from("sealed"),
|
|
totalScore: 2,
|
|
severityBand: "unbanded",
|
|
takenAt: new Date("2026-09-01T08:00:00.000Z"),
|
|
},
|
|
select: { id: true },
|
|
}),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/anamnesis/audit-c/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/anamnesis/audit-c/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "anamnesis.audit_c.delete",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().mentalHealthAssessment.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.deletedAt != null,
|
|
});
|
|
|
|
manageContract("POST /api/allergies", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/allergies" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/allergies/route");
|
|
return call(POST as Handler, "POST", "/api/allergies", {
|
|
substance: "Pollen",
|
|
category: "ENVIRONMENT",
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "allergy.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().allergy.count({
|
|
where: { userId: ownerId, substance: "Pollen" },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("DELETE /api/allergies/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/allergies/[id]" && entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeAllergy(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/allergies/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/allergies/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "allergy.delete",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().allergy.findUnique({ where: { id: row.id } }))
|
|
?.deletedAt !== null,
|
|
});
|
|
|
|
manageContract("POST /api/family-history", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/family-history" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/family-history/route");
|
|
return call(POST as Handler, "POST", "/api/family-history", {
|
|
relationship: "MOTHER",
|
|
condition: "Hypertension",
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "family-history.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().familyHistoryEntry.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("PATCH /api/family-history/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/family-history/[id]" && entry.action === "PATCH",
|
|
)!,
|
|
prepare: (ownerId) => makeFamilyHistory(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } = await import("@/app/api/family-history/[id]/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/family-history/${row.id}`,
|
|
{ ageAtOnset: 50 },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "family-history.update",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().familyHistoryEntry.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.ageAtOnset === 50,
|
|
});
|
|
|
|
manageContract("DELETE /api/family-history/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/family-history/[id]" && entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeFamilyHistory(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/family-history/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/family-history/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "family-history.delete",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().familyHistoryEntry.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.deletedAt !== null,
|
|
});
|
|
|
|
writeEffectContract("POST /api/illness/episodes", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/illness/episodes" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/illness/episodes/route");
|
|
return call(POST as Handler, "POST", "/api/illness/episodes", {
|
|
label: "Influenza",
|
|
type: "INFECTION",
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "illness.episode.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().illnessEpisode.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("PATCH /api/illness/episodes/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/illness/episodes/[id]" && entry.action === "PATCH",
|
|
)!,
|
|
prepare: (ownerId) => makeEpisode(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } = await import("@/app/api/illness/episodes/[id]/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/illness/episodes/${row.id}`,
|
|
{ label: "Influenza A" },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "illness.episode.update",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().illnessEpisode.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.label === "Influenza A",
|
|
});
|
|
|
|
manageContract("DELETE /api/illness/episodes/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/illness/episodes/[id]" && entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeEpisode(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/illness/episodes/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/illness/episodes/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "illness.episode.delete",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().illnessEpisode.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.deletedAt !== null,
|
|
});
|
|
|
|
manageContract("PATCH /api/illness/episodes/[id]/resolve", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/illness/episodes/[id]/resolve",
|
|
)!,
|
|
prepare: (ownerId) => makeEpisode(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } =
|
|
await import("@/app/api/illness/episodes/[id]/resolve/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/illness/episodes/${row.id}/resolve`,
|
|
{},
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "illness.episode.resolve",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().illnessEpisode.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.resolvedAt !== null,
|
|
});
|
|
|
|
manageContract("POST /api/illness/episodes/[id]/restore", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/illness/episodes/[id]/restore",
|
|
)!,
|
|
prepare: async (ownerId) => {
|
|
const row = await makeEpisode(ownerId);
|
|
return getPrismaClient().illnessEpisode.update({
|
|
where: { id: row.id },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
},
|
|
act: async (row) => {
|
|
const { POST } =
|
|
await import("@/app/api/illness/episodes/[id]/restore/route");
|
|
return call(
|
|
POST as Handler,
|
|
"POST",
|
|
`/api/illness/episodes/${row.id}/restore`,
|
|
{},
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "illness.episode.restore",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().illnessEpisode.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.deletedAt === null,
|
|
});
|
|
|
|
async function makeSymptomDefinition(userId: string) {
|
|
return getPrismaClient().symptomDefinition.create({
|
|
data: {
|
|
userId,
|
|
labelEncrypted: encryptToBytes("Aura"),
|
|
icon: "Zap",
|
|
},
|
|
});
|
|
}
|
|
|
|
async function makeSymptomEvent(userId: string) {
|
|
const definition = await makeSymptomDefinition(userId);
|
|
return getPrismaClient().symptomEvent.create({
|
|
data: {
|
|
userId,
|
|
definitionId: definition.id,
|
|
occurredAt: new Date(ISO),
|
|
intensity: 6,
|
|
},
|
|
});
|
|
}
|
|
|
|
manageContract("POST /api/symptoms/definitions", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/symptoms/definitions" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/symptoms/definitions/route");
|
|
return call(POST as Handler, "POST", "/api/symptoms/definitions", {
|
|
label: "Aura",
|
|
icon: "Zap",
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "symptoms.definition.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().symptomDefinition.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("PATCH /api/symptoms/definitions/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/symptoms/definitions/[id]" &&
|
|
entry.action === "PATCH",
|
|
)!,
|
|
prepare: (ownerId) => makeSymptomDefinition(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } = await import("@/app/api/symptoms/definitions/[id]/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/symptoms/definitions/${row.id}`,
|
|
{ icon: "Brain" },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "symptoms.definition.update",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().symptomDefinition.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.icon === "Brain",
|
|
});
|
|
|
|
manageContract("DELETE /api/symptoms/definitions/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/symptoms/definitions/[id]" &&
|
|
entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeSymptomDefinition(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } =
|
|
await import("@/app/api/symptoms/definitions/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/symptoms/definitions/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "symptoms.definition.delete",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().symptomDefinition.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.isActive === false,
|
|
});
|
|
|
|
writeEffectContract("POST /api/symptoms/events", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/symptoms/events" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
definition: await makeSymptomDefinition(ownerId),
|
|
}),
|
|
act: async ({ definition }) => {
|
|
const { POST } = await import("@/app/api/symptoms/events/route");
|
|
return call(POST as Handler, "POST", "/api/symptoms/events", {
|
|
definitionId: definition.id,
|
|
intensity: 7,
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "symptoms.event.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().symptomEvent.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("PATCH /api/symptoms/events/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/symptoms/events/[id]" && entry.action === "PATCH",
|
|
)!,
|
|
prepare: (ownerId) => makeSymptomEvent(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } = await import("@/app/api/symptoms/events/[id]/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/symptoms/events/${row.id}`,
|
|
{ intensity: 9 },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "symptoms.event.update",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().symptomEvent.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.intensity === 9,
|
|
});
|
|
|
|
manageContract("DELETE /api/symptoms/events/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/symptoms/events/[id]" && entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeSymptomEvent(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/symptoms/events/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/symptoms/events/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "symptoms.event.delete",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().symptomEvent.findUnique({
|
|
where: { id: row.id },
|
|
})) === 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,
|
|
});
|
|
|
|
writeEffectContract("POST /api/vaccinations", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/vaccinations" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/vaccinations/route");
|
|
return call(POST as Handler, "POST", "/api/vaccinations", {
|
|
occurredAt: ISO,
|
|
antigenSlug: "tetanus",
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "vaccination.record.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().vaccinationRecord.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("PATCH /api/vaccinations/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/vaccinations/[id]" && entry.action === "PATCH",
|
|
)!,
|
|
prepare: (ownerId) => makeVaccination(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } = await import("@/app/api/vaccinations/[id]/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/vaccinations/${row.id}`,
|
|
{ lotNumber: "MX-2211" },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "vaccination.record.update",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().vaccinationRecord.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.lotNumber === "MX-2211",
|
|
});
|
|
|
|
manageContract("DELETE /api/vaccinations/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/vaccinations/[id]" && entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeVaccination(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/vaccinations/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/vaccinations/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "vaccination.record.delete",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().vaccinationRecord.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.deletedAt !== null,
|
|
});
|
|
|
|
manageContract("POST /api/vaccinations/[id]/restore", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) => entry.route === "/api/vaccinations/[id]/restore",
|
|
)!,
|
|
prepare: async (ownerId) => {
|
|
const row = await makeVaccination(ownerId);
|
|
return getPrismaClient().vaccinationRecord.update({
|
|
where: { id: row.id },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
},
|
|
act: async (row) => {
|
|
const { POST } = await import("@/app/api/vaccinations/[id]/restore/route");
|
|
return call(
|
|
POST as Handler,
|
|
"POST",
|
|
`/api/vaccinations/${row.id}/restore`,
|
|
{},
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "vaccination.record.restore",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().vaccinationRecord.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.deletedAt === null,
|
|
});
|
|
|
|
writeEffectContract("POST /api/vaccinations/custom", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/vaccinations/custom" && entry.action === "POST",
|
|
)!,
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
act: async () => {
|
|
const { POST } = await import("@/app/api/vaccinations/custom/route");
|
|
return call(POST as Handler, "POST", "/api/vaccinations/custom", {
|
|
name: "Cholera oral",
|
|
components: ["cholera"],
|
|
});
|
|
},
|
|
ok: 201,
|
|
auditAction: "vaccination.custom.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().customVaccine.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("PATCH /api/vaccinations/custom/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/vaccinations/custom/[id]" &&
|
|
entry.action === "PATCH",
|
|
)!,
|
|
prepare: (ownerId) => makeCustomVaccine(ownerId),
|
|
act: async (row) => {
|
|
const { PATCH } = await import("@/app/api/vaccinations/custom/[id]/route");
|
|
return call(
|
|
PATCH as Handler,
|
|
"PATCH",
|
|
`/api/vaccinations/custom/${row.id}`,
|
|
{ boosterIntervalMonths: 36 },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "vaccination.custom.update",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().customVaccine.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.boosterIntervalMonths === 36,
|
|
});
|
|
|
|
manageContract("DELETE /api/vaccinations/custom/[id]", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/vaccinations/custom/[id]" &&
|
|
entry.action === "DELETE",
|
|
)!,
|
|
prepare: (ownerId) => makeCustomVaccine(ownerId),
|
|
act: async (row) => {
|
|
const { DELETE } = await import("@/app/api/vaccinations/custom/[id]/route");
|
|
return call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/vaccinations/custom/${row.id}`,
|
|
undefined,
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 200,
|
|
auditAction: "vaccination.custom.delete",
|
|
applied: async (row) =>
|
|
(
|
|
await getPrismaClient().customVaccine.findUnique({
|
|
where: { id: row.id },
|
|
})
|
|
)?.deletedAt !== null,
|
|
});
|
|
|
|
proveWriteRoute("POST /api/vaccinations/[id]/booster", {
|
|
route: "/api/vaccinations/[id]/booster",
|
|
// The dose the booster is planned against. `makeVaccination` records a
|
|
// monovalent tetanus dose, so the mint keys the reminder on the tetanus
|
|
// antigen and `applied` can prove exactly one reminder now carries it.
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
vaccination: await makeVaccination(ownerId),
|
|
}),
|
|
params: ({ vaccination }) => ({ id: vaccination.id }),
|
|
body: () => ({ intervalMonths: 120, label: "Tetanus booster" }),
|
|
ok: 201,
|
|
auditAction: "vaccination.booster.planned",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().measurementReminder.count({
|
|
where: { userId: ownerId, vaccinationAntigen: "tetanus" },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("POST /api/illness/episodes/[id]/day-logs", {
|
|
contract: ADMITTED_MUTATING_HANDLERS.find(
|
|
(entry) =>
|
|
entry.route === "/api/illness/episodes/[id]/day-logs" &&
|
|
entry.action === "POST",
|
|
)!,
|
|
prepare: (ownerId) => makeEpisode(ownerId),
|
|
act: async (row) => {
|
|
const { POST } =
|
|
await import("@/app/api/illness/episodes/[id]/day-logs/route");
|
|
return call(
|
|
POST as Handler,
|
|
"POST",
|
|
`/api/illness/episodes/${row.id}/day-logs`,
|
|
{ date: "2026-07-15", feverC: 38.1 },
|
|
{ id: row.id },
|
|
);
|
|
},
|
|
ok: 201,
|
|
auditAction: "illness.day-log.upsert",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().illnessDayLog.count({
|
|
where: { episodeId: row.id, feverC: 38.1 },
|
|
})) === 1,
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 1c — the medication record, its events, supply, and import queue */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
proveWriteRoute("POST /api/medications/[id]/side-effects", {
|
|
route: "/api/medications/[id]/side-effects",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
medication: await makeMedication(ownerId),
|
|
}),
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
body: () => ({ entry: "NAUSEA", severity: 3 }),
|
|
ok: 201,
|
|
auditAction: "medication.sideEffect.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().medicationSideEffect.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
proveWriteRoute("POST /api/medications", {
|
|
route: "/api/medications",
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
body: () => ({
|
|
name: "Metformin",
|
|
dose: "500mg",
|
|
schedules: [{ windowStart: "08:00", windowEnd: "10:00" }],
|
|
}),
|
|
ok: 201,
|
|
auditAction: "medication.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().medication.count({
|
|
where: { userId: ownerId, name: "Metformin" },
|
|
})) === 1,
|
|
});
|
|
|
|
proveWriteRoute("POST /api/medications/intake", {
|
|
route: "/api/medications/intake",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeMedicationIntake(ownerId)),
|
|
}),
|
|
body: ({ event }) => ({ intakeId: event.id, status: "taken" }),
|
|
ok: 200,
|
|
auditAction: "medications.intake.update",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().medicationIntakeEvent.count({
|
|
where: { userId: ownerId, takenAt: { not: null }, deletedAt: null },
|
|
})) === 1,
|
|
});
|
|
|
|
proveWriteRoute("POST /api/medications/[id]/intake", {
|
|
route: "/api/medications/[id]/intake",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
medication: await makeMedication(ownerId),
|
|
}),
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
body: () => ({ takenAt: new Date().toISOString() }),
|
|
ok: 201,
|
|
auditAction: "medication.intake",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().medicationIntakeEvent.count({
|
|
where: { userId: ownerId, takenAt: { not: null } },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute("PUT /api/medications/[id]", {
|
|
route: "/api/medications/[id]",
|
|
action: "PUT",
|
|
prepare: (ownerId) => makeMedication(ownerId),
|
|
params: (medication) => ({ id: medication.id }),
|
|
body: () => ({ dose: "20mg" }),
|
|
ok: 200,
|
|
auditAction: "medication.update",
|
|
applied: async (medication) =>
|
|
(
|
|
await getPrismaClient().medication.findUnique({
|
|
where: { id: medication.id },
|
|
})
|
|
)?.dose === "20mg",
|
|
});
|
|
|
|
proveManageRoute("POST /api/medications/[id]/courses/end", {
|
|
route: "/api/medications/[id]/courses/end",
|
|
prepare: (ownerId) => makeMedication(ownerId),
|
|
params: (medication) => ({ id: medication.id }),
|
|
ok: 200,
|
|
auditAction: "medication.course.end",
|
|
applied: async (medication) =>
|
|
(
|
|
await getPrismaClient().medication.findUnique({
|
|
where: { id: medication.id },
|
|
})
|
|
)?.endedAt != null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/medications/[id]/schedule-revisions", {
|
|
route: "/api/medications/[id]/schedule-revisions",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
medication: await makeMedication(ownerId),
|
|
}),
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
body: () => ({
|
|
validFrom: new Date(Date.now() - 3 * 86_400_000).toISOString(),
|
|
validUntil: new Date(Date.now() - 2 * 86_400_000).toISOString(),
|
|
timesOfDay: ["08:00"],
|
|
}),
|
|
ok: 201,
|
|
auditAction: "medication.schedule_revision.created",
|
|
applied: async ({ medication }) =>
|
|
(await getPrismaClient().medicationScheduleRevision.count({
|
|
where: { medicationId: medication.id },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute(
|
|
"PATCH /api/medications/[id]/schedule-revisions/[revisionId]",
|
|
{
|
|
route: "/api/medications/[id]/schedule-revisions/[revisionId]",
|
|
action: "PATCH",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeScheduleRevision(ownerId)),
|
|
targetValidFrom: new Date(Date.now() - 4 * 86_400_000),
|
|
targetValidUntil: new Date(Date.now() - 3 * 86_400_000),
|
|
}),
|
|
params: ({ medication, revision }) => ({
|
|
id: medication.id,
|
|
revisionId: revision.id,
|
|
}),
|
|
body: ({ targetValidFrom, targetValidUntil }) => ({
|
|
validFrom: targetValidFrom.toISOString(),
|
|
validUntil: targetValidUntil.toISOString(),
|
|
timesOfDay: ["09:00"],
|
|
}),
|
|
ok: 200,
|
|
auditAction: "medication.schedule_revision.updated",
|
|
applied: async ({ revision, targetValidFrom }) =>
|
|
(
|
|
await getPrismaClient().medicationScheduleRevision.findUnique({
|
|
where: { id: revision.id },
|
|
})
|
|
)?.validFrom.toISOString() === targetValidFrom.toISOString(),
|
|
},
|
|
);
|
|
|
|
proveManageRoute(
|
|
"DELETE /api/medications/[id]/schedule-revisions/[revisionId]",
|
|
{
|
|
route: "/api/medications/[id]/schedule-revisions/[revisionId]",
|
|
action: "DELETE",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeScheduleRevision(ownerId)),
|
|
}),
|
|
params: ({ medication, revision }) => ({
|
|
id: medication.id,
|
|
revisionId: revision.id,
|
|
}),
|
|
ok: 200,
|
|
auditAction: "medication.schedule_revision.deleted",
|
|
applied: async ({ revision }) =>
|
|
(await getPrismaClient().medicationScheduleRevision.findUnique({
|
|
where: { id: revision.id },
|
|
})) === null,
|
|
},
|
|
);
|
|
|
|
proveManageRoute("POST /api/medications/[id]/inventory", {
|
|
route: "/api/medications/[id]/inventory",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
medication: await makeMedication(ownerId),
|
|
}),
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
body: () => ({ unitsTotal: 20, containerType: "BOTTLE" }),
|
|
ok: 201,
|
|
auditAction: "medication.inventory.create",
|
|
applied: async ({ medication }) =>
|
|
(await getPrismaClient().medicationInventoryItem.count({
|
|
where: { medicationId: medication.id },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute("PATCH /api/medications/[id]/inventory/[itemId]", {
|
|
route: "/api/medications/[id]/inventory/[itemId]",
|
|
action: "PATCH",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeInventoryItem(ownerId)),
|
|
}),
|
|
params: ({ medication, item }) => ({ id: medication.id, itemId: item.id }),
|
|
body: () => ({ unitsRemaining: 15 }),
|
|
ok: 200,
|
|
auditAction: "medication.inventory.update",
|
|
applied: async ({ item }) =>
|
|
Number(
|
|
(
|
|
await getPrismaClient().medicationInventoryItem.findUnique({
|
|
where: { id: item.id },
|
|
})
|
|
)?.unitsRemaining,
|
|
) === 15,
|
|
});
|
|
|
|
proveManageRoute("DELETE /api/medications/[id]/inventory/[itemId]", {
|
|
route: "/api/medications/[id]/inventory/[itemId]",
|
|
action: "DELETE",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeInventoryItem(ownerId)),
|
|
}),
|
|
params: ({ medication, item }) => ({ id: medication.id, itemId: item.id }),
|
|
ok: 200,
|
|
auditAction: "medication.inventory.delete",
|
|
applied: async ({ item }) =>
|
|
(await getPrismaClient().medicationInventoryItem.findUnique({
|
|
where: { id: item.id },
|
|
})) === null,
|
|
});
|
|
|
|
proveManageRoute("DELETE /api/medications/[id]/side-effects/[logId]", {
|
|
route: "/api/medications/[id]/side-effects/[logId]",
|
|
action: "DELETE",
|
|
prepare: async (ownerId) => {
|
|
const medication = await makeMedication(ownerId);
|
|
const log = await getPrismaClient().medicationSideEffect.create({
|
|
data: {
|
|
userId: ownerId,
|
|
medicationId: medication.id,
|
|
category: "GI",
|
|
entry: "NAUSEA",
|
|
severity: 3,
|
|
},
|
|
});
|
|
return { ownerId, medication, log };
|
|
},
|
|
params: ({ medication, log }) => ({ id: medication.id, logId: log.id }),
|
|
ok: 200,
|
|
auditAction: "medication.sideEffect.delete",
|
|
applied: async ({ log }) =>
|
|
(await getPrismaClient().medicationSideEffect.findUnique({
|
|
where: { id: log.id },
|
|
})) === null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/medications/[id]/glp1", {
|
|
route: "/api/medications/[id]/glp1",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
medication: await getPrismaClient().medication.create({
|
|
data: {
|
|
userId: ownerId,
|
|
name: "Semaglutide",
|
|
dose: "0.25mg",
|
|
treatmentClass: "GLP1",
|
|
},
|
|
}),
|
|
}),
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
body: () => ({
|
|
doseChange: { effectiveFrom: ISO, doseValue: 0.5, doseUnit: "mg" },
|
|
}),
|
|
ok: 201,
|
|
auditAction: "medication.glp1.update",
|
|
applied: async ({ medication }) =>
|
|
(await getPrismaClient().medicationDoseChange.count({
|
|
where: { medicationId: medication.id },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute("PUT /api/medications/[id]/ingredients", {
|
|
route: "/api/medications/[id]/ingredients",
|
|
action: "PUT",
|
|
prepare: async (ownerId) => {
|
|
// The composition lives behind the opt-in nutrients module.
|
|
await getPrismaClient().user.update({
|
|
where: { id: ownerId },
|
|
data: { modulePreferencesJson: { nutrients: true } },
|
|
});
|
|
return {
|
|
ownerId,
|
|
medication: await getPrismaClient().medication.create({
|
|
data: { userId: ownerId, name: "Vitamin D3", dose: "1000 IU" },
|
|
}),
|
|
};
|
|
},
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
body: () => ({
|
|
ingredients: [{ nutrient: "vitamin_d", amount: 1000, unit: "IU" }],
|
|
}),
|
|
ok: 200,
|
|
auditAction: "medication.ingredients.update",
|
|
applied: async ({ medication }) =>
|
|
(
|
|
await getPrismaClient().medicationIngredient.findFirst({
|
|
where: { medicationId: medication.id },
|
|
})
|
|
)?.amountPerUnit === 25,
|
|
});
|
|
|
|
proveManageRoute("PUT /api/medications/[id]/intake/[eventId]", {
|
|
route: "/api/medications/[id]/intake/[eventId]",
|
|
action: "PUT",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeMedicationIntake(ownerId)),
|
|
}),
|
|
params: ({ medication, event }) => ({ id: medication.id, eventId: event.id }),
|
|
body: () => ({ skipped: true }),
|
|
ok: 200,
|
|
auditAction: "medication.intake.update",
|
|
applied: async ({ event }) =>
|
|
(
|
|
await getPrismaClient().medicationIntakeEvent.findUnique({
|
|
where: { id: event.id },
|
|
})
|
|
)?.skipped === true,
|
|
});
|
|
|
|
proveManageRoute("DELETE /api/medications/[id]/intake/[eventId]", {
|
|
route: "/api/medications/[id]/intake/[eventId]",
|
|
action: "DELETE",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeMedicationIntake(ownerId)),
|
|
}),
|
|
params: ({ medication, event }) => ({ id: medication.id, eventId: event.id }),
|
|
ok: 200,
|
|
auditAction: "medication.intake.delete",
|
|
applied: async ({ event }) =>
|
|
(
|
|
await getPrismaClient().medicationIntakeEvent.findUnique({
|
|
where: { id: event.id },
|
|
})
|
|
)?.deletedAt !== null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/medications/[id]/intake/bulk-delete", {
|
|
route: "/api/medications/[id]/intake/bulk-delete",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeMedicationIntake(ownerId)),
|
|
}),
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
body: ({ event }) => ({ eventIds: [event.id] }),
|
|
ok: 200,
|
|
auditAction: "medication.intake.bulk_delete",
|
|
applied: async ({ event }) =>
|
|
(
|
|
await getPrismaClient().medicationIntakeEvent.findUnique({
|
|
where: { id: event.id },
|
|
})
|
|
)?.deletedAt !== null,
|
|
});
|
|
|
|
proveManageRoute("DELETE /api/medications/[id]/intake/purge", {
|
|
route: "/api/medications/[id]/intake/purge",
|
|
action: "DELETE",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
...(await makeMedicationIntake(ownerId)),
|
|
}),
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
ok: 200,
|
|
auditAction: "medication.intake.purge",
|
|
applied: async ({ event }) =>
|
|
(
|
|
await getPrismaClient().medicationIntakeEvent.findUnique({
|
|
where: { id: event.id },
|
|
})
|
|
)?.deletedAt !== null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/medications/[id]/intake/import", {
|
|
route: "/api/medications/[id]/intake/import",
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
medication: await makeMedication(ownerId),
|
|
}),
|
|
params: ({ medication }) => ({ id: medication.id }),
|
|
body: () => [{ datum: "2026-07-15", uhrzeit: "08:00:00" }],
|
|
ok: 202,
|
|
auditAction: "medication.intake.import.kickoff",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().medicationIntakeImportJob.count({
|
|
where: { recordUserId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
manageContract("POST /api/medications/intake/dose-history-import", {
|
|
contract: admittedContract("/api/medications/intake/dose-history-import"),
|
|
prepare: async (ownerId) => ({
|
|
ownerId,
|
|
medication: await makeMedication(ownerId, "Metformin"),
|
|
}),
|
|
act: async () => {
|
|
const handler = await matrixHandler(
|
|
admittedContract("/api/medications/intake/dose-history-import"),
|
|
);
|
|
const csv = [
|
|
"Date,Scheduled Date,Medication,Nickname,Dosage,Scheduled Dosage,Unit,Status,Archived,Codings",
|
|
"2026-07-15 08:00:00 +0000,2026-07-15 08:00:00 +0000,Metformin,Metformin,500,500,mg,Taken,No,",
|
|
].join("\n");
|
|
return handler(
|
|
new NextRequest(
|
|
"http://localhost/api/medications/intake/dose-history-import",
|
|
{
|
|
method: "POST",
|
|
headers: { "content-type": "text/csv" },
|
|
body: csv,
|
|
},
|
|
),
|
|
{ params: Promise.resolve({}) },
|
|
);
|
|
},
|
|
ok: 202,
|
|
auditAction: "medication.intake.history.import.kickoff",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().medicationIntakeImportJob.count({
|
|
where: { recordUserId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 1d — mood entries and validated mental-health assessments */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
async function makeMoodEntry(userId: string) {
|
|
return getPrismaClient().moodEntry.create({
|
|
data: {
|
|
userId,
|
|
date: "2026-07-15",
|
|
mood: "GUT",
|
|
score: 4,
|
|
moodLoggedAt: new Date(ISO),
|
|
source: "MANUAL",
|
|
},
|
|
});
|
|
}
|
|
|
|
proveManageRoute("POST /api/mood-entries", {
|
|
route: "/api/mood-entries",
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
body: () => ({ mood: "GUT", moodLoggedAt: ISO }),
|
|
ok: 201,
|
|
auditAction: "moodEntry.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().moodEntry.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute("PUT /api/mood-entries/[id]", {
|
|
route: "/api/mood-entries/[id]",
|
|
action: "PUT",
|
|
prepare: (ownerId) => makeMoodEntry(ownerId),
|
|
params: (entry) => ({ id: entry.id }),
|
|
body: () => ({ mood: "OKAY" }),
|
|
ok: 200,
|
|
auditAction: "moodEntry.update",
|
|
applied: async (entry) =>
|
|
(await getPrismaClient().moodEntry.findUnique({ where: { id: entry.id } }))
|
|
?.mood === "OKAY",
|
|
});
|
|
|
|
proveManageRoute("DELETE /api/mood-entries/[id]", {
|
|
route: "/api/mood-entries/[id]",
|
|
action: "DELETE",
|
|
prepare: (ownerId) => makeMoodEntry(ownerId),
|
|
params: (entry) => ({ id: entry.id }),
|
|
ok: 200,
|
|
auditAction: "moodEntry.delete",
|
|
applied: async (entry) =>
|
|
(await getPrismaClient().moodEntry.findUnique({ where: { id: entry.id } }))
|
|
?.deletedAt !== null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/mood-entries/bulk-delete", {
|
|
route: "/api/mood-entries/bulk-delete",
|
|
prepare: (ownerId) => makeMoodEntry(ownerId),
|
|
body: (entry) => ({ ids: [entry.id] }),
|
|
ok: 200,
|
|
auditAction: "mood.delete.bulk",
|
|
applied: async (entry) =>
|
|
(await getPrismaClient().moodEntry.findUnique({ where: { id: entry.id } }))
|
|
?.deletedAt !== null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/mood-entries/restore", {
|
|
route: "/api/mood-entries/restore",
|
|
prepare: async (ownerId) => {
|
|
const entry = await makeMoodEntry(ownerId);
|
|
return getPrismaClient().moodEntry.update({
|
|
where: { id: entry.id },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
},
|
|
body: (entry) => ({ ids: [entry.id] }),
|
|
ok: 200,
|
|
auditAction: "mood.restore",
|
|
applied: async (entry) =>
|
|
(await getPrismaClient().moodEntry.findUnique({ where: { id: entry.id } }))
|
|
?.deletedAt === null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/mental-health/assessments", {
|
|
route: "/api/mental-health/assessments",
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
body: () => ({
|
|
instrument: "PHQ9",
|
|
items: [0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
takenAt: ISO,
|
|
}),
|
|
ok: 201,
|
|
auditAction: "mental-health.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().mentalHealthAssessment.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 1e — cycle and record-wide mutations */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
async function enableCycle(userId: string) {
|
|
await getPrismaClient().user.update({
|
|
where: { id: userId },
|
|
data: { gender: "FEMALE" },
|
|
});
|
|
return userId;
|
|
}
|
|
|
|
async function makeCycleDayLog(userId: string) {
|
|
await enableCycle(userId);
|
|
return getPrismaClient().cycleDayLog.create({
|
|
data: {
|
|
userId,
|
|
date: "2026-07-15",
|
|
source: "MANUAL",
|
|
},
|
|
});
|
|
}
|
|
|
|
proveManageRoute("DELETE /api/cycle/cycles/[id]", {
|
|
route: "/api/cycle/cycles/[id]",
|
|
action: "DELETE",
|
|
prepare: async (ownerId) => {
|
|
await enableCycle(ownerId);
|
|
return getPrismaClient().menstrualCycle.create({
|
|
data: { userId: ownerId, startDate: "2026-07-01" },
|
|
});
|
|
},
|
|
params: (cycle) => ({ id: cycle.id }),
|
|
ok: 204,
|
|
auditAction: "cycle.cycle.delete",
|
|
applied: async (cycle) =>
|
|
(
|
|
await getPrismaClient().menstrualCycle.findUnique({
|
|
where: { id: cycle.id },
|
|
})
|
|
)?.deletedAt !== null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/cycle/day-logs", {
|
|
route: "/api/cycle/day-logs",
|
|
prepare: async (ownerId) => ({ ownerId: await enableCycle(ownerId) }),
|
|
body: () => ({ date: "2026-07-15", loggedAt: ISO, flow: "LIGHT" }),
|
|
ok: 201,
|
|
auditAction: "cycle.day-log.upsert",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().cycleDayLog.count({
|
|
where: { userId: ownerId, date: "2026-07-15" },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute("PATCH /api/cycle/day-logs/[id]", {
|
|
route: "/api/cycle/day-logs/[id]",
|
|
action: "PATCH",
|
|
prepare: (ownerId) => makeCycleDayLog(ownerId),
|
|
params: (row) => ({ id: row.id }),
|
|
body: () => ({ flow: "HEAVY" }),
|
|
ok: 200,
|
|
auditAction: "cycle.day-log.update",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().cycleDayLog.findUnique({ where: { id: row.id } }))
|
|
?.flow === "HEAVY",
|
|
});
|
|
|
|
proveManageRoute("DELETE /api/cycle/day-logs/[id]", {
|
|
route: "/api/cycle/day-logs/[id]",
|
|
action: "DELETE",
|
|
prepare: (ownerId) => makeCycleDayLog(ownerId),
|
|
params: (row) => ({ id: row.id }),
|
|
ok: 204,
|
|
auditAction: "cycle.day-log.delete",
|
|
applied: async (row) =>
|
|
(await getPrismaClient().cycleDayLog.findUnique({ where: { id: row.id } }))
|
|
?.deletedAt !== null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/cycle/period", {
|
|
route: "/api/cycle/period",
|
|
prepare: async (ownerId) => ({ ownerId: await enableCycle(ownerId) }),
|
|
body: () => ({ action: "start", date: "2026-07-15", loggedAt: ISO }),
|
|
ok: 200,
|
|
auditAction: "cycle.period.boundary",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().menstrualCycle.count({
|
|
where: { userId: ownerId, startDate: "2026-07-15" },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute("POST /api/cycle/symptoms/custom", {
|
|
route: "/api/cycle/symptoms/custom",
|
|
prepare: async (ownerId) => ({ ownerId: await enableCycle(ownerId) }),
|
|
body: () => ({ label: "Pelvic pressure" }),
|
|
ok: 201,
|
|
auditAction: "cycle.symptom.custom.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().cycleSymptom.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute("POST /api/medications/categories", {
|
|
route: "/api/medications/categories",
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
body: () => ({ label: "Travel kit" }),
|
|
ok: 201,
|
|
auditAction: "medication.category.custom.create",
|
|
applied: async ({ ownerId }) =>
|
|
(await getPrismaClient().medicationCategoryLabel.count({
|
|
where: { userId: ownerId },
|
|
})) === 1,
|
|
});
|
|
|
|
proveManageRoute("POST /api/nutrients/water", {
|
|
route: "/api/nutrients/water",
|
|
prepare: async (ownerId) => {
|
|
await getPrismaClient().user.update({
|
|
where: { id: ownerId },
|
|
data: { modulePreferencesJson: { nutrients: true } },
|
|
});
|
|
return { ownerId };
|
|
},
|
|
body: () => ({ amountMl: 250, mode: "set", day: "2026-07-15" }),
|
|
ok: 200,
|
|
auditAction: "nutrient.water.write",
|
|
applied: async ({ ownerId }) =>
|
|
(
|
|
await getPrismaClient().nutrientIntakeDay.findFirst({
|
|
where: {
|
|
userId: ownerId,
|
|
day: "2026-07-15",
|
|
nutrient: "water",
|
|
source: "MANUAL",
|
|
},
|
|
})
|
|
)?.amount === 250,
|
|
});
|
|
|
|
proveManageRoute("PATCH /api/insights/patterns/[id]", {
|
|
route: "/api/insights/patterns/[id]",
|
|
action: "PATCH",
|
|
prepare: async (ownerId) =>
|
|
getPrismaClient().correlationPattern.create({
|
|
data: {
|
|
userId: ownerId,
|
|
canonicalKey: "mood:water",
|
|
family: "mood",
|
|
factorKey: "water",
|
|
outcomeKey: "mood",
|
|
lagDays: 0,
|
|
sampleSize: 12,
|
|
effectSize: 0.3,
|
|
pValue: 0.01,
|
|
evidenceHash: "evidence-v1",
|
|
lastComputedAt: new Date(ISO),
|
|
},
|
|
}),
|
|
params: (pattern) => ({ id: pattern.id }),
|
|
body: () => ({ dismissed: true }),
|
|
ok: 200,
|
|
auditAction: "correlationPattern.dismiss",
|
|
applied: async (pattern) =>
|
|
(
|
|
await getPrismaClient().correlationPattern.findUnique({
|
|
where: { id: pattern.id },
|
|
})
|
|
)?.dismissedAt !== null,
|
|
});
|
|
|
|
proveManageRoute("POST /api/export/health-record", {
|
|
route: "/api/export/health-record",
|
|
prepare: async (ownerId) => ({ ownerId }),
|
|
body: () => ({ format: "fhir", selection: { v: 2, leaves: ["MOOD"] } }),
|
|
ok: 200,
|
|
auditAction: "health-record.export",
|
|
applied: async ({ ownerId }) => {
|
|
const row = await getPrismaClient().auditLog.findFirst({
|
|
where: { userId: ownerId, action: "health-record.export" },
|
|
});
|
|
return row !== null;
|
|
},
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 2 — reconstructable or refused */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
describe("what a destruction leaves behind", () => {
|
|
it("[J2-manage-edits-without-settings] C3: a hard delete files what it destroyed, and never the encrypted text", async () => {
|
|
// The preventive-care reminder is the sharpest case in the tree: the row
|
|
// is gone, no restore route reaches it, and the audit row used to say
|
|
// `{ reminderId }` — a pointer at nothing. An owner reading their feed
|
|
// could see that a recall disappeared and never learn what it was for.
|
|
const owner = await makeUser("owner");
|
|
const manager = await makeUser("manager");
|
|
const reminder = await makeReminder(owner.id);
|
|
|
|
await switchInto(owner.id, manager.id, "MANAGE");
|
|
const { DELETE } =
|
|
await import("@/app/api/measurement-reminders/[id]/route");
|
|
expect(
|
|
(
|
|
await call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/measurement-reminders/${reminder.id}`,
|
|
undefined,
|
|
{ id: reminder.id },
|
|
)
|
|
).status,
|
|
).toBe(200);
|
|
|
|
const { details } = await auditRow("measurementReminder.delete");
|
|
expect(details.destroyedModel).toBe("MeasurementReminder");
|
|
expect(details.destroyedId).toBe(reminder.id);
|
|
expect(details.destroyedLabel).toBe("Colonoscopy recall");
|
|
expect(details.destroyedAt).toBe(reminder.nextDueAt?.toISOString());
|
|
expect(details.intervalDays).toBe(365);
|
|
});
|
|
|
|
it("C3: the allergy delete carries the allergen and never the reaction", async () => {
|
|
const owner = await makeUser("owner");
|
|
const manager = await makeUser("manager");
|
|
const allergy = await getPrismaClient().allergy.create({
|
|
data: {
|
|
userId: owner.id,
|
|
substance: "Penicillin",
|
|
category: "MEDICATION",
|
|
type: "ALLERGY",
|
|
severity: "SEVERE",
|
|
status: "ACTIVE",
|
|
onsetAt: new Date("2020-03-01T00:00:00.000Z"),
|
|
},
|
|
});
|
|
|
|
await switchInto(owner.id, manager.id, "MANAGE");
|
|
const { DELETE } = await import("@/app/api/allergies/[id]/route");
|
|
expect(
|
|
(
|
|
await call(
|
|
DELETE as Handler,
|
|
"DELETE",
|
|
`/api/allergies/${allergy.id}`,
|
|
undefined,
|
|
{ id: allergy.id },
|
|
)
|
|
).status,
|
|
).toBe(200);
|
|
|
|
const { row, details } = await auditRow("allergy.delete");
|
|
expect(details.destroyedLabel).toBe("Penicillin");
|
|
expect(details.severity).toBe("SEVERE");
|
|
expect(details.destroyedAt).toBe("2020-03-01T00:00:00.000Z");
|
|
// …and the audit table did not become a second store for the encrypted
|
|
// half of the row. Asserted on the raw string, because a nested key would
|
|
// slip past a top-level property check.
|
|
expect(row.details ?? "").not.toContain("reaction");
|
|
});
|
|
|
|
it("[J2-manage-edits-without-settings] C4: an edit files the field family and the value that is gone", async () => {
|
|
const owner = await makeUser("owner");
|
|
const manager = await makeUser("manager");
|
|
const measurement = await makeMeasurement(owner.id, 80);
|
|
|
|
await switchInto(owner.id, manager.id, "MANAGE");
|
|
const { PUT } = await import("@/app/api/measurements/[id]/route");
|
|
expect(
|
|
(
|
|
await call(
|
|
PUT as Handler,
|
|
"PUT",
|
|
`/api/measurements/${measurement.id}`,
|
|
{ value: 74.5 },
|
|
{ id: measurement.id },
|
|
)
|
|
).status,
|
|
).toBe(200);
|
|
|
|
const { details } = await auditRow("measurement.update");
|
|
expect(details.fields).toEqual(["value"]);
|
|
expect((details.previous as Record<string, unknown>).value).toBe(80);
|
|
});
|
|
|
|
it("a restore puts back the row it names, and leaves the others tombstoned", async () => {
|
|
// The fake-that-ignores-its-`where` case: a count check passes on a
|
|
// restore that clears every tombstone in the record. The id is what
|
|
// defeats it.
|
|
const owner = await makeUser("owner");
|
|
const manager = await makeUser("manager");
|
|
const kept = await makeMeasurement(owner.id, 81);
|
|
const other = await makeMeasurement(owner.id, 82);
|
|
await getPrismaClient().measurement.updateMany({
|
|
where: { id: { in: [kept.id, other.id] } },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
|
|
await switchInto(owner.id, manager.id, "MANAGE");
|
|
const { POST } = await import("@/app/api/measurements/restore/route");
|
|
const response = await call(
|
|
POST as Handler,
|
|
"POST",
|
|
"/api/measurements/restore",
|
|
{
|
|
ids: [kept.id],
|
|
},
|
|
);
|
|
expect(response.status).toBe(200);
|
|
|
|
const restored = await getPrismaClient().measurement.findUnique({
|
|
where: { id: kept.id },
|
|
});
|
|
const untouched = await getPrismaClient().measurement.findUnique({
|
|
where: { id: other.id },
|
|
});
|
|
expect(restored?.deletedAt).toBeNull();
|
|
expect(untouched?.deletedAt).not.toBeNull();
|
|
|
|
const { row } = await auditRow("measurement.restore");
|
|
expect(row.userId).toBe(owner.id);
|
|
expect(row.actorUserId).toBe(manager.id);
|
|
});
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 3 — the conditions that are not about the audit row */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
describe("the conditions the admissions were granted on", () => {
|
|
it("C1: the manager burns their own rate allowance, not the owner's", async () => {
|
|
// The buckets live in Postgres, so the key itself is readable. A bucket
|
|
// keyed on the record would let a manager lock the owner out of their own
|
|
// restore button by hammering it.
|
|
const owner = await makeUser("owner");
|
|
const manager = await makeUser("manager");
|
|
const row = await makeMeasurement(owner.id);
|
|
await getPrismaClient().measurement.update({
|
|
where: { id: row.id },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
|
|
await switchInto(owner.id, manager.id, "MANAGE");
|
|
const { POST } = await import("@/app/api/measurements/restore/route");
|
|
expect(
|
|
(
|
|
await call(POST as Handler, "POST", "/api/measurements/restore", {
|
|
ids: [row.id],
|
|
})
|
|
).status,
|
|
).toBe(200);
|
|
|
|
const buckets = await getPrismaClient().rateLimit.findMany();
|
|
expect(buckets.length).toBeGreaterThan(0);
|
|
expect(buckets.some((b) => b.key.includes(manager.id))).toBe(true);
|
|
expect(buckets.some((b) => b.key.includes(owner.id))).toBe(false);
|
|
});
|
|
|
|
it("C5: a manager's navigation enqueues no generation; the owner's does", async () => {
|
|
// Both halves, and the second is what makes the first mean anything: the
|
|
// narrative route warms unconditionally, so a zero on the delegated side
|
|
// could just as easily mean the route never reached the enqueue at all.
|
|
const owner = await makeUser("owner");
|
|
const manager = await makeUser("manager");
|
|
const { GET } = await import("@/app/api/insights/narrative/route");
|
|
|
|
await signIn(owner.id);
|
|
expect(
|
|
(await call(GET as Handler, "GET", "/api/insights/narrative?period=week"))
|
|
.status,
|
|
).toBe(200);
|
|
expect(enqueueNarrativeWarm).toHaveBeenCalledTimes(1);
|
|
|
|
enqueueNarrativeWarm.mockClear();
|
|
|
|
await switchInto(owner.id, manager.id, "MANAGE");
|
|
const delegated = await call(
|
|
GET as Handler,
|
|
"GET",
|
|
"/api/insights/narrative?period=week",
|
|
);
|
|
expect(delegated.status).toBe(200);
|
|
expect(enqueueNarrativeWarm).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("C5: a WRITE grant cannot read the generated surfaces at all", async () => {
|
|
const owner = await makeUser("owner");
|
|
const helper = await makeUser("helper");
|
|
await switchInto(owner.id, helper.id, "WRITE");
|
|
|
|
const { GET } = await import("@/app/api/insights/narrative/route");
|
|
await expectDenied(
|
|
await call(GET as Handler, "GET", "/api/insights/narrative?period=week"),
|
|
);
|
|
expect(enqueueNarrativeWarm).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("C6: the delegated mood create refuses the sync client's externalId", async () => {
|
|
const owner = await makeUser("owner");
|
|
const manager = await makeUser("manager");
|
|
const body = {
|
|
mood: "GUT",
|
|
moodLoggedAt: ISO,
|
|
// A stable, device-shaped id: the floor on `externalId` refuses an
|
|
// unstable one at the schema, and this test is about the level, not
|
|
// about that floor.
|
|
externalId: "8B0F3C2E-1D4A-4F5B-9C6D-7E8F90A1B2C3",
|
|
source: "MOODLOG",
|
|
};
|
|
|
|
await switchInto(owner.id, manager.id, "MANAGE");
|
|
const { POST } = await import("@/app/api/mood-entries/route");
|
|
const refused = await call(
|
|
POST as Handler,
|
|
"POST",
|
|
"/api/mood-entries",
|
|
body,
|
|
);
|
|
expect(refused.status).toBe(422);
|
|
expect((await payload(refused)).meta?.errorCode).toBe(
|
|
"mood.create.external_id_not_delegable",
|
|
);
|
|
expect(
|
|
await getPrismaClient().moodEntry.count({ where: { userId: owner.id } }),
|
|
).toBe(0);
|
|
|
|
// The same body from the owner's own session still lands: C6 closes the
|
|
// delegated branch, not the route.
|
|
await signIn(owner.id);
|
|
const accepted = await call(
|
|
POST as Handler,
|
|
"POST",
|
|
"/api/mood-entries",
|
|
body,
|
|
);
|
|
expect(accepted.status).toBe(201);
|
|
expect(
|
|
await getPrismaClient().moodEntry.count({ where: { userId: owner.id } }),
|
|
).toBe(1);
|
|
});
|
|
});
|
|
|
|
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(93);
|
|
expect([...STRICT_DRIVER_KEYS].sort()).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
// Every registered driver went through its owned effect and actor audit.
|
|
// After all cases, not as one of them: the set fills as the effect cases
|
|
// run, and a case placed last in the file was read first whenever the order
|
|
// was shuffled. A run filtered down to some cases has nothing to say here.
|
|
afterAll(() => {
|
|
if (EFFECT_CASES_STARTED.size !== STRICT_DRIVER_KEYS.size) return;
|
|
const expected = ADMITTED_MUTATING_HANDLERS.map(matrixKey).sort();
|
|
expect(REAL_EFFECT_KEYS.size).toBe(93);
|
|
expect([...REAL_EFFECT_KEYS].sort()).toEqual(expected);
|
|
});
|