// Time handling for the sales day. // // Timestamps are stored in the DB as UTC ISO strings (`new Date().toISOString()`). // For display and for grouping into business days we convert to a fixed local // timezone (default Europe/Berlin) so the numbers match the bar's wall clock // regardless of where the server runs. export const TZ = process.env.WUTZ_TZ ?? 'Europe/Berlin'; // A sale at e.g. 03:00 still belongs to the previous night's business day. // Anything before this local hour counts towards the day before. export const BUSINESS_DAY_CUTOFF_HOUR = Number(process.env.WUTZ_DAY_CUTOFF_HOUR ?? 5); /** * Parse a value stored in `created_at`. New rows are UTC ISO strings (with `Z`), * but legacy rows written by SQLite's CURRENT_TIMESTAMP look like * `YYYY-MM-DD HH:MM:SS` and are also UTC — normalise both to a Date. */ export function parseDbTime(s: string): Date { if (/[zZ]|[+-]\d{2}:\d{2}$/.test(s)) return new Date(s); return new Date(s.replace(' ', 'T') + 'Z'); } interface LocalParts { year: number; month: number; day: number; hour: number; minute: number; second: number; } function localParts(d: Date, tz: string = TZ): LocalParts { const fmt = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', // force midnight as 00, never the 24 artifact }); const parts: Record = {}; for (const p of fmt.formatToParts(d)) { if (p.type !== 'literal') parts[p.type] = p.value; } return { year: Number(parts.year), month: Number(parts.month), day: Number(parts.day), hour: Number(parts.hour), minute: Number(parts.minute), second: Number(parts.second), }; } /** Local wall-clock time as `YYYY-MM-DD HH:MM:SS` for CSV / display. */ export function formatLocal(d: Date, tz: string = TZ): string { const p = localParts(d, tz); const pad = (n: number) => String(n).padStart(2, '0'); return `${p.year}-${pad(p.month)}-${pad(p.day)} ${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)}`; } /** * The business day a timestamp belongs to, as `YYYY-MM-DD`. * Hours before BUSINESS_DAY_CUTOFF_HOUR are attributed to the previous day. */ export function businessDay(d: Date, tz: string = TZ): string { const p = localParts(d, tz); // Build a date from the local Y-M-D and roll back a day when before cutoff. const date = new Date(Date.UTC(p.year, p.month - 1, p.day)); if (p.hour < BUSINESS_DAY_CUTOFF_HOUR) { date.setUTCDate(date.getUTCDate() - 1); } const pad = (n: number) => String(n).padStart(2, '0'); return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`; }