collabSupport/services/jiraPoller/enrichmentRules.js
jmcqueen 7dc326c404 Split voice commands into status vs diag and redesign MPP phone output.
Replace /phonestatus follow-ups with /voicestatus, /wanstatus, /phonediag, and /dectdiag; extend /voicediag with relay probes and section-based MPP diagnostics.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 13:55:41 -04:00

154 lines
3.8 KiB
JavaScript

// services/jiraPoller/enrichmentRules.js
// Rule-based enrichment planner for the hourly Jira poller.
export const COMM_SERVICES_COMPONENT = 'Communication Services';
export const CHECK_IDS = {
voicestatus: 'voicestatus',
callreport: 'callreport',
avstatus: 'avstatus',
};
const CALL_QUALITY_PATTERNS = [
/\bgarbled\b/i,
/\bstatic(?:y|s)?\b/i,
/\bchoppy\b/i,
/\bmuffled\b/i,
/\bcan(?:'t| ?not) connect\b/i,
/\bnot connecting\b/i,
/\bcan(?:'t| ?not) call\b/i,
/\bno dial tone\b/i,
/\bone[- ]way\b/i,
/\bdropped calls?\b/i,
/\bcall quality\b/i,
/\bbad audio\b/i,
/\bpoor audio\b/i,
/\bvoice quality\b/i,
/\bno audio\b/i,
/\bdead air\b/i,
/\bintermittent\b/i,
];
const SPAM_PATTERNS = [
/\bspam\b/i,
/\brobocall/i,
/\brobo[- ]?call/i,
/\bnuisance calls?\b/i,
/\bunwanted calls?\b/i,
/\bauto[- ]?dial/i,
/\btelemarketer/i,
/\bprank calls?\b/i,
/\bharassing calls?\b/i,
];
function hasComponent(components, name) {
return (components || []).some((c) => c?.name === name);
}
function ticketText(ticket) {
const summary = ticket?.summary || '';
const description = ticket?.description || '';
return `${summary}\n${description}`.toLowerCase();
}
function matchesAny(text, patterns) {
return patterns.some((re) => re.test(text));
}
/**
* Extract store number from summary text (e.g. "Store 2477 - ...").
* @param {string} summary
* @returns {string|null}
*/
export function extractStoreFromSummary(summary) {
if (!summary) return null;
const m = String(summary).match(/\bstore\s+(\d{2,6})\b/i);
return m ? m[1] : null;
}
/**
* @param {object} ticket
* @param {string} ticket.summary
* @param {string} [ticket.description]
* @param {Array<{name?: string}>} [ticket.components]
* @param {object} classification from classifyTicket()
* @param {string|null} classification.storeNum
* @param {'phone'|'av'|'skip'} classification.kind
* @returns {{
* checks: string[],
* matchedRules: string[],
* storeNum: string|null,
* skip: boolean,
* skipReason?: string,
* }}
*/
export function resolveEnrichmentPlan(ticket, classification) {
const text = ticketText(ticket);
const isCommServices = hasComponent(ticket?.components, COMM_SERVICES_COMPONENT);
const callQuality = isCommServices && matchesAny(text, CALL_QUALITY_PATTERNS);
const spam = isCommServices && matchesAny(text, SPAM_PATTERNS);
let storeNum = classification?.storeNum || null;
if (!storeNum && (callQuality || spam)) {
storeNum = extractStoreFromSummary(ticket?.summary);
}
const matchedRules = [];
if (callQuality) matchedRules.push('comm-call-quality');
if (spam) matchedRules.push('comm-spam');
if (callQuality || spam) {
if (!storeNum) {
return {
checks: [],
matchedRules,
storeNum: null,
skip: true,
skipReason: 'Communication Services symptom matched but no store number found',
};
}
const checks = [];
if (callQuality) {
checks.push(CHECK_IDS.voicestatus, CHECK_IDS.callreport);
} else if (spam) {
checks.push(CHECK_IDS.callreport);
}
return { checks: [...new Set(checks)], matchedRules, storeNum, skip: false };
}
if (classification?.kind === 'skip' || !storeNum) {
return {
checks: [],
matchedRules: [],
storeNum: null,
skip: true,
skipReason: classification?.reason || 'no store number',
};
}
if (classification.kind === 'phone') {
return {
checks: [CHECK_IDS.voicestatus],
matchedRules: ['default-phone'],
storeNum,
skip: false,
};
}
if (classification.kind === 'av') {
return {
checks: [CHECK_IDS.avstatus],
matchedRules: ['default-av'],
storeNum,
skip: false,
};
}
return {
checks: [],
matchedRules: [],
storeNum,
skip: true,
skipReason: `unsupported kind: ${classification?.kind}`,
};
}