diff --git a/src/app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts b/src/app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts index d9774e10d..d2c02ed12 100644 --- a/src/app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts +++ b/src/app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts @@ -233,6 +233,25 @@ describe("POST /api/medications/[id]/schedule-revisions", () => { expect(json.error).toMatch(/current plan/i); }); + it("422s when no revision exists and the era reaches past createdAt", async () => { + // With zero archived revisions the live plan has covered everything + // since `createdAt` (2026-01-10); a manual era ending inside the + // tracked window would re-score live compliance against the manual + // snapshot. The boundary is createdAt, not "now". + const res = await POST( + postReq({ + ...VALID_BODY, + validFrom: "2026-01-01T00:00:00.000Z", + validUntil: "2026-02-01T00:00:00.000Z", + }), + ROUTE_CTX, + ); + expect(res.status).toBe(422); + const json = await res.json(); + expect(json.error).toMatch(/current plan/i); + expect(prisma.medicationScheduleRevision.create).not.toHaveBeenCalled(); + }); + it("creates a MANUAL revision with a daily snapshot entry", async () => { vi.mocked(prisma.medicationScheduleRevision.create).mockResolvedValue({ id: "rev-new", diff --git a/src/app/api/medications/[id]/schedule-revisions/route.ts b/src/app/api/medications/[id]/schedule-revisions/route.ts index 0a4ca4259..c7cd2075e 100644 --- a/src/app/api/medications/[id]/schedule-revisions/route.ts +++ b/src/app/api/medications/[id]/schedule-revisions/route.ts @@ -137,6 +137,14 @@ export const POST = apiHandler( const guard = await assertMedicationOwnership(id, user.id); if (guard) return guard; + const med = await prisma.medication.findUnique({ + where: { id }, + select: { createdAt: true }, + }); + if (!med) { + return apiError("Medication not found", 404); + } + const { data: body, error: jsonError } = await safeJson(request, { maxBytes: 16 * 1024, }); @@ -162,13 +170,18 @@ export const POST = apiHandler( }); // "Before the current plan": the live era began at the newest - // archived `validUntil`; with no archive it spans up to now, so a - // manual era may cover anything up to this instant. + // archived `validUntil`; with no archive the live plan has covered + // everything since the medication was created (the era splitter + // reads `[newest validUntil, ∞)` — or the whole range when no + // revision exists), so a manual era must end at or before + // `createdAt`. Allowing it to reach "now" would let the manual + // snapshot swallow tracked live history and re-score compliance + // against the wrong plan. const liveStart = existing.reduce( (latest, r) => (r.validUntil > latest ? r.validUntil : latest), new Date(0), ); - const liveBoundary = existing.length > 0 ? liveStart : new Date(); + const liveBoundary = existing.length > 0 ? liveStart : med.createdAt; if (validUntil.getTime() > liveBoundary.getTime()) { return apiError( "A manual era must end before the current plan begins",