mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
The window before a booked visit started at the last matching visit however far back it lay, while the health-report export covers 730 days and fell back to its 90-day default for anything longer. The card then offered a report the export did not build. The window now stops at the export's limit, read from one shared constant, and preparation.capped says when it did; the web says "The last two years" for it. The export clamps an over-long explicit range to the last 730 days before its end instead of falling back to 90 days, so a custom range or a linked window cannot shrink silently either, and its day count forgives the hour a clock change adds, so two years of local days count as 730.
228 lines
7.2 KiB
TypeScript
228 lines
7.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { EncounterKind } from "@/generated/prisma/client";
|
|
import { REPORT_MAX_RANGE_DAYS } from "@/lib/doctor-report-range";
|
|
import { daysBetweenDateKeys, shiftDateKey } from "@/lib/tz/format";
|
|
import {
|
|
PREPARATION_DEFAULT_DAYS,
|
|
resolvePreparation,
|
|
specialtyKey,
|
|
type PreparationVisit,
|
|
} from "../preparation";
|
|
|
|
const TZ = "Europe/Berlin";
|
|
const TODAY = "2026-10-21";
|
|
|
|
function visit(
|
|
id: string,
|
|
occurredAt: string,
|
|
over: {
|
|
kind?: EncounterKind;
|
|
practitionerId?: string | null;
|
|
name?: string;
|
|
specialty?: string | null;
|
|
} = {},
|
|
): PreparationVisit {
|
|
const practitionerId =
|
|
over.practitionerId === undefined ? null : over.practitionerId;
|
|
return {
|
|
id,
|
|
occurredAt: new Date(occurredAt),
|
|
kind: over.kind ?? "OTHER",
|
|
practitionerId,
|
|
practitioner: practitionerId
|
|
? { name: over.name ?? practitionerId, specialty: over.specialty ?? null }
|
|
: null,
|
|
};
|
|
}
|
|
|
|
const internist = (id: string, at: string) =>
|
|
visit(id, at, {
|
|
kind: "SPECIALIST",
|
|
practitionerId: "p-int",
|
|
name: "Internal practice",
|
|
specialty: "Internal medicine",
|
|
});
|
|
|
|
// The report from the issue: an internist a week ago, a dermatologist next.
|
|
const next = visit("next", "2026-10-23T08:00:00.000Z", {
|
|
kind: "SPECIALIST",
|
|
practitionerId: "p-derm",
|
|
name: "Skin practice",
|
|
specialty: "Dermatology",
|
|
});
|
|
|
|
function resolve(target: PreparationVisit, done: PreparationVisit[]) {
|
|
return resolvePreparation({ next: target, done, tz: TZ, today: TODAY });
|
|
}
|
|
|
|
describe("the window before a booked visit", () => {
|
|
it("starts at the last visit with the same practitioner", () => {
|
|
const prep = resolve(next, [
|
|
internist("int", "2026-10-16T09:00:00.000Z"),
|
|
visit("derm", "2026-03-02T09:00:00.000Z", {
|
|
kind: "SPECIALIST",
|
|
practitionerId: "p-derm",
|
|
name: "Skin practice",
|
|
specialty: "Dermatology",
|
|
}),
|
|
]);
|
|
expect(prep).toEqual({
|
|
from: "2026-03-02",
|
|
to: "2026-10-23",
|
|
basis: "practitioner",
|
|
since: {
|
|
id: "derm",
|
|
occurredAt: "2026-03-02T09:00:00.000Z",
|
|
kind: "SPECIALIST",
|
|
practitionerName: "Skin practice",
|
|
specialty: "Dermatology",
|
|
},
|
|
capped: false,
|
|
});
|
|
});
|
|
|
|
it("else at the last visit in the same specialty, compared without case", () => {
|
|
const prep = resolve(next, [
|
|
internist("int", "2026-10-16T09:00:00.000Z"),
|
|
visit("other-derm", "2025-11-20T09:00:00.000Z", {
|
|
kind: "SPECIALIST",
|
|
practitionerId: "p-derm-old",
|
|
name: "Former skin practice",
|
|
specialty: " dermatology ",
|
|
}),
|
|
]);
|
|
expect(prep.basis).toBe("specialty");
|
|
expect(prep.from).toBe("2025-11-20");
|
|
expect(prep.since?.id).toBe("other-derm");
|
|
expect(prep.since?.specialty).toBe("dermatology");
|
|
});
|
|
|
|
it("never falls back to the last visit of any kind", () => {
|
|
// The issue: one week of internist data offered for the dermatologist.
|
|
const prep = resolve(next, [internist("int", "2026-10-16T09:00:00.000Z")]);
|
|
expect(prep).toEqual({
|
|
from: "2026-07-23",
|
|
to: "2026-10-23",
|
|
basis: "default",
|
|
since: null,
|
|
capped: false,
|
|
});
|
|
expect(PREPARATION_DEFAULT_DAYS).toBe(90);
|
|
});
|
|
|
|
it("matches on kind only for a kind that names one line of care", () => {
|
|
const therapy = visit("next-t", "2026-10-25T08:00:00.000Z", {
|
|
kind: "THERAPY",
|
|
});
|
|
const done = [
|
|
visit("acute", "2026-10-18T09:00:00.000Z", { kind: "ACUTE" }),
|
|
visit("t1", "2026-10-11T09:00:00.000Z", { kind: "THERAPY" }),
|
|
];
|
|
const prep = resolve(therapy, done);
|
|
expect(prep.basis).toBe("kind");
|
|
expect(prep.from).toBe("2026-10-11");
|
|
|
|
// A specialist visit with no specialty says nothing about which one.
|
|
const specialist = visit("next-s", "2026-10-25T08:00:00.000Z", {
|
|
kind: "SPECIALIST",
|
|
});
|
|
expect(
|
|
resolve(specialist, [
|
|
visit("s1", "2026-10-11T09:00:00.000Z", { kind: "SPECIALIST" }),
|
|
]).basis,
|
|
).toBe("default");
|
|
// Neither does "other", the kind nobody picked.
|
|
const other = visit("next-o", "2026-10-25T08:00:00.000Z");
|
|
expect(
|
|
resolve(other, [visit("o1", "2026-10-11T09:00:00.000Z")]).basis,
|
|
).toBe("default");
|
|
});
|
|
|
|
it("prefers the practitioner over a newer visit in the same specialty", () => {
|
|
const prep = resolve(next, [
|
|
visit("derm-b", "2026-09-01T09:00:00.000Z", {
|
|
kind: "SPECIALIST",
|
|
practitionerId: "p-derm-b",
|
|
specialty: "Dermatology",
|
|
}),
|
|
visit("derm-a", "2026-01-15T09:00:00.000Z", {
|
|
kind: "SPECIALIST",
|
|
practitionerId: "p-derm",
|
|
specialty: "Dermatology",
|
|
}),
|
|
]);
|
|
expect(prep.basis).toBe("practitioner");
|
|
expect(prep.since?.id).toBe("derm-a");
|
|
});
|
|
|
|
it("caps a window older than a report covers, still naming its visit", () => {
|
|
const derm = (id: string, at: string) =>
|
|
visit(id, at, {
|
|
kind: "SPECIALIST",
|
|
practitionerId: "p-derm",
|
|
name: "Skin practice",
|
|
specialty: "Dermatology",
|
|
});
|
|
const prep = resolve(next, [derm("old", "2023-04-03T09:00:00.000Z")]);
|
|
expect(prep).toMatchObject({
|
|
from: shiftDateKey("2026-10-23", -(REPORT_MAX_RANGE_DAYS - 1)),
|
|
to: "2026-10-23",
|
|
basis: "practitioner",
|
|
capped: true,
|
|
since: { id: "old", occurredAt: "2023-04-03T09:00:00.000Z" },
|
|
});
|
|
expect(daysBetweenDateKeys(prep.from, prep.to) + 1).toBe(
|
|
REPORT_MAX_RANGE_DAYS,
|
|
);
|
|
// One day inside the limit is left as it is.
|
|
const edge = shiftDateKey("2026-10-23", -(REPORT_MAX_RANGE_DAYS - 1));
|
|
expect(
|
|
resolve(next, [derm("edge", `${edge}T09:00:00.000Z`)]),
|
|
).toMatchObject({ from: edge, capped: false });
|
|
expect(
|
|
resolve(next, [derm("over", `${shiftDateKey(edge, -1)}T09:00:00.000Z`)]),
|
|
).toMatchObject({ from: edge, capped: true });
|
|
});
|
|
|
|
it("only counts visits before the booked one", () => {
|
|
const later = visit("later", "2026-10-24T09:00:00.000Z", {
|
|
practitionerId: "p-derm",
|
|
});
|
|
expect(resolve(next, [later]).basis).toBe("default");
|
|
});
|
|
|
|
it("starts on the matched visit's own day in the record's zone", () => {
|
|
// 23:30 UTC is the next calendar day in Berlin.
|
|
const prep = resolve(next, [
|
|
visit("late", "2026-05-04T23:30:00.000Z", { practitionerId: "p-derm" }),
|
|
]);
|
|
expect(prep.from).toBe("2026-05-05");
|
|
});
|
|
|
|
it("ends on the booked visit's own day in the record's zone, whatever the rule", () => {
|
|
// 23:30 UTC on the 22nd is the 23rd in Berlin.
|
|
const late = visit("late-next", "2026-10-22T23:30:00.000Z", {
|
|
practitionerId: "p-derm",
|
|
});
|
|
expect(resolve(late, []).to).toBe("2026-10-23");
|
|
expect(
|
|
resolve(late, [
|
|
visit("before", "2026-05-04T08:00:00.000Z", {
|
|
practitionerId: "p-derm",
|
|
}),
|
|
]).to,
|
|
).toBe("2026-10-23");
|
|
});
|
|
});
|
|
|
|
describe("specialty comparison", () => {
|
|
it("folds case and spacing, and nothing else", () => {
|
|
expect(specialtyKey(" Internal Medicine ")).toBe("internal medicine");
|
|
expect(specialtyKey("")).toBeNull();
|
|
expect(specialtyKey(" ")).toBeNull();
|
|
expect(specialtyKey(null)).toBeNull();
|
|
expect(specialtyKey("Dermatology")).not.toBe(specialtyKey("Skin doctor"));
|
|
});
|
|
});
|