mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
fix(medications): keep an extra Apple Health dose off a slot that already holds one
A HealthKit dose event binds to a slot by window band, and a weekly schedule's band reaches a day either side of its slot. An extra dose logged the day after a scheduled one landed on that slot and overwrote it last-write-wins: the slot's time changed and the extra dose was stored nowhere. An external dose record whose band slot already carries a different take is now recorded as its own unscheduled dose. The dose history now lets the take stored on a slot claim it before an extra dose stored on its own instant, instead of following the order the rows were read in, so the extra dose reads as off-schedule.
This commit is contained in:
1 parent
cdd1e9c87f
commit
dad9a7778e
6 files changed
+602
-3
No files matched your search
@@ -1380,9 +1380,11 @@ describe("POST /api/medications/intake/bulk — v1.28 Apple Health dose events",
|
||||
});
|
||||
|
||||
it("stamps the externalId onto the pending slot row when the Apple dose converges by band", async () => {
|
||||
// Dedup pre-check first (empty), then the canonical-slot find returns
|
||||
// the worker-minted pending REMINDER row for the 19:00 slot.
|
||||
// Dedup pre-check first (empty), then the held-dose probe (no take on
|
||||
// the slot yet), then the canonical-slot find returns the worker-minted
|
||||
// pending REMINDER row for the 19:00 slot.
|
||||
vi.mocked(prisma.medicationIntakeEvent.findMany)
|
||||
.mockResolvedValueOnce([] as never)
|
||||
.mockResolvedValueOnce([] as never)
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
resolveForcedSlotForWrite,
|
||||
resolveSlotForWriteByBand,
|
||||
resolveSlotInstantForWrite,
|
||||
slotHeldByAnotherDose,
|
||||
} from "@/lib/medications/scheduling/slot-upsert";
|
||||
import { resolveInjectionSiteForWrite } from "@/lib/medications/injection-site-write";
|
||||
import {
|
||||
@@ -338,6 +339,9 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
let duplicates = 0;
|
||||
// External dose records kept as their own unscheduled dose because their
|
||||
// band slot already carried a different one.
|
||||
let extraDosesUnscheduled = 0;
|
||||
const skipped: Array<{ index: number; reason: string }> = [];
|
||||
const unstableShapes: UnstableExternalIdShape[] = [];
|
||||
// v1.4.39 W-MED — collect distinct `(medicationId, dayKey)` pairs
|
||||
@@ -443,6 +447,9 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
// a band decision. Pending echoes / skips carry no decision
|
||||
// (undefined → the upsert leaves an existing USER_PIN untouched).
|
||||
let attributionSource: "AUTO" | "USER_PIN" | undefined;
|
||||
// Set when the band slot is held by a different dose and this take is
|
||||
// recorded unscheduled; it must not converge onto any slot row below.
|
||||
let heldByAnotherDose = false;
|
||||
if (isExplicitTaken && entry.forceSlotInstant !== undefined) {
|
||||
canonicalSlot = await resolveForcedSlotForWrite({
|
||||
userId: user.id,
|
||||
@@ -486,6 +493,26 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
});
|
||||
canonicalSlot = attribution.slotInstant;
|
||||
attributionSource = "AUTO";
|
||||
// An external dose record (a HealthKit dose event) whose band slot
|
||||
// already carries a different recorded dose is an extra dose, not a
|
||||
// correction of that one. Converging would overwrite the slot's
|
||||
// take last-write-wins and the extra dose would exist nowhere; it is
|
||||
// recorded as its own unscheduled dose instead.
|
||||
if (
|
||||
canonicalSlot !== null &&
|
||||
entry.externalId !== undefined &&
|
||||
(await slotHeldByAnotherDose({
|
||||
userId: user.id,
|
||||
medicationId: entry.medicationId,
|
||||
canonicalSlot,
|
||||
takenAt: entry.takenAt as Date,
|
||||
externalId: entry.externalId,
|
||||
}))
|
||||
) {
|
||||
canonicalSlot = null;
|
||||
heldByAnotherDose = true;
|
||||
extraDosesUnscheduled += 1;
|
||||
}
|
||||
} else if (isExplicitSkip && entry.scheduledFor === undefined) {
|
||||
// v1.16.9 — a slot-less deliberate skip resolves by band
|
||||
// membership on the skip moment, exactly like a take does. The
|
||||
@@ -666,6 +693,7 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
// late-morning dose consuming the evening slot). It records
|
||||
// standalone (ad-hoc) instead.
|
||||
const existingSlotRow =
|
||||
!heldByAnotherDose &&
|
||||
entry.scheduledFor !== undefined &&
|
||||
mayConvergeOntoSuppliedSlot({
|
||||
skipped: entry.skipped,
|
||||
@@ -878,6 +906,7 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
updated,
|
||||
duplicates,
|
||||
skipped: skipped.length,
|
||||
extra_doses_unscheduled: extraDosesUnscheduled,
|
||||
...(unstableShapes.length > 0
|
||||
? unstableExternalIdMeta("medication.intake.bulk", unstableShapes)
|
||||
: {}),
|
||||
|
||||
Reference in new issue
Block a user