shuttlepro-shared 1.4.1 → 1.4.2
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.
- package/common/repositories/call.repository.js +136 -196
- package/package.json +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const Call = require("../../models/Call");
|
|
2
|
-
const mongoose = require("mongoose");
|
|
3
2
|
|
|
4
3
|
class CallRepository {
|
|
5
4
|
constructor() {
|
|
@@ -9,39 +8,32 @@ class CallRepository {
|
|
|
9
8
|
};
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
// Create a new call
|
|
11
|
+
// Create a new call without transaction
|
|
13
12
|
async createCall(callData) {
|
|
14
|
-
const session = await mongoose.startSession();
|
|
15
|
-
|
|
16
13
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
profileId: callData.profileId || null,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
call.addToHistory("created", null, "Call created in system");
|
|
34
|
-
await call.save({ session });
|
|
35
|
-
|
|
36
|
-
await this._updateCallMetrics("created", session);
|
|
37
|
-
|
|
38
|
-
return call;
|
|
14
|
+
const call = new Call({
|
|
15
|
+
callId: callData.callId,
|
|
16
|
+
callerName: callData.callerName,
|
|
17
|
+
callerNumber: callData.callerNumber,
|
|
18
|
+
whatsappSdp: callData.whatsappSdp,
|
|
19
|
+
status: callData.status || "pending",
|
|
20
|
+
autoAccepted: callData.autoAccepted || false,
|
|
21
|
+
metadata: callData.metadata || {},
|
|
22
|
+
tags: callData.tags || [],
|
|
23
|
+
priority: callData.priority || 0,
|
|
24
|
+
platformId: callData.platformId || null,
|
|
25
|
+
workspaceId: callData.workspaceId || null,
|
|
26
|
+
profileId: callData.profileId || null,
|
|
39
27
|
});
|
|
28
|
+
|
|
29
|
+
call.addToHistory("created", null, "Call created in system");
|
|
30
|
+
await call.save();
|
|
31
|
+
|
|
32
|
+
await this._updateCallMetrics("created");
|
|
33
|
+
return call;
|
|
40
34
|
} catch (error) {
|
|
41
35
|
console.error(`Failed to create call ${callData.callId}:`, error);
|
|
42
36
|
throw error;
|
|
43
|
-
} finally {
|
|
44
|
-
await session.endSession();
|
|
45
37
|
}
|
|
46
38
|
}
|
|
47
39
|
|
|
@@ -52,7 +44,6 @@ class CallRepository {
|
|
|
52
44
|
|
|
53
45
|
if (options.lean) query.lean();
|
|
54
46
|
|
|
55
|
-
// Auto-populate unless explicitly disabled
|
|
56
47
|
if (options.populate !== false) {
|
|
57
48
|
query.populate([
|
|
58
49
|
{ path: "platformId", model: "Integration" },
|
|
@@ -72,7 +63,7 @@ class CallRepository {
|
|
|
72
63
|
}
|
|
73
64
|
}
|
|
74
65
|
|
|
75
|
-
// Bulk get calls
|
|
66
|
+
// Bulk get calls
|
|
76
67
|
async getCalls(filters = {}, options = {}) {
|
|
77
68
|
try {
|
|
78
69
|
const {
|
|
@@ -90,7 +81,6 @@ class CallRepository {
|
|
|
90
81
|
} = filters;
|
|
91
82
|
|
|
92
83
|
const query = {};
|
|
93
|
-
|
|
94
84
|
if (status)
|
|
95
85
|
query.status = Array.isArray(status) ? { $in: status } : status;
|
|
96
86
|
if (agentId) query.agentId = agentId;
|
|
@@ -106,7 +96,7 @@ class CallRepository {
|
|
|
106
96
|
}
|
|
107
97
|
|
|
108
98
|
if (priority !== undefined) query.priority = { $gte: priority };
|
|
109
|
-
if (tags
|
|
99
|
+
if (tags?.length > 0) query.tags = { $in: tags };
|
|
110
100
|
|
|
111
101
|
const skip = (page - 1) * limit;
|
|
112
102
|
|
|
@@ -144,128 +134,108 @@ class CallRepository {
|
|
|
144
134
|
}
|
|
145
135
|
}
|
|
146
136
|
|
|
147
|
-
// Update call
|
|
148
|
-
async updateCall(callId, updateData
|
|
149
|
-
const session = options.session || (await mongoose.startSession());
|
|
150
|
-
const shouldCommitSession = !options.session;
|
|
151
|
-
|
|
137
|
+
// Update call
|
|
138
|
+
async updateCall(callId, updateData) {
|
|
152
139
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (!call) throw new Error(`Call ${callId} not found`);
|
|
140
|
+
const call = await Call.findOne({ callId });
|
|
141
|
+
if (!call) throw new Error(`Call ${callId} not found`);
|
|
156
142
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
143
|
+
if (updateData.__v !== undefined && call.__v !== updateData.__v) {
|
|
144
|
+
throw new Error(`Concurrent update detected for call ${callId}`);
|
|
145
|
+
}
|
|
160
146
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
if (updateData.status && updateData.status !== call.status) {
|
|
182
|
-
call.addToHistory(
|
|
183
|
-
updateData.status,
|
|
184
|
-
updateData.agentId || call.agentId,
|
|
185
|
-
updateData.reason || `Status changed to ${updateData.status}`
|
|
186
|
-
);
|
|
147
|
+
const allowedFields = [
|
|
148
|
+
"status",
|
|
149
|
+
"agentId",
|
|
150
|
+
"callerName",
|
|
151
|
+
"callerNumber",
|
|
152
|
+
"priority",
|
|
153
|
+
"tags",
|
|
154
|
+
"metadata",
|
|
155
|
+
"isOnHold",
|
|
156
|
+
"platformId",
|
|
157
|
+
"workspaceId",
|
|
158
|
+
"profileId",
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
Object.keys(updateData).forEach((key) => {
|
|
162
|
+
if (allowedFields.includes(key)) {
|
|
163
|
+
call[key] = updateData[key];
|
|
187
164
|
}
|
|
165
|
+
});
|
|
188
166
|
|
|
189
|
-
|
|
190
|
-
|
|
167
|
+
if (updateData.status && updateData.status !== call.status) {
|
|
168
|
+
call.addToHistory(
|
|
169
|
+
updateData.status,
|
|
170
|
+
updateData.agentId || call.agentId,
|
|
171
|
+
updateData.reason || `Status changed to ${updateData.status}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
191
174
|
|
|
192
|
-
|
|
193
|
-
|
|
175
|
+
call.updateHeartbeat();
|
|
176
|
+
await call.save();
|
|
177
|
+
return call;
|
|
194
178
|
} catch (error) {
|
|
195
179
|
console.error(`Failed to update call ${callId}:`, error);
|
|
196
180
|
throw error;
|
|
197
|
-
} finally {
|
|
198
|
-
if (shouldCommitSession) await session.endSession();
|
|
199
181
|
}
|
|
200
182
|
}
|
|
201
183
|
|
|
202
184
|
// Assign call to agent
|
|
203
|
-
async assignCallToAgent(callId, agentId
|
|
204
|
-
const session = await mongoose.startSession();
|
|
205
|
-
|
|
185
|
+
async assignCallToAgent(callId, agentId) {
|
|
206
186
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!call) throw new Error(`Call ${callId} not found`);
|
|
187
|
+
const call = await Call.findOne({ callId });
|
|
188
|
+
if (!call) throw new Error(`Call ${callId} not found`);
|
|
210
189
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
190
|
+
if (call.status !== "pending" && call.status !== "hold") {
|
|
191
|
+
throw new Error(`Call ${callId} is not available for assignment`);
|
|
192
|
+
}
|
|
214
193
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
194
|
+
call.agentId = agentId;
|
|
195
|
+
call.status = "active";
|
|
196
|
+
call.startTime = new Date();
|
|
197
|
+
call.isOnHold = false;
|
|
198
|
+
call.addToHistory("answered", agentId, "Call answered by agent");
|
|
199
|
+
call.updateHeartbeat();
|
|
221
200
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
201
|
+
await call.save();
|
|
202
|
+
return call;
|
|
225
203
|
} catch (error) {
|
|
226
204
|
console.error(
|
|
227
205
|
`Failed to assign call ${callId} to agent ${agentId}:`,
|
|
228
206
|
error
|
|
229
207
|
);
|
|
230
208
|
throw error;
|
|
231
|
-
} finally {
|
|
232
|
-
await session.endSession();
|
|
233
209
|
}
|
|
234
210
|
}
|
|
235
211
|
|
|
236
|
-
// Bulk
|
|
212
|
+
// Bulk update calls
|
|
237
213
|
async bulkUpdateCalls(operations) {
|
|
238
|
-
const session = await mongoose.startSession();
|
|
239
|
-
|
|
240
214
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
details: op.details || "Bulk operation",
|
|
256
|
-
},
|
|
215
|
+
const bulkOps = operations.map((op) => ({
|
|
216
|
+
updateOne: {
|
|
217
|
+
filter: { callId: op.callId },
|
|
218
|
+
update: {
|
|
219
|
+
$set: {
|
|
220
|
+
...op.updateData,
|
|
221
|
+
lastHeartbeat: new Date(),
|
|
222
|
+
},
|
|
223
|
+
$push: {
|
|
224
|
+
callHistory: {
|
|
225
|
+
timestamp: new Date(),
|
|
226
|
+
action: op.action || "bulk_update",
|
|
227
|
+
agentId: op.agentId,
|
|
228
|
+
details: op.details || "Bulk operation",
|
|
257
229
|
},
|
|
258
230
|
},
|
|
259
231
|
},
|
|
260
|
-
}
|
|
232
|
+
},
|
|
233
|
+
}));
|
|
261
234
|
|
|
262
|
-
|
|
263
|
-
});
|
|
235
|
+
return await Call.bulkWrite(bulkOps);
|
|
264
236
|
} catch (error) {
|
|
265
237
|
console.error("Failed to perform bulk update:", error);
|
|
266
238
|
throw error;
|
|
267
|
-
} finally {
|
|
268
|
-
await session.endSession();
|
|
269
239
|
}
|
|
270
240
|
}
|
|
271
241
|
|
|
@@ -374,43 +344,34 @@ class CallRepository {
|
|
|
374
344
|
|
|
375
345
|
// Cleanup stale connections
|
|
376
346
|
async cleanupStaleConnections(timeoutMinutes = 5) {
|
|
377
|
-
const session = await mongoose.startSession();
|
|
378
|
-
|
|
379
347
|
try {
|
|
380
|
-
|
|
381
|
-
const cutoffTime = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
|
382
|
-
|
|
383
|
-
const staleCalls = await Call.find({
|
|
384
|
-
status: { $in: ["active", "hold", "pending"] },
|
|
385
|
-
lastHeartbeat: { $lt: cutoffTime },
|
|
386
|
-
}).session(session);
|
|
387
|
-
|
|
388
|
-
const results = [];
|
|
389
|
-
|
|
390
|
-
for (const call of staleCalls) {
|
|
391
|
-
if (call.agentId) {
|
|
392
|
-
await this.moveCallToHoldQueue(call.callId, "Connection timeout", {
|
|
393
|
-
session,
|
|
394
|
-
});
|
|
395
|
-
results.push({ callId: call.callId, action: "moved_to_hold" });
|
|
396
|
-
} else {
|
|
397
|
-
await this.endCall(call.callId, null, { session });
|
|
398
|
-
results.push({ callId: call.callId, action: "ended" });
|
|
399
|
-
}
|
|
400
|
-
}
|
|
348
|
+
const cutoffTime = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
|
401
349
|
|
|
402
|
-
|
|
403
|
-
|
|
350
|
+
const staleCalls = await Call.find({
|
|
351
|
+
status: { $in: ["active", "hold", "pending"] },
|
|
352
|
+
lastHeartbeat: { $lt: cutoffTime },
|
|
404
353
|
});
|
|
354
|
+
|
|
355
|
+
const results = [];
|
|
356
|
+
|
|
357
|
+
for (const call of staleCalls) {
|
|
358
|
+
if (call.agentId) {
|
|
359
|
+
await this.moveCallToHoldQueue(call.callId, "Connection timeout");
|
|
360
|
+
results.push({ callId: call.callId, action: "moved_to_hold" });
|
|
361
|
+
} else {
|
|
362
|
+
await this.endCall(call.callId);
|
|
363
|
+
results.push({ callId: call.callId, action: "ended" });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log(`Cleaned up ${results.length} stale connections`);
|
|
368
|
+
return results;
|
|
405
369
|
} catch (error) {
|
|
406
370
|
console.error("Failed to cleanup stale connections:", error);
|
|
407
371
|
throw error;
|
|
408
|
-
} finally {
|
|
409
|
-
await session.endSession();
|
|
410
372
|
}
|
|
411
373
|
}
|
|
412
374
|
|
|
413
|
-
// Helper methods
|
|
414
375
|
_buildDateFilter(timeframe) {
|
|
415
376
|
const now = new Date();
|
|
416
377
|
switch (timeframe) {
|
|
@@ -464,7 +425,7 @@ class CallRepository {
|
|
|
464
425
|
};
|
|
465
426
|
}
|
|
466
427
|
|
|
467
|
-
async _updateCallMetrics(action
|
|
428
|
+
async _updateCallMetrics(action) {
|
|
468
429
|
try {
|
|
469
430
|
console.log(`Updating metrics for action: ${action}`);
|
|
470
431
|
} catch (error) {
|
|
@@ -472,67 +433,46 @@ class CallRepository {
|
|
|
472
433
|
}
|
|
473
434
|
}
|
|
474
435
|
|
|
475
|
-
async moveCallToHoldQueue(
|
|
476
|
-
callId,
|
|
477
|
-
reason = "Agent disconnected",
|
|
478
|
-
options = {}
|
|
479
|
-
) {
|
|
480
|
-
const session = options.session || (await mongoose.startSession());
|
|
481
|
-
const shouldCommitSession = !options.session;
|
|
482
|
-
|
|
436
|
+
async moveCallToHoldQueue(callId, reason = "Agent disconnected") {
|
|
483
437
|
try {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
console.log(
|
|
502
|
-
`Call ${callId} moved back to hold queue due to: ${reason}`
|
|
503
|
-
);
|
|
504
|
-
return call;
|
|
505
|
-
});
|
|
438
|
+
const call = await Call.findOne({ callId });
|
|
439
|
+
if (!call) throw new Error(`Call ${callId} not found`);
|
|
440
|
+
|
|
441
|
+
const previousAgentId = call.agentId;
|
|
442
|
+
|
|
443
|
+
call.agentId = null;
|
|
444
|
+
call.status = "hold";
|
|
445
|
+
call.isOnHold = true;
|
|
446
|
+
call.autoAccepted = true;
|
|
447
|
+
call.isHoldConnection = true;
|
|
448
|
+
call.priority = Math.min(call.priority + 1, 10);
|
|
449
|
+
call.addToHistory("disconnected", previousAgentId, reason);
|
|
450
|
+
call.updateHeartbeat();
|
|
451
|
+
|
|
452
|
+
await call.save();
|
|
453
|
+
console.log(`Call ${callId} moved back to hold queue due to: ${reason}`);
|
|
454
|
+
return call;
|
|
506
455
|
} catch (error) {
|
|
507
456
|
console.error(`Failed to move call ${callId} to hold queue:`, error);
|
|
508
457
|
throw error;
|
|
509
|
-
} finally {
|
|
510
|
-
if (shouldCommitSession) await session.endSession();
|
|
511
458
|
}
|
|
512
459
|
}
|
|
513
460
|
|
|
514
|
-
async endCall(callId, agentId = null
|
|
515
|
-
const session = options.session || (await mongoose.startSession());
|
|
516
|
-
const shouldCommitSession = !options.session;
|
|
517
|
-
|
|
461
|
+
async endCall(callId, agentId = null) {
|
|
518
462
|
try {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (!call) throw new Error(`Call ${callId} not found`);
|
|
463
|
+
const call = await Call.findOne({ callId });
|
|
464
|
+
if (!call) throw new Error(`Call ${callId} not found`);
|
|
522
465
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
466
|
+
call.status = "ended";
|
|
467
|
+
call.endTime = new Date();
|
|
468
|
+
call.duration = call.currentDuration;
|
|
469
|
+
call.addToHistory("ended", agentId, "Call ended");
|
|
527
470
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
});
|
|
471
|
+
await call.save();
|
|
472
|
+
return call;
|
|
531
473
|
} catch (error) {
|
|
532
474
|
console.error(`Failed to end call ${callId}:`, error);
|
|
533
475
|
throw error;
|
|
534
|
-
} finally {
|
|
535
|
-
if (shouldCommitSession) await session.endSession();
|
|
536
476
|
}
|
|
537
477
|
}
|
|
538
478
|
}
|