shuttlepro-shared 1.4.35 → 1.4.36
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/index.js +0 -2
- package/common/repositories/workflowRun.repository.js +25 -0
- package/config/socket.js +30 -250
- package/models.js +0 -2
- package/package.json +1 -1
- package/common/repositories/call.repository.js +0 -432
- package/models/Call.js +0 -193
|
@@ -15,7 +15,6 @@ 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");
|
|
19
18
|
const interactiveRepository = require("./interactive.repository");
|
|
20
19
|
const workflowRunRepository = require("./workflowRun.repository");
|
|
21
20
|
exports.module = {
|
|
@@ -36,7 +35,6 @@ exports.module = {
|
|
|
36
35
|
customerProfileRepository,
|
|
37
36
|
customerTimelineRepository,
|
|
38
37
|
shopRepository,
|
|
39
|
-
callRepository,
|
|
40
38
|
interactiveRepository,
|
|
41
39
|
workflowRunRepository,
|
|
42
40
|
};
|
|
@@ -14,6 +14,31 @@ class WorkflowRunRepository {
|
|
|
14
14
|
return await WorkflowRun.find(filter).sort({ createdAt: -1 });
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
async findPaginated(filter = {}, page = 1, limit = 100, options = {}) {
|
|
18
|
+
const skip = Math.max(0, (Number(page) - 1) * Number(limit));
|
|
19
|
+
const query = WorkflowRun.find(filter)
|
|
20
|
+
.sort({ createdAt: -1 })
|
|
21
|
+
.skip(skip)
|
|
22
|
+
.limit(Number(limit))
|
|
23
|
+
// include essential fields from relations for UI
|
|
24
|
+
.populate({
|
|
25
|
+
path: "workflowId",
|
|
26
|
+
select: options.workflowSelect || "name business",
|
|
27
|
+
})
|
|
28
|
+
.populate({
|
|
29
|
+
path: "initialMessageId",
|
|
30
|
+
select: options.templateSelect || "name subject type",
|
|
31
|
+
})
|
|
32
|
+
.lean();
|
|
33
|
+
|
|
34
|
+
const [data, total] = await Promise.all([
|
|
35
|
+
query.exec(),
|
|
36
|
+
WorkflowRun.countDocuments(filter),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
return { data, total };
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
async update(id, updates) {
|
|
18
43
|
return await WorkflowRun.findByIdAndUpdate(id, updates, { new: true });
|
|
19
44
|
}
|
package/config/socket.js
CHANGED
|
@@ -4,47 +4,27 @@ 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
|
-
};
|
|
18
7
|
|
|
19
8
|
/**
|
|
20
9
|
* Initialize Socket.IO server with namespaces
|
|
21
10
|
* @param {http.Server} server - HTTP server instance
|
|
22
11
|
* @param {string} namespaceParam - Socket namespace name (default: "conversation")
|
|
23
12
|
* @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)
|
|
27
13
|
* @returns {SocketIO.Namespace} - The initialized namespace
|
|
28
14
|
*/
|
|
29
15
|
const initializeSocket = async (
|
|
30
16
|
server,
|
|
31
17
|
namespaceParam = "conversation",
|
|
32
|
-
redisChannel = "socket_events"
|
|
33
|
-
path = "/whatsapp-calling",
|
|
34
|
-
options = {}
|
|
18
|
+
redisChannel = "socket_events"
|
|
35
19
|
) => {
|
|
36
|
-
const {
|
|
37
|
-
customHandlers,
|
|
38
|
-
requireWorkspaceId = true,
|
|
39
|
-
cors = {
|
|
40
|
-
origin: "*",
|
|
41
|
-
methods: ["GET", "POST"],
|
|
42
|
-
},
|
|
43
|
-
} = options;
|
|
44
|
-
|
|
45
20
|
// Initialize Socket.IO server if not already done
|
|
46
21
|
if (!io) {
|
|
47
|
-
io = new Server(server, {
|
|
22
|
+
io = new Server(server, {
|
|
23
|
+
cors: {
|
|
24
|
+
origin: "*",
|
|
25
|
+
methods: ["GET", "POST"],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
48
28
|
console.log("✅ Socket.IO server initialized");
|
|
49
29
|
}
|
|
50
30
|
|
|
@@ -69,36 +49,21 @@ const initializeSocket = async (
|
|
|
69
49
|
await subscriber.subscribe(nsRedisChannel, (messageStr) => {
|
|
70
50
|
try {
|
|
71
51
|
const message = JSON.parse(messageStr);
|
|
72
|
-
const { workspaceId, event, data
|
|
52
|
+
const { workspaceId, event, data } = message;
|
|
73
53
|
|
|
74
54
|
console.log(`📥 Redis message received on ${nsRedisChannel}:`, {
|
|
75
55
|
workspaceId,
|
|
76
56
|
event,
|
|
77
|
-
target,
|
|
78
57
|
});
|
|
79
58
|
|
|
80
|
-
//
|
|
81
|
-
if (
|
|
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
|
|
59
|
+
// Emit to specific workspace room in this namespace
|
|
60
|
+
if (workspaceId) {
|
|
96
61
|
namespace.to(workspaceId).emit(event, data);
|
|
97
62
|
console.log(
|
|
98
63
|
`📢 Event ${event} emitted to workspace ${workspaceId} in /${namespaceParam}`
|
|
99
64
|
);
|
|
100
65
|
} else {
|
|
101
|
-
//
|
|
66
|
+
// Broadcast to all clients in this namespace if no workspaceId specified
|
|
102
67
|
namespace.emit(event, data);
|
|
103
68
|
console.log(
|
|
104
69
|
`📢 Event ${event} broadcasted to all clients in /${namespaceParam}`
|
|
@@ -114,44 +79,20 @@ const initializeSocket = async (
|
|
|
114
79
|
|
|
115
80
|
// Handle new socket connections to this namespace
|
|
116
81
|
namespace.on("connection", (socket) => {
|
|
117
|
-
const { workspaceId
|
|
82
|
+
const { workspaceId } = socket.handshake.query;
|
|
118
83
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
console.warn(
|
|
122
|
-
`⚠️ Connection rejected: No workspaceId provided for namespace /${namespaceParam}`
|
|
123
|
-
);
|
|
84
|
+
if (!workspaceId) {
|
|
85
|
+
console.warn(`⚠️ Connection rejected: No workspaceId provided`);
|
|
124
86
|
socket.disconnect(true);
|
|
125
87
|
return;
|
|
126
88
|
}
|
|
127
89
|
|
|
128
90
|
console.log(
|
|
129
|
-
`✅ Client connected: ${socket.id} (Workspace: ${
|
|
130
|
-
workspaceId || "N/A"
|
|
131
|
-
}, CallId: ${callId || "N/A"}) in /${namespaceParam}`
|
|
91
|
+
`✅ Client connected: ${socket.id} (Workspace: ${workspaceId}) in /${namespaceParam}`
|
|
132
92
|
);
|
|
133
93
|
|
|
134
|
-
// Join workspace room
|
|
135
|
-
|
|
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
|
-
};
|
|
94
|
+
// Join workspace room
|
|
95
|
+
socket.join(workspaceId);
|
|
155
96
|
|
|
156
97
|
// Send connection confirmation to client
|
|
157
98
|
socket.emit("connected", {
|
|
@@ -159,70 +100,25 @@ const initializeSocket = async (
|
|
|
159
100
|
socketId: socket.id,
|
|
160
101
|
namespace: namespaceParam,
|
|
161
102
|
workspaceId,
|
|
162
|
-
callId,
|
|
163
|
-
agentId,
|
|
164
103
|
});
|
|
165
104
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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);
|
|
105
|
+
// Handle custom events from clients
|
|
106
|
+
socket.on("client_event", (data) => {
|
|
107
|
+
console.log(`📥 Client event from ${socket.id}:`, data);
|
|
108
|
+
// You can process client events here
|
|
109
|
+
});
|
|
198
110
|
|
|
199
111
|
// Handle disconnect
|
|
200
112
|
socket.on("disconnect", (reason) => {
|
|
201
113
|
console.log(
|
|
202
|
-
`❌ Client disconnected: ${socket.id} (Workspace: ${
|
|
203
|
-
workspaceId || "N/A"
|
|
204
|
-
}, Reason: ${reason}) from /${namespaceParam}`
|
|
114
|
+
`❌ Client disconnected: ${socket.id} (Workspace: ${workspaceId}, Reason: ${reason})`
|
|
205
115
|
);
|
|
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
|
-
});
|
|
116
|
+
socket.leave(workspaceId);
|
|
218
117
|
});
|
|
219
118
|
|
|
220
119
|
// Handle errors
|
|
221
120
|
socket.on("error", (error) => {
|
|
222
|
-
console.error(
|
|
223
|
-
`❌ Socket error for ${socket.id} in /${namespaceParam}:`,
|
|
224
|
-
error
|
|
225
|
-
);
|
|
121
|
+
console.error(`❌ Socket error for ${socket.id}:`, error);
|
|
226
122
|
});
|
|
227
123
|
});
|
|
228
124
|
|
|
@@ -232,64 +128,15 @@ const initializeSocket = async (
|
|
|
232
128
|
return namespace;
|
|
233
129
|
};
|
|
234
130
|
|
|
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
|
-
|
|
283
131
|
/**
|
|
284
132
|
* Send event to clients via Redis pub/sub
|
|
285
133
|
* @param {object} params - Event parameters
|
|
286
|
-
* @param {string}
|
|
134
|
+
* @param {string} params.workspaceId - Target workspace ID
|
|
287
135
|
* @param {string} params.event - Event name
|
|
288
136
|
* @param {any} params.data - Event data payload
|
|
289
137
|
* @param {string} [params.namespace="conversation"] - Target namespace
|
|
290
138
|
* @param {string} [params.redisChannel="socket_events"] - Base Redis channel
|
|
291
|
-
* @
|
|
292
|
-
* @returns {Promise<boolean>}
|
|
139
|
+
* @returns {Promise<void>}
|
|
293
140
|
*/
|
|
294
141
|
const sendEventToServer = async ({
|
|
295
142
|
workspaceId,
|
|
@@ -297,7 +144,6 @@ const sendEventToServer = async ({
|
|
|
297
144
|
data,
|
|
298
145
|
namespace = "conversation",
|
|
299
146
|
redisChannel = "socket_events",
|
|
300
|
-
target,
|
|
301
147
|
}) => {
|
|
302
148
|
try {
|
|
303
149
|
// Create namespace-specific Redis channel
|
|
@@ -307,21 +153,13 @@ const sendEventToServer = async ({
|
|
|
307
153
|
await connectRedis();
|
|
308
154
|
|
|
309
155
|
// Create message payload
|
|
310
|
-
const message = JSON.stringify({
|
|
311
|
-
workspaceId,
|
|
312
|
-
event,
|
|
313
|
-
data,
|
|
314
|
-
target,
|
|
315
|
-
timestamp: new Date().toISOString(),
|
|
316
|
-
});
|
|
156
|
+
const message = JSON.stringify({ workspaceId, event, data });
|
|
317
157
|
|
|
318
158
|
// Publish to Redis
|
|
319
159
|
await publisher.publish(nsRedisChannel, message);
|
|
320
160
|
|
|
321
161
|
console.log(
|
|
322
|
-
`📢 Event published to Redis channel ${nsRedisChannel}: ${event} (Workspace: ${
|
|
323
|
-
workspaceId || "N/A"
|
|
324
|
-
}, Target: ${target || "default"})`
|
|
162
|
+
`📢 Event published to Redis channel ${nsRedisChannel}: ${event} (Workspace: ${workspaceId})`
|
|
325
163
|
);
|
|
326
164
|
|
|
327
165
|
return true;
|
|
@@ -331,62 +169,4 @@ const sendEventToServer = async ({
|
|
|
331
169
|
}
|
|
332
170
|
};
|
|
333
171
|
|
|
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
|
-
};
|
|
172
|
+
module.exports = { initializeSocket, sendEventToServer };
|
package/models.js
CHANGED
|
@@ -106,7 +106,6 @@ 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");
|
|
110
109
|
|
|
111
110
|
module.exports = {
|
|
112
111
|
Activity,
|
|
@@ -217,5 +216,4 @@ module.exports = {
|
|
|
217
216
|
Workspace,
|
|
218
217
|
WorkflowRun,
|
|
219
218
|
Shop,
|
|
220
|
-
Call,
|
|
221
219
|
};
|
package/package.json
CHANGED
|
@@ -1,432 +0,0 @@
|
|
|
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();
|
package/models/Call.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
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);
|