feat(vorsorge): make the mental-wellbeing screening plannable

PHQ9_SCORE / GAD7_SCORE join the Vorsorge reminder type allow-list as
their own "Seelisches Wohlbefinden" group, with a 4-week default
cadence (both instruments cover the last two weeks; 28 joins the
interval presets). The reminders ride the existing activity-aware
engine unchanged: they fire only when overdue, and a completed check-in
auto-satisfies them — the assessment route now kicks the eventful
reminder-satisfy worker, which resolves against the server-written
score row; the 15-min cron stays the idempotent safety-net. The
screening reminder's primary action opens the check-in page (a score is
never typed in), and the dispatcher gates the reminder on the opt-in
mental-health module so a disabled module never nags. OpenAPI spec
regenerated for the widened MeasurementReminderType enum.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 15:05:22 +02:00
1 parent b6aedfeb61
commit 01beabb99b
16 files changed
+256 -30

No files matched your search

@@ -37,6 +37,12 @@ vi.mock("@/lib/ai/coach/bytes-codec", () => ({
encryptToBytes: vi.fn(() => Buffer.from("ciphertext")),
}));
// v1.27.6 — a completed check-in kicks the eventful Vorsorge satisfy worker
// so a plannable screening reminder resolves immediately.
vi.mock("@/lib/jobs/reminder-satisfy", () => ({
enqueueReminderSatisfy: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
vi.mock("@/lib/auth/audit", () => ({
auditLog: vi.fn().mockResolvedValue(undefined),
@@ -55,6 +61,7 @@ vi.mock("next/headers", () => ({
}));
import { POST } from "../route";
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
import { getSession } from "@/lib/auth/session";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { checkRateLimit } from "@/lib/rate-limit";
@@ -200,6 +207,10 @@ describe("POST /api/mental-health/assessments", () => {
expect(measArg.data.unit).toBe("score");
expect(measArg.data.source).toBe("COMPUTED");
expect(measArg.data.externalId).toBe("assessment:mha_2");
// The eventful Vorsorge satisfy worker is kicked so a plannable
// screening reminder (PHQ9_SCORE / GAD7_SCORE) auto-resolves at once.
expect(enqueueReminderSatisfy).toHaveBeenCalledWith("user-1");
});
it("dedups a repeat externalId: returns the existing administration, writes nothing new", async () => {
@@ -239,6 +250,8 @@ describe("POST /api/mental-health/assessments", () => {
);
expect(prisma.mentalHealthAssessment.create).not.toHaveBeenCalled();
expect(prisma.measurement.create).not.toHaveBeenCalled();
// No new trend point → nothing to satisfy either.
expect(enqueueReminderSatisfy).not.toHaveBeenCalled();
});
it("persists client provenance + externalId on a first write", async () => {
@@ -28,6 +28,7 @@ import {
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { withIdempotency } from "@/lib/idempotency";
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
@@ -327,6 +328,12 @@ async function postAssessment(request: NextRequest): Promise<Response> {
},
});
// v1.27.6 — a screening can be planned as a Vorsorge reminder keyed on
// PHQ9_SCORE / GAD7_SCORE. Kick the eventful satisfy worker so completing
// a check-in resolves the reminder immediately (the ingest-route
// precedent); the 15-min cron stays the idempotent safety-net.
await enqueueReminderSatisfy(user.id);
await auditLog("mental-health.create", {
userId: user.id,
// The score band is non-sensitive; item answers are NEVER logged.