@app-connect/core 1.5.8 → 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,486 +1,551 @@
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
- if (subject && (userSettings?.addCallLogSubject?.value ?? true)) {
65
- body = upsertCallSubject({ body, subject, logFormat });
66
- }
67
-
68
- if (contactInfo?.phoneNumber && (userSettings?.addCallLogContactNumber?.value ?? false)) {
69
- body = upsertContactPhoneNumber({
70
- body,
71
- phoneNumber: contactInfo.phoneNumber,
72
- direction: callLog?.direction,
73
- logFormat
74
- });
75
- }
76
-
77
- if (resolvedStartTime && (userSettings?.addCallLogDateTime?.value ?? true)) {
78
- body = upsertCallDateTime({
79
- body,
80
- startTime: resolvedStartTime,
81
- timezoneOffset,
82
- logFormat
83
- });
84
- }
85
-
86
- if (duration && (userSettings?.addCallLogDuration?.value ?? true)) {
87
- body = upsertCallDuration({ body, duration, logFormat });
88
- }
89
-
90
- if (result && (userSettings?.addCallLogResult?.value ?? true)) {
91
- body = upsertCallResult({ body, result, logFormat });
92
- }
93
-
94
- if (recordingLink && (userSettings?.addCallLogRecording?.value ?? true)) {
95
- body = upsertCallRecording({ body, recordingLink, logFormat });
96
- }
97
-
98
- if (aiNote && (userSettings?.addCallLogAINote?.value ?? true)) {
99
- body = upsertAiNote({ body, aiNote, logFormat });
100
- }
101
-
102
- if (transcript && (userSettings?.addCallLogTranscript?.value ?? true)) {
103
- body = upsertTranscript({ body, transcript, logFormat });
104
- }
105
-
106
- return body;
107
- }
108
-
109
- /**
110
- * Upsert functions for different log components
111
- */
112
-
113
- function upsertCallAgentNote({ body, note, logFormat }) {
114
- if (!note) return body;
115
-
116
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
117
- // HTML logFormat with proper Agent notes section handling
118
- const noteRegex = RegExp('<b>Agent notes</b>([\\s\\S]+?)Call details</b>');
119
- if (noteRegex.test(body)) {
120
- return body.replace(noteRegex, `<b>Agent notes</b><br>${note}<br><br><b>Call details</b>`);
121
- }
122
- return `<b>Agent notes</b><br>${note}<br><br><b>Call details</b><br>` + body;
123
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
124
- // Markdown logFormat with proper Agent notes section handling
125
- const noteRegex = /## Agent notes\n([\s\S]*?)\n## Call details/;
126
- if (noteRegex.test(body)) {
127
- return body.replace(noteRegex, `## Agent notes\n${note}\n\n## Call details`);
128
- }
129
- if (body.startsWith('## Call details')) {
130
- return `## Agent notes\n${note}\n\n` + body;
131
- }
132
- return `## Agent notes\n${note}\n\n## Call details\n` + body;
133
- } else {
134
- // Plain text logFormat - FIXED REGEX for multi-line notes with blank lines
135
- const noteRegex = /- (?:Note|Agent notes): ([\s\S]*?)(?=\n- [A-Z][a-zA-Z\s/]*:|\n$|$)/;
136
- if (noteRegex.test(body)) {
137
- return body.replace(noteRegex, `- Note: ${note}`);
138
- }
139
- return `- Note: ${note}\n` + body;
140
- }
141
- }
142
-
143
- function upsertCallSessionId({ body, id, logFormat }) {
144
- if (!id) return body;
145
-
146
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
147
- // More flexible regex that handles both <li> wrapped and unwrapped content
148
- const idRegex = /(?:<li>)?<b>Session Id<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
149
- if (idRegex.test(body)) {
150
- return body.replace(idRegex, `<li><b>Session Id</b>: ${id}</li>`);
151
- }
152
- return body + `<li><b>Session Id</b>: ${id}</li>`;
153
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
154
- // Markdown format: **Session Id**: value
155
- const sessionIdRegex = /\*\*Session Id\*\*: [^\n]*\n*/;
156
- if (sessionIdRegex.test(body)) {
157
- return body.replace(sessionIdRegex, `**Session Id**: ${id}\n`);
158
- }
159
- return body + `**Session Id**: ${id}\n`;
160
- } else {
161
- // Match Session Id field and any trailing newlines, replace with single newline
162
- const sessionIdRegex = /- Session Id: [^\n]*\n*/;
163
- if (sessionIdRegex.test(body)) {
164
- return body.replace(sessionIdRegex, `- Session Id: ${id}\n`);
165
- }
166
- return body + `- Session Id: ${id}\n`;
167
- }
168
- }
169
-
170
- function upsertCallSubject({ body, subject, logFormat }) {
171
- if (!subject) return body;
172
-
173
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
174
- // More flexible regex that handles both <li> wrapped and unwrapped content
175
- const subjectRegex = /(?:<li>)?<b>Summary<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
176
- if (subjectRegex.test(body)) {
177
- return body.replace(subjectRegex, `<li><b>Summary</b>: ${subject}</li>`);
178
- }
179
- return body + `<li><b>Summary</b>: ${subject}</li>`;
180
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
181
- // Markdown format: **Summary**: value
182
- const subjectRegex = /\*\*Summary\*\*: [^\n]*\n*/;
183
- if (subjectRegex.test(body)) {
184
- return body.replace(subjectRegex, `**Summary**: ${subject}\n`);
185
- }
186
- return body + `**Summary**: ${subject}\n`;
187
- } else {
188
- // Match Summary field and any trailing newlines, replace with single newline
189
- const subjectRegex = /- Summary: [^\n]*\n*/;
190
- if (subjectRegex.test(body)) {
191
- return body.replace(subjectRegex, `- Summary: ${subject}\n`);
192
- }
193
- return body + `- Summary: ${subject}\n`;
194
- }
195
- }
196
-
197
- function upsertContactPhoneNumber({ body, phoneNumber, direction, logFormat }) {
198
- if (!phoneNumber) return body;
199
-
200
- const label = direction === 'Outbound' ? 'Recipient' : 'Caller';
201
- let result = body;
202
-
203
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
204
- // More flexible regex that handles both <li> wrapped and unwrapped content
205
- const phoneNumberRegex = new RegExp(`(?:<li>)?<b>${label} phone number</b>:\\s*([^<\\n]+)(?:</li>|(?=<|$))`, 'i');
206
- if (phoneNumberRegex.test(result)) {
207
- result = result.replace(phoneNumberRegex, `<li><b>${label} phone number</b>: ${phoneNumber}</li>`);
208
- } else {
209
- result += `<li><b>${label} phone number</b>: ${phoneNumber}</li>`;
210
- }
211
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
212
- // Markdown format: **Contact Number**: value
213
- const phoneNumberRegex = /\*\*Contact Number\*\*: [^\n]*\n*/;
214
- if (phoneNumberRegex.test(result)) {
215
- result = result.replace(phoneNumberRegex, `**Contact Number**: ${phoneNumber}\n`);
216
- } else {
217
- result += `**Contact Number**: ${phoneNumber}\n`;
218
- }
219
- } else {
220
- // More flexible regex that handles both with and without newlines
221
- const phoneNumberRegex = /- Contact Number: ([^\n-]+)(?=\n-|\n|$)/;
222
- if (phoneNumberRegex.test(result)) {
223
- result = result.replace(phoneNumberRegex, `- Contact Number: ${phoneNumber}\n`);
224
- } else {
225
- result += `- Contact Number: ${phoneNumber}\n`;
226
- }
227
- }
228
- return result;
229
- }
230
-
231
- function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat }) {
232
- if (!startTime) return body;
233
-
234
- // Simple approach: convert to moment and apply timezone offset
235
- let momentTime = moment(startTime);
236
- if (timezoneOffset) {
237
- // Handle both string offsets ('+05:30') and numeric offsets (330 minutes or 5.5 hours)
238
- if (typeof timezoneOffset === 'string' && timezoneOffset.includes(':')) {
239
- // String logFormat like '+05:30' or '-05:00'
240
- momentTime = momentTime.utcOffset(timezoneOffset);
241
- } else {
242
- // Numeric logFormat (minutes or hours)
243
- momentTime = momentTime.utcOffset(Number(timezoneOffset));
244
- }
245
- }
246
- const formattedDateTime = momentTime.format('YYYY-MM-DD hh:mm:ss A');
247
- let result = body;
248
-
249
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
250
- // More flexible regex that handles both <li> wrapped and unwrapped content
251
- const dateTimeRegex = /(?:<li>)?<b>Date\/time<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
252
- if (dateTimeRegex.test(result)) {
253
- result = result.replace(dateTimeRegex, `<li><b>Date/time</b>: ${formattedDateTime}</li>`);
254
- } else {
255
- result += `<li><b>Date/time</b>: ${formattedDateTime}</li>`;
256
- }
257
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
258
- // Markdown format: **Date/Time**: value
259
- const dateTimeRegex = /\*\*Date\/Time\*\*: [^\n]*\n*/;
260
- if (dateTimeRegex.test(result)) {
261
- result = result.replace(dateTimeRegex, `**Date/Time**: ${formattedDateTime}\n`);
262
- } else {
263
- result += `**Date/Time**: ${formattedDateTime}\n`;
264
- }
265
- } else {
266
- // Handle duplicated Date/Time entries and match complete date/time values
267
- const dateTimeRegex = /(?:- Date\/Time: [^-]*(?:-[^-]*)*)+/;
268
- if (dateTimeRegex.test(result)) {
269
- result = result.replace(dateTimeRegex, `- Date/Time: ${formattedDateTime}\n`);
270
- } else {
271
- result += `- Date/Time: ${formattedDateTime}\n`;
272
- }
273
- }
274
- return result;
275
- }
276
-
277
- function upsertCallDuration({ body, duration, logFormat }) {
278
- if (!duration) return body;
279
-
280
- const formattedDuration = secondsToHoursMinutesSeconds(duration);
281
- let result = body;
282
-
283
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
284
- // More flexible regex that handles both <li> wrapped and unwrapped content
285
- const durationRegex = /(?:<li>)?<b>Duration<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
286
- if (durationRegex.test(result)) {
287
- result = result.replace(durationRegex, `<li><b>Duration</b>: ${formattedDuration}</li>`);
288
- } else {
289
- result += `<li><b>Duration</b>: ${formattedDuration}</li>`;
290
- }
291
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
292
- // Markdown format: **Duration**: value
293
- const durationRegex = /\*\*Duration\*\*: [^\n]*\n*/;
294
- if (durationRegex.test(result)) {
295
- result = result.replace(durationRegex, `**Duration**: ${formattedDuration}\n`);
296
- } else {
297
- result += `**Duration**: ${formattedDuration}\n`;
298
- }
299
- } else {
300
- // More flexible regex that handles both with and without newlines
301
- const durationRegex = /- Duration: ([^\n-]+)(?=\n-|\n|$)/;
302
- if (durationRegex.test(result)) {
303
- result = result.replace(durationRegex, `- Duration: ${formattedDuration}\n`);
304
- } else {
305
- result += `- Duration: ${formattedDuration}\n`;
306
- }
307
- }
308
- return result;
309
- }
310
-
311
- function upsertCallResult({ body, result, logFormat }) {
312
- if (!result) return body;
313
-
314
- let bodyResult = body;
315
-
316
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
317
- // More flexible regex that handles both <li> wrapped and unwrapped content
318
- const resultRegex = /(?:<li>)?<b>Result<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
319
- if (resultRegex.test(bodyResult)) {
320
- bodyResult = bodyResult.replace(resultRegex, `<li><b>Result</b>: ${result}</li>`);
321
- } else {
322
- bodyResult += `<li><b>Result</b>: ${result}</li>`;
323
- }
324
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
325
- // Markdown format: **Result**: value
326
- const resultRegex = /\*\*Result\*\*: [^\n]*\n*/;
327
- if (resultRegex.test(bodyResult)) {
328
- bodyResult = bodyResult.replace(resultRegex, `**Result**: ${result}\n`);
329
- } else {
330
- bodyResult += `**Result**: ${result}\n`;
331
- }
332
- } else {
333
- // More flexible regex that handles both with and without newlines
334
- const resultRegex = /- Result: ([^\n-]+)(?=\n-|\n|$)/;
335
- if (resultRegex.test(bodyResult)) {
336
- bodyResult = bodyResult.replace(resultRegex, `- Result: ${result}\n`);
337
- } else {
338
- bodyResult += `- Result: ${result}\n`;
339
- }
340
- }
341
- return bodyResult;
342
- }
343
-
344
- function upsertCallRecording({ body, recordingLink, logFormat }) {
345
- if (!recordingLink) return body;
346
-
347
- let result = body;
348
-
349
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
350
- // More flexible regex that handles both <li> wrapped and unwrapped content
351
- const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
352
- if (recordingLink) {
353
- if (recordingLinkRegex.test(result)) {
354
- if (recordingLink.startsWith('http')) {
355
- result = result.replace(recordingLinkRegex, `<li><b>Call recording link</b>: <a target="_blank" href="${recordingLink}">open</a></li>`);
356
- } else {
357
- result = result.replace(recordingLinkRegex, `<li><b>Call recording link</b>: (pending...)</li>`);
358
- }
359
- } else {
360
- let text = '';
361
- if (recordingLink.startsWith('http')) {
362
- text = `<li><b>Call recording link</b>: <a target="_blank" href="${recordingLink}">open</a></li>`;
363
- } else {
364
- text = '<li><b>Call recording link</b>: (pending...)</li>';
365
- }
366
- if (result.indexOf('</ul>') === -1) {
367
- result += text;
368
- } else {
369
- result = result.replace('</ul>', `${text}</ul>`);
370
- }
371
- }
372
- }
373
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
374
- // Markdown format: **Call recording link**: value
375
- const recordingLinkRegex = /\*\*Call recording link\*\*: [^\n]*\n*/;
376
- if (recordingLinkRegex.test(result)) {
377
- result = result.replace(recordingLinkRegex, `**Call recording link**: ${recordingLink}\n`);
378
- } else {
379
- result += `**Call recording link**: ${recordingLink}\n`;
380
- }
381
- } else {
382
- // Match recording link field and any trailing content, replace with single newline
383
- const recordingLinkRegex = /- Call recording link: [^\n]*\n*/;
384
- if (recordingLinkRegex.test(result)) {
385
- result = result.replace(recordingLinkRegex, `- Call recording link: ${recordingLink}\n`);
386
- } else {
387
- if (result && !result.endsWith('\n')) {
388
- result += '\n';
389
- }
390
- result += `- Call recording link: ${recordingLink}\n`;
391
- }
392
- }
393
- return result;
394
- }
395
-
396
- function upsertAiNote({ body, aiNote, logFormat }) {
397
- if (!aiNote) return body;
398
-
399
- const clearedAiNote = aiNote.replace(/\n+$/, '');
400
- let result = body;
401
-
402
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
403
- const formattedAiNote = clearedAiNote.replace(/(?:\r\n|\r|\n)/g, '<br>');
404
- const aiNoteRegex = /<div><b>AI Note<\/b><br>(.+?)<\/div>/;
405
- if (aiNoteRegex.test(result)) {
406
- result = result.replace(aiNoteRegex, `<div><b>AI Note</b><br>${formattedAiNote}</div>`);
407
- } else {
408
- result += `<div><b>AI Note</b><br>${formattedAiNote}</div><br>`;
409
- }
410
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
411
- // Markdown format: ### AI Note
412
- const aiNoteRegex = /### AI Note\n([\s\S]*?)(?=\n### |\n$|$)/;
413
- if (aiNoteRegex.test(result)) {
414
- result = result.replace(aiNoteRegex, `### AI Note\n${clearedAiNote}\n`);
415
- } else {
416
- result += `### AI Note\n${clearedAiNote}\n`;
417
- }
418
- } else {
419
- const aiNoteRegex = /- AI Note:([\s\S]*?)--- END/;
420
- if (aiNoteRegex.test(result)) {
421
- result = result.replace(aiNoteRegex, `- AI Note:\n${clearedAiNote}\n--- END`);
422
- } else {
423
- result += `- AI Note:\n${clearedAiNote}\n--- END\n`;
424
- }
425
- }
426
- return result;
427
- }
428
-
429
- function upsertTranscript({ body, transcript, logFormat }) {
430
- if (!transcript) return body;
431
-
432
- let result = body;
433
-
434
- if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
435
- const formattedTranscript = transcript.replace(/(?:\r\n|\r|\n)/g, '<br>');
436
- const transcriptRegex = /<div><b>Transcript<\/b><br>(.+?)<\/div>/;
437
- if (transcriptRegex.test(result)) {
438
- result = result.replace(transcriptRegex, `<div><b>Transcript</b><br>${formattedTranscript}</div>`);
439
- } else {
440
- result += `<div><b>Transcript</b><br>${formattedTranscript}</div><br>`;
441
- }
442
- } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
443
- // Markdown format: ### Transcript
444
- const transcriptRegex = /### Transcript\n([\s\S]*?)(?=\n### |\n$|$)/;
445
- if (transcriptRegex.test(result)) {
446
- result = result.replace(transcriptRegex, `### Transcript\n${transcript}\n`);
447
- } else {
448
- result += `### Transcript\n${transcript}\n`;
449
- }
450
- } else {
451
- const transcriptRegex = /- Transcript:([\s\S]*?)--- END/;
452
- if (transcriptRegex.test(result)) {
453
- result = result.replace(transcriptRegex, `- Transcript:\n${transcript}\n--- END`);
454
- } else {
455
- result += `- Transcript:\n${transcript}\n--- END\n`;
456
- }
457
- }
458
- return result;
459
- }
460
-
461
- /**
462
- * Helper function to determine format type for a CRM platform
463
- * @param {string} platform - CRM platform name
464
- * @returns {string} Format type
465
- */
466
- function getLogFormatType(platform) {
467
- const manifest = adapterRegistry.getManifest(platform, true);
468
- const platformConfig = manifest.platforms?.[platform];
469
- return platformConfig?.logFormat;
470
- }
471
-
472
- module.exports = {
473
- composeCallLog,
474
- getLogFormatType,
475
- // Export individual upsert functions for backward compatibility
476
- upsertCallAgentNote,
477
- upsertCallSessionId,
478
- upsertCallSubject,
479
- upsertContactPhoneNumber,
480
- upsertCallDateTime,
481
- upsertCallDuration,
482
- upsertCallResult,
483
- upsertCallRecording,
484
- upsertAiNote,
485
- 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
486
551
  };