feat(18-03): route all-day reminder TZ through stored household_timezone

- reminderScheduler.ts: add import { getHouseholdTimezone } from '../lib/householdTimezone.js'
  and replace bare process.env.TZ ?? Intl... at line 247 with await getHouseholdTimezone(db)
- outboxWorker.ts: add same import and replace BOTH bare tz lookups at the update-branch
  (~line 501) and create-branch (~line 607) with await getHouseholdTimezone(db)
- D-05 satisfied: all three all-day sites now read from the single stored accessor
- D-06 satisfied: getHouseholdTimezone falls back to process.env.TZ → Intl when unset;
  existing process.env.TZ-pinned tests pass unchanged
- D-07 satisfied: eventDateTime.ts and hydrateEvents.ts are not modified
- outboxWorker.test.ts: update wireMockChain() to handle app_config table with where().limit()
  chain returning empty rows (D-06 fallback), so existing CAL-13 all-day test stays green
- reminderScheduler.test.ts: update mockTwoQueries to mock the new third db.select() call
  (getHouseholdTimezone) returning no row (D-06 fallback), keeping all 37 existing tests green
- All 76 broker tests pass; tsc --noEmit clean
This commit is contained in:
Lucas Berger
2026-06-14 22:31:37 -04:00
parent 94daca3c7a
commit c80845cdba
4 changed files with 35 additions and 7 deletions
+7 -2
View File
@@ -43,6 +43,7 @@ import {
} from './vevent.js';
import type { FastmailClient } from './client.js';
import { dispatchEventChange } from '../lib/eventChangeDispatcher.js';
import { getHouseholdTimezone } from '../lib/householdTimezone.js';
import { onOutboxDrain } from '../lib/outboxTrigger.js';
// ── Constants (D-07) ────────────────────────────────────────────────────────
@@ -498,7 +499,9 @@ async function dispatchRow(row: OutboxRow): Promise<DispatchResult> {
fields.allDay &&
fields.start
) {
const tz = process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
// D-05: route through the single stored-TZ accessor (no inline fallback duplicated here).
// D-06: getHouseholdTimezone falls back to process.env.TZ → Intl when no row is stored.
const tz = await getHouseholdTimezone(db);
const leadDays = fields.reminderLeadMinutes / 1440;
allDayAlertInstantUtcUpdate = computeAlertInstantUtc(fields.start, leadDays, tz);
}
@@ -604,7 +607,9 @@ async function dispatchRow(row: OutboxRow): Promise<DispatchResult> {
// carries an explicit picker value or no reminder at all; no preserve path needed).
let allDayAlertInstantUtcCreate: Date | undefined;
if (fields.reminderLeadMinutes != null && fields.allDay && fields.start) {
const tz = process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
// D-05: route through the single stored-TZ accessor (no inline fallback duplicated here).
// D-06: getHouseholdTimezone falls back to process.env.TZ → Intl when no row is stored.
const tz = await getHouseholdTimezone(db);
const leadDays = fields.reminderLeadMinutes / 1440;
allDayAlertInstantUtcCreate = computeAlertInstantUtc(fields.start, leadDays, tz);
}
+4 -1
View File
@@ -48,6 +48,7 @@ import { calendarEvents, pushSubscriptions } from '../db/schema.js';
import { dispatchPush } from '../lib/pushDispatcher.js';
import { computeAlertInstantUtc } from './vevent.js';
import type { NotificationPayload } from '../lib/pushDispatcher.js';
import { getHouseholdTimezone } from '../lib/householdTimezone.js';
// Maximum lead time preset (2880 min = 2 days for timed; 7 days for all-day is handled separately)
// Pre-filter window upper bound for timed events: fetch events starting up to MAX_LEAD_MINUTES out.
@@ -244,7 +245,9 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
}
// Process ALL-DAY events
const serverTz = process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
// D-05: read the stored household timezone through the single accessor (no inline fallback).
// D-06: getHouseholdTimezone falls back to process.env.TZ → Intl when no row is stored.
const serverTz = await getHouseholdTimezone(db);
for (const row of allDayRows) {
// Drizzle's date() column type is Date|null in TS but mysql2 returns ISO string at runtime
const dtstartDate = row.dtstartDate as unknown as string; // 'YYYY-MM-DD'