/** * Day-breaker: assigns a calendar workDate to a raw instant, in a given IANA * timezone (defaulting to UTC when the caller has none available, e.g. an * Assignment whose Position isn't bound to a physical Site). ReportedTimeBlock's * `workDate` is a pure calendar-date value (no time/tz component once stored), * so this always normalizes to a UTC-midnight Date representing that calendar day. */ export function toWorkDate(instant: Date, timezone = "UTC"): Date { const partsIn = (tz: string) => new Intl.DateTimeFormat("en-CA", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(instant); let parts: Intl.DateTimeFormatPart[]; try { parts = partsIn(timezone); } catch { // Intl.DateTimeFormat throws RangeError for an unknown/misconfigured IANA timezone, and // the timezone ultimately comes from DB (Site.timezone). Fall back to UTC so day-breaking // stays best-effort and a single bad value can't crash the triggering punch/void/unvoid // transaction (ADR-024). parts = partsIn("UTC"); } const value = (type: string) => parts.find((p) => p.type === type)?.value; const year = value("year") ?? "1970"; const month = value("month") ?? "01"; const day = value("day") ?? "01"; return new Date(`${year}-${month}-${day}T00:00:00.000Z`); } export function addDays(date: Date, days: number): Date { return new Date(date.getTime() + days * 24 * 60 * 60 * 1000); } /** * Public day-breaker API. `formReportedBlocks` requires the caller to pass the `workDate` it * computes, which must use exactly these semantics — so consumers resolving that input reach for the * same helpers rather than reimplementing them. Bundled into one object to keep the module registry * namespace clean (mirrors the `jpTimeClassificationStrategy` export precedent). */ export const dayBreaker = { toWorkDate, addDays };