@app-connect/core 0.0.3 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,452 +1,551 @@
1
- const moment = require('moment-timezone');
2
- const { secondsToHoursMinutesSeconds } = require('./util');
3
- const adapterRegistry = require('../adapter/registry');
4
-
5
- /**
6
- * Centralized call log composition module
7
- * Supports both plain text and HTML formats used across different CRM adapters
8
- */
9
-
10
- // Format types
11
- const FORMAT_TYPES = {
12
- PLAIN_TEXT: 'plainText',
13
- HTML: 'html'
14
- };
15
-
16
- /**
17
- * Compose call log details based on user settings and format type
18
- * @param {Object} params - Composition parameters
19
- * @param {string} params.logFormat - logFormat type: 'plainText' or 'html'
20
- * @param {string} params.existingBody - Existing log body (for updates)
21
- * @param {Object} params.callLog - Call log information
22
- * @param {Object} params.contactInfo - Contact information
23
- * @param {Object} params.user - User information
24
- * @param {string} params.note - User note
25
- * @param {string} params.aiNote - AI generated note
26
- * @param {string} params.transcript - Call transcript
27
- * @param {string} params.recordingLink - Recording link
28
- * @param {string} params.subject - Call subject
29
- * @param {Date} params.startTime - Call start time
30
- * @param {number} params.duration - Call duration in seconds
31
- * @param {string} params.result - Call result
32
- * @returns {Promise<string>} Composed log body
33
- */
34
- async function composeCallLog(params) {
35
- const {
36
- logFormat = FORMAT_TYPES.PLAIN_TEXT,
37
- existingBody = '',
38
- callLog,
39
- contactInfo,
40
- user,
41
- note,
42
- aiNote,
43
- transcript,
44
- recordingLink,
45
- subject,
46
- startTime,
47
- duration,
48
- result,
49
- platform
50
- } = params;
51
-
52
- let body = existingBody;
53
- const userSettings = user.userSettings || {};
54
- // Determine timezone handling
55
- let resolvedStartTime = startTime || callLog?.startTime;
56
- let timezoneOffset = user.timezoneOffset;
57
- if (resolvedStartTime) {
58
- resolvedStartTime = moment(resolvedStartTime);
59
- }
60
- // Apply upsert functions based on user settings
61
- if (note && (userSettings?.addCallLogNote?.value ?? true)) {
62
- body = upsertCallAgentNote({ body, note, logFormat });
63
- }
64
-
65
- if (callLog?.sessionId && (userSettings?.addCallSessionId?.value ?? false)) {
66
- body = upsertCallSessionId({ body, id: callLog.sessionId, logFormat });
67
- }
68
-
69
- if (subject && (userSettings?.addCallLogSubject?.value ?? true)) {
70
- body = upsertCallSubject({ body, subject, logFormat });
71
- }
72
-
73
- if (contactInfo?.phoneNumber && (userSettings?.addCallLogContactNumber?.value ?? false)) {
74
- body = upsertContactPhoneNumber({
75
- body,
76
- phoneNumber: contactInfo.phoneNumber,
77
- direction: callLog?.direction,
78
- logFormat
79
- });
80
- }
81
-
82
- if (resolvedStartTime && (userSettings?.addCallLogDateTime?.value ?? true)) {
83
- body = upsertCallDateTime({
84
- body,
85
- startTime: resolvedStartTime,
86
- timezoneOffset,
87
- logFormat
88
- });
89
- }
90
-
91
- if (duration && (userSettings?.addCallLogDuration?.value ?? true)) {
92
- body = upsertCallDuration({ body, duration, logFormat });
93
- }
94
-
95
- if (result && (userSettings?.addCallLogResult?.value ?? true)) {
96
- body = upsertCallResult({ body, result, logFormat });
97
- }
98
-
99
- if (recordingLink && (userSettings?.addCallLogRecording?.value ?? true)) {
100
- body = upsertCallRecording({ body, recordingLink, logFormat });
101
- }
102
-
103
- if (aiNote && (userSettings?.addCallLogAINote?.value ?? true)) {
104
- body = upsertAiNote({ body, aiNote, logFormat });
105
- }
106
-
107
- if (transcript && (userSettings?.addCallLogTranscript?.value ?? true)) {
108
- body = upsertTranscript({ body, transcript, logFormat });
109
- }
110
-
111
- return body;
112
- }
113
-
114
- /**
115
- * Upsert functions for different log components
116
- */
117
-
118
- function upsertCallAgentNote({ body, note, logFormat }) {
119
- if (!note) return body;
120
- if (logFormat === FORMAT_TYPES.HTML) {
121
- // HTML logFormat with proper Agent notes section handling
122
- const noteRegex = RegExp('<b>Agent notes</b>([\\s\\S]+?)Call details</b>');
123
- if (noteRegex.test(body)) {
124
- return body.replace(noteRegex, `<b>Agent notes</b><br>${note}<br><br><b>Call details</b>`);
125
- }
126
- else {
127
- return `<b>Agent notes</b><br>${note}<br><br><b>Call details</b><br>` + body;
128
- }
129
- } else {
130
- // Plain text logFormat - FIXED REGEX for multi-line notes with blank lines
131
- const noteRegex = /- (?:Note|Agent notes): ([\s\S]*?)(?=\n- [A-Z][a-zA-Z\s/]*:|\n$|$)/;
132
- if (noteRegex.test(body)) {
133
- return body.replace(noteRegex, `- Note: ${note}`);
134
- } else {
135
- return `- Note: ${note}\n` + body;
136
- }
137
- }
138
- }
139
-
140
- function upsertCallSessionId({ body, id, logFormat }) {
141
- if (!id) return body;
142
-
143
- if (logFormat === FORMAT_TYPES.HTML) {
144
- // More flexible regex that handles both <li> wrapped and unwrapped content
145
- const idRegex = /(?:<li>)?<b>Session Id<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
146
- if (idRegex.test(body)) {
147
- return body.replace(idRegex, `<li><b>Session Id</b>: ${id}</li>`);
148
- } else {
149
- return body + `<li><b>Session Id</b>: ${id}</li>`;
150
- }
151
- } else {
152
- // Match Session Id field and any trailing newlines, replace with single newline
153
- const sessionIdRegex = /- Session Id: [^\n]*\n*/;
154
- if (sessionIdRegex.test(body)) {
155
- return body.replace(sessionIdRegex, `- Session Id: ${id}\n`);
156
- } else {
157
- return body + `- Session Id: ${id}\n`;
158
- }
159
- }
160
- }
161
-
162
- function upsertCallSubject({ body, subject, logFormat }) {
163
- if (!subject) return body;
164
-
165
- if (logFormat === FORMAT_TYPES.HTML) {
166
- // More flexible regex that handles both <li> wrapped and unwrapped content
167
- const subjectRegex = /(?:<li>)?<b>Summary<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
168
- if (subjectRegex.test(body)) {
169
- return body.replace(subjectRegex, `<li><b>Summary</b>: ${subject}</li>`);
170
- } else {
171
- return body + `<li><b>Summary</b>: ${subject}</li>`;
172
- }
173
- } else {
174
- // Match Summary field and any trailing newlines, replace with single newline
175
- const subjectRegex = /- Summary: [^\n]*\n*/;
176
- if (subjectRegex.test(body)) {
177
- return body.replace(subjectRegex, `- Summary: ${subject}\n`);
178
- } else {
179
- return body + `- Summary: ${subject}\n`;
180
- }
181
- }
182
- }
183
-
184
- function upsertContactPhoneNumber({ body, phoneNumber, direction, logFormat }) {
185
- if (!phoneNumber) return body;
186
-
187
- const label = direction === 'Outbound' ? 'Recipient' : 'Caller';
188
- let result = body;
189
-
190
- if (logFormat === FORMAT_TYPES.HTML) {
191
- // More flexible regex that handles both <li> wrapped and unwrapped content
192
- const phoneNumberRegex = new RegExp(`(?:<li>)?<b>${label} phone number</b>:\\s*([^<\\n]+)(?:</li>|(?=<|$))`, 'i');
193
- if (phoneNumberRegex.test(result)) {
194
- result = result.replace(phoneNumberRegex, `<li><b>${label} phone number</b>: ${phoneNumber}</li>`);
195
- } else {
196
- result += `<li><b>${label} phone number</b>: ${phoneNumber}</li>`;
197
- }
198
- } else {
199
- // More flexible regex that handles both with and without newlines
200
- const phoneNumberRegex = /- Contact Number: ([^\n-]+)(?=\n-|\n|$)/;
201
- if (phoneNumberRegex.test(result)) {
202
- result = result.replace(phoneNumberRegex, `- Contact Number: ${phoneNumber}\n`);
203
- } else {
204
- result += `- Contact Number: ${phoneNumber}\n`;
205
- }
206
- }
207
- return result;
208
- }
209
-
210
- function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat }) {
211
- if (!startTime) return body;
212
-
213
- // Simple approach: convert to moment and apply timezone offset
214
- let momentTime = moment(startTime);
215
- if (timezoneOffset) {
216
- // Handle both string offsets ('+05:30') and numeric offsets (330 minutes or 5.5 hours)
217
- if (typeof timezoneOffset === 'string' && timezoneOffset.includes(':')) {
218
- // String logFormat like '+05:30' or '-05:00'
219
- momentTime = momentTime.utcOffset(timezoneOffset);
220
- } else {
221
- // Numeric logFormat (minutes or hours)
222
- momentTime = momentTime.utcOffset(Number(timezoneOffset));
223
- }
224
- }
225
- const formattedDateTime = momentTime.format('YYYY-MM-DD hh:mm:ss A');
226
- let result = body;
227
-
228
- if (logFormat === FORMAT_TYPES.HTML) {
229
- // More flexible regex that handles both <li> wrapped and unwrapped content
230
- const dateTimeRegex = /(?:<li>)?<b>Date\/time<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
231
- if (dateTimeRegex.test(result)) {
232
- result = result.replace(dateTimeRegex, `<li><b>Date/time</b>: ${formattedDateTime}</li>`);
233
- } else {
234
- result += `<li><b>Date/time</b>: ${formattedDateTime}</li>`;
235
- }
236
- } else {
237
- // Handle duplicated Date/Time entries and match complete date/time values
238
- const dateTimeRegex = /(?:- Date\/Time: [^-]*(?:-[^-]*)*)+/;
239
- if (dateTimeRegex.test(result)) {
240
- result = result.replace(dateTimeRegex, `- Date/Time: ${formattedDateTime}\n`);
241
- } else {
242
- result += `- Date/Time: ${formattedDateTime}\n`;
243
- }
244
- }
245
- return result;
246
- }
247
-
248
- function upsertCallDuration({ body, duration, logFormat }) {
249
- if (!duration) return body;
250
-
251
- const formattedDuration = secondsToHoursMinutesSeconds(duration);
252
- let result = body;
253
-
254
- if (logFormat === FORMAT_TYPES.HTML) {
255
- // More flexible regex that handles both <li> wrapped and unwrapped content
256
- const durationRegex = /(?:<li>)?<b>Duration<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
257
- if (durationRegex.test(result)) {
258
- result = result.replace(durationRegex, `<li><b>Duration</b>: ${formattedDuration}</li>`);
259
- } else {
260
- result += `<li><b>Duration</b>: ${formattedDuration}</li>`;
261
- }
262
- } else {
263
- // More flexible regex that handles both with and without newlines
264
- const durationRegex = /- Duration: ([^\n-]+)(?=\n-|\n|$)/;
265
- if (durationRegex.test(result)) {
266
- result = result.replace(durationRegex, `- Duration: ${formattedDuration}\n`);
267
- } else {
268
- result += `- Duration: ${formattedDuration}\n`;
269
- }
270
- }
271
- return result;
272
- }
273
-
274
- function upsertCallResult({ body, result, logFormat }) {
275
- if (!result) return body;
276
-
277
- let bodyResult = body;
278
-
279
- if (logFormat === FORMAT_TYPES.HTML) {
280
- // More flexible regex that handles both <li> wrapped and unwrapped content
281
- const resultRegex = /(?:<li>)?<b>Result<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
282
- if (resultRegex.test(bodyResult)) {
283
- bodyResult = bodyResult.replace(resultRegex, `<li><b>Result</b>: ${result}</li>`);
284
- } else {
285
- bodyResult += `<li><b>Result</b>: ${result}</li>`;
286
- }
287
- } else {
288
- // More flexible regex that handles both with and without newlines
289
- const resultRegex = /- Result: ([^\n-]+)(?=\n-|\n|$)/;
290
- if (resultRegex.test(bodyResult)) {
291
- bodyResult = bodyResult.replace(resultRegex, `- Result: ${result}\n`);
292
- } else {
293
- bodyResult += `- Result: ${result}\n`;
294
- }
295
- }
296
- return bodyResult;
297
- }
298
-
299
- function upsertCallRecording({ body, recordingLink, logFormat }) {
300
- // console.log({ m: "upsertCallRecording", recordingLink, hasBody: !!body, logFormat, bodyLength: body?.length });
301
- if (!recordingLink) return body;
302
-
303
- let result = body;
304
-
305
- if (logFormat === FORMAT_TYPES.HTML) {
306
- // More flexible regex that handles both <li> wrapped and unwrapped content
307
- const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
308
- if (recordingLink) {
309
- if (recordingLinkRegex.test(result)) {
310
- if (recordingLink.startsWith('http')) {
311
- result = result.replace(recordingLinkRegex, `<li><b>Call recording link</b>: <a target="_blank" href="${recordingLink}">open</a></li>`);
312
- } else {
313
- result = result.replace(recordingLinkRegex, `<li><b>Call recording link</b>: (pending...)</li>`);
314
- }
315
- } else {
316
- let text = '';
317
- if (recordingLink.startsWith('http')) {
318
- text = `<li><b>Call recording link</b>: <a target="_blank" href="${recordingLink}">open</a></li>`;
319
- } else {
320
- text = '<li><b>Call recording link</b>: (pending...)</li>';
321
- }
322
- if (result.indexOf('</ul>') === -1) {
323
- result += text;
324
- } else {
325
- result = result.replace('</ul>', `${text}</ul>`);
326
- }
327
- }
328
- }
329
- } else {
330
- // Match recording link field and any trailing content, replace with single newline
331
- const recordingLinkRegex = /- Call recording link: [^\n]*\n*/;
332
- if (recordingLinkRegex.test(result)) {
333
- result = result.replace(recordingLinkRegex, `- Call recording link: ${recordingLink}\n`);
334
- } else {
335
- if (result && !result.endsWith('\n')) {
336
- result += '\n';
337
- }
338
- result += `- Call recording link: ${recordingLink}\n`;
339
- }
340
- }
341
- return result;
342
- }
343
-
344
- function upsertAiNote({ body, aiNote, logFormat }) {
345
- if (!aiNote) return body;
346
-
347
- const clearedAiNote = aiNote.replace(/\n+$/, '');
348
- let result = body;
349
-
350
- if (logFormat === FORMAT_TYPES.HTML) {
351
- const formattedAiNote = clearedAiNote.replace(/(?:\r\n|\r|\n)/g, '<br>');
352
- const aiNoteRegex = /<div><b>AI Note<\/b><br>(.+?)<\/div>/;
353
- if (aiNoteRegex.test(result)) {
354
- result = result.replace(aiNoteRegex, `<div><b>AI Note</b><br>${formattedAiNote}</div>`);
355
- } else {
356
- result += `<div><b>AI Note</b><br>${formattedAiNote}</div><br>`;
357
- }
358
- } else {
359
- const aiNoteRegex = /- AI Note:([\s\S]*?)--- END/;
360
- if (aiNoteRegex.test(result)) {
361
- result = result.replace(aiNoteRegex, `- AI Note:\n${clearedAiNote}\n--- END`);
362
- } else {
363
- result += `- AI Note:\n${clearedAiNote}\n--- END\n`;
364
- }
365
- }
366
- return result;
367
- }
368
-
369
- function upsertTranscript({ body, transcript, logFormat }) {
370
- if (!transcript) return body;
371
-
372
- let result = body;
373
-
374
- if (logFormat === FORMAT_TYPES.HTML) {
375
- const formattedTranscript = transcript.replace(/(?:\r\n|\r|\n)/g, '<br>');
376
- const transcriptRegex = /<div><b>Transcript<\/b><br>(.+?)<\/div>/;
377
- if (transcriptRegex.test(result)) {
378
- result = result.replace(transcriptRegex, `<div><b>Transcript</b><br>${formattedTranscript}</div>`);
379
- } else {
380
- result += `<div><b>Transcript</b><br>${formattedTranscript}</div><br>`;
381
- }
382
- } else {
383
- const transcriptRegex = /- Transcript:([\s\S]*?)--- END/;
384
- if (transcriptRegex.test(result)) {
385
- result = result.replace(transcriptRegex, `- Transcript:\n${transcript}\n--- END`);
386
- } else {
387
- result += `- Transcript:\n${transcript}\n--- END\n`;
388
- }
389
- }
390
- return result;
391
- }
392
-
393
- /**
394
- * Helper function to determine format type for a CRM platform
395
- * @param {string} platform - CRM platform name
396
- * @returns {string} Format type
397
- */
398
- function getLogFormatType(platform) {
399
- const manifest = adapterRegistry.getManifest(platform, true);
400
- const platformConfig = manifest.platforms?.[platform];
401
- return platformConfig?.logFormat;
402
- }
403
-
404
- /**
405
- * Create a specialized composition function for specific CRM requirements
406
- * @param {string} platform - CRM platform name
407
- * @returns {Function} Customized composition function
408
- */
409
- function createComposer(platform) {
410
- const logFormat = getLogFormatType(platform);
411
-
412
- return async function (params) {
413
- // Add platform-specific formatting
414
- if (logFormat === FORMAT_TYPES.HTML && platform === 'pipedrive') {
415
- // Pipedrive wraps call details in <ul> tags
416
- const composed = await composeCallLog({ ...params, logFormat });
417
- if (composed && !composed.includes('<ul>')) {
418
- return `<b>Call details</b><ul>${composed}</ul>`;
419
- }
420
- return composed;
421
- }
422
-
423
- if (logFormat === FORMAT_TYPES.HTML && platform === 'bullhorn') {
424
- // Bullhorn also wraps call details in <ul> tags
425
- const composed = await composeCallLog({ ...params, logFormat });
426
- if (composed && !composed.includes('<ul>')) {
427
- return `<b>Call details</b><ul>${composed}</ul>`;
428
- }
429
- return composed;
430
- }
431
-
432
- return composeCallLog({ ...params, logFormat });
433
- };
434
- }
435
-
436
- module.exports = {
437
- composeCallLog,
438
- createComposer,
439
- getLogFormatType,
440
- FORMAT_TYPES,
441
- // Export individual upsert functions for backward compatibility
442
- upsertCallAgentNote,
443
- upsertCallSessionId,
444
- upsertCallSubject,
445
- upsertContactPhoneNumber,
446
- upsertCallDateTime,
447
- upsertCallDuration,
448
- upsertCallResult,
449
- upsertCallRecording,
450
- upsertAiNote,
451
- upsertTranscript
1
+ const moment = require('moment-timezone');
2
+ const { secondsToHoursMinutesSeconds } = require('./util');
3
+ const adapterRegistry = require('../adapter/registry');
4
+ const { LOG_DETAILS_FORMAT_TYPE } = require('./constants');
5
+
6
+ /**
7
+ * Centralized call log composition module
8
+ * Supports both plain text and HTML formats used across different CRM adapters
9
+ */
10
+
11
+ /**
12
+ * Compose call log details based on user settings and format type
13
+ * @param {Object} params - Composition parameters
14
+ * @param {string} params.logFormat - logFormat type: 'plainText' or 'html'
15
+ * @param {string} params.existingBody - Existing log body (for updates)
16
+ * @param {Object} params.callLog - Call log information
17
+ * @param {Object} params.contactInfo - Contact information
18
+ * @param {Object} params.user - User information
19
+ * @param {string} params.note - User note
20
+ * @param {string} params.aiNote - AI generated note
21
+ * @param {string} params.transcript - Call transcript
22
+ * @param {string} params.recordingLink - Recording link
23
+ * @param {string} params.subject - Call subject
24
+ * @param {Date} params.startTime - Call start time
25
+ * @param {number} params.duration - Call duration in seconds
26
+ * @param {string} params.result - Call result
27
+ * @returns {Promise<string>} Composed log body
28
+ */
29
+ async function composeCallLog(params) {
30
+ const {
31
+ logFormat = LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT,
32
+ existingBody = '',
33
+ callLog,
34
+ contactInfo,
35
+ user,
36
+ note,
37
+ aiNote,
38
+ transcript,
39
+ recordingLink,
40
+ subject,
41
+ startTime,
42
+ duration,
43
+ result,
44
+ platform
45
+ } = params;
46
+
47
+ let body = existingBody;
48
+ const userSettings = user.userSettings || {};
49
+ // Determine timezone handling
50
+ let resolvedStartTime = startTime || callLog?.startTime;
51
+ let timezoneOffset = user.timezoneOffset;
52
+ if (resolvedStartTime) {
53
+ resolvedStartTime = moment(resolvedStartTime);
54
+ }
55
+ // Apply upsert functions based on user settings
56
+ if (note && (userSettings?.addCallLogNote?.value ?? true)) {
57
+ body = upsertCallAgentNote({ body, note, logFormat });
58
+ }
59
+
60
+ if (callLog?.sessionId && (userSettings?.addCallSessionId?.value ?? false)) {
61
+ body = upsertCallSessionId({ body, id: callLog.sessionId, logFormat });
62
+ }
63
+
64
+ const ringcentralUsername = callLog.direction === 'Inbound' ? callLog?.to?.name : callLog?.from?.name;
65
+ if (ringcentralUsername && (userSettings?.addRingCentralUserName?.value ?? false)) {
66
+ body = upsertRingCentralUserName({ body, userName: ringcentralUsername, logFormat });
67
+ }
68
+
69
+ const ringcentralNumber = callLog.direction === 'Inbound' ? callLog?.to?.phoneNumber : callLog?.from?.phoneNumber;
70
+ if (ringcentralNumber && (userSettings?.addRingCentralNumber?.value ?? false)) {
71
+ const ringcentralExtensionNumber = callLog.direction === 'Inbound' ? callLog?.from?.extensionNumber : callLog?.to?.extensionNumber;
72
+ body = upsertRingCentralNumberAndExtension({ body, number: ringcentralNumber, extension: ringcentralExtensionNumber ?? '', logFormat });
73
+ }
74
+
75
+ if (subject && (userSettings?.addCallLogSubject?.value ?? true)) {
76
+ body = upsertCallSubject({ body, subject, logFormat });
77
+ }
78
+
79
+ if (contactInfo?.phoneNumber && (userSettings?.addCallLogContactNumber?.value ?? false)) {
80
+ body = upsertContactPhoneNumber({
81
+ body,
82
+ phoneNumber: contactInfo.phoneNumber,
83
+ direction: callLog?.direction,
84
+ logFormat
85
+ });
86
+ }
87
+
88
+ if (resolvedStartTime && (userSettings?.addCallLogDateTime?.value ?? true)) {
89
+ body = upsertCallDateTime({
90
+ body,
91
+ startTime: resolvedStartTime,
92
+ timezoneOffset,
93
+ logDateFormat: userSettings?.logDateFormat?.value ?? 'YYYY-MM-DD hh:mm:ss A',
94
+ logFormat
95
+ });
96
+ }
97
+
98
+ if (typeof duration !== 'undefined' && (userSettings?.addCallLogDuration?.value ?? true)) {
99
+ body = upsertCallDuration({ body, duration, logFormat });
100
+ }
101
+
102
+ if (result && (userSettings?.addCallLogResult?.value ?? true)) {
103
+ body = upsertCallResult({ body, result, logFormat });
104
+ }
105
+
106
+ if (recordingLink && (userSettings?.addCallLogRecording?.value ?? true)) {
107
+ body = upsertCallRecording({ body, recordingLink, logFormat });
108
+ }
109
+
110
+ if (aiNote && (userSettings?.addCallLogAINote?.value ?? true)) {
111
+ body = upsertAiNote({ body, aiNote, logFormat });
112
+ }
113
+
114
+ if (transcript && (userSettings?.addCallLogTranscript?.value ?? true)) {
115
+ body = upsertTranscript({ body, transcript, logFormat });
116
+ }
117
+
118
+ return body;
119
+ }
120
+
121
+ /**
122
+ * Upsert functions for different log components
123
+ */
124
+
125
+ function upsertCallAgentNote({ body, note, logFormat }) {
126
+ if (!note) return body;
127
+
128
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
129
+ // HTML logFormat with proper Agent notes section handling
130
+ const noteRegex = RegExp('<b>Agent notes</b>([\\s\\S]+?)Call details</b>');
131
+ if (noteRegex.test(body)) {
132
+ return body.replace(noteRegex, `<b>Agent notes</b><br>${note}<br><br><b>Call details</b>`);
133
+ }
134
+ return `<b>Agent notes</b><br>${note}<br><br><b>Call details</b><br>` + body;
135
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
136
+ // Markdown logFormat with proper Agent notes section handling
137
+ const noteRegex = /## Agent notes\n([\s\S]*?)\n## Call details/;
138
+ if (noteRegex.test(body)) {
139
+ return body.replace(noteRegex, `## Agent notes\n${note}\n\n## Call details`);
140
+ }
141
+ if (body.startsWith('## Call details')) {
142
+ return `## Agent notes\n${note}\n\n` + body;
143
+ }
144
+ return `## Agent notes\n${note}\n\n## Call details\n` + body;
145
+ } else {
146
+ // Plain text logFormat - FIXED REGEX for multi-line notes with blank lines
147
+ const noteRegex = /- (?:Note|Agent notes): ([\s\S]*?)(?=\n- [A-Z][a-zA-Z\s/]*:|\n$|$)/;
148
+ if (noteRegex.test(body)) {
149
+ return body.replace(noteRegex, `- Note: ${note}`);
150
+ }
151
+ return `- Note: ${note}\n` + body;
152
+ }
153
+ }
154
+
155
+ function upsertCallSessionId({ body, id, logFormat }) {
156
+ if (!id) return body;
157
+
158
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
159
+ // More flexible regex that handles both <li> wrapped and unwrapped content
160
+ const idRegex = /(?:<li>)?<b>Session Id<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
161
+ if (idRegex.test(body)) {
162
+ return body.replace(idRegex, `<li><b>Session Id</b>: ${id}</li>`);
163
+ }
164
+ return body + `<li><b>Session Id</b>: ${id}</li>`;
165
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
166
+ // Markdown format: **Session Id**: value
167
+ const sessionIdRegex = /\*\*Session Id\*\*: [^\n]*\n*/;
168
+ if (sessionIdRegex.test(body)) {
169
+ return body.replace(sessionIdRegex, `**Session Id**: ${id}\n`);
170
+ }
171
+ return body + `**Session Id**: ${id}\n`;
172
+ } else {
173
+ // Match Session Id field and any trailing newlines, replace with single newline
174
+ const sessionIdRegex = /- Session Id: [^\n]*\n*/;
175
+ if (sessionIdRegex.test(body)) {
176
+ return body.replace(sessionIdRegex, `- Session Id: ${id}\n`);
177
+ }
178
+ return body + `- Session Id: ${id}\n`;
179
+ }
180
+ }
181
+
182
+ function upsertRingCentralUserName({ body, userName, logFormat }) {
183
+ if (!userName) return body;
184
+
185
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
186
+ const userNameRegex = /(?:<li>)?<b>RingCentral user name<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
187
+ if (userNameRegex.test(body)) {
188
+ return body.replace(userNameRegex, `<li><b>RingCentral user name</b>: ${userName}</li>`);
189
+ } else {
190
+ return body + `<li><b>RingCentral user name</b>: ${userName}</li>`;
191
+ }
192
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
193
+ const userNameRegex = /\*\*RingCentral user name\*\*: [^\n]*\n*/i;
194
+ if (userNameRegex.test(body)) {
195
+ return body.replace(userNameRegex, `**RingCentral user name**: ${userName}\n`);
196
+ } else {
197
+ return body + `**RingCentral user name**: ${userName}\n`;
198
+ }
199
+ } else {
200
+ const userNameRegex = /- RingCentral user name: [^\n]*\n*/;
201
+ if (userNameRegex.test(body)) {
202
+ return body.replace(userNameRegex, `- RingCentral user name: ${userName}\n`);
203
+ } else {
204
+ return body + `- RingCentral user name: ${userName}\n`;
205
+ }
206
+ }
207
+ }
208
+
209
+ function upsertRingCentralNumberAndExtension({ body, number, extension, logFormat }) {
210
+ if (!number && !extension) return body;
211
+
212
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
213
+ const numberAndExtensionRegex = /(?:<li>)?<b>RingCentral number and extension<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
214
+ if (numberAndExtensionRegex.test(body)) {
215
+ return body.replace(numberAndExtensionRegex, `<li><b>RingCentral number and extension</b>: ${number} ${extension}</li>`);
216
+ }
217
+ return body + `<li><b>RingCentral number and extension</b>: ${number} ${extension}</li>`;
218
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
219
+ const numberAndExtensionRegex = /\*\*RingCentral number and extension\*\*: [^\n]*\n*/i;
220
+ if (numberAndExtensionRegex.test(body)) {
221
+ return body.replace(numberAndExtensionRegex, `**RingCentral number and extension**: ${number} ${extension}\n`);
222
+ }
223
+ return body + `**RingCentral number and extension**: ${number} ${extension}\n`;
224
+ } else {
225
+ const numberAndExtensionRegex = /- RingCentral number and extension: [^\n]*\n*/;
226
+ if (numberAndExtensionRegex.test(body)) {
227
+ return body.replace(numberAndExtensionRegex, `- RingCentral number and extension: ${number} ${extension}\n`);
228
+ }
229
+ return body + `- RingCentral number and extension: ${number} ${extension}\n`;
230
+ }
231
+ }
232
+
233
+ function upsertCallSubject({ body, subject, logFormat }) {
234
+ if (!subject) return body;
235
+
236
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
237
+ // More flexible regex that handles both <li> wrapped and unwrapped content
238
+ const subjectRegex = /(?:<li>)?<b>Summary<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
239
+ if (subjectRegex.test(body)) {
240
+ return body.replace(subjectRegex, `<li><b>Summary</b>: ${subject}</li>`);
241
+ }
242
+ return body + `<li><b>Summary</b>: ${subject}</li>`;
243
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
244
+ // Markdown format: **Summary**: value
245
+ const subjectRegex = /\*\*Summary\*\*: [^\n]*\n*/;
246
+ if (subjectRegex.test(body)) {
247
+ return body.replace(subjectRegex, `**Summary**: ${subject}\n`);
248
+ }
249
+ return body + `**Summary**: ${subject}\n`;
250
+ } else {
251
+ // Match Summary field and any trailing newlines, replace with single newline
252
+ const subjectRegex = /- Summary: [^\n]*\n*/;
253
+ if (subjectRegex.test(body)) {
254
+ return body.replace(subjectRegex, `- Summary: ${subject}\n`);
255
+ }
256
+ return body + `- Summary: ${subject}\n`;
257
+ }
258
+ }
259
+
260
+ function upsertContactPhoneNumber({ body, phoneNumber, direction, logFormat }) {
261
+ if (!phoneNumber) return body;
262
+
263
+ const label = direction === 'Outbound' ? 'Recipient' : 'Caller';
264
+ let result = body;
265
+
266
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
267
+ // More flexible regex that handles both <li> wrapped and unwrapped content
268
+ const phoneNumberRegex = new RegExp(`(?:<li>)?<b>${label} phone number</b>:\\s*([^<\\n]+)(?:</li>|(?=<|$))`, 'i');
269
+ if (phoneNumberRegex.test(result)) {
270
+ result = result.replace(phoneNumberRegex, `<li><b>${label} phone number</b>: ${phoneNumber}</li>`);
271
+ } else {
272
+ result += `<li><b>${label} phone number</b>: ${phoneNumber}</li>`;
273
+ }
274
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
275
+ // Markdown format: **Contact Number**: value
276
+ const phoneNumberRegex = /\*\*Contact Number\*\*: [^\n]*\n*/;
277
+ if (phoneNumberRegex.test(result)) {
278
+ result = result.replace(phoneNumberRegex, `**Contact Number**: ${phoneNumber}\n`);
279
+ } else {
280
+ result += `**Contact Number**: ${phoneNumber}\n`;
281
+ }
282
+ } else {
283
+ // More flexible regex that handles both with and without newlines
284
+ const phoneNumberRegex = /- Contact Number: ([^\n-]+)(?=\n-|\n|$)/;
285
+ if (phoneNumberRegex.test(result)) {
286
+ result = result.replace(phoneNumberRegex, `- Contact Number: ${phoneNumber}\n`);
287
+ } else {
288
+ result += `- Contact Number: ${phoneNumber}\n`;
289
+ }
290
+ }
291
+ return result;
292
+ }
293
+
294
+ function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat, logDateFormat }) {
295
+ if (!startTime) return body;
296
+
297
+ // Simple approach: convert to moment and apply timezone offset
298
+ let momentTime = moment(startTime);
299
+ if (timezoneOffset) {
300
+ // Handle both string offsets ('+05:30') and numeric offsets (330 minutes or 5.5 hours)
301
+ if (typeof timezoneOffset === 'string' && timezoneOffset.includes(':')) {
302
+ // String logFormat like '+05:30' or '-05:00'
303
+ momentTime = momentTime.utcOffset(timezoneOffset);
304
+ } else {
305
+ // Numeric logFormat (minutes or hours)
306
+ momentTime = momentTime.utcOffset(Number(timezoneOffset));
307
+ }
308
+ }
309
+ const formattedDateTime = momentTime.format(logDateFormat || 'YYYY-MM-DD hh:mm:ss A');
310
+ let result = body;
311
+
312
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
313
+ // More flexible regex that handles both <li> wrapped and unwrapped content
314
+ const dateTimeRegex = /(?:<li>)?<b>Date\/time<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
315
+ if (dateTimeRegex.test(result)) {
316
+ result = result.replace(dateTimeRegex, `<li><b>Date/time</b>: ${formattedDateTime}</li>`);
317
+ } else {
318
+ result += `<li><b>Date/time</b>: ${formattedDateTime}</li>`;
319
+ }
320
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
321
+ // Markdown format: **Date/Time**: value
322
+ const dateTimeRegex = /\*\*Date\/Time\*\*: [^\n]*\n*/;
323
+ if (dateTimeRegex.test(result)) {
324
+ result = result.replace(dateTimeRegex, `**Date/Time**: ${formattedDateTime}\n`);
325
+ } else {
326
+ result += `**Date/Time**: ${formattedDateTime}\n`;
327
+ }
328
+ } else {
329
+ // Handle duplicated Date/Time entries and match complete date/time values
330
+ const dateTimeRegex = /(?:- Date\/Time: [^-]*(?:-[^-]*)*)+/;
331
+ if (dateTimeRegex.test(result)) {
332
+ result = result.replace(dateTimeRegex, `- Date/Time: ${formattedDateTime}\n`);
333
+ } else {
334
+ result += `- Date/Time: ${formattedDateTime}\n`;
335
+ }
336
+ }
337
+ return result;
338
+ }
339
+
340
+ function upsertCallDuration({ body, duration, logFormat }) {
341
+ if (typeof duration === 'undefined') return body;
342
+
343
+ const formattedDuration = secondsToHoursMinutesSeconds(duration);
344
+ let result = body;
345
+
346
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
347
+ // More flexible regex that handles both <li> wrapped and unwrapped content
348
+ const durationRegex = /(?:<li>)?<b>Duration<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
349
+ if (durationRegex.test(result)) {
350
+ result = result.replace(durationRegex, `<li><b>Duration</b>: ${formattedDuration}</li>`);
351
+ } else {
352
+ result += `<li><b>Duration</b>: ${formattedDuration}</li>`;
353
+ }
354
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
355
+ // Markdown format: **Duration**: value
356
+ const durationRegex = /\*\*Duration\*\*: [^\n]*\n*/;
357
+ if (durationRegex.test(result)) {
358
+ result = result.replace(durationRegex, `**Duration**: ${formattedDuration}\n`);
359
+ } else {
360
+ result += `**Duration**: ${formattedDuration}\n`;
361
+ }
362
+ } else {
363
+ // More flexible regex that handles both with and without newlines
364
+ const durationRegex = /- Duration: ([^\n-]+)(?=\n-|\n|$)/;
365
+ if (durationRegex.test(result)) {
366
+ result = result.replace(durationRegex, `- Duration: ${formattedDuration}\n`);
367
+ } else {
368
+ result += `- Duration: ${formattedDuration}\n`;
369
+ }
370
+ }
371
+ return result;
372
+ }
373
+
374
+ function upsertCallResult({ body, result, logFormat }) {
375
+ if (!result) return body;
376
+
377
+ let bodyResult = body;
378
+
379
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
380
+ // More flexible regex that handles both <li> wrapped and unwrapped content
381
+ const resultRegex = /(?:<li>)?<b>Result<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
382
+ if (resultRegex.test(bodyResult)) {
383
+ bodyResult = bodyResult.replace(resultRegex, `<li><b>Result</b>: ${result}</li>`);
384
+ } else {
385
+ bodyResult += `<li><b>Result</b>: ${result}</li>`;
386
+ }
387
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
388
+ // Markdown format: **Result**: value
389
+ const resultRegex = /\*\*Result\*\*: [^\n]*\n*/;
390
+ if (resultRegex.test(bodyResult)) {
391
+ bodyResult = bodyResult.replace(resultRegex, `**Result**: ${result}\n`);
392
+ } else {
393
+ bodyResult += `**Result**: ${result}\n`;
394
+ }
395
+ } else {
396
+ // More flexible regex that handles both with and without newlines
397
+ const resultRegex = /- Result: ([^\n-]+)(?=\n-|\n|$)/;
398
+ if (resultRegex.test(bodyResult)) {
399
+ bodyResult = bodyResult.replace(resultRegex, `- Result: ${result}\n`);
400
+ } else {
401
+ bodyResult += `- Result: ${result}\n`;
402
+ }
403
+ }
404
+ return bodyResult;
405
+ }
406
+
407
+ function upsertCallRecording({ body, recordingLink, logFormat }) {
408
+ if (!recordingLink) return body;
409
+
410
+ let result = body;
411
+
412
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
413
+ // More flexible regex that handles both <li> wrapped and unwrapped content
414
+ const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
415
+ if (recordingLink) {
416
+ if (recordingLinkRegex.test(result)) {
417
+ if (recordingLink.startsWith('http')) {
418
+ result = result.replace(recordingLinkRegex, `<li><b>Call recording link</b>: <a target="_blank" href="${recordingLink}">open</a></li>`);
419
+ } else {
420
+ result = result.replace(recordingLinkRegex, `<li><b>Call recording link</b>: (pending...)</li>`);
421
+ }
422
+ } else {
423
+ let text = '';
424
+ if (recordingLink.startsWith('http')) {
425
+ text = `<li><b>Call recording link</b>: <a target="_blank" href="${recordingLink}">open</a></li>`;
426
+ } else {
427
+ text = '<li><b>Call recording link</b>: (pending...)</li>';
428
+ }
429
+ if (result.indexOf('</ul>') === -1) {
430
+ result += text;
431
+ } else {
432
+ result = result.replace('</ul>', `${text}</ul>`);
433
+ }
434
+ }
435
+ }
436
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
437
+ // Markdown format: **Call recording link**: value
438
+ const recordingLinkRegex = /\*\*Call recording link\*\*: [^\n]*\n*/;
439
+ if (recordingLinkRegex.test(result)) {
440
+ result = result.replace(recordingLinkRegex, `**Call recording link**: ${recordingLink}\n`);
441
+ } else {
442
+ result += `**Call recording link**: ${recordingLink}\n`;
443
+ }
444
+ } else {
445
+ // Match recording link field and any trailing content, replace with single newline
446
+ const recordingLinkRegex = /- Call recording link: [^\n]*\n*/;
447
+ if (recordingLinkRegex.test(result)) {
448
+ result = result.replace(recordingLinkRegex, `- Call recording link: ${recordingLink}\n`);
449
+ } else {
450
+ if (result && !result.endsWith('\n')) {
451
+ result += '\n';
452
+ }
453
+ result += `- Call recording link: ${recordingLink}\n`;
454
+ }
455
+ }
456
+ return result;
457
+ }
458
+
459
+ function upsertAiNote({ body, aiNote, logFormat }) {
460
+ if (!aiNote) return body;
461
+
462
+ const clearedAiNote = aiNote.replace(/\n+$/, '');
463
+ let result = body;
464
+
465
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
466
+ const formattedAiNote = clearedAiNote.replace(/(?:\r\n|\r|\n)/g, '<br>');
467
+ const aiNoteRegex = /<div><b>AI Note<\/b><br>(.+?)<\/div>/;
468
+ if (aiNoteRegex.test(result)) {
469
+ result = result.replace(aiNoteRegex, `<div><b>AI Note</b><br>${formattedAiNote}</div>`);
470
+ } else {
471
+ result += `<div><b>AI Note</b><br>${formattedAiNote}</div><br>`;
472
+ }
473
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
474
+ // Markdown format: ### AI Note
475
+ const aiNoteRegex = /### AI Note\n([\s\S]*?)(?=\n### |\n$|$)/;
476
+ if (aiNoteRegex.test(result)) {
477
+ result = result.replace(aiNoteRegex, `### AI Note\n${clearedAiNote}\n`);
478
+ } else {
479
+ result += `### AI Note\n${clearedAiNote}\n`;
480
+ }
481
+ } else {
482
+ const aiNoteRegex = /- AI Note:([\s\S]*?)--- END/;
483
+ if (aiNoteRegex.test(result)) {
484
+ result = result.replace(aiNoteRegex, `- AI Note:\n${clearedAiNote}\n--- END`);
485
+ } else {
486
+ result += `- AI Note:\n${clearedAiNote}\n--- END\n`;
487
+ }
488
+ }
489
+ return result;
490
+ }
491
+
492
+ function upsertTranscript({ body, transcript, logFormat }) {
493
+ if (!transcript) return body;
494
+
495
+ let result = body;
496
+
497
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
498
+ const formattedTranscript = transcript.replace(/(?:\r\n|\r|\n)/g, '<br>');
499
+ const transcriptRegex = /<div><b>Transcript<\/b><br>(.+?)<\/div>/;
500
+ if (transcriptRegex.test(result)) {
501
+ result = result.replace(transcriptRegex, `<div><b>Transcript</b><br>${formattedTranscript}</div>`);
502
+ } else {
503
+ result += `<div><b>Transcript</b><br>${formattedTranscript}</div><br>`;
504
+ }
505
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
506
+ // Markdown format: ### Transcript
507
+ const transcriptRegex = /### Transcript\n([\s\S]*?)(?=\n### |\n$|$)/;
508
+ if (transcriptRegex.test(result)) {
509
+ result = result.replace(transcriptRegex, `### Transcript\n${transcript}\n`);
510
+ } else {
511
+ result += `### Transcript\n${transcript}\n`;
512
+ }
513
+ } else {
514
+ const transcriptRegex = /- Transcript:([\s\S]*?)--- END/;
515
+ if (transcriptRegex.test(result)) {
516
+ result = result.replace(transcriptRegex, `- Transcript:\n${transcript}\n--- END`);
517
+ } else {
518
+ result += `- Transcript:\n${transcript}\n--- END\n`;
519
+ }
520
+ }
521
+ return result;
522
+ }
523
+
524
+ /**
525
+ * Helper function to determine format type for a CRM platform
526
+ * @param {string} platform - CRM platform name
527
+ * @returns {string} Format type
528
+ */
529
+ function getLogFormatType(platform) {
530
+ const manifest = adapterRegistry.getManifest(platform, true);
531
+ const platformConfig = manifest.platforms?.[platform];
532
+ return platformConfig?.logFormat;
533
+ }
534
+
535
+ module.exports = {
536
+ composeCallLog,
537
+ getLogFormatType,
538
+ // Export individual upsert functions for backward compatibility
539
+ upsertCallAgentNote,
540
+ upsertCallSessionId,
541
+ upsertRingCentralUserName,
542
+ upsertRingCentralNumberAndExtension,
543
+ upsertCallSubject,
544
+ upsertContactPhoneNumber,
545
+ upsertCallDateTime,
546
+ upsertCallDuration,
547
+ upsertCallResult,
548
+ upsertCallRecording,
549
+ upsertAiNote,
550
+ upsertTranscript
452
551
  };