mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
test(analytics): pin the analytics response before moving its builder
A parity walk over GET /api/analytics on an account with a full record (record-fixture.ts, reusable by the other read routes): an unknown slice, the summaries slice and the default body cold and warm, the modules off, a second language with a personal blood-pressure target, an empty account and an anonymous call. Background work a request leaves running is settled before the next one. The blood-pressure fast path's rollup row counts are left out of the pinned wide event: they follow the rollup tier's coverage, not the code, while the answer they fed stays pinned in the body.
This commit is contained in:
1 parent
905d4b167c
commit
d24c544e98
11 files changed
+13631
No files matched your search
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"status": 422,
|
||||
"cacheControl": null,
|
||||
"body": {
|
||||
"data": null,
|
||||
"error": "Validation failed",
|
||||
"details": {
|
||||
"issues": [
|
||||
{
|
||||
"path": "slice",
|
||||
"code": "invalid_value",
|
||||
"message": "Invalid input: expected \"summaries\""
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
"errorCode": "analytics.invalid_query",
|
||||
"requestId": "<requestId>"
|
||||
}
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 422,
|
||||
"action": {
|
||||
"name": "analytics.get"
|
||||
},
|
||||
"meta": {
|
||||
"outcome": "invalid_query"
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
],
|
||||
"effects": [],
|
||||
"stored": {
|
||||
"healthScores": [],
|
||||
"patterns": []
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+1299
File diff suppressed because it is too large.
Load diff
+2169
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"status": 401,
|
||||
"cacheControl": null,
|
||||
"body": {
|
||||
"data": null,
|
||||
"error": "Not authenticated",
|
||||
"meta": {
|
||||
"errorCode": "auth.missing",
|
||||
"requestId": "<requestId>"
|
||||
}
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 401,
|
||||
"action": null,
|
||||
"meta": null,
|
||||
"warnings": null,
|
||||
"error": {
|
||||
"type": "HttpError",
|
||||
"message": "Not authenticated"
|
||||
}
|
||||
}
|
||||
],
|
||||
"effects": [],
|
||||
"stored": {
|
||||
"healthScores": [
|
||||
{
|
||||
"dayKey": "2026-10-15",
|
||||
"timezone": "Europe/Berlin",
|
||||
"composite": 86,
|
||||
"band": "green",
|
||||
"scoreVersion": 4,
|
||||
"composition": [
|
||||
"BLOOD_PRESSURE",
|
||||
"GLYCAEMIA",
|
||||
"ACTIVITY",
|
||||
"SLEEP"
|
||||
],
|
||||
"pillarScores": {
|
||||
"SLEEP": 92,
|
||||
"ACTIVITY": 78,
|
||||
"GLYCAEMIA": 100,
|
||||
"BLOOD_PRESSURE": 73
|
||||
},
|
||||
"inputFingerprint": "5653cd8c051404182d6c6f813a48b1c2ebc3abe1116901466ab7cc1ab70cb276",
|
||||
"computedAt": "<now>"
|
||||
}
|
||||
],
|
||||
"patterns": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Parity for `GET /api/analytics` while the response builder moves out of
|
||||
* the route into `src/lib/analytics/response/`.
|
||||
*
|
||||
* One account with a full record (see `record-fixture.ts`) walks through the
|
||||
* cases the route tells apart, in order, each request seeing the cache the
|
||||
* earlier ones filled: a `slice` outside the closed set, the summaries slice
|
||||
* cold and warm, the default body cold and warm, the summaries slice after
|
||||
* the glucose, sleep and recovery modules were switched off (gated after the
|
||||
* cache read), and the default body in another language with a personal
|
||||
* blood-pressure target (a fresh cache cell, so the builder runs against the
|
||||
* clinical band a second time and with glucose off). An account with no
|
||||
* profile and no data, and an anonymous call, close the walk. After each
|
||||
* request the snapshot holds the response and its cache header, the wide
|
||||
* event, the rollup refreshes the request handed off, and the stored health
|
||||
* scores and correlation patterns.
|
||||
*
|
||||
* The clock is pinned (`Date` only). The record's rollups are built once
|
||||
* before the walk; the fire-and-forget refresh the route starts on every
|
||||
* request is recorded instead of run, so the wide event never depends on
|
||||
* whether it finished first.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { GET } from "@/app/api/analytics/route";
|
||||
import { settleBackgroundTasks } from "@/lib/logging/background-tasks";
|
||||
|
||||
import { anonymous, callRoute, type RouteAuth } from "../../../kit/call-route";
|
||||
import { makeSession, makeUser } from "../../../kit/factories";
|
||||
import {
|
||||
effectRecorder,
|
||||
eventFacts,
|
||||
expectParity,
|
||||
stable,
|
||||
wideEventRecorder,
|
||||
} from "../../../kit/parity";
|
||||
import { getPrismaClient, truncateAllTables } from "../../setup";
|
||||
import { parityId, seedParityRecord } from "./record-fixture";
|
||||
|
||||
vi.mock("@/lib/logging/transports", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@/lib/logging/transports")>();
|
||||
const { wideEventRecorder: recorder } = await import("../../../kit/parity");
|
||||
return {
|
||||
...actual,
|
||||
emitIfSampled: (event: (typeof recorder.events)[number]) => {
|
||||
recorder.events.push(event);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/rollups/measurement-rollups", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@/lib/rollups/measurement-rollups")>();
|
||||
const { effectRecorder } = await import("../../../kit/parity");
|
||||
return {
|
||||
...actual,
|
||||
ensureUserRollupsFresh: async (...args: unknown[]) => {
|
||||
effectRecorder.record("ensureUserRollupsFresh", args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const NOW = new Date("2026-10-15T12:00:00.000Z");
|
||||
const TZ = "Europe/Berlin";
|
||||
const OWNER_ID = parityId("analytics", 1);
|
||||
const EMPTY_ID = parityId("analytics", 2);
|
||||
|
||||
let owner: RouteAuth;
|
||||
let empty: RouteAuth;
|
||||
|
||||
async function stored(userId: string) {
|
||||
const [healthScores, patterns] = await Promise.all([
|
||||
prisma.healthScoreRecord.findMany({
|
||||
where: { userId },
|
||||
orderBy: { dayKey: "asc" },
|
||||
select: {
|
||||
dayKey: true,
|
||||
timezone: true,
|
||||
composite: true,
|
||||
band: true,
|
||||
scoreVersion: true,
|
||||
composition: true,
|
||||
pillarScores: true,
|
||||
inputFingerprint: true,
|
||||
computedAt: true,
|
||||
},
|
||||
}),
|
||||
prisma.correlationPattern.findMany({
|
||||
where: { userId },
|
||||
orderBy: { canonicalKey: "asc" },
|
||||
select: {
|
||||
canonicalKey: true,
|
||||
family: true,
|
||||
factorKey: true,
|
||||
outcomeKey: true,
|
||||
lagDays: true,
|
||||
sampleSize: true,
|
||||
effectSize: true,
|
||||
pValue: true,
|
||||
qValue: true,
|
||||
evidenceHash: true,
|
||||
isCurrent: true,
|
||||
lastComputedAt: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
return { healthScores, patterns };
|
||||
}
|
||||
|
||||
async function step(
|
||||
name: string,
|
||||
options: {
|
||||
auth?: RouteAuth;
|
||||
query?: Record<string, string>;
|
||||
userId?: string;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
wideEventRecorder.take();
|
||||
effectRecorder.take();
|
||||
const res = await callRoute(GET, {
|
||||
method: "GET",
|
||||
route: "/api/analytics",
|
||||
query: options.query,
|
||||
auth: options.auth ?? owner,
|
||||
});
|
||||
const events = wideEventRecorder.take();
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
// Work the request left running (a rollup fill behind a read) finishes
|
||||
// before the next request, so each case starts from the same store.
|
||||
await settleBackgroundTasks();
|
||||
await expectParity(
|
||||
`analytics.${name}`,
|
||||
stable(
|
||||
{
|
||||
status: res.status,
|
||||
cacheControl: res.headers.get("cache-control"),
|
||||
body: res.body,
|
||||
events: events.map(eventFacts).map(withoutRollupCoverage),
|
||||
effects: effectRecorder.take(),
|
||||
stored: await stored(options.userId ?? OWNER_ID),
|
||||
},
|
||||
{ since: NOW },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The blood-pressure fast path reports how many rollup rows it read. That
|
||||
* count follows the rollup tier's coverage at the moment of the request,
|
||||
* which background refills outside this route decide, so it differs between
|
||||
* two runs of the same code while the answer it fed does not. The answer
|
||||
* stays pinned in `body`; only the three diagnostic counts are dropped.
|
||||
*/
|
||||
function withoutRollupCoverage(facts: unknown): unknown {
|
||||
const meta = (facts as { meta?: { analytics?: { bp_in_target?: object } } })
|
||||
.meta;
|
||||
const bp = meta?.analytics?.bp_in_target as
|
||||
Record<string, unknown> | undefined;
|
||||
if (bp) {
|
||||
delete bp.row_count;
|
||||
delete bp.sys_rows;
|
||||
delete bp.dia_rows;
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.useFakeTimers({ toFake: ["Date"], now: NOW });
|
||||
await truncateAllTables(prisma);
|
||||
const ownerRow = await makeUser({
|
||||
id: OWNER_ID,
|
||||
timezone: TZ,
|
||||
locale: "en",
|
||||
heightCm: 178,
|
||||
dateOfBirth: new Date("1975-04-02T00:00:00.000Z"),
|
||||
gender: "male",
|
||||
});
|
||||
await seedParityRecord({ userId: OWNER_ID, now: NOW, tz: TZ, tag: "an" });
|
||||
const rollups = await vi.importActual<
|
||||
typeof import("@/lib/rollups/measurement-rollups")
|
||||
>("@/lib/rollups/measurement-rollups");
|
||||
await rollups.ensureUserRollupsFresh(OWNER_ID);
|
||||
await settleBackgroundTasks();
|
||||
owner = await makeSession(ownerRow);
|
||||
const emptyRow = await makeUser({ id: EMPTY_ID, timezone: "UTC" });
|
||||
empty = await makeSession(emptyRow);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("GET /api/analytics parity", () => {
|
||||
it("01 a slice outside the closed set", async () => {
|
||||
await step("01-invalid-slice", { query: { slice: "summary" } });
|
||||
});
|
||||
|
||||
it("02 the summaries slice, cold", async () => {
|
||||
await step("02-summaries-cold", { query: { slice: "summaries" } });
|
||||
});
|
||||
|
||||
it("03 the summaries slice, warm", async () => {
|
||||
await step("03-summaries-warm", { query: { slice: "summaries" } });
|
||||
});
|
||||
|
||||
it("04 the default body, cold", async () => {
|
||||
await step("04-default-cold");
|
||||
});
|
||||
|
||||
it("05 the default body, warm", async () => {
|
||||
await step("05-default-warm");
|
||||
});
|
||||
|
||||
it("06 the summaries slice with modules off", async () => {
|
||||
await prisma.user.update({
|
||||
where: { id: OWNER_ID },
|
||||
data: {
|
||||
modulePreferencesJson: {
|
||||
glucose: false,
|
||||
sleep: false,
|
||||
recovery: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await step("06-summaries-modules-off", { query: { slice: "summaries" } });
|
||||
});
|
||||
|
||||
it("07 the default body in another language with a personal target", async () => {
|
||||
await prisma.user.update({
|
||||
where: { id: OWNER_ID },
|
||||
data: {
|
||||
locale: "de",
|
||||
thresholdsJson: {
|
||||
BLOOD_PRESSURE_SYS: { min: 100, max: 125 },
|
||||
BLOOD_PRESSURE_DIA: { min: 60, max: 80 },
|
||||
},
|
||||
},
|
||||
});
|
||||
await step("07-default-de-personal-target");
|
||||
});
|
||||
|
||||
it("08 an account with no profile and no data", async () => {
|
||||
await step("08-empty-account", { auth: empty, userId: EMPTY_ID });
|
||||
});
|
||||
|
||||
it("09 an anonymous call", async () => {
|
||||
await step("09-anonymous", { auth: anonymous });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* One representative health record for the read-route parity walks.
|
||||
*
|
||||
* The aggregate routes (`/api/analytics`, `/api/dashboard/summary`,
|
||||
* `/api/insights/comprehensive`, `/api/insights/generate`) shape their body
|
||||
* from many domains at once, so a parity walk over them needs a record in
|
||||
* which every block has something to say: daily weight and blood pressure
|
||||
* long enough for the 90-day windows, a resting-pulse series, glucose in
|
||||
* three meal contexts (one untagged), staged sleep nights with a nap, steps,
|
||||
* oxygen saturation, HRV, a mood diary, a blood-pressure medication with an
|
||||
* intake history that has skips and gaps, a second medication, and an open
|
||||
* illness episode.
|
||||
*
|
||||
* Every value is a function of the day offset from `now`, so the record is
|
||||
* the same on every run. Medications carry fixed ids, because some payloads
|
||||
* hash or echo them; measurement ids are generated and blanked by `stable`.
|
||||
*
|
||||
* Not a test file. Only for tests under `tests/integration/`.
|
||||
*/
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
|
||||
import { getPrismaClient } from "../../setup";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** A cuid-shaped fixed id: `c` and 24 lower-case alphanumerics. */
|
||||
export function parityId(tag: string, n: number): string {
|
||||
return `c${`${tag}${n.toString(36)}`.padStart(24, "0")}`;
|
||||
}
|
||||
|
||||
/** A local `YYYY-MM-DD` key of `at` in `tz`. */
|
||||
function dayKey(at: Date, tz: string): string {
|
||||
return new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: tz,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).format(at);
|
||||
}
|
||||
|
||||
/** The UTC instant `hh:mm` on the day `d` days before `now`. */
|
||||
function at(now: Date, d: number, hh: number, mm = 0): Date {
|
||||
const base = new Date(now.getTime() - d * DAY_MS);
|
||||
base.setUTCHours(hh, mm, 0, 0);
|
||||
return base;
|
||||
}
|
||||
|
||||
export interface RecordFixture {
|
||||
bpMedicationId: string;
|
||||
otherMedicationId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the record onto an existing account. `tag` keeps the fixed ids of
|
||||
* two records in one database apart.
|
||||
*/
|
||||
export async function seedParityRecord(options: {
|
||||
userId: string;
|
||||
now: Date;
|
||||
tz: string;
|
||||
tag: string;
|
||||
}): Promise<RecordFixture> {
|
||||
const { userId, now, tz, tag } = options;
|
||||
const prisma = getPrismaClient();
|
||||
const rows: Prisma.MeasurementCreateManyInput[] = [];
|
||||
const push = (row: Omit<Prisma.MeasurementCreateManyInput, "userId">) =>
|
||||
rows.push({ userId, createdAt: row.measuredAt, ...row });
|
||||
|
||||
for (let d = 0; d < 60; d++) {
|
||||
push({
|
||||
type: "WEIGHT",
|
||||
value: Math.round((82 + d * 0.04) * 10) / 10,
|
||||
unit: "kg",
|
||||
source: "WITHINGS",
|
||||
deviceType: "scale",
|
||||
measuredAt: at(now, d, 6, 30),
|
||||
});
|
||||
push({
|
||||
type: "PULSE",
|
||||
value: 64 + (d % 6),
|
||||
unit: "bpm",
|
||||
measuredAt: at(now, d, 7, 1),
|
||||
});
|
||||
if (d % 7 === 0) {
|
||||
push({
|
||||
type: "BODY_FAT",
|
||||
value: Math.round((24 + d * 0.02) * 10) / 10,
|
||||
unit: "%",
|
||||
source: "WITHINGS",
|
||||
measuredAt: at(now, d, 6, 31),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let d = 0; d < 120; d++) {
|
||||
push({
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
value: 123 + ((d * 7) % 15),
|
||||
unit: "mmHg",
|
||||
measuredAt: at(now, d, 7, 0),
|
||||
});
|
||||
push({
|
||||
type: "BLOOD_PRESSURE_DIA",
|
||||
value: 78 + ((d * 5) % 9),
|
||||
unit: "mmHg",
|
||||
measuredAt: at(now, d, 7, 0),
|
||||
});
|
||||
}
|
||||
for (let d = 0; d < 40; d++) {
|
||||
push({
|
||||
type: "RESTING_HEART_RATE",
|
||||
value: 58 + (d % 4),
|
||||
unit: "bpm",
|
||||
source: "APPLE_HEALTH",
|
||||
measuredAt: at(now, d, 5, 0),
|
||||
});
|
||||
}
|
||||
for (let d = 0; d < 20; d++) {
|
||||
push({
|
||||
type: "BLOOD_GLUCOSE",
|
||||
value: 92 + (d % 7),
|
||||
unit: "mg/dL",
|
||||
glucoseContext: "FASTING",
|
||||
measuredAt: at(now, d, 6, 15),
|
||||
});
|
||||
push({
|
||||
type: "BLOOD_GLUCOSE",
|
||||
value: 128 + ((d * 3) % 20),
|
||||
unit: "mg/dL",
|
||||
glucoseContext: "POSTPRANDIAL",
|
||||
measuredAt: at(now, d, 11, 0),
|
||||
});
|
||||
push({
|
||||
type: "BLOOD_GLUCOSE",
|
||||
value: 105 + (d % 4),
|
||||
unit: "mg/dL",
|
||||
measuredAt: at(now, d, 10, 0),
|
||||
});
|
||||
}
|
||||
// Staged nights ending on the morning of each wake day, and one nap.
|
||||
for (let d = 0; d < 21; d++) {
|
||||
const stage = (
|
||||
sleepStage: "CORE" | "DEEP" | "REM" | "AWAKE",
|
||||
hh: number,
|
||||
mm: number,
|
||||
minutes: number,
|
||||
) =>
|
||||
push({
|
||||
type: "SLEEP_DURATION",
|
||||
value: minutes,
|
||||
unit: "min",
|
||||
source: "APPLE_HEALTH",
|
||||
deviceType: "watch",
|
||||
sleepStage,
|
||||
measuredAt: at(now, d, hh, mm),
|
||||
});
|
||||
stage("CORE", 1, 0, 200 + (d % 5) * 10);
|
||||
stage("DEEP", 2, 30, 70 + (d % 3) * 5);
|
||||
stage("REM", 4, 0, 85 + (d % 4) * 5);
|
||||
stage("AWAKE", 4, 15, 12);
|
||||
}
|
||||
push({
|
||||
type: "SLEEP_DURATION",
|
||||
value: 35,
|
||||
unit: "min",
|
||||
source: "APPLE_HEALTH",
|
||||
deviceType: "watch",
|
||||
sleepStage: "CORE",
|
||||
measuredAt: at(now, 2, 13, 40),
|
||||
});
|
||||
for (let d = 0; d < 30; d++) {
|
||||
push({
|
||||
type: "ACTIVITY_STEPS",
|
||||
value: 6000 + ((d * 373) % 4000),
|
||||
unit: "count",
|
||||
source: "APPLE_HEALTH",
|
||||
measuredAt: at(now, d, 9, 0),
|
||||
});
|
||||
push({
|
||||
type: "HEART_RATE_VARIABILITY",
|
||||
value: 45 + (d % 8),
|
||||
unit: "ms",
|
||||
source: "APPLE_HEALTH",
|
||||
measuredAt: at(now, d, 4, 30),
|
||||
});
|
||||
}
|
||||
for (let d = 0; d < 10; d++) {
|
||||
push({
|
||||
type: "OXYGEN_SATURATION",
|
||||
value: 96 + (d % 3),
|
||||
unit: "%",
|
||||
source: "APPLE_HEALTH",
|
||||
measuredAt: at(now, d, 4, 40),
|
||||
});
|
||||
}
|
||||
await prisma.measurement.createMany({ data: rows });
|
||||
|
||||
const moods = ["LAUSIG", "SCHLECHT", "OKAY", "GUT", "SUPER_GUT"];
|
||||
const moodRows: Prisma.MoodEntryCreateManyInput[] = [];
|
||||
for (let d = 0; d < 45; d++) {
|
||||
const score = 1 + ((d * 3) % 5);
|
||||
const loggedAt = at(now, d, 9, 30);
|
||||
moodRows.push({
|
||||
userId,
|
||||
date: dayKey(loggedAt, tz),
|
||||
mood: moods[score - 1],
|
||||
score,
|
||||
moodLoggedAt: loggedAt,
|
||||
tz,
|
||||
createdAt: loggedAt,
|
||||
});
|
||||
}
|
||||
await prisma.moodEntry.createMany({ data: moodRows });
|
||||
|
||||
const bpMedicationId = parityId(`${tag}med`, 1);
|
||||
const otherMedicationId = parityId(`${tag}med`, 2);
|
||||
const created = new Date(now.getTime() - 90 * DAY_MS);
|
||||
await prisma.medication.create({
|
||||
data: {
|
||||
id: bpMedicationId,
|
||||
userId,
|
||||
name: "Ramipril",
|
||||
dose: "5mg",
|
||||
createdAt: created,
|
||||
schedules: {
|
||||
create: [
|
||||
{
|
||||
id: parityId(`${tag}sch`, 1),
|
||||
windowStart: "08:00",
|
||||
windowEnd: "10:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
categoryAssignment: { create: { category: "BLOOD_PRESSURE" } },
|
||||
},
|
||||
});
|
||||
await prisma.medication.create({
|
||||
data: {
|
||||
id: otherMedicationId,
|
||||
userId,
|
||||
name: "Vitamin D",
|
||||
dose: "1000 IU",
|
||||
createdAt: created,
|
||||
schedules: {
|
||||
create: [
|
||||
{
|
||||
id: parityId(`${tag}sch`, 2),
|
||||
windowStart: "20:00",
|
||||
windowEnd: "21:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const intakes: Prisma.MedicationIntakeEventCreateManyInput[] = [];
|
||||
for (let d = 1; d <= 45; d++) {
|
||||
if (d % 9 === 0) continue;
|
||||
const scheduledFor = at(now, d, 6, 0);
|
||||
const skipped = d % 7 === 0;
|
||||
intakes.push({
|
||||
id: parityId(`${tag}ie`, d),
|
||||
userId,
|
||||
medicationId: bpMedicationId,
|
||||
scheduledFor,
|
||||
takenAt: skipped ? null : at(now, d, 6, 20),
|
||||
skipped,
|
||||
createdAt: scheduledFor,
|
||||
});
|
||||
if (d % 2 === 0) {
|
||||
const evening = at(now, d, 18, 0);
|
||||
intakes.push({
|
||||
id: parityId(`${tag}ie`, 100 + d),
|
||||
userId,
|
||||
medicationId: otherMedicationId,
|
||||
scheduledFor: evening,
|
||||
takenAt: at(now, d, 18, 30),
|
||||
createdAt: evening,
|
||||
});
|
||||
}
|
||||
}
|
||||
await prisma.medicationIntakeEvent.createMany({ data: intakes });
|
||||
|
||||
await prisma.illnessEpisode.create({
|
||||
data: {
|
||||
id: parityId(`${tag}ill`, 1),
|
||||
userId,
|
||||
label: "Common cold",
|
||||
type: "INFECTION",
|
||||
onsetAt: new Date(now.getTime() - 3 * DAY_MS),
|
||||
createdAt: new Date(now.getTime() - 3 * DAY_MS),
|
||||
},
|
||||
});
|
||||
|
||||
return { bpMedicationId, otherMedicationId };
|
||||
}
|
||||
Reference in new issue
Block a user