fix(export): cap the FHIR MedicationAdministration count per export

The administration set was windowed only to the report period (up to 730
days). A chronic medication dosed several times a day across that window,
multiplied across several medications, can still materialise thousands of
MedicationAdministration resources into a single Bundle — bounded by the
window but unbounded by dose frequency.

Cap the set at the most-recent 1000 acted intakes overall. The omitted
rows are the oldest in the window, so recent adherence is preserved
intact. When the cap trims rows the aggregator records the original and
included counts and the FHIR document narrative discloses the
truncation, so the export is honest about the omitted history rather
than silently dropping it. Cover the cap in the report aggregator and
the narrative note in the bundle builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit c6ce7df2bf6efb5e2c9d948d8f189d95d1289758)
This commit is contained in:
Marc-André Bombeck committed 2026-06-02 17:34:52 +02:00
1 parent f0f2caab79
commit ada9006d9c
4 files changed
+151 -1

No files matched your search

+45 -1
View File
@@ -129,6 +129,18 @@ export interface DoctorReportData {
/** Delivery form — ORAL / INJECTION / OTHER — for the route mapping. */
deliveryForm: string | null;
}>;
/**
* v1.9.0 — set when {@link MAX_MEDICATION_ADMINISTRATIONS} trimmed the
* administration set. `total` is the full acted-intake count over the
* window; `included` is how many (the most-recent) survived the cap.
* Null/absent when nothing was trimmed. The FHIR builder discloses
* this in the document narrative so the export is honest about the
* omitted (oldest) rows. Optional so pre-v1.9.0 fixtures still typecheck.
*/
medicationAdministrationsTruncation?: {
total: number;
included: number;
} | null;
mood: DoctorReportMood | null;
/**
* v1.4.25 W4d — GLP-1 therapy section. Populated when the user has at
@@ -194,6 +206,21 @@ const MIN_RANGE_DAYS = 1;
const MAX_RANGE_DAYS = 730;
const DEFAULT_RANGE_DAYS = 90;
/**
* v1.9.0 — hard ceiling on the number of `MedicationAdministration`
* source rows materialised per export. The report window is already
* bounded at {@link MAX_RANGE_DAYS} (730 days), but a chronic medication
* dosed several times a day across that window — multiplied by several
* such medications — can still produce thousands of administration
* resources in a single Bundle (e.g. 4×/day × 730 days ≈ 2 920 per med).
* Cap at the MOST-RECENT 1 000 acted intakes overall: that preserves the
* recent-adherence picture a clinician cares about while bounding the
* in-memory array and the serialised Bundle. When the cap trims rows the
* aggregator flags it so the FHIR narrative can disclose the truncation
* rather than silently dropping history.
*/
const MAX_MEDICATION_ADMINISTRATIONS = 1000;
export interface DoctorReportRange {
/** Start of the reporting window (UTC, inclusive). */
start: Date;
@@ -508,6 +535,20 @@ export async function collectDoctorReportData(
});
}
// v1.9.0 — bound the administration set. `intakeEvents` is ordered
// `scheduledFor: asc`, so the most-recent acted rows are at the tail;
// keep the last N and flag the trim so the FHIR narrative can disclose
// it. The omitted rows are the OLDEST in the window — recent adherence
// is preserved intact.
const totalAdministrations = medicationAdministrations.length;
const medicationAdministrationsTruncated =
totalAdministrations > MAX_MEDICATION_ADMINISTRATIONS;
const cappedAdministrations = medicationAdministrationsTruncated
? medicationAdministrations.slice(
totalAdministrations - MAX_MEDICATION_ADMINISTRATIONS,
)
: medicationAdministrations;
// Mood summary.
const moodScores = moodEntries.map((e) => e.score);
const mood: DoctorReportMood | null =
@@ -724,7 +765,10 @@ export async function collectDoctorReportData(
label: s.label,
})),
})),
medicationAdministrations,
medicationAdministrations: cappedAdministrations,
medicationAdministrationsTruncation: medicationAdministrationsTruncated
? { total: totalAdministrations, included: cappedAdministrations.length }
: null,
mood,
glp1,
};
@@ -304,6 +304,47 @@ describe("buildFhirDocumentBundle", () => {
}
});
it("discloses an administration truncation in the Composition narrative", () => {
const bundle = buildFhirDocumentBundle(
makeData({
medicationAdministrations: [
{
medicationName: "Mounjaro",
effectiveAt: "2026-04-30T08:00:00.000Z",
status: "completed",
doseText: "10mg",
dose: { value: 10, unit: "mg" },
injectionSite: null,
atcCode: null,
rxNormCode: null,
deliveryForm: "INJECTION",
},
],
medicationAdministrationsTruncation: { total: 2920, included: 1000 },
}),
{ insuranceNumber: null },
FIXED_NOW,
);
const composition = bundle.entry[0].resource;
if (composition.resourceType === "Composition") {
expect(composition.text?.div).toContain(
"Medication administrations truncated: showing the most recent 1000 of 2920",
);
}
});
it("omits the truncation sentence when nothing was capped", () => {
const bundle = buildFhirDocumentBundle(
makeData({ medicationAdministrationsTruncation: null }),
{ insuranceNumber: null },
FIXED_NOW,
);
const composition = bundle.entry[0].resource;
if (composition.resourceType === "Composition") {
expect(composition.text?.div).not.toContain("truncated");
}
});
it("maps weight to LOINC 29463-7 / UCUM kg with the latest reading", () => {
const bundle = buildFhirDocumentBundle(
makeData(),
+9
View File
@@ -534,10 +534,19 @@ export function buildFhirDocumentBundle(
}
// --- Composition (leading "cover" resource) ----------------------------
// v1.9.0 — when the aggregator capped the administration set, disclose
// it in the narrative so the export is honest: it carries the
// most-recent N of M acted intakes, the oldest having been omitted.
const truncation = data.medicationAdministrationsTruncation;
const narrativeText = [
`Health record for ${escapeXml(displayName ?? "patient")}.`,
`Reporting period ${data.period.start.slice(0, 10)} to ${data.period.end.slice(0, 10)}.`,
`${observationRefs.length} observation(s), ${medicationRefs.length} medication(s), ${administrationRefs.length} administration(s).`,
...(truncation
? [
`Medication administrations truncated: showing the most recent ${truncation.included} of ${truncation.total} recorded; older entries omitted.`,
]
: []),
].join(" ");
const composition: FhirComposition = {
@@ -239,3 +239,59 @@ describe("doctor-report — per-section toggles", () => {
expect(Object.keys(data.compliance).length).toBeGreaterThan(0);
});
});
describe("doctor-report — MedicationAdministration export cap", () => {
it("caps the administration set at 1000 most-recent rows and flags the truncation", async () => {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "dr-admin-cap",
email: "dr-admin-cap@example.test",
heightCm: 180,
},
});
const med = await prisma.medication.create({
data: { userId: user.id, name: "Metformin", dose: "500 mg", active: true },
});
// 1001 acted (taken) intakes spread across the window so the cap
// must trim exactly one — the OLDEST — keeping the most-recent 1000.
const rangeMs = RANGE.end.getTime() - RANGE.start.getTime();
const rows = Array.from({ length: 1001 }, (_, i) => {
const at = new Date(
RANGE.start.getTime() + Math.floor((rangeMs * i) / 1001),
);
return {
userId: user.id,
medicationId: med.id,
scheduledFor: at,
takenAt: at,
};
});
await prisma.medicationIntakeEvent.createMany({ data: rows });
const data = await collectDoctorReportData(user.id, RANGE);
expect(data.medicationAdministrations).toBeDefined();
expect(data.medicationAdministrations?.length).toBe(1000);
expect(data.medicationAdministrationsTruncation).toEqual({
total: 1001,
included: 1000,
});
// The OLDEST row was dropped — the earliest surviving administration
// is strictly after the window start.
const first = data.medicationAdministrations?.[0];
expect(new Date(first!.effectiveAt).getTime()).toBeGreaterThan(
RANGE.start.getTime(),
);
});
it("leaves the set untouched (no truncation flag) below the cap", async () => {
const userId = await seedUserWithEveryDataType("dr-admin-no-cap");
const data = await collectDoctorReportData(userId, RANGE);
expect(data.medicationAdministrations?.length).toBe(1);
expect(data.medicationAdministrationsTruncation ?? null).toBeNull();
});
});