fix(medications): report the dose-history window doses were read over

The dose-history response reported the slot window: `from` clamped
to the medication's creation, `to` the request's end. Recorded doses
are now read from the requested start and, for a dose recorded ahead
of its slot, up to that slot, so the response reported a narrower
window than the rows it carried.

`from` and `to` now report the window recorded doses were read over.
Both fields keep their shape; the OpenAPI descriptions say what they
mean.

Refs #1028
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 08:12:23 +02:00
1 parent c41df33dd3
commit 3170a622d1
4 files changed
+37 -12

No files matched your search

+11 -6
View File
@@ -5961,21 +5961,23 @@ paths:
- in: query
name: from
schema:
description: Window start (inclusive). Defaults to 90 days before `to`; clamped to the medication's `createdAt` and a
366-day span floor.
description: Window start (inclusive). Defaults to 90 days before `to`; clamped to a 366-day span floor. Recorded doses
are returned from this instant; expected slots are not minted before the medication's `createdAt`.
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: Window start (inclusive). Defaults to 90 days before `to`; clamped to the medication's `createdAt` and a
366-day span floor.
description: Window start (inclusive). Defaults to 90 days before `to`; clamped to a 366-day span floor. Recorded doses
are returned from this instant; expected slots are not minted before the medication's `createdAt`.
- in: query
name: to
schema:
description: Window end (inclusive). Defaults to now. Must be ≥ `from`.
description: Window end (inclusive). Defaults to now. Must be ≥ `from`. A dose recorded ahead of its slot (the slot's
time within a day after `to`) is still returned.
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: Window end (inclusive). Defaults to now. Must be ≥ `from`.
description: Window end (inclusive). Defaults to now. Must be ≥ `from`. A dose recorded ahead of its slot (the slot's
time within a day after `to`) is still returned.
responses:
"200":
description: Dose-history ledger.
@@ -43671,10 +43673,13 @@ components:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: "Start of the window recorded doses were read over: the requested `from` after the span clamp."
to:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: "End of the window recorded doses were read over: the requested `to`, or the slot time of a dose recorded
ahead of its slot when that lies later (at most one day after `to`)."
family:
type: string
enum:
@@ -344,6 +344,9 @@ describe("GET /api/medications/[id]/dose-history — intakes recorded before cre
json.data.rows as Array<{ intake: { id: string | null } | null }>
).map((r) => r.intake?.id);
expect(ids).toContain("evt-earlier-today");
// The response reports the window the recorded doses were read
// over: the requested start, not the medication's creation.
expect(json.data.from).toBe(from);
} finally {
vi.useRealTimers();
}
@@ -410,6 +413,9 @@ describe("GET /api/medications/[id]/dose-history — early take (#1028 class)",
status: string;
intake: { id: string | null } | null;
}>;
// `to` reports how far recorded doses were read: up to the early
// dose's slot, past the request's own now.
expect(json.data.to).toBe(slot.toISOString());
const early = rows.find((r) => r.intake?.id === "evt-early");
expect(early?.status).toBe("taken_on_time");
// The read horizon stretches only to the recorded anchor: no later
@@ -313,9 +313,13 @@ export const GET = apiHandler(
meta: { row_count: serialized.length, family },
});
// The window the recorded doses were read over (#1028): from the
// requested start (slots additionally start at the medication's
// creation) to the read horizon, which reaches past `to` only for a dose
// recorded ahead of its slot.
return apiSuccess({
from: from.toISOString(),
to: to.toISOString(),
from: intakeFrom.toISOString(),
to: horizon.toISOString(),
family,
hasExpectedSlots: bands.length > 0,
rows: serialized,
+14 -4
View File
@@ -911,12 +911,14 @@ export const doseHistoryQuery = z.object({
.datetime({ offset: true })
.optional()
.describe(
"Window start (inclusive). Defaults to 90 days before `to`; clamped to the medication's `createdAt` and a 366-day span floor.",
"Window start (inclusive). Defaults to 90 days before `to`; clamped to a 366-day span floor. Recorded doses are returned from this instant; expected slots are not minted before the medication's `createdAt`.",
),
to: z.iso
.datetime({ offset: true })
.optional()
.describe("Window end (inclusive). Defaults to now. Must be ≥ `from`."),
.describe(
"Window end (inclusive). Defaults to now. Must be ≥ `from`. A dose recorded ahead of its slot (the slot's time within a day after `to`) is still returned.",
),
});
const doseHistoryRow = z
@@ -982,8 +984,16 @@ const doseHistoryRow = z
export const doseHistoryResponse = z
.object({
from: z.iso.datetime({ offset: true }),
to: z.iso.datetime({ offset: true }),
from: z.iso
.datetime({ offset: true })
.describe(
"Start of the window recorded doses were read over: the requested `from` after the span clamp.",
),
to: z.iso
.datetime({ offset: true })
.describe(
"End of the window recorded doses were read over: the requested `to`, or the slot time of a dose recorded ahead of its slot when that lies later (at most one day after `to`).",
),
family: z
.enum(["daily", "weekly", "one_shot", "none"])
.describe("Cadence family the window's slots were minted under."),