Merge branch 'fix/meds-ended-and-mental-health' into release/v1.39.4

This commit is contained in:
Marc-André Bombeck committed 2026-09-27 14:18:09 +02:00
commit 206da07e8c
42 files changed
+1075 -34

No files matched your search

+7
View File
@@ -47,6 +47,7 @@ import {
scheduleWireFields,
} from "@/lib/medications/intake-tracking";
import { floorWhole } from "@/lib/medications/units-per-dose";
import { resolveIntakeActionability } from "@/lib/medications/intake-actionable";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
@@ -243,6 +244,12 @@ export const GET = apiHandler(
category,
nextDueAt: display ? display.at.toISOString() : null,
nextDueOverdue: display?.overdue ?? false,
// v1.39.4 (#1040) — see `resolveIntakeActionability`.
...resolveIntakeActionability(
medication,
now,
user.timezone || DEFAULT_TIMEZONE,
),
stockUnitsRemaining,
stockDosesRemaining,
runwayDays,
+126 -1
View File
@@ -55,7 +55,10 @@ vi.mock("@/lib/medication-category", () => ({
getMedicationCategories: vi.fn().mockResolvedValue({}),
setMedicationCategory: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/tz/local-day", () => ({
vi.mock("@/lib/tz/local-day", async (importOriginal) => ({
// The schedule engine reads the real local-day helpers; only the today
// bounds are pinned per test.
...(await importOriginal<typeof import("@/lib/tz/local-day")>()),
getUserTodayBounds: vi
.fn()
.mockReturnValue({ start: new Date(), end: new Date() }),
@@ -361,6 +364,18 @@ describe("POST /api/medications — v1.5.4 clinical category extension", () => {
expect(res.status).toBe(201);
});
it("accepts MENTAL_HEALTH as a valid clinical category (v1.39.4, #1041)", async () => {
const res = await POST(
postReq({
name: "Sertraline",
dose: "50 mg",
category: "MENTAL_HEALTH",
schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
}),
);
expect(res.status).toBe(201);
});
it("still 422s on an unknown category string", async () => {
const res = await POST(
postReq({
@@ -916,3 +931,113 @@ describe("GET /api/medications — provenance echo (v1.32.25)", () => {
expect(native?.externalSource).toBeNull();
});
});
describe("GET /api/medications — intake actions follow the course (#1040)", () => {
function wireEmptyReads() {
vi.mocked(cachedSwr).mockImplementation((async (
_c: unknown,
_k: string,
f: () => Promise<unknown>,
) => f()) as never);
vi.mocked(getUserTodayBounds).mockReturnValue({
start: new Date("2026-09-27T00:00:00Z"),
end: new Date("2026-09-27T23:59:59Z"),
} as never);
vi.mocked(prisma.medicationIntakeEvent.groupBy).mockResolvedValue(
[] as never,
);
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue(
[] as never,
);
vi.mocked(prisma.medicationScheduleRevision.groupBy).mockResolvedValue(
[] as never,
);
vi.mocked(prisma.medicationInventoryItem.groupBy).mockResolvedValue(
[] as never,
);
vi.mocked(getMedicationCategories).mockResolvedValue({} as never);
}
function med(id: string, over: Record<string, unknown>) {
return {
id,
userId: "user-1",
name: id,
dose: "10 mg",
treatmentClass: "GENERIC",
unitsPerDose: 1,
active: true,
trackIntake: true,
asNeeded: false,
oneShot: false,
pausedAt: null,
startsOn: new Date("2026-09-01T00:00:00Z"),
endsOn: null,
createdAt: new Date("2026-09-01T08:00:00Z"),
schedules: [
{
id: `${id}-s`,
medicationId: id,
windowStart: "08:00",
windowEnd: "09:00",
timesOfDay: ["08:00"],
daysOfWeek: null,
rrule: "FREQ=DAILY",
rollingIntervalDays: null,
reminderGraceMinutes: null,
cyclicOnWeeks: null,
cyclicOffWeeks: null,
scheduleType: "SCHEDULED",
unitsPerDose: null,
doseWindows: null,
label: null,
dose: null,
},
],
...over,
};
}
it("withholds the dose actions from a course that ended yesterday and keeps them on its last day", async () => {
vi.useFakeTimers({ toFake: ["Date"] });
// 10:00 in Berlin (the default zone) on 2026-09-27.
vi.setSystemTime(new Date("2026-09-27T08:00:00Z"));
try {
wireEmptyReads();
vi.mocked(prisma.medication.findMany).mockResolvedValue([
med("ended", { endsOn: new Date("2026-09-26T00:00:00Z") }),
med("last-day", { endsOn: new Date("2026-09-27T00:00:00Z") }),
med("upcoming", { startsOn: new Date("2026-09-28T00:00:00Z") }),
med("chronic", {}),
med("record-only", { trackIntake: false }),
] as never);
const res = await (
GET as unknown as (req: NextRequest) => Promise<Response>
)(new NextRequest("http://localhost/api/medications"));
expect(res.status).toBe(200);
const body = (await res.json()) as {
data: Array<{
id: string;
courseStatus: string;
intakeActionable: boolean;
}>;
};
const byId = Object.fromEntries(
body.data.map((m) => [
m.id,
[m.courseStatus, m.intakeActionable] as const,
]),
);
expect(byId).toEqual({
ended: ["ENDED", false],
"last-day": ["CURRENT", true],
upcoming: ["UPCOMING", false],
chronic: ["CURRENT", true],
"record-only": ["CURRENT", false],
});
} finally {
vi.useRealTimers();
}
});
});