mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(medications): never snap a taken intake onto a future dose slot
A taken intake whose write named (or snapped near) a slot still in the future was attributed to that slot and rendered as taken before its time — a late morning dose collapsing onto the evening slot of a twice-daily med. The slot resolver now takes an isTakenWrite flag and a now reference: a taken write skips any candidate slot more than a small clock-skew grace into the future, so it snaps to a current/past slot or keeps its own row. Pending sync echoes are unaffected and still map to future slots.
This commit is contained in:
1 parent
212e8c9c04
commit
3ec80adbd2
7 files changed
+148
-2
No files matched your search
@@ -19,7 +19,7 @@
|
||||
* cares about the Prisma `where` fragment derived from the new knob.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
@@ -391,6 +391,19 @@ describe("POST /api/medications/[id]/intake — v1.8.2 reconcile (M2 inventory)"
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Pin the clock past the 07:00 / 19:00 CEST slots of 2026-06-15 so the
|
||||
// dose-safety future-slot guard treats these taken writes as landing on
|
||||
// a current/past slot. Otherwise the fixtures sit days ahead of the
|
||||
// real test clock and the guard refuses to snap a taken write forward.
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(new Date("2026-06-15T18:00:00.000Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("does NOT decrement inventory on a re-post of an already-taken slot (M2)", async () => {
|
||||
const { consumeOneDose } = await import(
|
||||
"@/lib/medications/inventory/service"
|
||||
|
||||
@@ -159,6 +159,9 @@ async function postIntake(request: NextRequest, { params }: RouteParams) {
|
||||
// `takenAt`-only / defaulted-now write must not snap across the wide
|
||||
// ±halfGap window onto a far slot (phantom morning dose).
|
||||
instantIsExplicit: scheduledFor !== undefined,
|
||||
// Dose-safety: a taken write (any non-skip on this route) must never
|
||||
// snap forward onto a future slot — see resolve-slot-instant.ts.
|
||||
isTakenWrite: isExplicitTaken,
|
||||
});
|
||||
|
||||
// Idempotency check (explicit key or server-side dedup window)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* v1.4.43 W6 — multi-issue 422 envelope on POST /api/medications/intake/bulk.
|
||||
* Preserves the `medications.intake.bulk.invalid` errorCode meta passthrough.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
@@ -186,6 +186,13 @@ describe("POST /api/medications/intake/bulk — v1.8.2 reconcile", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Pin the clock past both fixture slots (07:00 / 19:00 CEST on
|
||||
// 2026-06-15) so the dose-safety future-slot guard treats these taken
|
||||
// writes as landing on a current/past slot, not a future one. Without
|
||||
// the pin these fixtures are dated days ahead of the real test clock
|
||||
// and the guard (correctly) refuses to snap a taken write forward.
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(new Date("2026-06-15T18:00:00.000Z"));
|
||||
vi.mocked(prisma.medication.findMany).mockResolvedValue([
|
||||
{ id: "med-1" },
|
||||
] as never);
|
||||
@@ -194,6 +201,10 @@ describe("POST /api/medications/intake/bulk — v1.8.2 reconcile", () => {
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("C2 — a pending echo onto an already-TAKEN slot is reported duplicate, NOT a downgrade", async () => {
|
||||
// Existing TAKEN row at the 07:00 slot. The bulk entry is a pending
|
||||
// echo (no takenAt, skipped false) — must NOT clear takenAt.
|
||||
|
||||
@@ -238,6 +238,10 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
// `takenAt`-only / defaulted-now write must not snap across the
|
||||
// wide ±halfGap window onto a far slot (phantom morning dose).
|
||||
instantIsExplicit: entry.scheduledFor !== undefined,
|
||||
// Dose-safety: a taken write (has `takenAt`, not skipped) must never
|
||||
// snap forward onto a future slot. A pending sync echo (no `takenAt`)
|
||||
// legitimately maps to a future slot, so the guard stays off for it.
|
||||
isTakenWrite: !entry.skipped && entry.takenAt !== undefined,
|
||||
});
|
||||
|
||||
const scheduledFor = canonicalSlot ?? incomingScheduledFor;
|
||||
|
||||
Reference in new issue
Block a user