fix(medications): stop defaulted-now intake from auto-taking a far slot

A slot-less "taken now" write (no client scheduledFor) defaulted its
instant to now/takenAt and snapped across the wide ±halfGap window onto
the nearest scheduled slot. For a 07:00 / 19:00 med a midday write
captured the 07:00 morning slot and stamped it taken — a phantom morning
dose. The web fix only threaded the displayed slot on the card; any
client posting takenAt-only (iOS bulk/sync) still tripped it server-side.

Thread an instantIsExplicit flag through resolveSlotInstantForWrite into
the canonical resolver. An explicit scheduledFor keeps the full ±halfGap
snap unchanged. A defaulted-now write snaps only inside the tight
dose-grace window and otherwise resolves to null (PRN), so it records a
standalone taken-now row instead of back-filling a far slot. Both intake
call sites pass scheduledFor !== undefined; takenAt alone is not explicit.

Add resolver unit coverage for midday defaulted-now writes (07:00/19:00 →
null at 11:00/12:00/13:00, explicit 07:00 still snaps) and a DB-backed
regression that a takenAt-only midday write leaves the 07:00 slot untaken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-05 15:59:02 +02:00
1 parent 7affbd0a11
commit 6871e8ea77
6 files changed
+205 -1

No files matched your search

@@ -155,6 +155,10 @@ async function postIntake(request: NextRequest, { params }: RouteParams) {
medicationId: id,
userTz: user.timezone,
incoming: incomingScheduledFor,
// Only a client-supplied `scheduledFor` names a real slot. A
// `takenAt`-only / defaulted-now write must not snap across the wide
// ±halfGap window onto a far slot (phantom morning dose).
instantIsExplicit: scheduledFor !== undefined,
});
// Idempotency check (explicit key or server-side dedup window)
@@ -234,6 +234,10 @@ async function postBulk(request: NextRequest): Promise<Response> {
medicationId: entry.medicationId,
userTz: user.timezone,
incoming: incomingScheduledFor,
// Only a client-supplied `scheduledFor` names a real slot. A
// `takenAt`-only / defaulted-now write must not snap across the
// wide ±halfGap window onto a far slot (phantom morning dose).
instantIsExplicit: entry.scheduledFor !== undefined,
});
const scheduledFor = canonicalSlot ?? incomingScheduledFor;