feat(medications): rule on each export status for what it actually is

The seven documented statuses are four kinds of thing, so they get four
rulings instead of one.

Taken and Skipped state a decision about a dose and each maps onto something
already recorded here — a row with a take time, and a row marked skipped. That
second mapping was checked rather than assumed: skipped is documented as a
deliberate skip and held apart from autoMissed, which is the marker for a dose
nobody ever acted on, and the compliance re-fold counts a skipped slot in its
own column and never as taken. So a deliberate skip arrives as one.

Not Interacted, Not Logged and Unspecified are absences rather than
statements. Written as taken they would invent a dose; written as skipped they
would invent a decision, because not answering a notification is not choosing
not to take something; written as pending they would put outstanding doses
from years ago in front of someone today. Snoozed describes what happened to a
reminder. Notification Not Sent is about the software. None of the five
becomes a row.

All five are refused, but each under its own name, because "the file says
nothing about this dose" and "this row is about a reminder" are different
things and a person acts on them differently. Three reasons where there was
one, each with its own sentence in six locales.

An inactive medication's history imports and the medication stays inactive.
Preserving what happened and putting a medication back on the active list are
two acts, and only the first belongs to an import.
This commit is contained in:
Marc-André Bombeck committed 2026-07-26 16:37:10 +02:00
1 parent 36d589e11b
commit b627b92809
17 files changed
+216 -52

No files matched your search

@@ -129,7 +129,7 @@ describe("dose-history import — the reported export end to end", () => {
skipped: 8,
// The eight `Not Interacted` rows: the file states no decision about those
// doses, so there is nothing honest to write, and the count says which.
skipReasons: [{ reason: "status_not_recorded", count: 8 }],
skipReasons: [{ reason: "status_no_dose_information", count: 8 }],
});
// Every row of the file is accounted for by one of the two numbers.
expect(result!.imported + result!.skipped).toBe(3395);
@@ -223,7 +223,7 @@ describe("dose-history import — the reported export end to end", () => {
skipped: 3395,
skipReasons: [
{ reason: "already_recorded", count: 3387 },
{ reason: "status_not_recorded", count: 8 },
{ reason: "status_no_dose_information", count: 8 },
],
});
await expect(
@@ -252,6 +252,29 @@ describe("dose-history import — the reported export end to end", () => {
).resolves.toBe(2);
}, 240_000);
it("imports an inactive medication's history without putting it back on the active list", async () => {
const prisma = getPrismaClient();
const user = await seedRegimen();
// Standing in for a medication archived in the source app: the person has
// stopped taking it here too. Importing the history it names is one act;
// making the medication current again is a different one, and only the first
// was asked for. Someone re-adding it later should find the history waiting,
// not find it already re-added on their behalf.
await prisma.medication.updateMany({
where: { userId: user.id },
data: { active: false },
});
const { jobId } = await enqueue(user.id);
const result = await processMedicationIntakeImportJob(jobId);
expect(result!.imported).toBe(3387);
const stillInactive = await prisma.medication.count({
where: { userId: user.id, active: false },
});
expect(stillInactive).toBe(16);
}, 240_000);
it("re-folds the compliance rollup for every medication and day it touched", async () => {
const prisma = getPrismaClient();
const user = await seedRegimen();
@@ -167,7 +167,13 @@ describe("medication intake import job — concurrent retry", () => {
total: 4,
imported: 3,
skippedByReason: { duplicate_in_file: 1 },
touchedDays: ["2026-07-20", "2026-07-21"],
// A job can now cover a whole regimen, so a day whose rollup needs
// re-folding is addressed by medication AND day — two medications touched
// on one day are two rollup rows, which a bare day key cannot name.
touchedDays: [
`${medication.id} 2026-07-20`,
`${medication.id} 2026-07-21`,
],
rollupProcessed: 2,
});
expect(audits).toHaveLength(1);