From e452dd62e226b194805e1a0d66e0e4ab6d156f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 20 Oct 2026 03:35:36 +0200 Subject: [PATCH] fix(telegram): snooze a skipped dose to the end of the person's own day MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skip button set the rest-of-day snooze with `setHours(23, 59, …)` in the process zone, which runs in UTC, so a skip after midnight UTC but before local midnight east of it silenced the next local day's reminders too, and one west of it ended the snooze hours early. The snooze now ends at the last instant of the account's local day. --- src/app/api/telegram/webhook/__tests__/route.test.ts | 10 +++++++--- src/lib/telegram-webhook-handlers.ts | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) 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