collabSupport/services/voiceReport/businessWindow.js
jmcqueen 1eaabbcee1 Add /voicereport store digest and harden Webex CDR feed handling.
Groups cdr_feed legs by Correlation ID for call-level summaries, fixes report-column field parsing and Docker proxy routing, and adds /calltest post-call Twilio and CDR enrichment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 13:43:11 -04:00

144 lines
4.2 KiB
JavaScript
Raw 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/voiceReport/businessWindow.js
// Store-local business-hour windows for /voicereport (default 9am9pm).
import { DateTime } from 'luxon';
import { DISPLAY_TIMEZONE } from '../../utils/time.js';
const TWELVE_HOURS_MS = 12 * 60 * 60 * 1000;
function envInt(name, fallback) {
const v = Number(process.env[name]);
return Number.isFinite(v) ? v : fallback;
}
export function getBusinessHourConfig() {
return {
startHour: envInt('VOICEREPORT_BUSINESS_START_HOUR', 9),
endHour: envInt('VOICEREPORT_BUSINESS_END_HOUR', 21),
apiLagMs: envInt('VOICEREPORT_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;
}