collabSupport/services/callReport/businessWindow.js
jmcqueen a25fc08fe2 Rename /voicereport to /callreport with scoped user and phone targets.
Add store/email/phone filtering, richer call-line formatting, CDR feed pagination and queueing, and split Jira poller enrichment into testable modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 10:10:03 -04:00

140 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// services/callReport/businessWindow.js
// Store-local business-hour windows for /callreport (default 9am9pm).
import { DateTime } from 'luxon';
import { DISPLAY_TIMEZONE } from '../../utils/time.js';
import { callReportEnvInt } from './env.js';
const TWELVE_HOURS_MS = 12 * 60 * 60 * 1000;
export function getBusinessHourConfig() {
return {
startHour: callReportEnvInt('BUSINESS_START_HOUR', 9),
endHour: callReportEnvInt('BUSINESS_END_HOUR', 21),
apiLagMs: callReportEnvInt('API_LAG_MS', 5 * 60 * 1000),
};
}
/**
* Parse user date arg into a mode + calendar date in store TZ.
* @param {string|null|undefined} raw
* @param {string} timeZone IANA zone
* @param {Date} [now] injectable for tests
*/
export function parseReportDateArg(raw, timeZone, now = new Date()) {
const zone = timeZone || DISPLAY_TIMEZONE;
const nowLocal = DateTime.fromJSDate(now, { zone });
const token = String(raw || 'yesterday').trim().toLowerCase();
if (!token || token === 'yesterday') {
const day = nowLocal.minus({ days: 1 }).startOf('day');
return { mode: 'fullDay', day, label: day.toISODate() };
}
if (token === 'today') {
return { mode: 'today', day: nowLocal.startOf('day'), label: nowLocal.toISODate() };
}
const parsed = DateTime.fromISO(token, { zone });
if (parsed.isValid) {
return { mode: 'fullDay', day: parsed.startOf('day'), label: parsed.toISODate() };
}
return { error: `Unrecognised date "${raw}". Use yesterday, today, or YYYY-MM-DD.` };
}
/**
* Compute UTC ISO window for cdr_feed / Prisma queries.
* @returns {{ startTime: string, endTime: string, timeZone: string, label: string, mode: string, ready: boolean, reason?: string }}
*/
export function computeBusinessWindow({ timeZone, dateArg, now = new Date() }) {
const zone = timeZone || DISPLAY_TIMEZONE;
const cfg = getBusinessHourConfig();
const parsed = parseReportDateArg(dateArg, zone, now);
if (parsed.error) {
return { ready: false, reason: parsed.error, timeZone: zone };
}
const startLocal = parsed.day.set({
hour: cfg.startHour,
minute: 0,
second: 0,
millisecond: 0,
});
let endLocal;
const nowLocal = DateTime.fromJSDate(now, { zone });
if (parsed.mode === 'today') {
const businessEnd = parsed.day.set({
hour: cfg.endHour,
minute: 0,
second: 0,
millisecond: 0,
});
const lagEnd = nowLocal.minus({ milliseconds: cfg.apiLagMs });
if (lagEnd < startLocal.plus({ minutes: 1 })) {
return {
ready: false,
reason: 'Business window has not started yet, or insufficient data after the 5-minute API lag.',
timeZone: zone,
label: parsed.label,
mode: parsed.mode,
};
}
endLocal = DateTime.min(lagEnd, businessEnd);
} else {
endLocal = parsed.day.set({
hour: cfg.endHour,
minute: 0,
second: 0,
millisecond: 0,
});
}
if (endLocal <= startLocal) {
return {
ready: false,
reason: 'Computed window is empty (end is not after start).',
timeZone: zone,
label: parsed.label,
mode: parsed.mode,
};
}
let startMs = startLocal.toUTC().toMillis();
let endMs = endLocal.toUTC().toMillis();
const latestAllowedEnd = now.getTime() - cfg.apiLagMs;
if (endMs > latestAllowedEnd) endMs = latestAllowedEnd;
if (endMs - startMs > TWELVE_HOURS_MS) startMs = endMs - TWELVE_HOURS_MS;
if (startMs >= endMs) {
return {
ready: false,
reason: 'Window invalid after API lag clamp.',
timeZone: zone,
label: parsed.label,
mode: parsed.mode,
};
}
return {
ready: true,
timeZone: zone,
label: parsed.label,
mode: parsed.mode,
startTime: new Date(startMs).toISOString(),
endTime: new Date(endMs).toISOString(),
startLocal: startLocal.toISO(),
endLocal: endLocal.toISO(),
};
}
/**
* True when a timestamp (ISO or ms) falls inside the business window.
*/
export function isWithinWindow(isoOrMs, window) {
if (!window?.startTime || !window?.endTime) return false;
const t = new Date(isoOrMs).getTime();
const s = new Date(window.startTime).getTime();
const e = new Date(window.endTime).getTime();
return t >= s && t <= e;
}