diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 3933181fb..ee9bd53e2 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -6585,6 +6585,13 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationRequest" responses: + "200": + description: "A mirror create re-posting an `(externalSource, externalId)` pair the caller already holds: the existing + medication, nothing written." + content: + application/json: + schema: + $ref: "#/components/schemas/CreateMedicationResponse" "201": description: Created medication with its schedules. content: diff --git a/messages/de.json b/messages/de.json index ae2bbef23..52c9e2293 100644 --- a/messages/de.json +++ b/messages/de.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} wollte eine Einnahme ändern und wurde abgewiesen", "rejectedDoseImport": "{name} wollte Einnahmen importieren und wurde abgewiesen", "rejectedTrackedValue": "{name} wollte einen eigenen Messwert eintragen und wurde abgewiesen", + "rejectedTrackedValueTypeCreate": "{name} wollte eine eigene Messgröße anlegen und wurde abgewiesen", + "rejectedTrackedValueTypeUpdate": "{name} wollte eine eigene Messgröße ändern und wurde abgewiesen", + "rejectedMedicationLayout": "{name} wollte die Medikamentenliste neu anordnen und wurde abgewiesen", "deniedDoseImport": "Ein von {name} gestarteter Einnahme-Import konnte nicht eingereiht werden", "anamnesisFactCreate": "{name} hat einen Fakt zur Vorgeschichte eingetragen", "customMetricCreate": "{name} hat eine eigene Messgröße angelegt", diff --git a/messages/en.json b/messages/en.json index 0c7e01d4f..00826c3db 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} tried to change a dose and was refused", "rejectedDoseImport": "{name} tried to import doses and was refused", "rejectedTrackedValue": "{name} tried to log a tracked value and was refused", + "rejectedTrackedValueTypeCreate": "{name} tried to add a tracked value type and was refused", + "rejectedTrackedValueTypeUpdate": "{name} tried to change a tracked value type and was refused", + "rejectedMedicationLayout": "{name} tried to rearrange the medication list and was refused", "deniedDoseImport": "A dose import {name} started could not be queued", "anamnesisFactCreate": "{name} added a health background fact", "customMetricCreate": "{name} added a tracked value type", diff --git a/messages/es.json b/messages/es.json index c0eb52e19..ecc6d0e7c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} intentó cambiar una dosis y fue rechazado", "rejectedDoseImport": "{name} intentó importar dosis y fue rechazado", "rejectedTrackedValue": "{name} intentó registrar un valor propio y fue rechazado", + "rejectedTrackedValueTypeCreate": "{name} intentó añadir un tipo de valor propio y fue rechazado", + "rejectedTrackedValueTypeUpdate": "{name} intentó cambiar un tipo de valor propio y fue rechazado", + "rejectedMedicationLayout": "{name} intentó reordenar la lista de medicamentos y fue rechazado", "deniedDoseImport": "Una importación de dosis iniciada por {name} no se pudo poner en cola", "anamnesisFactCreate": "{name} añadió un dato de antecedentes de salud", "customMetricCreate": "{name} añadió un tipo de valor propio", diff --git a/messages/fr.json b/messages/fr.json index 1ae591455..69fbcdfb8 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} a tenté de modifier une prise et a été refusé", "rejectedDoseImport": "{name} a tenté d'importer des prises et a été refusé", "rejectedTrackedValue": "{name} a tenté d'enregistrer une valeur personnalisée et a été refusé", + "rejectedTrackedValueTypeCreate": "{name} a tenté d'ajouter un type de valeur personnalisée et a été refusé", + "rejectedTrackedValueTypeUpdate": "{name} a tenté de modifier un type de valeur personnalisée et a été refusé", + "rejectedMedicationLayout": "{name} a tenté de réorganiser la liste des médicaments et a été refusé", "deniedDoseImport": "Un import de prises lancé par {name} n'a pas pu être mis en file", "anamnesisFactCreate": "{name} a ajouté un élément d'antécédents de santé", "customMetricCreate": "{name} a ajouté un type de valeur personnalisée", diff --git a/messages/it.json b/messages/it.json index 9e30bf5ca..211211b5a 100644 --- a/messages/it.json +++ b/messages/it.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} ha provato a modificare una dose ed è stato rifiutato", "rejectedDoseImport": "{name} ha provato a importare delle dosi ed è stato rifiutato", "rejectedTrackedValue": "{name} ha provato a registrare un valore personalizzato ed è stato rifiutato", + "rejectedTrackedValueTypeCreate": "{name} ha provato ad aggiungere un tipo di valore personalizzato ed è stato rifiutato", + "rejectedTrackedValueTypeUpdate": "{name} ha provato a modificare un tipo di valore personalizzato ed è stato rifiutato", + "rejectedMedicationLayout": "{name} ha provato a riordinare l'elenco dei farmaci ed è stato rifiutato", "deniedDoseImport": "Un'importazione di dosi avviata da {name} non è stata messa in coda", "anamnesisFactCreate": "{name} ha aggiunto un dato di anamnesi", "customMetricCreate": "{name} ha aggiunto un tipo di valore personalizzato", diff --git a/messages/ko.json b/messages/ko.json index 0197d663d..5069c1d45 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} 님이 복용 기록을 수정하려다 거절됐어요", "rejectedDoseImport": "{name} 님이 복용 기록을 불러오려다 거절됐어요", "rejectedTrackedValue": "{name} 님이 추적 값을 기록하려다 거절됐어요", + "rejectedTrackedValueTypeCreate": "{name} 님이 추적 값 종류를 추가하려다 거절됐어요", + "rejectedTrackedValueTypeUpdate": "{name} 님이 추적 값 종류를 수정하려다 거절됐어요", + "rejectedMedicationLayout": "{name} 님이 약 목록 순서를 바꾸려다 거절됐어요", "deniedDoseImport": "{name} 님이 시작한 복용 기록 불러오기를 대기열에 넣지 못했어요", "anamnesisFactCreate": "{name} 님이 건강 배경 항목을 추가했어요", "customMetricCreate": "{name} 님이 추적 값 종류를 추가했어요", diff --git a/messages/nl.json b/messages/nl.json index 86a5440ec..f091c3c1d 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} probeerde een dosis te wijzigen, maar dat werd geweigerd", "rejectedDoseImport": "{name} probeerde doses te importeren, maar dat werd geweigerd", "rejectedTrackedValue": "{name} probeerde een bijgehouden waarde vast te leggen, maar dat werd geweigerd", + "rejectedTrackedValueTypeCreate": "{name} probeerde een type bijgehouden waarde toe te voegen, maar dat werd geweigerd", + "rejectedTrackedValueTypeUpdate": "{name} probeerde een type bijgehouden waarde te wijzigen, maar dat werd geweigerd", + "rejectedMedicationLayout": "{name} probeerde de medicijnlijst anders te ordenen, maar dat werd geweigerd", "deniedDoseImport": "Een dosisimport die {name} had gestart, kon niet in de wachtrij worden gezet", "anamnesisFactCreate": "{name} heeft een gegeven aan de gezondheidsachtergrond toegevoegd", "customMetricCreate": "{name} heeft een type bijgehouden waarde toegevoegd", diff --git a/messages/pl.json b/messages/pl.json index 916c2239a..947424424 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} próbował zmienić przyjęcie dawki i został odrzucony", "rejectedDoseImport": "{name} próbował zaimportować przyjęcia dawek i został odrzucony", "rejectedTrackedValue": "{name} próbował zapisać własny pomiar i został odrzucony", + "rejectedTrackedValueTypeCreate": "{name} próbował dodać własny typ pomiaru i został odrzucony", + "rejectedTrackedValueTypeUpdate": "{name} próbował zmienić własny typ pomiaru i został odrzucony", + "rejectedMedicationLayout": "{name} próbował zmienić kolejność listy leków i został odrzucony", "deniedDoseImport": "Import przyjęć rozpoczęty przez {name} nie mógł zostać zakolejkowany", "anamnesisFactCreate": "{name} dodał fakt z wywiadu zdrowotnego", "customMetricCreate": "{name} dodał własny typ pomiaru", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index fcd90cced..277297c97 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -12719,6 +12719,9 @@ "rejectedDoseUpdate": "{name} tentou alterar uma dose e foi recusado", "rejectedDoseImport": "{name} tentou importar doses e foi recusado", "rejectedTrackedValue": "{name} tentou registrar um valor acompanhado e foi recusado", + "rejectedTrackedValueTypeCreate": "{name} tentou adicionar um tipo de valor acompanhado e foi recusado", + "rejectedTrackedValueTypeUpdate": "{name} tentou alterar um tipo de valor acompanhado e foi recusado", + "rejectedMedicationLayout": "{name} tentou reorganizar a lista de medicamentos e foi recusado", "deniedDoseImport": "Uma importação de doses que {name} iniciou não pôde ser enfileirada", "anamnesisFactCreate": "{name} adicionou um fato do histórico de saúde", "customMetricCreate": "{name} adicionou um tipo de valor acompanhado", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87e3bc627..88a0075bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ overrides: proxy-addr@<2.0.8: ^2.0.8 mysql2@<3.22.0: ^3.22.0 minimatch@<3.1.4: ^3.1.4 - ip-address@<10.7.1: ^10.7.1 + ip-address@<10.7.4: ^10.7.4 sharp@<0.35.0: ^0.35.3 pdfjs-dist@>=5.6.83 <6.2.108: 6.2.108 deepmerge-ts@<8.0.0: ^8.0.0 @@ -4891,12 +4891,8 @@ packages: iobuffer@5.4.0: resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} - ip-address@10.7.2: - resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==} - engines: {node: '>= 12'} - - ip-address@10.7.3: - resolution: {integrity: sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==} + ip-address@10.7.4: + resolution: {integrity: sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -11875,7 +11871,7 @@ snapshots: dependencies: debug: 4.4.3(supports-color@7.2.0) express: 5.2.1(supports-color@7.2.0) - ip-address: 10.7.2 + ip-address: 10.7.4 transitivePeerDependencies: - supports-color @@ -12285,9 +12281,7 @@ snapshots: iobuffer@5.4.0: {} - ip-address@10.7.2: {} - - ip-address@10.7.3: {} + ip-address@10.7.4: {} ipaddr.js@1.9.1: {} @@ -14024,7 +14018,7 @@ snapshots: socks@2.8.10: dependencies: - ip-address: 10.7.3 + ip-address: 10.7.4 smart-buffer: 4.2.0 sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2f95c35f1..b70efd59f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -65,8 +65,9 @@ overrides: # 10.0.0.1 rather than being rejected. That is the exact alt-notation class # `isPublicUrl` exists to catch, and this copy arrives under the MCP SDK's # rate limiter where nothing of ours screens it. - # Later advisories up to 10.7.0 raised the floor from 10.3.1 to 10.7.1. - "ip-address@<10.7.1": "^10.7.1" + # Later advisories raised the floor from 10.3.1 to 10.7.1, then to 10.7.4 + # (a ReDoS in the Address6 zone identifier, through 10.7.3). + "ip-address@<10.7.4": "^10.7.4" "sharp@<0.35.0": "^0.35.3" # A crafted PDF could run arbitrary JavaScript through the renderer. Our own # copy is pinned past it in `devDependencies`, but a second copy arrives diff --git a/src/__tests__/audit-write-path-guard.test.ts b/src/__tests__/audit-write-path-guard.test.ts new file mode 100644 index 000000000..32fda84fc --- /dev/null +++ b/src/__tests__/audit-write-path-guard.test.ts @@ -0,0 +1,63 @@ +/** + * Structural guard: audit rows are written through `auditLog()`. + * + * `auditLog` decides two things a bare `prisma.auditLog.create` cannot: who + * acted when the row is not theirs (the delegate on a shared record), and + * whether a refused attempt (`.validation-failed`) becomes a row + * at all. An owner's own refused request stays on the wide event; only a + * refusal somebody else caused reaches the ledger the owner's activity feed + * reads. Twelve routes wrote their refusal row directly and filed every + * malformed body an owner's app sent, a few per sync, with no actor on a + * delegate's. + * + * Whitespace-tolerant: the calls were written across two lines + * (`prisma.auditLog` then `.create(`). + */ +import { describe, expect, it } from "vitest"; + +import { readSourceFolder, stripComments } from "./helpers/source-files"; + +/** Files that write the table directly, and why. */ +const DIRECT_WRITERS: Record = { + "lib/auth/audit.ts": "the one writer", + "lib/insights/briefing-failure-marker.ts": + "a dated failure marker the briefing read looks up again, not an audit of an act", +}; + +const RAW_WRITE = /\bauditLog\s*\.\s*create(?:Many(?:AndReturn)?)?\s*\(/; + +describe("audit rows go through auditLog()", () => { + const files = readSourceFolder("src", { + floor: 3000, + include: (rel) => + !rel.startsWith("generated/") && + !rel.includes("__tests__/") && + !/\.test\.tsx?$/.test(rel), + }); + const writers = files + .filter(([, text]) => RAW_WRITE.test(stripComments(text))) + .map(([rel]) => rel); + + it("finds the writers it polices", () => { + expect(files.length).toBeGreaterThan(3000); + expect(writers.length).toBeGreaterThanOrEqual(2); + expect(writers).toContain("lib/auth/audit.ts"); + }); + + it("no other file writes the audit table directly", () => { + expect(writers.filter((rel) => !(rel in DIRECT_WRITERS))).toEqual([]); + }); + + it("lists no file that stopped writing it", () => { + expect( + Object.keys(DIRECT_WRITERS).filter((rel) => !writers.includes(rel)), + ).toEqual([]); + }); + + it("the matcher reads a call split across lines", () => { + expect(RAW_WRITE.test("prisma.auditLog\n .create({ data })")).toBe( + true, + ); + expect(RAW_WRITE.test('auditLog("x.validation-failed", {})')).toBe(false); + }); +}); diff --git a/src/__tests__/background-task-settle-guard.test.ts b/src/__tests__/background-task-settle-guard.test.ts index 322c7dd6f..e6b564813 100644 --- a/src/__tests__/background-task-settle-guard.test.ts +++ b/src/__tests__/background-task-settle-guard.test.ts @@ -145,9 +145,10 @@ describe("background-task settle contract", () => { it("registers what fireAndForget starts", async () => { let release: () => void = () => {}; fireAndForget( - new Promise((resolve) => { - release = resolve; - }), + () => + new Promise((resolve) => { + release = resolve; + }), { action: "guard.background.registered" }, ); expect(pendingBackgroundTaskCount()).toBe(1); @@ -161,17 +162,19 @@ describe("background-task settle contract", () => { let innerDone = false; fireAndForget( - Promise.resolve().then(() => { - fireAndForget( - new Promise((resolve) => - setTimeout(() => { - innerDone = true; - resolve(); - }, 10), - ), - { action: "guard.background.cascade.inner" }, - ); - }), + () => + Promise.resolve().then(() => { + fireAndForget( + () => + new Promise((resolve) => + setTimeout(() => { + innerDone = true; + resolve(); + }, 10), + ), + { action: "guard.background.cascade.inner" }, + ); + }), { action: "guard.background.cascade.outer" }, ); diff --git a/src/__tests__/baselines/source-file-lines.json b/src/__tests__/baselines/source-file-lines.json index 798eba59d..434124a03 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": 91, - "total": 101359, + "total": 101342, "entries": { "app/medications/page-client.tsx": 839, "components/charts/mood-chart.tsx": 1166, @@ -38,7 +38,7 @@ "lib/analytics/sleep-night.ts": 1345, "lib/analytics/summaries-slice.ts": 1248, "lib/auth/session.ts": 811, - "lib/cache/server-cache.ts": 851, + "lib/cache/server-cache.ts": 848, "lib/cycle/prediction.ts": 883, "lib/daily/digest.ts": 1317, "lib/daily/load-digest.ts": 1000, @@ -82,7 +82,7 @@ "lib/measurements/import-apple-health-export.ts": 1722, "lib/medications/scheduling/cadence.ts": 978, "lib/medications/scheduling/recurrence.ts": 1108, - "lib/medications/scheduling/slot-upsert.ts": 926, + "lib/medications/scheduling/slot-upsert.ts": 912, "lib/notifications/senders/apns.ts": 1100, "lib/oura/sync.ts": 900, "lib/rollups/measurement-rollups.ts": 1496, diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index d7c1b5530..4dcbcf91c 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": 355, - "total": 114858, + "total": 114773, "entries": { "app/achievements/page.tsx :: AchievementsPage": 206, "app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314, @@ -15,16 +15,16 @@ "app/api/cycle/calendar/route.ts :: apiHandler(arg)": 230, "app/api/cycle/day-logs/bulk/route.ts :: postBulk": 213, "app/api/cycle/insights/route.ts :: apiHandler(arg)": 226, - "app/api/devices/route.ts :: apiHandler(arg)": 231, + "app/api/devices/route.ts :: apiHandler(arg)": 226, "app/api/documents/inbound/bulk/route.ts :: apiHandler(arg)": 207, "app/api/encounters/[id]/route.ts :: apiHandler(arg) #2": 269, "app/api/environment/route.ts :: apiHandler(arg)": 201, "app/api/export/health-record/route.ts :: apiHandler(arg)": 241, - "app/api/ingest/medication/route.ts :: apiHandler(arg)": 267, + "app/api/ingest/medication/route.ts :: apiHandler(arg)": 262, "app/api/insights/cards/route.ts :: apiHandler(arg)": 211, "app/api/integrations/status/route.ts :: apiHandler(arg)": 451, "app/api/labs/[id]/route.ts :: apiHandler(arg) #2": 239, - "app/api/labs/ocr/commit/route.ts :: commitOcrRows": 285, + "app/api/labs/ocr/commit/route.ts :: commitOcrRows": 280, "app/api/labs/route.ts :: postLabResult": 242, "app/api/mcp/oauth/token/route.ts :: POST": 250, "app/api/mcp/oauth/token/route.ts :: POST > withBackgroundEvent(arg)": 248, @@ -346,7 +346,7 @@ "lib/notifications/senders/web-push.ts :: sendViaWebPush": 268, "lib/personal-records/pr-detection-worker.ts :: detectPersonalRecordsForUser": 239, "lib/personal-records/pr-direction.ts :: getPRDirection": 202, - "lib/record-activity/activity-verb.ts :: recordActivityVerbLine": 395, + "lib/record-activity/activity-verb.ts :: recordActivityVerbLine": 325, "lib/strava/sync.ts :: runStravaSync": 219, "lib/support/report.ts :: collectSupportReportFacts": 332, "lib/targets/build-response.ts :: buildTargetsResponse": 330, diff --git a/src/__tests__/detached-work-guard.test.ts b/src/__tests__/detached-work-guard.test.ts new file mode 100644 index 000000000..8f3992048 --- /dev/null +++ b/src/__tests__/detached-work-guard.test.ts @@ -0,0 +1,93 @@ +/** + * Structural guard: work a request starts and does not wait for is started + * through `runDetached`, so it cannot rename the request's wide event. + * + * Detached work runs inside the request's context. Before `runDetached`, an + * `annotate({ action })` from it landed on the request's event whenever it + * ran before the event was emitted: the measurement tail's status re-warm + * logged a hand-entered reading as `insights.status.invalidate.skipped` on + * some requests and as `measurement.create` on others. + * + * Two shapes start detached work. `fireAndForget` takes a function and + * starts it detached itself (its signature makes a running promise a type + * error). `trackBackgroundTask` registers a handle, so every call outside + * the logging module has to build that handle with `runDetached`, unless the + * file is listed below with the reason no request can be its caller. + */ +import { describe, expect, it } from "vitest"; + +import { readSourceFolder, stripComments } from "./helpers/source-files"; + +/** Files whose detached work never starts inside a request, and why. */ +const NO_REQUEST_CALLER: Record = { + "lib/db-session.ts": + "the pool's connect hook pins the session time zone; it annotates nothing", + "lib/pwa/query-persister.ts": + "runs in the browser, where there is no wide event (and no async_hooks)", + "lib/jobs/reminder/climb-backfill-at-boot.ts": + "started once at worker boot, outside any request", +}; + +/** The argument text of every `trackBackgroundTask(...)` call in `code`. */ +export function trackedHandles(code: string): string[] { + const out: string[] = []; + for (const m of code.matchAll(/\btrackBackgroundTask\s*\(/g)) { + let i = m.index + m[0].length; + let depth = 1; + while (depth > 0 && i < code.length) { + if (code[i] === "(") depth += 1; + else if (code[i] === ")") depth -= 1; + i += 1; + } + out.push(code.slice(m.index + m[0].length, i - 1)); + } + return out; +} + +const startsDetached = (handle: string) => /^\s*runDetached\s*\(/.test(handle); + +describe("detached work cannot rename the request's event", () => { + const files = readSourceFolder("src", { + floor: 3000, + include: (rel) => + !rel.startsWith("generated/") && + !rel.startsWith("lib/logging/") && + !rel.includes("__tests__/") && + !/\.test\.tsx?$/.test(rel), + }); + const calls = files.flatMap(([rel, text]) => + trackedHandles(stripComments(text)).map((handle) => ({ rel, handle })), + ); + + it("finds the detached starts it polices", () => { + expect(calls.length).toBeGreaterThanOrEqual(8); + expect(calls.map((c) => c.rel)).toContain( + "lib/rollups/after-measurement-mutation.ts", + ); + }); + + it("starts every request-path handle with runDetached", () => { + const offenders = calls + .filter((c) => !(c.rel in NO_REQUEST_CALLER)) + .filter((c) => !startsDetached(c.handle)) + .map((c) => `${c.rel}: ${c.handle.replace(/\s+/g, " ").slice(0, 80)}`); + expect(offenders).toEqual([]); + }); + + it("lists no file that stopped starting detached work", () => { + const callers = new Set(calls.map((c) => c.rel)); + expect( + Object.keys(NO_REQUEST_CALLER).filter((rel) => !callers.has(rel)), + ).toEqual([]); + }); + + it("the matcher reads a nested handle and tells the two shapes apart", () => { + const [plain, detached] = trackedHandles( + "trackBackgroundTask(work(a, b(c)).catch(() => {}));\n" + + "trackBackgroundTask(runDetached(() => work()).catch(() => {}));", + ); + expect(plain).toBe("work(a, b(c)).catch(() => {})"); + expect(startsDetached(plain)).toBe(false); + expect(startsDetached(detached)).toBe(true); + }); +}); diff --git a/src/__tests__/medication-intake-tracking-guard.test.ts b/src/__tests__/medication-intake-tracking-guard.test.ts index 0558a4887..d84b41aae 100644 --- a/src/__tests__/medication-intake-tracking-guard.test.ts +++ b/src/__tests__/medication-intake-tracking-guard.test.ts @@ -115,10 +115,17 @@ const MAY_READ_EVERY_ROW: Record = { "units consumed by a recorded dose", "lib/medications/intake-slot-dedup.ts": "folds duplicate rows of doses already recorded", + // The medication create and edit write the schedule rows and answer + // through `medicationDetail` (read-medication.ts), which shapes them with + // scheduleWireFields and derives the due state the switch allows. + "lib/medications/create-medication.ts": + "writes the schedules; answers through medicationDetail", + "lib/medications/update-medication/update-medication.ts": + "writes the schedules; answers through medicationDetail", // The medication edit: the schedule count and the primary schedule's id // for the reminder-window bridge, and the replaced rows archived as an era // whose payload is empty for a stretch with tracking off. Nothing due comes - // out of either; the edit answers through scheduleWireFields. + // out of either; the edit answers through medicationDetail. "lib/medications/update-medication/preflight.ts": "schedule count and the primary schedule's id, derives nothing due", "lib/medications/update-medication/write.ts": diff --git a/src/app/api/admin/provider-health/route.ts b/src/app/api/admin/provider-health/route.ts index 38a761efe..341164e1c 100644 --- a/src/app/api/admin/provider-health/route.ts +++ b/src/app/api/admin/provider-health/route.ts @@ -10,7 +10,7 @@ import { type ModelListing, } from "@/lib/ai/model-availability"; import { decrypt } from "@/lib/crypto"; -import { isAnthropicBaseUrl } from "@/lib/ai/provider"; +import { isAnthropicBaseUrl } from "@/lib/ai/anthropic-host"; import { isCanonicalOpenAIEndpoint } from "@/lib/ai/openai-capabilities"; import { loadAiUsageSettings } from "@/lib/ai/capabilities/usage"; import { diff --git a/src/app/api/consent/ai/route.ts b/src/app/api/consent/ai/route.ts index 86421e8ca..13d49882f 100644 --- a/src/app/api/consent/ai/route.ts +++ b/src/app/api/consent/ai/route.ts @@ -74,10 +74,11 @@ export const POST = apiHandler(async (request: NextRequest) => { // is rebuilt from a backup. Fire-and-forget; audit failures must // never bubble into a user-facing 500. fireAndForget( - auditLog("consent.ai.grant", { - userId: user.id, - details: { kind, receiptId: receipt.id }, - }), + () => + auditLog("consent.ai.grant", { + userId: user.id, + details: { kind, receiptId: receipt.id }, + }), { action: "consent.ai.audit" }, ); diff --git a/src/app/api/custom-metrics/[id]/entries/[entryId]/route.ts b/src/app/api/custom-metrics/[id]/entries/[entryId]/route.ts index 2f1d171fe..cd30b6315 100644 --- a/src/app/api/custom-metrics/[id]/entries/[entryId]/route.ts +++ b/src/app/api/custom-metrics/[id]/entries/[entryId]/route.ts @@ -57,17 +57,12 @@ export const PATCH = apiHandler( const auditIssues = sanitiseZodIssues(error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "custom-metric.entry.update.validation-failed", - details: JSON.stringify({ issues: auditIssues, entryId }), - }, - }) - .catch(() => { - /* swallow — the 422 response is the contract */ - }); + auditLog("custom-metric.entry.update.validation-failed", { + userId: user.id, + details: { issues: auditIssues, entryId }, + }).catch(() => { + /* swallow — the 422 response is the contract */ + }); }, }); if (!parsed.ok) return parsed.error; diff --git a/src/app/api/custom-metrics/[id]/route.ts b/src/app/api/custom-metrics/[id]/route.ts index 3a2ec7842..2b149bd4d 100644 --- a/src/app/api/custom-metrics/[id]/route.ts +++ b/src/app/api/custom-metrics/[id]/route.ts @@ -89,20 +89,15 @@ export const PATCH = apiHandler( const auditIssues = sanitiseZodIssues(error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "custom-metric.metric.update.validation-failed", - details: JSON.stringify({ - issues: auditIssues, - customMetricId: id, - }), - }, - }) - .catch(() => { - /* swallow — the 422 response is the contract */ - }); + auditLog("custom-metric.metric.update.validation-failed", { + userId: user.id, + details: { + issues: auditIssues, + customMetricId: id, + }, + }).catch(() => { + /* swallow — the 422 response is the contract */ + }); }, }); if (!parsed.ok) return parsed.error; diff --git a/src/app/api/custom-metrics/__tests__/route.test.ts b/src/app/api/custom-metrics/__tests__/route.test.ts index 57a10098b..50ee33ccf 100644 --- a/src/app/api/custom-metrics/__tests__/route.test.ts +++ b/src/app/api/custom-metrics/__tests__/route.test.ts @@ -42,6 +42,7 @@ vi.mock("next/headers", () => ({ import { GET, POST } from "../route"; import { getSession } from "@/lib/auth/session"; import { prisma } from "@/lib/db"; +import { auditLog } from "@/lib/auth/audit"; const SESSION_OK = { session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, @@ -73,6 +74,7 @@ const ROW = { beforeEach(() => { vi.resetAllMocks(); + vi.mocked(auditLog).mockResolvedValue(undefined); vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never); }); diff --git a/src/app/api/custom-metrics/route.ts b/src/app/api/custom-metrics/route.ts index 74bf526ba..dfc9aa12e 100644 --- a/src/app/api/custom-metrics/route.ts +++ b/src/app/api/custom-metrics/route.ts @@ -70,17 +70,12 @@ async function postCustomMetric(request: NextRequest) { const auditIssues = sanitiseZodIssues(error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "custom-metric.metric.create.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => { - /* swallow — the 422 response is the contract */ - }); + auditLog("custom-metric.metric.create.validation-failed", { + userId: user.id, + details: { issues: auditIssues }, + }).catch(() => { + /* swallow — the 422 response is the contract */ + }); }, }); if (!parsed.ok) return parsed.error; diff --git a/src/app/api/dashboard/chart-overlay-prefs/__tests__/route.test.ts b/src/app/api/dashboard/chart-overlay-prefs/__tests__/route.test.ts index 65db8fd81..29b6c0ccf 100644 --- a/src/app/api/dashboard/chart-overlay-prefs/__tests__/route.test.ts +++ b/src/app/api/dashboard/chart-overlay-prefs/__tests__/route.test.ts @@ -109,7 +109,7 @@ describe("PUT /api/dashboard/chart-overlay-prefs — 422 multi-issue (v1.4.43 W6 expect(body.details.issues.length).toBeGreaterThanOrEqual(3); }); - it("writes a dashboard.chart-overlay.validation-failed audit row", async () => { + it("keeps the owner's own dashboard.chart-overlay refusal off the ledger", async () => { const res = await PUT( putReq({ chartKey: "not-a-chart", @@ -122,11 +122,9 @@ describe("PUT /api/dashboard/chart-overlay-prefs — 422 multi-issue (v1.4.43 W6 ); expect(res.status).toBe(422); await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); - const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as { - data: { userId: string; action: string }; - }; - expect(call.data.action).toBe("dashboard.chart-overlay.validation-failed"); + // An owner's own refused request is not an audit event: `auditLog` + // keeps it on the wide event and files no row. + expect(prisma.auditLog.create).not.toHaveBeenCalled(); }); it("does not block the 422 when the audit-row write rejects", async () => { diff --git a/src/app/api/dashboard/chart-overlay-prefs/route.ts b/src/app/api/dashboard/chart-overlay-prefs/route.ts index ab5c51a89..ded1da044 100644 --- a/src/app/api/dashboard/chart-overlay-prefs/route.ts +++ b/src/app/api/dashboard/chart-overlay-prefs/route.ts @@ -15,6 +15,7 @@ * the new prefs for the supplied `chartKey`, and writes it back inside the * same request. */ +import { auditLog } from "@/lib/auth/audit"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { apiSuccess, sanitiseZodIssues } from "@/lib/api-response"; import { parseJsonBody } from "@/lib/api/body"; @@ -72,17 +73,12 @@ export const PUT = apiHandler(async (request: NextRequest) => { shouldEmitAuditRow(user.id, "dashboard.chart-overlay.validation-failed") ) { const issues = sanitiseZodIssues(error.issues); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "dashboard.chart-overlay.validation-failed", - details: JSON.stringify({ issues }), - }, - }) - .catch(() => { - /* swallow — 422 response is the contract */ - }); + auditLog("dashboard.chart-overlay.validation-failed", { + userId: user.id, + details: { issues }, + }).catch(() => { + /* swallow — 422 response is the contract */ + }); } }, }); diff --git a/src/app/api/devices/__tests__/route.test.ts b/src/app/api/devices/__tests__/route.test.ts index 88c52f32d..ef65576c7 100644 --- a/src/app/api/devices/__tests__/route.test.ts +++ b/src/app/api/devices/__tests__/route.test.ts @@ -62,6 +62,7 @@ import { POST } from "../route"; import { prisma } from "@/lib/db"; import { getSession } from "@/lib/auth/session"; import { checkRateLimit } from "@/lib/rate-limit"; +import { auditLog } from "@/lib/auth/audit"; const SESSION_OK = { session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, @@ -78,6 +79,7 @@ function req(body: unknown): NextRequest { beforeEach(() => { vi.resetAllMocks(); + vi.mocked(auditLog).mockResolvedValue(undefined); vi.mocked(prisma.device.findUnique).mockResolvedValue(null); vi.mocked(prisma.device.findFirst).mockResolvedValue(null); vi.mocked(prisma.device.create).mockResolvedValue({ id: "dev-1" } as never); @@ -406,19 +408,16 @@ describe("POST /api/devices — 422 multi-issue envelope (v1.4.43 W6)", () => { ); expect(res.status).toBe(422); await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); - const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as { - data: { userId: string; action: string; details: string }; - }; - expect(call.data.userId).toBe("user-1"); - expect(call.data.action).toBe("devices.register.validation-failed"); + // Through `auditLog()`, which decides whether the refusal is filed. + expect(auditLog).toHaveBeenCalledWith( + "devices.register.validation-failed", + expect.objectContaining({ userId: "user-1" }), + ); }); it("does not block the 422 when the audit-row write rejects", async () => { vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); - vi.mocked(prisma.auditLog.create).mockRejectedValueOnce( - new Error("db down"), - ); + vi.mocked(auditLog).mockRejectedValueOnce(new Error("db down")); const res = await POST( req({ bundleId: "io.healthlog.app", apnsEnvironment: "tomorrow" }), ); diff --git a/src/app/api/devices/route.ts b/src/app/api/devices/route.ts index 5ad6c53a1..36f24678b 100644 --- a/src/app/api/devices/route.ts +++ b/src/app/api/devices/route.ts @@ -124,17 +124,12 @@ export const POST = apiHandler(async (request: NextRequest) => { const auditIssues = sanitiseZodIssues(error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "devices.register.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => { - /* swallow — 422 response is the contract */ - }); + auditLog("devices.register.validation-failed", { + userId: user.id, + details: { issues: auditIssues }, + }).catch(() => { + /* swallow — 422 response is the contract */ + }); }, }); if (!parsed.ok) return parsed.error; diff --git a/src/app/api/ingest/medication/__tests__/route.test.ts b/src/app/api/ingest/medication/__tests__/route.test.ts index 421dee241..aae965196 100644 --- a/src/app/api/ingest/medication/__tests__/route.test.ts +++ b/src/app/api/ingest/medication/__tests__/route.test.ts @@ -66,6 +66,7 @@ import { prisma } from "@/lib/db"; import { checkAuthSurfaceRateLimit, checkRateLimit } from "@/lib/rate-limit"; import { isApiGloballyEnabled } from "@/lib/app-settings"; import { invalidateUserMedications } from "@/lib/cache/invalidate"; +import { auditLog } from "@/lib/auth/audit"; function postReq( body: unknown, @@ -83,6 +84,7 @@ function postReq( beforeEach(() => { vi.resetAllMocks(); + vi.mocked(auditLog).mockResolvedValue(undefined); vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never); vi.mocked(checkAuthSurfaceRateLimit).mockResolvedValue({ allowed: true, @@ -145,18 +147,15 @@ describe("POST /api/ingest/medication — 422 multi-issue (v1.4.43 W6)", () => { const res = await POST(postReq({ medicationName: "" })); expect(res.status).toBe(422); await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); - const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as { - data: { userId: string; action: string }; - }; - expect(call.data.userId).toBe("user-1"); - expect(call.data.action).toBe("ingest.medication.validation-failed"); + // Through `auditLog()`, which decides whether the refusal is filed. + expect(auditLog).toHaveBeenCalledWith( + "ingest.medication.validation-failed", + expect.objectContaining({ userId: "user-1" }), + ); }); it("does not block the 422 when the audit-row write rejects", async () => { - vi.mocked(prisma.auditLog.create).mockRejectedValueOnce( - new Error("db down"), - ); + vi.mocked(auditLog).mockRejectedValueOnce(new Error("db down")); const res = await POST(postReq({ medicationName: "" })); expect(res.status).toBe(422); }); diff --git a/src/app/api/ingest/medication/route.ts b/src/app/api/ingest/medication/route.ts index f39f89c08..35176e405 100644 --- a/src/app/api/ingest/medication/route.ts +++ b/src/app/api/ingest/medication/route.ts @@ -174,17 +174,12 @@ export const POST = apiHandler(async (request: NextRequest) => { const auditIssues = sanitiseZodIssues(error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: apiToken.userId, - action: "ingest.medication.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => { - /* swallow — 422 response is the contract */ - }); + auditLog("ingest.medication.validation-failed", { + userId: apiToken.userId, + details: { issues: auditIssues }, + }).catch(() => { + /* swallow — 422 response is the contract */ + }); }, }); if (!parsed.ok) return parsed.error; diff --git a/src/app/api/insights/layout/__tests__/route.test.ts b/src/app/api/insights/layout/__tests__/route.test.ts index 3be6182da..341812186 100644 --- a/src/app/api/insights/layout/__tests__/route.test.ts +++ b/src/app/api/insights/layout/__tests__/route.test.ts @@ -44,6 +44,12 @@ vi.mock("@/lib/logging/context", async (importOriginal) => { }; }); +// The real `auditLog`, observed: it decides whether a refusal is filed. +vi.mock("@/lib/auth/audit", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, auditLog: vi.fn(actual.auditLog) }; +}); + vi.mock("next/headers", () => ({ headers: vi.fn(async () => ({ get: () => null })), cookies: vi.fn(async () => ({ @@ -57,6 +63,7 @@ import { GET, PUT, DELETE } from "../route"; import { prisma } from "@/lib/db"; import { getSession } from "@/lib/auth/session"; import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; import { __resetAllCachesForTests } from "@/lib/cache/server-cache"; import { __resetAuditDedupMemoForTests } from "@/lib/audit-dedup"; import { DEFAULT_INSIGHTS_LAYOUT } from "@/lib/insights-layout"; @@ -442,31 +449,31 @@ describe("PUT /api/insights/layout — 422 multi-issue envelope", () => { ); }); - it("writes one audit-ledger row keyed insights.layout.validation-failed", async () => { + it("hands the insights.layout.validation-failed refusal to auditLog, which files no row for the owner", async () => { const res = await callPut(makeReq({ version: 99, tiles: [] })); expect(res.status).toBe(422); await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); - const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as { - data: { userId: string; action: string; details: string }; - }; - expect(call.data.userId).toBe("user-1"); - expect(call.data.action).toBe("insights.layout.validation-failed"); - const details = JSON.parse(call.data.details) as { + expect(auditLog).toHaveBeenCalledTimes(1); + const [action, opts] = vi.mocked(auditLog).mock.calls[0]!; + expect(action).toBe("insights.layout.validation-failed"); + expect(opts?.userId).toBe("user-1"); + // An owner's own refused request is not an audit event. + expect(prisma.auditLog.create).not.toHaveBeenCalled(); + const details = opts?.details as { issues: Array<{ path: string; code: string }>; }; expect(details.issues.length).toBe(2); for (const issue of details.issues) { - // The persisted row strips `message` so a future Zod code that + // The filed issues strip `message` so a future Zod code that // echoes the offending value cannot leak into the audit // surface. expect(Object.keys(issue).sort()).toEqual(["code", "path"]); } }); - it("dedups the audit-ledger write across two sequential 422s for the same user", async () => { + it("dedups the audit call across two sequential 422s for the same user", async () => { const res1 = await callPut(makeReq({ version: 99, tiles: [] })); expect(res1.status).toBe(422); const res2 = await callPut(makeReq({ version: 99, tiles: [] })); @@ -474,7 +481,8 @@ describe("PUT /api/insights/layout — 422 multi-issue envelope", () => { await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); + expect(auditLog).toHaveBeenCalledTimes(1); + expect(prisma.auditLog.create).not.toHaveBeenCalled(); // The full multi-issue envelope still rides on every 422 — the // dedup only suppresses the breadcrumb write, never the response. diff --git a/src/app/api/insights/layout/route.ts b/src/app/api/insights/layout/route.ts index 640720b0d..3243bfe47 100644 --- a/src/app/api/insights/layout/route.ts +++ b/src/app/api/insights/layout/route.ts @@ -13,6 +13,7 @@ * single blob with a `surface` discriminator would force every read to * demux first. */ +import { auditLog } from "@/lib/auth/audit"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { apiSuccess, @@ -178,17 +179,12 @@ export const PUT = apiHandler(async (request: NextRequest) => { const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "insights.layout.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => { - /* swallow — validation response is the contract, audit row is best-effort */ - }); + auditLog("insights.layout.validation-failed", { + userId: user.id, + details: { issues: auditIssues }, + }).catch(() => { + /* swallow — validation response is the contract, audit row is best-effort */ + }); } return returnAllZodIssues(parsed.error, 422); } diff --git a/src/app/api/integrations/healthkit/__tests__/route.test.ts b/src/app/api/integrations/healthkit/__tests__/route.test.ts index 7804fad49..c7d7c6176 100644 --- a/src/app/api/integrations/healthkit/__tests__/route.test.ts +++ b/src/app/api/integrations/healthkit/__tests__/route.test.ts @@ -48,6 +48,7 @@ import { prisma } from "@/lib/db"; import { getSession } from "@/lib/auth/session"; import { listLiveMeasurementTypes } from "@/lib/measurements/live-types"; import { APPLE_HEALTH_DATA_STALE_AFTER_MS } from "@/lib/integrations/sync-verdict"; +import { auditLog } from "@/lib/auth/audit"; const SESSION_OK = { session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, @@ -56,6 +57,7 @@ const SESSION_OK = { beforeEach(() => { vi.resetAllMocks(); + vi.mocked(auditLog).mockResolvedValue(undefined); vi.mocked(prisma.user.findUnique).mockResolvedValue({ healthKitConfigJson: null, healthKitLastSyncedAt: null, @@ -220,18 +222,16 @@ describe("PATCH /api/integrations/healthkit — 422 multi-issue (v1.4.43 W6)", ( ); expect(res.status).toBe(422); await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); - const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as { - data: { userId: string; action: string }; - }; - expect(call.data.action).toBe("integrations.healthkit.validation-failed"); + // Through `auditLog()`, which decides whether the refusal is filed. + expect(auditLog).toHaveBeenCalledWith( + "integrations.healthkit.validation-failed", + expect.anything(), + ); }); it("does not block the 422 when the audit-row write rejects", async () => { vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); - vi.mocked(prisma.auditLog.create).mockRejectedValueOnce( - new Error("db down"), - ); + vi.mocked(auditLog).mockRejectedValueOnce(new Error("db down")); const res = await PATCH( patchReq({ entries: [{ id: "bodyMass", direction: "JUNK" }] }), ); diff --git a/src/app/api/integrations/healthkit/route.ts b/src/app/api/integrations/healthkit/route.ts index 1db234f6c..62a991514 100644 --- a/src/app/api/integrations/healthkit/route.ts +++ b/src/app/api/integrations/healthkit/route.ts @@ -268,17 +268,12 @@ export const PATCH = apiHandler(async (request: NextRequest) => { const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "integrations.healthkit.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => { - /* swallow — 422 response is the contract */ - }); + auditLog("integrations.healthkit.validation-failed", { + userId: user.id, + details: { issues: auditIssues }, + }).catch(() => { + /* swallow — 422 response is the contract */ + }); return returnAllZodIssues(parsed.error, 422); } diff --git a/src/app/api/labs/ocr/commit/route.ts b/src/app/api/labs/ocr/commit/route.ts index 9db6f0e75..e3a2bce86 100644 --- a/src/app/api/labs/ocr/commit/route.ts +++ b/src/app/api/labs/ocr/commit/route.ts @@ -163,15 +163,10 @@ async function commitOcrRows(request: NextRequest) { const auditIssues = sanitiseZodIssues(error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "labs.ocr.commit.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => {}); + auditLog("labs.ocr.commit.validation-failed", { + userId: user.id, + details: { issues: auditIssues }, + }).catch(() => {}); }, }); if (!parsed.ok) return parsed.error; @@ -402,7 +397,7 @@ async function commitOcrRows(request: NextRequest) { // A lab panel just landed — resolve any "annual blood panel" reminders now // rather than waiting on the cron. Fire-and-forget. if (inserted.length > 0) { - fireAndForget(enqueueReminderSatisfy(user.id), { + fireAndForget(() => enqueueReminderSatisfy(user.id), { action: "reminder.satisfy.enqueue", }); diff --git a/src/app/api/labs/route.ts b/src/app/api/labs/route.ts index 4732acab9..65373f80c 100644 --- a/src/app/api/labs/route.ts +++ b/src/app/api/labs/route.ts @@ -373,7 +373,7 @@ async function postLabResult(request: NextRequest) { // v1.18.1 — eventful Lab↔Vorsorge satisfaction. A lab panel just // landed; resolve the user's free-text "annual blood panel" reminders // now rather than waiting on the 15-min cron. Fire-and-forget. - fireAndForget(enqueueReminderSatisfy(user.id), { + fireAndForget(() => enqueueReminderSatisfy(user.id), { action: "reminder.satisfy.enqueue", }); diff --git a/src/app/api/measurements/__tests__/arrival-wiring.test.ts b/src/app/api/measurements/__tests__/arrival-wiring.test.ts index ded4e7f73..2e86f8ae0 100644 --- a/src/app/api/measurements/__tests__/arrival-wiring.test.ts +++ b/src/app/api/measurements/__tests__/arrival-wiring.test.ts @@ -57,7 +57,8 @@ vi.mock("@/lib/logging/context", async (importOriginal) => ({ annotate: vi.fn(), })); vi.mock("@/lib/logging/fire-and-forget", () => ({ - fireAndForget: (promise: Promise) => void promise.catch(() => {}), + fireAndForget: (start: () => Promise) => + void start().catch(() => {}), })); vi.mock("@/lib/crypto/note-cipher", () => ({ encryptNote: vi.fn(() => null), diff --git a/src/app/api/medications/[id]/__tests__/route.test.ts b/src/app/api/medications/[id]/__tests__/route.test.ts index 8cc13470d..a1791b827 100644 --- a/src/app/api/medications/[id]/__tests__/route.test.ts +++ b/src/app/api/medications/[id]/__tests__/route.test.ts @@ -41,6 +41,39 @@ vi.mock("@/lib/db", () => ({ }, })); +// The answer is the medication's read model (`medicationDetail`), whose +// reads and resolution the integration walks hold against the contract. +// Here it stands in with the part these tests read: the written row with +// its schedules wired as every medication answer wires them. +vi.mock("@/lib/medications/read-medication", async (importOriginal) => { + const actual = + await importOriginal(); + const { scheduleWireFields } = + await import("@/lib/medications/intake-tracking"); + const { serializeScheduleUnitsPerDose } = + await import("@/lib/medications/schedule-units-dto"); + return { + ...actual, + medicationDetail: vi.fn( + async ({ + medication, + }: { + medication: Parameters[0]["medication"]; + }) => ({ + ...medication, + unitsPerDose: Number(medication.unitsPerDose), + ...scheduleWireFields( + medication.trackIntake, + serializeScheduleUnitsPerDose( + medication.schedules, + medication.unitsPerDose, + ), + ), + }), + ), + }; +}); + vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() })); vi.mock("@/lib/auth/audit", () => ({ auditLog: vi.fn().mockResolvedValue(undefined), diff --git a/src/app/api/medications/__tests__/route.test.ts b/src/app/api/medications/__tests__/route.test.ts index d91bb73e9..1159f63a2 100644 --- a/src/app/api/medications/__tests__/route.test.ts +++ b/src/app/api/medications/__tests__/route.test.ts @@ -34,6 +34,39 @@ vi.mock("@/lib/db", () => ({ }, })); +// The answer is the medication's read model (`medicationDetail`), whose +// reads and resolution the integration walks hold against the contract. +// Here it stands in with the part these tests read: the written row with +// its schedules wired as every medication answer wires them. +vi.mock("@/lib/medications/read-medication", async (importOriginal) => { + const actual = + await importOriginal(); + const { scheduleWireFields } = + await import("@/lib/medications/intake-tracking"); + const { serializeScheduleUnitsPerDose } = + await import("@/lib/medications/schedule-units-dto"); + return { + ...actual, + medicationDetail: vi.fn( + async ({ + medication, + }: { + medication: Parameters[0]["medication"]; + }) => ({ + ...medication, + unitsPerDose: Number(medication.unitsPerDose), + ...scheduleWireFields( + medication.trackIntake, + serializeScheduleUnitsPerDose( + medication.schedules, + medication.unitsPerDose, + ), + ), + }), + ), + }; +}); + vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() })); vi.mock("@/lib/auth/audit", () => ({ auditLog: vi.fn().mockResolvedValue(undefined), @@ -88,6 +121,7 @@ vi.mock("next/headers", () => ({ import { GET, POST } from "../route"; import { prisma } from "@/lib/db"; +import { medicationDetail } from "@/lib/medications/read-medication"; import { getSession } from "@/lib/auth/session"; import { cachedSwr } from "@/lib/cache/server-cache"; import { getUserTodayBounds } from "@/lib/tz/local-day"; @@ -657,11 +691,18 @@ describe("POST /api/medications — Apple Health mirror (v1.28)", () => { const res = await POST(postReq(MIRROR_BODY)); expect(res.status).toBe(200); const body = (await res.json()) as { - data: { id: string; externalId: string; category: string }; + data: { id: string; externalId: string }; }; expect(body.data.id).toBe("med-existing"); expect(body.data.externalId).toBe("hk-concept-1"); - expect(body.data.category).toBe("OTHER"); + // Answered with the read model of the stored row, keeping the create's + // action on the event. + expect(medicationDetail).toHaveBeenCalledWith( + expect.objectContaining({ + medication: expect.objectContaining({ id: "med-existing" }), + announce: false, + }), + ); // Idempotent replay: nothing written. expect(prisma.medication.create).not.toHaveBeenCalled(); }); diff --git a/src/app/api/medications/intake/bulk/__tests__/route.test.ts b/src/app/api/medications/intake/bulk/__tests__/route.test.ts index bd530c53c..006d9c7ee 100644 --- a/src/app/api/medications/intake/bulk/__tests__/route.test.ts +++ b/src/app/api/medications/intake/bulk/__tests__/route.test.ts @@ -85,6 +85,7 @@ import { consumeForIntake, restoreForIntake, } from "@/lib/medications/inventory/consumption"; +import { auditLog } from "@/lib/auth/audit"; const SESSION_OK = { session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, @@ -109,6 +110,7 @@ function postReq(body: unknown): NextRequest { beforeEach(() => { vi.resetAllMocks(); + vi.mocked(auditLog).mockResolvedValue(undefined); vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never); vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never); @@ -176,17 +178,15 @@ describe("POST /api/medications/intake/bulk — 422 multi-issue (v1.4.43 W6)", ( ); expect(res.status).toBe(422); await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); - const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as { - data: { userId: string; action: string }; - }; - expect(call.data.action).toBe("medications.intake.bulk.validation-failed"); + // Through `auditLog()`, which decides whether the refusal is filed. + expect(auditLog).toHaveBeenCalledWith( + "medications.intake.bulk.validation-failed", + expect.anything(), + ); }); it("does not block the 422 when the audit-row write rejects", async () => { - vi.mocked(prisma.auditLog.create).mockRejectedValueOnce( - new Error("db down"), - ); + vi.mocked(auditLog).mockRejectedValueOnce(new Error("db down")); const res = await POST( postReq({ entries: [{ medicationId: "", scheduledFor: "not-iso" }], diff --git a/src/app/api/medications/intake/bulk/route.ts b/src/app/api/medications/intake/bulk/route.ts index 80d490cb6..9f0c0ca27 100644 --- a/src/app/api/medications/intake/bulk/route.ts +++ b/src/app/api/medications/intake/bulk/route.ts @@ -39,10 +39,10 @@ * Locked contract — see `.planning/v15-ios-handoff/06-ios-responsibilities.md` * §"Cumulative metrics" sibling section for the SyncMode rationale. */ +import { auditLog } from "@/lib/auth/audit"; import { NextRequest } from "next/server"; import { z } from "zod/v4"; -import { prisma } from "@/lib/db"; import { apiHandler, isScopedCredential, requireAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { @@ -198,17 +198,12 @@ async function postBulk(request: NextRequest): Promise { const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "medications.intake.bulk.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => { - /* swallow — 422 response is the contract */ - }); + auditLog("medications.intake.bulk.validation-failed", { + userId: user.id, + details: { issues: auditIssues }, + }).catch(() => { + /* swallow — 422 response is the contract */ + }); return returnAllZodIssues(parsed.error, 422, { errorCode: "medications.intake.bulk.invalid", }); diff --git a/src/app/api/medications/layout/__tests__/route.test.ts b/src/app/api/medications/layout/__tests__/route.test.ts index 3e02ca491..cc8f500b6 100644 --- a/src/app/api/medications/layout/__tests__/route.test.ts +++ b/src/app/api/medications/layout/__tests__/route.test.ts @@ -243,7 +243,7 @@ describe("PUT /api/medications/layout — 422 multi-issue envelope", () => { expect(res.status).toBe(422); }); - it("writes one deduped audit breadcrumb for repeated 422s", async () => { + it("files no row for an owner's repeated refusals", async () => { const res1 = await callPut(makeReq({ version: 9 })); const res2 = await callPut(makeReq({ version: 9 })); expect(res1.status).toBe(422); @@ -251,12 +251,9 @@ describe("PUT /api/medications/layout — 422 multi-issue envelope", () => { await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); - const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as { - data: { userId: string; action: string }; - }; - expect(call.data.userId).toBe("user-1"); - expect(call.data.action).toBe("medication.layout.validation-failed"); + // An owner's own refused request is not an audit event: `auditLog` + // keeps it on the wide event and files no row. + expect(prisma.auditLog.create).not.toHaveBeenCalled(); }); }); diff --git a/src/app/api/medications/layout/route.ts b/src/app/api/medications/layout/route.ts index 5434fa6c0..84b5a4add 100644 --- a/src/app/api/medications/layout/route.ts +++ b/src/app/api/medications/layout/route.ts @@ -11,6 +11,7 @@ * lives on its own `User` column (`medication_list_layout_json`) per * the per-surface-column convention. */ +import { auditLog } from "@/lib/auth/audit"; import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler"; import { apiSuccess, @@ -151,17 +152,12 @@ export const PUT = apiHandler(async (request: NextRequest) => { const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "medication.layout.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => { - /* swallow — validation response is the contract, audit row is best-effort */ - }); + auditLog("medication.layout.validation-failed", { + userId: user.id, + details: { issues: auditIssues }, + }).catch(() => { + /* swallow — validation response is the contract, audit row is best-effort */ + }); } return returnAllZodIssues(parsed.error, 422); } diff --git a/src/app/api/mood-entries/bulk/__tests__/route.test.ts b/src/app/api/mood-entries/bulk/__tests__/route.test.ts index f67b039eb..e77da8c5b 100644 --- a/src/app/api/mood-entries/bulk/__tests__/route.test.ts +++ b/src/app/api/mood-entries/bulk/__tests__/route.test.ts @@ -100,6 +100,7 @@ import { prisma } from "@/lib/db"; import { getSession } from "@/lib/auth/session"; import { checkRateLimit } from "@/lib/rate-limit"; import { createTagLinks } from "@/lib/mood/tag-links"; +import { auditLog } from "@/lib/auth/audit"; const SESSION_OK = { session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, @@ -139,6 +140,7 @@ function existingRow(overrides: { beforeEach(() => { vi.resetAllMocks(); + vi.mocked(auditLog).mockResolvedValue(undefined); vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never); vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never); @@ -213,17 +215,15 @@ describe("POST /api/mood-entries/bulk — 422 multi-issue (v1.4.43 W6)", () => { ); expect(res.status).toBe(422); await new Promise((r) => setTimeout(r, 5)); - expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); - const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as { - data: { userId: string; action: string }; - }; - expect(call.data.action).toBe("mood.bulk.validation-failed"); + // Through `auditLog()`, which decides whether the refusal is filed. + expect(auditLog).toHaveBeenCalledWith( + "mood.bulk.validation-failed", + expect.anything(), + ); }); it("does not block the 422 when the audit-row write rejects", async () => { - vi.mocked(prisma.auditLog.create).mockRejectedValueOnce( - new Error("db down"), - ); + vi.mocked(auditLog).mockRejectedValueOnce(new Error("db down")); const res = await POST( postReq({ entries: [ diff --git a/src/app/api/mood-entries/bulk/route.ts b/src/app/api/mood-entries/bulk/route.ts index 407b3015e..685ee208a 100644 --- a/src/app/api/mood-entries/bulk/route.ts +++ b/src/app/api/mood-entries/bulk/route.ts @@ -24,10 +24,10 @@ * The route authenticates, meters, caps and parses; the per-entry writes live * in `src/lib/mood/bulk-ingest.ts`. */ +import { auditLog } from "@/lib/auth/audit"; import { NextRequest } from "next/server"; import { z } from "zod/v4"; -import { prisma } from "@/lib/db"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { @@ -206,17 +206,12 @@ async function postBulk(request: NextRequest): Promise { const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "mood.bulk.validation-failed", - details: JSON.stringify({ issues: auditIssues }), - }, - }) - .catch(() => { - /* swallow — 422 response is the contract */ - }); + auditLog("mood.bulk.validation-failed", { + userId: user.id, + details: { issues: auditIssues }, + }).catch(() => { + /* swallow — 422 response is the contract */ + }); return returnAllZodIssues(parsed.error, 422, { errorCode: "mood.bulk.invalid", }); diff --git a/src/hooks/use-workouts.ts b/src/hooks/use-workouts.ts index 3485551dd..d17aa8d97 100644 --- a/src/hooks/use-workouts.ts +++ b/src/hooks/use-workouts.ts @@ -7,6 +7,7 @@ import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useAuth } from "@/hooks/use-auth"; import { queryKeys } from "@/lib/query-keys"; import { apiGet } from "@/lib/api/api-fetch"; +import type { RouteSample } from "@/lib/workouts/route-samples"; /** * v1.4.32 — TanStack Query wrapper for `GET /api/workouts`. @@ -325,7 +326,7 @@ export interface WorkoutDetailPayload extends WorkoutListEntry { metadata: unknown; route: { geometry: unknown; - sampleTimestamps: string[] | null; + sampleTimestamps: RouteSample[] | null; } | null; /** * Raw per-workout HR sample envelope. `samples.samples` is dropped diff --git a/src/lib/ai/anthropic-host.ts b/src/lib/ai/anthropic-host.ts new file mode 100644 index 000000000..6a9b04eed --- /dev/null +++ b/src/lib/ai/anthropic-host.ts @@ -0,0 +1,18 @@ +/** + * True when a base URL addresses Anthropic's API: the host itself or any + * subdomain of `anthropic.com`, case-insensitively. Parsed rather than + * substring-matched, so `https://anthropic.com.attacker.example/v1` and + * `https://evil.example/?x=api.anthropic.com` both answer false. + * + * An unparseable value answers false and keeps the OpenAI path, which is the + * behaviour that value had before this function existed. + */ +export function isAnthropicBaseUrl(baseUrl: string): boolean { + let host: string; + try { + host = new URL(baseUrl).hostname.toLowerCase(); + } catch { + return false; + } + return host === "anthropic.com" || host.endsWith(".anthropic.com"); +} diff --git a/src/lib/ai/provider.ts b/src/lib/ai/provider.ts index 9c788bbee..d589685ce 100644 --- a/src/lib/ai/provider.ts +++ b/src/lib/ai/provider.ts @@ -3,7 +3,8 @@ import { readOperatorRow } from "@/lib/settings/operator-row"; import { bindResponseTimeout } from "./effective-timeout"; import { bindReasoningEffort } from "./reasoning-effort"; import { prisma } from "@/lib/db"; -import { decrypt } from "@/lib/crypto"; +import { openStoredKey } from "./stored-key"; +import { isAnthropicBaseUrl } from "./anthropic-host"; import { CodexClient, resolveCodexVisionSlug } from "./codex-client"; import { OpenAIClient } from "./openai-client"; import { AnthropicClient } from "./anthropic-client"; @@ -400,8 +401,12 @@ function buildCompatProvider(row: { }): AIProvider | null { const model = row.aiCompatModel?.trim() || row.aiModel?.trim() || null; if (!row.aiCompatBaseUrl || !model) return null; + const apiKey = row.aiCompatKeyEncrypted + ? openStoredKey(row.aiCompatKeyEncrypted) + : ""; + if (apiKey === null) return null; return new OpenAIClient({ - apiKey: row.aiCompatKeyEncrypted ? decrypt(row.aiCompatKeyEncrypted) : "", + apiKey, model, baseUrl: row.aiCompatBaseUrl, providerType: "openai-compatible", @@ -424,17 +429,21 @@ function buildUserProvider(row: UserAIRow): AIProvider | null { // configuration survived in the row, refuse to forward an Anthropic // key to it. Anthropic has no per-tenant base URL the UI exposes; // the SDK default is correct. + const apiKey = openStoredKey(row.aiAnthropicKeyEncrypted); + if (apiKey === null) return null; return new AnthropicClient({ - apiKey: decrypt(row.aiAnthropicKeyEncrypted), + apiKey, model: row.aiModel ?? "claude-sonnet-4-6", }); } case "LOCAL": { if (!row.aiBaseUrl) return null; + const apiKey = row.aiLocalKeyEncrypted + ? openStoredKey(row.aiLocalKeyEncrypted) + : null; + if (row.aiLocalKeyEncrypted && apiKey === null) return null; return new LocalOpenAICompatibleClient({ - apiKey: row.aiLocalKeyEncrypted - ? decrypt(row.aiLocalKeyEncrypted) - : null, + apiKey, model: row.aiModel ?? "local-model", baseUrl: row.aiBaseUrl, operatorTrusted: isOperatorOwned(row), @@ -454,9 +463,12 @@ function buildUserProvider(row: UserAIRow): AIProvider | null { // Belt-and-braces: ignore any persisted `aiBaseUrl`. The column is // shared with LOCAL, so a stale LAN URL there would otherwise // redirect the user's OpenAI key to a private host. - if (!row.aiOpenaiKeyEncrypted) return null; + const apiKey = row.aiOpenaiKeyEncrypted + ? openStoredKey(row.aiOpenaiKeyEncrypted) + : null; + if (apiKey === null) return null; return new OpenAIClient({ - apiKey: decrypt(row.aiOpenaiKeyEncrypted), + apiKey, model: row.aiModel ?? "gpt-4o", baseUrl: "https://api.openai.com/v1", }); @@ -469,25 +481,6 @@ function buildUserProvider(row: UserAIRow): AIProvider | null { } } -/** - * True when a base URL addresses Anthropic's API: the host itself or any - * subdomain of `anthropic.com`, case-insensitively. Parsed rather than - * substring-matched, so `https://anthropic.com.attacker.example/v1` and - * `https://evil.example/?x=api.anthropic.com` both answer false. - * - * An unparseable value answers false and keeps the OpenAI path, which is the - * behaviour that value had before this function existed. - */ -export function isAnthropicBaseUrl(baseUrl: string): boolean { - let host: string; - try { - host = new URL(baseUrl).hostname.toLowerCase(); - } catch { - return false; - } - return host === "anthropic.com" || host.endsWith(".anthropic.com"); -} - /** * The operator's global provider — one key, one model, one base URL in the * admin settings, serving every user who has configured nothing themselves. @@ -515,17 +508,20 @@ async function resolveAdminProvider(): Promise { where: { id: "singleton" }, }); - if (settings?.adminAiKeyEncrypted) { + const adminKey = settings?.adminAiKeyEncrypted + ? openStoredKey(settings.adminAiKeyEncrypted) + : null; + if (settings && adminKey !== null) { const baseUrl = settings.adminAiBaseUrl ?? "https://api.openai.com/v1"; if (isAnthropicBaseUrl(baseUrl)) { return new AnthropicClient({ - apiKey: decrypt(settings.adminAiKeyEncrypted), + apiKey: adminKey, model: settings.adminAiModel ?? "claude-sonnet-4-6", baseUrl, }); } return new OpenAIClient({ - apiKey: decrypt(settings.adminAiKeyEncrypted), + apiKey: adminKey, model: settings.adminAiModel ?? "gpt-4o", baseUrl, // The instance-wide provider is the operator's own configuration. @@ -1351,27 +1347,31 @@ async function resolveProviderForType( } case "openai": { const enc = ctx.userRow?.aiOpenaiKeyEncrypted; - if (!enc) return null; + const apiKey = enc ? openStoredKey(enc) : null; + if (apiKey === null) return null; return new OpenAIClient({ - apiKey: decrypt(enc), + apiKey, model: ctx.userRow?.aiModel ?? "gpt-4o", baseUrl: "https://api.openai.com/v1", }); } case "anthropic": { const enc = ctx.userRow?.aiAnthropicKeyEncrypted; - if (!enc) return null; + const apiKey = enc ? openStoredKey(enc) : null; + if (apiKey === null) return null; return new AnthropicClient({ - apiKey: decrypt(enc), + apiKey, model: ctx.userRow?.aiModel ?? "claude-sonnet-4-6", }); } case "local": { if (!ctx.userRow?.aiBaseUrl) return null; + const apiKey = ctx.userRow.aiLocalKeyEncrypted + ? openStoredKey(ctx.userRow.aiLocalKeyEncrypted) + : null; + if (ctx.userRow.aiLocalKeyEncrypted && apiKey === null) return null; return new LocalOpenAICompatibleClient({ - apiKey: ctx.userRow.aiLocalKeyEncrypted - ? decrypt(ctx.userRow.aiLocalKeyEncrypted) - : null, + apiKey, model: ctx.userRow.aiModel ?? "local-model", baseUrl: ctx.userRow.aiBaseUrl, operatorTrusted: isOperatorOwned(ctx.userRow), @@ -1505,7 +1505,7 @@ async function resolveProviderForTestUnbound( const apiKey = override.anthropicKey?.trim() || (stored?.aiAnthropicKeyEncrypted - ? decrypt(stored.aiAnthropicKeyEncrypted) + ? (openStoredKey(stored.aiAnthropicKeyEncrypted) ?? "") : ""); if (!apiKey) { throw new AITestConfigError(422, "Anthropic API key not configured"); @@ -1534,7 +1534,7 @@ async function resolveProviderForTestUnbound( const apiKey = override.localKey?.trim() || (stored?.aiLocalKeyEncrypted - ? decrypt(stored.aiLocalKeyEncrypted) + ? (openStoredKey(stored.aiLocalKeyEncrypted) ?? null) : null); return new LocalOpenAICompatibleClient({ apiKey, @@ -1583,7 +1583,7 @@ async function resolveProviderForTestUnbound( const apiKey = override.compatKey?.trim() || (stored?.aiCompatKeyEncrypted - ? decrypt(stored.aiCompatKeyEncrypted) + ? (openStoredKey(stored.aiCompatKeyEncrypted) ?? "") : ""); return new OpenAIClient({ apiKey, @@ -1608,7 +1608,7 @@ async function resolveProviderForTestUnbound( const userKey = override.openaiKey?.trim() || (stored?.aiOpenaiKeyEncrypted - ? decrypt(stored.aiOpenaiKeyEncrypted) + ? (openStoredKey(stored.aiOpenaiKeyEncrypted) ?? "") : ""); if (userKey) { return new OpenAIClient({ diff --git a/src/lib/ai/reasoning/controls.ts b/src/lib/ai/reasoning/controls.ts index a34655774..624d7055f 100644 --- a/src/lib/ai/reasoning/controls.ts +++ b/src/lib/ai/reasoning/controls.ts @@ -9,7 +9,8 @@ import { readOperatorRow } from "@/lib/settings/operator-row"; import { prisma } from "@/lib/db"; import { annotate, getEvent } from "@/lib/logging/context"; import { memoizePerRequest } from "@/lib/request-cache"; -import { isAnthropicBaseUrl, probeProviderChain } from "@/lib/ai/provider"; +import { isAnthropicBaseUrl } from "@/lib/ai/anthropic-host"; +import { probeProviderChain } from "@/lib/ai/provider"; import { isOperatorFundedProvider } from "@/lib/ai/coach/budget"; import type { ProviderChainType } from "@/lib/ai/provider-chain"; import type { ProviderWorkAuthority } from "@/lib/sharing/provider-work-authority"; diff --git a/src/lib/ai/stored-key.ts b/src/lib/ai/stored-key.ts new file mode 100644 index 000000000..e4e6ad007 --- /dev/null +++ b/src/lib/ai/stored-key.ts @@ -0,0 +1,21 @@ +/** + * Opening a stored AI-provider key. + */ +import { decrypt } from "@/lib/crypto"; +import { getEvent } from "@/lib/logging/context"; + +/** + * A stored provider key, or null when it cannot be opened: a key id retired + * from `ENCRYPTION_KEYS`, a corrupt row. An unreadable key is an unconfigured + * provider, the same as no key at all. Its entry drops out of the chain, so a + * generation answers the no-provider refusal (422 `ai.provider.none`) + * instead of the decrypt's 500, and the operator sees the warning. + */ +export function openStoredKey(encrypted: string): string | null { + try { + return decrypt(encrypted); + } catch { + getEvent()?.addWarning("ai: a stored provider key could not be opened"); + return null; + } +} diff --git a/src/lib/ai/types.ts b/src/lib/ai/types.ts index bf4b47fc2..4dcbdc1bf 100644 --- a/src/lib/ai/types.ts +++ b/src/lib/ai/types.ts @@ -75,7 +75,16 @@ export const insightRecommendationSchema = z.union([ }), ]); -export const insightResultSchema = z.object({ +/** + * The fields every insight reply must carry. Loose on purpose: the briefing + * prompt also asks for `dailyBriefing`, `trendAnnotations` and + * `storyboardAnnotations`, each read downstream (the briefing card, the + * dashboard snapshot, the today read), and a strict object dropped exactly + * those from a reply that validated, while a reply that did not validate + * kept them. Validation checks these fields and leaves the rest of the reply + * as it came. + */ +export const insightResultSchema = z.looseObject({ insightType: z.string().optional(), summary: z.string(), classification: z.enum([ diff --git a/src/lib/cache/server-cache.ts b/src/lib/cache/server-cache.ts index 48a2dd842..265d1215f 100644 --- a/src/lib/cache/server-cache.ts +++ b/src/lib/cache/server-cache.ts @@ -36,6 +36,7 @@ */ import { trackBackgroundTask } from "@/lib/logging/background-tasks"; +import { runDetached } from "@/lib/logging/detached"; import type { WorkoutsProjection } from "@/lib/workouts/list-read"; export interface ServerCacheOptions { @@ -406,23 +407,19 @@ export class ServerCache { // single-flight slot as `wrap`, so an invalidation that lands // while it is in flight discards its (pre-write) result instead // of re-caching it as fresh. - const refresh = this.startBuild(key, builder, ttlMsOverride).promise; - // Detach: the foreground caller does not await the rebuild. - // Handle the rejection on the detached handle so an - // unhandled-rejection warning never fires — but never silently: - // a refresh that keeps failing means the bucket serves an - // ever-older stale value until the SWR window finally lapses, - // so the failure must reach the logs. There is no request - // context here (the caller already returned), so a structured - // console.error stands in for the wide-event annotation; the - // key is logged as its djb2 hash, matching the cache-outcome - // meta convention, so the userId segment never reaches stdout. - // Registered so the test suite can await it: the error below is a - // console write on a detached handle, and one that lands while a - // vitest worker is closing its RPC channel fails the whole run. - // No-op outside test. + // Detached (`runDetached`): the caller does not await the rebuild, + // and the rebuild cannot rename the caller's event. Its rejection is + // handled, but never silently: a refresh that keeps failing serves + // an ever-older value until the SWR window lapses. It may outlive + // the event, so a structured console.error carries it, the key as + // its djb2 hash (the cache-outcome meta convention) so the userId + // segment never reaches stdout. Registered so the test suite can + // await it: a console write landing while a vitest worker closes + // its RPC channel fails the whole run. No-op outside test. trackBackgroundTask( - refresh.catch((err) => { + runDetached( + () => this.startBuild(key, builder, ttlMsOverride).promise, + ).catch((err) => { console.error( `cache.${this.opts.name ?? "unnamed"}.refresh_failed`, JSON.stringify({ diff --git a/src/lib/fitbit/jobs/sync-handlers.ts b/src/lib/fitbit/jobs/sync-handlers.ts index 3b9ea163d..148c89428 100644 --- a/src/lib/fitbit/jobs/sync-handlers.ts +++ b/src/lib/fitbit/jobs/sync-handlers.ts @@ -79,7 +79,7 @@ export async function handleFitbitSync( // reminders eventfully. Fire-and-forget; the cron is the net. onUserSynced: (userId, imported) => { if (imported > 0) { - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); } diff --git a/src/lib/insights/__tests__/background-reasoning-wire.test.ts b/src/lib/insights/__tests__/background-reasoning-wire.test.ts index a135cbea1..aeb843364 100644 --- a/src/lib/insights/__tests__/background-reasoning-wire.test.ts +++ b/src/lib/insights/__tests__/background-reasoning-wire.test.ts @@ -48,7 +48,6 @@ vi.mock("@/lib/ai/provider", () => ({ resolveProvider: vi.fn(), probeProviderPresence: vi.fn(async () => true), probeProviderChain: vi.fn(), - isAnthropicBaseUrl: () => false, })); vi.mock("@/lib/ai/capabilities/gate", () => ({ diff --git a/src/lib/insights/briefing-request/__tests__/parse-briefing-reply.test.ts b/src/lib/insights/briefing-request/__tests__/parse-briefing-reply.test.ts new file mode 100644 index 000000000..68de24fa8 --- /dev/null +++ b/src/lib/insights/briefing-request/__tests__/parse-briefing-reply.test.ts @@ -0,0 +1,37 @@ +/** + * A reply that fits the insight schema keeps the blocks the prompt asks for + * beside it. The strict object used to drop `dailyBriefing` from exactly the + * replies that validated. + */ +import { describe, expect, it } from "vitest"; + +import { parseBriefingReply } from "../grounding"; + +const VALID = { + summary: "Steady week.", + classification: "gut", + findings: [], + correlations: [], + recommendations: [], + dataQuality: { coverage: "good", gaps: [], confidence: "hoch" }, + disclaimer: "Consult your doctor.", +}; + +const BLOCKS = { + dailyBriefing: { paragraph: "Readings held steady.", signalsOfDay: [] }, + trendAnnotations: [{ metric: "weight", note: "flat" }], + storyboardAnnotations: [], +}; + +describe("parseBriefingReply", () => { + it("keeps the briefing blocks of a reply that validates", () => { + const parsed = parseBriefingReply(JSON.stringify({ ...VALID, ...BLOCKS })); + expect(parsed).toMatchObject(BLOCKS); + expect(parsed).toMatchObject({ summary: "Steady week." }); + }); + + it("keeps a reply that does not validate as it was parsed", () => { + const loose = { summary: "No classification.", ...BLOCKS }; + expect(parseBriefingReply(JSON.stringify(loose))).toEqual(loose); + }); +}); diff --git a/src/lib/insights/briefing-request/completion.ts b/src/lib/insights/briefing-request/completion.ts index a63f86804..923c29720 100644 --- a/src/lib/insights/briefing-request/completion.ts +++ b/src/lib/insights/briefing-request/completion.ts @@ -45,8 +45,8 @@ export function runBriefingCall( // `getInsightsSystemPrompt` returned the old `{changed, stable, drivers, // …}` shape and never asked the model for dailyBriefing, so the hero // strip's "Re-run analysis" button kept producing a payload with no - // briefing block. The reply tolerates the strict shape via - // `insightResultSchema.safeParse`'s soft fallback to the parsed object. + // briefing block. `insightResultSchema` is loose, so a reply that + // validates keeps these blocks, and one that does not is kept as parsed. systemPrompt: buildSystemPromptWithReferences( ctx.locale, prompt.referenceMetrics, diff --git a/src/lib/insights/comprehensive-generate.ts b/src/lib/insights/comprehensive-generate.ts index 137f488aa..8ae3feb59 100644 --- a/src/lib/insights/comprehensive-generate.ts +++ b/src/lib/insights/comprehensive-generate.ts @@ -342,8 +342,8 @@ async function rerollBriefingParagraph(args: { /** * Parse + (best-effort) validate a comprehensive provider reply. Returns the - * parsed payload (validated when it matches `insightResultSchema`, else the - * raw object so the richer optional blocks survive), or null when the reply + * parsed payload (validated when it matches `insightResultSchema`, which + * keeps the richer optional blocks, else the raw object), or null when the reply * is not valid JSON even after fence-stripping. Null signals the caller to * run its one corrective retry. */ diff --git a/src/lib/jobs/boss-ready.ts b/src/lib/jobs/boss-ready.ts index 1388fcfff..0c06af8f2 100644 --- a/src/lib/jobs/boss-ready.ts +++ b/src/lib/jobs/boss-ready.ts @@ -23,6 +23,7 @@ import type { PgBoss } from "pg-boss"; import { startupPhase } from "@/lib/boot/startup-state"; import { getGlobalBoss } from "@/lib/jobs/boss-instance"; import { trackBackgroundTask } from "@/lib/logging/background-tasks"; +import { runDetached } from "@/lib/logging/detached"; /** The global slot a producer (re)start is parked in while it runs. */ export const BOSS_START_KEY = "__healthlog_pgboss_start__" as const; @@ -80,14 +81,16 @@ export async function enqueueWhenBossReady( } if (!bossStartPending()) return "skipped"; trackBackgroundTask( - whenBossReady(label).then(async (boss) => { - if (!boss) return; - try { - await send(boss); - } catch (err) { - console.warn(`[pg-boss] deferred ${label} enqueue failed:`, err); - } - }), + runDetached(() => + whenBossReady(label).then(async (boss) => { + if (!boss) return; + try { + await send(boss); + } catch (err) { + console.warn(`[pg-boss] deferred ${label} enqueue failed:`, err); + } + }), + ), ); return "deferred"; } diff --git a/src/lib/jobs/import-tail.ts b/src/lib/jobs/import-tail.ts index a63dbf71d..0df38f7ca 100644 --- a/src/lib/jobs/import-tail.ts +++ b/src/lib/jobs/import-tail.ts @@ -25,6 +25,7 @@ import type { MeasurementType } from "@/generated/prisma/client"; import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; import { invalidateStatusInsightsForTypes } from "@/lib/insights/status-invalidation"; import { trackBackgroundTask } from "@/lib/logging/background-tasks"; +import { runDetached } from "@/lib/logging/detached"; import { recomputeUserRollups, ROLLUP_FOLD_WINDOW_MS, @@ -68,9 +69,11 @@ export async function afterBulkImport( if (types.length > 0) { // Registered so the test suite can await the detached handle. trackBackgroundTask( - invalidateStatusInsightsForTypes(userId, types, { - source: "manual", - }).catch((err) => { + runDetached(() => + invalidateStatusInsightsForTypes(userId, types, { + source: "manual", + }), + ).catch((err) => { console.warn( `[${label}] status-insight invalidate failed for user ${userId}`, err, diff --git a/src/lib/jobs/reminder/google-health-sync.ts b/src/lib/jobs/reminder/google-health-sync.ts index 0e49f4f64..44e59eadf 100644 --- a/src/lib/jobs/reminder/google-health-sync.ts +++ b/src/lib/jobs/reminder/google-health-sync.ts @@ -105,7 +105,7 @@ export async function handleGoogleHealthSync( // next hour's, which is not a failure to retry. if (result.failed && shouldStop()) { if (result.imported > 0) { - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); } @@ -128,7 +128,7 @@ export async function handleGoogleHealthSync( : "complete"; if (result.imported > 0) { - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); } diff --git a/src/lib/jobs/reminder/poll-cohort.ts b/src/lib/jobs/reminder/poll-cohort.ts index 7bd205f59..25f62ebde 100644 --- a/src/lib/jobs/reminder/poll-cohort.ts +++ b/src/lib/jobs/reminder/poll-cohort.ts @@ -225,7 +225,7 @@ export function makePollCohortHandler({ // v1.18.1 — a fresh reading landed; resolve this user's Vorsorge // reminders eventfully. Fire-and-forget; the cron is the net. if (imported > 0) { - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); } diff --git a/src/lib/jobs/reminder/whoop-sync.ts b/src/lib/jobs/reminder/whoop-sync.ts index 429463e18..bf22df904 100644 --- a/src/lib/jobs/reminder/whoop-sync.ts +++ b/src/lib/jobs/reminder/whoop-sync.ts @@ -131,7 +131,7 @@ export async function runWhoopResourceSync( // sync tail the webhook / per-resource cron paths pass through — // `syncUserWhoop` covers the manual + backfill paths itself. invalidateUserMeasurements(userId); - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); } diff --git a/src/lib/jobs/reminder/withings-sync.ts b/src/lib/jobs/reminder/withings-sync.ts index af4447739..ca5de77a6 100644 --- a/src/lib/jobs/reminder/withings-sync.ts +++ b/src/lib/jobs/reminder/withings-sync.ts @@ -124,7 +124,7 @@ export async function handleWithingsFallbackSync( // v1.18.1 — a fresh reading landed; resolve this user's Vorsorge // reminders eventfully. Fire-and-forget; the cron is the net. if (result.imported > 0) { - fireAndForget(enqueueReminderSatisfy(connection.userId), { + fireAndForget(() => enqueueReminderSatisfy(connection.userId), { action: "reminder.satisfy.enqueue", }); } @@ -304,7 +304,7 @@ export async function handleWithingsActivitySync( imported, }); if (imported > 0) { - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); } @@ -377,7 +377,7 @@ export async function handleWithingsSleepSync( imported, }); if (imported > 0) { - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); } diff --git a/src/lib/logging/__tests__/detached.test.ts b/src/lib/logging/__tests__/detached.test.ts new file mode 100644 index 000000000..0166e3dd4 --- /dev/null +++ b/src/lib/logging/__tests__/detached.test.ts @@ -0,0 +1,82 @@ +/** + * Detached work keeps its hands off the action of the event it was started + * from, and keeps everything else it may do there. + */ +import { describe, expect, it } from "vitest"; + +import { withBackgroundEvent } from "../background"; +import { settleBackgroundTasks } from "../background-tasks"; +import { annotate, eventStorage, getEvent } from "../context"; +import { runDetached } from "../detached"; +import { WideEventBuilder } from "../event-builder"; +import { fireAndForget } from "../fire-and-forget"; + +describe("runDetached", () => { + it("drops the detached work's action for the parent event and keeps its meta", async () => { + const event = new WideEventBuilder(); + await eventStorage.run(event, async () => { + annotate({ action: { name: "measurement.create" } }); + await runDetached(async () => { + annotate({ + action: { name: "insights.status.invalidate.skipped" }, + meta: { reason: "no_provider" }, + }); + // The request's own event is still the one it reads. + expect(getEvent()).toBe(event); + }); + expect(event.toJSON().action?.name).toBe("measurement.create"); + // Back in the request, the request names its action as before. + annotate({ action: { name: "measurement.create.done" } }); + }); + expect(event.toJSON().action?.name).toBe("measurement.create.done"); + expect(event.toJSON().meta?.reason).toBe("no_provider"); + }); + + it("leaves an event the detached work opens for itself its own action", async () => { + const event = new WideEventBuilder(); + let inner: string | undefined; + await eventStorage.run(event, async () => { + annotate({ action: { name: "measurement.create" } }); + await runDetached(() => + withBackgroundEvent("test.inner", async (evt) => { + annotate({ action: { name: "test.inner.ran" } }); + inner = evt.toJSON().action?.name; + }), + ); + }); + expect(inner).toBe("test.inner.ran"); + expect(event.toJSON().action?.name).toBe("measurement.create"); + }); + + it("starts the work synchronously and turns a synchronous throw into a rejection", async () => { + let started = false; + const event = new WideEventBuilder(); + await eventStorage.run(event, async () => { + const pending = runDetached(async () => { + started = true; + }); + expect(started).toBe(true); + await pending; + await expect( + runDetached(() => { + throw new Error("sync"); + }), + ).rejects.toThrow("sync"); + }); + }); + + it("detaches what fireAndForget starts", async () => { + const event = new WideEventBuilder(); + await eventStorage.run(event, async () => { + annotate({ action: { name: "labs.create" } }); + fireAndForget( + async () => { + annotate({ action: { name: "reminder.satisfy.enqueue" } }); + }, + { action: "reminder.satisfy.enqueue" }, + ); + await settleBackgroundTasks(); + }); + expect(event.toJSON().action?.name).toBe("labs.create"); + }); +}); diff --git a/src/lib/logging/__tests__/fire-and-forget.test.ts b/src/lib/logging/__tests__/fire-and-forget.test.ts index a0fbb3e23..608151d68 100644 --- a/src/lib/logging/__tests__/fire-and-forget.test.ts +++ b/src/lib/logging/__tests__/fire-and-forget.test.ts @@ -12,7 +12,7 @@ afterEach(() => { describe("fireAndForget", () => { it("does not throw and does not reject when the promise resolves", async () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); - fireAndForget(Promise.resolve("ok"), { action: "test.noop.run" }); + fireAndForget(() => Promise.resolve("ok"), { action: "test.noop.run" }); await flush(); expect(warn).not.toHaveBeenCalled(); }); @@ -22,7 +22,7 @@ describe("fireAndForget", () => { const builder = new WideEventBuilder(); await eventStorage.run(builder, async () => { - fireAndForget(Promise.reject(new Error("boom")), { + fireAndForget(() => Promise.reject(new Error("boom")), { action: "reminder.satisfy.enqueue", meta: { userScope: "u_1" }, }); @@ -39,7 +39,7 @@ describe("fireAndForget", () => { it("warns with the action label only — never the error payload", async () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); - fireAndForget(Promise.reject(new Error("secret-token-xyz")), { + fireAndForget(() => Promise.reject(new Error("secret-token-xyz")), { action: "integration.reauth.mark", }); await flush(); @@ -52,7 +52,7 @@ describe("fireAndForget", () => { it("is a safe no-op outside a request context", async () => { vi.spyOn(console, "warn").mockImplementation(() => {}); expect(() => { - fireAndForget(Promise.reject(new Error("no context")), { + fireAndForget(() => Promise.reject(new Error("no context")), { action: "test.orphan.run", }); }).not.toThrow(); @@ -63,7 +63,7 @@ describe("fireAndForget", () => { vi.spyOn(console, "warn").mockImplementation(() => {}); const builder = new WideEventBuilder(); await eventStorage.run(builder, async () => { - fireAndForget(Promise.reject("plain-string"), { + fireAndForget(() => Promise.reject("plain-string"), { action: "test.reject.string", }); await flush(); diff --git a/src/lib/logging/context.ts b/src/lib/logging/context.ts index a7e51266b..82ea9fd4f 100644 --- a/src/lib/logging/context.ts +++ b/src/lib/logging/context.ts @@ -1,6 +1,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import type { WideEvent } from "./types"; import { WideEventBuilder } from "./event-builder"; +import { detachedParent, registerDetachedEventSource } from "./detached"; import { registerSourcePriorityParseObserver } from "@/lib/validations/source-priority"; import { registerRecurrenceParseErrorObserver } from "@/lib/medications/scheduling/recurrence"; import { @@ -16,6 +17,10 @@ export function getEvent(): WideEventBuilder | null { return eventStorage.getStore() ?? null; } +// `runDetached` (./detached) reads the current event through this getter; +// registered here so the two modules do not import each other. +registerDetachedEventSource(getEvent); + /** * Convenience: Felder im aktuellen Event annotieren. * Safe to call anywhere — no-op wenn kein Kontext aktiv. @@ -26,7 +31,10 @@ export function annotate(fields: { }): void { const evt = getEvent(); if (!evt) return; - if (fields.action) evt.setAction(fields.action); + // Detached work never renames the event it was started from. + if (fields.action && detachedParent() !== evt) { + evt.setAction(fields.action); + } if (fields.meta) { for (const [key, value] of Object.entries(fields.meta)) { evt.addMeta(key, value); diff --git a/src/lib/logging/detached.ts b/src/lib/logging/detached.ts new file mode 100644 index 000000000..7b241d6bb --- /dev/null +++ b/src/lib/logging/detached.ts @@ -0,0 +1,47 @@ +/** + * Start work the caller does not wait for (a status re-warm, a reminder + * enqueue, a stale-cache rebuild) so it cannot rename the event it was + * started from. + * + * Detached work still runs inside the request's context: it reads the + * request's auth for its audit rows and adds meta, warnings and external + * calls to the request's event while that event is open. What it must not do + * is set the event's action. Its `annotate({ action })` used to land on the + * request's event whenever it ran before the event was emitted, so the same + * write was logged as `measurement.create` on one request and as the + * re-warm's `insights.status.invalidate.skipped` on the next. Under + * `runDetached` an action from the detached work is dropped for the parent + * event (`annotate` in ./context checks `detachedParent`); an event the work + * opens for itself (`withBackgroundEvent`) is its own and keeps its action. + * + * Kept apart from ./context, which most unit tests replace wholesale: a test + * that mocks the context still gets a working `runDetached`, which then + * simply starts the work. + */ +import { AsyncLocalStorage } from "node:async_hooks"; + +/** The event a piece of detached work was started from. */ +const detachedFrom = new AsyncLocalStorage(); + +let currentEvent: () => object | null = () => null; + +/** Called once by ./context with its event getter. */ +export function registerDetachedEventSource(source: () => object | null): void { + currentEvent = source; +} + +/** The event the running work was detached from, if any. */ +export function detachedParent(): object | undefined { + return detachedFrom.getStore(); +} + +/** Start `start` detached from the current event; it runs synchronously. */ +export function runDetached(start: () => Promise): Promise { + const parent = currentEvent(); + if (!parent) return start(); + try { + return detachedFrom.run(parent, start); + } catch (error) { + return Promise.reject(error); + } +} diff --git a/src/lib/logging/fire-and-forget.ts b/src/lib/logging/fire-and-forget.ts index a6f06895c..6a45b2535 100644 --- a/src/lib/logging/fire-and-forget.ts +++ b/src/lib/logging/fire-and-forget.ts @@ -1,4 +1,5 @@ import { annotate } from "./context"; +import { runDetached } from "./detached"; import { trackBackgroundTask } from "./background-tasks"; /** @@ -22,23 +23,26 @@ import { trackBackgroundTask } from "./background-tasks"; * Only the caller-supplied action label — never the error text — reaches * the raw console sink, keeping it redaction-safe by construction. * - * The promise stays fire-and-forget: the caller does not await it and its - * resolution is discarded exactly as before. Under test the settled handle is - * registered with `trackBackgroundTask` so the suite setup can await it — see + * The work stays fire-and-forget: the caller does not await it and its + * resolution is discarded exactly as before. It is started through + * `runDetached`, so nothing it annotates can rename the caller's event; that + * is why it is passed as a function rather than as a promise already + * running. Under test the settled handle is registered with + * `trackBackgroundTask` so the suite setup can await it — see * `./background-tasks` for why a late `console.warn` is otherwise a teardown * hazard. * - * @param promise The background work. May already be running. + * @param start Starts the background work. * @param context.action A stable `..` label (never user * input) identifying which background task failed. * @param context.meta Optional extra fields to attach to the breadcrumb; they * ride the annotate/redact path with the error message. */ export function fireAndForget( - promise: Promise, + start: () => Promise, context: { action: string; meta?: Record }, ): void { - const settled = Promise.resolve(promise).catch((error: unknown) => { + const settled = runDetached(start).catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); annotate({ meta: { diff --git a/src/lib/measurements/batch-ingest/report.ts b/src/lib/measurements/batch-ingest/report.ts index 8cce1851e..d47ebe43c 100644 --- a/src/lib/measurements/batch-ingest/report.ts +++ b/src/lib/measurements/batch-ingest/report.ts @@ -201,7 +201,7 @@ export async function enqueueRecordJobs(args: { // Eventful Vorsorge satisfaction. A matching reading just landed; resolve // the user's reminders now rather than waiting on the 15-min cron. // Fire-and-forget — the cron reconciles on enqueue miss. - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); try { diff --git a/src/lib/measurements/create/create-readings.ts b/src/lib/measurements/create/create-readings.ts index 0ab7ea82d..c89a2fc5f 100644 --- a/src/lib/measurements/create/create-readings.ts +++ b/src/lib/measurements/create/create-readings.ts @@ -208,7 +208,7 @@ async function afterReadingsCreated( // Eventful Vorsorge satisfaction. Resolve the user's reminders against the // just-landed readings now. Fire-and-forget. - fireAndForget(enqueueReminderSatisfy(user.id), { + fireAndForget(() => enqueueReminderSatisfy(user.id), { action: "reminder.satisfy.enqueue", }); @@ -232,17 +232,18 @@ async function afterReadingsCreated( // symptom-coupled emergency copy. Fire-and-forget — a notification failure // never fails the write. fireAndForget( - runSafetyFloorCheck({ - userId: user.id, - written: rows.map((r) => ({ - type: r.type, - value: r.value, - measuredAt: r.measuredAt, - glucoseContext: r.glucoseContext, - })), - symptomsPresent: options.symptomsPresent, - timezone: user.timezone ?? undefined, - }), + () => + runSafetyFloorCheck({ + userId: user.id, + written: rows.map((r) => ({ + type: r.type, + value: r.value, + measuredAt: r.measuredAt, + glucoseContext: r.glucoseContext, + })), + symptomsPresent: options.symptomsPresent, + timezone: user.timezone ?? undefined, + }), { action: "measurement.safety_floor.check" }, ); diff --git a/src/lib/measurements/create/create-sleep-stage.ts b/src/lib/measurements/create/create-sleep-stage.ts index f42b235d9..79a7da6f8 100644 --- a/src/lib/measurements/create/create-sleep-stage.ts +++ b/src/lib/measurements/create/create-sleep-stage.ts @@ -147,7 +147,7 @@ export async function createNamedSleepStage( }, }); invalidateUserMeasurements(userId, { evict: true }); - fireAndForget(enqueueReminderSatisfy(userId), { + fireAndForget(() => enqueueReminderSatisfy(userId), { action: "reminder.satisfy.enqueue", }); if (status === "inserted") { diff --git a/src/lib/medications/create-medication.ts b/src/lib/medications/create-medication.ts index 4b9a370de..11162bfe6 100644 --- a/src/lib/medications/create-medication.ts +++ b/src/lib/medications/create-medication.ts @@ -10,33 +10,27 @@ import type { z } from "zod/v4"; import { prisma } from "@/lib/db"; -import type { Prisma } from "@/generated/prisma/client"; -import { annotate, getEvent } from "@/lib/logging/context"; +import { annotate } from "@/lib/logging/context"; import { auditLog } from "@/lib/auth/audit"; -import type { SessionUser } from "@/lib/auth/session-user"; -import { - courseRefusalResponse, - resolveCourseFields, -} from "@/lib/medications/courses"; +import { courseRefusalResponse } from "@/lib/medications/courses"; import { dateOfDayKey, dayKeyOfDate, validateCourses, } from "@/lib/medications/course-window"; import { DEFAULT_TIMEZONE, userDayKey } from "@/lib/tz/format"; -import { - isCustomMedicationCategoryKey, - type createMedicationSchema, -} from "@/lib/validations/medication"; +import type { createMedicationSchema } from "@/lib/validations/medication"; import { isAssignableCategory, - resolveMedicationCategories, setMedicationCategory, } from "@/lib/medication-category"; import { serializeScheduleRecurrence } from "@/lib/medication-schedule"; import { invalidateUserMedications } from "@/lib/cache/invalidate"; -import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto"; -import { scheduleWireFields } from "@/lib/medications/intake-tracking"; +import type { Medication, MedicationSchedule } from "@/generated/prisma/client"; +import { + medicationDetail, + type ReadUser, +} from "@/lib/medications/read-medication"; export type CreateMedicationInput = z.output; type ScheduleInput = NonNullable[number]; @@ -67,28 +61,14 @@ export type CreateMedicationOutcome = * EXISTING row (status 200, vs 201 for a fresh create) when the caller * re-posts a `(externalSource, externalId)` pair this user already mirrors — * the iOS re-sync replays its full HealthKit medication list, and a replay - * must neither duplicate nor 500. Response shape mirrors the fresh-create - * envelope (schedules included, category joined, Decimal unwrapped); no - * audit row and no cache eviction because nothing was written. + * must neither duplicate nor 500. It answers the medication's read model, + * the same body the fresh create and the detail read answer; no audit row + * and no cache eviction because nothing was written. */ async function existingMirrorBody( - medication: Record & { - id: string; - unitsPerDose: unknown; - trackIntake: boolean; - schedules: Array<{ unitsPerDose: Prisma.Decimal | null }>; - }, + user: ReadUser, + medication: Medication & { schedules: MedicationSchedule[] }, ): Promise { - let category = "OTHER"; - let categoryLabel: string | null = null; - try { - const categories = await resolveMedicationCategories([medication.id]); - category = categories[medication.id]?.category ?? "OTHER"; - categoryLabel = categories[medication.id]?.categoryLabel ?? null; - } catch { - getEvent()?.addWarning("Medication categories could not be loaded"); - } - annotate({ action: { name: "medication.create", @@ -97,25 +77,14 @@ async function existingMirrorBody( }, meta: { idempotent_replay: true }, }); - - return { - kind: "existing", - body: { - ...medication, - unitsPerDose: Number(medication.unitsPerDose), - // #219 — Decimal → number for the per-schedule column too. - // (#1033) — record-only medications ship `schedules: []`. - ...scheduleWireFields( - medication.trackIntake, - serializeScheduleUnitsPerDose( - medication.schedules, - Number(medication.unitsPerDose), - ), - ), - category, - categoryLabel, - }, - }; + // The read model, exactly as `GET /api/medications/{id}` answers it. + const body = await medicationDetail({ + user, + grantId: null, + medication, + announce: false, + }); + return { kind: "existing", body }; } /** @@ -127,16 +96,17 @@ async function existingMirrorBody( * create covers the concurrent race. Null when the create goes ahead. */ async function existingMirrorOrCap( - userId: string, + user: ReadUser, externalSource: NonNullable, externalId: string, ): Promise { + const userId = user.id; const existing = await prisma.medication.findFirst({ where: { userId, externalSource, externalId }, include: { schedules: true }, }); if (existing) { - return existingMirrorBody(existing); + return existingMirrorBody(user, existing); } // Growth guard. This point is only reached when the idempotency lookup @@ -438,7 +408,7 @@ function insertMedication( } export async function createMedication(input: { - user: Pick; + user: ReadUser; data: CreateMedicationInput; ipAddress: string | null; }): Promise { @@ -468,11 +438,7 @@ export async function createMedication(input: { } if (externalSource !== undefined && externalId !== undefined) { - const mirror = await existingMirrorOrCap( - user.id, - externalSource, - externalId, - ); + const mirror = await existingMirrorOrCap(user, externalSource, externalId); if (mirror) return mirror; } @@ -516,19 +482,12 @@ export async function createMedication(input: { where: { userId: user.id, externalSource, externalId }, include: { schedules: true }, }); - if (winner) return existingMirrorBody(winner); + if (winner) return existingMirrorBody(user, winner); } throw err; } - const normalizedCategory = await setMedicationCategory( - medication.id, - category, - ); - const categoryLabel = isCustomMedicationCategoryKey(normalizedCategory) - ? ((await resolveMedicationCategories([medication.id]))[medication.id] - ?.categoryLabel ?? null) - : null; + await setMedicationCategory(medication.id, category); await auditLog("medication.create", { userId: user.id, @@ -554,30 +513,12 @@ export async function createMedication(input: { // read reflects the new schedule. invalidateUserMedications(user.id, { evict: true }); - return { - kind: "created", - body: { - ...medication, - unitsPerDose: Number(medication.unitsPerDose), - // #219 — Decimal → number for the per-schedule column too. - // (#1033) — record-only medications ship `schedules: []`. - ...scheduleWireFields( - medication.trackIntake, - serializeScheduleUnitsPerDose( - medication.schedules, - medication.unitsPerDose, - ), - ), - category: normalizedCategory, - categoryLabel, - // (#1024) — the courses, as every medication read carries them. - ...( - await resolveCourseFields( - [medication], - new Date(), - user.timezone || DEFAULT_TIMEZONE, - ) - ).get(medication.id), - }, - }; + // The read model, exactly as `GET /api/medications/{id}` answers it. + const body = await medicationDetail({ + user, + grantId: null, + medication, + announce: false, + }); + return { kind: "created", body }; } diff --git a/src/lib/medications/read-medication.ts b/src/lib/medications/read-medication.ts index 9b1c02f85..248235e3e 100644 --- a/src/lib/medications/read-medication.ts +++ b/src/lib/medications/read-medication.ts @@ -36,7 +36,7 @@ import { resolveIntakeActionability } from "@/lib/medications/intake-actionable" import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; import { buildRelated } from "@/lib/links/related"; -type ReadUser = Pick; +export type ReadUser = Pick; type MedicationWithSchedules = Medication & { schedules: MedicationSchedule[]; }; @@ -195,6 +195,26 @@ export async function readMedication(input: { if (!medication || medication.userId !== user.id) { return null; } + return medicationDetail({ user, grantId, medication, announce: true }); +} + +/** + * The read model of a medication row in hand: the detail answer, and the + * answer of a create or an edit, which pass the row they just wrote so they + * answer exactly what `GET /api/medications/{id}` would. + */ +export async function medicationDetail(input: { + user: ReadUser; + grantId: string | null; + medication: MedicationWithSchedules; + /** + * Name the request's action `medication.get`. A write answering with the + * read model passes false and keeps its own action. + */ + announce: boolean; +}): Promise> { + const { user, grantId, medication } = input; + const id = medication.id; let category = "OTHER"; let categoryLabel: string | null = null; @@ -208,13 +228,15 @@ export async function readMedication(input: { const { display, userTz, now } = await loadDisplayDue(user, id, medication); - annotate({ - action: { - name: "medication.get", - entity_type: "medication", - entity_id: id, - }, - }); + if (input.announce) { + annotate({ + action: { + name: "medication.get", + entity_type: "medication", + entity_id: id, + }, + }); + } const { schedulesDto, stockUnitsRemaining, stockDosesRemaining, runwayDays } = await loadStock(user, id, medication); diff --git a/src/lib/medications/scheduling/__tests__/apply-canonical-slot-write.test.ts b/src/lib/medications/scheduling/__tests__/apply-canonical-slot-write.test.ts index abfd20310..e08399a02 100644 --- a/src/lib/medications/scheduling/__tests__/apply-canonical-slot-write.test.ts +++ b/src/lib/medications/scheduling/__tests__/apply-canonical-slot-write.test.ts @@ -18,14 +18,27 @@ import { applyCanonicalSlotWrite, type SlotIntakeRow } from "../slot-upsert"; const SLOT = new Date("2026-06-15T05:00:00.000Z"); function row(overrides: Partial = {}): SlotIntakeRow { + const createdAt = new Date("2026-06-15T00:00:00.000Z"); return { id: "row-1", + userId: "user-1", + medicationId: "med-1", takenAt: null, skipped: false, + autoMissed: false, + autoRecorded: false, + attributionSource: "AUTO", idempotencyKey: null, scheduledFor: SLOT, source: "REMINDER", - createdAt: new Date("2026-06-15T00:00:00.000Z"), + createdAt, + injectionSite: null, + doseTaken: null, + inventoryConsumption: null, + externalId: null, + updatedAt: createdAt, + syncVersion: 0, + deletedAt: null, ...overrides, }; } diff --git a/src/lib/medications/scheduling/slot-upsert.ts b/src/lib/medications/scheduling/slot-upsert.ts index 69faf5844..13a791050 100644 --- a/src/lib/medications/scheduling/slot-upsert.ts +++ b/src/lib/medications/scheduling/slot-upsert.ts @@ -14,7 +14,10 @@ * (PRN / off-slot beyond tolerance / cyclic off-week / no schedules) — * the caller's signal to keep the unmodified insert behaviour. */ -import type { PrismaClient } from "@/generated/prisma/client"; +import type { + MedicationIntakeEvent, + PrismaClient, +} from "@/generated/prisma/client"; import { prisma as defaultPrisma } from "@/lib/db"; import { isP2002 } from "@/lib/prisma-errors"; @@ -30,16 +33,12 @@ import type { InjectionSiteKey } from "@/lib/medications/injection-sites"; type PrismaLike = Pick; -/** The minimal intake-row shape the slot upsert reads + returns. */ -export interface SlotIntakeRow { - id: string; - takenAt: Date | null; - skipped: boolean; - idempotencyKey: string | null; - scheduledFor: Date; - source: string; - createdAt: Date; -} +/** + * The intake row the slot upsert reads and returns: the whole row, so a + * write that converged onto a slot answers the same event shape as one that + * inserted its own (the declared `MedicationIntakeEvent`). + */ +export type SlotIntakeRow = MedicationIntakeEvent; export interface ApplyCanonicalSlotWriteInput { client: PrismaLike; @@ -151,16 +150,6 @@ export interface ApplyCanonicalSlotWriteResult { outcomeChangeRefused: boolean; } -const SLOT_ROW_SELECT = { - id: true, - takenAt: true, - skipped: true, - idempotencyKey: true, - scheduledFor: true, - source: true, - createdAt: true, -} as const; - /** * Forward grace a TAKEN write may still snap onto a slot whose anchor sits * ahead of the take — the same clock-skew / early-dose margin the resolver's @@ -239,7 +228,6 @@ async function findSlotRows( scheduledFor: canonicalSlot, deletedAt: null, }, - select: SLOT_ROW_SELECT, // H1 — deterministic order; actioned-preference applied in // `pickSlotRow`. orderBy: [{ createdAt: "asc" }, { id: "asc" }], @@ -311,7 +299,6 @@ export async function applyCanonicalSlotWrite( // v1.28 — external dose-event id only when the write carries one. ...(externalId !== null && { externalId }), }, - select: SLOT_ROW_SELECT, })) as SlotIntakeRow; return { row: created, @@ -453,7 +440,6 @@ async function applyToExisting( // never clears a previously-recorded id. ...(externalId !== null && { externalId }), }, - select: SLOT_ROW_SELECT, })) as SlotIntakeRow; return { diff --git a/src/lib/medications/update-medication/update-medication.ts b/src/lib/medications/update-medication/update-medication.ts index be7c9ba20..b698fb894 100644 --- a/src/lib/medications/update-medication/update-medication.ts +++ b/src/lib/medications/update-medication/update-medication.ts @@ -7,6 +7,10 @@ * everything after it run here, in the order they always ran. The outcome is * a refusal the route answers with `apiError`, or the edited medication. */ +import { + medicationDetail, + type ReadUser, +} from "@/lib/medications/read-medication"; import { prisma } from "@/lib/db"; import { enqueueSupplementNutrientRecompute, @@ -14,7 +18,6 @@ import { } from "@/lib/medications/ingredients/recompute-job"; import { annotate } from "@/lib/logging/context"; import { auditLog } from "@/lib/auth/audit"; -import type { SessionUser } from "@/lib/auth/session-user"; import { overwriteDetails } from "@/lib/sharing/audit-details"; import { invalidateUserMedications } from "@/lib/cache/invalidate"; import { @@ -25,11 +28,8 @@ import { import { CourseWriteError, courseRefusalResponse, - resolveCourseFields, } from "@/lib/medications/courses"; import { refreshDoseDay } from "@/lib/medications/dose-day-refresh"; -import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto"; -import { scheduleWireFields } from "@/lib/medications/intake-tracking"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; import { applyRecordOnlyRule, @@ -49,7 +49,7 @@ import { type MedicationEditWrite, } from "./write"; -type EditUser = Pick; +type EditUser = ReadUser; type EditedMedication = Awaited>; export type MedicationEditOutcome = @@ -238,7 +238,6 @@ async function writeCategory( } if (categoryToWrite !== undefined) await setMedicationCategory(id, categoryToWrite); - return (await resolveMedicationCategories([id]))[id]; } /** The audit row: C4 on the medication row itself, C7 on the schedule. */ @@ -336,12 +335,7 @@ export async function updateMedication(input: { plan.primaryScheduleGracePatch, ); - const resolvedCategory = await writeCategory( - id, - write.input.category, - input.authMethod, - ); - const normalizedCategory = resolvedCategory?.category ?? "OTHER"; + await writeCategory(id, write.input.category, input.authMethod); // A changed units-per-dose (on the medication or on a slot) changes what // every taken dose added, so a medication with a composition has its doses @@ -364,24 +358,12 @@ export async function updateMedication(input: { // read reflects the schedule change. invalidateUserMedications(user.id, { evict: true }); - return { - ok: true, - body: { - ...medication, - unitsPerDose: Number(medication.unitsPerDose), - ...scheduleWireFields( - medication.trackIntake, - serializeScheduleUnitsPerDose( - medication.schedules, - medication.unitsPerDose, - ), - ), - category: normalizedCategory, - categoryLabel: resolvedCategory?.categoryLabel ?? null, - // The courses, as every medication read carries them (#1024). - ...( - await resolveCourseFields([medication], new Date(), write.courseTz) - ).get(id), - }, - }; + // The read model, exactly as `GET /api/medications/{id}` answers it. + const body = await medicationDetail({ + user, + grantId: null, + medication, + announce: false, + }); + return { ok: true, body }; } diff --git a/src/lib/openapi/routes/medications/paths.ts b/src/lib/openapi/routes/medications/paths.ts index 8ebdf52e0..d5ef6928f 100644 --- a/src/lib/openapi/routes/medications/paths.ts +++ b/src/lib/openapi/routes/medications/paths.ts @@ -255,6 +255,13 @@ const medicationIntakeImportJobStatusEnvelope = dataEnvelope( "MedicationIntakeImportJobStatusEnvelope", ); +// Shared by the create's 201 and its mirror-replay 200: the envelope id is +// registered once. +const createMedicationResponse = dataEnvelope( + medicationDetailEntry, + "CreateMedicationResponse", +); + export const medicationPaths: NonNullable = { "/api/medications/intake": { get: { @@ -969,12 +976,14 @@ export const medicationPaths: NonNullable = { "201": { description: "Created medication with its schedules.", content: { - "application/json": { - schema: dataEnvelope( - medicationDetailEntry, - "CreateMedicationResponse", - ), - }, + "application/json": { schema: createMedicationResponse }, + }, + }, + "200": { + description: + "A mirror create re-posting an `(externalSource, externalId)` pair the caller already holds: the existing medication, nothing written.", + content: { + "application/json": { schema: createMedicationResponse }, }, }, ...stdResponses, diff --git a/src/lib/record-activity/activity-verb.ts b/src/lib/record-activity/activity-verb.ts index 1b37e5254..cbaae53b6 100644 --- a/src/lib/record-activity/activity-verb.ts +++ b/src/lib/record-activity/activity-verb.ts @@ -61,6 +61,8 @@ export function recordActivityVerbLine( action: string, name: string, ): string { + const refused = refusedAttemptVerb(t, action, name); + if (refused !== null) return refused; switch (action) { case "measurement.create": return t("recordSharing.activityVerb.measurementCreate", { name }); @@ -342,6 +344,54 @@ export function recordActivityVerbLine( case "health-record.export": return t("recordSharing.activityVerb.healthRecordExport", { name }); + /* ---------------------------------------------------------------- */ + /* Acts a delegable module performs outside its own route file. */ + /* */ + /* The two below are filed by library modules the routes import — a */ + /* background worker's completion row and a lifecycle reconciliation */ + /* — and a guard that walked only `route.ts` could not see either. */ + /* Both carry the actor through, so both reach the owner's feed. */ + /* ---------------------------------------------------------------- */ + + case "medication.intake.import": + return t("recordSharing.activityVerb.medicationIntakeImportFinished", { + name, + }); + case "medication.oneShot.reconciled": + return t("recordSharing.activityVerb.medicationOneShotReconciled", { + name, + }); + + case "anamnesis.fact.create": + return t("recordSharing.activityVerb.anamnesisFactCreate", { name }); + case "customMetric.create": + return t("recordSharing.activityVerb.customMetricCreate", { name }); + case "customMetric.update": + return t("recordSharing.activityVerb.customMetricUpdate", { name }); + case "customMetric.delete": + return t("recordSharing.activityVerb.customMetricDelete", { name }); + case "documents.inbound.store": + return t("recordSharing.activityVerb.documentStore", { name }); + case "documents.inbound.update": + return t("recordSharing.activityVerb.documentUpdate", { name }); + case "documents.inbound.discard": + return t("recordSharing.activityVerb.documentDiscard", { name }); + + default: + return t("recordSharing.activity.acted", { name }); + } +} + +/** + * The sentence for an attempt that was refused, or null when `action` is no + * refusal. Asked first by `recordActivityVerbLine`. + */ +function refusedAttemptVerb( + t: Translator, + action: string, + name: string, +): string | null { + switch (action) { /* ---------------------------------------------------------------- */ /* Attempts that were REFUSED, and things that happened around them. */ /* */ @@ -407,47 +457,23 @@ export function recordActivityVerbLine( return t("recordSharing.activityVerb.rejectedDoseImport", { name }); case "custom-metric.entry.create.validation-failed": return t("recordSharing.activityVerb.rejectedTrackedValue", { name }); + case "custom-metric.metric.create.validation-failed": + return t("recordSharing.activityVerb.rejectedTrackedValueTypeCreate", { + name, + }); + case "custom-metric.metric.update.validation-failed": + return t("recordSharing.activityVerb.rejectedTrackedValueTypeUpdate", { + name, + }); + case "medication.layout.validation-failed": + return t("recordSharing.activityVerb.rejectedMedicationLayout", { name }); // Not a validation failure: the request was well-formed and admitted, // and the queue would not take it. The owner sees an import that never // ran rather than one that was refused, because those need different // next steps. case "medication.intake.import.kickoff.denied": return t("recordSharing.activityVerb.deniedDoseImport", { name }); - - /* ---------------------------------------------------------------- */ - /* Acts a delegable module performs outside its own route file. */ - /* */ - /* The two below are filed by library modules the routes import — a */ - /* background worker's completion row and a lifecycle reconciliation */ - /* — and a guard that walked only `route.ts` could not see either. */ - /* Both carry the actor through, so both reach the owner's feed. */ - /* ---------------------------------------------------------------- */ - - case "medication.intake.import": - return t("recordSharing.activityVerb.medicationIntakeImportFinished", { - name, - }); - case "medication.oneShot.reconciled": - return t("recordSharing.activityVerb.medicationOneShotReconciled", { - name, - }); - - case "anamnesis.fact.create": - return t("recordSharing.activityVerb.anamnesisFactCreate", { name }); - case "customMetric.create": - return t("recordSharing.activityVerb.customMetricCreate", { name }); - case "customMetric.update": - return t("recordSharing.activityVerb.customMetricUpdate", { name }); - case "customMetric.delete": - return t("recordSharing.activityVerb.customMetricDelete", { name }); - case "documents.inbound.store": - return t("recordSharing.activityVerb.documentStore", { name }); - case "documents.inbound.update": - return t("recordSharing.activityVerb.documentUpdate", { name }); - case "documents.inbound.discard": - return t("recordSharing.activityVerb.documentDiscard", { name }); - default: - return t("recordSharing.activity.acted", { name }); + return null; } } diff --git a/src/lib/rollups/__tests__/after-measurement-mutation.test.ts b/src/lib/rollups/__tests__/after-measurement-mutation.test.ts index 17fd3ca91..5d330e882 100644 --- a/src/lib/rollups/__tests__/after-measurement-mutation.test.ts +++ b/src/lib/rollups/__tests__/after-measurement-mutation.test.ts @@ -41,6 +41,9 @@ vi.mock("@/lib/insights/status-invalidation", () => ({ })); import { afterMeasurementMutation } from "../after-measurement-mutation"; +import { annotate, eventStorage } from "@/lib/logging/context"; +import { WideEventBuilder } from "@/lib/logging/event-builder"; +import { settleBackgroundTasks } from "@/lib/logging/background-tasks"; import type { MeasurementType } from "@/generated/prisma/client"; beforeEach(() => { @@ -163,4 +166,35 @@ describe("afterMeasurementMutation", () => { warn.mockRestore(); } }); + + it("keeps the request's action name while the detached re-warm annotates its own", async () => { + // The re-warm is detached from the request but starts inside its + // context. It annotates an action of its own when it skips; that must + // never rename the write the request is reporting. + mocks.invalidate.mockImplementation(async () => { + annotate({ + action: { name: "insights.status.invalidate.skipped" }, + meta: { reason: "no_provider" }, + }); + }); + const event = new WideEventBuilder(); + await eventStorage.run(event, async () => { + annotate({ action: { name: "measurement.create" } }); + await afterMeasurementMutation( + "user-1", + [ + { + type: "WEIGHT" as MeasurementType, + measuredAt: day("2026-05-10T08:00:00Z"), + }, + ], + { source: "manual" }, + ); + await settleBackgroundTasks(); + }); + expect(mocks.invalidate).toHaveBeenCalledTimes(1); + expect(event.toJSON().action?.name).toBe("measurement.create"); + // What the detached work says still reaches the request's event. + expect(event.toJSON().meta?.reason).toBe("no_provider"); + }); }); diff --git a/src/lib/rollups/after-measurement-mutation.ts b/src/lib/rollups/after-measurement-mutation.ts index 6cd1f834e..1dfaa8955 100644 --- a/src/lib/rollups/after-measurement-mutation.ts +++ b/src/lib/rollups/after-measurement-mutation.ts @@ -37,6 +37,7 @@ import { type StatusIngestSource, } from "@/lib/insights/status-invalidation"; import { trackBackgroundTask } from "@/lib/logging/background-tasks"; +import { runDetached } from "@/lib/logging/detached"; import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; import { earliestMeasuredAt } from "@/lib/cache/closed-block"; import type { MeasurementType } from "@/generated/prisma/client"; @@ -83,9 +84,11 @@ export async function afterMeasurementMutation( // write on a detached handle, and one that lands while a vitest worker is // closing its RPC channel fails the whole run. No-op outside test. trackBackgroundTask( - invalidateStatusInsightsForTypes(userId, types, { - source: options.source, - }).catch((err) => { + runDetached(() => + invalidateStatusInsightsForTypes(userId, types, { + source: options.source, + }), + ).catch((err) => { console.warn(`[${label}] status-insight invalidate failed`, err); }), ); diff --git a/src/lib/rollups/mood-rollups.ts b/src/lib/rollups/mood-rollups.ts index 4dfe341e7..cf249bb05 100644 --- a/src/lib/rollups/mood-rollups.ts +++ b/src/lib/rollups/mood-rollups.ts @@ -49,6 +49,7 @@ import { prisma } from "@/lib/db"; import { getGlobalBoss } from "@/lib/jobs/boss-instance"; import { enqueueWhenBossReady } from "@/lib/jobs/boss-ready"; import { annotate } from "@/lib/logging/context"; +import { runDetached } from "@/lib/logging/detached"; import { trackBackgroundTask } from "@/lib/logging/background-tasks"; import { startOfUtcDay } from "@/lib/tz/start-of-utc-day"; import { @@ -318,25 +319,27 @@ export async function recomputeMoodBucketsForEntry( // write on a detached handle, and one that lands while a vitest worker is // closing its RPC channel fails the whole run. No-op outside test. trackBackgroundTask( - Promise.all( - ASYNC_GRANULARITIES.map((granularity) => { - const { from, to } = bucketSpan(dateLabel, granularity); - return enqueueMoodRollupRecompute({ - userId, - granularity, - from, - to, - }).catch((err: unknown) => { - const message = err instanceof Error ? err.message : String(err); - annotate({ - meta: { - mood_rollup_enqueue_failed: true, - mood_rollup_enqueue_error: message, - }, + runDetached(() => + Promise.all( + ASYNC_GRANULARITIES.map((granularity) => { + const { from, to } = bucketSpan(dateLabel, granularity); + return enqueueMoodRollupRecompute({ + userId, + granularity, + from, + to, + }).catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err); + annotate({ + meta: { + mood_rollup_enqueue_failed: true, + mood_rollup_enqueue_error: message, + }, + }); + console.error("[mood-rollups] async enqueue failed:", message); }); - console.error("[mood-rollups] async enqueue failed:", message); - }); - }), + }), + ), ), ); } diff --git a/src/lib/workouts/__tests__/route-svg.test.ts b/src/lib/workouts/__tests__/route-svg.test.ts index 0061b74c2..c582de0f5 100644 --- a/src/lib/workouts/__tests__/route-svg.test.ts +++ b/src/lib/workouts/__tests__/route-svg.test.ts @@ -150,6 +150,23 @@ describe("haversineMeters", () => { }); describe("buildGpxDocument", () => { + it("writes the instant of a stored sample object", () => { + const gpx = buildGpxDocument({ + coordinates: [ + [11.0, 50.0], + [11.001, 50.001], + ], + timestamps: [ + { t: "2026-05-15T07:00:00Z", hr: 120 }, + { t: "2026-05-15T07:00:05Z", speedMs: 3.1 }, + ], + sportType: "running", + startedAt: "2026-05-15T07:00:00Z", + }); + expect(gpx).toContain(""); + expect(gpx).toContain(""); + }); + it("emits trkpt elements with elevation and time when present", () => { const gpx = buildGpxDocument({ coordinates: [ diff --git a/src/lib/workouts/__tests__/splits.test.ts b/src/lib/workouts/__tests__/splits.test.ts index 6b335053a..48e9e6074 100644 --- a/src/lib/workouts/__tests__/splits.test.ts +++ b/src/lib/workouts/__tests__/splits.test.ts @@ -42,6 +42,16 @@ describe("computeSplits", () => { } }); + it("reads the stored sample shape, an instant with its channels", () => { + // Both ingests store `{ t, speedMs?, hr? }` per point; older tracks a + // bare instant. The two shapes time the same track identically. + const { coords, timestamps } = track(3200, 320, 960); + const samples = timestamps.map((t, i) => ({ t, hr: 120 + (i % 30) })); + const splits = computeSplits(coords, samples); + expect(splits).not.toBeNull(); + expect(splits).toEqual(computeSplits(coords, timestamps)); + }); + it("returns null without aligned timestamps", () => { const { coords } = track(3000, 300, 900); expect(computeSplits(coords, null)).toBeNull(); diff --git a/src/lib/workouts/detail.ts b/src/lib/workouts/detail.ts index ed1d6e026..126fe6943 100644 --- a/src/lib/workouts/detail.ts +++ b/src/lib/workouts/detail.ts @@ -237,7 +237,7 @@ async function readEnrichment( : null; const splits = geometryCoords && Array.isArray(row.route?.sampleTimestamps) - ? computeSplits(geometryCoords, row.route.sampleTimestamps as string[]) + ? computeSplits(geometryCoords, row.route.sampleTimestamps) : null; // Sport context: the user's own last-180-days average for this diff --git a/src/lib/workouts/route-samples.ts b/src/lib/workouts/route-samples.ts new file mode 100644 index 000000000..11d403518 --- /dev/null +++ b/src/lib/workouts/route-samples.ts @@ -0,0 +1,17 @@ +/** + * One point of a stored route's `sampleTimestamps`, aligned with the + * geometry's coordinates. Both ingests store `{ t, speedMs?, hr? }`; tracks + * written before the per-point channels carry a bare ISO instant. Every + * reader goes through `routeSampleInstant` so neither shape is skipped. + */ +export type RouteSample = string | { t: string; speedMs?: number; hr?: number }; + +/** The ISO instant of one sample, or null when it carries none. */ +export function routeSampleInstant(sample: unknown): string | null { + if (typeof sample === "string") return sample.length > 0 ? sample : null; + if (typeof sample === "object" && sample !== null && "t" in sample) { + const t = (sample as { t: unknown }).t; + return typeof t === "string" && t.length > 0 ? t : null; + } + return null; +} diff --git a/src/lib/workouts/route-svg.ts b/src/lib/workouts/route-svg.ts index 2c4d29952..c07e455c4 100644 --- a/src/lib/workouts/route-svg.ts +++ b/src/lib/workouts/route-svg.ts @@ -16,6 +16,7 @@ * correction squashes the longitude axis by `cos(midLat)` so one screen * unit east equals one screen unit north in real metres. */ +import { routeSampleInstant } from "@/lib/workouts/route-samples"; export type RouteCoordinate = [number, number] | [number, number, number] | number[]; @@ -323,7 +324,8 @@ function escapeXml(value: string): string { */ export function buildGpxDocument(params: { coordinates: readonly RouteCoordinate[]; - timestamps?: readonly string[] | null; + /** The stored samples: an instant each, or `{ t, … }` (`RouteSample`). */ + timestamps?: readonly unknown[] | null; sportType: string; startedAt: string; }): string { @@ -336,8 +338,8 @@ export function buildGpxDocument(params: { if (!isFiniteNumber(lon) || !isFiniteNumber(lat)) return; const parts = [``]; if (c.length >= 3 && isFiniteNumber(c[2])) parts.push(`${c[2]}`); - const ts = timestamps?.[i]; - if (typeof ts === "string" && ts.length > 0) { + const ts = routeSampleInstant(timestamps?.[i]); + if (ts) { parts.push(``); } parts.push(``); diff --git a/src/lib/workouts/splits.ts b/src/lib/workouts/splits.ts index 3a8377c1b..6e353647d 100644 --- a/src/lib/workouts/splits.ts +++ b/src/lib/workouts/splits.ts @@ -14,6 +14,7 @@ import { haversineMeters, type RouteCoordinate, } from "@/lib/workouts/route-svg"; +import { routeSampleInstant } from "@/lib/workouts/route-samples"; export interface WorkoutSplit { /** 1-based kilometre index. */ @@ -34,7 +35,8 @@ const KM = 1000; */ export function computeSplits( coordinates: readonly RouteCoordinate[], - timestamps: readonly string[] | null | undefined, + /** The stored samples: an instant each, or `{ t, … }` (`RouteSample`). */ + timestamps: readonly unknown[] | null | undefined, ): WorkoutSplit[] | null { if (!timestamps || timestamps.length !== coordinates.length) return null; @@ -43,7 +45,7 @@ export function computeSplits( coordinates.forEach((c, i) => { const [lonLat] = extractLonLat([c]); if (!lonLat) return; - const tMs = Date.parse(timestamps[i] ?? ""); + const tMs = Date.parse(routeSampleInstant(timestamps[i]) ?? ""); if (!Number.isFinite(tMs)) return; pts.push({ lon: lonLat[0], lat: lonLat[1], tMs }); }); diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap index a0924a58b..6e6417972 100644 --- a/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap @@ -1,23 +1,23 @@ { - "status": 500, + "status": 422, "body": { "data": null, - "error": "Interner Serverfehler", + "error": "No AI provider configured. Connect ChatGPT in settings or ask your admin to set up an API key.", "meta": { - "errorCode": "http.internal", + "errorCode": "ai.provider.none", "requestId": "" } }, "events": [ { - "status": 500, + "status": 422, "action": null, "meta": null, - "warnings": null, - "error": { - "type": "TypeError", - "message": "Invalid authentication tag length: 0" - } + "warnings": [ + "ai: a stored provider key could not be opened", + "ai: a stored provider key could not be opened" + ], + "error": null } ], "effects": [], diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap index 646d8ce72..5a2a4783c 100644 --- a/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap @@ -14,6 +14,19 @@ "confidence": "hoch" }, "disclaimer": "Consult your doctor.", + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, "briefingGeneratedAt": "anchor+0d+12:00:00" }, "cached": false, @@ -222,6 +235,19 @@ "confidence": "hoch" }, "disclaimer": "Consult your doctor.", + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, "briefingGeneratedAt": "anchor+0d+12:00:00" }, "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", 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 caac60735..a51c9f9c7 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 @@ -83,7 +83,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-15T18:00:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 9253f75d6..633798338 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 76cbe084b..798ac25e4 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 9592315ae..b2c594dfe 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 58c4631c6..341c62676 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": false, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 80d5f956d..eb29dbaf6 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 f960f6d45..d9b77b14f 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 41c112d3d..1aff35567 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 e537872e2..d6d69d3b3 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 aa42bd9a1..8298a4f74 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 812cf0903..b8c24ee3e 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 @@ -63,7 +63,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": null, + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": false, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 152c79133..fb5603aae 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 @@ -62,7 +62,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T05:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 160f86063..725e80fbf 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 @@ -63,7 +63,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": null, + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": false, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 18bea73e4..fe0b6e967 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 @@ -62,7 +62,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T06:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 f1c85281a..582f4f13f 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 @@ -40,7 +40,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": null, + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 7ca869539..0560579b9 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 @@ -71,7 +71,17 @@ ], "courseCount": 1, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-20T07:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "UPCOMING", + "intakeActionable": false, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 e89aacbc9..7d472d215 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 @@ -79,7 +79,17 @@ ], "courseCount": 2, "previousCourseEndedOn": "2026-08-31", - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T06:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 50d79355e..02ed0c383 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 @@ -62,7 +62,27 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-17T07:00:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": 3, + "stockDosesRemaining": 1, + "runwayDays": 10, + "ingredients": [ + { + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "labelKey": "nutrients.names.vitamin_d", + "label": "Vitamin D" + } + ], + "related": [] }, "error": null }, 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 46f368ca1..ee901103c 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 @@ -89,7 +89,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T09:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 4d2dad010..8d502393a 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 @@ -30,7 +30,8 @@ }, "meta": { "write_bucket": "own", - "issue_count": 2 + "issue_count": 2, + "rejected_attempt": "medications.intake.bulk.validation-failed" }, "warnings": null, "error": null @@ -584,24 +585,6 @@ "skipped": 7 }, "ipAddress": null - }, - { - "action": "medications.intake.bulk.validation-failed", - "userId": "", - "actorUserId": null, - "details": { - "issues": [ - { - "path": "entries.0.medicationId", - "code": "too_small" - }, - { - "path": "entries.0.takenAt", - "code": "invalid_format" - } - ] - }, - "ipAddress": null } ], "idempotency": [] 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 4249b41f7..1b576c2b2 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 @@ -567,24 +567,6 @@ "skipped": 7 }, "ipAddress": null - }, - { - "action": "medications.intake.bulk.validation-failed", - "userId": "", - "actorUserId": null, - "details": { - "issues": [ - { - "path": "entries.0.medicationId", - "code": "too_small" - }, - { - "path": "entries.0.takenAt", - "code": "invalid_format" - } - ] - }, - "ipAddress": null } ], "idempotency": [] 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 6656a318d..7f14348a3 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 @@ -662,24 +662,6 @@ "skipped": 7 }, "ipAddress": null - }, - { - "action": "medications.intake.bulk.validation-failed", - "userId": "", - "actorUserId": null, - "details": { - "issues": [ - { - "path": "entries.0.medicationId", - "code": "too_small" - }, - { - "path": "entries.0.takenAt", - "code": "invalid_format" - } - ] - }, - "ipAddress": null } ], "idempotency": [] 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 216e2e279..8b38a8d26 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 @@ -617,24 +617,6 @@ "skipped": 7 }, "ipAddress": null - }, - { - "action": "medications.intake.bulk.validation-failed", - "userId": "", - "actorUserId": null, - "details": { - "issues": [ - { - "path": "entries.0.medicationId", - "code": "too_small" - }, - { - "path": "entries.0.takenAt", - "code": "invalid_format" - } - ] - }, - "ipAddress": null } ], "idempotency": [] 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 ef83ea41b..e30efa5ac 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 @@ -619,24 +619,6 @@ "skipped": 7 }, "ipAddress": null - }, - { - "action": "medications.intake.bulk.validation-failed", - "userId": "", - "actorUserId": null, - "details": { - "issues": [ - { - "path": "entries.0.medicationId", - "code": "too_small" - }, - { - "path": "entries.0.takenAt", - "code": "invalid_format" - } - ] - }, - "ipAddress": null } ], "idempotency": [] 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 8fff49289..06466d200 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 @@ -617,24 +617,6 @@ "skipped": 7 }, "ipAddress": null - }, - { - "action": "medications.intake.bulk.validation-failed", - "userId": "", - "actorUserId": null, - "details": { - "issues": [ - { - "path": "entries.0.medicationId", - "code": "too_small" - }, - { - "path": "entries.0.takenAt", - "code": "invalid_format" - } - ] - }, - "ipAddress": null } ], "idempotency": [] 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 cef0085b9..6136a16fc 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-14T20:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -18,7 +30,7 @@ "action": { "name": "medication.intake.update", "entity_type": "intake_event", - "entity_id": "" + "entity_id": "" }, "meta": { "medication_id": "" @@ -32,7 +44,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -42,7 +54,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "Parity one-shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -76,7 +88,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Parity tablet", "dose": "10mg", "treatmentClass": "GENERIC", @@ -171,7 +183,7 @@ "intakes": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-10T09:00:00.000Z", "takenAt": "2026-10-10T09:30:00.000Z", @@ -192,7 +204,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": "2026-10-11T08:20:00.000Z", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:20:00.000Z", @@ -234,7 +246,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:05:00.000Z", @@ -255,7 +267,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-13T08:40:00.000Z", @@ -276,7 +288,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -296,8 +308,8 @@ "deletedAt": null }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T08:10:00.000Z", @@ -318,7 +330,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", @@ -342,7 +354,7 @@ "inventoryItems": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "state": "IN_USE", "containerType": "OTHER", @@ -366,10 +378,10 @@ "audit": [ { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "eventId": "", + "eventId": "", "medicationId": "", "fields": [ "scheduledFor", @@ -384,7 +396,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -400,7 +412,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", 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 292c4f3a2..554560af1 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-12T20:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -18,7 +30,7 @@ "action": { "name": "medication.intake.update", "entity_type": "intake_event", - "entity_id": "" + "entity_id": "" }, "meta": { "medication_id": "" @@ -32,7 +44,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -42,7 +54,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "Parity one-shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -76,7 +88,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Parity tablet", "dose": "10mg", "treatmentClass": "GENERIC", @@ -171,7 +183,7 @@ "intakes": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-10T09:00:00.000Z", "takenAt": "2026-10-10T09:30:00.000Z", @@ -192,7 +204,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": "2026-10-11T08:20:00.000Z", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:20:00.000Z", @@ -234,7 +246,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:05:00.000Z", @@ -255,7 +267,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -276,7 +288,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-13T08:40:00.000Z", @@ -296,8 +308,8 @@ "deletedAt": null }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -318,7 +330,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T08:10:00.000Z", @@ -339,7 +351,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", @@ -363,7 +375,7 @@ "inventoryItems": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "state": "IN_USE", "containerType": "OTHER", @@ -387,7 +399,7 @@ "audit": [ { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -405,10 +417,10 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "eventId": "", + "eventId": "", "medicationId": "", "fields": [ "scheduledFor" @@ -421,7 +433,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -437,7 +449,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", 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 99fba82d2..a0661d19b 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-13T20:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -18,7 +30,7 @@ "action": { "name": "medication.intake.update", "entity_type": "intake_event", - "entity_id": "" + "entity_id": "" }, "meta": { "medication_id": "" @@ -32,7 +44,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -42,7 +54,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "Parity one-shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -76,7 +88,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Parity tablet", "dose": "10mg", "treatmentClass": "GENERIC", @@ -171,7 +183,7 @@ "intakes": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-10T09:00:00.000Z", "takenAt": "2026-10-10T09:30:00.000Z", @@ -192,7 +204,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": "2026-10-11T08:20:00.000Z", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:20:00.000Z", @@ -234,7 +246,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:05:00.000Z", @@ -254,8 +266,8 @@ "deletedAt": "2026-10-12T09:00:00.000Z" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -276,7 +288,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-13T08:40:00.000Z", @@ -297,7 +309,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -318,7 +330,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -339,7 +351,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T08:10:00.000Z", @@ -360,7 +372,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", @@ -384,7 +396,7 @@ "inventoryItems": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "state": "IN_USE", "containerType": "OTHER", @@ -408,7 +420,7 @@ "audit": [ { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -426,10 +438,10 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "eventId": "", + "eventId": "", "medicationId": "", "fields": [ "scheduledFor" @@ -442,7 +454,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -458,7 +470,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -474,7 +486,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", 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 8ca547dfa..8af962b7e 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T08:45:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-14T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -18,7 +30,7 @@ "action": { "name": "medication.intake.update", "entity_type": "intake_event", - "entity_id": "" + "entity_id": "" }, "meta": { "medication_id": "" @@ -32,7 +44,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -42,7 +54,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "Parity one-shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -76,7 +88,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Parity tablet", "dose": "10mg", "treatmentClass": "GENERIC", @@ -171,7 +183,7 @@ "intakes": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-10T09:00:00.000Z", "takenAt": "2026-10-10T09:30:00.000Z", @@ -192,7 +204,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": "2026-10-11T08:20:00.000Z", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:20:00.000Z", @@ -234,7 +246,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:05:00.000Z", @@ -255,7 +267,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -276,7 +288,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-13T08:40:00.000Z", @@ -297,7 +309,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -318,7 +330,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:15:00.000Z", @@ -339,7 +351,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T08:10:00.000Z", @@ -360,7 +372,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T08:45:00.000Z", @@ -380,8 +392,8 @@ "deletedAt": null }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", @@ -405,7 +417,7 @@ "inventoryItems": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "state": "IN_USE", "containerType": "OTHER", @@ -429,7 +441,7 @@ "audit": [ { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -447,10 +459,10 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "eventId": "", + "eventId": "", "medicationId": "", "fields": [ "scheduledFor", @@ -465,7 +477,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -481,7 +493,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -497,7 +509,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", @@ -513,7 +525,7 @@ }, { "action": "medication.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "eventId": "", 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 d6d98517a..3fa6e304b 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-14T20:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -19,13 +31,13 @@ "name": "medications.intake.update" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 0, - "intakeId": "", + "intakeId": "", "status": "taken" }, "warnings": null, @@ -37,7 +49,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -45,7 +57,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -53,9 +65,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -64,8 +76,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "badgeCount": 2 } @@ -76,7 +88,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "Daily pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -113,7 +125,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -147,7 +159,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Three times", "dose": "10mg", "treatmentClass": "GENERIC", @@ -180,8 +192,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -259,7 +271,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -339,7 +351,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -364,8 +376,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-09T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -385,8 +397,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-10T08:00:00.000Z", "takenAt": "2026-10-10T08:05:00.000Z", "skipped": false, @@ -406,8 +418,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -427,8 +439,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:10:00.000Z", "skipped": false, @@ -448,7 +460,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T07:00:00.000Z", "takenAt": "2026-10-13T07:05:00.000Z", @@ -469,8 +481,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": null, "skipped": false, @@ -490,7 +502,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T07:00:00.000Z", "takenAt": null, @@ -510,9 +522,9 @@ "deletedAt": null }, { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -532,8 +544,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", "skipped": false, @@ -548,7 +560,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -558,7 +570,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T06:00:00.000Z", "takenAt": null, @@ -579,8 +591,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": "2026-10-15T08:30:00.000Z", "skipped": false, @@ -595,7 +607,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -605,7 +617,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T09:00:00.000Z", "takenAt": null, @@ -626,7 +638,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T10:00:00.000Z", "takenAt": "2026-10-15T10:05:00.000Z", @@ -647,7 +659,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T13:00:00.000Z", "takenAt": null, @@ -668,8 +680,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": false, @@ -691,9 +703,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -716,7 +728,7 @@ "audit": [ { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "intakeId": "", @@ -726,10 +738,10 @@ }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "intakeId": "", + "intakeId": "", "status": "taken" }, "ipAddress": null 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 666d31663..3b10a3bff 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-13T11:00:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-13T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -19,13 +31,13 @@ "name": "medications.intake.update" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 0, - "intakeId": "", + "intakeId": "", "status": "taken" }, "warnings": null, @@ -37,7 +49,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -45,7 +57,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -53,9 +65,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -64,8 +76,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "badgeCount": 2 } @@ -76,7 +88,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "Daily pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -113,7 +125,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -147,7 +159,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Three times", "dose": "10mg", "treatmentClass": "GENERIC", @@ -180,8 +192,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -259,7 +271,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -339,7 +351,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -364,8 +376,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-09T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -385,8 +397,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-10T08:00:00.000Z", "takenAt": "2026-10-10T08:05:00.000Z", "skipped": false, @@ -406,8 +418,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -427,8 +439,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:10:00.000Z", "skipped": false, @@ -448,7 +460,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T07:00:00.000Z", "takenAt": "2026-10-13T07:05:00.000Z", @@ -469,8 +481,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-13T11:00:00.000Z", "skipped": false, @@ -485,7 +497,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -494,9 +506,9 @@ "deletedAt": null }, { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": null, "skipped": false, @@ -516,7 +528,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T07:00:00.000Z", "takenAt": null, @@ -537,8 +549,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -558,8 +570,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", "skipped": false, @@ -574,7 +586,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -584,7 +596,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T06:00:00.000Z", "takenAt": null, @@ -605,8 +617,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": "2026-10-15T08:30:00.000Z", "skipped": false, @@ -621,7 +633,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -631,7 +643,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T09:00:00.000Z", "takenAt": null, @@ -652,7 +664,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T10:00:00.000Z", "takenAt": "2026-10-15T10:05:00.000Z", @@ -673,7 +685,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T13:00:00.000Z", "takenAt": null, @@ -694,8 +706,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": false, @@ -717,9 +729,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -742,7 +754,7 @@ "audit": [ { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "intakeId": "", @@ -752,7 +764,7 @@ }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "intakeId": "", @@ -762,10 +774,10 @@ }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "intakeId": "", + "intakeId": "", "status": "taken" }, "ipAddress": null 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 bd0c5ac92..a7a6cb365 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 @@ -3,12 +3,29 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": "", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-15T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "REMINDER", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -19,7 +36,7 @@ "name": "medications.intake.update" }, "meta": { - "intakeId": "", + "intakeId": "", "status": "taken" }, "warnings": null, @@ -31,7 +48,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -39,7 +56,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -47,9 +64,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -58,8 +75,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "badgeCount": 2 } @@ -69,8 +86,8 @@ "stored": { "medications": [ { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Daily pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -106,8 +123,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -140,8 +157,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Three times", "dose": "10mg", "treatmentClass": "GENERIC", @@ -174,8 +191,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -212,8 +229,8 @@ ], "schedules": [ { - "id": "", - "medicationId": "", + "id": "", + "medicationId": "", "windowStart": "06:00", "windowEnd": "07:00", "label": null, @@ -232,8 +249,8 @@ "doseWindows": null }, { - "id": "", - "medicationId": "", + "id": "", + "medicationId": "", "windowStart": "07:00", "windowEnd": "08:00", "label": null, @@ -252,8 +269,8 @@ "doseWindows": null }, { - "id": "", - "medicationId": "", + "id": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -272,8 +289,8 @@ "doseWindows": null }, { - "id": "", - "medicationId": "", + "id": "", + "medicationId": "", "windowStart": "09:00", "windowEnd": "11:00", "label": null, @@ -292,8 +309,8 @@ "doseWindows": null }, { - "id": "", - "medicationId": "", + "id": "", + "medicationId": "", "windowStart": "10:00", "windowEnd": "11:00", "label": null, @@ -312,8 +329,8 @@ "doseWindows": null }, { - "id": "", - "medicationId": "", + "id": "", + "medicationId": "", "windowStart": "13:00", "windowEnd": "14:00", "label": null, @@ -332,8 +349,8 @@ "doseWindows": null }, { - "id": "", - "medicationId": "", + "id": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -357,9 +374,9 @@ "courses": [], "intakes": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-09T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -378,9 +395,9 @@ "deletedAt": "2026-10-09T09:00:00.000Z" }, { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-10T08:00:00.000Z", "takenAt": "2026-10-10T08:05:00.000Z", "skipped": false, @@ -399,9 +416,9 @@ "deletedAt": null }, { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -420,9 +437,9 @@ "deletedAt": null }, { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -441,9 +458,9 @@ "deletedAt": null }, { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T07:00:00.000Z", "takenAt": "2026-10-13T07:20:00.000Z", "skipped": false, @@ -462,9 +479,9 @@ "deletedAt": null }, { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-13T11:00:00.000Z", "skipped": false, @@ -479,7 +496,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -489,8 +506,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": null, "skipped": false, @@ -510,8 +527,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T07:00:00.000Z", "takenAt": null, "skipped": false, @@ -531,8 +548,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -552,8 +569,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:30:00.000Z", "skipped": false, @@ -568,7 +585,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -578,8 +595,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T06:00:00.000Z", "takenAt": null, "skipped": false, @@ -599,8 +616,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": "", "skipped": false, @@ -615,7 +632,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -625,8 +642,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T09:00:00.000Z", "takenAt": "2026-10-15T09:15:00.000Z", "skipped": false, @@ -646,8 +663,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T10:00:00.000Z", "takenAt": "2026-10-15T10:05:00.000Z", "skipped": false, @@ -667,8 +684,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T13:00:00.000Z", "takenAt": null, "skipped": true, @@ -687,9 +704,9 @@ "deletedAt": null }, { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": false, @@ -711,9 +728,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -736,47 +753,47 @@ "audit": [ { "action": "medications.intake.update", - "userId": "", - "actorUserId": "", + "userId": "", + "actorUserId": "", "details": { - "intakeId": "", + "intakeId": "", "status": "taken" }, "ipAddress": null }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "intakeId": "", + "intakeId": "", "status": "skipped" }, "ipAddress": null }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "intakeId": "", + "intakeId": "", "status": "snoozed" }, "ipAddress": null }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "intakeId": "", + "intakeId": "", "status": "snoozed" }, "ipAddress": null }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "intakeId": "", @@ -786,7 +803,7 @@ }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "intakeId": "", @@ -796,7 +813,7 @@ }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "intakeId": "", @@ -806,17 +823,17 @@ }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "intakeId": "", + "intakeId": "", "status": "taken" }, "ipAddress": null }, { "action": "medications.intake.update", - "userId": "", + "userId": "", "actorUserId": null, "details": { "intakeId": "", 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 b77e18c79..beb3a4a7a 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": "2026-10-15T08:30:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-15T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "REMINDER", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,10 +33,10 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 1, "skipped": false @@ -38,7 +50,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -46,7 +58,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -54,9 +66,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -65,8 +77,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "badgeCount": 2 } @@ -77,7 +89,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -111,7 +123,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -145,7 +157,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -178,8 +190,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -272,7 +284,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -332,7 +344,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -357,8 +369,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -378,8 +390,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -399,8 +411,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -420,8 +432,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": "2026-10-15T08:30:00.000Z", "skipped": false, @@ -436,7 +448,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -448,9 +460,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -473,10 +485,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, 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 ad845eda4..3187012c4 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, - "idempotencyKey": null, - "scheduledFor": "2026-10-15T20:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,7 +33,7 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "skipped": true }, "warnings": null, @@ -33,7 +45,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -41,7 +53,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -49,9 +61,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "skipped" } ] @@ -60,8 +72,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "badgeCount": 2 } @@ -72,7 +84,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -106,7 +118,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -140,7 +152,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -173,8 +185,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -208,7 +220,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -267,7 +279,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -327,7 +339,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -352,8 +364,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -373,8 +385,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -394,8 +406,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -415,8 +427,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": "2026-10-15T08:30:00.000Z", "skipped": false, @@ -441,7 +453,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": "2026-10-15T11:00:00.000Z", @@ -462,8 +474,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -486,8 +498,8 @@ "inventoryItems": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -510,10 +522,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -521,7 +533,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -532,10 +544,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": 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 e7d80ad2d..4a8993e7b 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 @@ -3,12 +3,29 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, - "idempotencyKey": null, - "scheduledFor": "2026-10-15T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "REMINDER", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,10 +38,10 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "skipped": true }, @@ -37,7 +54,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -45,7 +62,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -53,9 +70,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "skipped" } ] @@ -64,8 +81,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "badgeCount": 2 } @@ -76,7 +93,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -110,7 +127,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -144,7 +161,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -177,8 +194,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -212,7 +229,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -271,7 +288,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -331,7 +348,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -356,8 +373,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -377,8 +394,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -398,8 +415,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -419,8 +436,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -440,7 +457,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": "2026-10-15T11:00:00.000Z", @@ -461,8 +478,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -484,9 +501,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -509,10 +526,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -520,7 +537,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -531,10 +548,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -542,10 +559,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": 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 ea57265ca..b10338d3a 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, "skipped": true, - "idempotencyKey": null, - "scheduledFor": "2026-10-15T11:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,7 +33,7 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "skipped": true }, "warnings": null, @@ -33,7 +45,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -41,7 +53,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -49,9 +61,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "skipped" } ] @@ -60,8 +72,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "badgeCount": 2 } @@ -71,8 +83,8 @@ "stored": { "medications": [ { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -106,7 +118,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -140,7 +152,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -174,7 +186,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -208,7 +220,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -352,7 +364,7 @@ "intakes": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, @@ -373,7 +385,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": null, @@ -394,7 +406,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", @@ -415,7 +427,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, @@ -436,8 +448,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, "skipped": true, @@ -457,7 +469,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, @@ -481,7 +493,7 @@ "inventoryItems": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "state": "IN_USE", "containerType": "OTHER", @@ -505,7 +517,7 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -516,10 +528,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -527,7 +539,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -538,7 +550,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -549,10 +561,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -560,10 +572,10 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { - "medicationId": "", + "medicationId": "", "reason": "resolved_event" }, "ipAddress": null 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 b2b4705c1..56d4d628a 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-14T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,10 +33,10 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 0, "skipped": false @@ -38,7 +50,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -46,7 +58,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -54,9 +66,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -65,8 +77,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "badgeCount": 2 } @@ -77,7 +89,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -111,7 +123,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -145,7 +157,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -178,8 +190,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -272,7 +284,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -332,7 +344,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -357,8 +369,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -378,8 +390,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": null, "skipped": false, @@ -399,8 +411,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, @@ -415,7 +427,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -425,8 +437,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -446,8 +458,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -467,7 +479,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -488,8 +500,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -511,9 +523,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -536,10 +548,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -547,7 +559,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -558,10 +570,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -569,10 +581,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -580,10 +592,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -591,7 +603,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -602,7 +614,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", 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 ea65b8d16..00581cc79 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-13T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", "source": "REMINDER", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,10 +33,10 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 0, "skipped": false @@ -38,7 +50,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -46,7 +58,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -54,9 +66,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -65,8 +77,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "badgeCount": 2 } @@ -77,7 +89,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -111,7 +123,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -145,7 +157,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -178,8 +190,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -272,7 +284,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -332,7 +344,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -357,8 +369,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -378,8 +390,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", "skipped": false, @@ -394,7 +406,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -404,8 +416,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, @@ -420,7 +432,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -430,8 +442,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -451,8 +463,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -472,7 +484,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -493,8 +505,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -516,9 +528,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -541,10 +553,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -552,7 +564,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -563,10 +575,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -574,10 +586,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -585,10 +597,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -596,10 +608,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -607,7 +619,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -618,7 +630,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", 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 a5b9e6892..f97aba489 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", "skipped": false, - "idempotencyKey": "dose-key-0001", - "scheduledFor": "2026-10-13T20:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,10 +33,10 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 0, "skipped": false @@ -38,7 +50,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -46,7 +58,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -54,9 +66,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -65,8 +77,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "badgeCount": 2 } @@ -77,7 +89,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -111,7 +123,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -145,7 +157,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -178,8 +190,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -272,7 +284,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -332,7 +344,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -357,8 +369,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -378,8 +390,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", "skipped": false, @@ -394,7 +406,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -404,8 +416,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", "skipped": false, @@ -420,7 +432,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -430,8 +442,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, @@ -446,7 +458,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -456,8 +468,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -477,8 +489,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -498,7 +510,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -519,8 +531,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -542,9 +554,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -567,10 +579,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -578,7 +590,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -589,10 +601,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -600,10 +612,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -611,10 +623,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -622,10 +634,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -633,10 +645,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -644,7 +656,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -655,7 +667,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", 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 f4e433b3e..15d1bf5cc 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-12T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,10 +33,10 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 0, "skipped": false @@ -38,7 +50,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -46,7 +58,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -54,9 +66,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -65,8 +77,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "badgeCount": 2 } @@ -77,7 +89,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -111,7 +123,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -145,7 +157,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -178,8 +190,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -272,7 +284,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -332,7 +344,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -357,8 +369,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", "skipped": false, @@ -373,7 +385,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -383,8 +395,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -404,8 +416,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", "skipped": false, @@ -420,7 +432,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -430,8 +442,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", "skipped": false, @@ -446,7 +458,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -456,8 +468,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, @@ -472,7 +484,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -482,8 +494,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -503,8 +515,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -524,7 +536,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -545,8 +557,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -568,9 +580,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -593,10 +605,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -604,7 +616,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -615,10 +627,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -626,10 +638,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -637,10 +649,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -648,10 +660,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -659,10 +671,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -670,10 +682,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -681,7 +693,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -692,7 +704,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -703,7 +715,7 @@ ], "idempotency": [ { - "userId": "", + "userId": "", "key": "", "method": "POST", "path": "/api/medications//intake", 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 e66eeed11..a2bab3d76 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-12T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -30,8 +42,8 @@ "stored": { "medications": [ { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -64,8 +76,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -98,8 +110,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -132,8 +144,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -167,7 +179,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -206,7 +218,7 @@ "schedules": [ { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "07:00", "windowEnd": "08:00", "label": null, @@ -226,7 +238,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -266,7 +278,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "09:00", "windowEnd": "11:00", "label": null, @@ -286,7 +298,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -311,8 +323,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", "skipped": false, @@ -337,8 +349,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -358,8 +370,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", "skipped": false, @@ -384,8 +396,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", "skipped": false, @@ -410,8 +422,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, @@ -436,8 +448,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -457,8 +469,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -478,8 +490,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, "skipped": true, @@ -499,8 +511,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -523,8 +535,8 @@ "inventoryItems": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -547,10 +559,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -558,10 +570,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -569,10 +581,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -580,10 +592,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -591,10 +603,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -602,10 +614,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -613,10 +625,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -624,10 +636,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -635,10 +647,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -646,10 +658,10 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { - "medicationId": "", + "medicationId": "", "reason": "resolved_event" }, "ipAddress": null @@ -657,7 +669,7 @@ ], "idempotency": [ { - "userId": "", + "userId": "", "key": "", "method": "POST", "path": "/api/medications//intake", 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 fdc949900..b60ae002d 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:10:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-11T20:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "API", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,10 +33,10 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 0, "skipped": false @@ -38,7 +50,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -46,7 +58,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -54,9 +66,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -65,8 +77,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "badgeCount": 2 } @@ -77,7 +89,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -111,7 +123,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -145,7 +157,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -178,8 +190,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -272,7 +284,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -332,7 +344,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -357,8 +369,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:10:00.000Z", "skipped": false, @@ -373,7 +385,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -383,8 +395,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", "skipped": false, @@ -399,7 +411,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -409,8 +421,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -430,8 +442,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", "skipped": false, @@ -446,7 +458,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -456,8 +468,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", "skipped": false, @@ -472,7 +484,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -482,8 +494,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, @@ -498,7 +510,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -508,8 +520,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": "2026-10-14T20:05:00.000Z", "skipped": false, @@ -529,8 +541,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -550,7 +562,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -571,8 +583,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -594,9 +606,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -619,10 +631,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -630,7 +642,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -641,10 +653,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -652,10 +664,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -663,10 +675,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -674,10 +686,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -685,10 +697,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -696,10 +708,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -707,10 +719,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -718,7 +730,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -729,7 +741,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -740,7 +752,7 @@ ], "idempotency": [ { - "userId": "", + "userId": "", "key": "", "method": "POST", "path": "/api/medications//intake", 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 3b22d87c1..6794d89eb 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": null, "skipped": true, - "idempotencyKey": null, - "scheduledFor": "2026-10-14T20:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,7 +33,7 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "skipped": true }, "warnings": null, @@ -33,7 +45,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -41,7 +53,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -49,9 +61,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "skipped" } ] @@ -60,8 +72,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "badgeCount": 2 } @@ -72,7 +84,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -106,7 +118,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -140,7 +152,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -173,8 +185,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -208,7 +220,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -267,7 +279,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -327,7 +339,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -352,8 +364,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:10:00.000Z", "skipped": false, @@ -378,8 +390,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", "skipped": false, @@ -404,8 +416,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -425,8 +437,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", "skipped": false, @@ -451,8 +463,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", "skipped": false, @@ -477,8 +489,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, @@ -503,8 +515,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -524,8 +536,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -545,7 +557,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -566,8 +578,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -590,8 +602,8 @@ "inventoryItems": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -614,10 +626,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -625,7 +637,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -636,10 +648,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -647,10 +659,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -658,10 +670,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -669,10 +681,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -680,10 +692,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -691,10 +703,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -702,10 +714,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -713,7 +725,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -724,10 +736,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -735,7 +747,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -745,10 +757,10 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { - "medicationId": "", + "medicationId": "", "reason": "resolved_event" }, "ipAddress": null @@ -756,7 +768,7 @@ ], "idempotency": [ { - "userId": "", + "userId": "", "key": "", "method": "POST", "path": "/api/medications//intake", 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 30e91414f..c4053484a 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": "2026-10-11T08:20:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-11T08:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,10 +33,10 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 1, "item_ids": [ - "" + "" ], "auto_opened": 0, "skipped": false @@ -38,7 +50,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -46,7 +58,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -54,9 +66,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", + "ownerId": "", "actorId": "", - "medicationId": "", + "medicationId": "", "status": "taken" } ] @@ -65,8 +77,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "badgeCount": 2 } @@ -77,7 +89,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -111,7 +123,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -145,7 +157,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -178,8 +190,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -213,7 +225,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -272,7 +284,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "08:00", "windowEnd": "10:00", "label": null, @@ -332,7 +344,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "20:00", "windowEnd": "22:00", "label": null, @@ -357,8 +369,8 @@ "intakes": [ { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": "2026-10-11T08:20:00.000Z", "skipped": false, @@ -373,7 +385,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -383,8 +395,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:10:00.000Z", "skipped": false, @@ -399,7 +411,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -409,8 +421,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", "skipped": false, @@ -425,7 +437,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -435,8 +447,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -456,8 +468,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", "skipped": false, @@ -472,7 +484,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -482,8 +494,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", "skipped": false, @@ -498,7 +510,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -508,8 +520,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", "skipped": false, @@ -524,7 +536,7 @@ "inventoryConsumption": [ { "units": 1, - "itemId": "" + "itemId": "" } ], "externalId": null, @@ -534,8 +546,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -555,8 +567,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, "skipped": true, @@ -576,7 +588,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -597,8 +609,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, "skipped": true, @@ -620,9 +632,9 @@ "doseChanges": [], "inventoryItems": [ { - "id": "", - "userId": "", - "medicationId": "", + "id": "", + "userId": "", + "medicationId": "", "state": "IN_USE", "containerType": "OTHER", "unitsTotal": "30", @@ -645,10 +657,10 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": "", "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -656,10 +668,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -667,7 +679,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -678,10 +690,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -689,10 +701,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -700,10 +712,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -711,10 +723,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -722,10 +734,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -733,10 +745,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -744,10 +756,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -755,7 +767,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -766,10 +778,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": true }, @@ -777,7 +789,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -787,10 +799,10 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { - "medicationId": "", + "medicationId": "", "reason": "resolved_event" }, "ipAddress": null @@ -798,7 +810,7 @@ ], "idempotency": [ { - "userId": "", + "userId": "", "key": "", "method": "POST", "path": "/api/medications//intake", 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 8c3e32ca8..6ac1a2a5d 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", "takenAt": "2026-10-15T09:10:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-15T09:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -21,7 +33,7 @@ "entity_id": "" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 0, "item_ids": [], "auto_opened": 0, @@ -36,7 +48,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -44,7 +56,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -52,9 +64,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -63,8 +75,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T09:00:00.000Z", "badgeCount": 2 } @@ -75,7 +87,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -109,7 +121,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -143,7 +155,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -177,7 +189,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -210,8 +222,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -290,7 +302,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "09:00", "windowEnd": "11:00", "label": null, @@ -355,7 +367,7 @@ "intakes": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": "2026-10-11T08:20:00.000Z", @@ -381,7 +393,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:10:00.000Z", @@ -407,7 +419,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", @@ -433,7 +445,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, @@ -454,7 +466,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", @@ -480,7 +492,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", @@ -506,7 +518,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", @@ -532,7 +544,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": null, @@ -553,7 +565,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, @@ -574,8 +586,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T09:00:00.000Z", "takenAt": "2026-10-15T09:10:00.000Z", "skipped": false, @@ -595,7 +607,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -616,7 +628,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, @@ -640,7 +652,7 @@ "inventoryItems": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "state": "IN_USE", "containerType": "OTHER", @@ -664,7 +676,7 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -675,7 +687,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -686,7 +698,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -697,7 +709,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -708,7 +720,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -719,7 +731,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -730,7 +742,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -741,7 +753,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -752,10 +764,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -763,7 +775,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -774,7 +786,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -785,7 +797,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -796,7 +808,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -807,7 +819,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -817,7 +829,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -828,7 +840,7 @@ ], "idempotency": [ { - "userId": "", + "userId": "", "key": "", "method": "POST", "path": "/api/medications//intake", 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 07271f7b7..034101fed 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 @@ -3,12 +3,24 @@ "body": { "data": { "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", "takenAt": "2026-10-15T10:00:00.000Z", "skipped": false, - "idempotencyKey": null, - "scheduledFor": "2026-10-15T09:00:00.000Z", + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", "source": "WEB", - "createdAt": "" + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null }, "error": null }, @@ -19,7 +31,7 @@ "name": "medication.oneShot.reconciled" }, "meta": { - "medication_id": "", + "medication_id": "", "units": 0, "item_ids": [], "auto_opened": 0, @@ -35,7 +47,7 @@ "effect": "queueMedicationIntakeSync", "args": [ { - "userId": "", + "userId": "", "originDeviceToken": null } ] @@ -43,7 +55,7 @@ { "effect": "countOutstandingDosesToday", "args": [ - "", + "", "UTC" ] }, @@ -51,9 +63,9 @@ "effect": "notifyDelegatedIntake", "args": [ { - "ownerId": "", - "actorId": "", - "medicationId": "", + "ownerId": "", + "actorId": "", + "medicationId": "", "status": "taken" } ] @@ -62,8 +74,8 @@ "effect": "dispatchMedicationIntakeWebClear", "args": [ { - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T09:00:00.000Z", "badgeCount": 2 } @@ -74,7 +86,7 @@ "medications": [ { "id": "", - "userId": "", + "userId": "", "name": "As needed", "dose": "10mg", "treatmentClass": "GENERIC", @@ -108,7 +120,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Record only", "dose": "10mg", "treatmentClass": "GENERIC", @@ -141,8 +153,8 @@ "updatedAt": "" }, { - "id": "", - "userId": "", + "id": "", + "userId": "", "name": "Single shot", "dose": "10mg", "treatmentClass": "GENERIC", @@ -176,7 +188,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Twice daily", "dose": "10mg", "treatmentClass": "GENERIC", @@ -210,7 +222,7 @@ }, { "id": "", - "userId": "", + "userId": "", "name": "Weekly pen", "dose": "10mg", "treatmentClass": "GENERIC", @@ -309,7 +321,7 @@ }, { "id": "", - "medicationId": "", + "medicationId": "", "windowStart": "09:00", "windowEnd": "11:00", "label": null, @@ -354,7 +366,7 @@ "intakes": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T08:00:00.000Z", "takenAt": "2026-10-11T08:20:00.000Z", @@ -380,7 +392,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-11T20:00:00.000Z", "takenAt": "2026-10-11T20:10:00.000Z", @@ -406,7 +418,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T08:00:00.000Z", "takenAt": "2026-10-12T08:15:00.000Z", @@ -432,7 +444,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-12T20:00:00.000Z", "takenAt": null, @@ -453,7 +465,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T08:00:00.000Z", "takenAt": "2026-10-15T11:30:00.000Z", @@ -479,7 +491,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-13T20:00:00.000Z", "takenAt": "2026-10-13T20:30:00.000Z", @@ -505,7 +517,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T08:00:00.000Z", "takenAt": "2026-10-14T11:00:00.000Z", @@ -531,7 +543,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-14T20:00:00.000Z", "takenAt": null, @@ -552,7 +564,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T08:00:00.000Z", "takenAt": null, @@ -573,7 +585,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T09:00:00.000Z", "takenAt": "2026-10-15T09:10:00.000Z", @@ -594,8 +606,8 @@ }, { "id": "", - "userId": "", - "medicationId": "", + "userId": "", + "medicationId": "", "scheduledFor": "2026-10-15T09:00:00.000Z", "takenAt": "2026-10-15T10:00:00.000Z", "skipped": false, @@ -615,7 +627,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T11:00:00.000Z", "takenAt": null, @@ -636,7 +648,7 @@ }, { "id": "", - "userId": "", + "userId": "", "medicationId": "", "scheduledFor": "2026-10-15T20:00:00.000Z", "takenAt": null, @@ -660,7 +672,7 @@ "inventoryItems": [ { "id": "", - "userId": "", + "userId": "", "medicationId": "", "state": "IN_USE", "containerType": "OTHER", @@ -684,7 +696,7 @@ "audit": [ { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -695,7 +707,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -706,7 +718,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -717,7 +729,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -728,7 +740,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -739,7 +751,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -750,7 +762,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -761,7 +773,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -772,7 +784,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -783,10 +795,10 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "eventId": "", "skipped": false }, @@ -794,7 +806,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -805,7 +817,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -816,7 +828,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -827,7 +839,7 @@ }, { "action": "medication.intake", - "userId": "", + "userId": "", "actorUserId": null, "details": { "medicationId": "", @@ -838,17 +850,17 @@ }, { "action": "medication.oneShot.reconciled", - "userId": "", + "userId": "", "actorUserId": null, "details": { - "medicationId": "", + "medicationId": "", "action": "deactivate" }, "ipAddress": null }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -858,7 +870,7 @@ }, { "action": "medications.intake.update.refused", - "userId": "", + "userId": "", "actorUserId": "", "details": { "medicationId": "", @@ -869,7 +881,7 @@ ], "idempotency": [ { - "userId": "", + "userId": "", "key": "", "method": "POST", "path": "/api/medications//intake", 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 8882ea379..42f544ad5 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.11-plain.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.11-plain.json.snap @@ -62,7 +62,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T05:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 24775d467..9a4f14354 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.12-full.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.12-full.json.snap @@ -80,7 +80,17 @@ ], "courseCount": 1, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-12T16:30:00.000Z", + "remindersActive": false, + "nextDueOverdue": true, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 c23aca305..f3a3616bd 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 @@ -40,7 +40,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": null, + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 a43296040..e90f7436b 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 @@ -71,7 +71,17 @@ ], "courseCount": 1, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-20T07:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "UPCOMING", + "intakeActionable": false, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 81a5f1d0c..70bc8174e 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 @@ -105,7 +105,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": null, + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": false, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 b12f37c1b..6c2a8d479 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 @@ -71,7 +71,17 @@ ], "courseCount": 1, "previousCourseEndedOn": "2026-10-05", - "canStartCourse": true + "canStartCourse": true, + "nextDueAt": null, + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "ENDED", + "intakeActionable": false, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 302f69f2d..5b84cf296 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.15a-mirror.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.15a-mirror.json.snap @@ -62,7 +62,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T06:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 25c308749..ca1c2c536 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 @@ -58,7 +58,21 @@ } ], "category": "OTHER", - "categoryLabel": null + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false, + "nextDueAt": "2026-10-16T06:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 d6d186d9a..075912d7f 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 @@ -62,7 +62,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T06:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 478f992f5..b2041d2c4 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 @@ -62,7 +62,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T06:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, 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 ca74c6b43..524d5101f 100644 --- a/tests/integration/routes/parity/__snapshots__/medications.18a-writer.json.snap +++ b/tests/integration/routes/parity/__snapshots__/medications.18a-writer.json.snap @@ -62,7 +62,17 @@ "courses": [], "courseCount": 0, "previousCourseEndedOn": null, - "canStartCourse": false + "canStartCourse": false, + "nextDueAt": "2026-10-16T06:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] }, "error": null }, diff --git a/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.05-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.05-invalid.json.snap index a6898965c..6028d3721 100644 --- a/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.05-invalid.json.snap +++ b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.05-invalid.json.snap @@ -30,7 +30,8 @@ }, "meta": { "write_bucket": "own", - "issue_count": 2 + "issue_count": 2, + "rejected_attempt": "mood.bulk.validation-failed" }, "warnings": null, "error": null diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap index b4b299c13..1b9359e7a 100644 --- a/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap @@ -145,7 +145,13 @@ } ] }, - "splits": null, + "splits": [ + { + "km": 1, + "durationSec": 530, + "paceSecPerKm": 530 + } + ], "sportContext": { "count": 1, "avgDurationSec": 1800, diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap index d1510e4c2..b2b4dc296 100644 --- a/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap @@ -115,7 +115,13 @@ } ] }, - "splits": null, + "splits": [ + { + "km": 1, + "durationSec": 530, + "paceSecPerKm": 530 + } + ], "sportContext": { "count": 1, "avgDurationSec": 1800, diff --git a/tests/integration/routes/parity/insights-generate.parity.test.ts b/tests/integration/routes/parity/insights-generate.parity.test.ts index 86b33a7fd..81d01d215 100644 --- a/tests/integration/routes/parity/insights-generate.parity.test.ts +++ b/tests/integration/routes/parity/insights-generate.parity.test.ts @@ -797,11 +797,7 @@ describe("POST /api/insights/generate parity", () => { await post( "P09-unreadable-key", { force: true }, - { - auth: unreadableKey, - userId: UNREADABLE_KEY_ID, - undeclared: "an unreadable stored key ends in 500 (pinned as it is)", - }, + { auth: unreadableKey, userId: UNREADABLE_KEY_ID }, ); }); diff --git a/tests/integration/routes/parity/medication-detail.parity.test.ts b/tests/integration/routes/parity/medication-detail.parity.test.ts index 64b13ac07..8ca3932e7 100644 --- a/tests/integration/routes/parity/medication-detail.parity.test.ts +++ b/tests/integration/routes/parity/medication-detail.parity.test.ts @@ -73,15 +73,6 @@ vi.mock( const prisma = getPrismaClient(); const NOW = new Date("2026-10-15T12:00:00.000Z"); const ROUTE = "/api/medications/{id}"; -/** - * The edit answers without the read-model fields (reminder state, stock and - * runway, composition, links) the registered medication resource declares. - * The parity snapshot pins that answer as it is; the contract check is the - * route's own tests' business. - */ -const EDIT_ANSWER = { - skip: "the edit answers without the read-model fields of the resource", -}; const CUSTOM_KEY = "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13"; let ownerId: string; @@ -127,7 +118,6 @@ async function put( body, auth: caller.auth ?? owner, headers: caller.headers, - contract: EDIT_ANSWER, }, userIds, now: NOW, diff --git a/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts b/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts index ec3fc649b..f51313028 100644 --- a/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts +++ b/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts @@ -94,7 +94,6 @@ vi.mock("@/lib/medications/outstanding-doses", async (importOriginal) => { const prisma = getPrismaClient(); const NOW = new Date("2026-10-15T12:00:00.000Z"); const ROUTE = "/api/medications/intake/bulk"; -const INVALID_ACTION = "medications.intake.bulk.validation-failed"; let ownerId: string; let userIds: string[]; @@ -130,8 +129,8 @@ async function syncState() { } /** - * An audit row a refusal writes without awaiting it: the schema refusal's - * own, and the narrow token's `auth.bearer.failure`. + * An audit row a refusal writes without awaiting it: the narrow token's + * `auth.bearer.failure`. (An owner's own schema refusal files none.) */ async function auditRows(action: string, count: number): Promise { // Polled on the real timer: the pinned clock stamps the rows read back, @@ -416,11 +415,9 @@ describe("POST /api/medications/intake/bulk parity", () => { medicationId: med.daily, })), }); - await post( - "04b-invalid", - { entries: [{ medicationId: "", takenAt: "never" }] }, - { settle: () => auditRows(INVALID_ACTION, 1) }, - ); + await post("04b-invalid", { + entries: [{ medicationId: "", takenAt: "never" }], + }); await post("04c-apple-not-mirrored", { entries: [ { diff --git a/tests/integration/routes/parity/medication-intake-event.parity.test.ts b/tests/integration/routes/parity/medication-intake-event.parity.test.ts index 415422a60..acfbe8d4b 100644 --- a/tests/integration/routes/parity/medication-intake-event.parity.test.ts +++ b/tests/integration/routes/parity/medication-intake-event.parity.test.ts @@ -59,15 +59,6 @@ vi.mock( const prisma = getPrismaClient(); const NOW = new Date("2026-10-15T12:00:00.000Z"); const ROUTE = "/api/medications/{id}/intake/{eventId}"; -/** - * A correction that moves the dose answers with the row the shared slot write - * returns, which carries fewer fields than the registered event resource. The - * parity snapshot pins that answer as it is; the contract check is the - * route's own tests' business. - */ -const SLOT_MOVE_ANSWER = { - skip: "a slot move answers with the narrower slot-write row", -}; let ownerId: string; let userIds: string[]; @@ -86,7 +77,6 @@ async function put( auth?: RouteAuth; headers?: Record; medicationId?: string; - contract?: { skip: string }; } = {}, ) { return parityStep({ @@ -99,7 +89,6 @@ async function put( body, auth: options.auth ?? owner, headers: options.headers, - contract: options.contract, }, userIds, now: NOW, @@ -252,22 +241,16 @@ describe("PUT /api/medications/{id}/intake/{eventId} parity", () => { }); it("03 a take corrected into the other window, onto a skipped slot", async () => { - const res = await put( - "03-move-by-band", - events.morningTaken, - { takenAt: "2026-10-14T20:30:00.000Z" }, - { contract: SLOT_MOVE_ANSWER }, - ); + const res = await put("03-move-by-band", events.morningTaken, { + takenAt: "2026-10-14T20:30:00.000Z", + }); events.moved = (res.data as { id: string }).id; }); it("04 a legacy take pinned onto a free slot", async () => { - const res = await put( - "04-pin-free-slot", - events.legacyTaken, - { forceSlotInstant: "2026-10-12T20:00:00.000Z" }, - { contract: SLOT_MOVE_ANSWER }, - ); + const res = await put("04-pin-free-slot", events.legacyTaken, { + forceSlotInstant: "2026-10-12T20:00:00.000Z", + }); events.pinned = (res.data as { id: string }).id; }); @@ -284,25 +267,17 @@ describe("PUT /api/medications/{id}/intake/{eventId} parity", () => { }); it("07 an unpin", async () => { - const res = await put( - "07-unpin", - events.pinned, - { forceSlotInstant: null }, - { contract: SLOT_MOVE_ANSWER }, - ); + const res = await put("07-unpin", events.pinned, { + forceSlotInstant: null, + }); events.pinned = (res.data as { id: string }).id; }); it("08 an explicit slot in the body", async () => { - const res = await put( - "08-explicit-slot", - events.moved, - { - scheduledFor: "2026-10-14T08:00:00.000Z", - takenAt: "2026-10-14T08:45:00.000Z", - }, - { contract: SLOT_MOVE_ANSWER }, - ); + const res = await put("08-explicit-slot", events.moved, { + scheduledFor: "2026-10-14T08:00:00.000Z", + takenAt: "2026-10-14T08:45:00.000Z", + }); events.moved = (res.data as { id: string }).id; }); diff --git a/tests/integration/routes/parity/medication-intake-status.parity.test.ts b/tests/integration/routes/parity/medication-intake-status.parity.test.ts index b83135251..92f6c25bb 100644 --- a/tests/integration/routes/parity/medication-intake-status.parity.test.ts +++ b/tests/integration/routes/parity/medication-intake-status.parity.test.ts @@ -132,21 +132,7 @@ async function get( }); } -/** - * A take that re-binds to another slot answers with the row the shared slot - * write returns, which carries fewer fields than the registered event - * resource. The parity snapshot pins that answer as it is; the contract check - * is the route's own tests' business. - */ -const SLOT_MOVE_ANSWER = { - skip: "a slot move answers with the narrower slot-write row", -}; - -async function post( - name: string, - body: unknown, - caller: Caller & { contract?: { skip: string } } = {}, -) { +async function post(name: string, body: unknown, caller: Caller = {}) { await parityStep({ name: `medication-intake-status.${name}`, handler: POST, @@ -156,7 +142,6 @@ async function post( body, auth: caller.auth ?? owner, headers: caller.headers, - contract: caller.contract, }, userIds, now: NOW, @@ -362,28 +347,20 @@ describe("POST /api/medications/intake parity", () => { }); it("12 a take that re-binds to another slot", async () => { - await post( - "12-take-slot-move", - { - intakeId: ev.moveMe, - status: "taken", - takenAt: "2026-10-14T20:30:00.000Z", - }, - { contract: SLOT_MOVE_ANSWER }, - ); + await post("12-take-slot-move", { + intakeId: ev.moveMe, + status: "taken", + takenAt: "2026-10-14T20:30:00.000Z", + }); }); it("13 a pinned take, and a pin that is no slot", async () => { - await post( - "13a-pin", - { - intakeId: ev.pinMe, - status: "taken", - takenAt: "2026-10-13T11:00:00.000Z", - forceSlotInstant: "2026-10-13T08:00:00.000Z", - }, - { contract: SLOT_MOVE_ANSWER }, - ); + await post("13a-pin", { + intakeId: ev.pinMe, + status: "taken", + takenAt: "2026-10-13T11:00:00.000Z", + forceSlotInstant: "2026-10-13T08:00:00.000Z", + }); await post("13b-pin-not-a-slot", { intakeId: ev.snoozeMe, status: "taken", @@ -451,7 +428,7 @@ describe("POST /api/medications/intake parity", () => { await post( "20a-writer-take", { intakeId: ev.todayEvening, status: "taken" }, - { ...writer, contract: SLOT_MOVE_ANSWER }, + { ...writer }, ); await post( "20b-writer-snooze-refused", diff --git a/tests/integration/routes/parity/medication-intake.parity.test.ts b/tests/integration/routes/parity/medication-intake.parity.test.ts index 5d66bd899..e87cf639b 100644 --- a/tests/integration/routes/parity/medication-intake.parity.test.ts +++ b/tests/integration/routes/parity/medication-intake.parity.test.ts @@ -108,15 +108,6 @@ vi.mock("@/lib/notifications/delegated-intake", async (importOriginal) => { const prisma = getPrismaClient(); const NOW = new Date("2026-10-15T12:00:00.000Z"); const ROUTE = "/api/medications/{id}/intake"; -/** - * A dose that converges onto a slot answers with the row the shared slot - * write returns, which carries fewer fields than the registered event - * resource. The parity snapshot pins that answer as it is; the contract check - * is the route's own tests' business. - */ -const DOSE_ANSWER = { - skip: "a slot write answers with the narrower slot-write row", -}; const REFUSED = "medications.intake.update.refused"; let ownerId: string; @@ -159,7 +150,6 @@ async function post( body, auth: options.auth ?? owner, headers: options.headers, - contract: DOSE_ANSWER, }, userIds, now: NOW, diff --git a/tests/integration/routes/parity/medications.parity.test.ts b/tests/integration/routes/parity/medications.parity.test.ts index 028bfffa6..0e3b15478 100644 --- a/tests/integration/routes/parity/medications.parity.test.ts +++ b/tests/integration/routes/parity/medications.parity.test.ts @@ -42,15 +42,6 @@ vi.mock("@/lib/logging/transports", async (importOriginal) => { const prisma = getPrismaClient(); const NOW = new Date("2026-10-15T12:00:00.000Z"); const ROUTE = "/api/medications"; -/** - * The create answers without the read-model fields (reminder state, course - * status, stock and runway) the registered medication resource declares. The - * parity snapshot pins that answer as it is; the contract check is the - * route's own tests' business. - */ -const CREATE_ANSWER = { - skip: "the create answers without the read-model fields of the resource", -}; const CUSTOM_KEY = "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13"; let ownerId: string; @@ -95,7 +86,6 @@ async function create( body, auth: options.auth ?? owner, headers: options.headers, - contract: CREATE_ANSWER, }, userIds: options.userIds ?? userIds, now: NOW,