fix(backup): keep a medication's creation date through a portable restore

A portable export left the medication's creation instant out, so a restore
stamped the restore time instead. The creation instant is where a
medication's expected doses start, so the whole restored history then read
as predating the medication: its missed doses dropped out of every
adherence rate. The portable file now carries the creation instant as the
disaster-recovery file already did, and the restore, which already honoured
it, brings it back. A round trip pins both file purposes.

Refs #1028
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 19:48:01 +02:00
1 parent a6b8c61a82
commit 8642d63b82
2 files changed
+64 -1

No files matched your search

+5 -1
View File
@@ -998,13 +998,17 @@ export async function buildFullBackupPayload(
reorderLeadDays: m.reorderLeadDays,
externalSource: m.externalSource,
externalId: m.externalId,
createdAt: m.createdAt.toISOString(),
updatedAt: m.updatedAt.toISOString(),
}
: {}),
name: m.name,
dose: m.dose,
active: m.active,
// Both purposes carry the creation instant: it is the floor of the
// medication's expected slots, so a restore stamped with the restore
// time would read the whole restored history as predating the
// medication and drop its misses from every rate (#1028).
createdAt: m.createdAt.toISOString(),
schedules: m.schedules.map((s) => ({
...(disasterRecovery
? {
@@ -2890,4 +2890,63 @@ describe("every model the plan claims two-ended survives a real restore", () =>
UNREADABLE_EXPORT_MARKER,
);
});
/**
* #1028 — a medication's creation instant is the floor of its expected
* slots: every adherence surface counts a slot before it only when a
* recorded dose claims it. A restore that stamped the restore time as the
* creation turned the whole restored history into "before the medication
* existed", so its misses dropped out of the rate. Both file purposes must
* bring the creation instant back.
*/
for (const purpose of ["portable-export", "disaster-recovery"] as const) {
it(`restores a medication's creation instant from a ${purpose} file`, async () => {
const prisma = getPrismaClient();
await seedAdminSession(prisma);
await createOwner(prisma);
const createdAt = AT("2025-11-03T16:05:00.000Z");
await prisma.medication.create({
data: {
userId: OWNER_ID,
name: "Metformin",
dose: "500mg",
createdAt,
schedules: {
create: {
windowStart: "08:00",
windowEnd: "08:00",
timesOfDay: ["08:00"],
},
},
},
});
const { payload } = await buildFullBackupPayload(prisma, OWNER_ID, {
purpose,
});
await prisma.user.delete({ where: { id: OWNER_ID } });
await createOwner(prisma);
const backup = await prisma.dataBackup.create({
data: {
userId: OWNER_ID,
type: "TWO_ENDED_ROUND_TRIP",
data: encrypt(JSON.stringify(payload)),
},
});
const response = await POST(
new Request(`http://localhost/api/admin/backups/${backup.id}/restore`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ confirm: "RESTORE" }),
}) as never,
{ params: Promise.resolve({ id: backup.id }) },
);
expect(response.status, JSON.stringify(await response.json())).toBe(200);
const restored = await prisma.medication.findFirstOrThrow({
where: { userId: OWNER_ID, name: "Metformin" },
});
expect(restored.createdAt.toISOString()).toBe(createdAt.toISOString());
});
}
});