shuttlepro-shared 1.3.98 → 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 +480 -0
- package/common/repositories/index.js +2 -0
- package/config/socket.js +249 -30
- package/models/Attribute.js +0 -5
- package/models/Automation.js +0 -10
- package/models/Call.js +188 -0
- package/models/Card.js +0 -5
- package/models/Category.js +0 -5
- package/models/Checkpoint.js +0 -5
- package/models/Customer.js +0 -5
- package/models/Location.js +0 -5
- package/models/Order.js +0 -6
- package/models/OrderPdf.js +0 -5
- package/models/OrderProduct.js +0 -5
- package/models/Product.js +0 -5
- package/models/ProductAttachment.js +0 -5
- package/models/ProductAttribute.js +0 -5
- package/models/ProductCategory.js +0 -5
- package/models/ProductTag.js +0 -5
- package/models/ProductVariant.js +0 -5
- package/models/Tag.js +0 -5
- package/models/VariantLocation.js +0 -5
- package/models.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,480 @@
|
|
|
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();
|
|
@@ -15,6 +15,7 @@ const notificationSettingsRepository = require("./notificationSettings.repositor
|
|
|
15
15
|
const customerProfileRepository = require("./customerProfile.repository");
|
|
16
16
|
const customerTimelineRepository = require("./customerTimeline.repository");
|
|
17
17
|
const shopRepository = require("./shop.repository");
|
|
18
|
+
const callRepository = require("./call.repository");
|
|
18
19
|
|
|
19
20
|
exports.module = {
|
|
20
21
|
workspaceRepository,
|
|
@@ -34,4 +35,5 @@ exports.module = {
|
|
|
34
35
|
customerProfileRepository,
|
|
35
36
|
customerTimelineRepository,
|
|
36
37
|
shopRepository,
|
|
38
|
+
callRepository,
|
|
37
39
|
};
|
package/config/socket.js
CHANGED
|
@@ -4,27 +4,46 @@ require("dotenv").config();
|
|
|
4
4
|
|
|
5
5
|
let io;
|
|
6
6
|
const namespaceSockets = {}; // Store different namespace sockets
|
|
7
|
+
const namespaceHandlers = {}; // Store custom handlers for each namespace
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register custom event handlers for a specific namespace
|
|
11
|
+
* @param {string} namespace - Namespace name
|
|
12
|
+
* @param {Function} handlerFunction - Function that sets up socket event handlers
|
|
13
|
+
*/
|
|
14
|
+
const registerNamespaceHandlers = (namespace, handlerFunction) => {
|
|
15
|
+
namespaceHandlers[namespace] = handlerFunction;
|
|
16
|
+
console.log(`📋 Registered custom handlers for namespace: /${namespace}`);
|
|
17
|
+
};
|
|
7
18
|
|
|
8
19
|
/**
|
|
9
20
|
* Initialize Socket.IO server with namespaces
|
|
10
21
|
* @param {http.Server} server - HTTP server instance
|
|
11
22
|
* @param {string} namespaceParam - Socket namespace name (default: "conversation")
|
|
12
23
|
* @param {string} redisChannel - Redis channel for this namespace (default: "socket_events")
|
|
24
|
+
* @param {Object} options - Additional options for namespace configuration
|
|
25
|
+
* @param {Function} options.customHandlers - Custom socket event handlers for this namespace
|
|
26
|
+
* @param {boolean} options.requireWorkspaceId - Whether workspace ID is required (default: true)
|
|
13
27
|
* @returns {SocketIO.Namespace} - The initialized namespace
|
|
14
28
|
*/
|
|
15
29
|
const initializeSocket = async (
|
|
16
30
|
server,
|
|
17
31
|
namespaceParam = "conversation",
|
|
18
|
-
redisChannel = "socket_events"
|
|
32
|
+
redisChannel = "socket_events",
|
|
33
|
+
options = {}
|
|
19
34
|
) => {
|
|
35
|
+
const {
|
|
36
|
+
customHandlers,
|
|
37
|
+
requireWorkspaceId = true,
|
|
38
|
+
cors = {
|
|
39
|
+
origin: "*",
|
|
40
|
+
methods: ["GET", "POST"],
|
|
41
|
+
},
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
20
44
|
// Initialize Socket.IO server if not already done
|
|
21
45
|
if (!io) {
|
|
22
|
-
io = new Server(server, {
|
|
23
|
-
cors: {
|
|
24
|
-
origin: "*",
|
|
25
|
-
methods: ["GET", "POST"],
|
|
26
|
-
},
|
|
27
|
-
});
|
|
46
|
+
io = new Server(server, { cors });
|
|
28
47
|
console.log("✅ Socket.IO server initialized");
|
|
29
48
|
}
|
|
30
49
|
|
|
@@ -49,21 +68,36 @@ const initializeSocket = async (
|
|
|
49
68
|
await subscriber.subscribe(nsRedisChannel, (messageStr) => {
|
|
50
69
|
try {
|
|
51
70
|
const message = JSON.parse(messageStr);
|
|
52
|
-
const { workspaceId, event, data } = message;
|
|
71
|
+
const { workspaceId, event, data, target } = message;
|
|
53
72
|
|
|
54
73
|
console.log(`📥 Redis message received on ${nsRedisChannel}:`, {
|
|
55
74
|
workspaceId,
|
|
56
75
|
event,
|
|
76
|
+
target,
|
|
57
77
|
});
|
|
58
78
|
|
|
59
|
-
//
|
|
60
|
-
if (
|
|
79
|
+
// Handle different targeting options
|
|
80
|
+
if (target === "broadcast") {
|
|
81
|
+
// Broadcast to all clients in this namespace
|
|
82
|
+
namespace.emit(event, data);
|
|
83
|
+
console.log(
|
|
84
|
+
`📢 Event ${event} broadcasted to all clients in /${namespaceParam}`
|
|
85
|
+
);
|
|
86
|
+
} else if (target && target.startsWith("socket:")) {
|
|
87
|
+
// Target specific socket ID
|
|
88
|
+
const socketId = target.replace("socket:", "");
|
|
89
|
+
namespace.to(socketId).emit(event, data);
|
|
90
|
+
console.log(
|
|
91
|
+
`📢 Event ${event} emitted to socket ${socketId} in /${namespaceParam}`
|
|
92
|
+
);
|
|
93
|
+
} else if (workspaceId) {
|
|
94
|
+
// Emit to specific workspace room in this namespace
|
|
61
95
|
namespace.to(workspaceId).emit(event, data);
|
|
62
96
|
console.log(
|
|
63
97
|
`📢 Event ${event} emitted to workspace ${workspaceId} in /${namespaceParam}`
|
|
64
98
|
);
|
|
65
99
|
} else {
|
|
66
|
-
//
|
|
100
|
+
// Default: broadcast to all clients in this namespace
|
|
67
101
|
namespace.emit(event, data);
|
|
68
102
|
console.log(
|
|
69
103
|
`📢 Event ${event} broadcasted to all clients in /${namespaceParam}`
|
|
@@ -79,20 +113,44 @@ const initializeSocket = async (
|
|
|
79
113
|
|
|
80
114
|
// Handle new socket connections to this namespace
|
|
81
115
|
namespace.on("connection", (socket) => {
|
|
82
|
-
const { workspaceId } = socket.handshake.query;
|
|
116
|
+
const { workspaceId, callId, agentId } = socket.handshake.query;
|
|
83
117
|
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
// Workspace ID validation (can be disabled for certain namespaces)
|
|
119
|
+
if (requireWorkspaceId && !workspaceId) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`⚠️ Connection rejected: No workspaceId provided for namespace /${namespaceParam}`
|
|
122
|
+
);
|
|
86
123
|
socket.disconnect(true);
|
|
87
124
|
return;
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
console.log(
|
|
91
|
-
`✅ Client connected: ${socket.id} (Workspace: ${
|
|
128
|
+
`✅ Client connected: ${socket.id} (Workspace: ${
|
|
129
|
+
workspaceId || "N/A"
|
|
130
|
+
}, CallId: ${callId || "N/A"}) in /${namespaceParam}`
|
|
92
131
|
);
|
|
93
132
|
|
|
94
|
-
// Join workspace room
|
|
95
|
-
|
|
133
|
+
// Join workspace room if workspaceId is provided
|
|
134
|
+
if (workspaceId) {
|
|
135
|
+
socket.join(workspaceId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Join additional rooms based on query parameters
|
|
139
|
+
if (callId) {
|
|
140
|
+
socket.join(`call:${callId}`);
|
|
141
|
+
}
|
|
142
|
+
if (agentId) {
|
|
143
|
+
socket.join(`agent:${agentId}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Store socket metadata
|
|
147
|
+
socket.metadata = {
|
|
148
|
+
workspaceId,
|
|
149
|
+
callId,
|
|
150
|
+
agentId,
|
|
151
|
+
namespace: namespaceParam,
|
|
152
|
+
connectedAt: new Date(),
|
|
153
|
+
};
|
|
96
154
|
|
|
97
155
|
// Send connection confirmation to client
|
|
98
156
|
socket.emit("connected", {
|
|
@@ -100,25 +158,70 @@ const initializeSocket = async (
|
|
|
100
158
|
socketId: socket.id,
|
|
101
159
|
namespace: namespaceParam,
|
|
102
160
|
workspaceId,
|
|
161
|
+
callId,
|
|
162
|
+
agentId,
|
|
103
163
|
});
|
|
104
164
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
165
|
+
// Apply custom handlers if provided
|
|
166
|
+
if (customHandlers && typeof customHandlers === "function") {
|
|
167
|
+
try {
|
|
168
|
+
customHandlers(socket, namespace);
|
|
169
|
+
console.log(
|
|
170
|
+
`📋 Applied custom handlers for socket ${socket.id} in /${namespaceParam}`
|
|
171
|
+
);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(
|
|
174
|
+
`❌ Error applying custom handlers for socket ${socket.id}:`,
|
|
175
|
+
error
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Apply registered namespace handlers
|
|
181
|
+
if (namespaceHandlers[namespaceParam]) {
|
|
182
|
+
try {
|
|
183
|
+
namespaceHandlers[namespaceParam](socket, namespace);
|
|
184
|
+
console.log(
|
|
185
|
+
`📋 Applied registered handlers for socket ${socket.id} in /${namespaceParam}`
|
|
186
|
+
);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(
|
|
189
|
+
`❌ Error applying registered handlers for socket ${socket.id}:`,
|
|
190
|
+
error
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Default event handlers for backward compatibility
|
|
196
|
+
setupDefaultHandlers(socket, namespace, namespaceParam);
|
|
110
197
|
|
|
111
198
|
// Handle disconnect
|
|
112
199
|
socket.on("disconnect", (reason) => {
|
|
113
200
|
console.log(
|
|
114
|
-
`❌ Client disconnected: ${socket.id} (Workspace: ${
|
|
201
|
+
`❌ Client disconnected: ${socket.id} (Workspace: ${
|
|
202
|
+
workspaceId || "N/A"
|
|
203
|
+
}, Reason: ${reason}) from /${namespaceParam}`
|
|
115
204
|
);
|
|
116
|
-
|
|
205
|
+
|
|
206
|
+
// Leave all rooms
|
|
207
|
+
if (workspaceId) socket.leave(workspaceId);
|
|
208
|
+
if (callId) socket.leave(`call:${callId}`);
|
|
209
|
+
if (agentId) socket.leave(`agent:${agentId}`);
|
|
210
|
+
|
|
211
|
+
// Emit disconnect event to namespace for cleanup
|
|
212
|
+
namespace.emit("client_disconnected", {
|
|
213
|
+
socketId: socket.id,
|
|
214
|
+
metadata: socket.metadata,
|
|
215
|
+
reason,
|
|
216
|
+
});
|
|
117
217
|
});
|
|
118
218
|
|
|
119
219
|
// Handle errors
|
|
120
220
|
socket.on("error", (error) => {
|
|
121
|
-
console.error(
|
|
221
|
+
console.error(
|
|
222
|
+
`❌ Socket error for ${socket.id} in /${namespaceParam}:`,
|
|
223
|
+
error
|
|
224
|
+
);
|
|
122
225
|
});
|
|
123
226
|
});
|
|
124
227
|
|
|
@@ -128,15 +231,64 @@ const initializeSocket = async (
|
|
|
128
231
|
return namespace;
|
|
129
232
|
};
|
|
130
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Setup default event handlers for backward compatibility
|
|
236
|
+
* @param {Socket} socket - Socket.IO socket instance
|
|
237
|
+
* @param {Namespace} namespace - Socket.IO namespace instance
|
|
238
|
+
* @param {string} namespaceParam - Namespace name
|
|
239
|
+
*/
|
|
240
|
+
const setupDefaultHandlers = (socket, namespace, namespaceParam) => {
|
|
241
|
+
// Handle custom events from clients (backward compatibility)
|
|
242
|
+
socket.on("client_event", (data) => {
|
|
243
|
+
console.log(
|
|
244
|
+
`📥 Client event from ${socket.id} in /${namespaceParam}:`,
|
|
245
|
+
data
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Emit to Redis for other instances to handle
|
|
249
|
+
sendEventToServer({
|
|
250
|
+
workspaceId: socket.metadata.workspaceId,
|
|
251
|
+
event: "client_event_received",
|
|
252
|
+
data: {
|
|
253
|
+
socketId: socket.id,
|
|
254
|
+
originalData: data,
|
|
255
|
+
metadata: socket.metadata,
|
|
256
|
+
},
|
|
257
|
+
namespace: namespaceParam,
|
|
258
|
+
}).catch((err) => {
|
|
259
|
+
console.error(`❌ Error publishing client_event to Redis:`, err);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Handle room join requests
|
|
264
|
+
socket.on("join_room", (roomName) => {
|
|
265
|
+
if (roomName && typeof roomName === "string") {
|
|
266
|
+
socket.join(roomName);
|
|
267
|
+
socket.emit("room_joined", { room: roomName, success: true });
|
|
268
|
+
console.log(`📥 Socket ${socket.id} joined room: ${roomName}`);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Handle room leave requests
|
|
273
|
+
socket.on("leave_room", (roomName) => {
|
|
274
|
+
if (roomName && typeof roomName === "string") {
|
|
275
|
+
socket.leave(roomName);
|
|
276
|
+
socket.emit("room_left", { room: roomName, success: true });
|
|
277
|
+
console.log(`📤 Socket ${socket.id} left room: ${roomName}`);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
131
282
|
/**
|
|
132
283
|
* Send event to clients via Redis pub/sub
|
|
133
284
|
* @param {object} params - Event parameters
|
|
134
|
-
* @param {string} params.workspaceId - Target workspace ID
|
|
285
|
+
* @param {string} [params.workspaceId] - Target workspace ID
|
|
135
286
|
* @param {string} params.event - Event name
|
|
136
287
|
* @param {any} params.data - Event data payload
|
|
137
288
|
* @param {string} [params.namespace="conversation"] - Target namespace
|
|
138
289
|
* @param {string} [params.redisChannel="socket_events"] - Base Redis channel
|
|
139
|
-
* @
|
|
290
|
+
* @param {string} [params.target] - Specific target (broadcast, socket:socketId, etc.)
|
|
291
|
+
* @returns {Promise<boolean>}
|
|
140
292
|
*/
|
|
141
293
|
const sendEventToServer = async ({
|
|
142
294
|
workspaceId,
|
|
@@ -144,6 +296,7 @@ const sendEventToServer = async ({
|
|
|
144
296
|
data,
|
|
145
297
|
namespace = "conversation",
|
|
146
298
|
redisChannel = "socket_events",
|
|
299
|
+
target,
|
|
147
300
|
}) => {
|
|
148
301
|
try {
|
|
149
302
|
// Create namespace-specific Redis channel
|
|
@@ -153,13 +306,21 @@ const sendEventToServer = async ({
|
|
|
153
306
|
await connectRedis();
|
|
154
307
|
|
|
155
308
|
// Create message payload
|
|
156
|
-
const message = JSON.stringify({
|
|
309
|
+
const message = JSON.stringify({
|
|
310
|
+
workspaceId,
|
|
311
|
+
event,
|
|
312
|
+
data,
|
|
313
|
+
target,
|
|
314
|
+
timestamp: new Date().toISOString(),
|
|
315
|
+
});
|
|
157
316
|
|
|
158
317
|
// Publish to Redis
|
|
159
318
|
await publisher.publish(nsRedisChannel, message);
|
|
160
319
|
|
|
161
320
|
console.log(
|
|
162
|
-
`📢 Event published to Redis channel ${nsRedisChannel}: ${event} (Workspace: ${
|
|
321
|
+
`📢 Event published to Redis channel ${nsRedisChannel}: ${event} (Workspace: ${
|
|
322
|
+
workspaceId || "N/A"
|
|
323
|
+
}, Target: ${target || "default"})`
|
|
163
324
|
);
|
|
164
325
|
|
|
165
326
|
return true;
|
|
@@ -169,4 +330,62 @@ const sendEventToServer = async ({
|
|
|
169
330
|
}
|
|
170
331
|
};
|
|
171
332
|
|
|
172
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Get a specific namespace instance
|
|
335
|
+
* @param {string} namespace - Namespace name
|
|
336
|
+
* @returns {SocketIO.Namespace|null} - Namespace instance or null if not found
|
|
337
|
+
*/
|
|
338
|
+
const getNamespace = (namespace) => {
|
|
339
|
+
return namespaceSockets[namespace] || null;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get all active namespaces
|
|
344
|
+
* @returns {Object} - Object containing all active namespaces
|
|
345
|
+
*/
|
|
346
|
+
const getAllNamespaces = () => {
|
|
347
|
+
return { ...namespaceSockets };
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get Socket.IO server instance
|
|
352
|
+
* @returns {SocketIO.Server|null} - Server instance or null if not initialized
|
|
353
|
+
*/
|
|
354
|
+
const getSocketServer = () => {
|
|
355
|
+
return io || null;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Emit event to specific room in a namespace
|
|
360
|
+
* @param {string} namespace - Namespace name
|
|
361
|
+
* @param {string} room - Room name
|
|
362
|
+
* @param {string} event - Event name
|
|
363
|
+
* @param {any} data - Event data
|
|
364
|
+
* @returns {boolean} - Success status
|
|
365
|
+
*/
|
|
366
|
+
const emitToRoom = (namespace, room, event, data) => {
|
|
367
|
+
try {
|
|
368
|
+
const ns = namespaceSockets[namespace];
|
|
369
|
+
if (!ns) {
|
|
370
|
+
console.warn(`❌ Namespace /${namespace} not found`);
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
ns.to(room).emit(event, data);
|
|
375
|
+
console.log(`📢 Event ${event} emitted to room ${room} in /${namespace}`);
|
|
376
|
+
return true;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error(`❌ Error emitting to room ${room} in /${namespace}:`, error);
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
module.exports = {
|
|
384
|
+
initializeSocket,
|
|
385
|
+
sendEventToServer,
|
|
386
|
+
registerNamespaceHandlers,
|
|
387
|
+
getNamespace,
|
|
388
|
+
getAllNamespaces,
|
|
389
|
+
getSocketServer,
|
|
390
|
+
emitToRoom,
|
|
391
|
+
};
|
package/models/Attribute.js
CHANGED
|
@@ -10,11 +10,6 @@ const AttributeSchema = new Schema(
|
|
|
10
10
|
ref: "Workspace",
|
|
11
11
|
default: null,
|
|
12
12
|
},
|
|
13
|
-
websiteId: {
|
|
14
|
-
type: Schema.Types.ObjectId,
|
|
15
|
-
ref: "Website",
|
|
16
|
-
default: null,
|
|
17
|
-
},
|
|
18
13
|
oldId: { type: String, default: "" },
|
|
19
14
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
20
15
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
package/models/Automation.js
CHANGED
|
@@ -61,9 +61,6 @@ const AutomationAction = new Schema({
|
|
|
61
61
|
deleteComment: {
|
|
62
62
|
enabled: { type: Boolean, default: false },
|
|
63
63
|
},
|
|
64
|
-
hideComment: {
|
|
65
|
-
enabled: { type: Boolean, default: false },
|
|
66
|
-
},
|
|
67
64
|
closeChat: {
|
|
68
65
|
enabled: { type: Boolean, default: false },
|
|
69
66
|
},
|
|
@@ -99,11 +96,6 @@ const AutomationAction = new Schema({
|
|
|
99
96
|
default: null,
|
|
100
97
|
},
|
|
101
98
|
members: [userJoin],
|
|
102
|
-
templateFormId: {
|
|
103
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
104
|
-
ref: "FormTemplate",
|
|
105
|
-
default: null,
|
|
106
|
-
},
|
|
107
99
|
},
|
|
108
100
|
escalation: {
|
|
109
101
|
enabled: { type: Boolean, default: false },
|
|
@@ -168,7 +160,6 @@ const AutomationCondition = new Schema({
|
|
|
168
160
|
"post",
|
|
169
161
|
"shift",
|
|
170
162
|
"newCommentPost",
|
|
171
|
-
"profile",
|
|
172
163
|
],
|
|
173
164
|
},
|
|
174
165
|
keyValue: {
|
|
@@ -192,7 +183,6 @@ const AutomationCondition = new Schema({
|
|
|
192
183
|
"orderConfirmation",
|
|
193
184
|
"orderPublish",
|
|
194
185
|
"newCommentPost",
|
|
195
|
-
"profile",
|
|
196
186
|
],
|
|
197
187
|
},
|
|
198
188
|
subKeyValue: {
|
package/models/Call.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
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);
|
package/models/Card.js
CHANGED
|
@@ -156,11 +156,6 @@ const cardSchema = new mongoose.Schema(
|
|
|
156
156
|
type: mongoose.Schema.Types.Mixed,
|
|
157
157
|
default: {},
|
|
158
158
|
},
|
|
159
|
-
templateFormId: {
|
|
160
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
161
|
-
ref: "FormTemplate",
|
|
162
|
-
default: null,
|
|
163
|
-
},
|
|
164
159
|
},
|
|
165
160
|
{ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
|
|
166
161
|
);
|
package/models/Category.js
CHANGED
|
@@ -11,11 +11,6 @@ const CategorySchema = new Schema(
|
|
|
11
11
|
ref: "Workspace",
|
|
12
12
|
default: null,
|
|
13
13
|
},
|
|
14
|
-
websiteId: {
|
|
15
|
-
type: Schema.Types.ObjectId,
|
|
16
|
-
ref: "Website",
|
|
17
|
-
default: null,
|
|
18
|
-
},
|
|
19
14
|
oldId: { type: String, default: "" },
|
|
20
15
|
webCategoryId: { type: Number, default: null },
|
|
21
16
|
parentId: { type: Schema.Types.ObjectId, ref: "Category", default: null },
|
package/models/Checkpoint.js
CHANGED
|
@@ -14,11 +14,6 @@ const CheckpointSchema = new Schema(
|
|
|
14
14
|
ref: "Workspace",
|
|
15
15
|
default: null,
|
|
16
16
|
},
|
|
17
|
-
websiteId: {
|
|
18
|
-
type: Schema.Types.ObjectId,
|
|
19
|
-
ref: "Website",
|
|
20
|
-
default: null,
|
|
21
|
-
},
|
|
22
17
|
},
|
|
23
18
|
{ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
|
|
24
19
|
);
|
package/models/Customer.js
CHANGED
|
@@ -18,11 +18,6 @@ const CustomerSchema = new Schema(
|
|
|
18
18
|
ref: "Workspace",
|
|
19
19
|
default: null,
|
|
20
20
|
},
|
|
21
|
-
websiteId: {
|
|
22
|
-
type: Schema.Types.ObjectId,
|
|
23
|
-
ref: "Website",
|
|
24
|
-
default: null,
|
|
25
|
-
},
|
|
26
21
|
isBlackListed: { type: Boolean, default: false },
|
|
27
22
|
createdBy: {
|
|
28
23
|
type: Schema.Types.ObjectId,
|
package/models/Location.js
CHANGED
|
@@ -14,11 +14,6 @@ const LocationSchema = new Schema(
|
|
|
14
14
|
oldId: { type: String, default: "" },
|
|
15
15
|
webLocationId: { type: Number, default: null },
|
|
16
16
|
bcLocationId: { type: String, default: "" },
|
|
17
|
-
websiteId: {
|
|
18
|
-
type: Schema.Types.ObjectId,
|
|
19
|
-
ref: "Website",
|
|
20
|
-
default: null,
|
|
21
|
-
},
|
|
22
17
|
workspaceId: {
|
|
23
18
|
type: Schema.Types.ObjectId,
|
|
24
19
|
ref: "Workspace",
|
package/models/Order.js
CHANGED
|
@@ -103,11 +103,6 @@ const OrderSchema = new Schema(
|
|
|
103
103
|
ref: "Workspace",
|
|
104
104
|
default: null,
|
|
105
105
|
},
|
|
106
|
-
websiteId: {
|
|
107
|
-
type: Schema.Types.ObjectId,
|
|
108
|
-
ref: "Website",
|
|
109
|
-
default: null,
|
|
110
|
-
},
|
|
111
106
|
orderType: { type: String, default: "" },
|
|
112
107
|
barCode: { type: String, default: "" },
|
|
113
108
|
quantity: { type: Number, default: 0 },
|
|
@@ -228,7 +223,6 @@ const markOrUnMarkOrderAsDuplicate = async (doc) => {
|
|
|
228
223
|
const phoneRegex = getPhoneRegex(doc?.customerPhone);
|
|
229
224
|
let orders = await Order.find({
|
|
230
225
|
workspaceId: doc?.workspaceId,
|
|
231
|
-
websiteId: doc?.websiteId,
|
|
232
226
|
isDeleted: false,
|
|
233
227
|
statusType: "pending",
|
|
234
228
|
customerPhone: phoneRegex,
|
package/models/OrderPdf.js
CHANGED
|
@@ -20,11 +20,6 @@ const OrderPdfScehma = new mongoose.Schema(
|
|
|
20
20
|
ref: "Workspace",
|
|
21
21
|
default: null,
|
|
22
22
|
},
|
|
23
|
-
websiteId: {
|
|
24
|
-
type: Schema.Types.ObjectId,
|
|
25
|
-
ref: "Website",
|
|
26
|
-
default: null,
|
|
27
|
-
},
|
|
28
23
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
29
24
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
30
25
|
},
|
package/models/OrderProduct.js
CHANGED
|
@@ -31,11 +31,6 @@ const OrderProductSchema = new Schema(
|
|
|
31
31
|
ref: "Workspace",
|
|
32
32
|
default: null,
|
|
33
33
|
},
|
|
34
|
-
websiteId: {
|
|
35
|
-
type: Schema.Types.ObjectId,
|
|
36
|
-
ref: "Website",
|
|
37
|
-
default: null,
|
|
38
|
-
},
|
|
39
34
|
},
|
|
40
35
|
{ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
|
|
41
36
|
);
|
package/models/Product.js
CHANGED
|
@@ -31,11 +31,6 @@ const ProductSchema = new Schema(
|
|
|
31
31
|
type: Schema.Types.ObjectId,
|
|
32
32
|
ref: "Workspace",
|
|
33
33
|
default: null,
|
|
34
|
-
},
|
|
35
|
-
websiteId: {
|
|
36
|
-
type: Schema.Types.ObjectId,
|
|
37
|
-
ref: "Website",
|
|
38
|
-
default: null,
|
|
39
34
|
},
|
|
40
35
|
storeType: { type: String, default: "LOCAL" },
|
|
41
36
|
isDeleted: { type: Boolean, default: false },
|
|
@@ -22,11 +22,6 @@ const ProductAttachmentSchema = new Schema(
|
|
|
22
22
|
ref: "Workspace",
|
|
23
23
|
default: null,
|
|
24
24
|
},
|
|
25
|
-
websiteId: {
|
|
26
|
-
type: Schema.Types.ObjectId,
|
|
27
|
-
ref: "Website",
|
|
28
|
-
default: null,
|
|
29
|
-
},
|
|
30
25
|
height: { type: Number, default: 0 },
|
|
31
26
|
variantIds: [
|
|
32
27
|
{ type: Schema.Types.ObjectId, ref: "ProductVariant", default: null },
|
|
@@ -16,11 +16,6 @@ const ProductAttributeSchema = new mongoose.Schema(
|
|
|
16
16
|
ref: "Workspace",
|
|
17
17
|
default: null,
|
|
18
18
|
},
|
|
19
|
-
websiteId: {
|
|
20
|
-
type: Schema.Types.ObjectId,
|
|
21
|
-
ref: "Website",
|
|
22
|
-
default: null,
|
|
23
|
-
},
|
|
24
19
|
isDeleted: { type: Boolean, default: false },
|
|
25
20
|
position: { type: String, default: "1" },
|
|
26
21
|
},
|
|
@@ -11,11 +11,6 @@ const ProductCategorySchema = new Schema(
|
|
|
11
11
|
ref: "Workspace",
|
|
12
12
|
default: null,
|
|
13
13
|
},
|
|
14
|
-
websiteId: {
|
|
15
|
-
type: Schema.Types.ObjectId,
|
|
16
|
-
ref: "Website",
|
|
17
|
-
default: null,
|
|
18
|
-
},
|
|
19
14
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
20
15
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
21
16
|
isDeleted: { type: Boolean, default: false },
|
package/models/ProductTag.js
CHANGED
|
@@ -11,11 +11,6 @@ const ProductTagSchema = new Schema(
|
|
|
11
11
|
ref: "Workspace",
|
|
12
12
|
default: null,
|
|
13
13
|
},
|
|
14
|
-
websiteId: {
|
|
15
|
-
type: Schema.Types.ObjectId,
|
|
16
|
-
ref: "Website",
|
|
17
|
-
default: null,
|
|
18
|
-
},
|
|
19
14
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
20
15
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
21
16
|
isDeleted: { type: Boolean, default: false },
|
package/models/ProductVariant.js
CHANGED
|
@@ -17,11 +17,6 @@ const ProductVariantSchema = new Schema(
|
|
|
17
17
|
ref: "Workspace",
|
|
18
18
|
default: null,
|
|
19
19
|
},
|
|
20
|
-
websiteId: {
|
|
21
|
-
type: Schema.Types.ObjectId,
|
|
22
|
-
ref: "Website",
|
|
23
|
-
default: null,
|
|
24
|
-
},
|
|
25
20
|
webVariantId: { type: Number, default: null },
|
|
26
21
|
inventoryPolicy: { type: String, default: "deny" }, //deny,continue
|
|
27
22
|
taxable: { type: Boolean, default: false },
|
package/models/Tag.js
CHANGED
|
@@ -8,11 +8,6 @@ const TagSchema = new Schema(
|
|
|
8
8
|
ref: "Workspace",
|
|
9
9
|
default: null,
|
|
10
10
|
},
|
|
11
|
-
websiteId: {
|
|
12
|
-
type: Schema.Types.ObjectId,
|
|
13
|
-
ref: "Website",
|
|
14
|
-
default: null,
|
|
15
|
-
},
|
|
16
11
|
oldId: { type: String, default: "" },
|
|
17
12
|
webTagId: { type: Number, default: "" },
|
|
18
13
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
@@ -13,11 +13,6 @@ const VariantLocationSchema = new Schema(
|
|
|
13
13
|
ref: "Workspace",
|
|
14
14
|
default: null,
|
|
15
15
|
},
|
|
16
|
-
websiteId: {
|
|
17
|
-
type: Schema.Types.ObjectId,
|
|
18
|
-
ref: "Website",
|
|
19
|
-
default: null,
|
|
20
|
-
},
|
|
21
16
|
productId: { type: Schema.Types.ObjectId, ref: "Product", default: null },
|
|
22
17
|
locationId: { type: Schema.Types.ObjectId, ref: "Location", default: null },
|
|
23
18
|
webVariantLocationId: { type: Number, default: null },
|
package/models.js
CHANGED
|
@@ -104,6 +104,7 @@ const WhatsappFlow = require("./models/WhatsappFlow");
|
|
|
104
104
|
const Workflow = require("./models/Workflow");
|
|
105
105
|
const Workspace = require("./models/Workspace");
|
|
106
106
|
const Shop = require("./models/Shop");
|
|
107
|
+
const Call = require("./models/Call");
|
|
107
108
|
|
|
108
109
|
module.exports = {
|
|
109
110
|
Activity,
|
|
@@ -212,4 +213,5 @@ module.exports = {
|
|
|
212
213
|
Workflow,
|
|
213
214
|
Workspace,
|
|
214
215
|
Shop,
|
|
216
|
+
Call,
|
|
215
217
|
};
|