shuttlepro-shared 1.4.2 → 1.4.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.
- package/common/repositories/customerProfile.repository.js +32 -0
- package/common/repositories/index.js +2 -3
- package/common/repositories/interactive.repository.js +124 -0
- package/config/socket.js +30 -249
- package/models/Attribute.js +5 -0
- package/models/Automation.js +10 -0
- package/models/Card.js +5 -0
- package/models/Category.js +5 -0
- package/models/Chatbot.js +1 -0
- package/models/Checkpoint.js +5 -0
- package/models/Customer.js +5 -0
- package/models/Interactive.js +70 -0
- package/models/Location.js +5 -0
- package/models/Order.js +6 -0
- package/models/OrderPdf.js +5 -0
- package/models/OrderProduct.js +5 -0
- package/models/Product.js +5 -0
- package/models/ProductAttachment.js +5 -0
- package/models/ProductAttribute.js +5 -0
- package/models/ProductCategory.js +5 -0
- package/models/ProductTag.js +5 -0
- package/models/ProductVariant.js +5 -0
- package/models/Tag.js +5 -0
- package/models/VariantLocation.js +5 -0
- package/models.js +2 -2
- package/package.json +1 -1
- package/common/repositories/call.repository.js +0 -480
- package/models/Call.js +0 -188
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
const Call = require("../../models/Call");
|
|
2
|
-
|
|
3
|
-
class CallRepository {
|
|
4
|
-
constructor() {
|
|
5
|
-
this.pipelines = {
|
|
6
|
-
callStats: this._buildCallStatsPipeline(),
|
|
7
|
-
holdQueue: this._buildHoldQueuePipeline(),
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Create a new call without transaction
|
|
12
|
-
async createCall(callData) {
|
|
13
|
-
try {
|
|
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,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
call.addToHistory("created", null, "Call created in system");
|
|
30
|
-
await call.save();
|
|
31
|
-
|
|
32
|
-
await this._updateCallMetrics("created");
|
|
33
|
-
return call;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error(`Failed to create call ${callData.callId}:`, error);
|
|
36
|
-
throw error;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Get call by ID
|
|
41
|
-
async getCall(callId, options = {}) {
|
|
42
|
-
try {
|
|
43
|
-
const query = Call.findOne({ callId });
|
|
44
|
-
|
|
45
|
-
if (options.lean) query.lean();
|
|
46
|
-
|
|
47
|
-
if (options.populate !== false) {
|
|
48
|
-
query.populate([
|
|
49
|
-
{ path: "platformId", model: "Integration" },
|
|
50
|
-
{ path: "workspaceId", model: "Workspace" },
|
|
51
|
-
{ path: "profileId", model: "Profile" },
|
|
52
|
-
]);
|
|
53
|
-
} else if (options.populate) {
|
|
54
|
-
query.populate(options.populate);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (options.select) query.select(options.select);
|
|
58
|
-
|
|
59
|
-
return await query.exec();
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error(`Failed to get call ${callId}:`, error);
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Bulk get calls
|
|
67
|
-
async getCalls(filters = {}, options = {}) {
|
|
68
|
-
try {
|
|
69
|
-
const {
|
|
70
|
-
status,
|
|
71
|
-
agentId,
|
|
72
|
-
dateRange,
|
|
73
|
-
priority,
|
|
74
|
-
tags,
|
|
75
|
-
platformId,
|
|
76
|
-
workspaceId,
|
|
77
|
-
profileId,
|
|
78
|
-
page = 1,
|
|
79
|
-
limit = 50,
|
|
80
|
-
sort = { createdAt: -1 },
|
|
81
|
-
} = filters;
|
|
82
|
-
|
|
83
|
-
const query = {};
|
|
84
|
-
if (status)
|
|
85
|
-
query.status = Array.isArray(status) ? { $in: status } : status;
|
|
86
|
-
if (agentId) query.agentId = agentId;
|
|
87
|
-
if (platformId) query.platformId = platformId;
|
|
88
|
-
if (workspaceId) query.workspaceId = workspaceId;
|
|
89
|
-
if (profileId) query.profileId = profileId;
|
|
90
|
-
|
|
91
|
-
if (dateRange) {
|
|
92
|
-
query.createdAt = {
|
|
93
|
-
$gte: new Date(dateRange.start),
|
|
94
|
-
$lte: new Date(dateRange.end),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (priority !== undefined) query.priority = { $gte: priority };
|
|
99
|
-
if (tags?.length > 0) query.tags = { $in: tags };
|
|
100
|
-
|
|
101
|
-
const skip = (page - 1) * limit;
|
|
102
|
-
|
|
103
|
-
const [calls, total] = await Promise.all([
|
|
104
|
-
Call.find(query)
|
|
105
|
-
.sort(sort)
|
|
106
|
-
.skip(skip)
|
|
107
|
-
.limit(limit)
|
|
108
|
-
.lean(options.lean !== false)
|
|
109
|
-
.populate(
|
|
110
|
-
options.populate !== false
|
|
111
|
-
? [
|
|
112
|
-
{ path: "platformId", model: "Integration" },
|
|
113
|
-
{ path: "workspaceId", model: "Workspace" },
|
|
114
|
-
{ path: "profileId", model: "Profile" },
|
|
115
|
-
]
|
|
116
|
-
: []
|
|
117
|
-
)
|
|
118
|
-
.exec(),
|
|
119
|
-
Call.countDocuments(query),
|
|
120
|
-
]);
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
calls,
|
|
124
|
-
pagination: {
|
|
125
|
-
page,
|
|
126
|
-
limit,
|
|
127
|
-
total,
|
|
128
|
-
pages: Math.ceil(total / limit),
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error("Failed to get calls:", error);
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Update call
|
|
138
|
-
async updateCall(callId, updateData) {
|
|
139
|
-
try {
|
|
140
|
-
const call = await Call.findOne({ callId });
|
|
141
|
-
if (!call) throw new Error(`Call ${callId} not found`);
|
|
142
|
-
|
|
143
|
-
if (updateData.__v !== undefined && call.__v !== updateData.__v) {
|
|
144
|
-
throw new Error(`Concurrent update detected for call ${callId}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
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];
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
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
|
-
}
|
|
174
|
-
|
|
175
|
-
call.updateHeartbeat();
|
|
176
|
-
await call.save();
|
|
177
|
-
return call;
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.error(`Failed to update call ${callId}:`, error);
|
|
180
|
-
throw error;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Assign call to agent
|
|
185
|
-
async assignCallToAgent(callId, agentId) {
|
|
186
|
-
try {
|
|
187
|
-
const call = await Call.findOne({ callId });
|
|
188
|
-
if (!call) throw new Error(`Call ${callId} not found`);
|
|
189
|
-
|
|
190
|
-
if (call.status !== "pending" && call.status !== "hold") {
|
|
191
|
-
throw new Error(`Call ${callId} is not available for assignment`);
|
|
192
|
-
}
|
|
193
|
-
|
|
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();
|
|
200
|
-
|
|
201
|
-
await call.save();
|
|
202
|
-
return call;
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error(
|
|
205
|
-
`Failed to assign call ${callId} to agent ${agentId}:`,
|
|
206
|
-
error
|
|
207
|
-
);
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Bulk update calls
|
|
213
|
-
async bulkUpdateCalls(operations) {
|
|
214
|
-
try {
|
|
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",
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
}));
|
|
234
|
-
|
|
235
|
-
return await Call.bulkWrite(bulkOps);
|
|
236
|
-
} catch (error) {
|
|
237
|
-
console.error("Failed to perform bulk update:", error);
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Hold queue with priority
|
|
243
|
-
async getHoldQueue(options = {}) {
|
|
244
|
-
try {
|
|
245
|
-
const pipeline = [
|
|
246
|
-
{ $match: { status: "hold", isOnHold: true } },
|
|
247
|
-
{
|
|
248
|
-
$addFields: {
|
|
249
|
-
waitingTime: {
|
|
250
|
-
$divide: [{ $subtract: [new Date(), "$createdAt"] }, 1000 * 60],
|
|
251
|
-
},
|
|
252
|
-
priorityScore: {
|
|
253
|
-
$add: ["$priority", { $multiply: ["$waitingTime", 0.1] }],
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
{ $sort: { priorityScore: -1, createdAt: 1 } },
|
|
258
|
-
];
|
|
259
|
-
|
|
260
|
-
if (options.limit) pipeline.push({ $limit: options.limit });
|
|
261
|
-
|
|
262
|
-
return await Call.aggregate(pipeline);
|
|
263
|
-
} catch (error) {
|
|
264
|
-
console.error("Failed to get hold queue:", error);
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Get available calls
|
|
270
|
-
async getAvailableCalls(options = {}) {
|
|
271
|
-
try {
|
|
272
|
-
return await Call.find({
|
|
273
|
-
status: { $in: ["pending", "hold"] },
|
|
274
|
-
agentId: null,
|
|
275
|
-
})
|
|
276
|
-
.sort({ priority: -1, createdAt: 1 })
|
|
277
|
-
.limit(options.limit || 20)
|
|
278
|
-
.lean(options.lean !== false);
|
|
279
|
-
} catch (error) {
|
|
280
|
-
console.error("Failed to get available calls:", error);
|
|
281
|
-
throw error;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Advanced stats
|
|
286
|
-
async getAdvancedStatistics(timeframe = "today", groupBy = null) {
|
|
287
|
-
try {
|
|
288
|
-
const dateFilter = this._buildDateFilter(timeframe);
|
|
289
|
-
|
|
290
|
-
const pipeline = [
|
|
291
|
-
{ $match: dateFilter },
|
|
292
|
-
{
|
|
293
|
-
$group: {
|
|
294
|
-
_id: groupBy ? `$${groupBy}` : null,
|
|
295
|
-
totalCalls: { $sum: 1 },
|
|
296
|
-
activeCalls: {
|
|
297
|
-
$sum: { $cond: [{ $in: ["$status", ["active", "hold"]] }, 1, 0] },
|
|
298
|
-
},
|
|
299
|
-
completedCalls: {
|
|
300
|
-
$sum: { $cond: [{ $eq: ["$status", "ended"] }, 1, 0] },
|
|
301
|
-
},
|
|
302
|
-
abandonedCalls: {
|
|
303
|
-
$sum: { $cond: [{ $eq: ["$status", "abandoned"] }, 1, 0] },
|
|
304
|
-
},
|
|
305
|
-
averageDuration: { $avg: { $ifNull: ["$duration", 0] } },
|
|
306
|
-
totalDuration: { $sum: { $ifNull: ["$duration", 0] } },
|
|
307
|
-
averageWaitTime: {
|
|
308
|
-
$avg: {
|
|
309
|
-
$divide: [
|
|
310
|
-
{ $subtract: ["$startTime", "$createdAt"] },
|
|
311
|
-
1000 * 60,
|
|
312
|
-
],
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
$addFields: {
|
|
319
|
-
answerRate: {
|
|
320
|
-
$cond: [
|
|
321
|
-
{ $gt: ["$totalCalls", 0] },
|
|
322
|
-
{ $divide: ["$completedCalls", "$totalCalls"] },
|
|
323
|
-
0,
|
|
324
|
-
],
|
|
325
|
-
},
|
|
326
|
-
abandonRate: {
|
|
327
|
-
$cond: [
|
|
328
|
-
{ $gt: ["$totalCalls", 0] },
|
|
329
|
-
{ $divide: ["$abandonedCalls", "$totalCalls"] },
|
|
330
|
-
0,
|
|
331
|
-
],
|
|
332
|
-
},
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
];
|
|
336
|
-
|
|
337
|
-
const result = await Call.aggregate(pipeline);
|
|
338
|
-
return result[0] || this._getEmptyStats();
|
|
339
|
-
} catch (error) {
|
|
340
|
-
console.error("Failed to get advanced statistics:", error);
|
|
341
|
-
throw error;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Cleanup stale connections
|
|
346
|
-
async cleanupStaleConnections(timeoutMinutes = 5) {
|
|
347
|
-
try {
|
|
348
|
-
const cutoffTime = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
|
349
|
-
|
|
350
|
-
const staleCalls = await Call.find({
|
|
351
|
-
status: { $in: ["active", "hold", "pending"] },
|
|
352
|
-
lastHeartbeat: { $lt: cutoffTime },
|
|
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;
|
|
369
|
-
} catch (error) {
|
|
370
|
-
console.error("Failed to cleanup stale connections:", error);
|
|
371
|
-
throw error;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
_buildDateFilter(timeframe) {
|
|
376
|
-
const now = new Date();
|
|
377
|
-
switch (timeframe) {
|
|
378
|
-
case "today":
|
|
379
|
-
const today = new Date(now);
|
|
380
|
-
today.setHours(0, 0, 0, 0);
|
|
381
|
-
return { createdAt: { $gte: today } };
|
|
382
|
-
case "week":
|
|
383
|
-
const weekAgo = new Date(now);
|
|
384
|
-
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
385
|
-
return { createdAt: { $gte: weekAgo } };
|
|
386
|
-
case "month":
|
|
387
|
-
const monthAgo = new Date(now);
|
|
388
|
-
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
|
389
|
-
return { createdAt: { $gte: monthAgo } };
|
|
390
|
-
default:
|
|
391
|
-
return {};
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
_buildCallStatsPipeline() {
|
|
396
|
-
return [
|
|
397
|
-
{
|
|
398
|
-
$group: {
|
|
399
|
-
_id: "$status",
|
|
400
|
-
count: { $sum: 1 },
|
|
401
|
-
avgDuration: { $avg: "$duration" },
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
];
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
_buildHoldQueuePipeline() {
|
|
408
|
-
return [
|
|
409
|
-
{ $match: { status: "hold", isOnHold: true } },
|
|
410
|
-
{ $sort: { priority: -1, createdAt: 1 } },
|
|
411
|
-
];
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
_getEmptyStats() {
|
|
415
|
-
return {
|
|
416
|
-
totalCalls: 0,
|
|
417
|
-
activeCalls: 0,
|
|
418
|
-
completedCalls: 0,
|
|
419
|
-
abandonedCalls: 0,
|
|
420
|
-
averageDuration: 0,
|
|
421
|
-
totalDuration: 0,
|
|
422
|
-
averageWaitTime: 0,
|
|
423
|
-
answerRate: 0,
|
|
424
|
-
abandonRate: 0,
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async _updateCallMetrics(action) {
|
|
429
|
-
try {
|
|
430
|
-
console.log(`Updating metrics for action: ${action}`);
|
|
431
|
-
} catch (error) {
|
|
432
|
-
console.error("Failed to update metrics:", error);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
async moveCallToHoldQueue(callId, reason = "Agent disconnected") {
|
|
437
|
-
try {
|
|
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;
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.error(`Failed to move call ${callId} to hold queue:`, error);
|
|
457
|
-
throw error;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
async endCall(callId, agentId = null) {
|
|
462
|
-
try {
|
|
463
|
-
const call = await Call.findOne({ callId });
|
|
464
|
-
if (!call) throw new Error(`Call ${callId} not found`);
|
|
465
|
-
|
|
466
|
-
call.status = "ended";
|
|
467
|
-
call.endTime = new Date();
|
|
468
|
-
call.duration = call.currentDuration;
|
|
469
|
-
call.addToHistory("ended", agentId, "Call ended");
|
|
470
|
-
|
|
471
|
-
await call.save();
|
|
472
|
-
return call;
|
|
473
|
-
} catch (error) {
|
|
474
|
-
console.error(`Failed to end call ${callId}:`, error);
|
|
475
|
-
throw error;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
module.exports = new CallRepository();
|
package/models/Call.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
2
|
-
const { Schema } = mongoose;
|
|
3
|
-
|
|
4
|
-
const callSchema = new mongoose.Schema(
|
|
5
|
-
{
|
|
6
|
-
callId: {
|
|
7
|
-
type: String,
|
|
8
|
-
required: true,
|
|
9
|
-
unique: true,
|
|
10
|
-
index: true,
|
|
11
|
-
},
|
|
12
|
-
callerName: {
|
|
13
|
-
type: String,
|
|
14
|
-
required: true,
|
|
15
|
-
},
|
|
16
|
-
callerNumber: {
|
|
17
|
-
type: String,
|
|
18
|
-
required: true,
|
|
19
|
-
index: true,
|
|
20
|
-
},
|
|
21
|
-
status: {
|
|
22
|
-
type: String,
|
|
23
|
-
enum: ["pending", "hold", "active", "ended", "disconnected"],
|
|
24
|
-
default: "pending",
|
|
25
|
-
index: true,
|
|
26
|
-
},
|
|
27
|
-
queuePosition: {
|
|
28
|
-
type: Number,
|
|
29
|
-
default: 0,
|
|
30
|
-
},
|
|
31
|
-
isOnHold: {
|
|
32
|
-
type: Boolean,
|
|
33
|
-
default: false,
|
|
34
|
-
index: true,
|
|
35
|
-
},
|
|
36
|
-
autoAccepted: {
|
|
37
|
-
type: Boolean,
|
|
38
|
-
default: false,
|
|
39
|
-
},
|
|
40
|
-
isHoldConnection: {
|
|
41
|
-
type: Boolean,
|
|
42
|
-
default: false,
|
|
43
|
-
},
|
|
44
|
-
agentId: {
|
|
45
|
-
type: String,
|
|
46
|
-
default: null,
|
|
47
|
-
index: true,
|
|
48
|
-
},
|
|
49
|
-
startTime: {
|
|
50
|
-
type: Date,
|
|
51
|
-
default: null,
|
|
52
|
-
},
|
|
53
|
-
endTime: {
|
|
54
|
-
type: Date,
|
|
55
|
-
default: null,
|
|
56
|
-
},
|
|
57
|
-
duration: {
|
|
58
|
-
type: Number, // in seconds
|
|
59
|
-
default: 0,
|
|
60
|
-
},
|
|
61
|
-
whatsappSdp: {
|
|
62
|
-
type: String,
|
|
63
|
-
default: null,
|
|
64
|
-
},
|
|
65
|
-
browserSdp: {
|
|
66
|
-
type: String,
|
|
67
|
-
default: null,
|
|
68
|
-
},
|
|
69
|
-
connectionMetadata: {
|
|
70
|
-
browserConnectionState: {
|
|
71
|
-
type: String,
|
|
72
|
-
default: "new",
|
|
73
|
-
},
|
|
74
|
-
whatsappConnectionState: {
|
|
75
|
-
type: String,
|
|
76
|
-
default: "new",
|
|
77
|
-
},
|
|
78
|
-
lastHeartbeat: {
|
|
79
|
-
type: Date,
|
|
80
|
-
default: Date.now,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
type: { type: String, default: "inbound" },
|
|
84
|
-
callHistory: [
|
|
85
|
-
{
|
|
86
|
-
action: {
|
|
87
|
-
type: String,
|
|
88
|
-
enum: [
|
|
89
|
-
"created",
|
|
90
|
-
"answered",
|
|
91
|
-
"held",
|
|
92
|
-
"unheld",
|
|
93
|
-
"transferred",
|
|
94
|
-
"ended",
|
|
95
|
-
"disconnected",
|
|
96
|
-
"reconnected",
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
timestamp: {
|
|
100
|
-
type: Date,
|
|
101
|
-
default: Date.now,
|
|
102
|
-
},
|
|
103
|
-
agentId: String,
|
|
104
|
-
details: String,
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
platformId: {
|
|
108
|
-
type: Schema.Types.ObjectId,
|
|
109
|
-
ref: "Integration",
|
|
110
|
-
default: null,
|
|
111
|
-
},
|
|
112
|
-
workspaceId: { type: Schema.Types.ObjectId, ref: "Workspace" },
|
|
113
|
-
profileId: { type: Schema.Types.ObjectId, ref: "Profile" },
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
timestamps: true,
|
|
117
|
-
collection: "calls",
|
|
118
|
-
}
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// Indexes for performance
|
|
122
|
-
callSchema.index({ status: 1, createdAt: 1 });
|
|
123
|
-
callSchema.index({ agentId: 1, status: 1 });
|
|
124
|
-
callSchema.index({ callerNumber: 1, createdAt: -1 });
|
|
125
|
-
callSchema.index({ isOnHold: 1, status: 1 });
|
|
126
|
-
|
|
127
|
-
// Virtual for call duration calculation
|
|
128
|
-
callSchema.virtual("currentDuration").get(function () {
|
|
129
|
-
if (this.startTime) {
|
|
130
|
-
const endTime = this.endTime || new Date();
|
|
131
|
-
return Math.floor((endTime - this.startTime) / 1000);
|
|
132
|
-
}
|
|
133
|
-
return 0;
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Methods
|
|
137
|
-
callSchema.methods.addToHistory = function (
|
|
138
|
-
action,
|
|
139
|
-
agentId = null,
|
|
140
|
-
details = null
|
|
141
|
-
) {
|
|
142
|
-
this.callHistory.push({
|
|
143
|
-
action,
|
|
144
|
-
agentId,
|
|
145
|
-
details,
|
|
146
|
-
timestamp: new Date(),
|
|
147
|
-
});
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
callSchema.methods.updateHeartbeat = function () {
|
|
151
|
-
this.connectionMetadata.lastHeartbeat = new Date();
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
callSchema.methods.isStale = function (timeoutMinutes = 5) {
|
|
155
|
-
const timeout = timeoutMinutes * 60 * 1000; // Convert to milliseconds
|
|
156
|
-
return new Date() - this.connectionMetadata.lastHeartbeat > timeout;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Static methods
|
|
160
|
-
callSchema.statics.getActiveCallsForAgent = function (agentId) {
|
|
161
|
-
return this.find({
|
|
162
|
-
agentId,
|
|
163
|
-
status: { $in: ["active", "hold"] },
|
|
164
|
-
}).sort({ createdAt: 1 });
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
callSchema.statics.getHoldQueue = function () {
|
|
168
|
-
return this.find({
|
|
169
|
-
status: "hold",
|
|
170
|
-
agentId: null,
|
|
171
|
-
}).sort({ createdAt: 1 });
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
callSchema.statics.getPendingCalls = function () {
|
|
175
|
-
return this.find({
|
|
176
|
-
status: "pending",
|
|
177
|
-
}).sort({ createdAt: 1 });
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
callSchema.statics.getStaleConnections = function (timeoutMinutes = 5) {
|
|
181
|
-
const timeout = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
|
182
|
-
return this.find({
|
|
183
|
-
status: { $in: ["active", "hold", "pending"] },
|
|
184
|
-
"connectionMetadata.lastHeartbeat": { $lt: timeout },
|
|
185
|
-
});
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
module.exports = mongoose.model("Call", callSchema);
|