mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
`measured_at` and `scheduled_for` are naive UTC timestamps. The dashboard streak and the medication-compliance backfill keyed them with a single `AT TIME ZONE tz`, which reads the value as local time and shifts it the wrong way: in Berlin a reading at 23:30 UTC counted for the day before instead of the day after, so a streak could break or stretch near midnight and the backfill recomputed the wrong day. The streak's measurement days now come from `readActivityDayKeys`: one existence probe per local day instead of a DISTINCT over a year of readings. The DAY rollups cannot answer this because their buckets are UTC days. The two compliance-rollup readers had no caller left and are removed with the coverage probe that carried the same shift. A guard fails on any single shift of a naive column into a bound zone.
125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
/**
|
|
* Local day keys from naive UTC timestamps, against a real Postgres.
|
|
*
|
|
* `measured_at` and `scheduled_for` hold UTC wall-clock time in `timestamp`
|
|
* columns. The dashboard streak and the medication-compliance backfill keyed
|
|
* them with a single `AT TIME ZONE tz`, which reads the value as local time
|
|
* and shifts it the wrong way. Both now agree with `userDayKey`: a reading at
|
|
* 23:30 UTC is the next day in Berlin and still the same day in Los Angeles,
|
|
* a reading at 03:00 UTC is the previous day in Los Angeles.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
import { readActivityDayKeys } from "@/lib/dashboard/summary-reads";
|
|
import { recomputeUserMedicationCompliance } from "@/lib/rollups/medication-compliance-rollups";
|
|
import { userDayKey } from "@/lib/tz/format";
|
|
|
|
vi.mock("@/lib/db-compat", () => ({
|
|
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
const prisma = getPrismaClient();
|
|
const USER = "local-day-keys";
|
|
const now = new Date();
|
|
/** UTC midnight ten days back. */
|
|
const D = Date.UTC(
|
|
now.getUTCFullYear(),
|
|
now.getUTCMonth(),
|
|
now.getUTCDate() - 10,
|
|
);
|
|
const LATE = new Date(D + 23.5 * 3_600_000); // 23:30 UTC on D
|
|
const EARLY = new Date(D + 3 * 3_600_000); // 03:00 UTC on D
|
|
const key = (ms: number) => new Date(ms).toISOString().slice(0, 10);
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(prisma);
|
|
await prisma.user.create({
|
|
data: {
|
|
id: USER,
|
|
username: USER,
|
|
email: `${USER}@example.test`,
|
|
timezone: "Europe/Berlin",
|
|
},
|
|
});
|
|
});
|
|
|
|
describe("readActivityDayKeys", () => {
|
|
beforeEach(async () => {
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{ at: LATE, deleted: false },
|
|
{ at: EARLY, deleted: false },
|
|
{ at: new Date(D - 3 * 86_400_000), deleted: true },
|
|
].map(({ at, deleted }, i) => ({
|
|
id: `${USER}-m${i}`,
|
|
userId: USER,
|
|
type: "WEIGHT" as const,
|
|
source: "MANUAL" as const,
|
|
value: 80,
|
|
unit: "kg",
|
|
measuredAt: at,
|
|
deletedAt: deleted ? new Date() : null,
|
|
})),
|
|
});
|
|
});
|
|
|
|
const since = new Date(D - 5 * 86_400_000);
|
|
|
|
it("keys a late reading onto the next day east of UTC", async () => {
|
|
const keys = await readActivityDayKeys(
|
|
prisma,
|
|
USER,
|
|
"Europe/Berlin",
|
|
since,
|
|
now,
|
|
);
|
|
expect(keys).toEqual([key(D), key(D + 86_400_000)]);
|
|
expect(keys).toEqual(
|
|
[EARLY, LATE].map((at) => userDayKey(at, "Europe/Berlin")),
|
|
);
|
|
});
|
|
|
|
it("keys an early reading onto the previous day west of UTC", async () => {
|
|
const keys = await readActivityDayKeys(
|
|
prisma,
|
|
USER,
|
|
"America/Los_Angeles",
|
|
since,
|
|
now,
|
|
);
|
|
expect(keys).toEqual([key(D - 86_400_000), key(D)]);
|
|
expect(keys).toEqual(
|
|
[EARLY, LATE].map((at) => userDayKey(at, "America/Los_Angeles")),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("recomputeUserMedicationCompliance", () => {
|
|
it("keys a late dose onto the person's own day", async () => {
|
|
const med = await prisma.medication.create({
|
|
data: { userId: USER, name: "Ramipril", dose: "5mg", active: true },
|
|
});
|
|
await prisma.medicationIntakeEvent.create({
|
|
data: {
|
|
userId: USER,
|
|
medicationId: med.id,
|
|
scheduledFor: LATE,
|
|
takenAt: LATE,
|
|
skipped: false,
|
|
source: "API",
|
|
},
|
|
});
|
|
|
|
await recomputeUserMedicationCompliance(USER, 30, "Europe/Berlin");
|
|
|
|
const rows = await prisma.medicationComplianceRollup.findMany({
|
|
where: { userId: USER },
|
|
select: { day: true, scheduled: true, taken: true },
|
|
});
|
|
expect(rows).toEqual([
|
|
{ day: key(D + 86_400_000), scheduled: 1, taken: 1 },
|
|
]);
|
|
});
|
|
});
|