shuttlepro-shared 1.4.36 → 1.4.38
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 +432 -0
- package/common/repositories/index.js +2 -0
- package/config/socket.js +250 -30
- package/models/Attribute.js +5 -0
- package/models/Call.js +193 -0
- package/models/Category.js +5 -0
- package/models/Checkpoint.js +5 -0
- package/models/Customer.js +5 -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/Workspace.js +3 -0
- package/models.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
const Call = require("../../models/Call");
|
|
2
|
+
const { Types } = require("mongoose");
|
|
3
|
+
|
|
4
|
+
class CallRepository {
|
|
5
|
+
/* ----------------- Internal Helpers ----------------- */
|
|
6
|
+
|
|
7
|
+
_utcDayBounds(from, to) {
|
|
8
|
+
const f = new Date(from);
|
|
9
|
+
const t = new Date(to);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
from: new Date(
|
|
13
|
+
Date.UTC(
|
|
14
|
+
f.getUTCFullYear(),
|
|
15
|
+
f.getUTCMonth(),
|
|
16
|
+
f.getUTCDate(),
|
|
17
|
+
0,
|
|
18
|
+
0,
|
|
19
|
+
0,
|
|
20
|
+
0
|
|
21
|
+
)
|
|
22
|
+
),
|
|
23
|
+
to: new Date(
|
|
24
|
+
Date.UTC(
|
|
25
|
+
t.getUTCFullYear(),
|
|
26
|
+
t.getUTCMonth(),
|
|
27
|
+
t.getUTCDate(),
|
|
28
|
+
23,
|
|
29
|
+
59,
|
|
30
|
+
59,
|
|
31
|
+
999
|
|
32
|
+
)
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_filterAllowedFields(data, allowed) {
|
|
38
|
+
return Object.keys(data).reduce((acc, key) => {
|
|
39
|
+
if (allowed.includes(key)) acc[key] = data[key];
|
|
40
|
+
return acc;
|
|
41
|
+
}, {});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_historyEntry({
|
|
45
|
+
action,
|
|
46
|
+
agentId = null,
|
|
47
|
+
details = "",
|
|
48
|
+
direction = "user",
|
|
49
|
+
platformTimestamp = "",
|
|
50
|
+
actionBy = null,
|
|
51
|
+
}) {
|
|
52
|
+
return {
|
|
53
|
+
timestamp: new Date(),
|
|
54
|
+
platformTimestamp,
|
|
55
|
+
action,
|
|
56
|
+
agentId,
|
|
57
|
+
details,
|
|
58
|
+
direction,
|
|
59
|
+
actionBy,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _safeExec(promise, errorMsg, fallback = null) {
|
|
64
|
+
try {
|
|
65
|
+
return await promise;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`❌ ${errorMsg}`, error);
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ----------------- Core CRUD ----------------- */
|
|
73
|
+
|
|
74
|
+
createCall(callData) {
|
|
75
|
+
const entry = this._historyEntry({
|
|
76
|
+
action: "created",
|
|
77
|
+
direction: callData?.direction || "user",
|
|
78
|
+
details: callData?.details || "Incomming call from whatsapp user",
|
|
79
|
+
platformTimestamp: callData.platformTimestamp || "",
|
|
80
|
+
agentId: callData.agentId || null,
|
|
81
|
+
actionBy: callData.userId || null,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return this._safeExec(
|
|
85
|
+
Call.create({
|
|
86
|
+
callId: callData.callId,
|
|
87
|
+
callerName: callData.callerName,
|
|
88
|
+
callerNumber: callData.callerNumber,
|
|
89
|
+
receiver: callData.receiver || "",
|
|
90
|
+
platformId: callData.platformId || null,
|
|
91
|
+
workspaceId: callData.workspaceId || null,
|
|
92
|
+
profileId: callData.profileId || null,
|
|
93
|
+
status: callData.status || "pending",
|
|
94
|
+
agentId: callData.agentId || null,
|
|
95
|
+
queuePosition: callData.queuePosition || 0,
|
|
96
|
+
whatsappSdp: callData.whatsappSdp || null,
|
|
97
|
+
metadata: callData.metadata || {},
|
|
98
|
+
tags: callData.tags || [],
|
|
99
|
+
callHistory: [entry],
|
|
100
|
+
}),
|
|
101
|
+
`Failed to create call ${callData.callId}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getCall(callId) {
|
|
106
|
+
return this._safeExec(
|
|
107
|
+
Call.findOne({ callId }).lean().exec(),
|
|
108
|
+
`Failed to get call ${callId}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
endCall(callId, updateObj = {}, history = {}) {
|
|
113
|
+
return this._safeExec(
|
|
114
|
+
Call.findOneAndUpdate(
|
|
115
|
+
{ callId },
|
|
116
|
+
{ $set: { ...updateObj }, $push: { callHistory: history } },
|
|
117
|
+
{ new: true }
|
|
118
|
+
),
|
|
119
|
+
`Failed to end call ${callId}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
updateCall(callId, updateData, history = {}) {
|
|
124
|
+
const updateOps = {
|
|
125
|
+
$set: { ...updateData },
|
|
126
|
+
};
|
|
127
|
+
updateOps.$push = {
|
|
128
|
+
callHistory: history,
|
|
129
|
+
};
|
|
130
|
+
return this._safeExec(
|
|
131
|
+
Call.findOneAndUpdate({ callId }, updateOps, { new: true }),
|
|
132
|
+
`Failed to update call ${callId}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
assignCallToAgent(callId, agentId) {
|
|
137
|
+
const entry = this._historyEntry({
|
|
138
|
+
action: "answered",
|
|
139
|
+
agentId,
|
|
140
|
+
details: "Call answered by agent",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return this._safeExec(
|
|
144
|
+
Call.findOneAndUpdate(
|
|
145
|
+
{ callId, status: { $in: ["pending", "hold"] } },
|
|
146
|
+
{
|
|
147
|
+
$set: {
|
|
148
|
+
agentId,
|
|
149
|
+
status: "active",
|
|
150
|
+
startTime: new Date(),
|
|
151
|
+
"connectionMetadata.lastHeartbeat": new Date(),
|
|
152
|
+
},
|
|
153
|
+
$push: { callHistory: entry },
|
|
154
|
+
},
|
|
155
|
+
{ new: true }
|
|
156
|
+
),
|
|
157
|
+
`Failed to assign call ${callId}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
bulkUpdateCalls(operations) {
|
|
162
|
+
const bulkOps = operations.map((op) => ({
|
|
163
|
+
updateOne: {
|
|
164
|
+
filter: { callId: op.callId },
|
|
165
|
+
update: {
|
|
166
|
+
$set: {
|
|
167
|
+
...op.updateData,
|
|
168
|
+
"connectionMetadata.lastHeartbeat": new Date(),
|
|
169
|
+
},
|
|
170
|
+
$push: {
|
|
171
|
+
callHistory: this._historyEntry({
|
|
172
|
+
action: op.action || "bulk_update",
|
|
173
|
+
agentId: op.agentId,
|
|
174
|
+
details: op.details || "Bulk operation",
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
return this._safeExec(Call.bulkWrite(bulkOps), "Bulk update failed", []);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
moveCallToHoldQueue(callId, reason = "Agent disconnected") {
|
|
185
|
+
const entry = this._historyEntry({
|
|
186
|
+
action: "disconnected",
|
|
187
|
+
details: reason,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return this._safeExec(
|
|
191
|
+
Call.findOneAndUpdate(
|
|
192
|
+
{ callId },
|
|
193
|
+
{
|
|
194
|
+
$set: {
|
|
195
|
+
agentId: null,
|
|
196
|
+
status: "hold",
|
|
197
|
+
autoAccepted: true,
|
|
198
|
+
"connectionMetadata.lastHeartbeat": new Date(),
|
|
199
|
+
},
|
|
200
|
+
$push: { callHistory: entry },
|
|
201
|
+
},
|
|
202
|
+
{ new: true }
|
|
203
|
+
),
|
|
204
|
+
`Failed to move call ${callId} to hold queue`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ----------------- Queries ----------------- */
|
|
209
|
+
|
|
210
|
+
async getCallsByWorkspace(
|
|
211
|
+
{ workspaceId, from, to },
|
|
212
|
+
{
|
|
213
|
+
filters = {},
|
|
214
|
+
select = null,
|
|
215
|
+
sortBy = { createdAt: -1 },
|
|
216
|
+
limit,
|
|
217
|
+
skip,
|
|
218
|
+
} = {}
|
|
219
|
+
) {
|
|
220
|
+
const { from: fromDate, to: toDate } = this._utcDayBounds(from, to);
|
|
221
|
+
|
|
222
|
+
console.log(
|
|
223
|
+
{
|
|
224
|
+
workspaceId,
|
|
225
|
+
createdAt: { $gte: fromDate, $lte: toDate },
|
|
226
|
+
...filters,
|
|
227
|
+
},
|
|
228
|
+
"query"
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
let query = Call.find({
|
|
232
|
+
workspaceId,
|
|
233
|
+
createdAt: { $gte: fromDate, $lte: toDate },
|
|
234
|
+
...filters,
|
|
235
|
+
})
|
|
236
|
+
.select(select || {})
|
|
237
|
+
.sort(sortBy)
|
|
238
|
+
.lean();
|
|
239
|
+
|
|
240
|
+
if (limit) query = query.limit(limit);
|
|
241
|
+
if (skip) query = query.skip(skip);
|
|
242
|
+
|
|
243
|
+
return query.exec();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async addCallHistory(callId, historyData = {}) {
|
|
247
|
+
const entry = historyData;
|
|
248
|
+
return this._safeExec(
|
|
249
|
+
Call.findOneAndUpdate(
|
|
250
|
+
{ callId },
|
|
251
|
+
{ $push: { callHistory: entry } },
|
|
252
|
+
{ new: true }
|
|
253
|
+
),
|
|
254
|
+
`Failed to add call history for ${callId}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getCallsByAgent(
|
|
259
|
+
agentId,
|
|
260
|
+
{
|
|
261
|
+
filters = {},
|
|
262
|
+
select = null,
|
|
263
|
+
sortBy = { createdAt: -1 },
|
|
264
|
+
limit,
|
|
265
|
+
skip,
|
|
266
|
+
} = {}
|
|
267
|
+
) {
|
|
268
|
+
let query = Call.find({ agentId, ...filters })
|
|
269
|
+
.select(select || {})
|
|
270
|
+
.sort(sortBy)
|
|
271
|
+
.lean();
|
|
272
|
+
if (limit) query = query.limit(limit);
|
|
273
|
+
if (skip) query = query.skip(skip);
|
|
274
|
+
return query.exec();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getCallsByReceiver(receiver, filters = {}) {
|
|
278
|
+
return Call.find({ receiver, ...filters })
|
|
279
|
+
.sort({ createdAt: -1 })
|
|
280
|
+
.lean()
|
|
281
|
+
.exec();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getActiveCalls(workspaceId, filters = {}) {
|
|
285
|
+
return Call.find({
|
|
286
|
+
workspaceId,
|
|
287
|
+
status: { $in: ["active"] },
|
|
288
|
+
...filters,
|
|
289
|
+
})
|
|
290
|
+
.lean()
|
|
291
|
+
.exec();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getPendingCalls(workspaceId) {
|
|
295
|
+
return Call.find({
|
|
296
|
+
workspaceId,
|
|
297
|
+
status: { $in: ["pending", "hold"] },
|
|
298
|
+
...filters,
|
|
299
|
+
})
|
|
300
|
+
.lean()
|
|
301
|
+
.exec();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getCallByFilter(filters = {}) {
|
|
305
|
+
return Call.findOne(filters).lean().exec();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getStaleCalls(timeoutMinutes = 5) {
|
|
309
|
+
const timeout = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
|
310
|
+
return Call.find({
|
|
311
|
+
status: { $in: ["active", "hold", "pending"] },
|
|
312
|
+
"connectionMetadata.lastHeartbeat": { $lt: timeout },
|
|
313
|
+
})
|
|
314
|
+
.lean()
|
|
315
|
+
.exec();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ----------------- Reporting ----------------- */
|
|
319
|
+
|
|
320
|
+
async getAgentStats({ from, to }, filters = {}) {
|
|
321
|
+
const { from: fromDate, to: toDate } = this._utcDayBounds(from, to);
|
|
322
|
+
|
|
323
|
+
const match = { createdAt: { $gte: fromDate, $lte: toDate }, ...filters };
|
|
324
|
+
if (match.workspaceId && typeof match.workspaceId === "string")
|
|
325
|
+
match.workspaceId = new Types.ObjectId(match.workspaceId);
|
|
326
|
+
if (match.agentId && typeof match.agentId === "string")
|
|
327
|
+
match.agentId = match.agentId;
|
|
328
|
+
|
|
329
|
+
const [stats] = await Call.aggregate([
|
|
330
|
+
{ $match: match },
|
|
331
|
+
{
|
|
332
|
+
$group: {
|
|
333
|
+
_id: filters.agentId ? "$agentId" : null,
|
|
334
|
+
totalCalls: { $sum: 1 },
|
|
335
|
+
|
|
336
|
+
pending: {
|
|
337
|
+
$sum: {
|
|
338
|
+
$cond: [{ $in: ["$status", ["pending", "hold"]] }, 1, 0],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
active: {
|
|
343
|
+
$sum: {
|
|
344
|
+
$cond: [{ $eq: ["$status", "active"] }, 1, 0],
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
abandoned: {
|
|
349
|
+
$sum: { $cond: [{ $eq: ["$status", "abandoned"] }, 1, 0] },
|
|
350
|
+
},
|
|
351
|
+
ended: {
|
|
352
|
+
$sum: { $cond: [{ $eq: ["$status", "ended"] }, 1, 0] },
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
$project: {
|
|
358
|
+
_id: 0,
|
|
359
|
+
totalCalls: 1,
|
|
360
|
+
pending: 1,
|
|
361
|
+
active: 1,
|
|
362
|
+
abandoned: 1,
|
|
363
|
+
ended: 1,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
stats || { totalCalls: 0, pending: 0, active: 0, abandoned: 0, ended: 0 }
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
getDailySummary(workspaceId, from, to, filters = {}, groupBy = null) {
|
|
374
|
+
const matchStage = {
|
|
375
|
+
workspaceId,
|
|
376
|
+
createdAt: { $gte: from, $lte: to },
|
|
377
|
+
...filters,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const groupId = {
|
|
381
|
+
day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
|
|
382
|
+
};
|
|
383
|
+
if (groupBy && ["agentId", "receiver", "platformId"].includes(groupBy))
|
|
384
|
+
groupId[groupBy] = `$${groupBy}`;
|
|
385
|
+
|
|
386
|
+
return Call.aggregate([
|
|
387
|
+
{ $match: matchStage },
|
|
388
|
+
{
|
|
389
|
+
$group: {
|
|
390
|
+
_id: groupId,
|
|
391
|
+
total: { $sum: 1 },
|
|
392
|
+
ended: { $sum: { $cond: [{ $eq: ["$status", "ended"] }, 1, 0] } },
|
|
393
|
+
abandoned: {
|
|
394
|
+
$sum: { $cond: [{ $eq: ["$status", "abandoned"] }, 1, 0] },
|
|
395
|
+
},
|
|
396
|
+
avgDuration: { $avg: "$duration" },
|
|
397
|
+
minDuration: { $min: "$duration" },
|
|
398
|
+
maxDuration: { $max: "$duration" },
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
$project: {
|
|
403
|
+
_id: 0,
|
|
404
|
+
day: "$_id.day",
|
|
405
|
+
groupBy: groupBy ? `$_id.${groupBy}` : null,
|
|
406
|
+
total: 1,
|
|
407
|
+
ended: 1,
|
|
408
|
+
abandoned: 1,
|
|
409
|
+
avgDuration: { $round: ["$avgDuration", 2] },
|
|
410
|
+
minDuration: 1,
|
|
411
|
+
maxDuration: 1,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{ $sort: { day: 1 } },
|
|
415
|
+
]);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
getQueueStats(workspaceId) {
|
|
419
|
+
return Call.aggregate([
|
|
420
|
+
{ $match: { workspaceId, status: "hold" } },
|
|
421
|
+
{
|
|
422
|
+
$group: {
|
|
423
|
+
_id: "$receiver",
|
|
424
|
+
count: { $sum: 1 },
|
|
425
|
+
avgQueuePosition: { $avg: "$queuePosition" },
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
]);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
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
|
const interactiveRepository = require("./interactive.repository");
|
|
19
20
|
const workflowRunRepository = require("./workflowRun.repository");
|
|
20
21
|
exports.module = {
|
|
@@ -35,6 +36,7 @@ exports.module = {
|
|
|
35
36
|
customerProfileRepository,
|
|
36
37
|
customerTimelineRepository,
|
|
37
38
|
shopRepository,
|
|
39
|
+
callRepository,
|
|
38
40
|
interactiveRepository,
|
|
39
41
|
workflowRunRepository,
|
|
40
42
|
};
|
package/config/socket.js
CHANGED
|
@@ -4,27 +4,47 @@ 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
|
+
path = "/whatsapp-calling",
|
|
34
|
+
options = {}
|
|
19
35
|
) => {
|
|
36
|
+
const {
|
|
37
|
+
customHandlers,
|
|
38
|
+
requireWorkspaceId = true,
|
|
39
|
+
cors = {
|
|
40
|
+
origin: "*",
|
|
41
|
+
methods: ["GET", "POST"],
|
|
42
|
+
},
|
|
43
|
+
} = options;
|
|
44
|
+
|
|
20
45
|
// Initialize Socket.IO server if not already done
|
|
21
46
|
if (!io) {
|
|
22
|
-
io = new Server(server, {
|
|
23
|
-
cors: {
|
|
24
|
-
origin: "*",
|
|
25
|
-
methods: ["GET", "POST"],
|
|
26
|
-
},
|
|
27
|
-
});
|
|
47
|
+
io = new Server(server, { path, cors });
|
|
28
48
|
console.log("✅ Socket.IO server initialized");
|
|
29
49
|
}
|
|
30
50
|
|
|
@@ -49,21 +69,36 @@ const initializeSocket = async (
|
|
|
49
69
|
await subscriber.subscribe(nsRedisChannel, (messageStr) => {
|
|
50
70
|
try {
|
|
51
71
|
const message = JSON.parse(messageStr);
|
|
52
|
-
const { workspaceId, event, data } = message;
|
|
72
|
+
const { workspaceId, event, data, target } = message;
|
|
53
73
|
|
|
54
74
|
console.log(`📥 Redis message received on ${nsRedisChannel}:`, {
|
|
55
75
|
workspaceId,
|
|
56
76
|
event,
|
|
77
|
+
target,
|
|
57
78
|
});
|
|
58
79
|
|
|
59
|
-
//
|
|
60
|
-
if (
|
|
80
|
+
// Handle different targeting options
|
|
81
|
+
if (target === "broadcast") {
|
|
82
|
+
// Broadcast to all clients in this namespace
|
|
83
|
+
namespace.emit(event, data);
|
|
84
|
+
console.log(
|
|
85
|
+
`📢 Event ${event} broadcasted to all clients in /${namespaceParam}`
|
|
86
|
+
);
|
|
87
|
+
} else if (target && target.startsWith("socket:")) {
|
|
88
|
+
// Target specific socket ID
|
|
89
|
+
const socketId = target.replace("socket:", "");
|
|
90
|
+
namespace.to(socketId).emit(event, data);
|
|
91
|
+
console.log(
|
|
92
|
+
`📢 Event ${event} emitted to socket ${socketId} in /${namespaceParam}`
|
|
93
|
+
);
|
|
94
|
+
} else if (workspaceId) {
|
|
95
|
+
// Emit to specific workspace room in this namespace
|
|
61
96
|
namespace.to(workspaceId).emit(event, data);
|
|
62
97
|
console.log(
|
|
63
98
|
`📢 Event ${event} emitted to workspace ${workspaceId} in /${namespaceParam}`
|
|
64
99
|
);
|
|
65
100
|
} else {
|
|
66
|
-
//
|
|
101
|
+
// Default: broadcast to all clients in this namespace
|
|
67
102
|
namespace.emit(event, data);
|
|
68
103
|
console.log(
|
|
69
104
|
`📢 Event ${event} broadcasted to all clients in /${namespaceParam}`
|
|
@@ -79,20 +114,44 @@ const initializeSocket = async (
|
|
|
79
114
|
|
|
80
115
|
// Handle new socket connections to this namespace
|
|
81
116
|
namespace.on("connection", (socket) => {
|
|
82
|
-
const { workspaceId } = socket.handshake.query;
|
|
117
|
+
const { workspaceId, callId, agentId } = socket.handshake.query;
|
|
83
118
|
|
|
84
|
-
|
|
85
|
-
|
|
119
|
+
// Workspace ID validation (can be disabled for certain namespaces)
|
|
120
|
+
if (requireWorkspaceId && !workspaceId) {
|
|
121
|
+
console.warn(
|
|
122
|
+
`⚠️ Connection rejected: No workspaceId provided for namespace /${namespaceParam}`
|
|
123
|
+
);
|
|
86
124
|
socket.disconnect(true);
|
|
87
125
|
return;
|
|
88
126
|
}
|
|
89
127
|
|
|
90
128
|
console.log(
|
|
91
|
-
`✅ Client connected: ${socket.id} (Workspace: ${
|
|
129
|
+
`✅ Client connected: ${socket.id} (Workspace: ${
|
|
130
|
+
workspaceId || "N/A"
|
|
131
|
+
}, CallId: ${callId || "N/A"}) in /${namespaceParam}`
|
|
92
132
|
);
|
|
93
133
|
|
|
94
|
-
// Join workspace room
|
|
95
|
-
|
|
134
|
+
// Join workspace room if workspaceId is provided
|
|
135
|
+
if (workspaceId) {
|
|
136
|
+
socket.join(workspaceId);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Join additional rooms based on query parameters
|
|
140
|
+
if (callId) {
|
|
141
|
+
socket.join(`call:${callId}`);
|
|
142
|
+
}
|
|
143
|
+
if (agentId) {
|
|
144
|
+
socket.join(`agent:${agentId}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Store socket metadata
|
|
148
|
+
socket.metadata = {
|
|
149
|
+
workspaceId,
|
|
150
|
+
callId,
|
|
151
|
+
agentId,
|
|
152
|
+
namespace: namespaceParam,
|
|
153
|
+
connectedAt: new Date(),
|
|
154
|
+
};
|
|
96
155
|
|
|
97
156
|
// Send connection confirmation to client
|
|
98
157
|
socket.emit("connected", {
|
|
@@ -100,25 +159,70 @@ const initializeSocket = async (
|
|
|
100
159
|
socketId: socket.id,
|
|
101
160
|
namespace: namespaceParam,
|
|
102
161
|
workspaceId,
|
|
162
|
+
callId,
|
|
163
|
+
agentId,
|
|
103
164
|
});
|
|
104
165
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
// Apply custom handlers if provided
|
|
167
|
+
if (customHandlers && typeof customHandlers === "function") {
|
|
168
|
+
try {
|
|
169
|
+
customHandlers(socket, namespace);
|
|
170
|
+
console.log(
|
|
171
|
+
`📋 Applied custom handlers for socket ${socket.id} in /${namespaceParam}`
|
|
172
|
+
);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(
|
|
175
|
+
`❌ Error applying custom handlers for socket ${socket.id}:`,
|
|
176
|
+
error
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Apply registered namespace handlers
|
|
182
|
+
if (namespaceHandlers[namespaceParam]) {
|
|
183
|
+
try {
|
|
184
|
+
namespaceHandlers[namespaceParam](socket, namespace);
|
|
185
|
+
console.log(
|
|
186
|
+
`📋 Applied registered handlers for socket ${socket.id} in /${namespaceParam}`
|
|
187
|
+
);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(
|
|
190
|
+
`❌ Error applying registered handlers for socket ${socket.id}:`,
|
|
191
|
+
error
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Default event handlers for backward compatibility
|
|
197
|
+
setupDefaultHandlers(socket, namespace, namespaceParam);
|
|
110
198
|
|
|
111
199
|
// Handle disconnect
|
|
112
200
|
socket.on("disconnect", (reason) => {
|
|
113
201
|
console.log(
|
|
114
|
-
`❌ Client disconnected: ${socket.id} (Workspace: ${
|
|
202
|
+
`❌ Client disconnected: ${socket.id} (Workspace: ${
|
|
203
|
+
workspaceId || "N/A"
|
|
204
|
+
}, Reason: ${reason}) from /${namespaceParam}`
|
|
115
205
|
);
|
|
116
|
-
|
|
206
|
+
|
|
207
|
+
// Leave all rooms
|
|
208
|
+
if (workspaceId) socket.leave(workspaceId);
|
|
209
|
+
if (callId) socket.leave(`call:${callId}`);
|
|
210
|
+
if (agentId) socket.leave(`agent:${agentId}`);
|
|
211
|
+
|
|
212
|
+
// Emit disconnect event to namespace for cleanup
|
|
213
|
+
namespace.emit("client_disconnected", {
|
|
214
|
+
socketId: socket.id,
|
|
215
|
+
metadata: socket.metadata,
|
|
216
|
+
reason,
|
|
217
|
+
});
|
|
117
218
|
});
|
|
118
219
|
|
|
119
220
|
// Handle errors
|
|
120
221
|
socket.on("error", (error) => {
|
|
121
|
-
console.error(
|
|
222
|
+
console.error(
|
|
223
|
+
`❌ Socket error for ${socket.id} in /${namespaceParam}:`,
|
|
224
|
+
error
|
|
225
|
+
);
|
|
122
226
|
});
|
|
123
227
|
});
|
|
124
228
|
|
|
@@ -128,15 +232,64 @@ const initializeSocket = async (
|
|
|
128
232
|
return namespace;
|
|
129
233
|
};
|
|
130
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Setup default event handlers for backward compatibility
|
|
237
|
+
* @param {Socket} socket - Socket.IO socket instance
|
|
238
|
+
* @param {Namespace} namespace - Socket.IO namespace instance
|
|
239
|
+
* @param {string} namespaceParam - Namespace name
|
|
240
|
+
*/
|
|
241
|
+
const setupDefaultHandlers = (socket, namespace, namespaceParam) => {
|
|
242
|
+
// Handle custom events from clients (backward compatibility)
|
|
243
|
+
socket.on("client_event", (data) => {
|
|
244
|
+
console.log(
|
|
245
|
+
`📥 Client event from ${socket.id} in /${namespaceParam}:`,
|
|
246
|
+
data
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Emit to Redis for other instances to handle
|
|
250
|
+
sendEventToServer({
|
|
251
|
+
workspaceId: socket.metadata.workspaceId,
|
|
252
|
+
event: "client_event_received",
|
|
253
|
+
data: {
|
|
254
|
+
socketId: socket.id,
|
|
255
|
+
originalData: data,
|
|
256
|
+
metadata: socket.metadata,
|
|
257
|
+
},
|
|
258
|
+
namespace: namespaceParam,
|
|
259
|
+
}).catch((err) => {
|
|
260
|
+
console.error(`❌ Error publishing client_event to Redis:`, err);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Handle room join requests
|
|
265
|
+
socket.on("join_room", (roomName) => {
|
|
266
|
+
if (roomName && typeof roomName === "string") {
|
|
267
|
+
socket.join(roomName);
|
|
268
|
+
socket.emit("room_joined", { room: roomName, success: true });
|
|
269
|
+
console.log(`📥 Socket ${socket.id} joined room: ${roomName}`);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Handle room leave requests
|
|
274
|
+
socket.on("leave_room", (roomName) => {
|
|
275
|
+
if (roomName && typeof roomName === "string") {
|
|
276
|
+
socket.leave(roomName);
|
|
277
|
+
socket.emit("room_left", { room: roomName, success: true });
|
|
278
|
+
console.log(`📤 Socket ${socket.id} left room: ${roomName}`);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
|
|
131
283
|
/**
|
|
132
284
|
* Send event to clients via Redis pub/sub
|
|
133
285
|
* @param {object} params - Event parameters
|
|
134
|
-
* @param {string} params.workspaceId - Target workspace ID
|
|
286
|
+
* @param {string} [params.workspaceId] - Target workspace ID
|
|
135
287
|
* @param {string} params.event - Event name
|
|
136
288
|
* @param {any} params.data - Event data payload
|
|
137
289
|
* @param {string} [params.namespace="conversation"] - Target namespace
|
|
138
290
|
* @param {string} [params.redisChannel="socket_events"] - Base Redis channel
|
|
139
|
-
* @
|
|
291
|
+
* @param {string} [params.target] - Specific target (broadcast, socket:socketId, etc.)
|
|
292
|
+
* @returns {Promise<boolean>}
|
|
140
293
|
*/
|
|
141
294
|
const sendEventToServer = async ({
|
|
142
295
|
workspaceId,
|
|
@@ -144,6 +297,7 @@ const sendEventToServer = async ({
|
|
|
144
297
|
data,
|
|
145
298
|
namespace = "conversation",
|
|
146
299
|
redisChannel = "socket_events",
|
|
300
|
+
target,
|
|
147
301
|
}) => {
|
|
148
302
|
try {
|
|
149
303
|
// Create namespace-specific Redis channel
|
|
@@ -153,13 +307,21 @@ const sendEventToServer = async ({
|
|
|
153
307
|
await connectRedis();
|
|
154
308
|
|
|
155
309
|
// Create message payload
|
|
156
|
-
const message = JSON.stringify({
|
|
310
|
+
const message = JSON.stringify({
|
|
311
|
+
workspaceId,
|
|
312
|
+
event,
|
|
313
|
+
data,
|
|
314
|
+
target,
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
});
|
|
157
317
|
|
|
158
318
|
// Publish to Redis
|
|
159
319
|
await publisher.publish(nsRedisChannel, message);
|
|
160
320
|
|
|
161
321
|
console.log(
|
|
162
|
-
`📢 Event published to Redis channel ${nsRedisChannel}: ${event} (Workspace: ${
|
|
322
|
+
`📢 Event published to Redis channel ${nsRedisChannel}: ${event} (Workspace: ${
|
|
323
|
+
workspaceId || "N/A"
|
|
324
|
+
}, Target: ${target || "default"})`
|
|
163
325
|
);
|
|
164
326
|
|
|
165
327
|
return true;
|
|
@@ -169,4 +331,62 @@ const sendEventToServer = async ({
|
|
|
169
331
|
}
|
|
170
332
|
};
|
|
171
333
|
|
|
172
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Get a specific namespace instance
|
|
336
|
+
* @param {string} namespace - Namespace name
|
|
337
|
+
* @returns {SocketIO.Namespace|null} - Namespace instance or null if not found
|
|
338
|
+
*/
|
|
339
|
+
const getNamespace = (namespace) => {
|
|
340
|
+
return namespaceSockets[namespace] || null;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get all active namespaces
|
|
345
|
+
* @returns {Object} - Object containing all active namespaces
|
|
346
|
+
*/
|
|
347
|
+
const getAllNamespaces = () => {
|
|
348
|
+
return { ...namespaceSockets };
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get Socket.IO server instance
|
|
353
|
+
* @returns {SocketIO.Server|null} - Server instance or null if not initialized
|
|
354
|
+
*/
|
|
355
|
+
const getSocketServer = () => {
|
|
356
|
+
return io || null;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Emit event to specific room in a namespace
|
|
361
|
+
* @param {string} namespace - Namespace name
|
|
362
|
+
* @param {string} room - Room name
|
|
363
|
+
* @param {string} event - Event name
|
|
364
|
+
* @param {any} data - Event data
|
|
365
|
+
* @returns {boolean} - Success status
|
|
366
|
+
*/
|
|
367
|
+
const emitToRoom = (namespace, room, event, data) => {
|
|
368
|
+
try {
|
|
369
|
+
const ns = namespaceSockets[namespace];
|
|
370
|
+
if (!ns) {
|
|
371
|
+
console.warn(`❌ Namespace /${namespace} not found`);
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
ns.to(room).emit(event, data);
|
|
376
|
+
console.log(`📢 Event ${event} emitted to room ${room} in /${namespace}`);
|
|
377
|
+
return true;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.error(`❌ Error emitting to room ${room} in /${namespace}:`, error);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
module.exports = {
|
|
385
|
+
initializeSocket,
|
|
386
|
+
sendEventToServer,
|
|
387
|
+
registerNamespaceHandlers,
|
|
388
|
+
getNamespace,
|
|
389
|
+
getAllNamespaces,
|
|
390
|
+
getSocketServer,
|
|
391
|
+
emitToRoom,
|
|
392
|
+
};
|
package/models/Attribute.js
CHANGED
|
@@ -10,6 +10,11 @@ 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
|
+
},
|
|
13
18
|
oldId: { type: String, default: "" },
|
|
14
19
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
15
20
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
package/models/Call.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
const { Schema } = mongoose;
|
|
3
|
+
|
|
4
|
+
const callSchema = new mongoose.Schema(
|
|
5
|
+
{
|
|
6
|
+
workspaceId: { type: Schema.Types.ObjectId, ref: "Workspace" },
|
|
7
|
+
callId: {
|
|
8
|
+
type: String,
|
|
9
|
+
required: true,
|
|
10
|
+
unique: true,
|
|
11
|
+
index: true,
|
|
12
|
+
},
|
|
13
|
+
callerName: {
|
|
14
|
+
type: String,
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
callerNumber: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: true,
|
|
20
|
+
index: true,
|
|
21
|
+
},
|
|
22
|
+
profileId: { type: Schema.Types.ObjectId, ref: "Profile" },
|
|
23
|
+
receiver: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: "",
|
|
26
|
+
},
|
|
27
|
+
platformId: {
|
|
28
|
+
type: Schema.Types.ObjectId,
|
|
29
|
+
ref: "Integration",
|
|
30
|
+
default: null,
|
|
31
|
+
},
|
|
32
|
+
type: { type: String, default: "inbound" },
|
|
33
|
+
status: {
|
|
34
|
+
type: String,
|
|
35
|
+
enum: ["pending", "hold", "active", "ended", "disconnected", "abandoned"],
|
|
36
|
+
default: "pending",
|
|
37
|
+
index: true,
|
|
38
|
+
},
|
|
39
|
+
queuePosition: {
|
|
40
|
+
type: Number,
|
|
41
|
+
default: 0,
|
|
42
|
+
},
|
|
43
|
+
agentId: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: null,
|
|
46
|
+
index: true,
|
|
47
|
+
},
|
|
48
|
+
startTime: {
|
|
49
|
+
type: Date,
|
|
50
|
+
default: null,
|
|
51
|
+
},
|
|
52
|
+
endTime: {
|
|
53
|
+
type: Date,
|
|
54
|
+
default: null,
|
|
55
|
+
},
|
|
56
|
+
duration: {
|
|
57
|
+
type: Number,
|
|
58
|
+
default: 0,
|
|
59
|
+
},
|
|
60
|
+
whatsappSdp: {
|
|
61
|
+
type: String,
|
|
62
|
+
default: null,
|
|
63
|
+
},
|
|
64
|
+
browserSdp: {
|
|
65
|
+
type: String,
|
|
66
|
+
default: null,
|
|
67
|
+
},
|
|
68
|
+
callHistory: [
|
|
69
|
+
{
|
|
70
|
+
action: {
|
|
71
|
+
type: String,
|
|
72
|
+
enum: [
|
|
73
|
+
"created",
|
|
74
|
+
"answered",
|
|
75
|
+
"held",
|
|
76
|
+
"unheld",
|
|
77
|
+
"transferred",
|
|
78
|
+
"ended",
|
|
79
|
+
"disconnected",
|
|
80
|
+
"reconnected",
|
|
81
|
+
"abandoned",
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
direction: String, // USER INITIATED // AGENT INITIATED
|
|
85
|
+
agentId: String, // agent id
|
|
86
|
+
actionBy: String,
|
|
87
|
+
details: String, // "Status changed to hold"
|
|
88
|
+
startTime: String, // platform start time
|
|
89
|
+
endTime: String, // platform endTime time
|
|
90
|
+
duration: Number, // platform call duration
|
|
91
|
+
timestamp: {
|
|
92
|
+
// system timestamp
|
|
93
|
+
type: Date,
|
|
94
|
+
default: Date.now,
|
|
95
|
+
},
|
|
96
|
+
platformTimestamp: String, // platformtimestamp
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
metadata: {},
|
|
100
|
+
tags: [],
|
|
101
|
+
connectionMetadata: {
|
|
102
|
+
browserConnectionState: {
|
|
103
|
+
type: String,
|
|
104
|
+
default: "new",
|
|
105
|
+
},
|
|
106
|
+
whatsappConnectionState: {
|
|
107
|
+
type: String,
|
|
108
|
+
default: "new",
|
|
109
|
+
},
|
|
110
|
+
lastHeartbeat: {
|
|
111
|
+
type: Date,
|
|
112
|
+
default: Date.now,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
rating: {
|
|
116
|
+
type: Number,
|
|
117
|
+
default: 0,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
timestamps: true,
|
|
122
|
+
collection: "calls",
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Indexes for performance
|
|
127
|
+
callSchema.index({ status: 1, createdAt: 1 });
|
|
128
|
+
callSchema.index({ agentId: 1, status: 1 });
|
|
129
|
+
callSchema.index({ callerNumber: 1, createdAt: -1 });
|
|
130
|
+
callSchema.index({ isOnHold: 1, status: 1 });
|
|
131
|
+
|
|
132
|
+
// Virtual for call duration calculation
|
|
133
|
+
callSchema.virtual("currentDuration").get(function () {
|
|
134
|
+
if (this.startTime) {
|
|
135
|
+
const endTime = this.endTime || new Date();
|
|
136
|
+
return Math.floor((endTime - this.startTime) / 1000);
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Methods
|
|
142
|
+
callSchema.methods.addToHistory = function (
|
|
143
|
+
action,
|
|
144
|
+
agentId = null,
|
|
145
|
+
details = null
|
|
146
|
+
) {
|
|
147
|
+
this.callHistory.push({
|
|
148
|
+
action,
|
|
149
|
+
agentId,
|
|
150
|
+
details,
|
|
151
|
+
timestamp: new Date(),
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
callSchema.methods.updateHeartbeat = function () {
|
|
156
|
+
this.connectionMetadata.lastHeartbeat = new Date();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
callSchema.methods.isStale = function (timeoutMinutes = 5) {
|
|
160
|
+
const timeout = timeoutMinutes * 60 * 1000; // Convert to milliseconds
|
|
161
|
+
return new Date() - this.connectionMetadata.lastHeartbeat > timeout;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Static methods
|
|
165
|
+
callSchema.statics.getActiveCallsForAgent = function (agentId) {
|
|
166
|
+
return this.find({
|
|
167
|
+
agentId,
|
|
168
|
+
status: { $in: ["active", "hold"] },
|
|
169
|
+
}).sort({ createdAt: 1 });
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
callSchema.statics.getHoldQueue = function () {
|
|
173
|
+
return this.find({
|
|
174
|
+
status: "hold",
|
|
175
|
+
agentId: null,
|
|
176
|
+
}).sort({ createdAt: 1 });
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
callSchema.statics.getPendingCalls = function () {
|
|
180
|
+
return this.find({
|
|
181
|
+
status: "pending",
|
|
182
|
+
}).sort({ createdAt: 1 });
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
callSchema.statics.getStaleConnections = function (timeoutMinutes = 5) {
|
|
186
|
+
const timeout = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
|
187
|
+
return this.find({
|
|
188
|
+
status: { $in: ["active", "hold", "pending"] },
|
|
189
|
+
"connectionMetadata.lastHeartbeat": { $lt: timeout },
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
module.exports = mongoose.model("Call", callSchema);
|
package/models/Category.js
CHANGED
|
@@ -11,6 +11,11 @@ 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
|
+
},
|
|
14
19
|
oldId: { type: String, default: "" },
|
|
15
20
|
webCategoryId: { type: Number, default: null },
|
|
16
21
|
parentId: { type: Schema.Types.ObjectId, ref: "Category", default: null },
|
package/models/Checkpoint.js
CHANGED
|
@@ -14,6 +14,11 @@ 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
|
+
},
|
|
17
22
|
},
|
|
18
23
|
{ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
|
|
19
24
|
);
|
package/models/Customer.js
CHANGED
|
@@ -18,6 +18,11 @@ 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
|
+
},
|
|
21
26
|
isBlackListed: { type: Boolean, default: false },
|
|
22
27
|
createdBy: {
|
|
23
28
|
type: Schema.Types.ObjectId,
|
package/models/Location.js
CHANGED
|
@@ -14,6 +14,11 @@ 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
|
+
},
|
|
17
22
|
workspaceId: {
|
|
18
23
|
type: Schema.Types.ObjectId,
|
|
19
24
|
ref: "Workspace",
|
package/models/Order.js
CHANGED
|
@@ -103,6 +103,11 @@ 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
|
+
},
|
|
106
111
|
orderType: { type: String, default: "" },
|
|
107
112
|
barCode: { type: String, default: "" },
|
|
108
113
|
quantity: { type: Number, default: 0 },
|
|
@@ -223,6 +228,7 @@ const markOrUnMarkOrderAsDuplicate = async (doc) => {
|
|
|
223
228
|
const phoneRegex = getPhoneRegex(doc?.customerPhone);
|
|
224
229
|
let orders = await Order.find({
|
|
225
230
|
workspaceId: doc?.workspaceId,
|
|
231
|
+
websiteId: doc?.websiteId,
|
|
226
232
|
isDeleted: false,
|
|
227
233
|
statusType: "pending",
|
|
228
234
|
customerPhone: phoneRegex,
|
package/models/OrderPdf.js
CHANGED
|
@@ -20,6 +20,11 @@ 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
|
+
},
|
|
23
28
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
24
29
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
25
30
|
},
|
package/models/OrderProduct.js
CHANGED
|
@@ -31,6 +31,11 @@ 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
|
+
},
|
|
34
39
|
},
|
|
35
40
|
{ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
|
|
36
41
|
);
|
package/models/Product.js
CHANGED
|
@@ -31,6 +31,11 @@ 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,
|
|
34
39
|
},
|
|
35
40
|
storeType: { type: String, default: "LOCAL" },
|
|
36
41
|
isDeleted: { type: Boolean, default: false },
|
|
@@ -22,6 +22,11 @@ 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
|
+
},
|
|
25
30
|
height: { type: Number, default: 0 },
|
|
26
31
|
variantIds: [
|
|
27
32
|
{ type: Schema.Types.ObjectId, ref: "ProductVariant", default: null },
|
|
@@ -16,6 +16,11 @@ 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
|
+
},
|
|
19
24
|
isDeleted: { type: Boolean, default: false },
|
|
20
25
|
position: { type: String, default: "1" },
|
|
21
26
|
},
|
|
@@ -11,6 +11,11 @@ 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
|
+
},
|
|
14
19
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
15
20
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
16
21
|
isDeleted: { type: Boolean, default: false },
|
package/models/ProductTag.js
CHANGED
|
@@ -11,6 +11,11 @@ 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
|
+
},
|
|
14
19
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
15
20
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
16
21
|
isDeleted: { type: Boolean, default: false },
|
package/models/ProductVariant.js
CHANGED
|
@@ -17,6 +17,11 @@ 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
|
+
},
|
|
20
25
|
webVariantId: { type: Number, default: null },
|
|
21
26
|
inventoryPolicy: { type: String, default: "deny" }, //deny,continue
|
|
22
27
|
taxable: { type: Boolean, default: false },
|
package/models/Tag.js
CHANGED
|
@@ -8,6 +8,11 @@ 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
|
+
},
|
|
11
16
|
oldId: { type: String, default: "" },
|
|
12
17
|
webTagId: { type: Number, default: "" },
|
|
13
18
|
createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
@@ -13,6 +13,11 @@ 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
|
+
},
|
|
16
21
|
productId: { type: Schema.Types.ObjectId, ref: "Product", default: null },
|
|
17
22
|
locationId: { type: Schema.Types.ObjectId, ref: "Location", default: null },
|
|
18
23
|
webVariantLocationId: { type: Number, default: null },
|
package/models/Workspace.js
CHANGED
|
@@ -347,6 +347,9 @@ const workspaceSchema = new mongoose.Schema(
|
|
|
347
347
|
type: String,
|
|
348
348
|
default: "",
|
|
349
349
|
},
|
|
350
|
+
genericKey: { type: Boolean, default: false },
|
|
351
|
+
tokens: { type: Number, default: 0 },
|
|
352
|
+
limit: { type: Number, default: 0 },
|
|
350
353
|
},
|
|
351
354
|
},
|
|
352
355
|
{ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
|
package/models.js
CHANGED
|
@@ -106,6 +106,7 @@ const WorkflowRun = require("./models/WorkflowRun");
|
|
|
106
106
|
const Workflow = require("./models/Workflow");
|
|
107
107
|
const Workspace = require("./models/Workspace");
|
|
108
108
|
const Shop = require("./models/Shop");
|
|
109
|
+
const Call = require("./models/Call");
|
|
109
110
|
|
|
110
111
|
module.exports = {
|
|
111
112
|
Activity,
|
|
@@ -216,4 +217,5 @@ module.exports = {
|
|
|
216
217
|
Workspace,
|
|
217
218
|
WorkflowRun,
|
|
218
219
|
Shop,
|
|
220
|
+
Call,
|
|
219
221
|
};
|