mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
Emit one `MedicationAdministration` per intake the user actually actioned — `completed` for a taken dose (`effectiveDateTime = takenAt`), `not-done` for an explicit skip (`effectiveDateTime = scheduledFor`). Pending / missed slots and soft-deleted tombstones are never exported, so the record only asserts administrations that happened. Each administration reuses the statement's ATC/RxNorm + text concept, so it is self-describing without a cross-resource reference. A `dosage` is emitted only when a structured dose Quantity is available (resolved from the dose-change history in effect at the administration instant) — a dosage carrying only `.text` would violate the R4 dose-or-rate invariant. Route + injection site ride along as text-only concepts. The administrations are referenced from the existing Medications section, so the document's two-section graph is preserved. `FhirMedicationAdministration` is new; `FhirDosage` gains `dose`, `route`, and `site`. The source rows come from the same aggregator the PDF consumes, keeping FHIR and PDF aligned by construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit dd22512f56b7b6dfa42386bbc88d1ced1d71255d)
135 lines
4.5 KiB
TypeScript
135 lines
4.5 KiB
TypeScript
/**
|
|
* v1.9.0 — `medicationAdministrations` aggregation + drug-code carry.
|
|
*
|
|
* Asserts the source rows the FHIR `MedicationAdministration` builder
|
|
* consumes are assembled correctly from `MedicationIntakeEvent`:
|
|
* - a taken row → `completed` with `effectiveAt = takenAt`,
|
|
* - an explicit skip → `not-done` with `effectiveAt = scheduledFor`,
|
|
* - a pending / missed slot (no `takenAt`, not skipped) → omitted,
|
|
* - a soft-deleted tombstone → omitted,
|
|
* - the structured dose-in-effect resolved from `MedicationDoseChange`,
|
|
* - the medication's `atcCode` / `rxNormCode` carried onto both the
|
|
* `medications[]` concept and each administration row.
|
|
*/
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
|
|
process.env.ENCRYPTION_KEY ??=
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
|
|
import { collectDoctorReportData } from "@/lib/doctor-report-data";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
const RANGE = {
|
|
start: new Date("2026-01-01T00:00:00.000Z"),
|
|
end: new Date("2026-06-01T00:00:00.000Z"),
|
|
days: 152,
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
});
|
|
|
|
describe("medicationAdministrations aggregation (v1.9.0)", () => {
|
|
it("maps acted intakes, omits missed + tombstoned, resolves dose-in-effect", async () => {
|
|
const prisma = getPrismaClient();
|
|
const user = await prisma.user.create({
|
|
data: { username: "admin-agg", email: "admin-agg@example.test" },
|
|
});
|
|
|
|
const med = await prisma.medication.create({
|
|
data: {
|
|
userId: user.id,
|
|
name: "Mounjaro",
|
|
dose: "10mg",
|
|
active: true,
|
|
treatmentClass: "GLP1",
|
|
deliveryForm: "INJECTION",
|
|
atcCode: "A10BX10",
|
|
rxNormCode: "2601723",
|
|
},
|
|
});
|
|
|
|
// Dose-change history: 5mg from Feb, stepped to 10mg from Apr.
|
|
await prisma.medicationDoseChange.createMany({
|
|
data: [
|
|
{
|
|
medicationId: med.id,
|
|
effectiveFrom: new Date("2026-02-01T00:00:00.000Z"),
|
|
doseValue: 5,
|
|
doseUnit: "mg",
|
|
},
|
|
{
|
|
medicationId: med.id,
|
|
effectiveFrom: new Date("2026-04-01T00:00:00.000Z"),
|
|
doseValue: 10,
|
|
doseUnit: "mg",
|
|
},
|
|
],
|
|
});
|
|
|
|
// A taken dose in March (dose-in-effect = 5mg) with a site.
|
|
await prisma.medicationIntakeEvent.create({
|
|
data: {
|
|
userId: user.id,
|
|
medicationId: med.id,
|
|
scheduledFor: new Date("2026-03-15T08:00:00.000Z"),
|
|
takenAt: new Date("2026-03-15T08:12:00.000Z"),
|
|
injectionSite: "ABDOMEN_LEFT",
|
|
},
|
|
});
|
|
// A skipped dose in April.
|
|
await prisma.medicationIntakeEvent.create({
|
|
data: {
|
|
userId: user.id,
|
|
medicationId: med.id,
|
|
scheduledFor: new Date("2026-04-15T08:00:00.000Z"),
|
|
skipped: true,
|
|
},
|
|
});
|
|
// A missed slot (no takenAt, not skipped) — must be omitted.
|
|
await prisma.medicationIntakeEvent.create({
|
|
data: {
|
|
userId: user.id,
|
|
medicationId: med.id,
|
|
scheduledFor: new Date("2026-05-01T08:00:00.000Z"),
|
|
},
|
|
});
|
|
// A soft-deleted (tombstoned) taken dose — must be omitted.
|
|
await prisma.medicationIntakeEvent.create({
|
|
data: {
|
|
userId: user.id,
|
|
medicationId: med.id,
|
|
scheduledFor: new Date("2026-05-10T08:00:00.000Z"),
|
|
takenAt: new Date("2026-05-10T08:05:00.000Z"),
|
|
deletedAt: new Date("2026-05-11T00:00:00.000Z"),
|
|
},
|
|
});
|
|
|
|
const data = await collectDoctorReportData(user.id, RANGE);
|
|
|
|
// Drug codes carried onto the medication concept source.
|
|
expect(data.medications[0].atcCode).toBe("A10BX10");
|
|
expect(data.medications[0].rxNormCode).toBe("2601723");
|
|
|
|
const admins = data.medicationAdministrations ?? [];
|
|
// Only the taken + skipped rows survive.
|
|
expect(admins).toHaveLength(2);
|
|
|
|
const taken = admins.find((a) => a.status === "completed");
|
|
expect(taken).toBeDefined();
|
|
expect(taken?.effectiveAt).toBe("2026-03-15T08:12:00.000Z");
|
|
expect(taken?.injectionSite).toBe("ABDOMEN_LEFT");
|
|
expect(taken?.deliveryForm).toBe("INJECTION");
|
|
expect(taken?.atcCode).toBe("A10BX10");
|
|
// Dose-in-effect at 2026-03-15 is the 5mg step (not yet 10mg).
|
|
expect(taken?.dose).toEqual({ value: 5, unit: "mg" });
|
|
|
|
const skipped = admins.find((a) => a.status === "not-done");
|
|
expect(skipped).toBeDefined();
|
|
// A skip uses scheduledFor and records no consumed dose.
|
|
expect(skipped?.effectiveAt).toBe("2026-04-15T08:00:00.000Z");
|
|
expect(skipped?.dose).toBeNull();
|
|
});
|
|
});
|