diff --git a/src/lib/export/full-backup-payload.ts b/src/lib/export/full-backup-payload.ts index 89f63c045..55c0d83b4 100644 --- a/src/lib/export/full-backup-payload.ts +++ b/src/lib/export/full-backup-payload.ts @@ -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 ? { diff --git a/tests/integration/backup-round-trip.test.ts b/tests/integration/backup-round-trip.test.ts index 8376beb3e..516d48541 100644 --- a/tests/integration/backup-round-trip.test.ts +++ b/tests/integration/backup-round-trip.test.ts @@ -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()); + }); + } });