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.
@@ -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, { path, cors });
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, target } = message;
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
- // 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
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
- // Default: broadcast to all clients in this namespace
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, callId, agentId } = socket.handshake.query;
82
+ const { workspaceId } = socket.handshake.query;
118
83
 
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
- );
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 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
- };
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
- // 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);
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} [params.workspaceId] - Target workspace ID
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
- * @param {string} [params.target] - Specific target (broadcast, socket:socketId, etc.)
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,6 +1,6 @@
1
1
  {
2
2
  "name": "shuttlepro-shared",
3
- "version": "1.4.35",
3
+ "version": "1.4.36",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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);