"""User-timezone helpers. All datetimes in the DB are stored as UTC. The helpers here bridge between that UTC storage and the user's configured local timezone (IANA string in the ``user_timezone`` setting). Use these anywhere the model or UI talks in terms of "today", "tomorrow", or a bare calendar date — never ``date.today()`` or ``datetime.now(timezone.utc)`` inside a per-user flow. """ from __future__ import annotations from datetime import date, datetime, timedelta from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from scribe.services.settings import get_setting # Day-rollover boundary for day-anchored "today" logic. The "day" flips at # this local hour (not midnight) so the 00:00–04:00 local window still # shows yesterday's content until the user "starts" the new day. DAY_ROLLOVER_HOUR = 4 async def get_user_tz(user_id: int) -> ZoneInfo: """Return the user's IANA ``ZoneInfo``, falling back to UTC.""" tz_str = await get_setting(user_id, "user_timezone") or "UTC" try: return ZoneInfo(tz_str) except (ZoneInfoNotFoundError, KeyError): return ZoneInfo("UTC") async def user_today(user_id: int) -> date: """Return today's calendar date in the user's local timezone.""" tz = await get_user_tz(user_id) return datetime.now(tz).date() async def user_day_date(user_id: int) -> date: """Return the current "day" in the user's local timezone. The day flips at ``DAY_ROLLOVER_HOUR`` (4am local) rather than midnight, so the 00:00–04:00 local window still returns *yesterday* — a day-anchored view stays on yesterday until the user crosses the rollover. """ tz = await get_user_tz(user_id) return (datetime.now(tz) - timedelta(hours=DAY_ROLLOVER_HOUR)).date()