mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
feat(modules): exclude disabled-module sections from doctor report and FHIR export
Resolve the per-user module map at report-build start and drop a disabled module's section/resources from the aggregated payload, so the PDF and the FHIR bundle reflect only the modules the user keeps. The module gate wins over the per-report section toggle: a module turned off cannot be shared even if its report toggle is on. Map report sections / FHIR resource groups to module keys: mood, sleep, cycle, labs via the existing section toggles; glucose panel + raw glucose series; recovery + stress wellness scores; workout activity series + strain score. Core clinical sections (weight, BP, pulse, medications) carry no module key and stay unconditional. FHIR resources inherit the exclusion because every builder derives from the gated aggregator output.
This commit is contained in:
1 parent
76d24dd684
commit
74bda80abb
2 files changed
+247
-3
No files matched your search
@@ -110,9 +110,60 @@ async function seedUserWithEveryDataType(username: string): Promise<string> {
|
||||
unit: "bpm",
|
||||
measuredAt,
|
||||
},
|
||||
// v1.18.0 module-gated data types (no `sections` toggle of their own).
|
||||
{
|
||||
userId: user.id,
|
||||
type: "BLOOD_GLUCOSE",
|
||||
value: 95,
|
||||
unit: "mg/dL",
|
||||
glucoseContext: "FASTING",
|
||||
measuredAt,
|
||||
},
|
||||
{
|
||||
userId: user.id,
|
||||
type: "RECOVERY_SCORE",
|
||||
value: 72,
|
||||
unit: "score",
|
||||
source: "WHOOP",
|
||||
measuredAt,
|
||||
},
|
||||
{
|
||||
userId: user.id,
|
||||
type: "STRESS_SCORE",
|
||||
value: 40,
|
||||
unit: "score",
|
||||
measuredAt,
|
||||
},
|
||||
{
|
||||
userId: user.id,
|
||||
type: "STRAIN_SCORE",
|
||||
value: 12,
|
||||
unit: "score",
|
||||
measuredAt,
|
||||
},
|
||||
{
|
||||
userId: user.id,
|
||||
type: "ACTIVITY_STEPS",
|
||||
value: 8200,
|
||||
unit: "count",
|
||||
measuredAt,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Lab result — module `labs` owns these; the `labs` section toggle is ON by
|
||||
// default so a present `labs` module surfaces the panel.
|
||||
await prisma.labResult.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
panel: "Lipids",
|
||||
analyte: "LDL",
|
||||
value: 110,
|
||||
unit: "mg/dL",
|
||||
takenAt: measuredAt,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.moodEntry.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
@@ -249,6 +300,107 @@ describe("doctor-report — per-section toggles", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("doctor-report — module enable/disable gating", () => {
|
||||
// A fully-enabled module map (the default-on shape `resolveModuleMap` would
|
||||
// return for an account that never toggled a module off).
|
||||
const ALL_ON = {
|
||||
cycle: true,
|
||||
mood: true,
|
||||
sleep: true,
|
||||
glucose: true,
|
||||
workouts: true,
|
||||
recovery: true,
|
||||
labs: true,
|
||||
achievements: true,
|
||||
coach: true,
|
||||
insights: true,
|
||||
doctorReport: true,
|
||||
} as const;
|
||||
|
||||
it("includes every module's data when all modules are enabled", async () => {
|
||||
const userId = await seedUserWithEveryDataType("dr-modules-all-on");
|
||||
|
||||
const data = await collectDoctorReportData(userId, RANGE, {
|
||||
// Opt mood in so the present-case asserts the full surface.
|
||||
sections: { mood: true },
|
||||
moduleMap: { ...ALL_ON },
|
||||
});
|
||||
|
||||
expect(Object.keys(data.glucoseStats).length).toBeGreaterThan(0);
|
||||
expect(data.measurements.ACTIVITY_STEPS).toBeDefined();
|
||||
expect(data.measurements.BLOOD_GLUCOSE).toBeDefined();
|
||||
expect(data.labResults).not.toBeNull();
|
||||
expect(data.mood).not.toBeNull();
|
||||
const scoreTypes = (data.wellnessScores ?? []).map((s) => s.type);
|
||||
expect(scoreTypes).toEqual(
|
||||
expect.arrayContaining([
|
||||
"RECOVERY_SCORE",
|
||||
"STRESS_SCORE",
|
||||
"STRAIN_SCORE",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("excludes a disabled module's section/resources from the payload", async () => {
|
||||
const userId = await seedUserWithEveryDataType("dr-modules-off");
|
||||
|
||||
const data = await collectDoctorReportData(userId, RANGE, {
|
||||
// The user opted mood in at the report level, but turned the modules off
|
||||
// — the module gate wins, so none of these surface.
|
||||
sections: { mood: true, sleep: true, labs: true },
|
||||
moduleMap: {
|
||||
...ALL_ON,
|
||||
glucose: false,
|
||||
sleep: false,
|
||||
workouts: false,
|
||||
recovery: false,
|
||||
labs: false,
|
||||
mood: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Glucose panel collapses entirely (stats + raw series).
|
||||
expect(data.glucoseStats).toEqual({});
|
||||
expect(data.glucoseRanges).toEqual({});
|
||||
expect(data.measurements.BLOOD_GLUCOSE).toBeUndefined();
|
||||
// Sleep series stripped (sections + module agree).
|
||||
expect(data.measurements.SLEEP_DURATION).toBeUndefined();
|
||||
// Workout activity series stripped.
|
||||
expect(data.measurements.ACTIVITY_STEPS).toBeUndefined();
|
||||
expect(data.stats.ACTIVITY_STEPS).toBeUndefined();
|
||||
// Labs panel gone.
|
||||
expect(data.labResults).toBeNull();
|
||||
// Mood never assembled.
|
||||
expect(data.mood).toBeNull();
|
||||
// Recovery + strain wellness scores dropped (recovery + workouts off).
|
||||
const scoreTypes = (data.wellnessScores ?? []).map((s) => s.type);
|
||||
expect(scoreTypes).not.toContain("RECOVERY_SCORE");
|
||||
expect(scoreTypes).not.toContain("STRESS_SCORE");
|
||||
expect(scoreTypes).not.toContain("STRAIN_SCORE");
|
||||
// Core clinical sections stay unconditional.
|
||||
expect(data.stats.WEIGHT).toBeDefined();
|
||||
expect(data.stats.BLOOD_PRESSURE_SYS).toBeDefined();
|
||||
expect(data.stats.PULSE).toBeDefined();
|
||||
expect(Object.keys(data.compliance).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("gates recovery and workouts independently (recovery off, workouts on)", async () => {
|
||||
const userId = await seedUserWithEveryDataType("dr-modules-recovery-off");
|
||||
|
||||
const data = await collectDoctorReportData(userId, RANGE, {
|
||||
moduleMap: { ...ALL_ON, recovery: false },
|
||||
});
|
||||
|
||||
const scoreTypes = (data.wellnessScores ?? []).map((s) => s.type);
|
||||
// Recovery readiness scores gone…
|
||||
expect(scoreTypes).not.toContain("RECOVERY_SCORE");
|
||||
expect(scoreTypes).not.toContain("STRESS_SCORE");
|
||||
// …but the workout-owned strain score + activity series stay.
|
||||
expect(scoreTypes).toContain("STRAIN_SCORE");
|
||||
expect(data.measurements.ACTIVITY_STEPS).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("doctor-report — MedicationAdministration export cap", () => {
|
||||
it("caps the administration set at the configured ceiling and flags the truncation", async () => {
|
||||
// The default ceiling is 5000; pin the operator-configurable
|
||||
|
||||
Reference in new issue
Block a user