From 19ae91f80ae6b624a34830b54b1b62b19151185c Mon Sep 17 00:00:00 2001 From: haritowa <6107211+haritowa@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:11:53 +0000 Subject: [PATCH 01/11] fix(medications): end a course on the person's day, as a date "End medication" sent new Date().toISOString(), a full instant, against an endsOn that only accepts YYYY-MM-DD, so every press answered 422 and the toast said the medication could not be ended. The instant's date was also the UTC one, a day off around midnight for anyone not on UTC. The button now sends the person's calendar day in their timezone (userDayKey), the same day key the medication card uses. --- .../destructive-zone-section.test.ts | 40 +++++++++++++++++++ .../sections/destructive-zone-section.tsx | 22 ++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/components/medications/sections/__tests__/destructive-zone-section.test.ts diff --git a/src/components/medications/sections/__tests__/destructive-zone-section.test.ts b/src/components/medications/sections/__tests__/destructive-zone-section.test.ts new file mode 100644 index 000000000..b1702880f --- /dev/null +++ b/src/components/medications/sections/__tests__/destructive-zone-section.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; + +import { updateMedicationSchema } from "@/lib/validations/medication/create-update"; + +import { endCourseBody } from "../destructive-zone-section"; + +/** + * "End medication" sent `new Date().toISOString()`, a full instant, against an + * `endsOn` that only takes `YYYY-MM-DD`, so every press answered 422 and the + * toast said the medication could not be ended. The body now carries the + * person's own calendar day. + */ +describe("endCourseBody", () => { + it("is a date the update schema accepts", () => { + const body = endCourseBody( + new Date("2026-10-21T09:30:00.000Z"), + "Europe/Berlin", + ); + expect(body).toEqual({ endsOn: "2026-10-21" }); + expect(updateMedicationSchema.safeParse(body).success).toBe(true); + }); + + it("is the person's day, not the UTC one", () => { + // 23:30 UTC is already the next day in Warsaw and still the same day in + // New York. + const lateUtc = new Date("2026-10-20T23:30:00.000Z"); + expect(endCourseBody(lateUtc, "Europe/Warsaw").endsOn).toBe("2026-10-21"); + expect(endCourseBody(lateUtc, "America/New_York").endsOn).toBe( + "2026-10-20", + ); + }); + + it("is what the update schema refused before: a full instant is not a date", () => { + expect( + updateMedicationSchema.safeParse({ + endsOn: new Date("2026-10-21T09:30:00.000Z").toISOString(), + }).success, + ).toBe(false); + }); +}); diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx index b8e247d76..ef5853c38 100644 --- a/src/components/medications/sections/destructive-zone-section.tsx +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -13,7 +13,7 @@ * - Tier 1 Pausieren: PUT `{ active: false | true }`. Server derives * `pausedAt`. Switch wraps in ` - + {canEndCourse(courseStatus) ? ( + <> + -
-
-

- {t("medications.detail.zone.end.title")} -

-

- {t("medications.detail.zone.end.helper")} -

-
- -
- - {/* Tier 2 — Beenden confirm */} - - - - - {t("medications.detail.zone.end.dialogTitle", { - name: medicationName, - })} - - - {t("medications.detail.zone.end.dialogBody")} - - - - {t("common.cancel")} - +
+

+ {t("medications.detail.zone.end.title")} +

+

+ {t("medications.detail.zone.end.helper")} +

+
+ + + + {/* Tier 2 — Beenden confirm */} + + + + + {t("medications.detail.zone.end.dialogTitle", { + name: medicationName, + })} + + + {t("medications.detail.zone.end.dialogBody")} + + + + {t("common.cancel")} + { + e.preventDefault(); + void tier2End(); + }} + disabled={tier2Busy} + aria-busy={tier2Busy || undefined} + className="font-semibold" + > + {tier2Busy && ( + + + + + + ) : null} ); } From 9b8c76390185ff237b5e0e11750fe4c677a82510 Mon Sep 17 00:00:00 2001 From: haritowa <6107211+haritowa@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:18:07 +0000 Subject: [PATCH 03/11] refactor(medications): tidy the End guard after review The course-status prop uses the shared CourseStatus type instead of a spelled-out union, and the End confirmation's open state is dropped when End stops being offered, so a dialog closed by a refetch cannot pop up by itself once a later course offers End again. --- .../sections/destructive-zone-section.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx index e39b51b44..f627a22b4 100644 --- a/src/components/medications/sections/destructive-zone-section.tsx +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -50,6 +50,7 @@ import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; import { invalidateMedicationReads, queryKeys } from "@/lib/query-keys"; import { apiDelete, apiPut } from "@/lib/api/api-fetch"; +import type { CourseStatus } from "@/lib/medications/course-window"; import { DEFAULT_TIMEZONE, userDayKey } from "@/lib/tz/format"; /** @@ -69,9 +70,7 @@ export function endCourseBody(now: Date, timeZone: string): { endsOn: string } { * started it would turn into a one-day course today. Unknown (an older * server) keeps the button. */ -export function canEndCourse( - courseStatus: "UPCOMING" | "CURRENT" | "ENDED" | undefined, -): boolean { +export function canEndCourse(courseStatus: CourseStatus | undefined): boolean { return courseStatus === undefined || courseStatus === "CURRENT"; } @@ -94,19 +93,30 @@ export function LifecycleManageBody({ medicationName: string; active: boolean; /** Where today sits in the course (server-resolved). */ - courseStatus?: "UPCOMING" | "CURRENT" | "ENDED"; + courseStatus?: CourseStatus; onAfterAction?: () => void; }) { const { t } = useTranslations(); const queryClient = useQueryClient(); const { user } = useAuth(); const userTz = user?.timezone || DEFAULT_TIMEZONE; + const canEnd = canEndCourse(courseStatus); const [paused, setPaused] = useState(!active); const [tier1Busy, setTier1Busy] = useState(false); const [endDialogOpen, setEndDialogOpen] = useState(false); const [tier2Busy, setTier2Busy] = useState(false); + // A refetch can stop offering End while its confirmation is open (the + // course ended in another tab). Drop the open state with it, or the dialog + // would pop up by itself once a later course offers End again. Adjusted + // during render, React's pattern for state that follows a prop. + const [offeredEnd, setOfferedEnd] = useState(canEnd); + if (offeredEnd !== canEnd) { + setOfferedEnd(canEnd); + if (!canEnd) setEndDialogOpen(false); + } + async function tier1Toggle(pauseNext: boolean) { if (tier1Busy) return; setTier1Busy(true); @@ -184,7 +194,7 @@ export function LifecycleManageBody({ /> - {canEndCourse(courseStatus) ? ( + {canEnd ? ( <> From fa20b1fe3a50fc44b6a81ee2ef3c86811c28f019 Mon Sep 17 00:00:00 2001 From: haritowa <6107211+haritowa@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:04:26 +0000 Subject: [PATCH 04/11] refactor(medications): give End its own component End and its confirmation now live in EndCourseControl, which renders only while the course runs. When End stops being offered, the open dialog and its state unmount with it, so the state no longer has to be reset during render. --- .../sections/destructive-zone-section.tsx | 214 ++++++++++-------- 1 file changed, 115 insertions(+), 99 deletions(-) diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx index f627a22b4..541d4ad8e 100644 --- a/src/components/medications/sections/destructive-zone-section.tsx +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -98,24 +98,9 @@ export function LifecycleManageBody({ }) { const { t } = useTranslations(); const queryClient = useQueryClient(); - const { user } = useAuth(); - const userTz = user?.timezone || DEFAULT_TIMEZONE; - const canEnd = canEndCourse(courseStatus); const [paused, setPaused] = useState(!active); const [tier1Busy, setTier1Busy] = useState(false); - const [endDialogOpen, setEndDialogOpen] = useState(false); - const [tier2Busy, setTier2Busy] = useState(false); - - // A refetch can stop offering End while its confirmation is open (the - // course ended in another tab). Drop the open state with it, or the dialog - // would pop up by itself once a later course offers End again. Adjusted - // during render, React's pattern for state that follows a prop. - const [offeredEnd, setOfferedEnd] = useState(canEnd); - if (offeredEnd !== canEnd) { - setOfferedEnd(canEnd); - if (!canEnd) setEndDialogOpen(false); - } async function tier1Toggle(pauseNext: boolean) { if (tier1Busy) return; @@ -143,27 +128,6 @@ export function LifecycleManageBody({ } } - async function tier2End() { - if (tier2Busy) return; - setTier2Busy(true); - try { - await apiPut( - `/api/medications/${medicationId}`, - endCourseBody(new Date(), userTz), - ); - await invalidateMedicationReads(queryClient); - toast.success(t("medications.detail.zone.end.toast")); - setEndDialogOpen(false); - onAfterAction?.(); - } catch (failure) { - showErrorToast(t("medications.detail.zone.end.failed"), { - cause: failure, - }); - } finally { - setTier2Busy(false); - } - } - return (
- {canEnd ? ( - <> - - -
-
-

- {t("medications.detail.zone.end.title")} -

-

- {t("medications.detail.zone.end.helper")} -

-
- -
- - {/* Tier 2 — Beenden confirm */} - - - - - {t("medications.detail.zone.end.dialogTitle", { - name: medicationName, - })} - - - {t("medications.detail.zone.end.dialogBody")} - - - - {t("common.cancel")} - { - e.preventDefault(); - void tier2End(); - }} - disabled={tier2Busy} - aria-busy={tier2Busy || undefined} - className="font-semibold" - > - {tier2Busy && ( - - - - - - ) : null} + {canEndCourse(courseStatus) && ( + + )}
); } +/** + * Tier 2: End the course today, behind a confirmation. Rendered only while + * the course runs, so when End stops being offered (the course ended in + * another tab) the open dialog and its state go away with it. + */ +function EndCourseControl({ + medicationId, + medicationName, + onAfterAction, +}: { + medicationId: string; + medicationName: string; + onAfterAction?: () => void; +}) { + const { t } = useTranslations(); + const queryClient = useQueryClient(); + const { user } = useAuth(); + const userTz = user?.timezone || DEFAULT_TIMEZONE; + + const [endDialogOpen, setEndDialogOpen] = useState(false); + const [tier2Busy, setTier2Busy] = useState(false); + + async function tier2End() { + if (tier2Busy) return; + setTier2Busy(true); + try { + await apiPut( + `/api/medications/${medicationId}`, + endCourseBody(new Date(), userTz), + ); + await invalidateMedicationReads(queryClient); + toast.success(t("medications.detail.zone.end.toast")); + setEndDialogOpen(false); + onAfterAction?.(); + } catch (failure) { + showErrorToast(t("medications.detail.zone.end.failed"), { + cause: failure, + }); + } finally { + setTier2Busy(false); + } + } + + return ( + <> + + +
+
+

+ {t("medications.detail.zone.end.title")} +

+

+ {t("medications.detail.zone.end.helper")} +

+
+ +
+ + {/* Tier 2 — Beenden confirm */} + + + + + {t("medications.detail.zone.end.dialogTitle", { + name: medicationName, + })} + + + {t("medications.detail.zone.end.dialogBody")} + + + + {t("common.cancel")} + { + e.preventDefault(); + void tier2End(); + }} + disabled={tier2Busy} + aria-busy={tier2Busy || undefined} + className="font-semibold" + > + {tier2Busy && ( + + + + + + ); +} + /** * v1.7.0 — irreversible danger zone: purge intake history + delete the * medication. v1.16.1 — the body is bare rows (the caller's From 5e8fb126a33f23e543a6c2f7c5bf951fb3ee8a41 Mon Sep 17 00:00:00 2001 From: haritowa <6107211+haritowa@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:53:18 +0000 Subject: [PATCH 05/11] refactor(medications): hand the lifecycle controls the medication LifecycleManageBody takes the medication (id, name, active, course status) instead of one prop per field. The call site in medication-detail-tabs.tsx shrinks to one line, so the course status reaches the controls without growing a file the size ratchet holds. --- .../detail/medication-detail-tabs.tsx | 7 +----- .../destructive-zone-section.test.tsx | 10 ++++---- .../sections/destructive-zone-section.tsx | 24 +++++++++---------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/components/medications/detail/medication-detail-tabs.tsx b/src/components/medications/detail/medication-detail-tabs.tsx index ba0e5da5d..9cefc459a 100644 --- a/src/components/medications/detail/medication-detail-tabs.tsx +++ b/src/components/medications/detail/medication-detail-tabs.tsx @@ -923,12 +923,7 @@ export function MedicationDetailTabs({ defaultOpen={false} >
- +
{/* v1.40 (#1024) — the courses: when it was taken, and a new course once the last one has ended. */} diff --git a/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx b/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx index 3e71ba1da..5992803ea 100644 --- a/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx +++ b/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx @@ -66,10 +66,12 @@ describe("End is offered only for a running course", () => { , diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx index 541d4ad8e..ae1c193c2 100644 --- a/src/components/medications/sections/destructive-zone-section.tsx +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -83,23 +83,23 @@ const PAUSE_HELPER_ID = "medication-detail-pause-helper"; * Bare (no section / card chrome); the caller supplies its own grouping. */ export function LifecycleManageBody({ - medicationId, - medicationName, - active, - courseStatus, + medication, onAfterAction, }: { - medicationId: string; - medicationName: string; - active: boolean; - /** Where today sits in the course (server-resolved). */ - courseStatus?: CourseStatus; + medication: { + id: string; + name: string; + active: boolean; + /** Where today sits in the course (server-resolved). */ + courseStatus?: CourseStatus; + }; onAfterAction?: () => void; }) { const { t } = useTranslations(); const queryClient = useQueryClient(); + const medicationId = medication.id; - const [paused, setPaused] = useState(!active); + const [paused, setPaused] = useState(!medication.active); const [tier1Busy, setTier1Busy] = useState(false); async function tier1Toggle(pauseNext: boolean) { @@ -158,10 +158,10 @@ export function LifecycleManageBody({ /> - {canEndCourse(courseStatus) && ( + {canEndCourse(medication.courseStatus) && ( )} From 23a67f61e6c76ee67b5af144ea71d62056269662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 24 Oct 2026 22:20:06 +0200 Subject: [PATCH 06/11] feat(medications): let a course end at an instant A course's end date is a calendar day and counts in full: the doses of that day stay due, reminded and expected until local midnight. That is right for an end date set in the editor and wrong for "End course", whose dialog promises that nothing more is due. A course now carries `endedAt` (migration 0482, additive, nullable), the instant it was ended on its `endsOn` day, projected onto the medication next to `startsOn` / `endsOn` by the one course writer. From that instant: - the schedule engine emits no slot, so a dose later that day is not due, not reminded, not projected onto the today list and never expected by compliance, while a dose earlier that day stays inside the course; - the course reads ENDED (`courseStatusAt` / `courseStatusOn`), so the dose actions, `remindersActive` and the reminder gate stop at once, follow-ups of an earlier dose included; - every engine context builder, compliance context, dose history, Coach snapshot, doctor report and the adherence storyline read it, through one select and one picker for the medication window. Any write that moves a course's `endsOn` clears the instant, so an end date chosen in the editor keeps meaning the whole day. Backups carry it in both purposes and a restore projects it again. `courseStatusAt` moves next to the other course rules. Refs #1280 --- docs/api/openapi.yaml | 58 ++++++++-- .../migration.sql | 29 +++++ prisma/schema.prisma | 13 +++ .../recurrence-course-anchor-guard.test.ts | 27 ++++- src/__tests__/tz-day-paths-harness.test.ts | 1 + .../medications/[id]/dose-history/route.ts | 2 +- src/lib/__tests__/doctor-report-data.test.ts | 4 + src/lib/ai/coach/__tests__/snapshot.test.ts | 1 + .../compliance-block-pre-creation.test.ts | 1 + src/lib/ai/coach/snapshot.ts | 4 +- .../__tests__/compliance-courses.test.ts | 3 + .../__tests__/compliance-skip-parity.test.ts | 1 + .../__tests__/compliance-bundle.test.ts | 49 +++++++-- .../__tests__/compliance-pause-era.test.ts | 2 + .../__tests__/compliance.property.test.ts | 1 + .../compliance/__tests__/compliance.test.ts | 9 ++ src/lib/analytics/compliance/adapters.ts | 1 + src/lib/analytics/compliance/types.ts | 13 ++- src/lib/dashboard/meds-today.ts | 4 +- src/lib/doctor-report-types.ts | 4 +- src/lib/doctor-report/collect.ts | 9 +- .../__tests__/full-backup-payload.test.ts | 6 ++ src/lib/export/medications-backup-schema.ts | 3 + src/lib/export/medications-backup.ts | 4 + src/lib/export/medications-restore.ts | 9 +- .../adherence-storyline-engine-parity.test.ts | 14 +++ .../insights/derived/adherence-storyline.ts | 2 + .../jobs/__tests__/intake-auto-take.test.ts | 1 + src/lib/jobs/intake-auto-take.ts | 6 +- src/lib/mcp/tools/get-medication-schedule.ts | 4 +- .../__tests__/auto-mark-taken.test.ts | 25 +++++ .../compliance-payload-heatmap.test.ts | 1 + .../compliance-payload-pre-creation.test.ts | 1 + .../__tests__/course-window.test.ts | 88 +++++++++++++-- .../__tests__/intake-actionable.test.ts | 50 ++++++++- .../__tests__/intake-tracking.test.ts | 1 + src/lib/medications/auto-mark-taken.ts | 13 ++- src/lib/medications/auto-take.ts | 4 +- src/lib/medications/compliance-payload.ts | 2 + src/lib/medications/course-window.ts | 101 ++++++++++++++++-- src/lib/medications/courses.ts | 85 +++++++++++---- src/lib/medications/intake-actionable.ts | 6 +- src/lib/medications/intake-slot-dedup.ts | 13 +-- src/lib/medications/list-read.ts | 4 +- src/lib/medications/read-medication.ts | 4 +- .../__tests__/attribute-intake.test.ts | 1 + .../scheduling/__tests__/next-due.test.ts | 1 + .../scheduling/__tests__/recurrence.test.ts | 68 ++++++++++++ .../__tests__/resolve-slot-instant.test.ts | 1 + .../rolling-taken-dose-not-redue.test.ts | 1 + .../__tests__/schedule-eras.test.ts | 2 + src/lib/medications/scheduling/cadence.ts | 22 ++-- .../scheduling/project-today-intakes.ts | 7 +- src/lib/medications/scheduling/recurrence.ts | 52 ++++----- src/lib/medications/scheduling/slot-upsert.ts | 8 +- .../medications/scheduling/worker-helpers.ts | 3 + src/lib/openapi/routes/medications/schemas.ts | 21 +++- src/lib/targets/types.ts | 4 +- .../export-disaster-recovery.json | 28 ++--- .../backup-parity/export-portable.json | 16 +-- .../restore-disaster-recovery.json | 29 ++--- .../backup-parity/restore-portable.json | 17 +-- .../backup-parity/restore-skips.json | 1 + .../backup-parity/restore-version-1.json | 4 + .../backup-parity/seeded-account.json | 15 +-- tests/integration/backup-fixture.ts | 6 +- .../intake-late-take-future-slot.test.ts | 1 + .../medication-auto-mark-taken.test.ts | 2 + 68 files changed, 813 insertions(+), 180 deletions(-) create mode 100644 prisma/migrations/0482_medication_course_ended_at/migration.sql diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index b92152575..061a59857 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -42034,6 +42034,11 @@ components: format: date 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])))$ - type: "null" + endedAt: + anyOf: + - type: string + minLength: 1 + - type: "null" note: anyOf: - type: string @@ -53787,9 +53792,10 @@ components: type: boolean description: "v1.42.2 — whether the server sends dose reminders for this medication: true only when `notificationsEnabled` is on, the medication is `active`, its course has not ended (`endsOn` is today or - later on the person's clock, or unset), its intake is tracked (`trackIntake`), it is not `asNeeded`, and - `autoMarkTaken` is off. The same decision gates every server-side reminder send. A client that arms local - reminders arms them from this, not from `notificationsEnabled`. Read-only — computed, not stored." + later on the person's clock, or unset, and no `endedAt` has passed), its intake is tracked (`trackIntake`), + it is not `asNeeded`, and `autoMarkTaken` is off. The same decision gates every server-side reminder send. A + client that arms local reminders arms them from this, not from `notificationsEnabled`. Read-only — computed, + not stored." nextDueAt: anyOf: - type: string @@ -53813,6 +53819,15 @@ components: 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)))$ - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. + endedAt: + description: "#1280 — the instant the latest course was ended with \"End course\" (the course's own `endedAt`). From it + on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. + Read-only — written by the course writer only." + anyOf: + - 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)))$ + - type: "null" oneShot: type: boolean description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` / @@ -53910,7 +53925,8 @@ components: - ENDED description: "v1.39.4 — where today (on the user's clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all - day. Read-only — computed, not stored." + day, unless it was ended with \"End course\": then it is ENDED from that instant (`endedAt`). Read-only — + computed, not stored." intakeActionable: type: boolean description: "v1.39.4 — whether the medication offers taken / skip today: true only when it is `active`, its intake is @@ -54037,13 +54053,23 @@ components: 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])))$ - type: "null" description: Last day, inclusive. NULL = open (only the latest course). + endedAt: + anyOf: + - 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)))$ + - type: "null" + description: '#1280 — the instant "End course" ended this course, on its `endsOn` day. No dose slot at or after it is + due, reminded or expected, and the course is ENDED from it on. NULL when the whole `endsOn` day counts (an + end date set in the editor). Any edit that moves `endsOn` clears it.' status: type: string enum: - UPCOMING - CURRENT - ENDED - description: Where today, on the person's clock, sits in this course. + description: Where today, on the person's clock, sits in this course. A course with `endedAt` is ENDED on its `endsOn` + day already. takenDoses: type: integer minimum: 0 @@ -54058,6 +54084,7 @@ components: - id - startsOn - endsOn + - endedAt - status - takenDoses - note @@ -54244,6 +54271,15 @@ components: 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)))$ - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. + endedAt: + description: "#1280 — the instant the latest course was ended with \"End course\" (the course's own `endedAt`). From it + on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. + Read-only — written by the course writer only." + anyOf: + - 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)))$ + - type: "null" oneShot: type: boolean description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` / @@ -54336,7 +54372,8 @@ components: - ENDED description: "v1.39.4 — where today (on the user's clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all - day. Read-only — computed, not stored." + day, unless it was ended with \"End course\": then it is ENDED from that instant (`endedAt`). Read-only — + computed, not stored." intakeActionable: type: boolean description: "v1.39.4 — whether the medication offers taken / skip today: true only when it is `active`, its intake is @@ -93793,6 +93830,15 @@ components: 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)))$ - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. + endedAt: + description: "#1280 — the instant the latest course was ended with \"End course\" (the course's own `endedAt`). From it + on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. + Read-only — written by the course writer only." + anyOf: + - 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)))$ + - type: "null" oneShot: type: boolean description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` / diff --git a/prisma/migrations/0482_medication_course_ended_at/migration.sql b/prisma/migrations/0482_medication_course_ended_at/migration.sql new file mode 100644 index 000000000..0b7385f57 --- /dev/null +++ b/prisma/migrations/0482_medication_course_ended_at/migration.sql @@ -0,0 +1,29 @@ +-- "End course" ends a course at the moment it is pressed (#1280). +-- +-- A course's `ends_on` is a calendar day and counts in full: the doses of +-- that day stay due, reminded and expected until local midnight. That is the +-- right reading for an end date written in the course editor ("the last day +-- of the pack"), and the wrong one for "End course", whose dialog promises +-- that nothing more is due. `ended_at` records the instant the course was +-- ended that way: it lies on the `ends_on` day, a dose slot at or after it +-- is not part of the course, and the course reads as ended from then on. +-- NULL keeps the full-day reading every existing course has. +-- +-- `medications.ended_at` is the projection of the latest course's value, +-- written next to `starts_on` / `ends_on` by the one course writer, so the +-- engines that read the medication's window read it too. +-- +-- Nullable, no default, no backfill: no existing course was ended this way. +-- Reversibility: dropping both columns restores the full-day reading. +-- +-- @retry-on-lock-timeout + +DO $migration$ +BEGIN + PERFORM set_config('lock_timeout', '5s', true); + ALTER TABLE "medication_courses" + ADD COLUMN IF NOT EXISTS "ended_at" TIMESTAMPTZ(6); + ALTER TABLE "medications" + ADD COLUMN IF NOT EXISTS "ended_at" TIMESTAMPTZ(6); +END +$migration$; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b5ecaa161..a7399eef4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2903,6 +2903,12 @@ model Medication { /// minting slots past this date. NULL = no end date (chronic). /// When `oneShot` is true, `endsOn` always equals `startsOn`. endsOn DateTime? @map("ends_on") @db.Date + /// #1280 — the instant the latest course was ended with "End course", the + /// projection of `MedicationCourse.endedAt` written next to `startsOn` / + /// `endsOn` by `src/lib/medications/courses.ts` only. On the `endsOn` day; + /// no dose slot at or after it is due, reminded or expected, and the course + /// reads as ended from it on. NULL = the course runs to the end of `endsOn`. + endedAt DateTime? @map("ended_at") @db.Timestamptz(6) /// v1.5 — single-administration medication (flu shot, vaccine, /// post-op single dose, etc.). When true the medication has at /// most one MedicationIntakeEvent and `active` flips to false once @@ -3157,6 +3163,13 @@ model MedicationCourse { startsOn DateTime @map("starts_on") @db.Date /// Last day, inclusive. NULL = open (only the latest course may be open). endsOn DateTime? @map("ends_on") @db.Date + /// #1280 — set by "End course": the instant the course ended, on the + /// `endsOn` day. A dose slot at or after it is not part of the course (not + /// due, not reminded, not expected by compliance), so the course ends when + /// the person ends it rather than at local midnight. NULL = the whole + /// `endsOn` day counts, the reading of an end date set in the editor. Any + /// later write that moves `endsOn` clears it. + endedAt DateTime? @map("ended_at") @db.Timestamptz(6) /// Why this course, in the person's words. AES-256-GCM at rest. noteEncrypted Bytes? @map("note_encrypted") createdAt DateTime @default(now()) @map("created_at") diff --git a/src/__tests__/recurrence-course-anchor-guard.test.ts b/src/__tests__/recurrence-course-anchor-guard.test.ts index c7e9fd1ca..6ed1d14b8 100644 --- a/src/__tests__/recurrence-course-anchor-guard.test.ts +++ b/src/__tests__/recurrence-course-anchor-guard.test.ts @@ -15,6 +15,12 @@ * which it is. The scan asserts a floor on what it found, so a matcher that * stopped matching fails instead of agreeing with an empty tree. * + * #1280 — the same list carries the instant "End course" ended a course: + * every medication builder threads `endedAt` into the context, or the + * engine would keep a dose due after the course was ended. A builder passes + * when its block names `endedAt` or spreads a window that carries it + * (`...window` from `mintWindow`, `...medicationWindow(row)`). + * * Limit, stated: a literal passed straight to the engine without the * `RecurrenceContext` annotation is invisible here. Every medication path * today goes through one of the annotated builders below. @@ -69,19 +75,22 @@ function blockAt(text: string, open: number): string { interface Builder { file: string; declaresCourseStart: boolean; + threadsEndedAt: boolean; } +const THREADS_ENDED_AT = /\bendedAt\b|\.\.\.\s*(window\b|medicationWindow\()/; + function builders(): Builder[] { const out: Builder[] = []; for (const file of sourceFiles(SRC)) { const text = readSourceText(file); for (const m of text.matchAll(DECLARATION)) { const open = m.index! + m[0].length - 1; + const block = blockAt(text, open); out.push({ file: relative(ROOT, file), - declaresCourseStart: /rollingAnchor\s*:\s*"courseStart"/.test( - blockAt(text, open), - ), + declaresCourseStart: /rollingAnchor\s*:\s*"courseStart"/.test(block), + threadsEndedAt: THREADS_ENDED_AT.test(block), }); } } @@ -114,6 +123,18 @@ describe("the rolling course anchor is opt-in", () => { expect(missing).toEqual([]); }); + it("has every medication builder thread the instant a course was ended", () => { + const medication = builders().filter((b) => + MEDICATION_BUILDERS.includes(b.file), + ); + expect(medication.length).toBeGreaterThanOrEqual(3); + expect(medication.filter((b) => !b.threadsEndedAt)).toEqual([]); + // The matcher, held against what it must and must not accept. + expect(THREADS_ENDED_AT.test("endedAt: row.endedAt,")).toBe(true); + expect(THREADS_ENDED_AT.test("...medicationWindow(row),")).toBe(true); + expect(THREADS_ENDED_AT.test("endsOn: row.endsOn,")).toBe(false); + }); + it("keeps every other builder on the default (the last intake anchors)", () => { const declaring = builders().filter( (b) => NON_MEDICATION_BUILDERS.includes(b.file) && b.declaresCourseStart, diff --git a/src/__tests__/tz-day-paths-harness.test.ts b/src/__tests__/tz-day-paths-harness.test.ts index 598ea1108..edd870974 100644 --- a/src/__tests__/tz-day-paths-harness.test.ts +++ b/src/__tests__/tz-day-paths-harness.test.ts @@ -155,6 +155,7 @@ describe.each(EDGES)("day paths with the host in $zone", (edge) => { startsOn: null, // `endsOn` is a @db.Date column: the calendar date as UTC midnight. endsOn: dayKeyAsUtcMidnight(edge.localDay), + endedAt: null, }, edge.now, edge.zone, diff --git a/src/app/api/medications/[id]/dose-history/route.ts b/src/app/api/medications/[id]/dose-history/route.ts index 61b323b4d..900a7cded 100644 --- a/src/app/api/medications/[id]/dose-history/route.ts +++ b/src/app/api/medications/[id]/dose-history/route.ts @@ -234,7 +234,7 @@ export const GET = apiHandler( createdAt: medication.createdAt, }; const ctx = buildRecurrenceContext({ - medication: bandMedication, + medication: { ...bandMedication, endedAt: medication.endedAt }, userTz, lastIntakeAt, }); diff --git a/src/lib/__tests__/doctor-report-data.test.ts b/src/lib/__tests__/doctor-report-data.test.ts index 47bb06580..2d656fd0a 100644 --- a/src/lib/__tests__/doctor-report-data.test.ts +++ b/src/lib/__tests__/doctor-report-data.test.ts @@ -317,6 +317,7 @@ describe("buildLedgerCompliance — doctor-report adherence via the dose ledger" trackIntake: true, startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-04-01T00:00:00.000Z"), schedules: [dailySchedule()], @@ -354,6 +355,7 @@ describe("buildLedgerCompliance — doctor-report adherence via the dose ledger" trackIntake: true, startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-04-01T00:00:00.000Z"), schedules: [dailySchedule()], @@ -429,6 +431,7 @@ describe("buildLedgerCompliance — doctor-report adherence via the dose ledger" trackIntake: true, startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-04-01T00:00:00.000Z"), schedules: [dailySchedule()], @@ -460,6 +463,7 @@ describe("buildLedgerCompliance — doctor-report adherence via the dose ledger" trackIntake: false, startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-04-01T00:00:00.000Z"), // The schedule stays on record; it must not produce a rate. diff --git a/src/lib/ai/coach/__tests__/snapshot.test.ts b/src/lib/ai/coach/__tests__/snapshot.test.ts index 561a2fe47..0067ddfc8 100644 --- a/src/lib/ai/coach/__tests__/snapshot.test.ts +++ b/src/lib/ai/coach/__tests__/snapshot.test.ts @@ -706,6 +706,7 @@ describe("buildCoachSnapshot", () => { name: "Testdrug", startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt, schedules: [schedule], diff --git a/src/lib/ai/coach/snapshot-blocks/__tests__/compliance-block-pre-creation.test.ts b/src/lib/ai/coach/snapshot-blocks/__tests__/compliance-block-pre-creation.test.ts index cf920a749..7650a2c3f 100644 --- a/src/lib/ai/coach/snapshot-blocks/__tests__/compliance-block-pre-creation.test.ts +++ b/src/lib/ai/coach/snapshot-blocks/__tests__/compliance-block-pre-creation.test.ts @@ -37,6 +37,7 @@ for (const tz of ["Asia/Kolkata", "America/New_York"]) { { startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: d1(16, 5), schedules: [ diff --git a/src/lib/ai/coach/snapshot.ts b/src/lib/ai/coach/snapshot.ts index 2bc7a4abc..b572218a7 100644 --- a/src/lib/ai/coach/snapshot.ts +++ b/src/lib/ai/coach/snapshot.ts @@ -17,6 +17,7 @@ */ import { loadFreeWeekdaySpansForPrompt } from "@/lib/profile/life-circumstances"; import { prisma } from "@/lib/db"; +import { MEDICATION_WINDOW_SELECT } from "@/lib/medications/course-window"; import { extractFeatures } from "@/lib/insights/features"; import type { CoachDataCluster } from "@/lib/validations/coach-prefs"; import type { UnitPreferences } from "@/lib/measurements/display-transform"; @@ -511,8 +512,7 @@ async function buildCoachSnapshotImpl( where: { userId, asNeeded: false, ...TRACKED_INTAKE_WHERE }, select: { id: true, - startsOn: true, - endsOn: true, + ...MEDICATION_WINDOW_SELECT, oneShot: true, createdAt: true, schedules: { select: SCHEDULE_COMPLIANCE_SELECT }, diff --git a/src/lib/analytics/__tests__/compliance-courses.test.ts b/src/lib/analytics/__tests__/compliance-courses.test.ts index 16602da2f..081911517 100644 --- a/src/lib/analytics/__tests__/compliance-courses.test.ts +++ b/src/lib/analytics/__tests__/compliance-courses.test.ts @@ -58,6 +58,7 @@ const MEDICATION = { // The row projects the latest course. startsOn: COURSES[1].startsOn, endsOn: COURSES[1].endsOn, + endedAt: null, oneShot: false, createdAt: new Date("2026-03-15T10:00:00.000Z"), }; @@ -124,6 +125,7 @@ describe("the streak across two courses (v1.40, #1024)", () => { const A = { startsOn: dateOfDayKey("2026-06-01"), endsOn: dateOfDayKey("2026-06-10"), + endedAt: null, }; const B = { startsOn: dateOfDayKey("2026-06-13"), endsOn: null }; const NOW_B = new Date("2026-06-27T18:00:00.000Z"); @@ -141,6 +143,7 @@ describe("the streak across two courses (v1.40, #1024)", () => { { startsOn: B.startsOn, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-05-20T10:00:00.000Z"), courses: [A, B], diff --git a/src/lib/analytics/__tests__/compliance-skip-parity.test.ts b/src/lib/analytics/__tests__/compliance-skip-parity.test.ts index 21a9c57de..c7e6e9678 100644 --- a/src/lib/analytics/__tests__/compliance-skip-parity.test.ts +++ b/src/lib/analytics/__tests__/compliance-skip-parity.test.ts @@ -61,6 +61,7 @@ const medication = { active: true, startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-01-01T00:00:00.000Z"), schedules: [twiceDaily], diff --git a/src/lib/analytics/compliance/__tests__/compliance-bundle.test.ts b/src/lib/analytics/compliance/__tests__/compliance-bundle.test.ts index 49b82e255..6a54235c7 100644 --- a/src/lib/analytics/compliance/__tests__/compliance-bundle.test.ts +++ b/src/lib/analytics/compliance/__tests__/compliance-bundle.test.ts @@ -136,7 +136,13 @@ describe("buildMedicationComplianceBundle — parity with the per-window composi const schedules = [dailySchedule()]; const events = dailyFixtureEvents(); const ctx = buildComplianceMedicationContext( - { startsOn: null, endsOn: null, oneShot: false, createdAt }, + { + startsOn: null, + endsOn: null, + endedAt: null, + oneShot: false, + createdAt, + }, lastNonSkippedTakenAt(events), TZ, ); @@ -191,6 +197,7 @@ describe("buildMedicationComplianceBundle — parity with the per-window composi { startsOn: new Date(NOW.getTime() - 380 * DAY_MS), endsOn: null, + endedAt: null, oneShot: false, createdAt, }, @@ -223,7 +230,13 @@ describe("buildMedicationComplianceBundle — parity with the per-window composi it("empty schedule list: zeros short-circuit matches calculateCompliance", () => { const createdAt = new Date(NOW.getTime() - 10 * DAY_MS); const ctx = buildComplianceMedicationContext( - { startsOn: null, endsOn: null, oneShot: false, createdAt }, + { + startsOn: null, + endsOn: null, + endedAt: null, + oneShot: false, + createdAt, + }, null, TZ, ); @@ -250,7 +263,13 @@ describe("dailyComplianceRatesFromLedger — cadence-aware per-day series", () = const createdAt = new Date(NOW.getTime() - 60 * DAY_MS); const schedules = [weeklyMondaySchedule()]; const ctx = buildComplianceMedicationContext( - { startsOn: null, endsOn: null, oneShot: false, createdAt }, + { + startsOn: null, + endsOn: null, + endedAt: null, + oneShot: false, + createdAt, + }, null, TZ, ); @@ -300,7 +319,13 @@ describe("dailyComplianceRatesFromLedger — cadence-aware per-day series", () = const createdAt = new Date(NOW.getTime() - 60 * DAY_MS); const schedules = [weeklyMondaySchedule()]; const ctx = buildComplianceMedicationContext( - { startsOn: null, endsOn: null, oneShot: false, createdAt }, + { + startsOn: null, + endsOn: null, + endedAt: null, + oneShot: false, + createdAt, + }, null, TZ, ); @@ -383,7 +408,13 @@ describe("tallyLedgerRows — windowed tallies over one shared ledger", () => { const schedules = [dailySchedule()]; const events = dailyFixtureEvents(); const ctx = buildComplianceMedicationContext( - { startsOn: null, endsOn: null, oneShot: false, createdAt }, + { + startsOn: null, + endsOn: null, + endedAt: null, + oneShot: false, + createdAt, + }, lastNonSkippedTakenAt(events), TZ, ); @@ -407,7 +438,13 @@ describe("tallyLedgerRows — windowed tallies over one shared ledger", () => { const schedules = [dailySchedule()]; const events = dailyFixtureEvents(); const ctx = buildComplianceMedicationContext( - { startsOn: null, endsOn: null, oneShot: false, createdAt }, + { + startsOn: null, + endsOn: null, + endedAt: null, + oneShot: false, + createdAt, + }, lastNonSkippedTakenAt(events), TZ, ); diff --git a/src/lib/analytics/compliance/__tests__/compliance-pause-era.test.ts b/src/lib/analytics/compliance/__tests__/compliance-pause-era.test.ts index 6cc5cff8c..18e8e6000 100644 --- a/src/lib/analytics/compliance/__tests__/compliance-pause-era.test.ts +++ b/src/lib/analytics/compliance/__tests__/compliance-pause-era.test.ts @@ -78,6 +78,7 @@ describe("H-MED1 — paused days drop out of the compliance denominator", () => const medBase = { startsOn: createdAt, endsOn, + endedAt: null, oneShot: false, createdAt, }; @@ -209,6 +210,7 @@ describe("M-MED3 — the compliance rate honours archived schedule eras", () => const medBase = { startsOn: createdAt, endsOn, + endedAt: null, oneShot: false, createdAt, }; diff --git a/src/lib/analytics/compliance/__tests__/compliance.property.test.ts b/src/lib/analytics/compliance/__tests__/compliance.property.test.ts index f4152968e..d018c01ec 100644 --- a/src/lib/analytics/compliance/__tests__/compliance.property.test.ts +++ b/src/lib/analytics/compliance/__tests__/compliance.property.test.ts @@ -55,6 +55,7 @@ function context(tz: string) { { startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2024-06-01T00:00:00Z"), }, diff --git a/src/lib/analytics/compliance/__tests__/compliance.test.ts b/src/lib/analytics/compliance/__tests__/compliance.test.ts index 8ca4f82c6..1f6a49ddf 100644 --- a/src/lib/analytics/compliance/__tests__/compliance.test.ts +++ b/src/lib/analytics/compliance/__tests__/compliance.test.ts @@ -417,6 +417,7 @@ describe("calculateCompliance — engine-routed (medicationContext)", () => { return { startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2024-12-01T00:00:00Z"), lastIntakeAt: null, @@ -1002,6 +1003,7 @@ describe("expectedSlotCountForDay + lastNonSkippedTakenAt", () => { { startsOn: overrides.startsOn ?? null, endsOn: overrides.endsOn ?? null, + endedAt: null, oneShot: overrides.oneShot ?? false, createdAt: overrides.createdAt ?? new Date("2024-12-01T00:00:00Z"), }, @@ -1121,6 +1123,7 @@ describe("expectedSlotCountForDay + lastNonSkippedTakenAt", () => { { startsOn: medCtx.startsOn, endsOn: medCtx.endsOn, + endedAt: null, oneShot: medCtx.oneShot, createdAt: medCtx.createdAt, lastIntakeAt: medCtx.lastIntakeAt, @@ -1466,6 +1469,7 @@ describe("buildComplianceDisplay — two rows, cadence-scaled windows", () => { { startsOn: overrides.startsOn ?? null, endsOn: overrides.endsOn ?? null, + endedAt: null, oneShot: overrides.oneShot ?? false, // Created well before the windows so no day is excluded. createdAt: overrides.createdAt ?? new Date("2025-01-01T00:00:00Z"), @@ -2102,6 +2106,7 @@ describe("expectedSlotsBetween", () => { { startsOn: overrides.startsOn ?? null, endsOn: overrides.endsOn ?? null, + endedAt: null, oneShot: overrides.oneShot ?? false, createdAt: overrides.createdAt ?? new Date("2025-01-01T00:00:00Z"), }, @@ -2317,6 +2322,7 @@ describe("calculateCompliance — autoMissed forgotten doses (BUG #2 / #3)", () { startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2025-01-08T00:00:00Z"), }, @@ -2370,6 +2376,7 @@ describe("calculateCompliance — irregular twice-daily taker (BUG 3 verify)", ( { startsOn: null, endsOn: null, + endedAt: null, oneShot: false, // Created well before the window so age never clamps the denominator. createdAt: new Date("2025-01-01T00:00:00Z"), @@ -2717,6 +2724,7 @@ describe("tallyComplianceFromLedger — the unified % keystone", () => { return { startsOn: new Date("2026-06-01T00:00:00Z"), endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-06-01T00:00:00Z"), lastIntakeAt: null, @@ -3048,6 +3056,7 @@ describe("tallyComplianceFromLedger — the unified % keystone", () => { id: "x", startsOn: ctxFor().startsOn, endsOn: null, + endedAt: null, oneShot: false, createdAt: ctxFor().createdAt, }, diff --git a/src/lib/analytics/compliance/adapters.ts b/src/lib/analytics/compliance/adapters.ts index 5d0aa907c..ecd8e9938 100644 --- a/src/lib/analytics/compliance/adapters.ts +++ b/src/lib/analytics/compliance/adapters.ts @@ -65,6 +65,7 @@ export function toRecurrenceCtx( id: idTag, startsOn: window.startsOn, endsOn: window.endsOn, + endedAt: ctx.endedAt ?? null, oneShot: ctx.oneShot, createdAt: ctx.createdAt, }, diff --git a/src/lib/analytics/compliance/types.ts b/src/lib/analytics/compliance/types.ts index f4b298447..622a6996a 100644 --- a/src/lib/analytics/compliance/types.ts +++ b/src/lib/analytics/compliance/types.ts @@ -144,7 +144,7 @@ export const SCHEDULE_COMPLIANCE_SELECT = { * sibling of {@link SCHEDULE_COMPLIANCE_SELECT}. */ export const COURSES_COMPLIANCE_SELECT = { - select: { startsOn: true, endsOn: true }, + select: { startsOn: true, endsOn: true, endedAt: true }, orderBy: { startsOn: "asc" }, } as const; @@ -158,6 +158,13 @@ export const COURSES_COMPLIANCE_SELECT = { export interface ComplianceMedicationContext { startsOn: Date | null; endsOn: Date | null; + /** + * #1280 — the instant "End course" ended the latest course: no slot at or + * after it is expected. Earlier courses carry their own in `courses`. + * Every production context comes from `buildComplianceMedicationContext`, + * whose input requires it. + */ + endedAt?: Date | null; oneShot: boolean; createdAt: Date; lastIntakeAt: Date | null; @@ -222,7 +229,7 @@ export function slotInsideCourses( ): boolean { const courses = ctx.courses; if (!courses || courses.length < 2) return true; - return isInsideCourses(userDayKey(at, ctx.timeZone), courses); + return isInsideCourses(userDayKey(at, ctx.timeZone), courses, at); } /** @@ -245,6 +252,7 @@ export function buildComplianceMedicationContext( med: { startsOn: Date | null; endsOn: Date | null; + endedAt: Date | null; oneShot: boolean; createdAt: Date; /** v1.16.3 — thread the archived eras when the bundle carries them. */ @@ -260,6 +268,7 @@ export function buildComplianceMedicationContext( return { startsOn: med.startsOn, endsOn: med.endsOn, + endedAt: med.endedAt, oneShot: med.oneShot, createdAt: med.createdAt, lastIntakeAt, diff --git a/src/lib/dashboard/meds-today.ts b/src/lib/dashboard/meds-today.ts index f8b9d54af..9a444e09c 100644 --- a/src/lib/dashboard/meds-today.ts +++ b/src/lib/dashboard/meds-today.ts @@ -35,6 +35,7 @@ * summary, never an overdue state, until a fresh block arrives. */ import type { PrismaClient } from "@/generated/prisma/client"; +import { medicationWindow } from "@/lib/medications/course-window"; import { projectTodayIntakesAndRecompute } from "@/lib/medications/scheduling/project-today-intakes"; import { loadAutoTakeRollingAnchors } from "@/lib/medications/auto-take"; import { @@ -220,8 +221,7 @@ export async function buildMedsTodayBlock( const display = computeDisplayDue({ medication: { id: m.id, - startsOn: m.startsOn, - endsOn: m.endsOn, + ...medicationWindow(m), oneShot: m.oneShot, createdAt: m.createdAt, }, diff --git a/src/lib/doctor-report-types.ts b/src/lib/doctor-report-types.ts index 22e8a5aab..ec7c151c4 100644 --- a/src/lib/doctor-report-types.ts +++ b/src/lib/doctor-report-types.ts @@ -615,6 +615,8 @@ export interface DoctorReportComplianceMedication { trackIntake: boolean; startsOn: Date | null; endsOn: Date | null; + /** #1280 — the instant "End course" ended the latest course. */ + endedAt: Date | null; oneShot: boolean; createdAt: Date; schedules: ComplianceSchedule[]; @@ -622,7 +624,7 @@ export interface DoctorReportComplianceMedication { /** v1.25 — pause eras so paused days drop out of the denominator. */ pauseEras?: MedicationPauseEraLike[]; /** v1.40 (#1024) — the courses, so a gap between two expects nothing. */ - courses?: { startsOn: Date; endsOn: Date | null }[]; + courses?: { startsOn: Date; endsOn: Date | null; endedAt: Date | null }[]; } /** A window-bounded intake row keyed to its medication. */ diff --git a/src/lib/doctor-report/collect.ts b/src/lib/doctor-report/collect.ts index eb19713ef..79b431564 100644 --- a/src/lib/doctor-report/collect.ts +++ b/src/lib/doctor-report/collect.ts @@ -15,7 +15,11 @@ * * Pure data assembly — no auth, no rate limit, no audit. Idempotent. */ -import { courseStatusOn, dayKeyOfDate } from "@/lib/medications/course-window"; +import { + courseStatusOn, + dayKeyOfDate, + medicationWindow, +} from "@/lib/medications/course-window"; import { prisma } from "@/lib/db"; import { getEffectiveRange, @@ -484,8 +488,7 @@ export async function collectDoctorReportData( name: m.name, asNeeded: m.asNeeded, trackIntake: m.trackIntake, - startsOn: m.startsOn, - endsOn: m.endsOn, + ...medicationWindow(m), oneShot: m.oneShot, createdAt: m.createdAt, schedules: m.schedules, diff --git a/src/lib/export/__tests__/full-backup-payload.test.ts b/src/lib/export/__tests__/full-backup-payload.test.ts index f0734dfb7..fe9b5f872 100644 --- a/src/lib/export/__tests__/full-backup-payload.test.ts +++ b/src/lib/export/__tests__/full-backup-payload.test.ts @@ -210,6 +210,8 @@ function makePrisma() { id: "course-march", startsOn: new Date("2026-03-01T00:00:00.000Z"), endsOn: new Date("2026-03-07T00:00:00.000Z"), + // #1280 — ended with "End course" after the morning dose. + endedAt: new Date("2026-03-07T09:30:00.000Z"), noteEncrypted: encryptToBytes("the March flu"), createdAt: new Date("2026-03-01T08:00:00.000Z"), updatedAt: new Date("2026-03-01T08:05:00.000Z"), @@ -218,6 +220,7 @@ function makePrisma() { id: "course-june", startsOn: new Date("2026-06-10T00:00:00.000Z"), endsOn: null, + endedAt: null, noteEncrypted: null, createdAt: new Date("2026-06-10T08:00:00.000Z"), updatedAt: new Date("2026-06-10T08:05:00.000Z"), @@ -806,12 +809,14 @@ describe("buildFullBackupPayload — medication courses (v1.40, #1024)", () => { { startsOn: "2026-03-01", endsOn: "2026-03-07", + endedAt: "2026-03-07T09:30:00.000Z", note: "the March flu", createdAt: "2026-03-01T08:00:00.000Z", }, { startsOn: "2026-06-10", endsOn: null, + endedAt: null, note: null, createdAt: "2026-06-10T08:00:00.000Z", }, @@ -823,6 +828,7 @@ describe("buildFullBackupPayload — medication courses (v1.40, #1024)", () => { expect(march).toMatchObject({ startsOn: "2026-03-01", endsOn: "2026-03-07", + endedAt: "2026-03-07T09:30:00.000Z", note: null, createdAt: "2026-03-01T08:00:00.000Z", updatedAt: "2026-03-01T08:05:00.000Z", diff --git a/src/lib/export/medications-backup-schema.ts b/src/lib/export/medications-backup-schema.ts index 4b43396fd..632d729c3 100644 --- a/src/lib/export/medications-backup-schema.ts +++ b/src/lib/export/medications-backup-schema.ts @@ -123,6 +123,9 @@ export const medicationCourseSchema = z .object({ startsOn: z.iso.date(), endsOn: z.iso.date().nullable(), + // #1280 — the instant "End course" ended the course, on its `endsOn` + // day. A file without it (any written before) restores the full-day end. + endedAt: isoDateTime.nullable().optional(), note: z.string().max(280).nullable().optional(), // The note's ciphertext, disaster-recovery files only. A file without it // (any written before) carries the readable note, which the restore seals. diff --git a/src/lib/export/medications-backup.ts b/src/lib/export/medications-backup.ts index 34b8f98eb..be78b9be1 100644 --- a/src/lib/export/medications-backup.ts +++ b/src/lib/export/medications-backup.ts @@ -228,6 +228,10 @@ function serialiseMedicationHistory( courses: m.courses.map((c) => ({ startsOn: dayKeyOfDate(c.startsOn), endsOn: c.endsOn ? dayKeyOfDate(c.endsOn) : null, + // #1280 — when "End course" ended it. Both purposes: without it the + // restored course runs to midnight of its last day and expects the + // doses the person had stopped. + endedAt: c.endedAt ? c.endedAt.toISOString() : null, ...(disasterRecovery ? { note: null, diff --git a/src/lib/export/medications-restore.ts b/src/lib/export/medications-restore.ts index d71a2505f..80e1c72b3 100644 --- a/src/lib/export/medications-restore.ts +++ b/src/lib/export/medications-restore.ts @@ -349,6 +349,7 @@ async function restoreCourses( const courseRows: Array<{ startsOn: Date; endsOn: Date | null; + endedAt?: Date | null; note: string | null; noteEncrypted?: string | null; createdAt?: Date; @@ -358,6 +359,7 @@ async function restoreCourses( ? m.courses.map((c) => ({ startsOn: dateOfDayKey(c.startsOn), endsOn: c.endsOn ? dateOfDayKey(c.endsOn) : null, + endedAt: c.endedAt ? new Date(c.endedAt) : null, note: c.note ?? null, ...(c.noteEncrypted !== undefined ? { noteEncrypted: c.noteEncrypted } @@ -380,6 +382,7 @@ async function restoreCourses( userId: ownerId, startsOn: c.startsOn, endsOn: c.endsOn, + endedAt: c.endedAt ?? null, // Ciphertext verbatim from a disaster-recovery file; the readable note // of a portable or older file sealed here. noteEncrypted: @@ -397,7 +400,11 @@ async function restoreCourses( const window = projectCourseWindow(courseRows); await tx.medication.update({ where: { id: created.id }, - data: { startsOn: window.startsOn, endsOn: window.endsOn }, + data: { + startsOn: window.startsOn, + endsOn: window.endsOn, + endedAt: window.endedAt, + }, }); } diff --git a/src/lib/insights/derived/__tests__/adherence-storyline-engine-parity.test.ts b/src/lib/insights/derived/__tests__/adherence-storyline-engine-parity.test.ts index cf100babc..c82ea1bf4 100644 --- a/src/lib/insights/derived/__tests__/adherence-storyline-engine-parity.test.ts +++ b/src/lib/insights/derived/__tests__/adherence-storyline-engine-parity.test.ts @@ -210,4 +210,18 @@ describe("adherence storyline course window", () => { ); expect(where.OR[1].endsOn.gte.toISOString()).toBe(lastDay.toISOString()); }); + + it("leaves out a course ended with End course on its last day (#1280)", async () => { + await buildAdherenceStoryline( + "user-1", + "UTC", + new Date("2026-06-21T12:00:00.000Z"), + ); + // The candidate read is the one that bounds the window by `endsOn`. + const where = mocks.medicationFindMany.mock.calls + .map(([args]) => (args as { where?: Record }).where) + .reverse() + .find((w) => w && "OR" in w); + expect(where).toHaveProperty("endedAt", null); + }); }); diff --git a/src/lib/insights/derived/adherence-storyline.ts b/src/lib/insights/derived/adherence-storyline.ts index 637e6f746..7dd40f298 100644 --- a/src/lib/insights/derived/adherence-storyline.ts +++ b/src/lib/insights/derived/adherence-storyline.ts @@ -184,6 +184,8 @@ export async function buildAdherenceStoryline( { endsOn: null }, { endsOn: { gte: dayKeyAsUtcMidnight(todayKey) } }, ], + // #1280 — a course ended with "End course" is over from that instant. + endedAt: null, }, select: { name: true, treatmentClass: true }, }); diff --git a/src/lib/jobs/__tests__/intake-auto-take.test.ts b/src/lib/jobs/__tests__/intake-auto-take.test.ts index bee2b010c..9fdc31a95 100644 --- a/src/lib/jobs/__tests__/intake-auto-take.test.ts +++ b/src/lib/jobs/__tests__/intake-auto-take.test.ts @@ -108,6 +108,7 @@ function medication( userId: overrides.userId ?? "u1", startsOn: overrides.startsOn ?? null, endsOn: overrides.endsOn ?? null, + endedAt: null, oneShot: overrides.oneShot ?? false, createdAt: overrides.createdAt ?? new Date("2026-01-01T00:00:00Z"), autoMarkTakenSince: diff --git a/src/lib/jobs/intake-auto-take.ts b/src/lib/jobs/intake-auto-take.ts index 2b7c7cc9f..1e6998558 100644 --- a/src/lib/jobs/intake-auto-take.ts +++ b/src/lib/jobs/intake-auto-take.ts @@ -53,6 +53,7 @@ * owns alone, so the two never write the same slot. */ import type { PrismaClient } from "@/generated/prisma/client"; +import { MEDICATION_WINDOW_SELECT } from "@/lib/medications/course-window"; import { invalidateUserMedications } from "@/lib/cache/invalidate"; import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking"; @@ -154,6 +155,8 @@ interface AutoTakeMedication { userId: string; startsOn: Date | null; endsOn: Date | null; + /** #1280 — no slot at or after the instant the course was ended is filled. */ + endedAt: Date | null; oneShot: boolean; createdAt: Date; autoMarkTakenSince: Date | null; @@ -262,8 +265,7 @@ export async function runIntakeAutoTakePass( select: { id: true, userId: true, - startsOn: true, - endsOn: true, + ...MEDICATION_WINDOW_SELECT, oneShot: true, createdAt: true, autoMarkTakenSince: true, diff --git a/src/lib/mcp/tools/get-medication-schedule.ts b/src/lib/mcp/tools/get-medication-schedule.ts index c69a3a525..51d2bb535 100644 --- a/src/lib/mcp/tools/get-medication-schedule.ts +++ b/src/lib/mcp/tools/get-medication-schedule.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; +import { medicationWindow } from "@/lib/medications/course-window"; import { prisma } from "@/lib/db"; import { annotate } from "@/lib/logging/context"; @@ -152,8 +153,7 @@ export const getMedicationScheduleTool: McpToolDefinition = { const display = computeDisplayDue({ medication: { id: m.id, - startsOn: m.startsOn, - endsOn: m.endsOn, + ...medicationWindow(m), oneShot: m.oneShot, createdAt: m.createdAt, }, diff --git a/src/lib/medications/__tests__/auto-mark-taken.test.ts b/src/lib/medications/__tests__/auto-mark-taken.test.ts index 7c072ee97..433d4ae18 100644 --- a/src/lib/medications/__tests__/auto-mark-taken.test.ts +++ b/src/lib/medications/__tests__/auto-mark-taken.test.ts @@ -25,6 +25,7 @@ describe("sendsMedicationReminders", () => { trackIntake: (bits & 8) !== 0, asNeeded: (bits & 16) !== 0, endsOn: ended ? new Date("2026-06-09T00:00:00.000Z") : null, + endedAt: null, }; expect(sendsMedicationReminders(med, at), JSON.stringify(med)).toBe( med.notificationsEnabled && @@ -44,6 +45,7 @@ describe("sendsMedicationReminders", () => { active: true, trackIntake: true, asNeeded: false, + endedAt: null, }; expect( sendsMedicationReminders( @@ -66,6 +68,29 @@ describe("sendsMedicationReminders", () => { ).toBe(false); }); + it("stops at the instant End course ended the course (#1280)", () => { + const med = { + notificationsEnabled: true, + autoMarkTaken: false, + active: true, + trackIntake: true, + asNeeded: false, + endsOn: new Date("2026-06-10T00:00:00.000Z"), + endedAt: new Date("2026-06-10T09:00:00.000Z"), + }; + // 10:00Z is after the instant: no reminder for the rest of the day, + // follow-ups of an earlier dose included. + expect(sendsMedicationReminders(med, at)).toBe(false); + expect( + sendsMedicationReminders(med, { + ...at, + now: new Date("2026-06-10T08:59:00.000Z"), + }), + ).toBe(true); + // The same last day without the instant reminds all day. + expect(sendsMedicationReminders({ ...med, endedAt: null }, at)).toBe(true); + }); + it("is the decision at every gate and on every published DTO", () => { const src = (rel: string) => readFileSync(join(process.cwd(), "src", rel), "utf8"); diff --git a/src/lib/medications/__tests__/compliance-payload-heatmap.test.ts b/src/lib/medications/__tests__/compliance-payload-heatmap.test.ts index 1039e91a5..1d6503741 100644 --- a/src/lib/medications/__tests__/compliance-payload-heatmap.test.ts +++ b/src/lib/medications/__tests__/compliance-payload-heatmap.test.ts @@ -55,6 +55,7 @@ it("counts an off-schedule row as taken only when it has a taken time", async () createdAt: new Date("2026-01-01T00:00:00Z"), startsOn: null, endsOn: null, + endedAt: null, oneShot: false, asNeeded: false, trackIntake: true, diff --git a/src/lib/medications/__tests__/compliance-payload-pre-creation.test.ts b/src/lib/medications/__tests__/compliance-payload-pre-creation.test.ts index 3c44e767b..2dacdc3b6 100644 --- a/src/lib/medications/__tests__/compliance-payload-pre-creation.test.ts +++ b/src/lib/medications/__tests__/compliance-payload-pre-creation.test.ts @@ -67,6 +67,7 @@ for (const tz of ["Asia/Kolkata", "America/New_York"]) { createdAt: d1(16, 5), startsOn: new Date("2026-09-23T00:00:00Z"), endsOn: new Date("2026-09-28T00:00:00Z"), + endedAt: null, oneShot: false, asNeeded: false, trackIntake: true, diff --git a/src/lib/medications/__tests__/course-window.test.ts b/src/lib/medications/__tests__/course-window.test.ts index f85f7ce91..9f3c83e00 100644 --- a/src/lib/medications/__tests__/course-window.test.ts +++ b/src/lib/medications/__tests__/course-window.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { canStartCourse, courseFromWindow, + courseStatusAt, courseStatusOn, dateOfDayKey, isInsideCourses, @@ -26,11 +27,25 @@ describe("projectCourseWindow", () => { expect(projectCourseWindow([JUNE, MARCH])).toEqual({ startsOn: d("2026-06-10"), endsOn: d("2026-06-16"), + endedAt: null, }); }); it("is empty with no course (chronic since creation)", () => { - expect(projectCourseWindow([])).toEqual({ startsOn: null, endsOn: null }); + expect(projectCourseWindow([])).toEqual({ + startsOn: null, + endsOn: null, + endedAt: null, + }); + }); + + it("carries the instant the latest course was ended", () => { + const endedAt = new Date("2026-06-16T10:00:00.000Z"); + expect(projectCourseWindow([MARCH, { ...JUNE, endedAt }])).toEqual({ + startsOn: d("2026-06-10"), + endsOn: d("2026-06-16"), + endedAt, + }); }); it.each([ @@ -116,17 +131,76 @@ describe("courseStatusOn", () => { }); describe("isInsideCourses", () => { + /** A slot at noon UTC of a day, the instant the day key belongs to. */ + const noon = (key: string) => new Date(`${key}T12:00:00.000Z`); + const inside = (key: string, courses: CourseSpan[]) => + isInsideCourses(key, courses, noon(key)); + it("admits the course days and not the gap", () => { const courses = [MARCH, JUNE]; - expect(isInsideCourses("2026-03-01", courses)).toBe(true); - expect(isInsideCourses("2026-03-07", courses)).toBe(true); - expect(isInsideCourses("2026-03-08", courses)).toBe(false); - expect(isInsideCourses("2026-06-16", courses)).toBe(true); - expect(isInsideCourses("2026-06-17", courses)).toBe(false); + expect(inside("2026-03-01", courses)).toBe(true); + expect(inside("2026-03-07", courses)).toBe(true); + expect(inside("2026-03-08", courses)).toBe(false); + expect(inside("2026-06-16", courses)).toBe(true); + expect(inside("2026-06-17", courses)).toBe(false); }); it("places no restriction without courses", () => { - expect(isInsideCourses("2026-03-08", [])).toBe(true); + expect(inside("2026-03-08", [])).toBe(true); + }); + + it("stops an ended course at the instant it was ended (#1280)", () => { + // MARCH ended with "End course" at 10:00Z on its last day: the morning + // slot belongs to it, the evening one does not. + const ended = { ...MARCH, endedAt: new Date("2026-03-07T10:00:00.000Z") }; + const courses = [ended, JUNE]; + const day = "2026-03-07"; + expect( + isInsideCourses(day, courses, new Date("2026-03-07T06:00:00.000Z")), + ).toBe(true); + expect( + isInsideCourses(day, courses, new Date("2026-03-07T10:00:00.000Z")), + ).toBe(false); + expect( + isInsideCourses(day, courses, new Date("2026-03-07T18:00:00.000Z")), + ).toBe(false); + // The days before the last one are untouched. + expect(inside("2026-03-06", courses)).toBe(true); + }); +}); + +describe("ending a course now (#1280)", () => { + const ENDED_AT = new Date("2026-06-12T10:00:00.000Z"); + const ended = { ...JUNE, endsOn: d("2026-06-12"), endedAt: ENDED_AT }; + + it("reads ENDED on the last day already, by day and by instant", () => { + expect(courseStatusOn(ended, "2026-06-12")).toBe("ENDED"); + expect(courseStatusOn(ended, "2026-06-11")).toBe("CURRENT"); + // The same end date set in the editor keeps the whole day. + expect(courseStatusOn({ ...ended, endedAt: null }, "2026-06-12")).toBe( + "CURRENT", + ); + const at = (iso: string) => courseStatusAt(ended, new Date(iso), "UTC"); + expect(at("2026-06-12T09:59:59.999Z")).toBe("CURRENT"); + expect(at("2026-06-12T10:00:00.000Z")).toBe("ENDED"); + expect(at("2026-06-12T22:00:00.000Z")).toBe("ENDED"); + }); + + it("lets a new course start once the ended one is behind it", () => { + const base = { active: true, oneShot: false, todayKey: "2026-06-12" }; + expect(canStartCourse({ ...base, courses: [MARCH, ended] })).toBe(true); + // The next course may start tomorrow, not on the ended course's day. + const next = course("2026-06-13", null); + expect(validateCourses([MARCH, ended, next], "2026-06-12", false)).toBe( + null, + ); + expect( + validateCourses( + [MARCH, ended, course("2026-06-12", null)], + "2026-06-12", + false, + ), + ).toBe("overlap"); }); }); diff --git a/src/lib/medications/__tests__/intake-actionable.test.ts b/src/lib/medications/__tests__/intake-actionable.test.ts index 93112e2f4..5c145d1d0 100644 --- a/src/lib/medications/__tests__/intake-actionable.test.ts +++ b/src/lib/medications/__tests__/intake-actionable.test.ts @@ -32,6 +32,7 @@ const base = { trackIntake: true, startsOn: day("2026-09-01"), endsOn: day("2026-09-20"), + endedAt: null, }; describe.each(ZONES)("course window in %s", (tz) => { @@ -42,13 +43,19 @@ describe.each(ZONES)("course window in %s", (tz) => { [23, 59], ] as const) { const r = resolveIntakeActionability(base, at(tz, 20, h, m), tz); - expect(r).toEqual({ courseStatus: "CURRENT", intakeActionable: true }); + expect(r).toEqual({ + courseStatus: "CURRENT", + intakeActionable: true, + }); } }); it("is ended from local midnight of the day after endsOn", () => { const r = resolveIntakeActionability(base, at(tz, 21, 0, 0), tz); - expect(r).toEqual({ courseStatus: "ENDED", intakeActionable: false }); + expect(r).toEqual({ + courseStatus: "ENDED", + intakeActionable: false, + }); }); it("is upcoming until local midnight of startsOn", () => { @@ -80,7 +87,10 @@ describe("resolveIntakeActionability", () => { it("refuses a paused or archived medication inside its course", () => { const r = resolveIntakeActionability({ ...base, active: false }, now, tz); - expect(r).toEqual({ courseStatus: "CURRENT", intakeActionable: false }); + expect(r).toEqual({ + courseStatus: "CURRENT", + intakeActionable: false, + }); }); it("refuses a record-only medication inside its course", () => { @@ -89,7 +99,10 @@ describe("resolveIntakeActionability", () => { now, tz, ); - expect(r).toEqual({ courseStatus: "CURRENT", intakeActionable: false }); + expect(r).toEqual({ + courseStatus: "CURRENT", + intakeActionable: false, + }); }); it("reports ENDED for a course that is also paused", () => { @@ -101,3 +114,32 @@ describe("resolveIntakeActionability", () => { expect(r.courseStatus).toBe("ENDED"); }); }); + +describe("a course ended with End course (#1280)", () => { + const ended = { + ...base, + endsOn: day("2026-09-20"), + endedAt: new Date("2026-09-20T10:00:00.000Z"), + }; + + it("is running until the instant and ended from it, not from midnight", () => { + const before = resolveIntakeActionability( + ended, + new Date("2026-09-20T09:59:00.000Z"), + "UTC", + ); + expect(before).toEqual({ + courseStatus: "CURRENT", + intakeActionable: true, + }); + const after = resolveIntakeActionability( + ended, + new Date("2026-09-20T10:00:00.000Z"), + "UTC", + ); + expect(after).toEqual({ + courseStatus: "ENDED", + intakeActionable: false, + }); + }); +}); diff --git a/src/lib/medications/__tests__/intake-tracking.test.ts b/src/lib/medications/__tests__/intake-tracking.test.ts index 518ba224f..643859e19 100644 --- a/src/lib/medications/__tests__/intake-tracking.test.ts +++ b/src/lib/medications/__tests__/intake-tracking.test.ts @@ -93,6 +93,7 @@ describe("buildCompliancePayload", () => { createdAt: new Date("2026-01-01T00:00:00Z"), startsOn: null, endsOn: null, + endedAt: null, oneShot: false, asNeeded: false, trackIntake: false, diff --git a/src/lib/medications/auto-mark-taken.ts b/src/lib/medications/auto-mark-taken.ts index b8644d5a5..8c8c3e355 100644 --- a/src/lib/medications/auto-mark-taken.ts +++ b/src/lib/medications/auto-mark-taken.ts @@ -12,10 +12,12 @@ * worker expands no slot past it), it has doses that fall due (intake * tracked, not as-needed), and its doses are not recorded automatically * (`autoMarkTaken`). A course that has not started yet still reminds once - * it does. The person's own reminder preference stays stored untouched: + * it does. #1280 — a course ended with "End course" stops reminding at the + * instant it was ended (`endedAt`), including the follow-up nags of a dose + * that fell due before it. The person's own reminder preference stays stored untouched: * switching auto-mark-taken off restores whatever they had chosen. */ -import { courseStatusAt } from "@/lib/medications/scheduling/recurrence"; +import { courseStatusAt } from "@/lib/medications/course-window"; export interface ReminderGateMedication { notificationsEnabled: boolean; @@ -24,6 +26,7 @@ export interface ReminderGateMedication { trackIntake: boolean; asNeeded: boolean; endsOn: Date | null; + endedAt: Date | null; } export function sendsMedicationReminders( @@ -37,7 +40,11 @@ export function sendsMedicationReminders( medication.trackIntake && !medication.asNeeded && courseStatusAt( - { startsOn: null, endsOn: medication.endsOn }, + { + startsOn: null, + endsOn: medication.endsOn, + endedAt: medication.endedAt, + }, at.now, at.timeZone, ) !== "ENDED" diff --git a/src/lib/medications/auto-take.ts b/src/lib/medications/auto-take.ts index 271f0b3b8..b8c12d3b3 100644 --- a/src/lib/medications/auto-take.ts +++ b/src/lib/medications/auto-take.ts @@ -14,6 +14,7 @@ * effective `lastIntakeAt`. */ import type { PrismaClient } from "@/generated/prisma/client"; +import { MEDICATION_WINDOW_SELECT } from "@/lib/medications/course-window"; import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking"; import { @@ -361,8 +362,7 @@ export async function loadAutoTakeRollingAnchors( }, select: { id: true, - startsOn: true, - endsOn: true, + ...MEDICATION_WINDOW_SELECT, oneShot: true, createdAt: true, autoMarkTakenSince: true, diff --git a/src/lib/medications/compliance-payload.ts b/src/lib/medications/compliance-payload.ts index 992e7825b..c7042e23b 100644 --- a/src/lib/medications/compliance-payload.ts +++ b/src/lib/medications/compliance-payload.ts @@ -92,6 +92,8 @@ export interface ComplianceMedicationInput { createdAt: Date; startsOn: Date | null; endsOn: Date | null; + /** #1280 — the instant "End course" ended the latest course. */ + endedAt: Date | null; oneShot: boolean; asNeeded: boolean; /** v1.39.1 (#1033) — intake tracking off: no adherence, heatmap empty. */ diff --git a/src/lib/medications/course-window.ts b/src/lib/medications/course-window.ts index 97f3fc997..333feff9a 100644 --- a/src/lib/medications/course-window.ts +++ b/src/lib/medications/course-window.ts @@ -15,13 +15,27 @@ * projection written at the last course write stays correct as days pass. A * write that would break the invariant is refused (`validateCourses`). * + * #1280 — "End course" ends a course at an instant, `endedAt`, which lies on + * its `endsOn` day. A slot at or after it is not part of the course, and the + * course reads as ENDED from then on. An end date set in the editor carries + * no `endedAt` and keeps the full-day reading. The instant is what keeps + * compliance honest both ways: a dose taken earlier that day stays inside + * the course and counts, and a later slot of the same day is neither due nor + * missed. + * * Pure: no database, no clock (callers pass `today`). */ import { dateOnlyKey, dayKeyAsUtcMidnight } from "@/lib/tz/date-only"; +import { userDayKey } from "@/lib/tz/format"; export interface CourseSpan { startsOn: Date; endsOn: Date | null; + /** + * #1280 — the instant "End course" ended this course, on its `endsOn` day. + * Absent or null: the course runs to the end of `endsOn`. + */ + endedAt?: Date | null; } export type CourseStatus = "UPCOMING" | "CURRENT" | "ENDED"; @@ -32,6 +46,26 @@ export const dayKeyOfDate = dateOnlyKey; /** A `YYYY-MM-DD` key as the `@db.Date` value Prisma writes. */ export const dateOfDayKey = dayKeyAsUtcMidnight; +/** + * The medication's window as the engines read it: the latest course's + * `startsOn` / `endsOn` and, since #1280, the instant "End course" ended it. + * One select and one picker, so a reader cannot take the dates and drop the + * instant. + */ +export const MEDICATION_WINDOW_SELECT = { + startsOn: true, + endsOn: true, + endedAt: true, +} as const; + +export function medicationWindow(m: { + startsOn: Date | null; + endsOn: Date | null; + endedAt: Date | null; +}): { startsOn: Date | null; endsOn: Date | null; endedAt: Date | null } { + return { startsOn: m.startsOn, endsOn: m.endsOn, endedAt: m.endedAt }; +} + /** Courses ascending by start. */ export function sortCourses(courses: readonly T[]): T[] { return [...courses].sort( @@ -40,24 +74,71 @@ export function sortCourses(courses: readonly T[]): T[] { } /** - * The window `Medication.startsOn/endsOn` carries: the latest course, or - * nothing at all when the medication has no course (chronic since creation). + * The window `Medication.startsOn/endsOn/endedAt` carries: the latest course, + * or nothing at all when the medication has no course (chronic since + * creation). */ export function projectCourseWindow(courses: readonly CourseSpan[]): { startsOn: Date | null; endsOn: Date | null; + endedAt: Date | null; } { - if (courses.length === 0) return { startsOn: null, endsOn: null }; + if (courses.length === 0) { + return { startsOn: null, endsOn: null, endedAt: null }; + } const latest = sortCourses(courses)[courses.length - 1]; - return { startsOn: latest.startsOn, endsOn: latest.endsOn }; + return { + startsOn: latest.startsOn, + endsOn: latest.endsOn, + endedAt: latest.endedAt ?? null, + }; } -/** Where `todayKey` sits in one course. Both ends inclusive. */ +/** + * Where `todayKey` sits in one course. Both ends inclusive, except that a + * course ended with "End course" is ENDED on its `endsOn` day already: the + * instant it ended lies on that day, and every read that asks about that day + * comes after the instant (it is written as the moment of the request). + */ export function courseStatusOn( course: CourseSpan, todayKey: string, ): CourseStatus { if (todayKey < dayKeyOfDate(course.startsOn)) return "UPCOMING"; + if (course.endsOn) { + const endKey = dayKeyOfDate(course.endsOn); + if (todayKey > endKey) return "ENDED"; + if (course.endedAt && todayKey === endKey) return "ENDED"; + } + return "CURRENT"; +} + +/** + * Where `now` sits in a medication's window (`Medication.startsOn/endsOn/ + * endedAt`, the projection of its latest course), in calendar days on the + * person's clock. `startsOn` / `endsOn` are calendar dates and are read as + * such; only `now` is converted to its local day. Both ends are inclusive: + * a course that ends today is CURRENT for the whole of today, one that + * starts today is CURRENT from local midnight. A medication without dates is + * CURRENT. #1280 — a course ended with "End course" is ENDED from the + * instant it was ended (`endedAt`), not from the next local midnight. + */ +export function courseStatusAt( + course: { + startsOn: Date | null; + endsOn: Date | null; + endedAt?: Date | null; + }, + now: Date, + timeZone: string, +): CourseStatus { + if (course.endedAt && now.getTime() >= course.endedAt.getTime()) { + return "ENDED"; + } + const todayKey = userDayKey(now, timeZone); + if (course.startsOn && todayKey < dayKeyOfDate(course.startsOn)) { + return "UPCOMING"; + } if (course.endsOn && todayKey > dayKeyOfDate(course.endsOn)) return "ENDED"; return "CURRENT"; } @@ -104,18 +185,22 @@ export function validateCourses( } /** - * Whether an instant's local day (`dayKey`) falls inside any course. No - * courses means no restriction (chronic since creation). + * Whether a slot at `at`, whose local day is `dayKey`, falls inside any + * course. No courses means no restriction (chronic since creation). A course + * ended with "End course" stops at its `endedAt`: a slot at or after it is + * outside, even on the `endsOn` day. */ export function isInsideCourses( dayKey: string, courses: readonly CourseSpan[], + at: Date, ): boolean { if (courses.length === 0) return true; return courses.some( (c) => dayKey >= dayKeyOfDate(c.startsOn) && - (c.endsOn === null || dayKey <= dayKeyOfDate(c.endsOn)), + (c.endsOn === null || dayKey <= dayKeyOfDate(c.endsOn)) && + (!c.endedAt || at.getTime() < c.endedAt.getTime()), ); } diff --git a/src/lib/medications/courses.ts b/src/lib/medications/courses.ts index da6766994..2e6ce8772 100644 --- a/src/lib/medications/courses.ts +++ b/src/lib/medications/courses.ts @@ -9,6 +9,10 @@ * columns for a medication that has courses, so reminders, the next-due line * and `intakeActionable` — which read the columns — can never disagree with * the course list. + * + * #1280 — a course may carry `endedAt`, the instant it was ended at. Every + * write that moves a course's `endsOn` clears it: an end date chosen in the + * editor means the whole day. */ import type { Prisma } from "@/generated/prisma/client"; import { decryptFromBytes, encryptToBytes } from "@/lib/ai/coach/bytes-codec"; @@ -99,9 +103,19 @@ export interface CourseRow { id: string; startsOn: Date; endsOn: Date | null; + endedAt: Date | null; noteEncrypted: Uint8Array | null; } +/** The course columns every course read and write selects. */ +const COURSE_ROW_SELECT = { + id: true, + startsOn: true, + endsOn: true, + endedAt: true, + noteEncrypted: true, +} as const; + async function lockMedication(tx: Tx, medicationId: string) { await tx.$queryRaw` SELECT pg_advisory_xact_lock( @@ -119,6 +133,7 @@ async function loadOwned(tx: Tx, userId: string, medicationId: string) { oneShot: true, startsOn: true, endsOn: true, + endedAt: true, createdAt: true, }, }); @@ -126,12 +141,16 @@ async function loadOwned(tx: Tx, userId: string, medicationId: string) { const courses = await tx.medicationCourse.findMany({ where: { medicationId }, orderBy: [{ startsOn: "asc" }, { id: "asc" }], - select: { id: true, startsOn: true, endsOn: true, noteEncrypted: true }, + select: COURSE_ROW_SELECT, }); return { medication, courses }; } -type ProjectedWindow = { startsOn: Date | null; endsOn: Date | null }; +type ProjectedWindow = { + startsOn: Date | null; + endsOn: Date | null; + endedAt: Date | null; +}; /** * Write the medication's window from its courses. The only writer. @@ -148,13 +167,17 @@ async function project(tx: Tx, medicationId: string): Promise { }), tx.medicationCourse.findMany({ where: { medicationId }, - select: { startsOn: true, endsOn: true }, + select: { startsOn: true, endsOn: true, endedAt: true }, }), ]); const window = projectCourseWindow(courses); await tx.medication.update({ where: { id: medicationId }, - data: { startsOn: window.startsOn, endsOn: window.endsOn }, + data: { + startsOn: window.startsOn, + endsOn: window.endsOn, + endedAt: window.endedAt, + }, }); if (before && windowWidens(before, window)) { await moveAutoTakeFloorToNow(tx, medicationId); @@ -244,7 +267,7 @@ export async function createCourse( endsOn: args.endsOn, noteEncrypted: encryptNote(args.note) ?? null, }, - select: { id: true, startsOn: true, endsOn: true, noteEncrypted: true }, + select: COURSE_ROW_SELECT, }); await project(tx, args.medicationId); return created; @@ -269,9 +292,11 @@ export async function updateCourse( ); const current = courses.find((c) => c.id === args.courseId); if (!current) throw new CourseWriteError("notFound"); + const endsOn = args.endsOn === undefined ? current.endsOn : args.endsOn; const next = { startsOn: args.startsOn ?? current.startsOn, - endsOn: args.endsOn === undefined ? current.endsOn : args.endsOn, + endsOn, + endedAt: keptEndedAt(current, endsOn), }; refuseIfInvalid( courses.map((c) => (c.id === current.id ? next : c)), @@ -284,9 +309,10 @@ export async function updateCourse( data: { startsOn: next.startsOn, endsOn: next.endsOn, + endedAt: next.endedAt, ...(noteEncrypted !== undefined && { noteEncrypted }), }, - select: { id: true, startsOn: true, endsOn: true, noteEncrypted: true }, + select: COURSE_ROW_SELECT, }); await project(tx, args.medicationId); return updated; @@ -385,14 +411,15 @@ async function writeCurrentWindow( ); if (latest) { + const edited = { ...next, endedAt: keptEndedAt(latest, next.endsOn) }; refuseIfInvalid( - courses.map((c) => (c.id === latest.id ? next : c)), + courses.map((c) => (c.id === latest.id ? edited : c)), todayKey, medication.oneShot, ); await tx.medicationCourse.update({ where: { id: latest.id }, - data: next, + data: edited, }); } else { refuseIfInvalid([next], todayKey, medication.oneShot); @@ -407,6 +434,22 @@ async function writeCurrentWindow( return project(tx, args.medicationId); } +/** + * #1280 — the `endedAt` a course keeps through an edit: its own while its + * `endsOn` stays the day it was ended on, none once the end moves. An end + * date written in the editor is a whole day; only "End course" ends a course + * at an instant. + */ +function keptEndedAt( + course: { endsOn: Date | null; endedAt: Date | null }, + nextEndsOn: Date | null, +): Date | null { + if (!course.endedAt || !course.endsOn || !nextEndsOn) return null; + return course.endsOn.getTime() === nextEndsOn.getTime() + ? course.endedAt + : null; +} + /** * Whether the window a PUT is about to write would be refused. Checked * BEFORE the medication row is updated, so a refused window leaves the row @@ -424,7 +467,7 @@ export async function checkCurrentWindow( const courses = await prisma.medicationCourse.findMany({ where: { medicationId: args.medicationId, userId: args.userId }, orderBy: [{ startsOn: "asc" }, { id: "asc" }], - select: { id: true, startsOn: true, endsOn: true }, + select: { id: true, startsOn: true, endsOn: true, endedAt: true }, }); const latest = courses.at(-1); if (args.startsOn === null && args.endsOn === null) { @@ -438,7 +481,13 @@ export async function checkCurrentWindow( args.timeZone, ); return validateCourses( - latest ? courses.map((c) => (c.id === latest.id ? next : c)) : [next], + latest + ? courses.map((c) => + c.id === latest.id + ? { ...next, endedAt: keptEndedAt(latest, next.endsOn) } + : c, + ) + : [next], todayKey, args.oneShot, ); @@ -450,6 +499,11 @@ export interface MedicationCourseDto { id: string; startsOn: string; endsOn: string | null; + /** + * #1280 — the instant "End course" ended this course (ISO), or null when + * the whole `endsOn` day counts. + */ + endedAt: string | null; status: CourseStatus; /** Doses logged as taken whose slot falls on a day of this course. */ takenDoses: number; @@ -473,13 +527,7 @@ export async function loadCoursesFor( const rows = await prisma.medicationCourse.findMany({ where: { medicationId: { in: medicationIds } }, orderBy: [{ startsOn: "asc" }, { id: "asc" }], - select: { - id: true, - medicationId: true, - startsOn: true, - endsOn: true, - noteEncrypted: true, - }, + select: { ...COURSE_ROW_SELECT, medicationId: true }, }); for (const { medicationId, ...row } of rows) { const list = byMedication.get(medicationId) ?? []; @@ -530,6 +578,7 @@ export function buildCourseFields( id: c.id, startsOn: dayKeyOfDate(c.startsOn), endsOn: c.endsOn ? dayKeyOfDate(c.endsOn) : null, + endedAt: c.endedAt ? c.endedAt.toISOString() : null, status: courseStatusOn(c, todayKey), takenDoses: takenByCourse.get(c.id) ?? 0, note: c.noteEncrypted ? decryptFromBytes(c.noteEncrypted) : null, diff --git a/src/lib/medications/intake-actionable.ts b/src/lib/medications/intake-actionable.ts index f20072559..cd586a720 100644 --- a/src/lib/medications/intake-actionable.ts +++ b/src/lib/medications/intake-actionable.ts @@ -22,11 +22,14 @@ * * Logging a past dose from the history is a different verb and is not * gated here; an ended course can still be completed after the fact. + * + * #1280 — a course ended at an instant (`endedAt`) is ENDED from it, so the + * action row goes away at once instead of at midnight. */ import { courseStatusAt, type CourseStatus, -} from "@/lib/medications/scheduling/recurrence"; +} from "@/lib/medications/course-window"; import { isRecordOnly } from "@/lib/medications/intake-tracking"; export interface IntakeActionability { @@ -40,6 +43,7 @@ export function resolveIntakeActionability( trackIntake: boolean; startsOn: Date | null; endsOn: Date | null; + endedAt: Date | null; }, now: Date, timeZone: string, diff --git a/src/lib/medications/intake-slot-dedup.ts b/src/lib/medications/intake-slot-dedup.ts index 96d1903b8..982436f60 100644 --- a/src/lib/medications/intake-slot-dedup.ts +++ b/src/lib/medications/intake-slot-dedup.ts @@ -43,6 +43,10 @@ * unregistered queue silently never drains. */ import { prisma } from "@/lib/db"; +import { + MEDICATION_WINDOW_SELECT, + medicationWindow, +} from "@/lib/medications/course-window"; import { invalidateUserMedications } from "@/lib/cache/invalidate"; import { restoreForIntake } from "@/lib/medications/inventory/consumption"; import { isP2002 } from "@/lib/prisma-errors"; @@ -201,8 +205,7 @@ export async function dedupeUserIntakeSlots( select: { id: true, userId: true, - startsOn: true, - endsOn: true, + ...MEDICATION_WINDOW_SELECT, oneShot: true, createdAt: true, schedules: { @@ -348,8 +351,7 @@ export async function dedupeUserIntakeSlots( const canonical = resolveCanonicalSlotInstant({ medication: { id: med.id, - startsOn: med.startsOn, - endsOn: med.endsOn, + ...medicationWindow(med), oneShot: med.oneShot, createdAt: med.createdAt, schedules: med.schedules, @@ -551,8 +553,7 @@ async function convergeAdhocRowsOntoPendingSlots(input: { const medication: AttributeIntakeMedication = { id: med.id, - startsOn: med.startsOn, - endsOn: med.endsOn, + ...medicationWindow(med), oneShot: med.oneShot, createdAt: med.createdAt, schedules: med.schedules, diff --git a/src/lib/medications/list-read.ts b/src/lib/medications/list-read.ts index 9c916274b..1a99bddef 100644 --- a/src/lib/medications/list-read.ts +++ b/src/lib/medications/list-read.ts @@ -13,6 +13,7 @@ * readers. Mirrors `src/lib/dashboard/snapshot-read.ts` for the dashboard. */ import { sendsMedicationReminders } from "@/lib/medications/auto-mark-taken"; +import { medicationWindow } from "@/lib/medications/course-window"; import { loadAutoTakeRollingAnchors } from "@/lib/medications/auto-take"; import { prisma } from "@/lib/db"; import { annotate, getEvent } from "@/lib/logging/context"; @@ -244,8 +245,7 @@ export async function buildMedicationsList( const display = computeDisplayDue({ medication: { id: m.id, - startsOn: m.startsOn, - endsOn: m.endsOn, + ...medicationWindow(m), oneShot: m.oneShot, createdAt: m.createdAt, }, diff --git a/src/lib/medications/read-medication.ts b/src/lib/medications/read-medication.ts index 6f7adea8d..c41504c7f 100644 --- a/src/lib/medications/read-medication.ts +++ b/src/lib/medications/read-medication.ts @@ -8,6 +8,7 @@ * medication is not the record's. */ import type { Medication, MedicationSchedule } from "@/generated/prisma/client"; +import { medicationWindow } from "@/lib/medications/course-window"; import { prisma } from "@/lib/db"; import { sendsMedicationReminders } from "@/lib/medications/auto-mark-taken"; import { toIngredientDtos } from "@/lib/medications/ingredients/composition"; @@ -113,8 +114,7 @@ async function loadDisplayDue( const display = computeDisplayDue({ medication: { id: medication.id, - startsOn: medication.startsOn, - endsOn: medication.endsOn, + ...medicationWindow(medication), oneShot: medication.oneShot, createdAt: medication.createdAt, }, diff --git a/src/lib/medications/scheduling/__tests__/attribute-intake.test.ts b/src/lib/medications/scheduling/__tests__/attribute-intake.test.ts index 4c35f0cf9..752efc30c 100644 --- a/src/lib/medications/scheduling/__tests__/attribute-intake.test.ts +++ b/src/lib/medications/scheduling/__tests__/attribute-intake.test.ts @@ -51,6 +51,7 @@ function med( id: "med-1", startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-01-01T00:00:00Z"), schedules, diff --git a/src/lib/medications/scheduling/__tests__/next-due.test.ts b/src/lib/medications/scheduling/__tests__/next-due.test.ts index 6a6fb298f..a7be8bbbe 100644 --- a/src/lib/medications/scheduling/__tests__/next-due.test.ts +++ b/src/lib/medications/scheduling/__tests__/next-due.test.ts @@ -35,6 +35,7 @@ function makeMedication( id: "med-1", startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: d("2026-01-01T00:00:00Z"), ...overrides, diff --git a/src/lib/medications/scheduling/__tests__/recurrence.test.ts b/src/lib/medications/scheduling/__tests__/recurrence.test.ts index a2e7fac2f..4dbd67861 100644 --- a/src/lib/medications/scheduling/__tests__/recurrence.test.ts +++ b/src/lib/medications/scheduling/__tests__/recurrence.test.ts @@ -1295,3 +1295,71 @@ describe("occurrencesBetween — CYCLIC", () => { ); }); }); + +// ──────────────────────────────────────────────────────────────────── +// #1280 — a course ended with "End course" stops at that instant +// ──────────────────────────────────────────────────────────────────── + +describe("a course ended at an instant (#1280)", () => { + // 08:00 and 20:00 in Berlin (UTC+2 in June), ended at 12:00 local on the + // 10th, which is also its last day. + const schedule = makeSchedule({ + rrule: "FREQ=DAILY", + timesOfDay: ["08:00", "20:00"], + }); + const ended = makeCtx({ + medication: { + endsOn: d("2026-06-10T00:00:00Z"), + endedAt: d("2026-06-10T10:00:00Z"), + }, + }); + const wholeDay = makeCtx({ + medication: { endsOn: d("2026-06-10T00:00:00Z") }, + }); + + it("emits the slot before the instant and none after it", () => { + const from = d("2026-06-09T22:00:00Z"); + const to = d("2026-06-10T21:59:59Z"); + expect( + occurrencesBetween(schedule, from, to, ended).map((o) => + o.at.toISOString(), + ), + ).toEqual(["2026-06-10T06:00:00.000Z"]); + // The same end date without the instant keeps the evening dose. + expect(occurrencesBetween(schedule, from, to, wholeDay)).toHaveLength(2); + }); + + it("has no next dose once the course was ended", () => { + expect( + nextOccurrenceAfter(schedule, d("2026-06-10T10:30:00Z"), ended), + ).toBeNull(); + expect( + nextOccurrenceAfter(schedule, d("2026-06-10T10:30:00Z"), wholeDay)?.at, + ).toEqual(d("2026-06-10T18:00:00Z")); + }); + + it("stops a rolling cadence whose next dose falls after the instant", () => { + const rolling = makeSchedule({ + rollingIntervalDays: 1, + timesOfDay: ["20:00"], + }); + const ctx = makeCtx({ + lastIntakeAt: d("2026-06-09T18:00:00Z"), + medication: { + endsOn: d("2026-06-10T00:00:00Z"), + endedAt: d("2026-06-10T10:00:00Z"), + }, + }); + expect( + occurrencesBetween( + rolling, + d("2026-06-09T22:00:00Z"), + d("2026-06-10T21:59:59Z"), + ctx, + ), + ).toEqual([]); + expect( + nextOccurrenceAfter(rolling, d("2026-06-10T10:30:00Z"), ctx), + ).toBeNull(); + }); +}); diff --git a/src/lib/medications/scheduling/__tests__/resolve-slot-instant.test.ts b/src/lib/medications/scheduling/__tests__/resolve-slot-instant.test.ts index 1cc19bba7..136fbd599 100644 --- a/src/lib/medications/scheduling/__tests__/resolve-slot-instant.test.ts +++ b/src/lib/medications/scheduling/__tests__/resolve-slot-instant.test.ts @@ -46,6 +46,7 @@ function makeMedication( id: "med-1", startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-01-01T00:00:00Z"), schedules, diff --git a/src/lib/medications/scheduling/__tests__/rolling-taken-dose-not-redue.test.ts b/src/lib/medications/scheduling/__tests__/rolling-taken-dose-not-redue.test.ts index 1ff7974b3..3883bf121 100644 --- a/src/lib/medications/scheduling/__tests__/rolling-taken-dose-not-redue.test.ts +++ b/src/lib/medications/scheduling/__tests__/rolling-taken-dose-not-redue.test.ts @@ -34,6 +34,7 @@ const medication: WorkerMedicationRow = { id: "med-1", startsOn: new Date("2026-06-02T00:00:00.000Z"), endsOn: null, + endedAt: null, oneShot: false, createdAt: new Date("2026-06-01T00:00:00.000Z"), }; diff --git a/src/lib/medications/scheduling/__tests__/schedule-eras.test.ts b/src/lib/medications/scheduling/__tests__/schedule-eras.test.ts index 7d02534df..20ebf15f7 100644 --- a/src/lib/medications/scheduling/__tests__/schedule-eras.test.ts +++ b/src/lib/medications/scheduling/__tests__/schedule-eras.test.ts @@ -77,6 +77,7 @@ const medication = { id: "med-1", startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt, }; @@ -384,6 +385,7 @@ describe("write-attribution era pick", () => { id: "med-1", startsOn: null, endsOn: null, + endedAt: null, oneShot: false, createdAt, schedules: [ diff --git a/src/lib/medications/scheduling/cadence.ts b/src/lib/medications/scheduling/cadence.ts index c2875337d..44dbcb7f3 100644 --- a/src/lib/medications/scheduling/cadence.ts +++ b/src/lib/medications/scheduling/cadence.ts @@ -128,6 +128,8 @@ export interface ScheduleLike { export interface CadenceEngineContext { startsOn: Date | null; endsOn: Date | null; + /** #1280 — when "End course" ended the latest course (see the engine). */ + endedAt?: Date | null; oneShot: boolean; createdAt: Date; /** Latest non-skipped intake — only read by rolling cadences. */ @@ -150,18 +152,17 @@ export interface CadenceEngineContext { } /** v1.40 (#1024) — the span a timeline expands over (see `courses`). */ -function mintWindow(engineCtx: CadenceEngineContext): { - startsOn: Date | null; - endsOn: Date | null; -} { +function mintWindow(engineCtx: CadenceEngineContext) { const courses = engineCtx.courses; + const endedAt = engineCtx.endedAt ?? null; if (!courses || courses.length < 2) { - return { startsOn: engineCtx.startsOn, endsOn: engineCtx.endsOn }; + return { startsOn: engineCtx.startsOn, endsOn: engineCtx.endsOn, endedAt }; } const sorted = sortCourses(courses); return { startsOn: sorted[0].startsOn, endsOn: sorted[sorted.length - 1].endsOn, + endedAt, }; } @@ -169,7 +170,7 @@ function mintWindow(engineCtx: CadenceEngineContext): { function onCourseDay(engineCtx: CadenceEngineContext, at: Date): boolean { const courses = engineCtx.courses; if (!courses || courses.length < 2) return true; - return isInsideCourses(userDayKey(at, engineCtx.timeZone), courses); + return isInsideCourses(userDayKey(at, engineCtx.timeZone), courses, at); } /** Build a `CanonicalSchedule` from a `ScheduleLike` + its index. */ @@ -201,8 +202,7 @@ function toRecurrenceContext( return { medication: { id: "cadence-med", - startsOn: window.startsOn, - endsOn: window.endsOn, + ...window, oneShot: engineCtx.oneShot, createdAt: engineCtx.createdAt, }, @@ -908,16 +908,14 @@ function missedFromLedger( const window = mintWindow(engineCtx); const medication: BandMinterMedication = { id: "missed-tally", - startsOn: window.startsOn, - endsOn: window.endsOn, + ...window, oneShot: engineCtx.oneShot, createdAt: engineCtx.createdAt, }; const recurrenceCtx: RecurrenceContext = { medication: { id: "missed-tally", - startsOn: window.startsOn, - endsOn: window.endsOn, + ...window, oneShot: engineCtx.oneShot, createdAt: engineCtx.createdAt, }, diff --git a/src/lib/medications/scheduling/project-today-intakes.ts b/src/lib/medications/scheduling/project-today-intakes.ts index 20bfd2354..9ae4bddc4 100644 --- a/src/lib/medications/scheduling/project-today-intakes.ts +++ b/src/lib/medications/scheduling/project-today-intakes.ts @@ -26,6 +26,7 @@ * Returns the counts so the caller can surface them on `annotate(...)`. */ import { prisma } from "@/lib/db"; +import { MEDICATION_WINDOW_SELECT } from "@/lib/medications/course-window"; import { buildCanonicalSchedule, buildRecurrenceContext, @@ -58,8 +59,7 @@ export async function projectTodayIntakesAndRecompute(input: { where: { userId, active: true, ...TRACKED_INTAKE_WHERE }, select: { id: true, - startsOn: true, - endsOn: true, + ...MEDICATION_WINDOW_SELECT, oneShot: true, createdAt: true, // The live era start: no slot anchored before it is projected. Turning @@ -160,6 +160,9 @@ export async function projectTodayIntakesAndRecompute(input: { if (!Number.isFinite(h) || !Number.isFinite(m)) continue; const scheduledFor = localHmAsUtc(now, userTz, h, m); if (isBeforeLiveEra(scheduledFor, eraStart)) continue; + // #1280 — a course ended with "End course" expects nothing from + // that instant on; the schedule still emits its earlier slots today. + if (med.endedAt && scheduledFor >= med.endedAt) continue; projected.push({ medicationId: schedule.medicationId, scheduledFor, diff --git a/src/lib/medications/scheduling/recurrence.ts b/src/lib/medications/scheduling/recurrence.ts index 091ae2e4c..f2477ca41 100644 --- a/src/lib/medications/scheduling/recurrence.ts +++ b/src/lib/medications/scheduling/recurrence.ts @@ -143,6 +143,8 @@ export interface RecurrenceContext { id: string; startsOn: Date | null; endsOn: Date | null; + /** #1280 — when "End course" ended it; see `beforeCourseEnded`. */ + endedAt?: Date | null; oneShot: boolean; createdAt: Date; }; @@ -211,31 +213,6 @@ const ONE_MINUTE_MS = 60_000; // Public API // ──────────────────────────────────────────────────────────────────── -/** - * Where `now` sits relative to a medication's course, in calendar days on - * the user's clock. `startsOn` / `endsOn` are calendar dates and are read as - * such (never shifted through `timeZone`); only `now` is converted to the - * local day it falls on. Both ends are inclusive: a course that ends today - * is CURRENT for the whole of today, one that starts today is CURRENT from - * local midnight. - */ -export type CourseStatus = "UPCOMING" | "CURRENT" | "ENDED"; - -export function courseStatusAt( - course: { startsOn: Date | null; endsOn: Date | null }, - now: Date, - timeZone: string, -): CourseStatus { - const today = civilDayOfInstant(now, timeZone).getTime(); - if (course.startsOn && today < civilDayOfDate(course.startsOn).getTime()) { - return "UPCOMING"; - } - if (course.endsOn && today > civilDayOfDate(course.endsOn).getTime()) { - return "ENDED"; - } - return "CURRENT"; -} - /** * Emit every occurrence in `[from, to]` (inclusive of both ends) in * chronological order. Pure: no DB access, no side effects. @@ -273,9 +250,22 @@ export function occurrencesBetween( // independent: the legacy stride filters weeks first, the cyclic gate // filters the surviving slots second. if (schedule.scheduleType === "CYCLIC") { - return slots.filter((s) => isInCyclicOnWeek(s.at, schedule, ctx)); + slots = slots.filter((s) => isInCyclicOnWeek(s.at, schedule, ctx)); } - return slots; + // #1280 — a course ended with "End course" stops at that instant. + return slots.filter((s) => beforeCourseEnded(s.at, ctx)); +} + +/** + * #1280 — the cut every expansion path applies on top of the `endsOn` day + * cap: no slot at or after the instant "End course" ended the course. A dose + * later that day is not due, reminded or expected; an earlier one is. Every + * medication context builder threads `endedAt` (the course-anchor guard + * holds the list); a measurement or booster reminder has no course to end. + */ +function beforeCourseEnded(at: Date, ctx: RecurrenceContext): boolean { + const endedAt = ctx.medication.endedAt; + return !endedAt || at.getTime() < endedAt.getTime(); } /** @@ -354,7 +344,7 @@ export function nextOccurrenceAfter( for (const s of slots) { if (s.at.getTime() <= after.getTime()) continue; if (cyclic && !isInCyclicOnWeek(s.at, schedule, ctx)) continue; - return s; + return beforeCourseEnded(s.at, ctx) ? s : null; } return null; } @@ -405,7 +395,7 @@ export function nextOccurrenceAfter( for (const s of slots) { if (s.at.getTime() < floor.getTime()) continue; if (cyclic && !isInCyclicOnWeek(s.at, schedule, ctx)) continue; - return s; + return beforeCourseEnded(s.at, ctx) ? s : null; } return null; } @@ -608,6 +598,7 @@ export function advanceRollingOccurrence( return null; } const at = applyTimeOfDayToCivilDay(nextDay, time, ctx.timeZone); + if (!beforeCourseEnded(at, ctx)) return null; return buildOccurrence(at, time, schedule); } @@ -698,7 +689,8 @@ export function expandRollingRetrospective( const inWindow = (at: Date): boolean => at.getTime() >= from.getTime() && at.getTime() <= to.getTime() && - at.getTime() <= endsCap; + at.getTime() <= endsCap && + beforeCourseEnded(at, ctx); const slots: Occurrence[] = []; diff --git a/src/lib/medications/scheduling/slot-upsert.ts b/src/lib/medications/scheduling/slot-upsert.ts index 01fed425f..19471a7ac 100644 --- a/src/lib/medications/scheduling/slot-upsert.ts +++ b/src/lib/medications/scheduling/slot-upsert.ts @@ -18,6 +18,7 @@ import type { MedicationIntakeEvent, PrismaClient, } from "@/generated/prisma/client"; +import { medicationWindow } from "@/lib/medications/course-window"; import { prisma as defaultPrisma } from "@/lib/db"; import { isP2002 } from "@/lib/prisma-errors"; @@ -472,6 +473,7 @@ const MEDICATION_SELECT = { id: true, startsOn: true, endsOn: true, + endedAt: true, oneShot: true, createdAt: true, schedules: { select: SCHEDULE_SELECT }, @@ -557,8 +559,7 @@ export async function resolveSlotInstantForWrite( return resolveCanonicalSlotInstant({ medication: { id: medication.id, - startsOn: medication.startsOn, - endsOn: medication.endsOn, + ...medicationWindow(medication), oneShot: medication.oneShot, createdAt: medication.createdAt, schedules: medication.schedules as WorkerScheduleRow[], @@ -612,8 +613,7 @@ async function loadAttributeMedication(input: { return { medication: { id: medication.id, - startsOn: medication.startsOn, - endsOn: medication.endsOn, + ...medicationWindow(medication), oneShot: medication.oneShot, createdAt: medication.createdAt, schedules: medication.schedules as WorkerScheduleRow[], diff --git a/src/lib/medications/scheduling/worker-helpers.ts b/src/lib/medications/scheduling/worker-helpers.ts index 7d1a45199..959120ce6 100644 --- a/src/lib/medications/scheduling/worker-helpers.ts +++ b/src/lib/medications/scheduling/worker-helpers.ts @@ -69,6 +69,8 @@ export interface WorkerMedicationRow { id: string; startsOn: Date | null; endsOn: Date | null; + /** #1280 — the instant "End course" ended the course (see the engine). */ + endedAt: Date | null; oneShot: boolean; createdAt: Date; } @@ -149,6 +151,7 @@ export function buildRecurrenceContext(input: { id: input.medication.id, startsOn: input.medication.startsOn, endsOn: input.medication.endsOn, + endedAt: input.medication.endedAt, oneShot: input.medication.oneShot, createdAt: input.medication.createdAt, }, diff --git a/src/lib/openapi/routes/medications/schemas.ts b/src/lib/openapi/routes/medications/schemas.ts index 55e9fbe13..2e6c11d0c 100644 --- a/src/lib/openapi/routes/medications/schemas.ts +++ b/src/lib/openapi/routes/medications/schemas.ts @@ -66,9 +66,16 @@ export const medicationCourseResource = z .date() .nullable() .describe("Last day, inclusive. NULL = open (only the latest course)."), + endedAt: isoDateTime() + .nullable() + .describe( + '#1280 — the instant "End course" ended this course, on its `endsOn` day. No dose slot at or after it is due, reminded or expected, and the course is ENDED from it on. NULL when the whole `endsOn` day counts (an end date set in the editor). Any edit that moves `endsOn` clears it.', + ), status: z .enum(["UPCOMING", "CURRENT", "ENDED"]) - .describe("Where today, on the person's clock, sits in this course."), + .describe( + "Where today, on the person's clock, sits in this course. A course with `endedAt` is ENDED on its `endsOn` day already.", + ), takenDoses: z .number() .int() @@ -364,6 +371,12 @@ export const medicationResource = z .describe( "v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true.", ), + endedAt: isoDateTime() + .nullable() + .optional() + .describe( + '#1280 — the instant the latest course was ended with "End course" (the course\'s own `endedAt`). From it on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. Read-only — written by the course writer only.', + ), oneShot: z .boolean() .describe( @@ -435,12 +448,12 @@ export const medicationListEntry = medicationResource remindersActive: z .boolean() .describe( - "v1.42.2 — whether the server sends dose reminders for this medication: true only when `notificationsEnabled` is on, the medication is `active`, its course has not ended (`endsOn` is today or later on the person's clock, or unset), its intake is tracked (`trackIntake`), it is not `asNeeded`, and `autoMarkTaken` is off. The same decision gates every server-side reminder send. A client that arms local reminders arms them from this, not from `notificationsEnabled`. Read-only — computed, not stored.", + "v1.42.2 — whether the server sends dose reminders for this medication: true only when `notificationsEnabled` is on, the medication is `active`, its course has not ended (`endsOn` is today or later on the person's clock, or unset, and no `endedAt` has passed), its intake is tracked (`trackIntake`), it is not `asNeeded`, and `autoMarkTaken` is off. The same decision gates every server-side reminder send. A client that arms local reminders arms them from this, not from `notificationsEnabled`. Read-only — computed, not stored.", ), courseStatus: z .enum(["UPCOMING", "CURRENT", "ENDED"]) .describe( - "v1.39.4 — where today (on the user's clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all day. Read-only — computed, not stored.", + 'v1.39.4 — where today (on the user\'s clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all day, unless it was ended with "End course": then it is ENDED from that instant (`endedAt`). Read-only — computed, not stored.', ), intakeActionable: z .boolean() @@ -506,7 +519,7 @@ export const medicationDetailEntry = medicationResource courseStatus: z .enum(["UPCOMING", "CURRENT", "ENDED"]) .describe( - "v1.39.4 — where today (on the user's clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all day. Read-only — computed, not stored.", + 'v1.39.4 — where today (on the user\'s clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all day, unless it was ended with "End course": then it is ENDED from that instant (`endedAt`). Read-only — computed, not stored.', ), intakeActionable: z .boolean() diff --git a/src/lib/targets/types.ts b/src/lib/targets/types.ts index 5ae330480..2ffc716b4 100644 --- a/src/lib/targets/types.ts +++ b/src/lib/targets/types.ts @@ -72,12 +72,14 @@ export interface TargetMedication { createdAt: Date; startsOn: Date | null; endsOn: Date | null; + /** #1280 — the instant "End course" ended the latest course. */ + endedAt: Date | null; oneShot: boolean; schedules: ComplianceSchedule[]; scheduleRevisions?: ScheduleRevisionLike[]; pauseEras?: MedicationPauseEraLike[]; /** v1.40 (#1024) — the courses, so a gap between two expects nothing. */ - courses?: { startsOn: Date; endsOn: Date | null }[]; + courses?: { startsOn: Date; endsOn: Date | null; endedAt: Date | null }[]; } export interface TargetIntakeEvent extends IntakeEvent { diff --git a/tests/integration/__golden__/backup-parity/export-disaster-recovery.json b/tests/integration/__golden__/backup-parity/export-disaster-recovery.json index b14db8aff..c741d2b27 100644 --- a/tests/integration/__golden__/backup-parity/export-disaster-recovery.json +++ b/tests/integration/__golden__/backup-parity/export-disaster-recovery.json @@ -335,7 +335,7 @@ "reorderLeadDays": 3, "externalSource": "WEB", "externalId": "rt-Medication-externalId", - "updatedAt": "2026-05-01T11:51:00.000Z", + "updatedAt": "2026-05-01T11:52:00.000Z", "categoryUpdatedAt": "2026-05-01T21:22:00.000Z", "name": "Round-trip tablet", "dose": "5 mg", @@ -344,7 +344,7 @@ "autoMarkTaken": true, "autoMarkTakenSince": "2026-06-30T12:34:56.789Z", "category": "custom:5f0c6a52-6a43-4f63-9b39-0c1f4d7e2a10", - "createdAt": "2026-05-01T11:50:00.000Z", + "createdAt": "2026-05-01T11:51:00.000Z", "schedules": [ { "id": "", @@ -428,18 +428,20 @@ { "startsOn": "2026-03-01", "endsOn": "2026-03-07", + "endedAt": null, "note": null, "noteEncrypted": "", - "updatedAt": "2026-05-02T00:47:00.000Z", - "createdAt": "2026-05-02T00:46:00.000Z" + "updatedAt": "2026-05-02T00:48:00.000Z", + "createdAt": "2026-05-02T00:47:00.000Z" }, { "startsOn": "2026-06-10", "endsOn": "2026-06-16", + "endedAt": "2026-06-16T09:30:00.000Z", "note": null, "noteEncrypted": null, - "updatedAt": "2026-05-02T00:47:01.000Z", - "createdAt": "2026-05-02T00:46:01.000Z" + "updatedAt": "2026-05-02T00:48:01.000Z", + "createdAt": "2026-05-02T00:47:01.000Z" } ], "doseChanges": [ @@ -2772,8 +2774,8 @@ "reorderLeadDays": 3, "externalSource": "WEB", "externalId": "rt-Medication-externalId", - "createdAt": "2026-05-01T11:50:00.000Z", - "updatedAt": "2026-05-01T11:51:00.000Z", + "createdAt": "2026-05-01T11:51:00.000Z", + "updatedAt": "2026-05-01T11:52:00.000Z", "schedules": [ { "id": "", @@ -2995,18 +2997,20 @@ { "startsOn": "2026-03-01", "endsOn": "2026-03-07", + "endedAt": null, "note": null, "noteEncrypted": "", - "createdAt": "2026-05-02T00:46:00.000Z", - "updatedAt": "2026-05-02T00:47:00.000Z" + "createdAt": "2026-05-02T00:47:00.000Z", + "updatedAt": "2026-05-02T00:48:00.000Z" }, { "startsOn": "2026-06-10", "endsOn": "2026-06-16", + "endedAt": "2026-06-16T09:30:00.000Z", "note": null, "noteEncrypted": null, - "createdAt": "2026-05-02T00:46:01.000Z", - "updatedAt": "2026-05-02T00:47:01.000Z" + "createdAt": "2026-05-02T00:47:01.000Z", + "updatedAt": "2026-05-02T00:48:01.000Z" } ] } diff --git a/tests/integration/__golden__/backup-parity/export-portable.json b/tests/integration/__golden__/backup-parity/export-portable.json index 82a5663bd..436afeeb9 100644 --- a/tests/integration/__golden__/backup-parity/export-portable.json +++ b/tests/integration/__golden__/backup-parity/export-portable.json @@ -199,7 +199,7 @@ "autoMarkTaken": true, "autoMarkTakenSince": "2026-06-30T12:34:56.789Z", "category": "custom:5f0c6a52-6a43-4f63-9b39-0c1f4d7e2a10", - "createdAt": "2026-05-01T11:50:00.000Z", + "createdAt": "2026-05-01T11:51:00.000Z", "schedules": [ { "windowStart": "08:00", @@ -250,14 +250,16 @@ { "startsOn": "2026-03-01", "endsOn": "2026-03-07", + "endedAt": null, "note": "the March flu", - "createdAt": "2026-05-02T00:46:00.000Z" + "createdAt": "2026-05-02T00:47:00.000Z" }, { "startsOn": "2026-06-10", "endsOn": "2026-06-16", + "endedAt": "2026-06-16T09:30:00.000Z", "note": null, - "createdAt": "2026-05-02T00:46:01.000Z" + "createdAt": "2026-05-02T00:47:01.000Z" } ], "doseChanges": [ @@ -1850,7 +1852,7 @@ "autoMarkTaken": true, "autoMarkTakenSince": "2026-06-30T12:34:56.789Z", "category": "custom:5f0c6a52-6a43-4f63-9b39-0c1f4d7e2a10", - "createdAt": "2026-05-01T11:50:00.000Z", + "createdAt": "2026-05-01T11:51:00.000Z", "schedules": [ { "windowStart": "08:00", @@ -2022,14 +2024,16 @@ { "startsOn": "2026-03-01", "endsOn": "2026-03-07", + "endedAt": null, "note": "the March flu", - "createdAt": "2026-05-02T00:46:00.000Z" + "createdAt": "2026-05-02T00:47:00.000Z" }, { "startsOn": "2026-06-10", "endsOn": "2026-06-16", + "endedAt": "2026-06-16T09:30:00.000Z", "note": null, - "createdAt": "2026-05-02T00:46:01.000Z" + "createdAt": "2026-05-02T00:47:01.000Z" } ] } diff --git a/tests/integration/__golden__/backup-parity/restore-disaster-recovery.json b/tests/integration/__golden__/backup-parity/restore-disaster-recovery.json index f8e15f9f9..a8d078b9b 100644 --- a/tests/integration/__golden__/backup-parity/restore-disaster-recovery.json +++ b/tests/integration/__golden__/backup-parity/restore-disaster-recovery.json @@ -614,6 +614,7 @@ "snoozedUntil": "2026-06-30T12:34:56.789Z", "startsOn": "2026-06-10T00:00:00.000Z", "endsOn": "2026-06-16T00:00:00.000Z", + "endedAt": "2026-06-16T09:30:00.000Z", "oneShot": true, "asNeeded": true, "trackIntake": false, @@ -633,8 +634,8 @@ "reorderLeadDays": 3, "externalSource": "WEB", "externalId": "rt-Medication-externalId", - "createdAt": "2026-05-01T11:50:00.000Z", - "updatedAt": "2026-05-01T11:51:00.000Z" + "createdAt": "2026-05-01T11:51:00.000Z", + "updatedAt": "2026-05-01T11:52:00.000Z" } ], "MedicationSchedule": [ @@ -805,9 +806,10 @@ "userId": "round-trip-owner", "startsOn": "2026-03-01T00:00:00.000Z", "endsOn": "2026-03-07T00:00:00.000Z", + "endedAt": null, "noteEncrypted": "", - "createdAt": "2026-05-02T00:46:00.000Z", - "updatedAt": "2026-05-02T00:47:00.000Z" + "createdAt": "2026-05-02T00:47:00.000Z", + "updatedAt": "2026-05-02T00:48:00.000Z" }, { "id": "", @@ -815,9 +817,10 @@ "userId": "round-trip-owner", "startsOn": "2026-06-10T00:00:00.000Z", "endsOn": "2026-06-16T00:00:00.000Z", + "endedAt": "2026-06-16T09:30:00.000Z", "noteEncrypted": null, - "createdAt": "2026-05-02T00:46:01.000Z", - "updatedAt": "2026-05-02T00:47:01.000Z" + "createdAt": "2026-05-02T00:47:01.000Z", + "updatedAt": "2026-05-02T00:48:01.000Z" } ], "MedicationIngredient": [ @@ -3237,7 +3240,7 @@ "reorderLeadDays": 3, "externalSource": "WEB", "externalId": "rt-Medication-externalId", - "updatedAt": "2026-05-01T11:51:00.000Z", + "updatedAt": "2026-05-01T11:52:00.000Z", "categoryUpdatedAt": "2026-05-01T21:22:00.000Z", "name": "Round-trip tablet", "dose": "5 mg", @@ -3246,7 +3249,7 @@ "autoMarkTaken": true, "autoMarkTakenSince": "", "category": "custom:5f0c6a52-6a43-4f63-9b39-0c1f4d7e2a10", - "createdAt": "2026-05-01T11:50:00.000Z", + "createdAt": "2026-05-01T11:51:00.000Z", "schedules": [ { "id": "", @@ -3330,18 +3333,20 @@ { "startsOn": "2026-03-01", "endsOn": "2026-03-07", + "endedAt": null, "note": null, "noteEncrypted": "", - "updatedAt": "2026-05-02T00:47:00.000Z", - "createdAt": "2026-05-02T00:46:00.000Z" + "updatedAt": "2026-05-02T00:48:00.000Z", + "createdAt": "2026-05-02T00:47:00.000Z" }, { "startsOn": "2026-06-10", "endsOn": "2026-06-16", + "endedAt": "2026-06-16T09:30:00.000Z", "note": null, "noteEncrypted": null, - "updatedAt": "2026-05-02T00:47:01.000Z", - "createdAt": "2026-05-02T00:46:01.000Z" + "updatedAt": "2026-05-02T00:48:01.000Z", + "createdAt": "2026-05-02T00:47:01.000Z" } ], "doseChanges": [ diff --git a/tests/integration/__golden__/backup-parity/restore-portable.json b/tests/integration/__golden__/backup-parity/restore-portable.json index ed90139e4..68ae26e99 100644 --- a/tests/integration/__golden__/backup-parity/restore-portable.json +++ b/tests/integration/__golden__/backup-parity/restore-portable.json @@ -681,6 +681,7 @@ "snoozedUntil": null, "startsOn": "2026-06-10T00:00:00.000Z", "endsOn": "2026-06-16T00:00:00.000Z", + "endedAt": "2026-06-16T09:30:00.000Z", "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -698,7 +699,7 @@ "reorderLeadDays": null, "externalSource": null, "externalId": null, - "createdAt": "2026-05-01T11:50:00.000Z", + "createdAt": "2026-05-01T11:51:00.000Z", "updatedAt": "" } ], @@ -843,8 +844,9 @@ "userId": "round-trip-owner", "startsOn": "2026-03-01T00:00:00.000Z", "endsOn": "2026-03-07T00:00:00.000Z", + "endedAt": null, "noteEncrypted": "", - "createdAt": "2026-05-02T00:46:00.000Z", + "createdAt": "2026-05-02T00:47:00.000Z", "updatedAt": "" }, { @@ -853,8 +855,9 @@ "userId": "round-trip-owner", "startsOn": "2026-06-10T00:00:00.000Z", "endsOn": "2026-06-16T00:00:00.000Z", + "endedAt": "2026-06-16T09:30:00.000Z", "noteEncrypted": null, - "createdAt": "2026-05-02T00:46:01.000Z", + "createdAt": "2026-05-02T00:47:01.000Z", "updatedAt": "" } ], @@ -2596,7 +2599,7 @@ "autoMarkTaken": true, "autoMarkTakenSince": "", "category": "custom:5f0c6a52-6a43-4f63-9b39-0c1f4d7e2a10", - "createdAt": "2026-05-01T11:50:00.000Z", + "createdAt": "2026-05-01T11:51:00.000Z", "schedules": [ { "windowStart": "08:00", @@ -2647,14 +2650,16 @@ { "startsOn": "2026-03-01", "endsOn": "2026-03-07", + "endedAt": null, "note": "the March flu", - "createdAt": "2026-05-02T00:46:00.000Z" + "createdAt": "2026-05-02T00:47:00.000Z" }, { "startsOn": "2026-06-10", "endsOn": "2026-06-16", + "endedAt": "2026-06-16T09:30:00.000Z", "note": null, - "createdAt": "2026-05-02T00:46:01.000Z" + "createdAt": "2026-05-02T00:47:01.000Z" } ], "doseChanges": [ diff --git a/tests/integration/__golden__/backup-parity/restore-skips.json b/tests/integration/__golden__/backup-parity/restore-skips.json index d050c0382..1dec697bd 100644 --- a/tests/integration/__golden__/backup-parity/restore-skips.json +++ b/tests/integration/__golden__/backup-parity/restore-skips.json @@ -570,6 +570,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/__golden__/backup-parity/restore-version-1.json b/tests/integration/__golden__/backup-parity/restore-version-1.json index 7c6570625..68895abe8 100644 --- a/tests/integration/__golden__/backup-parity/restore-version-1.json +++ b/tests/integration/__golden__/backup-parity/restore-version-1.json @@ -521,6 +521,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -555,6 +556,7 @@ "snoozedUntil": null, "startsOn": "2026-07-01T00:00:00.000Z", "endsOn": "2026-08-01T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -652,6 +654,7 @@ "userId": "round-trip-owner", "startsOn": "2026-07-01T00:00:00.000Z", "endsOn": "2026-08-01T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -1480,6 +1483,7 @@ { "startsOn": "2026-07-01", "endsOn": "2026-08-01", + "endedAt": null, "note": null, "noteEncrypted": null, "updatedAt": "", diff --git a/tests/integration/__golden__/backup-parity/seeded-account.json b/tests/integration/__golden__/backup-parity/seeded-account.json index c4d46b2c9..cd3618a3f 100644 --- a/tests/integration/__golden__/backup-parity/seeded-account.json +++ b/tests/integration/__golden__/backup-parity/seeded-account.json @@ -212,6 +212,7 @@ "snoozedUntil": "2026-06-30T12:34:56.789Z", "startsOn": "2026-06-10T00:00:00.000Z", "endsOn": "2026-06-16T00:00:00.000Z", + "endedAt": "2026-06-16T09:30:00.000Z", "oneShot": true, "asNeeded": true, "trackIntake": false, @@ -231,8 +232,8 @@ "reorderLeadDays": 3, "externalSource": "WEB", "externalId": "rt-Medication-externalId", - "createdAt": "2026-05-01T11:50:00.000Z", - "updatedAt": "2026-05-01T11:51:00.000Z" + "createdAt": "2026-05-01T11:51:00.000Z", + "updatedAt": "2026-05-01T11:52:00.000Z" } ], "MedicationSchedule": [ @@ -403,9 +404,10 @@ "userId": "round-trip-owner", "startsOn": "2026-03-01T00:00:00.000Z", "endsOn": "2026-03-07T00:00:00.000Z", + "endedAt": null, "noteEncrypted": "", - "createdAt": "2026-05-02T00:46:00.000Z", - "updatedAt": "2026-05-02T00:47:00.000Z" + "createdAt": "2026-05-02T00:47:00.000Z", + "updatedAt": "2026-05-02T00:48:00.000Z" }, { "id": "", @@ -413,9 +415,10 @@ "userId": "round-trip-owner", "startsOn": "2026-06-10T00:00:00.000Z", "endsOn": "2026-06-16T00:00:00.000Z", + "endedAt": "2026-06-16T09:30:00.000Z", "noteEncrypted": null, - "createdAt": "2026-05-02T00:46:01.000Z", - "updatedAt": "2026-05-02T00:47:01.000Z" + "createdAt": "2026-05-02T00:47:01.000Z", + "updatedAt": "2026-05-02T00:48:01.000Z" } ], "MedicationIngredient": [ diff --git a/tests/integration/backup-fixture.ts b/tests/integration/backup-fixture.ts index ddf0f990e..8b271145c 100644 --- a/tests/integration/backup-fixture.ts +++ b/tests/integration/backup-fixture.ts @@ -190,7 +190,9 @@ export async function seedEveryTwoEndedModel( }); // Two courses with a gap, and the window the row projects from them (the - // latest). The earlier course exists nowhere else once it has ended. + // latest). The earlier course exists nowhere else once it has ended. The + // latest was ended with "End course" (#1280), so the instant has to come + // back with it. const medication = await prisma.medication.create({ data: { userId: OWNER_ID, @@ -198,6 +200,7 @@ export async function seedEveryTwoEndedModel( dose: "5 mg", startsOn: AT("2026-06-10T00:00:00.000Z"), endsOn: AT("2026-06-16T00:00:00.000Z"), + endedAt: AT("2026-06-16T09:30:00.000Z"), schedules: { create: { windowStart: "08:00", windowEnd: "09:00", label: "Morning" }, }, @@ -213,6 +216,7 @@ export async function seedEveryTwoEndedModel( userId: OWNER_ID, startsOn: AT("2026-06-10T00:00:00.000Z"), endsOn: AT("2026-06-16T00:00:00.000Z"), + endedAt: AT("2026-06-16T09:30:00.000Z"), }, ], }, diff --git a/tests/integration/intake-late-take-future-slot.test.ts b/tests/integration/intake-late-take-future-slot.test.ts index ddffd0969..797d809d3 100644 --- a/tests/integration/intake-late-take-future-slot.test.ts +++ b/tests/integration/intake-late-take-future-slot.test.ts @@ -217,6 +217,7 @@ async function readDisplayDue(medId: string, now: Date): Promise { id: med.id, startsOn: med.startsOn, endsOn: med.endsOn, + endedAt: null, oneShot: med.oneShot, createdAt: med.createdAt, }, diff --git a/tests/integration/medication-auto-mark-taken.test.ts b/tests/integration/medication-auto-mark-taken.test.ts index b30f2e907..6719dd9c2 100644 --- a/tests/integration/medication-auto-mark-taken.test.ts +++ b/tests/integration/medication-auto-mark-taken.test.ts @@ -585,6 +585,7 @@ describe("remindersActive — the published value is the gate", () => { trackIntake: boolean; asNeeded: boolean; endsOn: Date | null; + endedAt: Date | null; }> = []; for (let bits = 0; bits < 64; bits++) { combos.push({ @@ -595,6 +596,7 @@ describe("remindersActive — the published value is the gate", () => { asNeeded: (bits & 16) !== 0, // A course that ended the day before. endsOn: (bits & 32) !== 0 ? new Date("2026-06-09T00:00:00.000Z") : null, + endedAt: null, }); } const ids: string[] = []; From 275367f6e9d8a60b348d85b6ec6c0fe6e49a769e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 24 Oct 2026 22:24:15 +0200 Subject: [PATCH 07/11] feat(medications): end a course now through its own action "End course" now posts to `POST /api/medications/{id}/courses/end`, which ends the running course at that moment: its last day becomes today on the person's clock and the instant is stored as `endedAt`, so a dose later today is not due, not reminded and never missed, while one taken earlier still counts. Pending placeholders already minted for later slots are tombstoned. A medication without dates gets the course its window describes, ended now. End stays its own verb instead of `PUT { endsOn }`: an end date set through the PUT or the course editor keeps meaning the whole day, and the PUT keeps accepting it on any course, since moving an end is a legitimate edit. The list and detail reads serve `canEndCourse`, true exactly while a course is running, and the action refuses with 409 `medications.course.notRunning` when it is false, so no client can reopen an ended course or invent a one-day course from a planned one by ending it. The web controls render End from the served flag; the contributed tests keep their cases against it. Refs #1280 --- docs/api/openapi.yaml | 229 +++++--- .../aging-fixture-dates-guard.test.ts | 5 + .../api/medications/[id]/courses/end/route.ts | 78 +++ .../detail/medication-detail-tabs.tsx | 1 + .../destructive-zone-section.test.tsx | 36 +- .../sections/destructive-zone-section.tsx | 52 +- .../__tests__/course-window.test.ts | 25 + .../__tests__/intake-actionable.test.ts | 8 + src/lib/medications/course-window.ts | 23 + src/lib/medications/courses.ts | 102 +++- src/lib/medications/intake-actionable.ts | 9 +- src/lib/openapi/error-codes.ts | 1 + src/lib/openapi/routes/medications/paths.ts | 39 ++ src/lib/openapi/routes/medications/schemas.ts | 18 +- .../integration/medication-course-end.test.ts | 488 ++++++++++++++++++ 15 files changed, 990 insertions(+), 124 deletions(-) create mode 100644 src/app/api/medications/[id]/courses/end/route.ts create mode 100644 tests/integration/medication-course-end.test.ts diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 061a59857..d9ce7210b 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -6861,6 +6861,49 @@ paths: "409": *a7 "422": *a4 "429": *a5 + /api/medications/{id}/courses/end: + post: + tags: + - Medications + summary: End the running course now + description: "#1280 — \"End course\". No body. The running course's last day becomes today on the person's clock and the + moment is stored as its `endedAt`: a dose slot later today is not due, not reminded and not expected by + compliance, a dose taken earlier today still counts, and `courseStatus` reads ENDED at once. Pending + placeholders for slots at or after the moment are removed (tombstoned for sync). A medication with no dates gets + the course its window describes, from creation to today. An end date set through `PUT /api/medications/{id}` or + the course editor keeps meaning the whole day; only this action ends a course at an instant. Refused with 409 + `medications.course.notRunning` while no course is running — the rule the reads publish as `canEndCourse`. + Owner-only." + parameters: + - in: path + name: id + schema: + type: string + required: true + - *a6 + responses: + "200": + description: The medication's window after ending. + content: + application/json: + schema: + $ref: "#/components/schemas/EndMedicationCourseResponse" + "401": *a1 + "403": *a9 + "404": + description: Medication not found (`medications.course.notFound`), or another user's. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + "409": + description: "No course is running now (`medications.course.notRunning`): it has ended already or has not started." + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + "422": *a4 + "429": *a5 /api/medications/{id}/courses/{courseId}: patch: tags: @@ -38495,55 +38538,56 @@ components: medication.intake.notTracked, medication.link.invalid; medications — medications.category.invalid, medications.category.limitReached, medications.category.unknown, medications.course.currentOpen, medications.course.invalid, medications.course.invalidRange, medications.course.notFound, - medications.course.oneShot, medications.course.overlap, medications.course.windowRequired, - medications.ingredients.extract.budgetExceeded, medications.ingredients.extract.failed, - medications.ingredients.extract.fileTooLarge, medications.ingredients.extract.fileType, - medications.ingredients.extract.rateLimited, medications.ingredients.invalid, - medications.ingredients.unitMismatch, medications.intake.bulk.apple_health_not_mirrored, - medications.intake.bulk.invalid, medications.intake.bulk.too_large, - medications.intake.force_slot.invalid, medications.intake.force_slot.occupied, - medications.intake.injection_site.disallowed, medications.intake.taken_at.before_start, - medications.mirror.limit_exceeded, medications.notFound; mentalHealth — mentalHealth.rateLimited; module - — module.disabled; modules — modules.body.invalid_json, modules.invalid; mood — - mood.bulk-delete.invalid, mood.bulk.invalid, mood.bulk.too_large, mood.create.external_id_not_delegable, - mood.create.invalid, mood.duplicate_timestamp, mood.linkedContext.invalid, mood.list.invalid, - mood.not_found, mood.ratedFactor.out_of_range, mood.restore.invalid, mood.update.invalid; - notification-prefs — notification-prefs.body.invalid_json; notification — - notification.action.already_used, notification.action.expired, notification.action.invalid, - notification.action.module_disabled; nutrient — nutrient.batch.invalid, nutrient.batch.too_large, - nutrient.daily.invalid, nutrient.read.invalid, nutrient.water.invalid, nutrient.water.invalid_day; - onboarding — onboarding.answers.invalid, onboarding.answers.rateLimited, onboarding.complete.invalid, - onboarding.complete.recordNotFound, onboarding.complete.recordTargetMismatch, - onboarding.disclaimer.invalid, onboarding.restart.invalid, onboarding.restart.rateLimited, - onboarding.tour.invalid; passkey — passkey.challenge.expired, passkey.response.invalid, passkey.unknown, - passkey.verification.failed; personal_records — personal_records.invalid_query; practitioner — - practitioner.invalid; profile — profile.update.emailInUse, profile.update.emailRateLimited, - profile.update.emailSecondFactorRequired, profile.update.invalidBody, profile.update.nothingSaved; - profile_claim — profile_claim.invalid, profile_claim.oidc_only_unsupported, - profile_claim.sso_unavailable, profile_claim.taken; record_write — record_write.rate_limited; - report-selection — report-selection.body.invalid_json, report-selection.body.invalid_shape, - report-selection.leaves.unknown; route — route.retired; search — search.invalid; security_activity — - security_activity.invalid; share-link — share-link.document.decryptFailed, share-link.document.notFound, - share-link.document.rateLimited, share-link.documents.invalid, share-link.report.notFound, - share-link.report.rateLimited, share-link.selection.forbidden_leaf, share-link.selection.unknown_leaf; - sharing — sharing.accept.expired, sharing.accept.not_found, sharing.accept.not_pending, - sharing.accept.revoked, sharing.access.denied, sharing.invite.duplicate, sharing.invite.invalid, - sharing.invite.invalid_scope, sharing.invite.manage_browser_only, sharing.invite.no_account, - sharing.invite.refused, sharing.invite.self, sharing.not_permitted, sharing.renounce.already_ended, - sharing.renounce.not_found, sharing.revoke.already_ended, sharing.revoke.not_found, - sharing.session.changed, sharing.switch.invalid, sharing.switch.wrong_transport; source-priority — - source-priority.body.invalid_json, source-priority.body.invalid_shape; symptoms — - symptoms.definition.limitReached, symptoms.definition.notFound, symptoms.episode.notOpen, - symptoms.event.notFound, symptoms.invalid; timeline — timeline.invalid; tokens — - tokens.documents.ceiling_reached, tokens.measurements.ceiling_reached, - tokens.mirror.not_measurement_token, tokens.workouts.ceiling_reached; vaccination — - vaccination.booster-invalid, vaccination.booster-no-antigen, vaccination.booster-out-of-scope, - vaccination.custom-vaccine-not-found, vaccination.custom.invalid, vaccination.custom.name-taken, - vaccination.custom.not-found, vaccination.encounter-not-found, vaccination.identity-required, - vaccination.invalid, vaccination.link.invalid, vaccination.not-found, - vaccination.practitioner-not-found; workout — workout.batch.invalid, workout.batch.payload_too_large, - workout.batch.source_not_permitted, workout.batch.too_large, workout.delete.synced_source; (unprefixed) - — about_me_conflict, ai_response_truncated, coach_prefs_conflict, dashboard_layout_conflict, empty_file, + medications.course.notRunning, medications.course.oneShot, medications.course.overlap, + medications.course.windowRequired, medications.ingredients.extract.budgetExceeded, + medications.ingredients.extract.failed, medications.ingredients.extract.fileTooLarge, + medications.ingredients.extract.fileType, medications.ingredients.extract.rateLimited, + medications.ingredients.invalid, medications.ingredients.unitMismatch, + medications.intake.bulk.apple_health_not_mirrored, medications.intake.bulk.invalid, + medications.intake.bulk.too_large, medications.intake.force_slot.invalid, + medications.intake.force_slot.occupied, medications.intake.injection_site.disallowed, + medications.intake.taken_at.before_start, medications.mirror.limit_exceeded, medications.notFound; + mentalHealth — mentalHealth.rateLimited; module — module.disabled; modules — modules.body.invalid_json, + modules.invalid; mood — mood.bulk-delete.invalid, mood.bulk.invalid, mood.bulk.too_large, + mood.create.external_id_not_delegable, mood.create.invalid, mood.duplicate_timestamp, + mood.linkedContext.invalid, mood.list.invalid, mood.not_found, mood.ratedFactor.out_of_range, + mood.restore.invalid, mood.update.invalid; notification-prefs — notification-prefs.body.invalid_json; + notification — notification.action.already_used, notification.action.expired, + notification.action.invalid, notification.action.module_disabled; nutrient — nutrient.batch.invalid, + nutrient.batch.too_large, nutrient.daily.invalid, nutrient.read.invalid, nutrient.water.invalid, + nutrient.water.invalid_day; onboarding — onboarding.answers.invalid, onboarding.answers.rateLimited, + onboarding.complete.invalid, onboarding.complete.recordNotFound, + onboarding.complete.recordTargetMismatch, onboarding.disclaimer.invalid, onboarding.restart.invalid, + onboarding.restart.rateLimited, onboarding.tour.invalid; passkey — passkey.challenge.expired, + passkey.response.invalid, passkey.unknown, passkey.verification.failed; personal_records — + personal_records.invalid_query; practitioner — practitioner.invalid; profile — + profile.update.emailInUse, profile.update.emailRateLimited, profile.update.emailSecondFactorRequired, + profile.update.invalidBody, profile.update.nothingSaved; profile_claim — profile_claim.invalid, + profile_claim.oidc_only_unsupported, profile_claim.sso_unavailable, profile_claim.taken; record_write — + record_write.rate_limited; report-selection — report-selection.body.invalid_json, + report-selection.body.invalid_shape, report-selection.leaves.unknown; route — route.retired; search — + search.invalid; security_activity — security_activity.invalid; share-link — + share-link.document.decryptFailed, share-link.document.notFound, share-link.document.rateLimited, + share-link.documents.invalid, share-link.report.notFound, share-link.report.rateLimited, + share-link.selection.forbidden_leaf, share-link.selection.unknown_leaf; sharing — + sharing.accept.expired, sharing.accept.not_found, sharing.accept.not_pending, sharing.accept.revoked, + sharing.access.denied, sharing.invite.duplicate, sharing.invite.invalid, sharing.invite.invalid_scope, + sharing.invite.manage_browser_only, sharing.invite.no_account, sharing.invite.refused, + sharing.invite.self, sharing.not_permitted, sharing.renounce.already_ended, sharing.renounce.not_found, + sharing.revoke.already_ended, sharing.revoke.not_found, sharing.session.changed, sharing.switch.invalid, + sharing.switch.wrong_transport; source-priority — source-priority.body.invalid_json, + source-priority.body.invalid_shape; symptoms — symptoms.definition.limitReached, + symptoms.definition.notFound, symptoms.episode.notOpen, symptoms.event.notFound, symptoms.invalid; + timeline — timeline.invalid; tokens — tokens.documents.ceiling_reached, + tokens.measurements.ceiling_reached, tokens.mirror.not_measurement_token, + tokens.workouts.ceiling_reached; vaccination — vaccination.booster-invalid, + vaccination.booster-no-antigen, vaccination.booster-out-of-scope, vaccination.custom-vaccine-not-found, + vaccination.custom.invalid, vaccination.custom.name-taken, vaccination.custom.not-found, + vaccination.encounter-not-found, vaccination.identity-required, vaccination.invalid, + vaccination.link.invalid, vaccination.not-found, vaccination.practitioner-not-found; workout — + workout.batch.invalid, workout.batch.payload_too_large, workout.batch.source_not_permitted, + workout.batch.too_large, workout.delete.synced_source; (unprefixed) — about_me_conflict, + ai_response_truncated, coach_prefs_conflict, dashboard_layout_conflict, empty_file, health_score_config_conflict, insights_layout_conflict, invalid_base_updated_at, json_carries_no_intake_time, json_not_an_array, medication_layout_conflict, missing_required_columns, modules_conflict, mood_tag_layout_conflict, not_configured, notification_prefs_conflict, oidc_only, @@ -53820,9 +53864,9 @@ components: - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. endedAt: - description: "#1280 — the instant the latest course was ended with \"End course\" (the course's own `endedAt`). From it - on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. - Read-only — written by the course writer only." + description: "#1280 — the instant the latest course was ended with `POST /api/medications/{id}/courses/end` (the + course's own `endedAt`). From it on no dose is due or reminded and `courseStatus` is ENDED; NULL when the + course runs to the end of `endsOn`. Read-only — written by the course writer only." anyOf: - type: string format: date-time @@ -53925,13 +53969,19 @@ components: - ENDED description: "v1.39.4 — where today (on the user's clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all - day, unless it was ended with \"End course\": then it is ENDED from that instant (`endedAt`). Read-only — - computed, not stored." + day, unless it was ended with `POST /api/medications/{id}/courses/end`: then it is ENDED from that instant + (`endedAt`). Read-only — computed, not stored." intakeActionable: type: boolean description: "v1.39.4 — whether the medication offers taken / skip today: true only when it is `active`, its intake is tracked (`trackIntake`) and `courseStatus` is CURRENT. Clients render the dose actions from this and do not re-derive it. Logging a past dose from the history is not gated by it. Read-only — computed, not stored." + canEndCourse: + type: boolean + description: '#1280 — whether `POST /api/medications/{id}/courses/end` may end the course now: true exactly while a + course is running (`courseStatus` CURRENT; a medication with no dates is running). Clients offer "End + course" from this and do not re-derive it; the action refuses with 409 `medications.course.notRunning` when + it is false. Read-only — computed, not stored.' lastTakenAt: anyOf: - type: string @@ -54017,6 +54067,7 @@ components: - nextDueScheduleId - courseStatus - intakeActionable + - canEndCourse - lastTakenAt - todayEventCount - stockUnitsRemaining @@ -54059,9 +54110,9 @@ components: 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)))$ - type: "null" - description: '#1280 — the instant "End course" ended this course, on its `endsOn` day. No dose slot at or after it is - due, reminded or expected, and the course is ENDED from it on. NULL when the whole `endsOn` day counts (an - end date set in the editor). Any edit that moves `endsOn` clears it.' + description: "#1280 — the instant `POST /api/medications/{id}/courses/end` ended this course, on its `endsOn` day. No + dose slot at or after it is due, reminded or expected, and the course is ENDED from it on. NULL when the + whole `endsOn` day counts (an end date set in the editor). Any edit that moves `endsOn` clears it." status: type: string enum: @@ -54272,9 +54323,9 @@ components: - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. endedAt: - description: "#1280 — the instant the latest course was ended with \"End course\" (the course's own `endedAt`). From it - on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. - Read-only — written by the course writer only." + description: "#1280 — the instant the latest course was ended with `POST /api/medications/{id}/courses/end` (the + course's own `endedAt`). From it on no dose is due or reminded and `courseStatus` is ENDED; NULL when the + course runs to the end of `endsOn`. Read-only — written by the course writer only." anyOf: - type: string format: date-time @@ -54372,13 +54423,19 @@ components: - ENDED description: "v1.39.4 — where today (on the user's clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all - day, unless it was ended with \"End course\": then it is ENDED from that instant (`endedAt`). Read-only — - computed, not stored." + day, unless it was ended with `POST /api/medications/{id}/courses/end`: then it is ENDED from that instant + (`endedAt`). Read-only — computed, not stored." intakeActionable: type: boolean description: "v1.39.4 — whether the medication offers taken / skip today: true only when it is `active`, its intake is tracked (`trackIntake`) and `courseStatus` is CURRENT. Clients render the dose actions from this and do not re-derive it. Logging a past dose from the history is not gated by it. Read-only — computed, not stored." + canEndCourse: + type: boolean + description: '#1280 — whether `POST /api/medications/{id}/courses/end` may end the course now: true exactly while a + course is running (`courseStatus` CURRENT; a medication with no dates is running). Clients offer "End + course" from this and do not re-derive it; the action refuses with 409 `medications.course.notRunning` when + it is false. Read-only — computed, not stored.' stockUnitsRemaining: description: "v1.37.19 — usable inventory units left (same semantics as the list entry's field): NULL = inventory tracking off; 0 = tracking on, supply ran out." @@ -54445,6 +54502,7 @@ components: - canStartCourse - courseStatus - intakeActionable + - canEndCourse - stockUnitsRemaining - stockDosesRemaining - runwayDays @@ -54725,6 +54783,47 @@ components: - data - error additionalProperties: false + EndMedicationCourseResponse: + type: object + properties: + data: + type: object + properties: + startsOn: + anyOf: + - type: string + format: date + 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])))$ + - type: "null" + endsOn: + anyOf: + - type: string + format: date + 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])))$ + - type: "null" + endedAt: + anyOf: + - 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)))$ + - type: "null" + required: + - startsOn + - endsOn + - endedAt + additionalProperties: false + error: + type: "null" + meta: + type: object + properties: + requestId: + type: string + additionalProperties: false + required: + - data + - error + additionalProperties: false UpdateMedicationCourseResponse: type: object properties: @@ -93831,9 +93930,9 @@ components: - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. endedAt: - description: "#1280 — the instant the latest course was ended with \"End course\" (the course's own `endedAt`). From it - on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. - Read-only — written by the course writer only." + description: "#1280 — the instant the latest course was ended with `POST /api/medications/{id}/courses/end` (the + course's own `endedAt`). From it on no dose is due or reminded and `courseStatus` is ENDED; NULL when the + course runs to the end of `endsOn`. Read-only — written by the course writer only." anyOf: - type: string format: date-time diff --git a/src/__tests__/aging-fixture-dates-guard.test.ts b/src/__tests__/aging-fixture-dates-guard.test.ts index b4db923dd..ba785e961 100644 --- a/src/__tests__/aging-fixture-dates-guard.test.ts +++ b/src/__tests__/aging-fixture-dates-guard.test.ts @@ -73,6 +73,11 @@ const ALLOWLIST: Record = { max: 2, reason: "the cases pin the clock with fake timers around the fixed dates", }, + "tests/integration/medication-course-end.test.ts": { + max: 1, + reason: + "the cases pin the clock with fake timers; the history read starts on the fixed day the course was ended", + }, "tests/integration/symptoms.test.ts": { max: 1, reason: diff --git a/src/app/api/medications/[id]/courses/end/route.ts b/src/app/api/medications/[id]/courses/end/route.ts new file mode 100644 index 000000000..3a3088282 --- /dev/null +++ b/src/app/api/medications/[id]/courses/end/route.ts @@ -0,0 +1,78 @@ +/** + * #1280 — "End course": end the running course now. + * + * POST /api/medications/{id}/courses/end — no body. The course's last day + * becomes today on the person's clock and the moment is recorded as + * its `endedAt`, so a dose later today is not due, not reminded and + * not expected, while one taken earlier today still counts. The + * medication reads as ENDED at once. + * + * An action of its own rather than `PUT { endsOn }`: an end date set through + * the PUT or the course editor keeps meaning the whole day. Refused with 409 + * `medications.course.notRunning` while no course is running, the same rule + * the reads publish as `canEndCourse`. + * + * Owner-only, like the other course routes. + */ +import { NextRequest } from "next/server"; + +import { apiError, apiSuccess, getClientIp } from "@/lib/api-response"; +import { apiHandler, requireAuth } from "@/lib/api-handler"; +import { auditLog } from "@/lib/auth/audit"; +import { invalidateUserMedications } from "@/lib/cache/invalidate"; +import { withIdempotency } from "@/lib/idempotency"; +import { annotate } from "@/lib/logging/context"; +import { + CourseWriteError, + courseRefusalResponse, + endCurrentCourse, +} from "@/lib/medications/courses"; +import { dayKeyOfDate } from "@/lib/medications/course-window"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; + +export const dynamic = "force-dynamic"; + +type RouteParams = { params: Promise<{ id: string }> }; + +export const POST = apiHandler( + withIdempotency<[NextRequest, RouteParams]>(postEndCourse), +); + +async function postEndCourse( + request: NextRequest, + { params }: RouteParams, +): Promise { + const { user } = await requireAuth(); + const { id } = await params; + + try { + const window = await endCurrentCourse({ + userId: user.id, + medicationId: id, + timeZone: user.timezone || DEFAULT_TIMEZONE, + }); + invalidateUserMedications(user.id, { evict: true }); + const body = { + startsOn: window.startsOn ? dayKeyOfDate(window.startsOn) : null, + endsOn: window.endsOn ? dayKeyOfDate(window.endsOn) : null, + endedAt: window.endedAt ? window.endedAt.toISOString() : null, + }; + await auditLog("medication.course.end", { + userId: user.id, + ipAddress: getClientIp(request), + details: { medicationId: id, ...body }, + }); + annotate({ + action: { name: "medication.course.end" }, + meta: { medicationId: id }, + }); + return apiSuccess(body); + } catch (err) { + if (err instanceof CourseWriteError) { + const r = courseRefusalResponse(err.refusal); + annotate({ meta: { "medication.course.refused": err.refusal } }); + return apiError(r.message, r.status, { errorCode: r.errorCode }); + } + throw err; + } +} diff --git a/src/components/medications/detail/medication-detail-tabs.tsx b/src/components/medications/detail/medication-detail-tabs.tsx index 9cefc459a..12f6c37de 100644 --- a/src/components/medications/detail/medication-detail-tabs.tsx +++ b/src/components/medications/detail/medication-detail-tabs.tsx @@ -193,6 +193,7 @@ export interface MedicationDetailSnapshot { /** v1.40 (#1024) — the server-resolved courses. */ courses?: MedicationCourseWire[]; canStartCourse?: boolean; + canEndCourse?: boolean; recordedSchedules?: ScheduleSnapshot[]; schedules: ScheduleSnapshot[]; /** v1.43 — the composition per unit, as the server delivered it. */ diff --git a/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx b/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx index 5992803ea..ca6b40a6e 100644 --- a/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx +++ b/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx @@ -3,13 +3,19 @@ import { renderToStaticMarkup } from "react-dom/server"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { I18nProvider } from "@/lib/i18n/context"; -import { updateMedicationSchema } from "@/lib/validations/medication/create-update"; - import { canEndCourse, - endCourseBody, - LifecycleManageBody, -} from "../destructive-zone-section"; + courseEndNow, + dayKeyOfDate, +} from "@/lib/medications/course-window"; +import { updateMedicationSchema } from "@/lib/validations/medication/create-update"; + +import { LifecycleManageBody } from "../destructive-zone-section"; + +/** The end day End writes, as the `YYYY-MM-DD` an `endsOn` field carries. */ +function endCourseBody(now: Date, timeZone: string): { endsOn: string } { + return { endsOn: dayKeyOfDate(courseEndNow(now, timeZone).endsOn) }; +} vi.mock("@/hooks/use-auth", () => ({ useAuth: () => ({ @@ -22,8 +28,9 @@ vi.mock("@/hooks/use-auth", () => ({ /** * "End medication" sent `new Date().toISOString()`, a full instant, against an * `endsOn` that only takes `YYYY-MM-DD`, so every press answered 422 and the - * toast said the medication could not be ended. The body now carries the - * person's own calendar day. + * toast said the medication could not be ended. End now asks the server to + * end the course (`POST /courses/end`), which writes the person's own + * calendar day (`courseEndNow`) as the last day. */ describe("endCourseBody", () => { it("is a date the update schema accepts", () => { @@ -58,10 +65,12 @@ describe("endCourseBody", () => { * Ending writes today as the end of the latest course. On a course that has * already ended that would reopen it up to today, and on one that has not * started it would become a one-day course today, so End is only offered while - * the course runs. A medication with no dates is CURRENT. + * the course runs. The server resolves that as `canEndCourse` (a medication + * with no dates is CURRENT); the controls render from it, and without it + * (a read that does not carry it) End stays hidden. */ describe("End is offered only for a running course", () => { - function render(courseStatus?: "UPCOMING" | "CURRENT" | "ENDED") { + function render(served: boolean | undefined) { return renderToStaticMarkup( @@ -70,7 +79,7 @@ describe("End is offered only for a running course", () => { id: "med-1", name: "Example", active: true, - courseStatus, + canEndCourse: served, }} /> @@ -80,12 +89,13 @@ describe("End is offered only for a running course", () => { it.each([ ["CURRENT", true], - [undefined, true], + [undefined, false], ["ENDED", false], ["UPCOMING", false], ] as const)("course %s → End shown: %s", (status, shown) => { - expect(canEndCourse(status)).toBe(shown); - const html = render(status); + const served = status === undefined ? undefined : canEndCourse(status); + expect(served ?? false).toBe(shown); + const html = render(served); expect(html.includes('data-slot="destructive-zone-end"')).toBe(shown); // Pausing stays available whatever the course. expect(html).toContain('id="medication-detail-pause-switch"'); diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx index ae1c193c2..f048f06a6 100644 --- a/src/components/medications/sections/destructive-zone-section.tsx +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -13,8 +13,10 @@ * - Tier 1 Pausieren: PUT `{ active: false | true }`. Server derives * `pausedAt`. Switch wraps in ` - {canEndCourse(medication.courseStatus) && ( + {medication.canEndCourse === true && ( { expect(at("2026-06-12T22:00:00.000Z")).toBe("ENDED"); }); + it("offers End only while a course runs", () => { + expect(canEndCourse("CURRENT")).toBe(true); + expect(canEndCourse("ENDED")).toBe(false); + expect(canEndCourse("UPCOMING")).toBe(false); + // No dates at all is running. + expect( + canEndCourse( + courseStatusAt({ startsOn: null, endsOn: null }, ENDED_AT, "UTC"), + ), + ).toBe(true); + }); + + it("ends on the person's own day, at the moment itself", () => { + const late = new Date("2026-06-11T23:30:00.000Z"); + expect(courseEndNow(late, "Europe/Warsaw")).toEqual({ + endsOn: d("2026-06-12"), + endedAt: late, + }); + expect(courseEndNow(late, "America/New_York").endsOn).toEqual( + d("2026-06-11"), + ); + }); + it("lets a new course start once the ended one is behind it", () => { const base = { active: true, oneShot: false, todayKey: "2026-06-12" }; expect(canStartCourse({ ...base, courses: [MARCH, ended] })).toBe(true); diff --git a/src/lib/medications/__tests__/intake-actionable.test.ts b/src/lib/medications/__tests__/intake-actionable.test.ts index 5c145d1d0..5fdfac777 100644 --- a/src/lib/medications/__tests__/intake-actionable.test.ts +++ b/src/lib/medications/__tests__/intake-actionable.test.ts @@ -46,6 +46,7 @@ describe.each(ZONES)("course window in %s", (tz) => { expect(r).toEqual({ courseStatus: "CURRENT", intakeActionable: true, + canEndCourse: true, }); } }); @@ -55,6 +56,7 @@ describe.each(ZONES)("course window in %s", (tz) => { expect(r).toEqual({ courseStatus: "ENDED", intakeActionable: false, + canEndCourse: false, }); }); @@ -63,10 +65,12 @@ describe.each(ZONES)("course window in %s", (tz) => { expect(resolveIntakeActionability(med, at(tz, 24, 23, 59), tz)).toEqual({ courseStatus: "UPCOMING", intakeActionable: false, + canEndCourse: false, }); expect(resolveIntakeActionability(med, at(tz, 25, 0, 0), tz)).toEqual({ courseStatus: "CURRENT", intakeActionable: true, + canEndCourse: true, }); }); }); @@ -90,6 +94,7 @@ describe("resolveIntakeActionability", () => { expect(r).toEqual({ courseStatus: "CURRENT", intakeActionable: false, + canEndCourse: true, }); }); @@ -102,6 +107,7 @@ describe("resolveIntakeActionability", () => { expect(r).toEqual({ courseStatus: "CURRENT", intakeActionable: false, + canEndCourse: true, }); }); @@ -131,6 +137,7 @@ describe("a course ended with End course (#1280)", () => { expect(before).toEqual({ courseStatus: "CURRENT", intakeActionable: true, + canEndCourse: true, }); const after = resolveIntakeActionability( ended, @@ -140,6 +147,7 @@ describe("a course ended with End course (#1280)", () => { expect(after).toEqual({ courseStatus: "ENDED", intakeActionable: false, + canEndCourse: false, }); }); }); diff --git a/src/lib/medications/course-window.ts b/src/lib/medications/course-window.ts index 333feff9a..a97d9711c 100644 --- a/src/lib/medications/course-window.ts +++ b/src/lib/medications/course-window.ts @@ -143,6 +143,29 @@ export function courseStatusAt( return "CURRENT"; } +/** + * #1280 — the end "End course" writes: the person's own calendar day as the + * course's last day (never the UTC one, a day off around midnight), and the + * moment itself as `endedAt`. + */ +export function courseEndNow( + now: Date, + timeZone: string, +): { endsOn: Date; endedAt: Date } { + return { endsOn: dateOfDayKey(userDayKey(now, timeZone)), endedAt: now }; +} + +/** + * #1280 — whether "End course" may be pressed: a course is running now. On + * a course that has ended, ending it again would reopen it up to today; on + * one that has not started, it would turn into a one-day course today. The + * medication read wire publishes the answer as `canEndCourse`, and the End + * action refuses when it is false. + */ +export function canEndCourse(courseStatus: CourseStatus): boolean { + return courseStatus === "CURRENT"; +} + export type CourseRefusal = /** `endsOn` before `startsOn`. */ | "invalidRange" diff --git a/src/lib/medications/courses.ts b/src/lib/medications/courses.ts index 2e6ce8772..73471fead 100644 --- a/src/lib/medications/courses.ts +++ b/src/lib/medications/courses.ts @@ -10,16 +10,21 @@ * and `intakeActionable` — which read the columns — can never disagree with * the course list. * - * #1280 — a course may carry `endedAt`, the instant it was ended at. Every - * write that moves a course's `endsOn` clears it: an end date chosen in the - * editor means the whole day. + * #1280 — "End course" (`endCurrentCourse`) ends the running course at the + * moment it is pressed: `endsOn` becomes today and `endedAt` records the + * instant, so no slot after it is due, reminded or expected. Every other + * write that moves a course's `endsOn` clears `endedAt`: an end date chosen + * in the editor means the whole day. */ import type { Prisma } from "@/generated/prisma/client"; import { decryptFromBytes, encryptToBytes } from "@/lib/ai/coach/bytes-codec"; import { prisma } from "@/lib/db"; import { + canEndCourse, canStartCourse, + courseEndNow, courseFromWindow, + courseStatusAt, courseStatusOn, dateOfDayKey, dayKeyOfDate, @@ -43,7 +48,9 @@ export type CourseWriteRefusal = /** Clearing the window of a medication that has several courses. */ | "windowRequired" /** The course or the medication is not the caller's. */ - | "notFound"; + | "notFound" + /** #1280 — "End course" on a medication with no course running now. */ + | "notRunning"; export class CourseWriteError extends Error { constructor(readonly refusal: CourseWriteRefusal) { @@ -89,6 +96,12 @@ export function courseRefusalResponse(refusal: CourseWriteRefusal): { errorCode: "medications.course.invalidRange", message: "A course cannot end before it starts", }; + case "notRunning": + return { + status: 409, + errorCode: "medications.course.notRunning", + message: "No course is running to end", + }; case "windowRequired": return { status: 422, @@ -450,6 +463,87 @@ function keptEndedAt( : null; } +/** + * #1280 — "End course": end the running course now. + * + * The course's last day becomes today on the person's clock and `endedAt` + * records the moment. Doses earlier today stay inside the course and count; + * a slot later today is not due, not reminded and not expected, and the + * course reads as ENDED at once (`courseStatusAt`). A medication without a + * course row (chronic since creation, or a window never materialised) gets + * the course its window describes, ended now. + * + * The pending placeholders already minted for slots at or after that moment + * (the today projector mints the whole day) are tombstoned, as a schedule + * replace does, so no list shows them and the auto-miss pass never stamps + * them a miss. An actioned row is history and stays. + * + * Refused with `notRunning` unless a course is running now + * (`canEndCourse`): ending an ended course would reopen it up to today, and + * ending one that has not started would invent a one-day course. + */ +export async function endCurrentCourse( + args: WriteContext, +): Promise { + const now = args.now ?? new Date(); + const todayKey = userDayKey(now, args.timeZone); + const { endsOn: today, endedAt } = courseEndNow(now, args.timeZone); + return prisma.$transaction(async (tx) => { + await lockMedication(tx, args.medicationId); + const { medication, courses } = await loadOwned( + tx, + args.userId, + args.medicationId, + ); + if (!canEndCourse(courseStatusAt(medication, now, args.timeZone))) { + throw new CourseWriteError("notRunning"); + } + const latest = sortCourses(courses).at(-1); + if (latest) { + const ended = { startsOn: latest.startsOn, endsOn: today, endedAt }; + refuseIfInvalid( + courses.map((c) => (c.id === latest.id ? ended : c)), + todayKey, + medication.oneShot, + ); + await tx.medicationCourse.update({ + where: { id: latest.id }, + data: { endsOn: today, endedAt }, + }); + } else { + const implicit = windowAsCourse( + medication.startsOn, + today, + undefined, + medication.createdAt, + args.timeZone, + ); + refuseIfInvalid([implicit], todayKey, medication.oneShot); + await tx.medicationCourse.create({ + data: { + medicationId: args.medicationId, + userId: args.userId, + ...implicit, + endedAt, + }, + }); + } + await tx.medicationIntakeEvent.updateMany({ + where: { + userId: args.userId, + medicationId: args.medicationId, + deletedAt: null, + takenAt: null, + skipped: false, + autoMissed: false, + scheduledFor: { gte: now }, + }, + data: { deletedAt: now, syncVersion: { increment: 1 } }, + }); + return project(tx, args.medicationId); + }); +} + /** * Whether the window a PUT is about to write would be refused. Checked * BEFORE the medication row is updated, so a refused window leaves the row diff --git a/src/lib/medications/intake-actionable.ts b/src/lib/medications/intake-actionable.ts index cd586a720..4aaf27bcf 100644 --- a/src/lib/medications/intake-actionable.ts +++ b/src/lib/medications/intake-actionable.ts @@ -23,10 +23,13 @@ * Logging a past dose from the history is a different verb and is not * gated here; an ended course can still be completed after the fact. * - * #1280 — a course ended at an instant (`endedAt`) is ENDED from it, so the - * action row goes away at once instead of at midnight. + * #1280 — `canEndCourse` rides along: "End course" is offered exactly while + * the course is CURRENT, from the same resolution. A course ended with it is + * ENDED from that instant (`endedAt`), so the action row and the End button + * go away at once instead of at midnight. */ import { + canEndCourse, courseStatusAt, type CourseStatus, } from "@/lib/medications/course-window"; @@ -35,6 +38,7 @@ import { isRecordOnly } from "@/lib/medications/intake-tracking"; export interface IntakeActionability { courseStatus: CourseStatus; intakeActionable: boolean; + canEndCourse: boolean; } export function resolveIntakeActionability( @@ -55,5 +59,6 @@ export function resolveIntakeActionability( medication.active && !isRecordOnly(medication) && courseStatus === "CURRENT", + canEndCourse: canEndCourse(courseStatus), }; } diff --git a/src/lib/openapi/error-codes.ts b/src/lib/openapi/error-codes.ts index 7433c5490..eefaaf11d 100644 --- a/src/lib/openapi/error-codes.ts +++ b/src/lib/openapi/error-codes.ts @@ -312,6 +312,7 @@ export const ERROR_CODE_CATALOGUE: Readonly> = "medications.course.invalid", "medications.course.invalidRange", "medications.course.notFound", + "medications.course.notRunning", "medications.course.oneShot", "medications.course.overlap", "medications.course.windowRequired", diff --git a/src/lib/openapi/routes/medications/paths.ts b/src/lib/openapi/routes/medications/paths.ts index d5ef6928f..0f8961e85 100644 --- a/src/lib/openapi/routes/medications/paths.ts +++ b/src/lib/openapi/routes/medications/paths.ts @@ -1178,6 +1178,45 @@ export const medicationPaths: NonNullable = { }, }, }, + "/api/medications/{id}/courses/end": { + post: { + parameters: [idempotencyKeyParameter], + tags: ["Medications"], + summary: "End the running course now", + description: + "#1280 — \"End course\". No body. The running course's last day becomes today on the person's clock and the moment is stored as its `endedAt`: a dose slot later today is not due, not reminded and not expected by compliance, a dose taken earlier today still counts, and `courseStatus` reads ENDED at once. Pending placeholders for slots at or after the moment are removed (tombstoned for sync). A medication with no dates gets the course its window describes, from creation to today. An end date set through `PUT /api/medications/{id}` or the course editor keeps meaning the whole day; only this action ends a course at an instant. Refused with 409 `medications.course.notRunning` while no course is running — the rule the reads publish as `canEndCourse`. Owner-only.", + requestParams: { path: z.object({ id: z.string() }) }, + responses: { + ...idempotentWrite(), + "200": { + description: "The medication's window after ending.", + content: { + "application/json": { + schema: dataEnvelope( + z.object({ + startsOn: z.iso.date().nullable(), + endsOn: z.iso.date().nullable(), + endedAt: isoDateTime().nullable(), + }), + "EndMedicationCourseResponse", + ), + }, + }, + }, + "404": { + description: + "Medication not found (`medications.course.notFound`), or another user's.", + content: { "application/json": { schema: errorEnvelope } }, + }, + "409": { + description: + "No course is running now (`medications.course.notRunning`): it has ended already or has not started.", + content: { "application/json": { schema: errorEnvelope } }, + }, + ...stdResponses, + }, + }, + }, "/api/medications/{id}/courses/{courseId}": { patch: { tags: ["Medications"], diff --git a/src/lib/openapi/routes/medications/schemas.ts b/src/lib/openapi/routes/medications/schemas.ts index 2e6c11d0c..6a5d44487 100644 --- a/src/lib/openapi/routes/medications/schemas.ts +++ b/src/lib/openapi/routes/medications/schemas.ts @@ -69,7 +69,7 @@ export const medicationCourseResource = z endedAt: isoDateTime() .nullable() .describe( - '#1280 — the instant "End course" ended this course, on its `endsOn` day. No dose slot at or after it is due, reminded or expected, and the course is ENDED from it on. NULL when the whole `endsOn` day counts (an end date set in the editor). Any edit that moves `endsOn` clears it.', + "#1280 — the instant `POST /api/medications/{id}/courses/end` ended this course, on its `endsOn` day. No dose slot at or after it is due, reminded or expected, and the course is ENDED from it on. NULL when the whole `endsOn` day counts (an end date set in the editor). Any edit that moves `endsOn` clears it.", ), status: z .enum(["UPCOMING", "CURRENT", "ENDED"]) @@ -375,7 +375,7 @@ export const medicationResource = z .nullable() .optional() .describe( - '#1280 — the instant the latest course was ended with "End course" (the course\'s own `endedAt`). From it on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. Read-only — written by the course writer only.', + "#1280 — the instant the latest course was ended with `POST /api/medications/{id}/courses/end` (the course's own `endedAt`). From it on no dose is due or reminded and `courseStatus` is ENDED; NULL when the course runs to the end of `endsOn`. Read-only — written by the course writer only.", ), oneShot: z .boolean() @@ -453,13 +453,18 @@ export const medicationListEntry = medicationResource courseStatus: z .enum(["UPCOMING", "CURRENT", "ENDED"]) .describe( - 'v1.39.4 — where today (on the user\'s clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all day, unless it was ended with "End course": then it is ENDED from that instant (`endedAt`). Read-only — computed, not stored.', + "v1.39.4 — where today (on the user's clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all day, unless it was ended with `POST /api/medications/{id}/courses/end`: then it is ENDED from that instant (`endedAt`). Read-only — computed, not stored.", ), intakeActionable: z .boolean() .describe( "v1.39.4 — whether the medication offers taken / skip today: true only when it is `active`, its intake is tracked (`trackIntake`) and `courseStatus` is CURRENT. Clients render the dose actions from this and do not re-derive it. Logging a past dose from the history is not gated by it. Read-only — computed, not stored.", ), + canEndCourse: z + .boolean() + .describe( + '#1280 — whether `POST /api/medications/{id}/courses/end` may end the course now: true exactly while a course is running (`courseStatus` CURRENT; a medication with no dates is running). Clients offer "End course" from this and do not re-derive it; the action refuses with 409 `medications.course.notRunning` when it is false. Read-only — computed, not stored.', + ), lastTakenAt: isoDateTime() .nullable() .describe( @@ -519,13 +524,18 @@ export const medicationDetailEntry = medicationResource courseStatus: z .enum(["UPCOMING", "CURRENT", "ENDED"]) .describe( - 'v1.39.4 — where today (on the user\'s clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all day, unless it was ended with "End course": then it is ENDED from that instant (`endedAt`). Read-only — computed, not stored.', + "v1.39.4 — where today (on the user's clock) sits in the course: UPCOMING before `startsOn`, ENDED after `endsOn`, CURRENT otherwise. Calendar days, both ends inclusive, so a course that ends today is CURRENT all day, unless it was ended with `POST /api/medications/{id}/courses/end`: then it is ENDED from that instant (`endedAt`). Read-only — computed, not stored.", ), intakeActionable: z .boolean() .describe( "v1.39.4 — whether the medication offers taken / skip today: true only when it is `active`, its intake is tracked (`trackIntake`) and `courseStatus` is CURRENT. Clients render the dose actions from this and do not re-derive it. Logging a past dose from the history is not gated by it. Read-only — computed, not stored.", ), + canEndCourse: z + .boolean() + .describe( + '#1280 — whether `POST /api/medications/{id}/courses/end` may end the course now: true exactly while a course is running (`courseStatus` CURRENT; a medication with no dates is running). Clients offer "End course" from this and do not re-derive it; the action refuses with 409 `medications.course.notRunning` when it is false. Read-only — computed, not stored.', + ), stockUnitsRemaining: z .number() .nullable() diff --git a/tests/integration/medication-course-end.test.ts b/tests/integration/medication-course-end.test.ts new file mode 100644 index 000000000..f60717e17 --- /dev/null +++ b/tests/integration/medication-course-end.test.ts @@ -0,0 +1,488 @@ +/** + * "End course" (#1280) against a real Postgres, through the shipped routes, + * the reminder worker, the today projector and the auto-miss pass. + * + * What the file pins: + * - the medication's PUT takes `endsOn` as a date (the request the End + * button used to send as a full instant was refused with 422), and an + * end date set that way keeps meaning the whole day; + * - `POST /courses/end` ends the running course at that moment: the last + * day is today on the person's clock and `endedAt` the instant, and the + * reads serve it ENDED with `canEndCourse` false at once; + * - a dose slot later that day is not due (no placeholder on the today + * list), not reminded and never missed, while a dose taken earlier that + * day still counts in compliance; + * - `canEndCourse` is true only while a course runs, and the action refuses + * an ended or upcoming course with 409; + * - an edit that moves the end clears the instant. + * + * The person lives in Europe/Berlin (UTC+2 in June). The medication doses at + * 08:00 and 20:00 local, 06:00Z and 18:00Z. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { NextRequest } from "next/server"; + +import { cookieJar, headerJar } from "./mock-next-headers"; +import { getPrismaClient, truncateAllTables } from "./setup"; + +vi.mock("next/headers", async () => { + const { cookieJar, headerJar } = await import("./mock-next-headers"); + return { + headers: vi.fn(async () => ({ + get: (name: string) => headerJar.get(name.toLowerCase()) ?? null, + })), + cookies: vi.fn(async () => ({ + get: (name: string) => { + const value = cookieJar.get(name); + return value ? { name, value } : undefined; + }, + set: (name: string, value: string) => cookieJar.set(name, value), + delete: (name: string) => cookieJar.delete(name), + })), + }; +}); +vi.mock("@/lib/db-compat", () => ({ + ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), +})); +vi.mock("@/lib/notifications/dispatcher", () => ({ + dispatchNotification: vi.fn().mockResolvedValue({ + dispatched: true, + channelsAttempted: 1, + channelsSucceeded: 1, + }), +})); +vi.mock("@/lib/notifications/medication-intake-sync", () => ({ + queueMedicationIntakeSync: vi.fn(), +})); + +const { dispatchNotification } = await import("@/lib/notifications/dispatcher"); +const { runIntakeAutoSkipPass } = await import("@/lib/jobs/intake-auto-skip"); +const { handleReminderCheck } = + await import("@/lib/jobs/reminder/medication-reminder-check"); + +const TZ = "Europe/Berlin"; +const MORNING = "2026-06-10T06:00:00.000Z"; +const EVENING = "2026-06-10T18:00:00.000Z"; +const ENDED_AT = "2026-06-10T10:00:00.000Z"; + +type Handler = ( + request: NextRequest, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: { params: Promise }, +) => Promise; + +function at(iso: string): void { + vi.setSystemTime(new Date(iso)); +} + +function req(method: string, path: string, body?: unknown) { + return new NextRequest(`http://localhost${path}`, { + method, + headers: { "content-type": "application/json" }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); +} + +async function call( + modulePath: string, + method: "GET" | "POST" | "PUT" | "PATCH", + path: string, + params: Record, + body?: unknown, +) { + const route = (await import(/* @vite-ignore */ modulePath)) as Record< + string, + Handler + >; + const res = await route[method](req(method, path, body), { + params: Promise.resolve(params), + }); + return { status: res.status, body: await res.json() }; +} + +const endCourse = (id: string) => + call( + "@/app/api/medications/[id]/courses/end/route", + "POST", + `/api/medications/${id}/courses/end`, + { id }, + ); +const putMedication = (id: string, body: unknown) => + call( + "@/app/api/medications/[id]/route", + "PUT", + `/api/medications/${id}`, + { id }, + body, + ); +const detail = async (id: string) => + ( + await call( + "@/app/api/medications/[id]/route", + "GET", + `/api/medications/${id}`, + { id }, + ) + ).body.data; + +async function makeUser(): Promise { + const prisma = getPrismaClient(); + const user = await prisma.user.create({ + data: { + username: "course-end", + email: "course-end@example.test", + timezone: TZ, + locale: "en", + }, + }); + const session = await prisma.session.create({ + data: { + userId: user.id, + expiresAt: new Date("2030-01-01T00:00:00.000Z"), + }, + }); + cookieJar.set("healthlog_session", session.id); + return user.id; +} + +/** Twice a day, 08:00 and 20:00, created early on the 10th. */ +async function seedTwiceDaily( + userId: string, + window: { startsOn?: Date; endsOn?: Date } = {}, +): Promise { + const medication = await getPrismaClient().medication.create({ + data: { + userId, + name: "Amoxicillin", + dose: "500 mg", + createdAt: new Date("2026-06-10T03:00:00.000Z"), + notificationsEnabled: true, + ...window, + schedules: { + create: { + windowStart: "08:00", + windowEnd: "08:00", + timesOfDay: ["08:00", "20:00"], + rrule: "FREQ=DAILY", + scheduleType: "SCHEDULED", + }, + }, + }, + select: { id: true }, + }); + return medication.id; +} + +async function takeMorningDose(userId: string, medicationId: string) { + await getPrismaClient().medicationIntakeEvent.create({ + data: { + userId, + medicationId, + scheduledFor: new Date(MORNING), + takenAt: new Date("2026-06-10T06:10:00.000Z"), + source: "WEB", + }, + }); +} + +async function liveRowsOn(medicationId: string, scheduledFor: string) { + return getPrismaClient().medicationIntakeEvent.findMany({ + where: { + medicationId, + deletedAt: null, + scheduledFor: new Date(scheduledFor), + }, + }); +} + +function dispatchedFor(medicationId: string): number { + return vi + .mocked(dispatchNotification) + .mock.calls.filter( + ([arg]) => + (arg as { metadata?: { medicationId?: string } }).metadata + ?.medicationId === medicationId, + ).length; +} + +beforeEach(async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.mocked(dispatchNotification).mockClear(); + cookieJar.clear(); + headerJar.clear(); + await truncateAllTables(getPrismaClient()); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("the medication PUT takes the end as a date (#1280)", () => { + it("answers 200 for a date and 422 for the full instant the button used to send", async () => { + at(ENDED_AT); + const userId = await makeUser(); + const id = await seedTwiceDaily(userId); + + const instant = await putMedication(id, { endsOn: ENDED_AT }); + expect(instant.status).toBe(422); + + const date = await putMedication(id, { endsOn: "2026-06-10" }); + expect(date.status, JSON.stringify(date.body)).toBe(200); + // An end date written this way is the whole day: still running, still + // offering End, no instant recorded. + const read = await detail(id); + expect(read.courseStatus).toBe("CURRENT"); + expect(read.canEndCourse).toBe(true); + expect(read.endedAt).toBeNull(); + expect(read.courses[0]).toMatchObject({ + endsOn: "2026-06-10", + endedAt: null, + }); + }); +}); + +describe("POST /courses/end ends the course now", () => { + it("records today and the instant, and the reads serve it ended at once", async () => { + at(ENDED_AT); + const userId = await makeUser(); + const id = await seedTwiceDaily(userId); + expect((await detail(id)).canEndCourse).toBe(true); + + const res = await endCourse(id); + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(res.body.data).toEqual({ + startsOn: "2026-06-10", + endsOn: "2026-06-10", + endedAt: ENDED_AT, + }); + + const course = await getPrismaClient().medicationCourse.findFirstOrThrow({ + where: { medicationId: id }, + }); + expect(course.endedAt?.toISOString()).toBe(ENDED_AT); + const read = await detail(id); + expect(read).toMatchObject({ + endedAt: ENDED_AT, + courseStatus: "ENDED", + canEndCourse: false, + intakeActionable: false, + remindersActive: false, + canStartCourse: true, + }); + expect(read.courses[0]).toMatchObject({ + status: "ENDED", + endsOn: "2026-06-10", + endedAt: ENDED_AT, + }); + + // A second press finds nothing running. + const again = await endCourse(id); + expect(again.status).toBe(409); + expect(again.body.meta.errorCode).toBe("medications.course.notRunning"); + }); + + it("drops the evening dose: not listed, not reminded, never missed", async () => { + at("2026-06-10T09:00:00.000Z"); + const userId = await makeUser(); + const id = await seedTwiceDaily(userId); + // The today list mints both placeholders before the course is ended. + const today = () => + call( + "@/app/api/medications/intake/route", + "GET", + "/api/medications/intake?scope=today", + {}, + ); + expect((await today()).status).toBe(200); + expect(await liveRowsOn(id, EVENING)).toHaveLength(1); + + at(ENDED_AT); + expect((await endCourse(id)).status).toBe(200); + expect(await liveRowsOn(id, EVENING)).toHaveLength(0); + + // The today list does not bring it back. + at("2026-06-10T12:00:00.000Z"); + expect((await today()).status).toBe(200); + expect(await liveRowsOn(id, EVENING)).toHaveLength(0); + + // Through the evening window and its follow-ups: no reminder, no + // missed-dose placeholder. + for (const tick of [ + "2026-06-10T18:00:00.000Z", + "2026-06-10T18:45:00.000Z", + "2026-06-10T19:30:00.000Z", + "2026-06-10T21:00:00.000Z", + ]) { + at(tick); + await handleReminderCheck([]); + } + expect(dispatchedFor(id)).toBe(0); + expect(await liveRowsOn(id, EVENING)).toHaveLength(0); + + // Two days on, the auto-miss pass stamps the morning dose, which fell + // due while the course ran and was never taken, and nothing after the + // end. + await runIntakeAutoSkipPass(getPrismaClient(), { + nowMs: Date.parse("2026-06-12T12:00:00.000Z"), + }); + const missed = await getPrismaClient().medicationIntakeEvent.findMany({ + where: { medicationId: id, autoMissed: true }, + select: { scheduledFor: true }, + }); + expect(missed.map((r) => r.scheduledFor.toISOString())).toEqual([MORNING]); + }); + + it("lists no evening dose when the day's list is first read after the end", async () => { + at(ENDED_AT); + const userId = await makeUser(); + const id = await seedTwiceDaily(userId); + expect((await endCourse(id)).status).toBe(200); + const today = await call( + "@/app/api/medications/intake/route", + "GET", + "/api/medications/intake?scope=today", + {}, + ); + expect(today.status).toBe(200); + expect(await liveRowsOn(id, MORNING)).toHaveLength(1); + expect(await liveRowsOn(id, EVENING)).toHaveLength(0); + }); + + it("keeps the morning dose taken before the end in compliance", async () => { + at(ENDED_AT); + const userId = await makeUser(); + const id = await seedTwiceDaily(userId); + await takeMorningDose(userId, id); + expect((await endCourse(id)).status).toBe(200); + + at("2026-06-11T08:00:00.000Z"); + const compliance = await call( + "@/app/api/medications/[id]/compliance/route", + "GET", + `/api/medications/${id}/compliance`, + { id }, + ); + expect(compliance.status).toBe(200); + expect(compliance.body.data.compliance7).toMatchObject({ + totalExpected: 1, + taken: 1, + missed: 0, + rate: 100, + }); + + const history = await call( + "@/app/api/medications/[id]/dose-history/route", + "GET", + `/api/medications/${id}/dose-history?from=2026-06-10T00:00:00.000Z`, + { id }, + ); + expect(history.status).toBe(200); + const slots = ( + history.body.data.rows as Array<{ + kind: string; + at: string; + status: string; + }> + ).filter((r) => r.kind === "slot"); + expect(slots.map((r) => [r.at, r.status])).toEqual([ + [MORNING, "taken_on_time"], + ]); + }); + + it("ends a medication without dates from its creation day", async () => { + at(ENDED_AT); + const userId = await makeUser(); + const id = await seedTwiceDaily(userId); + expect( + await getPrismaClient().medicationCourse.count({ + where: { medicationId: id }, + }), + ).toBe(0); + expect((await endCourse(id)).status).toBe(200); + const row = await getPrismaClient().medication.findUniqueOrThrow({ + where: { id }, + select: { startsOn: true, endsOn: true, endedAt: true }, + }); + expect(row.startsOn?.toISOString().slice(0, 10)).toBe("2026-06-10"); + expect(row.endsOn?.toISOString().slice(0, 10)).toBe("2026-06-10"); + expect(row.endedAt?.toISOString()).toBe(ENDED_AT); + }); +}); + +describe("canEndCourse follows the course", () => { + it.each([ + ["running (no dates)", {}, true], + [ + "running (dated)", + { startsOn: new Date("2026-06-01T00:00:00.000Z") }, + true, + ], + ["upcoming", { startsOn: new Date("2026-06-20T00:00:00.000Z") }, false], + [ + "ended", + { + startsOn: new Date("2026-05-01T00:00:00.000Z"), + endsOn: new Date("2026-05-31T00:00:00.000Z"), + }, + false, + ], + ] as const)("%s → %s", async (_label, window, expected) => { + at(ENDED_AT); + const userId = await makeUser(); + const id = await seedTwiceDaily(userId, window); + expect((await detail(id)).canEndCourse).toBe(expected); + const list = await call( + "@/app/api/medications/route", + "GET", + "/api/medications", + {}, + ); + const entry = ( + list.body.data as Array<{ id: string; canEndCourse: boolean }> + ).find((m) => m.id === id); + expect(entry?.canEndCourse).toBe(expected); + + const res = await endCourse(id); + expect(res.status).toBe(expected ? 200 : 409); + }); +}); + +describe("an edit that moves the end clears the instant", () => { + it("reopens the course to the end of its new last day", async () => { + at(ENDED_AT); + const userId = await makeUser(); + const id = await seedTwiceDaily(userId); + expect((await endCourse(id)).status).toBe(200); + const course = await getPrismaClient().medicationCourse.findFirstOrThrow({ + where: { medicationId: id }, + }); + + // Same last day, only the note: the instant stays. + const note = await call( + "@/app/api/medications/[id]/courses/[courseId]/route", + "PATCH", + `/api/medications/${id}/courses/${course.id}`, + { id, courseId: course.id }, + { note: "Stopped after the morning dose" }, + ); + expect(note.status, JSON.stringify(note.body)).toBe(200); + expect((await detail(id)).endedAt).toBe(ENDED_AT); + + // A later last day: a whole-day end again, running once more. + const moved = await call( + "@/app/api/medications/[id]/courses/[courseId]/route", + "PATCH", + `/api/medications/${id}/courses/${course.id}`, + { id, courseId: course.id }, + { endsOn: "2026-06-12" }, + ); + expect(moved.status, JSON.stringify(moved.body)).toBe(200); + const read = await detail(id); + expect(read).toMatchObject({ + endedAt: null, + courseStatus: "CURRENT", + canEndCourse: true, + }); + }); +}); From 3df06b137d7364535148b88d8ce9bdf42ce4535e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 24 Oct 2026 22:25:18 +0200 Subject: [PATCH 08/11] chore(ratchet): lower the size baselines the course end shrank Refs #1280 --- src/__tests__/baselines/source-file-lines.json | 8 ++++---- src/__tests__/baselines/source-function-lines.json | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/__tests__/baselines/source-file-lines.json b/src/__tests__/baselines/source-file-lines.json index 3721a2163..89987a027 100644 --- a/src/__tests__/baselines/source-file-lines.json +++ b/src/__tests__/baselines/source-file-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", "count": 88, - "total": 98280, + "total": 98267, "entries": { "app/medications/page-client.tsx": 839, "components/charts/mood-chart.tsx": 1166, @@ -13,7 +13,7 @@ "components/insights/health-score-card.tsx": 1023, "components/insights/insights-tab-strip.tsx": 1086, "components/measurements/measurement-form.tsx": 815, - "components/medications/detail/medication-detail-tabs.tsx": 1160, + "components/medications/detail/medication-detail-tabs.tsx": 1157, "components/medications/medication-table.tsx": 853, "components/medications/wizard/wizard-payload.ts": 1414, "components/mood/mood-list.tsx": 1408, @@ -77,8 +77,8 @@ "lib/mcp/rich-reads.ts": 1467, "lib/measurements/dense-intraday-retention.ts": 1057, "lib/measurements/import-apple-health-export.ts": 1722, - "lib/medications/scheduling/cadence.ts": 978, - "lib/medications/scheduling/recurrence.ts": 1108, + "lib/medications/scheduling/cadence.ts": 976, + "lib/medications/scheduling/recurrence.ts": 1100, "lib/medications/scheduling/slot-upsert.ts": 912, "lib/notifications/senders/apns.ts": 1100, "lib/oura/sync.ts": 900, diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index 3a8b2011a..09e7d4fc7 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", "count": 351, - "total": 113740, + "total": 113731, "entries": { "app/achievements/page.tsx :: AchievementsPage": 206, "app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314, @@ -152,7 +152,7 @@ "components/measurement-reminders/vorsorge-section.tsx :: VorsorgeSection": 352, "components/measurements/measurement-form.tsx :: MeasurementForm": 500, "components/medications/detail/efficacy/efficacy-chart.tsx :: EfficacyChart": 361, - "components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 771, + "components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 767, "components/medications/dose-history-add-dialog.tsx :: LedgerAddDialog": 223, "components/medications/dose-strength-curve.tsx :: DoseStrengthCurve": 266, "components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 405, @@ -248,7 +248,7 @@ "lib/ai/anthropic-client.ts :: generateCompletion": 294, "lib/ai/coach/cycle-snapshot.ts :: buildCycleSnapshotBlock": 218, "lib/ai/coach/glp1-snapshot.ts :: buildGlp1SnapshotBlock": 218, - "lib/ai/coach/snapshot.ts :: buildCoachSnapshotImpl": 882, + "lib/ai/coach/snapshot.ts :: buildCoachSnapshotImpl": 881, "lib/ai/coach/tools/loop.ts :: runCoachToolLoop": 438, "lib/ai/coach/turn/model.ts :: runTurnModel": 540, "lib/ai/coach/turn/pipeline.ts :: runTurn": 274, @@ -276,7 +276,7 @@ "lib/doctor-report-pdf/clinical-records-notes-section.ts :: buildClinicalRecordsNotesSection": 648, "lib/doctor-report-pdf/measurements-charts-section.ts :: buildMeasurementsChartsSection": 384, "lib/doctor-report-pdf/medication-mood-wellness-section.ts :: buildMedicationMoodWellnessSection": 509, - "lib/doctor-report/collect.ts :: collectDoctorReportData": 668, + "lib/doctor-report/collect.ts :: collectDoctorReportData": 667, "lib/doctor-report/dense-buckets.ts :: loadDenseMeasurementBuckets": 210, "lib/documents/fenced-chat.ts :: streamFencedReply": 309, "lib/documents/ingest.ts :: ingestDocument": 272, @@ -333,8 +333,8 @@ "lib/measurements/import-apple-health-export.ts :: streamParseExportXml": 1245, "lib/measurements/import-apple-health-export.ts :: streamParseExportXml > (anonymous) #2": 323, "lib/medications/efficacy/build-efficacy.ts :: buildMedicationEfficacy": 268, - "lib/medications/intake-slot-dedup.ts :: dedupeUserIntakeSlots": 321, - "lib/medications/list-read.ts :: buildMedicationsList": 309, + "lib/medications/intake-slot-dedup.ts :: dedupeUserIntakeSlots": 319, + "lib/medications/list-read.ts :: buildMedicationsList": 308, "lib/medications/scheduling/dose-history.ts :: reconstructDoseHistory": 218, "lib/multipart/stream-to-disk.ts :: streamMultipartToDisk": 272, "lib/notifications/dispatcher.ts :: dispatchNotification": 378, From fd2c20dffa581916b73b7072095813568d62fdb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 24 Oct 2026 22:28:20 +0200 Subject: [PATCH 09/11] test(modules): list the course end route with its sibling course routes Ending the running course writes the same course rows the other course routes do, so it follows the same data-layer reasoning: the medications module gates the surface, not the row store. Refs #1280 --- src/app/api/__tests__/module-route-gate-inventory.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/api/__tests__/module-route-gate-inventory.test.ts b/src/app/api/__tests__/module-route-gate-inventory.test.ts index 9758c75e4..4ab806ca6 100644 --- a/src/app/api/__tests__/module-route-gate-inventory.test.ts +++ b/src/app/api/__tests__/module-route-gate-inventory.test.ts @@ -285,6 +285,8 @@ const EXEMPT_ROUTES: ReadonlyArray = [ // v1.40 (#1024) — a medication's courses, same again. "src/app/api/medications/[id]/courses/route.ts", "src/app/api/medications/[id]/courses/[courseId]/route.ts", + // #1280 — ending the running course is a write to the same course rows. + "src/app/api/medications/[id]/courses/end/route.ts", "src/app/api/medications/compliance/route.ts", "src/app/api/medications/intake/route.ts", "src/app/api/medications/intake/bulk/route.ts", From 8649e0413ff5de58e9357a4a79126ff3a2298eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 24 Oct 2026 22:37:43 +0200 Subject: [PATCH 10/11] test(parity): add the course end fields to the medication route snapshots The medication reads now carry `endedAt` and `canEndCourse`, and every course its `endedAt`; the row dumps carry the new columns. Additions only. Refs #1280 --- .../medication-detail.01-rolling.json.snap | 11 +++++++ .../medication-detail.02-daily.json.snap | 11 +++++++ ...medication-detail.03-record-only.json.snap | 11 +++++++ .../medication-detail.04-not-found.json.snap | 9 ++++++ .../medication-detail.05a-reader.json.snap | 11 +++++++ .../medication-detail.05b-anonymous.json.snap | 9 ++++++ ...edication-detail.11-plain-fields.json.snap | 11 +++++++ ...ation-detail.12-schedule-replace.json.snap | 11 +++++++ ...dication-detail.13-schedule-echo.json.snap | 11 +++++++ ...edication-detail.14-grace-bridge.json.snap | 11 +++++++ .../medication-detail.15a-pause.json.snap | 11 +++++++ .../medication-detail.15b-resume.json.snap | 11 +++++++ ...cation-detail.16-auto-mark-taken.json.snap | 11 +++++++ ...ation-detail.17a-custom-category.json.snap | 11 +++++++ ...ion-detail.17b-native-other-kept.json.snap | 11 +++++++ .../medication-detail.17c-web-other.json.snap | 11 +++++++ ...tion-detail.17d-unknown-category.json.snap | 9 ++++++ ...-detail.18a-record-schedule-kept.json.snap | 11 +++++++ ...edication-detail.18b-tracking-on.json.snap | 11 +++++++ ...dication-detail.18c-tracking-off.json.snap | 11 +++++++ ....19a-scheduled-without-schedules.json.snap | 9 ++++++ .../medication-detail.19b-scheduled.json.snap | 11 +++++++ ....19c-as-needed-keeping-schedules.json.snap | 9 ++++++ .../medication-detail.19d-as-needed.json.snap | 11 +++++++ ...etail.20a-one-shot-two-schedules.json.snap | 9 ++++++ ...n-detail.20b-one-shot-recurrence.json.snap | 9 ++++++ .../medication-detail.20c-one-shot.json.snap | 13 ++++++++ ...cation-detail.21a-course-overlap.json.snap | 10 +++++++ ...etail.21b-course-window-required.json.snap | 10 +++++++ ...medication-detail.21c-course-end.json.snap | 14 +++++++++ ...tion-detail.22-composition-units.json.snap | 12 ++++++++ .../medication-detail.23a-invalid.json.snap | 10 +++++++ .../medication-detail.23b-not-found.json.snap | 10 +++++++ .../medication-detail.24a-writer.json.snap | 10 +++++++ .../medication-detail.24b-manager.json.snap | 12 ++++++++ .../medication-detail.24c-anonymous.json.snap | 10 +++++++ .../medication-detail.31-delete.json.snap | 9 ++++++ .../medication-detail.32-not-found.json.snap | 9 ++++++ .../medication-detail.33a-manager.json.snap | 9 ++++++ .../medication-detail.33b-anonymous.json.snap | 9 ++++++ .../medication-intake-bulk.01-mixed.json.snap | 4 +++ ...ation-intake-bulk.02-mixed-again.json.snap | 4 +++ ...ation-intake-bulk.03a-background.json.snap | 4 +++ ...ication-intake-bulk.03b-backfill.json.snap | 4 +++ ...cation-intake-bulk.04a-too-large.json.snap | 4 +++ ...dication-intake-bulk.04b-invalid.json.snap | 4 +++ ...take-bulk.04c-apple-not-mirrored.json.snap | 4 +++ ...edication-intake-bulk.05a-native.json.snap | 4 +++ ...ion-intake-bulk.05b-narrow-token.json.snap | 4 +++ ...ication-intake-bulk.05c-delegate.json.snap | 4 +++ ...cation-intake-bulk.05d-anonymous.json.snap | 4 +++ ...on-intake-event.01-take-in-place.json.snap | 2 ++ ...cation-intake-event.02-skip-flip.json.snap | 2 ++ ...ion-intake-event.03-move-by-band.json.snap | 2 ++ ...on-intake-event.04-pin-free-slot.json.snap | 2 ++ ...n-intake-event.05-pin-not-a-slot.json.snap | 2 ++ ...ion-intake-event.06-pin-occupied.json.snap | 2 ++ ...medication-intake-event.07-unpin.json.snap | 2 ++ ...on-intake-event.08-explicit-slot.json.snap | 2 ++ ...ion-intake-event.09-before-start.json.snap | 2 ++ ...dication-intake-event.10-invalid.json.snap | 2 ++ ...tion-intake-event.11a-tombstoned.json.snap | 2 ++ ...ntake-event.11b-other-medication.json.snap | 2 ++ ...dication-intake-event.12a-writer.json.snap | 2 ++ ...ication-intake-event.12b-manager.json.snap | 2 ++ ...cation-intake-event.13-anonymous.json.snap | 2 ++ ...edication-intake-event.21-delete.json.snap | 2 ++ ...-intake-event.22-delete-one-shot.json.snap | 2 ++ ...intake-event.23-other-medication.json.snap | 2 ++ ...dication-intake-event.24a-writer.json.snap | 2 ++ ...ication-intake-event.24b-manager.json.snap | 2 ++ ...cation-intake-event.25-anonymous.json.snap | 2 ++ ...edication-intake-status.01-today.json.snap | 4 +++ ...ntake-status.02a-compliance-cold.json.snap | 4 +++ ...ntake-status.02b-compliance-warm.json.snap | 4 +++ ...ication-intake-status.03-invalid.json.snap | 4 +++ ...ication-intake-status.04a-reader.json.snap | 4 +++ ...tion-intake-status.04b-anonymous.json.snap | 4 +++ ...n-intake-status.11-take-in-place.json.snap | 4 +++ ...-intake-status.12-take-slot-move.json.snap | 4 +++ ...medication-intake-status.13a-pin.json.snap | 4 +++ ...intake-status.13b-pin-not-a-slot.json.snap | 4 +++ ...medication-intake-status.14-skip.json.snap | 4 +++ ...n-intake-status.15a-snooze-until.json.snap | 4 +++ ...intake-status.15b-snooze-default.json.snap | 4 +++ ...cation-intake-status.16a-unknown.json.snap | 4 +++ ...cation-intake-status.16b-foreign.json.snap | 4 +++ ...ion-intake-status.16c-tombstoned.json.snap | 4 +++ ...take-status.17a-record-only-open.json.snap | 4 +++ ...tatus.17b-record-only-correction.json.snap | 4 +++ ...take-status.18a-site-not-allowed.json.snap | 4 +++ ...edication-intake-status.18b-site.json.snap | 4 +++ ...ication-intake-status.19-invalid.json.snap | 4 +++ ...on-intake-status.20a-writer-take.json.snap | 4 +++ ...status.20b-writer-snooze-refused.json.snap | 4 +++ ...us.20c-writer-correction-refused.json.snap | 4 +++ ...ication-intake-status.20d-reader.json.snap | 4 +++ ...tion-intake-status.20e-anonymous.json.snap | 4 +++ ...edication-intake.01-take-in-band.json.snap | 5 ++++ ...ation-intake.02a-take-standalone.json.snap | 5 ++++ ...ntake.02b-take-standalone-repeat.json.snap | 5 ++++ ...ation-intake.03a-skip-named-slot.json.snap | 5 ++++ ...ication-intake.03b-skip-slotless.json.snap | 5 ++++ ...delegate-skip-standalone-refused.json.snap | 5 ++++ ...intake.03d-owner-skip-standalone.json.snap | 5 ++++ .../medication-intake.04a-pin.json.snap | 5 ++++ ...cation-intake.04b-pin-not-a-slot.json.snap | 5 ++++ ...dication-intake.04c-pin-occupied.json.snap | 5 ++++ ...ication-intake.05-named-backfill.json.snap | 5 ++++ .../medication-intake.06a-body-key.json.snap | 5 ++++ ...ation-intake.06b-body-key-replay.json.snap | 5 ++++ ...medication-intake.07a-header-key.json.snap | 5 ++++ ...ion-intake.07b-header-key-replay.json.snap | 5 ++++ .../medication-intake.08-native.json.snap | 5 ++++ ...intake.09a-delegate-skip-refused.json.snap | 5 ++++ ...ation-intake.09b-owner-skip-flip.json.snap | 5 ++++ ...edication-intake.10a-writer-take.json.snap | 5 ++++ .../medication-intake.10b-reader.json.snap | 5 ++++ .../medication-intake.10c-anonymous.json.snap | 5 ++++ ...tion-intake.11a-site-not-allowed.json.snap | 5 ++++ ...ication-intake.11b-site-and-dose.json.snap | 5 ++++ .../medication-intake.12-one-shot.json.snap | 5 ++++ .../medication-intake.13-invalid.json.snap | 5 ++++ .../medication-intake.14a-not-found.json.snap | 5 ++++ ...edication-intake.14b-record-only.json.snap | 5 ++++ .../medication-intake.21-default.json.snap | 5 ++++ ...ication-intake.22-completed-page.json.snap | 5 ++++ .../medication-intake.23a-skipped.json.snap | 5 ++++ .../medication-intake.23b-taken.json.snap | 5 ++++ .../medication-intake.24-invalid.json.snap | 5 ++++ .../medication-intake.25a-not-found.json.snap | 5 ++++ .../medication-intake.25b-reader.json.snap | 5 ++++ .../medication-intake.25c-anonymous.json.snap | 5 ++++ .../medications.01-list.json.snap | 3 ++ .../medications.02a-list-reader.json.snap | 3 ++ .../medications.02b-list-anonymous.json.snap | 1 + .../medications.11-plain.json.snap | 4 +++ .../medications.12-full.json.snap | 7 +++++ .../medications.13a-as-needed.json.snap | 7 +++++ .../medications.13b-one-shot.json.snap | 10 +++++++ ...ications.13c-prn-and-legacy-days.json.snap | 10 +++++++ .../medications.13d-ended-window.json.snap | 13 ++++++++ ...medications.14a-unknown-category.json.snap | 10 +++++++ ...ons.14b-course-ends-before-start.json.snap | 10 +++++++ ...tions.14c-one-shot-two-schedules.json.snap | 10 +++++++ ...ications.14d-one-shot-recurrence.json.snap | 10 +++++++ ...ications.14e-unstable-concept-id.json.snap | 10 +++++++ .../medications.14f-invalid.json.snap | 10 +++++++ .../medications.15a-mirror.json.snap | 13 ++++++++ .../medications.15b-mirror-replay.json.snap | 13 ++++++++ .../medications.16-mirror-cap.json.snap | 30 +++++++++++++++++++ ...medications.17a-idempotent-first.json.snap | 14 +++++++++ ...edications.17b-idempotent-replay.json.snap | 14 +++++++++ .../medications.18a-writer.json.snap | 15 ++++++++++ .../medications.18b-reader.json.snap | 13 ++++++++ .../medications.19-anonymous.json.snap | 13 ++++++++ 156 files changed, 1027 insertions(+) diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.01-rolling.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.01-rolling.json.snap index 7d5a09722..fadfcd7b5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.01-rolling.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.01-rolling.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": 3, "stockDosesRemaining": 3, "runwayDays": 21, @@ -116,6 +118,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -150,6 +153,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -184,6 +188,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -218,6 +223,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -252,6 +258,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -286,6 +293,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -320,6 +328,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -492,6 +501,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -502,6 +512,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.02-daily.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.02-daily.json.snap index 618a09d7a..908bc834c 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.02-daily.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.02-daily.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -89,6 +90,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -127,6 +129,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -161,6 +164,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -195,6 +199,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -229,6 +234,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -263,6 +269,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -297,6 +304,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -331,6 +339,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -503,6 +512,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -513,6 +523,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.03-record-only.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.03-record-only.json.snap index 9b038c771..308885b93 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.03-record-only.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.03-record-only.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -69,6 +70,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": false, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -175,6 +179,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -209,6 +214,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -243,6 +249,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -277,6 +284,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -311,6 +319,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -483,6 +492,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -493,6 +503,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.04-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.04-not-found.json.snap index 4be62bf07..3a838452f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.04-not-found.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.04-not-found.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -410,6 +417,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -420,6 +428,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.05a-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.05a-reader.json.snap index 7d5a09722..fadfcd7b5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.05a-reader.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.05a-reader.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": 3, "stockDosesRemaining": 3, "runwayDays": 21, @@ -116,6 +118,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -150,6 +153,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -184,6 +188,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -218,6 +223,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -252,6 +258,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -286,6 +293,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -320,6 +328,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -492,6 +501,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -502,6 +512,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.05b-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.05b-anonymous.json.snap index 408d19cc6..3d27bb59f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.05b-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.05b-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -207,6 +212,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -241,6 +247,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -413,6 +420,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -423,6 +431,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.11-plain-fields.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.11-plain-fields.json.snap index a51c9f9c7..e29ffb88c 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.11-plain-fields.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.11-plain-fields.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -89,6 +90,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -127,6 +129,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -161,6 +164,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -195,6 +199,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -229,6 +234,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -263,6 +269,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -297,6 +304,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -331,6 +339,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -503,6 +512,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -513,6 +523,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.12-schedule-replace.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.12-schedule-replace.json.snap index 633798338..ccbedd002 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.12-schedule-replace.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.12-schedule-replace.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -135,6 +137,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -169,6 +172,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -203,6 +207,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +242,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -271,6 +277,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -305,6 +312,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -339,6 +347,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -563,6 +572,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -573,6 +583,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.13-schedule-echo.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.13-schedule-echo.json.snap index 798ac25e4..e65487eca 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.13-schedule-echo.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.13-schedule-echo.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -133,6 +135,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -167,6 +170,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -201,6 +205,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +240,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -269,6 +275,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -303,6 +310,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -337,6 +345,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -561,6 +570,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -571,6 +581,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.14-grace-bridge.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.14-grace-bridge.json.snap index b2c594dfe..758c2b692 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.14-grace-bridge.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.14-grace-bridge.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -133,6 +135,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -167,6 +170,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -201,6 +205,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +240,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -269,6 +275,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -303,6 +310,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -337,6 +345,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -561,6 +570,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -571,6 +581,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.15a-pause.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.15a-pause.json.snap index 341c62676..fb0924bcd 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.15a-pause.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.15a-pause.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": false, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -133,6 +135,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -167,6 +170,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -201,6 +205,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +240,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -269,6 +275,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -303,6 +310,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -337,6 +345,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -570,6 +579,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -580,6 +590,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.15b-resume.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.15b-resume.json.snap index eb29dbaf6..a2e1884af 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.15b-resume.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.15b-resume.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -133,6 +135,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -167,6 +170,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -201,6 +205,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +240,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -269,6 +275,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -303,6 +310,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -337,6 +345,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -570,6 +579,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -580,6 +590,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.16-auto-mark-taken.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.16-auto-mark-taken.json.snap index d9b77b14f..199a3e754 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.16-auto-mark-taken.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.16-auto-mark-taken.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -133,6 +135,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -167,6 +170,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -201,6 +205,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +240,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -269,6 +275,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -303,6 +310,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -337,6 +345,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -570,6 +579,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -580,6 +590,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.17a-custom-category.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.17a-custom-category.json.snap index 1aff35567..9ea6183b1 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.17a-custom-category.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.17a-custom-category.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -133,6 +135,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -167,6 +170,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -201,6 +205,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +240,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -269,6 +275,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -303,6 +310,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -337,6 +345,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -570,6 +579,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -580,6 +590,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.17b-native-other-kept.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.17b-native-other-kept.json.snap index d6d69d3b3..7253a5ac5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.17b-native-other-kept.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.17b-native-other-kept.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -135,6 +137,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -169,6 +172,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -203,6 +207,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +242,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -271,6 +277,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -305,6 +312,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -339,6 +347,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -572,6 +581,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -582,6 +592,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.17c-web-other.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.17c-web-other.json.snap index 8298a4f74..a0a2f9f89 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.17c-web-other.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.17c-web-other.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -133,6 +135,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -167,6 +170,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -201,6 +205,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +240,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -269,6 +275,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -303,6 +310,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -337,6 +345,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -570,6 +579,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -580,6 +590,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.17d-unknown-category.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.17d-unknown-category.json.snap index 42df8b7a2..257b03adb 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.17d-unknown-category.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.17d-unknown-category.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -471,6 +478,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -481,6 +489,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.18a-record-schedule-kept.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.18a-record-schedule-kept.json.snap index b8c24ee3e..23d2b7dd8 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.18a-record-schedule-kept.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.18a-record-schedule-kept.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -69,6 +70,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": false, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -111,6 +113,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -145,6 +148,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -179,6 +183,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -213,6 +218,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -247,6 +253,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -281,6 +288,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -315,6 +323,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -548,6 +557,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -558,6 +568,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.18b-tracking-on.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.18b-tracking-on.json.snap index fb5603aae..3d331df26 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.18b-tracking-on.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.18b-tracking-on.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -109,6 +111,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -143,6 +146,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -177,6 +181,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -211,6 +216,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -245,6 +251,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -279,6 +286,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -313,6 +321,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -556,6 +565,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -566,6 +576,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.18c-tracking-off.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.18c-tracking-off.json.snap index 725e80fbf..243bfedfe 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.18c-tracking-off.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.18c-tracking-off.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -69,6 +70,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": false, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -110,6 +112,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -144,6 +147,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -178,6 +182,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -212,6 +217,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -246,6 +252,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -280,6 +287,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -314,6 +322,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -585,6 +594,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -595,6 +605,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.19a-scheduled-without-schedules.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.19a-scheduled-without-schedules.json.snap index a114b01cb..1b9000bc5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.19a-scheduled-without-schedules.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.19a-scheduled-without-schedules.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -509,6 +516,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -519,6 +527,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.19b-scheduled.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.19b-scheduled.json.snap index fe0b6e967..43a995ddf 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.19b-scheduled.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.19b-scheduled.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -108,6 +110,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -142,6 +145,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -176,6 +180,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -210,6 +215,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -244,6 +250,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -278,6 +285,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -312,6 +320,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -613,6 +622,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -623,6 +633,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.19c-as-needed-keeping-schedules.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.19c-as-needed-keeping-schedules.json.snap index fbcedbd50..302d9b2d6 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.19c-as-needed-keeping-schedules.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.19c-as-needed-keeping-schedules.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -539,6 +546,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -549,6 +557,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.19d-as-needed.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.19d-as-needed.json.snap index 582f4f13f..ab4e2d3fb 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.19d-as-needed.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.19d-as-needed.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -46,6 +47,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -86,6 +88,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -120,6 +123,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -154,6 +158,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -188,6 +193,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -222,6 +228,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -256,6 +263,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -290,6 +298,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -599,6 +608,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -609,6 +619,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.20a-one-shot-two-schedules.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.20a-one-shot-two-schedules.json.snap index bf46e1b89..c865ac348 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.20a-one-shot-two-schedules.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.20a-one-shot-two-schedules.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -547,6 +554,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -557,6 +565,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.20b-one-shot-recurrence.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.20b-one-shot-recurrence.json.snap index a7639f5bc..5c7c0c0ca 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.20b-one-shot-recurrence.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.20b-one-shot-recurrence.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -547,6 +554,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -557,6 +565,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.20c-one-shot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.20c-one-shot.json.snap index 0560579b9..45d3302dd 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.20c-one-shot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.20c-one-shot.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -64,6 +65,7 @@ "id": "", "startsOn": "2026-10-20", "endsOn": "2026-10-20", + "endedAt": null, "status": "UPCOMING", "takenDoses": 0, "note": null @@ -77,6 +79,7 @@ "nextDueOverdue": false, "courseStatus": "UPCOMING", "intakeActionable": false, + "canEndCourse": false, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -117,6 +120,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -151,6 +155,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -185,6 +190,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -219,6 +225,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -253,6 +260,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -287,6 +295,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -321,6 +330,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -658,6 +668,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -668,6 +679,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -678,6 +690,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.21a-course-overlap.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.21a-course-overlap.json.snap index 95ee616b8..643118922 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.21a-course-overlap.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.21a-course-overlap.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -575,6 +582,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -585,6 +593,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -595,6 +604,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.21b-course-window-required.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.21b-course-window-required.json.snap index 29c9db892..203769d88 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.21b-course-window-required.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.21b-course-window-required.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -575,6 +582,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -585,6 +593,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -595,6 +604,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.21c-course-end.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.21c-course-end.json.snap index 7d472d215..32048150f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.21c-course-end.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.21c-course-end.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -64,6 +65,7 @@ "id": "", "startsOn": "2026-08-01", "endsOn": "2026-08-31", + "endedAt": null, "status": "ENDED", "takenDoses": 0, "note": null @@ -72,6 +74,7 @@ "id": "", "startsOn": "2026-10-01", "endsOn": "2026-12-31", + "endedAt": null, "status": "CURRENT", "takenDoses": 0, "note": null @@ -85,6 +88,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -123,6 +127,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -157,6 +162,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -191,6 +197,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -225,6 +232,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -259,6 +267,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -293,6 +302,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -327,6 +337,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -664,6 +675,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -674,6 +686,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -684,6 +697,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.22-composition-units.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.22-composition-units.json.snap index 02ed0c383..093b55a10 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.22-composition-units.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.22-composition-units.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": 3, "stockDosesRemaining": 1, "runwayDays": 10, @@ -123,6 +125,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -157,6 +160,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -191,6 +195,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -225,6 +230,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -259,6 +265,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -293,6 +300,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -327,6 +335,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -664,6 +673,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -674,6 +684,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -684,6 +695,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.23a-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.23a-invalid.json.snap index 0967c7eaa..b7f9aba85 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.23a-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.23a-invalid.json.snap @@ -53,6 +53,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -87,6 +88,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -121,6 +123,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -155,6 +158,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -189,6 +193,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -223,6 +228,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -257,6 +263,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -594,6 +601,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -604,6 +612,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -614,6 +623,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.23b-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.23b-not-found.json.snap index 9848e8212..d587eb862 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.23b-not-found.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.23b-not-found.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -238,6 +244,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -575,6 +582,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -585,6 +593,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -595,6 +604,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.24a-writer.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.24a-writer.json.snap index 28c205ead..76f3545ba 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.24a-writer.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.24a-writer.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -175,6 +179,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -209,6 +214,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -243,6 +249,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -580,6 +587,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -590,6 +598,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -600,6 +609,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.24b-manager.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.24b-manager.json.snap index ee901103c..fc30cf253 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.24b-manager.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.24b-manager.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -95,6 +96,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -133,6 +135,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -167,6 +170,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -201,6 +205,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +240,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -269,6 +275,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -303,6 +310,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -337,6 +345,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -674,6 +683,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -684,6 +694,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -694,6 +705,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.24c-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.24c-anonymous.json.snap index 028da3263..5361c6039 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.24c-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.24c-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -207,6 +212,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -241,6 +247,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -578,6 +585,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -588,6 +596,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -598,6 +607,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.31-delete.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.31-delete.json.snap index 4459480a0..23dd277b8 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.31-delete.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.31-delete.json.snap @@ -43,6 +43,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -77,6 +78,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -111,6 +113,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -145,6 +148,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -179,6 +183,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -213,6 +218,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -530,6 +536,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -540,6 +547,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -550,6 +558,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.32-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.32-not-found.json.snap index 5f7a74b90..d5704e024 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.32-not-found.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.32-not-found.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -204,6 +209,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -521,6 +527,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -531,6 +538,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -541,6 +549,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.33a-manager.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.33a-manager.json.snap index 849675a62..21bb52672 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.33a-manager.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.33a-manager.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -175,6 +179,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -209,6 +214,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -526,6 +532,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -536,6 +543,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -546,6 +554,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.33b-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.33b-anonymous.json.snap index 29eef6fb4..2edeadaa2 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-detail.33b-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.33b-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -207,6 +212,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -524,6 +530,7 @@ "userId": "", "startsOn": "2026-08-01T00:00:00.000Z", "endsOn": "2026-08-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -534,6 +541,7 @@ "userId": "", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -544,6 +552,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.01-mixed.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.01-mixed.json.snap index 436ec3020..6a11c1d74 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.01-mixed.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.01-mixed.json.snap @@ -231,6 +231,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -265,6 +266,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -302,6 +304,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -336,6 +339,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.02-mixed-again.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.02-mixed-again.json.snap index 3cb7d5b8a..d75efdf72 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.02-mixed-again.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.02-mixed-again.json.snap @@ -219,6 +219,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -253,6 +254,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -290,6 +292,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -324,6 +327,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03a-background.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03a-background.json.snap index eebf4724a..20fed75d5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03a-background.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03a-background.json.snap @@ -90,6 +90,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -124,6 +125,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -161,6 +163,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -195,6 +198,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03b-backfill.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03b-backfill.json.snap index 628b2b978..0eabbaeb1 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03b-backfill.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03b-backfill.json.snap @@ -89,6 +89,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -123,6 +124,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -160,6 +162,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -194,6 +197,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04a-too-large.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04a-too-large.json.snap index 3b6ab3db5..2a194eb68 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04a-too-large.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04a-too-large.json.snap @@ -36,6 +36,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -70,6 +71,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04b-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04b-invalid.json.snap index 8d502393a..37645d0cf 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04b-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04b-invalid.json.snap @@ -54,6 +54,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -88,6 +89,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -125,6 +127,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -159,6 +162,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04c-apple-not-mirrored.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04c-apple-not-mirrored.json.snap index 1b576c2b2..c6d66efd3 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04c-apple-not-mirrored.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04c-apple-not-mirrored.json.snap @@ -36,6 +36,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -70,6 +71,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05a-native.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05a-native.json.snap index 7f14348a3..0f38ac32f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05a-native.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05a-native.json.snap @@ -92,6 +92,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -126,6 +127,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -163,6 +165,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -197,6 +200,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05b-narrow-token.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05b-narrow-token.json.snap index 8b38a8d26..14d3d3550 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05b-narrow-token.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05b-narrow-token.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -108,6 +110,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -142,6 +145,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05c-delegate.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05c-delegate.json.snap index e30efa5ac..0107f8c9a 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05c-delegate.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05c-delegate.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -110,6 +112,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -144,6 +147,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05d-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05d-anonymous.json.snap index 06466d200..aa8bddbc4 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05d-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05d-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -108,6 +110,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -142,6 +145,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.01-take-in-place.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.01-take-in-place.json.snap index 6ce397312..018ac0aed 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.01-take-in-place.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.01-take-in-place.json.snap @@ -71,6 +71,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -105,6 +106,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.02-skip-flip.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.02-skip-flip.json.snap index f264ae601..1ba3463cd 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.02-skip-flip.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.02-skip-flip.json.snap @@ -75,6 +75,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -109,6 +110,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.03-move-by-band.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.03-move-by-band.json.snap index 6136a16fc..dc2fbea2b 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.03-move-by-band.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.03-move-by-band.json.snap @@ -66,6 +66,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -100,6 +101,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.04-pin-free-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.04-pin-free-slot.json.snap index 554560af1..fc7baa67c 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.04-pin-free-slot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.04-pin-free-slot.json.snap @@ -66,6 +66,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -100,6 +101,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.05-pin-not-a-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.05-pin-not-a-slot.json.snap index c6e364cd5..7b077dba5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.05-pin-not-a-slot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.05-pin-not-a-slot.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.06-pin-occupied.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.06-pin-occupied.json.snap index 5e081753f..54240a673 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.06-pin-occupied.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.06-pin-occupied.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.07-unpin.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.07-unpin.json.snap index a0661d19b..7c0ec4913 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.07-unpin.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.07-unpin.json.snap @@ -66,6 +66,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -100,6 +101,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.08-explicit-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.08-explicit-slot.json.snap index 8af962b7e..605b48570 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.08-explicit-slot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.08-explicit-slot.json.snap @@ -66,6 +66,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -100,6 +101,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.09-before-start.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.09-before-start.json.snap index 84475f2d0..7251d4876 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.09-before-start.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.09-before-start.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.10-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.10-invalid.json.snap index e78d84965..2ede7d14c 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.10-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.10-invalid.json.snap @@ -55,6 +55,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -89,6 +90,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.11a-tombstoned.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.11a-tombstoned.json.snap index 7a2824812..4374fa8c5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.11a-tombstoned.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.11a-tombstoned.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.11b-other-medication.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.11b-other-medication.json.snap index 7a2824812..4374fa8c5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.11b-other-medication.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.11b-other-medication.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.12a-writer.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.12a-writer.json.snap index 5cc86ab63..83c820948 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.12a-writer.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.12a-writer.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.12b-manager.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.12b-manager.json.snap index d3b8934ca..f0912295a 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.12b-manager.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.12b-manager.json.snap @@ -66,6 +66,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -100,6 +101,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.13-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.13-anonymous.json.snap index a1e963e0d..3084379ef 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.13-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.13-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.21-delete.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.21-delete.json.snap index 4658173e8..d91c10a34 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.21-delete.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.21-delete.json.snap @@ -48,6 +48,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -82,6 +83,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.22-delete-one-shot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.22-delete-one-shot.json.snap index 7bc6ba40a..0d59213d6 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.22-delete-one-shot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.22-delete-one-shot.json.snap @@ -47,6 +47,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -81,6 +82,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.23-other-medication.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.23-other-medication.json.snap index c7950a1eb..d507d8089 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.23-other-medication.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.23-other-medication.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.24a-writer.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.24a-writer.json.snap index 9ae53c9b8..601f8ae80 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.24a-writer.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.24a-writer.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.24b-manager.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.24b-manager.json.snap index 32e1401fb..8d45ef6cb 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.24b-manager.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.24b-manager.json.snap @@ -48,6 +48,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -82,6 +83,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-event.25-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-event.25-anonymous.json.snap index 9d3d932eb..b053161b3 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-event.25-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-event.25-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.01-today.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.01-today.json.snap index 542dd90b5..ef3553289 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.01-today.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.01-today.json.snap @@ -85,6 +85,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -122,6 +123,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -156,6 +158,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -190,6 +193,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.02a-compliance-cold.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.02a-compliance-cold.json.snap index 6296a686c..dc985d803 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.02a-compliance-cold.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.02a-compliance-cold.json.snap @@ -73,6 +73,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -110,6 +111,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -144,6 +146,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -178,6 +181,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.02b-compliance-warm.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.02b-compliance-warm.json.snap index 20f1f535b..8e8e292b5 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.02b-compliance-warm.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.02b-compliance-warm.json.snap @@ -73,6 +73,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -110,6 +111,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -144,6 +146,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -178,6 +181,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.03-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.03-invalid.json.snap index 9de2f573c..3b0fbf4f0 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.03-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.03-invalid.json.snap @@ -53,6 +53,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -90,6 +91,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -124,6 +126,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -158,6 +161,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.04a-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.04a-reader.json.snap index 542dd90b5..ef3553289 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.04a-reader.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.04a-reader.json.snap @@ -85,6 +85,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -122,6 +123,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -156,6 +158,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -190,6 +193,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.04b-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.04b-anonymous.json.snap index 3016b5ab1..cae6c976b 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.04b-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.04b-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -74,6 +75,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -108,6 +110,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -142,6 +145,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.11-take-in-place.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.11-take-in-place.json.snap index 6001b05ef..99adee5e6 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.11-take-in-place.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.11-take-in-place.json.snap @@ -100,6 +100,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -137,6 +138,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -171,6 +173,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -205,6 +208,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.12-take-slot-move.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.12-take-slot-move.json.snap index 3fa6e304b..ed5baac0a 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.12-take-slot-move.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.12-take-slot-move.json.snap @@ -100,6 +100,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -137,6 +138,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -171,6 +173,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -205,6 +208,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.13a-pin.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.13a-pin.json.snap index 3b10a3bff..054ee2099 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.13a-pin.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.13a-pin.json.snap @@ -100,6 +100,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -137,6 +138,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -171,6 +173,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -205,6 +208,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.13b-pin-not-a-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.13b-pin-not-a-slot.json.snap index 11df637ed..6aa653941 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.13b-pin-not-a-slot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.13b-pin-not-a-slot.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -76,6 +77,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -110,6 +112,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -144,6 +147,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.14-skip.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.14-skip.json.snap index 522843f52..657ce643a 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.14-skip.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.14-skip.json.snap @@ -94,6 +94,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -131,6 +132,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -165,6 +167,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -199,6 +202,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.15a-snooze-until.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.15a-snooze-until.json.snap index 80c9de305..a308a9bbc 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.15a-snooze-until.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.15a-snooze-until.json.snap @@ -76,6 +76,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -113,6 +114,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -147,6 +149,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -181,6 +184,7 @@ "snoozedUntil": "2026-10-15T13:00:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.15b-snooze-default.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.15b-snooze-default.json.snap index 7ab938329..a7e84e0cc 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.15b-snooze-default.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.15b-snooze-default.json.snap @@ -76,6 +76,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -113,6 +114,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -147,6 +149,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -181,6 +184,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16a-unknown.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16a-unknown.json.snap index fba272828..6646f7cab 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16a-unknown.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16a-unknown.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16b-foreign.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16b-foreign.json.snap index fba272828..6646f7cab 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16b-foreign.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16b-foreign.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16c-tombstoned.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16c-tombstoned.json.snap index fba272828..6646f7cab 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16c-tombstoned.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16c-tombstoned.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.17a-record-only-open.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.17a-record-only-open.json.snap index 547106503..62c80aeb4 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.17a-record-only-open.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.17a-record-only-open.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.17b-record-only-correction.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.17b-record-only-correction.json.snap index e314edc22..934a769af 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.17b-record-only-correction.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.17b-record-only-correction.json.snap @@ -94,6 +94,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -131,6 +132,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -165,6 +167,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -199,6 +202,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.18a-site-not-allowed.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.18a-site-not-allowed.json.snap index df23d680d..c3e8a898d 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.18a-site-not-allowed.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.18a-site-not-allowed.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -76,6 +77,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -110,6 +112,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -144,6 +147,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.18b-site.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.18b-site.json.snap index 0579f7e56..ced1d3c2c 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.18b-site.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.18b-site.json.snap @@ -98,6 +98,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -135,6 +136,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -169,6 +171,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -203,6 +206,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.19-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.19-invalid.json.snap index d124ccc0f..fbc19c409 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.19-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.19-invalid.json.snap @@ -53,6 +53,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -90,6 +91,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -124,6 +126,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -158,6 +161,7 @@ "snoozedUntil": "2026-10-15T12:30:00.000Z", "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20a-writer-take.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20a-writer-take.json.snap index a7a6cb365..7aa4d03c3 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20a-writer-take.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20a-writer-take.json.snap @@ -99,6 +99,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +137,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -170,6 +172,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -204,6 +207,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20b-writer-snooze-refused.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20b-writer-snooze-refused.json.snap index 8dd33d85b..791a3e966 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20b-writer-snooze-refused.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20b-writer-snooze-refused.json.snap @@ -38,6 +38,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -75,6 +76,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -109,6 +111,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -143,6 +146,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20c-writer-correction-refused.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20c-writer-correction-refused.json.snap index 4083a8d7e..6aff596a7 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20c-writer-correction-refused.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20c-writer-correction-refused.json.snap @@ -38,6 +38,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -75,6 +76,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -109,6 +111,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -143,6 +146,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20d-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20d-reader.json.snap index 2f40671a2..bcef0c96f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20d-reader.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20d-reader.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -76,6 +77,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -110,6 +112,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -144,6 +147,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20e-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20e-anonymous.json.snap index 76f803a7e..a9a2c9851 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20e-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20e-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -74,6 +75,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -108,6 +110,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -142,6 +145,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.01-take-in-band.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.01-take-in-band.json.snap index beb3a4a7a..5bfe65d1a 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.01-take-in-band.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.01-take-in-band.json.snap @@ -101,6 +101,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -135,6 +136,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -169,6 +171,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -203,6 +206,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +241,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.02a-take-standalone.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.02a-take-standalone.json.snap index 5854deab4..87fd35d61 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.02a-take-standalone.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.02a-take-standalone.json.snap @@ -99,6 +99,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -133,6 +134,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -167,6 +169,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -201,6 +204,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +239,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.02b-take-standalone-repeat.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.02b-take-standalone-repeat.json.snap index 193515ce6..dd84287fe 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.02b-take-standalone-repeat.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.02b-take-standalone-repeat.json.snap @@ -50,6 +50,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -84,6 +85,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -118,6 +120,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -152,6 +155,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -186,6 +190,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.03a-skip-named-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.03a-skip-named-slot.json.snap index 3187012c4..df5287a0f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.03a-skip-named-slot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.03a-skip-named-slot.json.snap @@ -96,6 +96,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -130,6 +131,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -164,6 +166,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -198,6 +201,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -232,6 +236,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.03b-skip-slotless.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.03b-skip-slotless.json.snap index 4a8993e7b..121c49ebe 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.03b-skip-slotless.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.03b-skip-slotless.json.snap @@ -105,6 +105,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -139,6 +140,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -173,6 +175,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -207,6 +210,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -241,6 +245,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.03c-delegate-skip-standalone-refused.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.03c-delegate-skip-standalone-refused.json.snap index ed16d5992..2328084f0 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.03c-delegate-skip-standalone-refused.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.03c-delegate-skip-standalone-refused.json.snap @@ -38,6 +38,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -72,6 +73,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -174,6 +178,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.03d-owner-skip-standalone.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.03d-owner-skip-standalone.json.snap index b10338d3a..70490b749 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.03d-owner-skip-standalone.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.03d-owner-skip-standalone.json.snap @@ -96,6 +96,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -130,6 +131,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -164,6 +166,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -198,6 +201,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -232,6 +236,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.04a-pin.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.04a-pin.json.snap index 56d4d628a..d2cc36112 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.04a-pin.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.04a-pin.json.snap @@ -101,6 +101,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -135,6 +136,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -169,6 +171,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -203,6 +206,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +241,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.04b-pin-not-a-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.04b-pin-not-a-slot.json.snap index b9c5ed52a..e10c9693d 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.04b-pin-not-a-slot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.04b-pin-not-a-slot.json.snap @@ -38,6 +38,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -72,6 +73,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -174,6 +178,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.04c-pin-occupied.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.04c-pin-occupied.json.snap index 58bfa457d..32e10bb15 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.04c-pin-occupied.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.04c-pin-occupied.json.snap @@ -38,6 +38,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -72,6 +73,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -174,6 +178,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.05-named-backfill.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.05-named-backfill.json.snap index 00581cc79..0a3817108 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.05-named-backfill.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.05-named-backfill.json.snap @@ -101,6 +101,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -135,6 +136,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -169,6 +171,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -203,6 +206,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +241,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.06a-body-key.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.06a-body-key.json.snap index f97aba489..3374e4a7a 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.06a-body-key.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.06a-body-key.json.snap @@ -101,6 +101,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -135,6 +136,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -169,6 +171,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -203,6 +206,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +241,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.06b-body-key-replay.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.06b-body-key-replay.json.snap index 3f0b28077..6824257fe 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.06b-body-key-replay.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.06b-body-key-replay.json.snap @@ -55,6 +55,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -89,6 +90,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -123,6 +125,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -157,6 +160,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -191,6 +195,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.07a-header-key.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.07a-header-key.json.snap index 15d1bf5cc..f2645fa53 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.07a-header-key.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.07a-header-key.json.snap @@ -101,6 +101,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -135,6 +136,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -169,6 +171,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -203,6 +206,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +241,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.07b-header-key-replay.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.07b-header-key-replay.json.snap index a2bab3d76..897b3b218 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.07b-header-key-replay.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.07b-header-key-replay.json.snap @@ -55,6 +55,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -89,6 +90,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -123,6 +125,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -157,6 +160,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -191,6 +195,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.08-native.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.08-native.json.snap index b60ae002d..decbbe0d3 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.08-native.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.08-native.json.snap @@ -101,6 +101,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -135,6 +136,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -169,6 +171,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -203,6 +206,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +241,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.09a-delegate-skip-refused.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.09a-delegate-skip-refused.json.snap index 4943b7232..df4367392 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.09a-delegate-skip-refused.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.09a-delegate-skip-refused.json.snap @@ -38,6 +38,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -72,6 +73,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -174,6 +178,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.09b-owner-skip-flip.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.09b-owner-skip-flip.json.snap index 6794d89eb..0bdb15dc7 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.09b-owner-skip-flip.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.09b-owner-skip-flip.json.snap @@ -96,6 +96,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -130,6 +131,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -164,6 +166,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -198,6 +201,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -232,6 +236,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.10a-writer-take.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.10a-writer-take.json.snap index c4053484a..ca6facc1d 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.10a-writer-take.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.10a-writer-take.json.snap @@ -101,6 +101,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -135,6 +136,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -169,6 +171,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -203,6 +206,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -237,6 +241,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.10b-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.10b-reader.json.snap index ed6df220c..9491d583c 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.10b-reader.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.10b-reader.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -175,6 +179,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.10c-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.10c-anonymous.json.snap index 249b68b7d..6087c232b 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.10c-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.10c-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.11a-site-not-allowed.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.11a-site-not-allowed.json.snap index 717ee34c5..d9b7ac67b 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.11a-site-not-allowed.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.11a-site-not-allowed.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -175,6 +179,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.11b-site-and-dose.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.11b-site-and-dose.json.snap index 6ac1a2a5d..fdc78e6fd 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.11b-site-and-dose.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.11b-site-and-dose.json.snap @@ -99,6 +99,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -133,6 +134,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -167,6 +169,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -201,6 +204,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -235,6 +239,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.12-one-shot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.12-one-shot.json.snap index 034101fed..2cb5cf28f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.12-one-shot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.12-one-shot.json.snap @@ -98,6 +98,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -132,6 +133,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -166,6 +168,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -200,6 +203,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -234,6 +238,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.13-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.13-invalid.json.snap index aad4622ea..5a1c0e905 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.13-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.13-invalid.json.snap @@ -57,6 +57,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -91,6 +92,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -125,6 +127,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -159,6 +162,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -193,6 +197,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.14a-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.14a-not-found.json.snap index 8515c277e..7795a843f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.14a-not-found.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.14a-not-found.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.14b-record-only.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.14b-record-only.json.snap index 94711ec7a..a561d91dd 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.14b-record-only.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.14b-record-only.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.21-default.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.21-default.json.snap index 7f5fee32b..90f48bc02 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.21-default.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.21-default.json.snap @@ -284,6 +284,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -318,6 +319,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -352,6 +354,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -386,6 +389,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -420,6 +424,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.22-completed-page.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.22-completed-page.json.snap index 15c13c61b..954702e49 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.22-completed-page.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.22-completed-page.json.snap @@ -122,6 +122,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -156,6 +157,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -190,6 +192,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -224,6 +227,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -258,6 +262,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.23a-skipped.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.23a-skipped.json.snap index 75c78f503..c3e129027 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.23a-skipped.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.23a-skipped.json.snap @@ -128,6 +128,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -162,6 +163,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -196,6 +198,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -230,6 +233,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -264,6 +268,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.23b-taken.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.23b-taken.json.snap index ebf41b9e7..a99daab22 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.23b-taken.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.23b-taken.json.snap @@ -200,6 +200,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -234,6 +235,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -268,6 +270,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -302,6 +305,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -336,6 +340,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.24-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.24-invalid.json.snap index 01250074c..3f38a6124 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.24-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.24-invalid.json.snap @@ -54,6 +54,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -88,6 +89,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -122,6 +124,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -156,6 +159,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -190,6 +194,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.25a-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.25a-not-found.json.snap index 8515c277e..7795a843f 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.25a-not-found.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.25a-not-found.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -170,6 +174,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.25b-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.25b-reader.json.snap index 7f5fee32b..90f48bc02 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.25b-reader.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.25b-reader.json.snap @@ -284,6 +284,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -318,6 +319,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -352,6 +354,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -386,6 +389,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -420,6 +424,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.25c-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.25c-anonymous.json.snap index 31c8b1e6c..4940ea7b7 100644 --- a/tests/integration/routes/parity/__snapshots__/medication-intake.25c-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.25c-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-15T00:00:00.000Z", "endsOn": "2026-10-15T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medications.01-list.json.snap b/tests/integration/routes/parity/__snapshots__/medications.01-list.json.snap index f9dd28d2e..1511100dd 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.01-list.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.01-list.json.snap @@ -16,6 +16,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "nextDueScheduleId": "", "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null @@ -112,6 +114,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medications.02a-list-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medications.02a-list-reader.json.snap index 1a1069321..d6fe8ec45 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.02a-list-reader.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.02a-list-reader.json.snap @@ -16,6 +16,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "nextDueScheduleId": "", "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null @@ -112,6 +114,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medications.02b-list-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medications.02b-list-anonymous.json.snap index 45da3b863..6c6e62001 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.02b-list-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.02b-list-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medications.11-plain.json.snap b/tests/integration/routes/parity/__snapshots__/medications.11-plain.json.snap index 42f544ad5..99cf281ca 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.11-plain.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.11-plain.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medications.12-full.json.snap b/tests/integration/routes/parity/__snapshots__/medications.12-full.json.snap index 9a4f14354..296a57fc8 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.12-full.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.12-full.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -73,6 +74,7 @@ "id": "", "startsOn": "2026-10-10", "endsOn": "2026-12-31", + "endedAt": null, "status": "CURRENT", "takenDoses": 0, "note": null @@ -86,6 +88,7 @@ "nextDueOverdue": true, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -124,6 +127,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -158,6 +162,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -195,6 +200,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -293,6 +299,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.13a-as-needed.json.snap b/tests/integration/routes/parity/__snapshots__/medications.13a-as-needed.json.snap index f3a3616bd..5289efb77 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.13a-as-needed.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.13a-as-needed.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -46,6 +47,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -84,6 +86,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -118,6 +121,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -152,6 +156,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -189,6 +194,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -287,6 +293,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.13b-one-shot.json.snap b/tests/integration/routes/parity/__snapshots__/medications.13b-one-shot.json.snap index e90f7436b..626304105 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.13b-one-shot.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.13b-one-shot.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -64,6 +65,7 @@ "id": "", "startsOn": "2026-10-20", "endsOn": "2026-10-20", + "endedAt": null, "status": "UPCOMING", "takenDoses": 0, "note": null @@ -77,6 +79,7 @@ "nextDueOverdue": false, "courseStatus": "UPCOMING", "intakeActionable": false, + "canEndCourse": false, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -115,6 +118,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -149,6 +153,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -183,6 +188,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -220,6 +226,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -254,6 +261,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -372,6 +380,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -382,6 +391,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.13c-prn-and-legacy-days.json.snap b/tests/integration/routes/parity/__snapshots__/medications.13c-prn-and-legacy-days.json.snap index 70bc8174e..63fb80837 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.13c-prn-and-legacy-days.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.13c-prn-and-legacy-days.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -111,6 +112,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": false, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -149,6 +151,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -183,6 +186,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -217,6 +221,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -254,6 +259,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -288,6 +294,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -322,6 +329,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -500,6 +508,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -510,6 +519,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.13d-ended-window.json.snap b/tests/integration/routes/parity/__snapshots__/medications.13d-ended-window.json.snap index 6c2a8d479..7584b78fb 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.13d-ended-window.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.13d-ended-window.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -64,6 +65,7 @@ "id": "", "startsOn": "2026-10-05", "endsOn": "2026-10-05", + "endedAt": null, "status": "ENDED", "takenDoses": 0, "note": null @@ -77,6 +79,7 @@ "nextDueOverdue": false, "courseStatus": "ENDED", "intakeActionable": false, + "canEndCourse": false, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -115,6 +118,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -149,6 +153,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -183,6 +188,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -217,6 +223,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -254,6 +261,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -288,6 +296,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -322,6 +331,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -520,6 +530,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -530,6 +541,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -540,6 +552,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.14a-unknown-category.json.snap b/tests/integration/routes/parity/__snapshots__/medications.14a-unknown-category.json.snap index b55686124..da4e4314d 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.14a-unknown-category.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.14a-unknown-category.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -207,6 +212,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -241,6 +247,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -439,6 +446,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -449,6 +457,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -459,6 +468,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.14b-course-ends-before-start.json.snap b/tests/integration/routes/parity/__snapshots__/medications.14b-course-ends-before-start.json.snap index 5cc3ae9de..ba4dd30c7 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.14b-course-ends-before-start.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.14b-course-ends-before-start.json.snap @@ -43,6 +43,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -77,6 +78,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -111,6 +113,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -145,6 +148,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -182,6 +186,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -216,6 +221,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -250,6 +256,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -448,6 +455,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -458,6 +466,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -468,6 +477,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.14c-one-shot-two-schedules.json.snap b/tests/integration/routes/parity/__snapshots__/medications.14c-one-shot-two-schedules.json.snap index 688566769..e2885bb55 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.14c-one-shot-two-schedules.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.14c-one-shot-two-schedules.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -207,6 +212,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -241,6 +247,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -439,6 +446,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -449,6 +457,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -459,6 +468,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.14d-one-shot-recurrence.json.snap b/tests/integration/routes/parity/__snapshots__/medications.14d-one-shot-recurrence.json.snap index 85ecda70f..cc03c6261 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.14d-one-shot-recurrence.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.14d-one-shot-recurrence.json.snap @@ -34,6 +34,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -68,6 +69,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -102,6 +104,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -136,6 +139,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -207,6 +212,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -241,6 +247,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -439,6 +446,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -449,6 +457,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -459,6 +468,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.14e-unstable-concept-id.json.snap b/tests/integration/routes/parity/__snapshots__/medications.14e-unstable-concept-id.json.snap index a2b535824..6f801961c 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.14e-unstable-concept-id.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.14e-unstable-concept-id.json.snap @@ -49,6 +49,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -83,6 +84,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -117,6 +119,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -151,6 +154,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -188,6 +192,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -222,6 +227,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -256,6 +262,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -454,6 +461,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -464,6 +472,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -474,6 +483,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.14f-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medications.14f-invalid.json.snap index c0abd73eb..6f24feb0f 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.14f-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.14f-invalid.json.snap @@ -53,6 +53,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -87,6 +88,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -121,6 +123,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -155,6 +158,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -192,6 +196,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -226,6 +231,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -260,6 +266,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -458,6 +465,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -468,6 +476,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -478,6 +487,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.15a-mirror.json.snap b/tests/integration/routes/parity/__snapshots__/medications.15a-mirror.json.snap index 5b84cf296..6619f57b1 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.15a-mirror.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.15a-mirror.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -174,6 +178,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -208,6 +213,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -245,6 +251,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -279,6 +286,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -313,6 +321,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -347,6 +356,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -565,6 +575,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -575,6 +586,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -585,6 +597,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.15b-mirror-replay.json.snap b/tests/integration/routes/parity/__snapshots__/medications.15b-mirror-replay.json.snap index ca1c2c536..c811c4cd5 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.15b-mirror-replay.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.15b-mirror-replay.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -108,6 +110,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -142,6 +145,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -176,6 +180,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -210,6 +215,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -247,6 +253,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -281,6 +288,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -315,6 +323,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -349,6 +358,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -567,6 +577,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -577,6 +588,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -587,6 +599,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.16-mirror-cap.json.snap b/tests/integration/routes/parity/__snapshots__/medications.16-mirror-cap.json.snap index a7712a941..9f2877c5b 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.16-mirror-cap.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.16-mirror-cap.json.snap @@ -38,6 +38,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -72,6 +73,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -174,6 +178,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -208,6 +213,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -242,6 +248,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -276,6 +283,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -310,6 +318,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -344,6 +353,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -378,6 +388,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -412,6 +423,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -446,6 +458,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -480,6 +493,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -514,6 +528,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -548,6 +563,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -582,6 +598,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -616,6 +633,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -650,6 +668,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -684,6 +703,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -718,6 +738,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -752,6 +773,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -786,6 +808,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -820,6 +843,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -854,6 +878,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -888,6 +913,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -922,6 +948,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -956,6 +983,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -990,6 +1018,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -1024,6 +1053,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, diff --git a/tests/integration/routes/parity/__snapshots__/medications.17a-idempotent-first.json.snap b/tests/integration/routes/parity/__snapshots__/medications.17a-idempotent-first.json.snap index 075912d7f..ee499300f 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.17a-idempotent-first.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.17a-idempotent-first.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -174,6 +178,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -208,6 +213,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -245,6 +251,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -279,6 +286,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -313,6 +321,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -347,6 +356,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -381,6 +391,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -619,6 +630,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -629,6 +641,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -639,6 +652,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.17b-idempotent-replay.json.snap b/tests/integration/routes/parity/__snapshots__/medications.17b-idempotent-replay.json.snap index b2041d2c4..fa0acd8f8 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.17b-idempotent-replay.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.17b-idempotent-replay.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -175,6 +179,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -209,6 +214,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -246,6 +252,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -280,6 +287,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -314,6 +322,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -348,6 +357,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -382,6 +392,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -620,6 +631,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -630,6 +642,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -640,6 +653,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.18a-writer.json.snap b/tests/integration/routes/parity/__snapshots__/medications.18a-writer.json.snap index 524d5101f..55a10a069 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.18a-writer.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.18a-writer.json.snap @@ -15,6 +15,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -68,6 +69,7 @@ "nextDueOverdue": false, "courseStatus": "CURRENT", "intakeActionable": true, + "canEndCourse": true, "stockUnitsRemaining": null, "stockDosesRemaining": null, "runwayDays": null, @@ -106,6 +108,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -140,6 +143,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -174,6 +178,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -208,6 +213,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -242,6 +248,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -279,6 +286,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -313,6 +321,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -347,6 +356,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -381,6 +391,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -415,6 +426,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -673,6 +685,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -683,6 +696,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -693,6 +707,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.18b-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medications.18b-reader.json.snap index ee5305870..33f0d317f 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.18b-reader.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.18b-reader.json.snap @@ -39,6 +39,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -73,6 +74,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -107,6 +109,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -141,6 +144,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -175,6 +179,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -212,6 +217,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -246,6 +252,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -280,6 +287,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -314,6 +322,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -348,6 +357,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -606,6 +616,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -616,6 +627,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -626,6 +638,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" diff --git a/tests/integration/routes/parity/__snapshots__/medications.19-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medications.19-anonymous.json.snap index 5a4de87eb..5cfd95cf1 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.19-anonymous.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.19-anonymous.json.snap @@ -37,6 +37,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": true, "trackIntake": true, @@ -71,6 +72,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -105,6 +107,7 @@ "snoozedUntil": null, "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -139,6 +142,7 @@ "snoozedUntil": null, "startsOn": "2026-10-01T00:00:00.000Z", "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -173,6 +177,7 @@ "snoozedUntil": null, "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -210,6 +215,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -244,6 +250,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": false, @@ -278,6 +285,7 @@ "snoozedUntil": null, "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "oneShot": true, "asNeeded": false, "trackIntake": true, @@ -312,6 +320,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -346,6 +355,7 @@ "snoozedUntil": null, "startsOn": null, "endsOn": null, + "endedAt": null, "oneShot": false, "asNeeded": false, "trackIntake": true, @@ -604,6 +614,7 @@ "userId": "", "startsOn": "2026-10-05T00:00:00.000Z", "endsOn": "2026-10-05T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -614,6 +625,7 @@ "userId": "", "startsOn": "2026-10-10T00:00:00.000Z", "endsOn": "2026-12-31T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" @@ -624,6 +636,7 @@ "userId": "", "startsOn": "2026-10-20T00:00:00.000Z", "endsOn": "2026-10-20T00:00:00.000Z", + "endedAt": null, "noteEncrypted": null, "createdAt": "", "updatedAt": "" From ffe41941f1d7be165fd23af419bb20fa815b8648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 24 Oct 2026 22:39:19 +0200 Subject: [PATCH 11/11] test(contract): seed a deleted document so Recently deleted answers with a row The GET contract run requires every list operation to answer at least once with a row, and the new list of deleted documents had nothing to list. The seed now ingests a second document and tombstones it the way the delete route does. --- tests/integration/contract-seed.ts | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/integration/contract-seed.ts b/tests/integration/contract-seed.ts index a08484fd2..3ca47c3be 100644 --- a/tests/integration/contract-seed.ts +++ b/tests/integration/contract-seed.ts @@ -708,6 +708,37 @@ export async function seedContractAccount( }); if (ingested.kind === "stored") ids.document = ingested.document.id; else failures.push(`document: ingest answered ${ingested.kind}`); + // A second document, deleted the way the delete route tombstones one, so + // Recently deleted answers with a row. + const deleted = await ingestDocument({ + userId: CONTRACT_USER_ID, + scoped: false, + ipAddress: null, + bytes: Buffer.concat([pdf, Buffer.from("% deleted\n")]), + filename: "deleted.pdf", + title: "Deleted letter", + kind: null, + documentDate: dayKey(daysAgo(12)), + episodeIds: [], + encounterIds: [], + sourceSystem: null, + sourceId: null, + sourceInstance: null, + aiDeferred: true, + sourceKeyChecked: false, + limits: { maxFileBytes: 10 * 1024 * 1024, quotaBytes: 100 * 1024 * 1024 }, + documentAi: async () => ({ + available: false, + reason: "no_provider", + onDeviceAllowed: false, + }), + }); + if (deleted.kind === "stored") { + await prisma.inboundDocument.update({ + where: { id: deleted.document.id }, + data: { deletedAt: daysAgo(2) }, + }); + } else failures.push(`deleted document: ingest answered ${deleted.kind}`); // A chat about that document, as the document sheet starts one. if (ids.document) { const documentChat = await createConversation({