diff --git a/src/app/api/telegram/webhook/__tests__/route.test.ts b/src/app/api/telegram/webhook/__tests__/route.test.ts index 57c37c310..0516e8480 100644 --- a/src/app/api/telegram/webhook/__tests__/route.test.ts +++ b/src/app/api/telegram/webhook/__tests__/route.test.ts @@ -210,6 +210,7 @@ beforeEach(() => { }); afterEach(() => { + vi.useRealTimers(); if (ORIGINAL_SECRET === undefined) delete process.env.TELEGRAM_WEBHOOK_SECRET; else process.env.TELEGRAM_WEBHOOK_SECRET = ORIGINAL_SECRET; }); @@ -396,7 +397,11 @@ describe("Telegram webhook — callback dispatch", () => { expect(ackText).toMatch(/1 hour/i); }); - it("'skip:' creates an intake event with skipped=true and snoozes through end-of-day", async () => { + it("'skip:' creates an intake event with skipped=true and snoozes through the person's own end of day", async () => { + // 23:30 UTC is already 01:30 the next day in Berlin: the snooze ends at + // Berlin's next midnight, not at the end of the UTC day half an hour on. + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-06-10T23:30:00.000Z")); const res = await POST(tgRequest(callbackUpdate("skip:med-1"))); expect(res.status).toBe(200); @@ -417,8 +422,7 @@ describe("Telegram webhook — callback dispatch", () => { const updateArgs = vi.mocked(prisma.medication.update).mock.calls[0][0]; const snoozedUntil = (updateArgs.data as { snoozedUntil: Date }) .snoozedUntil; - expect(snoozedUntil.getHours()).toBe(23); - expect(snoozedUntil.getMinutes()).toBe(59); + expect(snoozedUntil.toISOString()).toBe("2026-06-11T21:59:59.999Z"); // Skip row + rest-of-day snooze commit atomically. expect(prisma.$transaction).toHaveBeenCalledTimes(1); // v1.16.10 — an explicit skip refunds a previously-consumed stamp diff --git a/src/lib/telegram-webhook-handlers.ts b/src/lib/telegram-webhook-handlers.ts index 474d54dec..cef417bb1 100644 --- a/src/lib/telegram-webhook-handlers.ts +++ b/src/lib/telegram-webhook-handlers.ts @@ -20,7 +20,7 @@ import { consumeForIntake, restoreForIntake, } from "@/lib/medications/inventory/consumption"; -import { localHmAsUtc } from "@/lib/tz/local-day"; +import { getUserTodayBounds, localHmAsUtc } from "@/lib/tz/local-day"; import { wallClockInTz } from "@/lib/tz/wall-clock"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; import { @@ -597,10 +597,12 @@ export async function handleCallback(update: TelegramUpdate) { let skippedScheduledFor: Date | null = null; if (!existing) { - const endOfDay = new Date(); - endOfDay.setHours(23, 59, 59, 999); const skipMoment = new Date(); const tz = user.timezone || DEFAULT_TIMEZONE; + // The rest of the person's own day, not of the server's: the process + // runs in UTC, so a Berlin skip after 01:00 used to snooze the next + // local morning's reminders as well. + const endOfDay = getUserTodayBounds(skipMoment, tz).end; // v1.16.9 — a Telegram skip answers a specific dose prompt, so it // must converge onto the slot's pending row like a take does. The