4.0 KiB
TIME.md — the business-time convention
Adopted 2026-06-12 (see TIME_UNIFICATION_PLAN.md at the repo root for the
audit and rollout). Never hand-roll an hour offset.
The convention
A business day runs 1:00am Eastern → 1:00am Eastern the next day.
Because 1am Eastern == midnight Central year-round (both observe US DST), this is identical to:
Business date = the America/Chicago calendar date.
BUSINESS_TZ = 'America/Chicago'— canonical, used for all boundary math.OFFICE_TZ = 'America/New_York'— display only (the office reads ET).
The single source of truth is shared/business-time/index.js
(@inventory/shared/business-time). The frontend mirror for the few
client-side needs is inventory/src/utils/businessTime.ts.
Approved idioms
| Context | Correct idiom |
|---|---|
PG timestamptz → business date |
(ts AT TIME ZONE 'America/Chicago')::date, or plain ts::date / CURRENT_DATE (the session TZ is pinned to America/Chicago in every pool config AND as the inventory_db database default) |
| MySQL DATETIME literal (stores Central wall-clock) → business date | DATE(col) / DATE_FORMAT(col, '%Y-%m-%d') — no hour shift |
| MySQL WHERE bounds for business days | Central wall-clock strings via toMySqlBound(): col >= ? AND col < ? |
| MySQL hour-of-day for the office | HOUR(ADDTIME(col, '01:00:00')) — ET = CT + 1h year-round |
| Luxon | dt.setZone('America/Chicago') (real conversion). Never keepLocalTime, never a fixed UTC-05:00 offset (wrong every winter) |
| Intervals | Half-open [start, nextStart). No -1 minute / -1 ms endpoints (legacy exception: the TimeManager compat class keeps an inclusive −1ms end for its existing consumers) |
| "Today" in JS | businessTodayStr() / businessToday() from the shared module — never new Date().toISOString().split('T')[0] (UTC) |
| node-postgres DATE columns → string | formatDateCol(value) (local-time formatting round-trips correctly) or ::text in SQL — never .toISOString().split('T')[0] (UTC shifts a day) |
| mysql2 driver config | dateStrings: true + parse with parseMySql() (zone Chicago). The import pipeline instead pins a dynamic Chicago offset (currentChicagoOffset() in scripts/import-from-prod.js) |
| Client → server | Named range presets, or date-only strings (YYYY-MM-DD = business dates). Never toISOString() of a browser-local midnight |
Range vocabulary (both servers, one list)
today, yesterday, twoDaysAgo, thisWeek, lastWeek, thisMonth, lastMonth, last7days, last30days, last90days, previous7days, previous30days, previous90days, custom
custom takes startDate/endDate as date-only business dates (preferred)
or zoned ISO instants (used as-is, end exclusive). Calendar periods
("March 2026") mean the business month: Mar 1 1am ET → Apr 1 1am ET.
Accepted deviations (documented, not fought)
- GA4: property TZ = America/New_York;
NdaysAgo= calendar days, midnight ET. Affects only 0:00–1:00am ET traffic attribution. - Meta insights:
time_zone: 'America/New_York'— same 1-hour deviation, same note. - Klaviyo: fetched with exact UTC bounds computed from our own range math — fully on-convention. API returns UTC datetimes.
- Pay periods (payroll dashboard): 14-day periods aligned to midnight ET Sundays — an HR display convention, intentionally not 1am-ET days.
Infrastructure pins (belt-and-suspenders)
ALTER DATABASE inventory_db SET timezone = 'America/Chicago'(database default).- Every pg Pool passes
options: '-c TimeZone=America/Chicago'(src/utils/db.js,shared/db/pg.js,scripts/metrics-new/utils/db.js,scripts/calculate-metrics-new.js,scripts/import/utils.js). - Node processes run with
TZ=America/Chicago(/var/www/ecosystem.config.cjsenv blocks;scripts/full-update.jsandscripts/forecast/run_forecast.jsset it themselves for cron contexts). forecast_engine.pyanchors onbusiness_today()(zoneinfo Chicago).