UNPKG

@app-connect/core

Version:
486 lines (443 loc) 19.8 kB
const moment = require('moment-timezone'); const { secondsToHoursMinutesSeconds } = require('./util'); const adapterRegistry = require('../adapter/registry'); const { LOG_DETAILS_FORMAT_TYPE } = require('./constants'); /** * Centralized call log composition module * Supports both plain text and HTML formats used across different CRM adapters */ /** * Compose call log details based on user settings and format type * @param {Object} params - Composition parameters * @param {string} params.logFormat - logFormat type: 'plainText' or 'html' * @param {string} params.existingBody - Existing log body (for updates) * @param {Object} params.callLog - Call log information * @param {Object} params.contactInfo - Contact information * @param {Object} params.user - User information * @param {string} params.note - User note * @param {string} params.aiNote - AI generated note * @param {string} params.transcript - Call transcript * @param {string} params.recordingLink - Recording link * @param {string} params.subject - Call subject * @param {Date} params.startTime - Call start time * @param {number} params.duration - Call duration in seconds * @param {string} params.result - Call result * @returns {Promise<string>} Composed log body */ async function composeCallLog(params) { const { logFormat = LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT, existingBody = '', callLog, contactInfo, user, note, aiNote, transcript, recordingLink, subject, startTime, duration, result, platform } = params; let body = existingBody; const userSettings = user.userSettings || {}; // Determine timezone handling let resolvedStartTime = startTime || callLog?.startTime; let timezoneOffset = user.timezoneOffset; if (resolvedStartTime) { resolvedStartTime = moment(resolvedStartTime); } // Apply upsert functions based on user settings if (note && (userSettings?.addCallLogNote?.value ?? true)) { body = upsertCallAgentNote({ body, note, logFormat }); } if (callLog?.sessionId && (userSettings?.addCallSessionId?.value ?? false)) { body = upsertCallSessionId({ body, id: callLog.sessionId, logFormat }); } if (subject && (userSettings?.addCallLogSubject?.value ?? true)) { body = upsertCallSubject({ body, subject, logFormat }); } if (contactInfo?.phoneNumber && (userSettings?.addCallLogContactNumber?.value ?? false)) { body = upsertContactPhoneNumber({ body, phoneNumber: contactInfo.phoneNumber, direction: callLog?.direction, logFormat }); } if (resolvedStartTime && (userSettings?.addCallLogDateTime?.value ?? true)) { body = upsertCallDateTime({ body, startTime: resolvedStartTime, timezoneOffset, logFormat }); } if (duration && (userSettings?.addCallLogDuration?.value ?? true)) { body = upsertCallDuration({ body, duration, logFormat }); } if (result && (userSettings?.addCallLogResult?.value ?? true)) { body = upsertCallResult({ body, result, logFormat }); } if (recordingLink && (userSettings?.addCallLogRecording?.value ?? true)) { body = upsertCallRecording({ body, recordingLink, logFormat }); } if (aiNote && (userSettings?.addCallLogAINote?.value ?? true)) { body = upsertAiNote({ body, aiNote, logFormat }); } if (transcript && (userSettings?.addCallLogTranscript?.value ?? true)) { body = upsertTranscript({ body, transcript, logFormat }); } return body; } /** * Upsert functions for different log components */ function upsertCallAgentNote({ body, note, logFormat }) { if (!note) return body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { // HTML logFormat with proper Agent notes section handling const noteRegex = RegExp('<b>Agent notes</b>([\\s\\S]+?)Call details</b>'); if (noteRegex.test(body)) { return body.replace(noteRegex, `<b>Agent notes</b><br>${note}<br><br><b>Call details</b>`); } return `<b>Agent notes</b><br>${note}<br><br><b>Call details</b><br>` + body; } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown logFormat with proper Agent notes section handling const noteRegex = /## Agent notes\n([\s\S]*?)\n## Call details/; if (noteRegex.test(body)) { return body.replace(noteRegex, `## Agent notes\n${note}\n\n## Call details`); } if (body.startsWith('## Call details')) { return `## Agent notes\n${note}\n\n` + body; } return `## Agent notes\n${note}\n\n## Call details\n` + body; } else { // Plain text logFormat - FIXED REGEX for multi-line notes with blank lines const noteRegex = /- (?:Note|Agent notes): ([\s\S]*?)(?=\n- [A-Z][a-zA-Z\s/]*:|\n$|$)/; if (noteRegex.test(body)) { return body.replace(noteRegex, `- Note: ${note}`); } return `- Note: ${note}\n` + body; } } function upsertCallSessionId({ body, id, logFormat }) { if (!id) return body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { // More flexible regex that handles both <li> wrapped and unwrapped content const idRegex = /(?:<li>)?<b>Session Id<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i; if (idRegex.test(body)) { return body.replace(idRegex, `<li><b>Session Id</b>: ${id}</li>`); } return body + `<li><b>Session Id</b>: ${id}</li>`; } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: **Session Id**: value const sessionIdRegex = /\*\*Session Id\*\*: [^\n]*\n*/; if (sessionIdRegex.test(body)) { return body.replace(sessionIdRegex, `**Session Id**: ${id}\n`); } return body + `**Session Id**: ${id}\n`; } else { // Match Session Id field and any trailing newlines, replace with single newline const sessionIdRegex = /- Session Id: [^\n]*\n*/; if (sessionIdRegex.test(body)) { return body.replace(sessionIdRegex, `- Session Id: ${id}\n`); } return body + `- Session Id: ${id}\n`; } } function upsertCallSubject({ body, subject, logFormat }) { if (!subject) return body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { // More flexible regex that handles both <li> wrapped and unwrapped content const subjectRegex = /(?:<li>)?<b>Summary<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i; if (subjectRegex.test(body)) { return body.replace(subjectRegex, `<li><b>Summary</b>: ${subject}</li>`); } return body + `<li><b>Summary</b>: ${subject}</li>`; } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: **Summary**: value const subjectRegex = /\*\*Summary\*\*: [^\n]*\n*/; if (subjectRegex.test(body)) { return body.replace(subjectRegex, `**Summary**: ${subject}\n`); } return body + `**Summary**: ${subject}\n`; } else { // Match Summary field and any trailing newlines, replace with single newline const subjectRegex = /- Summary: [^\n]*\n*/; if (subjectRegex.test(body)) { return body.replace(subjectRegex, `- Summary: ${subject}\n`); } return body + `- Summary: ${subject}\n`; } } function upsertContactPhoneNumber({ body, phoneNumber, direction, logFormat }) { if (!phoneNumber) return body; const label = direction === 'Outbound' ? 'Recipient' : 'Caller'; let result = body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { // More flexible regex that handles both <li> wrapped and unwrapped content const phoneNumberRegex = new RegExp(`(?:<li>)?<b>${label} phone number</b>:\\s*([^<\\n]+)(?:</li>|(?=<|$))`, 'i'); if (phoneNumberRegex.test(result)) { result = result.replace(phoneNumberRegex, `<li><b>${label} phone number</b>: ${phoneNumber}</li>`); } else { result += `<li><b>${label} phone number</b>: ${phoneNumber}</li>`; } } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: **Contact Number**: value const phoneNumberRegex = /\*\*Contact Number\*\*: [^\n]*\n*/; if (phoneNumberRegex.test(result)) { result = result.replace(phoneNumberRegex, `**Contact Number**: ${phoneNumber}\n`); } else { result += `**Contact Number**: ${phoneNumber}\n`; } } else { // More flexible regex that handles both with and without newlines const phoneNumberRegex = /- Contact Number: ([^\n-]+)(?=\n-|\n|$)/; if (phoneNumberRegex.test(result)) { result = result.replace(phoneNumberRegex, `- Contact Number: ${phoneNumber}\n`); } else { result += `- Contact Number: ${phoneNumber}\n`; } } return result; } function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat }) { if (!startTime) return body; // Simple approach: convert to moment and apply timezone offset let momentTime = moment(startTime); if (timezoneOffset) { // Handle both string offsets ('+05:30') and numeric offsets (330 minutes or 5.5 hours) if (typeof timezoneOffset === 'string' && timezoneOffset.includes(':')) { // String logFormat like '+05:30' or '-05:00' momentTime = momentTime.utcOffset(timezoneOffset); } else { // Numeric logFormat (minutes or hours) momentTime = momentTime.utcOffset(Number(timezoneOffset)); } } const formattedDateTime = momentTime.format('YYYY-MM-DD hh:mm:ss A'); let result = body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { // More flexible regex that handles both <li> wrapped and unwrapped content const dateTimeRegex = /(?:<li>)?<b>Date\/time<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i; if (dateTimeRegex.test(result)) { result = result.replace(dateTimeRegex, `<li><b>Date/time</b>: ${formattedDateTime}</li>`); } else { result += `<li><b>Date/time</b>: ${formattedDateTime}</li>`; } } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: **Date/Time**: value const dateTimeRegex = /\*\*Date\/Time\*\*: [^\n]*\n*/; if (dateTimeRegex.test(result)) { result = result.replace(dateTimeRegex, `**Date/Time**: ${formattedDateTime}\n`); } else { result += `**Date/Time**: ${formattedDateTime}\n`; } } else { // Handle duplicated Date/Time entries and match complete date/time values const dateTimeRegex = /(?:- Date\/Time: [^-]*(?:-[^-]*)*)+/; if (dateTimeRegex.test(result)) { result = result.replace(dateTimeRegex, `- Date/Time: ${formattedDateTime}\n`); } else { result += `- Date/Time: ${formattedDateTime}\n`; } } return result; } function upsertCallDuration({ body, duration, logFormat }) { if (!duration) return body; const formattedDuration = secondsToHoursMinutesSeconds(duration); let result = body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { // More flexible regex that handles both <li> wrapped and unwrapped content const durationRegex = /(?:<li>)?<b>Duration<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i; if (durationRegex.test(result)) { result = result.replace(durationRegex, `<li><b>Duration</b>: ${formattedDuration}</li>`); } else { result += `<li><b>Duration</b>: ${formattedDuration}</li>`; } } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: **Duration**: value const durationRegex = /\*\*Duration\*\*: [^\n]*\n*/; if (durationRegex.test(result)) { result = result.replace(durationRegex, `**Duration**: ${formattedDuration}\n`); } else { result += `**Duration**: ${formattedDuration}\n`; } } else { // More flexible regex that handles both with and without newlines const durationRegex = /- Duration: ([^\n-]+)(?=\n-|\n|$)/; if (durationRegex.test(result)) { result = result.replace(durationRegex, `- Duration: ${formattedDuration}\n`); } else { result += `- Duration: ${formattedDuration}\n`; } } return result; } function upsertCallResult({ body, result, logFormat }) { if (!result) return body; let bodyResult = body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { // More flexible regex that handles both <li> wrapped and unwrapped content const resultRegex = /(?:<li>)?<b>Result<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i; if (resultRegex.test(bodyResult)) { bodyResult = bodyResult.replace(resultRegex, `<li><b>Result</b>: ${result}</li>`); } else { bodyResult += `<li><b>Result</b>: ${result}</li>`; } } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: **Result**: value const resultRegex = /\*\*Result\*\*: [^\n]*\n*/; if (resultRegex.test(bodyResult)) { bodyResult = bodyResult.replace(resultRegex, `**Result**: ${result}\n`); } else { bodyResult += `**Result**: ${result}\n`; } } else { // More flexible regex that handles both with and without newlines const resultRegex = /- Result: ([^\n-]+)(?=\n-|\n|$)/; if (resultRegex.test(bodyResult)) { bodyResult = bodyResult.replace(resultRegex, `- Result: ${result}\n`); } else { bodyResult += `- Result: ${result}\n`; } } return bodyResult; } function upsertCallRecording({ body, recordingLink, logFormat }) { if (!recordingLink) return body; let result = body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { // More flexible regex that handles both <li> wrapped and unwrapped content const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i; if (recordingLink) { if (recordingLinkRegex.test(result)) { if (recordingLink.startsWith('http')) { result = result.replace(recordingLinkRegex, `<li><b>Call recording link</b>: <a target="_blank" href="${recordingLink}">open</a></li>`); } else { result = result.replace(recordingLinkRegex, `<li><b>Call recording link</b>: (pending...)</li>`); } } else { let text = ''; if (recordingLink.startsWith('http')) { text = `<li><b>Call recording link</b>: <a target="_blank" href="${recordingLink}">open</a></li>`; } else { text = '<li><b>Call recording link</b>: (pending...)</li>'; } if (result.indexOf('</ul>') === -1) { result += text; } else { result = result.replace('</ul>', `${text}</ul>`); } } } } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: **Call recording link**: value const recordingLinkRegex = /\*\*Call recording link\*\*: [^\n]*\n*/; if (recordingLinkRegex.test(result)) { result = result.replace(recordingLinkRegex, `**Call recording link**: ${recordingLink}\n`); } else { result += `**Call recording link**: ${recordingLink}\n`; } } else { // Match recording link field and any trailing content, replace with single newline const recordingLinkRegex = /- Call recording link: [^\n]*\n*/; if (recordingLinkRegex.test(result)) { result = result.replace(recordingLinkRegex, `- Call recording link: ${recordingLink}\n`); } else { if (result && !result.endsWith('\n')) { result += '\n'; } result += `- Call recording link: ${recordingLink}\n`; } } return result; } function upsertAiNote({ body, aiNote, logFormat }) { if (!aiNote) return body; const clearedAiNote = aiNote.replace(/\n+$/, ''); let result = body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { const formattedAiNote = clearedAiNote.replace(/(?:\r\n|\r|\n)/g, '<br>'); const aiNoteRegex = /<div><b>AI Note<\/b><br>(.+?)<\/div>/; if (aiNoteRegex.test(result)) { result = result.replace(aiNoteRegex, `<div><b>AI Note</b><br>${formattedAiNote}</div>`); } else { result += `<div><b>AI Note</b><br>${formattedAiNote}</div><br>`; } } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: ### AI Note const aiNoteRegex = /### AI Note\n([\s\S]*?)(?=\n### |\n$|$)/; if (aiNoteRegex.test(result)) { result = result.replace(aiNoteRegex, `### AI Note\n${clearedAiNote}\n`); } else { result += `### AI Note\n${clearedAiNote}\n`; } } else { const aiNoteRegex = /- AI Note:([\s\S]*?)--- END/; if (aiNoteRegex.test(result)) { result = result.replace(aiNoteRegex, `- AI Note:\n${clearedAiNote}\n--- END`); } else { result += `- AI Note:\n${clearedAiNote}\n--- END\n`; } } return result; } function upsertTranscript({ body, transcript, logFormat }) { if (!transcript) return body; let result = body; if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) { const formattedTranscript = transcript.replace(/(?:\r\n|\r|\n)/g, '<br>'); const transcriptRegex = /<div><b>Transcript<\/b><br>(.+?)<\/div>/; if (transcriptRegex.test(result)) { result = result.replace(transcriptRegex, `<div><b>Transcript</b><br>${formattedTranscript}</div>`); } else { result += `<div><b>Transcript</b><br>${formattedTranscript}</div><br>`; } } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) { // Markdown format: ### Transcript const transcriptRegex = /### Transcript\n([\s\S]*?)(?=\n### |\n$|$)/; if (transcriptRegex.test(result)) { result = result.replace(transcriptRegex, `### Transcript\n${transcript}\n`); } else { result += `### Transcript\n${transcript}\n`; } } else { const transcriptRegex = /- Transcript:([\s\S]*?)--- END/; if (transcriptRegex.test(result)) { result = result.replace(transcriptRegex, `- Transcript:\n${transcript}\n--- END`); } else { result += `- Transcript:\n${transcript}\n--- END\n`; } } return result; } /** * Helper function to determine format type for a CRM platform * @param {string} platform - CRM platform name * @returns {string} Format type */ function getLogFormatType(platform) { const manifest = adapterRegistry.getManifest(platform, true); const platformConfig = manifest.platforms?.[platform]; return platformConfig?.logFormat; } module.exports = { composeCallLog, getLogFormatType, // Export individual upsert functions for backward compatibility upsertCallAgentNote, upsertCallSessionId, upsertCallSubject, upsertContactPhoneNumber, upsertCallDateTime, upsertCallDuration, upsertCallResult, upsertCallRecording, upsertAiNote, upsertTranscript };