// services/jiraPoller/enrichmentRules.js // Rule-based enrichment planner for the hourly Jira poller. export const COMM_SERVICES_COMPONENT = 'Communication Services'; export const CHECK_IDS = { phonestatus: 'phonestatus', 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.phonestatus, 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.phonestatus], 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}`, }; }