Files
2026-06-17 15:06:38 -04:00

4.0 KiB
Raw Permalink Blame History

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:001: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)

  1. ALTER DATABASE inventory_db SET timezone = 'America/Chicago' (database default).
  2. 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).
  3. Node processes run with TZ=America/Chicago (/var/www/ecosystem.config.cjs env blocks; scripts/full-update.js and scripts/forecast/run_forecast.js set it themselves for cron contexts).
  4. forecast_engine.py anchors on business_today() (zoneinfo Chicago).