Enables outbound PSTN probes via TwiML webhooks with Webex result cards, status polling, and optional store CDR enrichment.
119 lines
3.8 KiB
JavaScript
119 lines
3.8 KiB
JavaScript
// services/renderers/callTestRenderer.js
|
||
|
||
function fmtMs(ms) {
|
||
if (ms == null || !Number.isFinite(ms)) return '—';
|
||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||
return `${(ms / 1000).toFixed(1)}s`;
|
||
}
|
||
|
||
function fmtTime(iso) {
|
||
if (!iso) return '—';
|
||
try {
|
||
return new Date(iso).toISOString();
|
||
} catch {
|
||
return String(iso);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {object} session
|
||
*/
|
||
export function renderCallTestStartedMarkdown(session) {
|
||
const modeLabel = session.mode === 'store' ? 'Store path' : 'Direct dial';
|
||
const storePart = session.storeNum ? ` for store **${session.storeNum}**` : '';
|
||
return (
|
||
`**Call test started** (${modeLabel})${storePart}\n\n` +
|
||
`- **Dialing:** ${session.dialNumber}\n` +
|
||
`- **Test ID:** \`${session.testId}\`\n` +
|
||
`- Results will post here when the call completes (~2–3 min).`
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param {object} session
|
||
*/
|
||
export function renderCallTestResultMarkdown(session) {
|
||
const modeLabel = session.mode === 'store' ? 'Store' : 'Direct';
|
||
const inProgress = session.result == null
|
||
&& (session.status === 'in_progress' || session.status === 'pending');
|
||
const pass = session.result === 'pass';
|
||
const warn = session.result === 'warn';
|
||
const icon = inProgress ? '⏳' : (pass || warn ? '✅' : '❌');
|
||
const headline = inProgress
|
||
? 'IN PROGRESS'
|
||
: (pass || warn ? 'PASSED' : 'FAILED');
|
||
|
||
let md = `**${icon} Call test ${headline}** (${modeLabel})\n\n`;
|
||
|
||
if (session.storeNum) md += `- **Store:** ${session.storeNum}\n`;
|
||
md += `- **Dialed:** ${session.dialNumber}\n`;
|
||
md += `- **Test ID:** \`${session.testId}\`\n`;
|
||
if (session.callSid) md += `- **Twilio CallSid:** \`${session.callSid}\`\n`;
|
||
|
||
if (session.resultReason) {
|
||
md += `- **Result:** ${session.resultReason}\n`;
|
||
}
|
||
if (session.failedAt) {
|
||
md += `- **Failed at:** ${session.failedAt}\n`;
|
||
}
|
||
|
||
const e = session.events || {};
|
||
md += '\n**Timings:**\n';
|
||
if (e.initiated) md += `- Initiated: ${fmtTime(e.initiated)}\n`;
|
||
if (e.ringing) md += `- Ringing: ${fmtTime(e.ringing)}\n`;
|
||
if (e.answered) md += `- Answered: ${fmtTime(e.answered)}\n`;
|
||
if (!e.answered && e['in-progress']) md += `- In progress: ${fmtTime(e['in-progress'])}\n`;
|
||
if (e.completed) md += `- Completed: ${fmtTime(e.completed)}\n`;
|
||
|
||
if (e.answered && e.initiated) {
|
||
md += `- Ring → answer: ${fmtMs(new Date(e.answered) - new Date(e.initiated))}\n`;
|
||
}
|
||
if (e.coreStartedAt) {
|
||
md += `- Core started: ${fmtTime(e.coreStartedAt)}\n`;
|
||
}
|
||
if (e.completed && e.answered) {
|
||
md += `- Answer → complete: ${fmtMs(new Date(e.completed) - new Date(e.answered))}\n`;
|
||
}
|
||
if (session.durationSec != null) {
|
||
md += `- Total duration: ${session.durationSec}s\n`;
|
||
}
|
||
|
||
if (session.mode === 'store' && e.dtmfStepAt) {
|
||
md += `- Entry → DTMF step: ${fmtTime(e.dtmfStepAt)}\n`;
|
||
}
|
||
|
||
return md.trim();
|
||
}
|
||
|
||
/**
|
||
* @param {object} session
|
||
* @param {object} cdr
|
||
*/
|
||
export function renderCallTestCdrMarkdown(session, cdr) {
|
||
if (!cdr?.available) {
|
||
return (
|
||
`**Call test CDR** (\`${session.testId}\`)\n\n` +
|
||
`_Detailed call history unavailable:_ ${cdr?.reason || 'not configured'}`
|
||
);
|
||
}
|
||
|
||
const summary = cdr.summary || {};
|
||
let md =
|
||
`**Call test CDR enrichment** (store **${session.storeNum}**)\n\n` +
|
||
`- **Window:** ${cdr.startTime} → ${cdr.endTime}\n` +
|
||
`- **Location:** ${cdr.location || '—'}\n` +
|
||
`- **Matching legs:** ${summary.totalCalls ?? cdr.rawCount ?? 0}\n`;
|
||
|
||
if (summary.inbound != null) md += `- Inbound legs: ${summary.inbound}\n`;
|
||
if (summary.missed != null) md += `- Missed / failed legs: ${summary.missed}\n`;
|
||
|
||
const samples = cdr.samples || [];
|
||
if (samples.length) {
|
||
md += '\n**Sample legs:**\n';
|
||
for (const s of samples.slice(0, 5)) {
|
||
md += `- ${s.start || '—'} | ${s.direction || '—'} | ${s.duration ?? '—'}s | ${s.otherParty || s.phoneNumber || '—'}\n`;
|
||
}
|
||
}
|
||
|
||
return md.trim();
|
||
}
|