shuttlepro-shared 1.4.22 → 1.4.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/common/repositories/customerProfile.repository.js +7 -1
- package/common/repositories/index.js +0 -3
- package/config/socket.js +30 -249
- package/models/Attribute.js +5 -0
- package/models/Automation.js +32 -0
- package/models/Category.js +5 -0
- package/models/Checkpoint.js +5 -0
- package/models/Customer.js +5 -0
- package/models/CustomerProfile.js +3 -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/Step.js +17 -0
- package/models/Tag.js +5 -0
- package/models/VariantLocation.js +5 -0
- package/models.js +0 -2
- package/package.json +1 -1
- package/common/repositories/call.repository.js +0 -403
- package/models/Call.js +0 -192
|
@@ -39,7 +39,12 @@ const updateCustomerProfile = async (id, data) => {
|
|
|
39
39
|
}).exec();
|
|
40
40
|
return updated;
|
|
41
41
|
};
|
|
42
|
-
|
|
42
|
+
const updateManyCustomerProfiles = async (filter, data) => {
|
|
43
|
+
const result = await CustomerProfile.updateMany(filter, {
|
|
44
|
+
$set: data,
|
|
45
|
+
}).exec();
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
43
48
|
const updateCustomerProfileByFilter = async (
|
|
44
49
|
filter,
|
|
45
50
|
data,
|
|
@@ -112,4 +117,5 @@ module.exports = {
|
|
|
112
117
|
findAllCustomerProfilesByFilter,
|
|
113
118
|
findAllCustomerProfilesByFilterFromDb,
|
|
114
119
|
searchCustomerProfilesByName,
|
|
120
|
+
updateManyCustomerProfiles,
|
|
115
121
|
};
|
|
@@ -15,9 +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");
|
|
19
18
|
const interactiveRepository = require("./interactive.repository");
|
|
20
|
-
|
|
21
19
|
exports.module = {
|
|
22
20
|
workspaceRepository,
|
|
23
21
|
integrationRepository,
|
|
@@ -36,6 +34,5 @@ exports.module = {
|
|
|
36
34
|
customerProfileRepository,
|
|
37
35
|
customerTimelineRepository,
|
|
38
36
|
shopRepository,
|
|
39
|
-
callRepository,
|
|
40
37
|
interactiveRepository,
|
|
41
38
|
};
|
package/config/socket.js
CHANGED
|
@@ -4,46 +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
|
-
options = {}
|
|
18
|
+
redisChannel = "socket_events"
|
|
34
19
|
) => {
|
|
35
|
-
const {
|
|
36
|
-
customHandlers,
|
|
37
|
-
requireWorkspaceId = true,
|
|
38
|
-
cors = {
|
|
39
|
-
origin: "*",
|
|
40
|
-
methods: ["GET", "POST"],
|
|
41
|
-
},
|
|
42
|
-
} = options;
|
|
43
|
-
|
|
44
20
|
// Initialize Socket.IO server if not already done
|
|
45
21
|
if (!io) {
|
|
46
|
-
io = new Server(server, {
|
|
22
|
+
io = new Server(server, {
|
|
23
|
+
cors: {
|
|
24
|
+
origin: "*",
|
|
25
|
+
methods: ["GET", "POST"],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
47
28
|
console.log("✅ Socket.IO server initialized");
|
|
48
29
|
}
|
|
49
30
|
|
|
@@ -68,36 +49,21 @@ const initializeSocket = async (
|
|
|
68
49
|
await subscriber.subscribe(nsRedisChannel, (messageStr) => {
|
|
69
50
|
try {
|
|
70
51
|
const message = JSON.parse(messageStr);
|
|
71
|
-
const { workspaceId, event, data
|
|
52
|
+
const { workspaceId, event, data } = message;
|
|
72
53
|
|
|
73
54
|
console.log(`📥 Redis message received on ${nsRedisChannel}:`, {
|
|
74
55
|
workspaceId,
|
|
75
56
|
event,
|
|
76
|
-
target,
|
|
77
57
|
});
|
|
78
58
|
|
|
79
|
-
//
|
|
80
|
-
if (
|
|
81
|
-
// Broadcast to all clients in this namespace
|
|
82
|
-
namespace.emit(event, data);
|
|
83
|
-
console.log(
|
|
84
|
-
`📢 Event ${event} broadcasted to all clients in /${namespaceParam}`
|
|
85
|
-
);
|
|
86
|
-
} else if (target && target.startsWith("socket:")) {
|
|
87
|
-
// Target specific socket ID
|
|
88
|
-
const socketId = target.replace("socket:", "");
|
|
89
|
-
namespace.to(socketId).emit(event, data);
|
|
90
|
-
console.log(
|
|
91
|
-
`📢 Event ${event} emitted to socket ${socketId} in /${namespaceParam}`
|
|
92
|
-
);
|
|
93
|
-
} else if (workspaceId) {
|
|
94
|
-
// Emit to specific workspace room in this namespace
|
|
59
|
+
// Emit to specific workspace room in this namespace
|
|
60
|
+
if (workspaceId) {
|
|
95
61
|
namespace.to(workspaceId).emit(event, data);
|
|
96
62
|
console.log(
|
|
97
63
|
`📢 Event ${event} emitted to workspace ${workspaceId} in /${namespaceParam}`
|
|
98
64
|
);
|
|
99
65
|
} else {
|
|
100
|
-
//
|
|
66
|
+
// Broadcast to all clients in this namespace if no workspaceId specified
|
|
101
67
|
namespace.emit(event, data);
|
|
102
68
|
console.log(
|
|
103
69
|
`📢 Event ${event} broadcasted to all clients in /${namespaceParam}`
|
|
@@ -113,44 +79,20 @@ const initializeSocket = async (
|
|
|
113
79
|
|
|
114
80
|
// Handle new socket connections to this namespace
|
|
115
81
|
namespace.on("connection", (socket) => {
|
|
116
|
-
const { workspaceId
|
|
82
|
+
const { workspaceId } = socket.handshake.query;
|
|
117
83
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
console.warn(
|
|
121
|
-
`⚠️ Connection rejected: No workspaceId provided for namespace /${namespaceParam}`
|
|
122
|
-
);
|
|
84
|
+
if (!workspaceId) {
|
|
85
|
+
console.warn(`⚠️ Connection rejected: No workspaceId provided`);
|
|
123
86
|
socket.disconnect(true);
|
|
124
87
|
return;
|
|
125
88
|
}
|
|
126
89
|
|
|
127
90
|
console.log(
|
|
128
|
-
`✅ Client connected: ${socket.id} (Workspace: ${
|
|
129
|
-
workspaceId || "N/A"
|
|
130
|
-
}, CallId: ${callId || "N/A"}) in /${namespaceParam}`
|
|
91
|
+
`✅ Client connected: ${socket.id} (Workspace: ${workspaceId}) in /${namespaceParam}`
|
|
131
92
|
);
|
|
132
93
|
|
|
133
|
-
// Join workspace room
|
|
134
|
-
|
|
135
|
-
socket.join(workspaceId);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Join additional rooms based on query parameters
|
|
139
|
-
if (callId) {
|
|
140
|
-
socket.join(`call:${callId}`);
|
|
141
|
-
}
|
|
142
|
-
if (agentId) {
|
|
143
|
-
socket.join(`agent:${agentId}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Store socket metadata
|
|
147
|
-
socket.metadata = {
|
|
148
|
-
workspaceId,
|
|
149
|
-
callId,
|
|
150
|
-
agentId,
|
|
151
|
-
namespace: namespaceParam,
|
|
152
|
-
connectedAt: new Date(),
|
|
153
|
-
};
|
|
94
|
+
// Join workspace room
|
|
95
|
+
socket.join(workspaceId);
|
|
154
96
|
|
|
155
97
|
// Send connection confirmation to client
|
|
156
98
|
socket.emit("connected", {
|
|
@@ -158,70 +100,25 @@ const initializeSocket = async (
|
|
|
158
100
|
socketId: socket.id,
|
|
159
101
|
namespace: namespaceParam,
|
|
160
102
|
workspaceId,
|
|
161
|
-
callId,
|
|
162
|
-
agentId,
|
|
163
103
|
});
|
|
164
104
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
`📋 Applied custom handlers for socket ${socket.id} in /${namespaceParam}`
|
|
171
|
-
);
|
|
172
|
-
} catch (error) {
|
|
173
|
-
console.error(
|
|
174
|
-
`❌ Error applying custom handlers for socket ${socket.id}:`,
|
|
175
|
-
error
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Apply registered namespace handlers
|
|
181
|
-
if (namespaceHandlers[namespaceParam]) {
|
|
182
|
-
try {
|
|
183
|
-
namespaceHandlers[namespaceParam](socket, namespace);
|
|
184
|
-
console.log(
|
|
185
|
-
`📋 Applied registered handlers for socket ${socket.id} in /${namespaceParam}`
|
|
186
|
-
);
|
|
187
|
-
} catch (error) {
|
|
188
|
-
console.error(
|
|
189
|
-
`❌ Error applying registered handlers for socket ${socket.id}:`,
|
|
190
|
-
error
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Default event handlers for backward compatibility
|
|
196
|
-
setupDefaultHandlers(socket, namespace, namespaceParam);
|
|
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
|
+
});
|
|
197
110
|
|
|
198
111
|
// Handle disconnect
|
|
199
112
|
socket.on("disconnect", (reason) => {
|
|
200
113
|
console.log(
|
|
201
|
-
`❌ Client disconnected: ${socket.id} (Workspace: ${
|
|
202
|
-
workspaceId || "N/A"
|
|
203
|
-
}, Reason: ${reason}) from /${namespaceParam}`
|
|
114
|
+
`❌ Client disconnected: ${socket.id} (Workspace: ${workspaceId}, Reason: ${reason})`
|
|
204
115
|
);
|
|
205
|
-
|
|
206
|
-
// Leave all rooms
|
|
207
|
-
if (workspaceId) socket.leave(workspaceId);
|
|
208
|
-
if (callId) socket.leave(`call:${callId}`);
|
|
209
|
-
if (agentId) socket.leave(`agent:${agentId}`);
|
|
210
|
-
|
|
211
|
-
// Emit disconnect event to namespace for cleanup
|
|
212
|
-
namespace.emit("client_disconnected", {
|
|
213
|
-
socketId: socket.id,
|
|
214
|
-
metadata: socket.metadata,
|
|
215
|
-
reason,
|
|
216
|
-
});
|
|
116
|
+
socket.leave(workspaceId);
|
|
217
117
|
});
|
|
218
118
|
|
|
219
119
|
// Handle errors
|
|
220
120
|
socket.on("error", (error) => {
|
|
221
|
-
console.error(
|
|
222
|
-
`❌ Socket error for ${socket.id} in /${namespaceParam}:`,
|
|
223
|
-
error
|
|
224
|
-
);
|
|
121
|
+
console.error(`❌ Socket error for ${socket.id}:`, error);
|
|
225
122
|
});
|
|
226
123
|
});
|
|
227
124
|
|
|
@@ -231,64 +128,15 @@ const initializeSocket = async (
|
|
|
231
128
|
return namespace;
|
|
232
129
|
};
|
|
233
130
|
|
|
234
|
-
/**
|
|
235
|
-
* Setup default event handlers for backward compatibility
|
|
236
|
-
* @param {Socket} socket - Socket.IO socket instance
|
|
237
|
-
* @param {Namespace} namespace - Socket.IO namespace instance
|
|
238
|
-
* @param {string} namespaceParam - Namespace name
|
|
239
|
-
*/
|
|
240
|
-
const setupDefaultHandlers = (socket, namespace, namespaceParam) => {
|
|
241
|
-
// Handle custom events from clients (backward compatibility)
|
|
242
|
-
socket.on("client_event", (data) => {
|
|
243
|
-
console.log(
|
|
244
|
-
`📥 Client event from ${socket.id} in /${namespaceParam}:`,
|
|
245
|
-
data
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
// Emit to Redis for other instances to handle
|
|
249
|
-
sendEventToServer({
|
|
250
|
-
workspaceId: socket.metadata.workspaceId,
|
|
251
|
-
event: "client_event_received",
|
|
252
|
-
data: {
|
|
253
|
-
socketId: socket.id,
|
|
254
|
-
originalData: data,
|
|
255
|
-
metadata: socket.metadata,
|
|
256
|
-
},
|
|
257
|
-
namespace: namespaceParam,
|
|
258
|
-
}).catch((err) => {
|
|
259
|
-
console.error(`❌ Error publishing client_event to Redis:`, err);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Handle room join requests
|
|
264
|
-
socket.on("join_room", (roomName) => {
|
|
265
|
-
if (roomName && typeof roomName === "string") {
|
|
266
|
-
socket.join(roomName);
|
|
267
|
-
socket.emit("room_joined", { room: roomName, success: true });
|
|
268
|
-
console.log(`📥 Socket ${socket.id} joined room: ${roomName}`);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// Handle room leave requests
|
|
273
|
-
socket.on("leave_room", (roomName) => {
|
|
274
|
-
if (roomName && typeof roomName === "string") {
|
|
275
|
-
socket.leave(roomName);
|
|
276
|
-
socket.emit("room_left", { room: roomName, success: true });
|
|
277
|
-
console.log(`📤 Socket ${socket.id} left room: ${roomName}`);
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
};
|
|
281
|
-
|
|
282
131
|
/**
|
|
283
132
|
* Send event to clients via Redis pub/sub
|
|
284
133
|
* @param {object} params - Event parameters
|
|
285
|
-
* @param {string}
|
|
134
|
+
* @param {string} params.workspaceId - Target workspace ID
|
|
286
135
|
* @param {string} params.event - Event name
|
|
287
136
|
* @param {any} params.data - Event data payload
|
|
288
137
|
* @param {string} [params.namespace="conversation"] - Target namespace
|
|
289
138
|
* @param {string} [params.redisChannel="socket_events"] - Base Redis channel
|
|
290
|
-
* @
|
|
291
|
-
* @returns {Promise<boolean>}
|
|
139
|
+
* @returns {Promise<void>}
|
|
292
140
|
*/
|
|
293
141
|
const sendEventToServer = async ({
|
|
294
142
|
workspaceId,
|
|
@@ -296,7 +144,6 @@ const sendEventToServer = async ({
|
|
|
296
144
|
data,
|
|
297
145
|
namespace = "conversation",
|
|
298
146
|
redisChannel = "socket_events",
|
|
299
|
-
target,
|
|
300
147
|
}) => {
|
|
301
148
|
try {
|
|
302
149
|
// Create namespace-specific Redis channel
|
|
@@ -306,21 +153,13 @@ const sendEventToServer = async ({
|
|
|
306
153
|
await connectRedis();
|
|
307
154
|
|
|
308
155
|
// Create message payload
|
|
309
|
-
const message = JSON.stringify({
|
|
310
|
-
workspaceId,
|
|
311
|
-
event,
|
|
312
|
-
data,
|
|
313
|
-
target,
|
|
314
|
-
timestamp: new Date().toISOString(),
|
|
315
|
-
});
|
|
156
|
+
const message = JSON.stringify({ workspaceId, event, data });
|
|
316
157
|
|
|
317
158
|
// Publish to Redis
|
|
318
159
|
await publisher.publish(nsRedisChannel, message);
|
|
319
160
|
|
|
320
161
|
console.log(
|
|
321
|
-
`📢 Event published to Redis channel ${nsRedisChannel}: ${event} (Workspace: ${
|
|
322
|
-
workspaceId || "N/A"
|
|
323
|
-
}, Target: ${target || "default"})`
|
|
162
|
+
`📢 Event published to Redis channel ${nsRedisChannel}: ${event} (Workspace: ${workspaceId})`
|
|
324
163
|
);
|
|
325
164
|
|
|
326
165
|
return true;
|
|
@@ -330,62 +169,4 @@ const sendEventToServer = async ({
|
|
|
330
169
|
}
|
|
331
170
|
};
|
|
332
171
|
|
|
333
|
-
|
|
334
|
-
* Get a specific namespace instance
|
|
335
|
-
* @param {string} namespace - Namespace name
|
|
336
|
-
* @returns {SocketIO.Namespace|null} - Namespace instance or null if not found
|
|
337
|
-
*/
|
|
338
|
-
const getNamespace = (namespace) => {
|
|
339
|
-
return namespaceSockets[namespace] || null;
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Get all active namespaces
|
|
344
|
-
* @returns {Object} - Object containing all active namespaces
|
|
345
|
-
*/
|
|
346
|
-
const getAllNamespaces = () => {
|
|
347
|
-
return { ...namespaceSockets };
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Get Socket.IO server instance
|
|
352
|
-
* @returns {SocketIO.Server|null} - Server instance or null if not initialized
|
|
353
|
-
*/
|
|
354
|
-
const getSocketServer = () => {
|
|
355
|
-
return io || null;
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Emit event to specific room in a namespace
|
|
360
|
-
* @param {string} namespace - Namespace name
|
|
361
|
-
* @param {string} room - Room name
|
|
362
|
-
* @param {string} event - Event name
|
|
363
|
-
* @param {any} data - Event data
|
|
364
|
-
* @returns {boolean} - Success status
|
|
365
|
-
*/
|
|
366
|
-
const emitToRoom = (namespace, room, event, data) => {
|
|
367
|
-
try {
|
|
368
|
-
const ns = namespaceSockets[namespace];
|
|
369
|
-
if (!ns) {
|
|
370
|
-
console.warn(`❌ Namespace /${namespace} not found`);
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
ns.to(room).emit(event, data);
|
|
375
|
-
console.log(`📢 Event ${event} emitted to room ${room} in /${namespace}`);
|
|
376
|
-
return true;
|
|
377
|
-
} catch (error) {
|
|
378
|
-
console.error(`❌ Error emitting to room ${room} in /${namespace}:`, error);
|
|
379
|
-
return false;
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
module.exports = {
|
|
384
|
-
initializeSocket,
|
|
385
|
-
sendEventToServer,
|
|
386
|
-
registerNamespaceHandlers,
|
|
387
|
-
getNamespace,
|
|
388
|
-
getAllNamespaces,
|
|
389
|
-
getSocketServer,
|
|
390
|
-
emitToRoom,
|
|
391
|
-
};
|
|
172
|
+
module.exports = { initializeSocket, sendEventToServer };
|
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/Automation.js
CHANGED
|
@@ -55,6 +55,32 @@ const AutomationAction = new Schema({
|
|
|
55
55
|
templateId: descriptionJoin,
|
|
56
56
|
delay: defaultStringType,
|
|
57
57
|
},
|
|
58
|
+
ticketList: {
|
|
59
|
+
enabled: { type: Boolean, default: false },
|
|
60
|
+
templateId: descriptionJoin,
|
|
61
|
+
delay: defaultStringType,
|
|
62
|
+
ticketList: [
|
|
63
|
+
{
|
|
64
|
+
id: {
|
|
65
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
66
|
+
ref: "Column",
|
|
67
|
+
required: true,
|
|
68
|
+
},
|
|
69
|
+
title: {
|
|
70
|
+
type: String,
|
|
71
|
+
required: true,
|
|
72
|
+
},
|
|
73
|
+
name: {
|
|
74
|
+
type: String,
|
|
75
|
+
required: true,
|
|
76
|
+
},
|
|
77
|
+
type: {
|
|
78
|
+
type: String,
|
|
79
|
+
required: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
58
84
|
markAsSpam: {
|
|
59
85
|
enabled: { type: Boolean, default: false },
|
|
60
86
|
},
|
|
@@ -64,6 +90,10 @@ const AutomationAction = new Schema({
|
|
|
64
90
|
hideComment: {
|
|
65
91
|
enabled: { type: Boolean, default: false },
|
|
66
92
|
},
|
|
93
|
+
futureRestriction: {
|
|
94
|
+
enabled: { type: Boolean, default: false },
|
|
95
|
+
action: { type: String, default: "" },
|
|
96
|
+
},
|
|
67
97
|
closeChat: {
|
|
68
98
|
enabled: { type: Boolean, default: false },
|
|
69
99
|
},
|
|
@@ -169,6 +199,7 @@ const AutomationCondition = new Schema({
|
|
|
169
199
|
"shift",
|
|
170
200
|
"newCommentPost",
|
|
171
201
|
"profile",
|
|
202
|
+
"moveTicket",
|
|
172
203
|
],
|
|
173
204
|
},
|
|
174
205
|
keyValue: {
|
|
@@ -193,6 +224,7 @@ const AutomationCondition = new Schema({
|
|
|
193
224
|
"orderPublish",
|
|
194
225
|
"newCommentPost",
|
|
195
226
|
"profile",
|
|
227
|
+
"moveTicket",
|
|
196
228
|
],
|
|
197
229
|
},
|
|
198
230
|
subKeyValue: {
|
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,
|
|
@@ -24,6 +24,9 @@ const CustomerProfileSchema = new Schema(
|
|
|
24
24
|
default: null,
|
|
25
25
|
},
|
|
26
26
|
body: {},
|
|
27
|
+
blocked: { type: Boolean, default: false }, // To Block User (Current Automation case)
|
|
28
|
+
hideComments: { type: Boolean, default: false }, // For Hide Comment Check
|
|
29
|
+
deleteComments: { type: Boolean, default: false }, // For Delete Comment Check
|
|
27
30
|
},
|
|
28
31
|
{ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
|
|
29
32
|
);
|
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/Step.js
CHANGED
|
@@ -45,6 +45,23 @@ const StepSchema = new Schema(
|
|
|
45
45
|
default: null,
|
|
46
46
|
},
|
|
47
47
|
chatbotId: { type: Schema.Types.ObjectId, ref: "Chatbot", default: null },
|
|
48
|
+
complaintCreation: {
|
|
49
|
+
successTemplate: {
|
|
50
|
+
type: Schema.Types.ObjectId,
|
|
51
|
+
ref: "DescriptionTemplate",
|
|
52
|
+
default: null,
|
|
53
|
+
},
|
|
54
|
+
alreadyExistTemplate: {
|
|
55
|
+
type: Schema.Types.ObjectId,
|
|
56
|
+
ref: "DescriptionTemplate",
|
|
57
|
+
default: null,
|
|
58
|
+
},
|
|
59
|
+
columnId: {
|
|
60
|
+
type: Schema.Types.ObjectId,
|
|
61
|
+
ref: "Column",
|
|
62
|
+
default: null,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
48
65
|
},
|
|
49
66
|
{ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
|
|
50
67
|
);
|
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.js
CHANGED
|
@@ -105,7 +105,6 @@ const WhatsappFlow = require("./models/WhatsappFlow");
|
|
|
105
105
|
const Workflow = require("./models/Workflow");
|
|
106
106
|
const Workspace = require("./models/Workspace");
|
|
107
107
|
const Shop = require("./models/Shop");
|
|
108
|
-
const Call = require("./models/Call");
|
|
109
108
|
|
|
110
109
|
module.exports = {
|
|
111
110
|
Activity,
|
|
@@ -215,5 +214,4 @@ module.exports = {
|
|
|
215
214
|
Workflow,
|
|
216
215
|
Workspace,
|
|
217
216
|
Shop,
|
|
218
|
-
Call,
|
|
219
217
|
};
|
package/package.json
CHANGED
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
const Call = require("../../models/Call");
|
|
2
|
-
const { Types } = require("mongoose");
|
|
3
|
-
|
|
4
|
-
class CallRepository {
|
|
5
|
-
constructor() {}
|
|
6
|
-
|
|
7
|
-
async createCall(callData) {
|
|
8
|
-
try {
|
|
9
|
-
const historyEntry = {
|
|
10
|
-
timestamp: new Date(),
|
|
11
|
-
platformTimestamp: callData.platformTimestamp || "",
|
|
12
|
-
action: "created",
|
|
13
|
-
agentId: null,
|
|
14
|
-
direction: "User Initiated",
|
|
15
|
-
details: "Call created from webhook",
|
|
16
|
-
};
|
|
17
|
-
return await Call.create({
|
|
18
|
-
callId: callData.callId,
|
|
19
|
-
callerName: callData.callerName,
|
|
20
|
-
callerNumber: callData.callerNumber,
|
|
21
|
-
receiver: callData.receiver || "",
|
|
22
|
-
platformId: callData.platformId || null,
|
|
23
|
-
workspaceId: callData.workspaceId || null,
|
|
24
|
-
profileId: callData.profileId || null,
|
|
25
|
-
status: callData.status || "pending",
|
|
26
|
-
agentId: callData.agentId || null,
|
|
27
|
-
queuePosition: callData.queuePosition || 0,
|
|
28
|
-
whatsappSdp: callData.whatsappSdp || null,
|
|
29
|
-
metadata: callData.metadata || {},
|
|
30
|
-
tags: callData.tags || [],
|
|
31
|
-
callHistory: [historyEntry],
|
|
32
|
-
});
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error(`❌ Failed to create call ${callData.callId}:`, error);
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async getCall(callId) {
|
|
40
|
-
try {
|
|
41
|
-
return await Call.findOne({ callId }).lean().exec();
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error(`❌ Failed to end call ${callId}:`, error);
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async endCall(callId, agentId = null, updateObj = {}, callHistory = {}) {
|
|
49
|
-
try {
|
|
50
|
-
return await Call.findOneAndUpdate(
|
|
51
|
-
{ callId },
|
|
52
|
-
{
|
|
53
|
-
$set: {
|
|
54
|
-
...updateObj,
|
|
55
|
-
},
|
|
56
|
-
$push: {
|
|
57
|
-
callHistory: {
|
|
58
|
-
...callHistory,
|
|
59
|
-
timestamp: new Date(),
|
|
60
|
-
agentId,
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
{ new: true }
|
|
65
|
-
);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
console.error(`❌ Failed to end call ${callId}:`, error);
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async updateCall(callId, updateData) {
|
|
73
|
-
try {
|
|
74
|
-
const allowedFields = [
|
|
75
|
-
"status",
|
|
76
|
-
"agentId",
|
|
77
|
-
"callerName",
|
|
78
|
-
"callerNumber",
|
|
79
|
-
"tags",
|
|
80
|
-
"metadata",
|
|
81
|
-
"platformId",
|
|
82
|
-
"workspaceId",
|
|
83
|
-
"profileId",
|
|
84
|
-
"receiver",
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
const filteredUpdate = {};
|
|
88
|
-
Object.keys(updateData).forEach((key) => {
|
|
89
|
-
if (allowedFields.includes(key)) filteredUpdate[key] = updateData[key];
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const updateOps = {
|
|
93
|
-
$set: {
|
|
94
|
-
...filteredUpdate,
|
|
95
|
-
"connectionMetadata.lastHeartbeat": new Date(),
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
if (updateData.status) {
|
|
100
|
-
updateOps.$push = {
|
|
101
|
-
callHistory: {
|
|
102
|
-
timestamp: new Date(),
|
|
103
|
-
action: updateData.status,
|
|
104
|
-
agentId: updateData.agentId || null,
|
|
105
|
-
details:
|
|
106
|
-
updateData.reason || `Status changed to ${updateData.status}`,
|
|
107
|
-
direction: "Agent Initiated",
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return await Call.findOneAndUpdate({ callId }, updateOps, { new: true });
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error(`❌ Failed to update call ${callId}:`, error);
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async assignCallToAgent(callId, agentId) {
|
|
120
|
-
try {
|
|
121
|
-
return await Call.findOneAndUpdate(
|
|
122
|
-
{ callId, status: { $in: ["pending", "hold"] } },
|
|
123
|
-
{
|
|
124
|
-
$set: {
|
|
125
|
-
agentId,
|
|
126
|
-
status: "active",
|
|
127
|
-
startTime: new Date(),
|
|
128
|
-
"connectionMetadata.lastHeartbeat": new Date(),
|
|
129
|
-
},
|
|
130
|
-
$push: {
|
|
131
|
-
callHistory: {
|
|
132
|
-
timestamp: new Date(),
|
|
133
|
-
action: "answered",
|
|
134
|
-
agentId,
|
|
135
|
-
details: "Call answered by agent",
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
{ new: true }
|
|
140
|
-
);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
console.error(`❌ Failed to assign call ${callId}:`, error);
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async bulkUpdateCalls(operations) {
|
|
148
|
-
try {
|
|
149
|
-
const bulkOps = operations.map((op) => ({
|
|
150
|
-
updateOne: {
|
|
151
|
-
filter: { callId: op.callId },
|
|
152
|
-
update: {
|
|
153
|
-
$set: {
|
|
154
|
-
...op.updateData,
|
|
155
|
-
"connectionMetadata.lastHeartbeat": new Date(),
|
|
156
|
-
},
|
|
157
|
-
$push: {
|
|
158
|
-
callHistory: {
|
|
159
|
-
timestamp: new Date(),
|
|
160
|
-
action: op.action || "bulk_update",
|
|
161
|
-
agentId: op.agentId,
|
|
162
|
-
details: op.details || "Bulk operation",
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
}));
|
|
168
|
-
return await Call.bulkWrite(bulkOps);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
console.error("❌ Bulk update failed:", error);
|
|
171
|
-
return [];
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async moveCallToHoldQueue(callId, reason = "Agent disconnected") {
|
|
176
|
-
try {
|
|
177
|
-
return await Call.findOneAndUpdate(
|
|
178
|
-
{ callId },
|
|
179
|
-
{
|
|
180
|
-
$set: {
|
|
181
|
-
agentId: null,
|
|
182
|
-
status: "hold",
|
|
183
|
-
autoAccepted: true,
|
|
184
|
-
"connectionMetadata.lastHeartbeat": new Date(),
|
|
185
|
-
},
|
|
186
|
-
$push: {
|
|
187
|
-
callHistory: {
|
|
188
|
-
timestamp: new Date(),
|
|
189
|
-
action: "disconnected",
|
|
190
|
-
agentId: null,
|
|
191
|
-
details: reason,
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
{ new: true }
|
|
196
|
-
);
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.error(`❌ Failed to move call ${callId} to hold queue:`, error);
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/* ----------------- Query Helpers ----------------- */
|
|
204
|
-
|
|
205
|
-
async getCallsByWorkspace(
|
|
206
|
-
workspaceId,
|
|
207
|
-
{
|
|
208
|
-
filters = {},
|
|
209
|
-
select = null,
|
|
210
|
-
sortBy = { createdAt: -1 },
|
|
211
|
-
limit = null,
|
|
212
|
-
skip = null,
|
|
213
|
-
} = {}
|
|
214
|
-
) {
|
|
215
|
-
let query = Call.find({ workspaceId, ...filters })
|
|
216
|
-
.select(select || {}) // if null → select all fields
|
|
217
|
-
.sort(sortBy) // default newest first
|
|
218
|
-
.lean();
|
|
219
|
-
|
|
220
|
-
if (limit !== null) {
|
|
221
|
-
query = query.limit(limit);
|
|
222
|
-
}
|
|
223
|
-
if (skip !== null) {
|
|
224
|
-
query = query.skip(skip);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return query.exec();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async getCallsByAgent(
|
|
231
|
-
agentId,
|
|
232
|
-
{
|
|
233
|
-
filters = {},
|
|
234
|
-
select = null,
|
|
235
|
-
sortBy = { createdAt: -1 },
|
|
236
|
-
limit = null,
|
|
237
|
-
skip = null,
|
|
238
|
-
} = {}
|
|
239
|
-
) {
|
|
240
|
-
let query = Call.find({ agentId, ...filters })
|
|
241
|
-
.select(select || {}) // if null → select all fields
|
|
242
|
-
.sort(sortBy) // default newest first
|
|
243
|
-
.lean();
|
|
244
|
-
|
|
245
|
-
if (limit !== null) {
|
|
246
|
-
query = query.limit(limit);
|
|
247
|
-
}
|
|
248
|
-
if (skip !== null) {
|
|
249
|
-
query = query.skip(skip);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return query.exec();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async getCallsByReceiver(receiver, filters = {}) {
|
|
256
|
-
return Call.find({ receiver, ...filters })
|
|
257
|
-
.sort({ createdAt: -1 })
|
|
258
|
-
.lean()
|
|
259
|
-
.exec();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async getActiveCalls(workspaceId) {
|
|
263
|
-
return Call.find({
|
|
264
|
-
workspaceId,
|
|
265
|
-
status: { $in: ["active", "hold"] },
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async getPendingCalls(workspaceId) {
|
|
270
|
-
return Call.find({ workspaceId, status: "pending" });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async getStaleCalls(timeoutMinutes = 5) {
|
|
274
|
-
const timeout = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
|
275
|
-
return Call.find({
|
|
276
|
-
status: { $in: ["active", "hold", "pending"] },
|
|
277
|
-
"connectionMetadata.lastHeartbeat": { $lt: timeout },
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/* ----------------- Reporting & Analytics ----------------- */
|
|
282
|
-
|
|
283
|
-
// Stats grouped by agent
|
|
284
|
-
|
|
285
|
-
async getAgentStats({ from, to }, filters = {}) {
|
|
286
|
-
// Normalize to UTC day bounds
|
|
287
|
-
const f = new Date(from);
|
|
288
|
-
const t = new Date(to);
|
|
289
|
-
const fromDate = new Date(
|
|
290
|
-
Date.UTC(f.getUTCFullYear(), f.getUTCMonth(), f.getUTCDate(), 0, 0, 0, 0)
|
|
291
|
-
);
|
|
292
|
-
const toDate = new Date(
|
|
293
|
-
Date.UTC(
|
|
294
|
-
t.getUTCFullYear(),
|
|
295
|
-
t.getUTCMonth(),
|
|
296
|
-
t.getUTCDate(),
|
|
297
|
-
23,
|
|
298
|
-
59,
|
|
299
|
-
59,
|
|
300
|
-
999
|
|
301
|
-
)
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
const match = {
|
|
305
|
-
createdAt: { $gte: fromDate, $lte: toDate },
|
|
306
|
-
...filters,
|
|
307
|
-
};
|
|
308
|
-
if (match.workspaceId && typeof match.workspaceId === "string") {
|
|
309
|
-
match.workspaceId = new Types.ObjectId(match.workspaceId);
|
|
310
|
-
}
|
|
311
|
-
if (match.agentId && typeof match.agentId === "string") {
|
|
312
|
-
match.agentId = new Types.ObjectId(match.agentId);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
console.log(match, "match");
|
|
316
|
-
|
|
317
|
-
const [stats] = await Call.aggregate([
|
|
318
|
-
{ $match: match },
|
|
319
|
-
{
|
|
320
|
-
$group: {
|
|
321
|
-
_id: filters.agentId ? "$agentId" : null,
|
|
322
|
-
totalCalls: { $sum: 1 },
|
|
323
|
-
pending: { $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] } },
|
|
324
|
-
abandoned: {
|
|
325
|
-
$sum: { $cond: [{ $eq: ["$status", "abandoned"] }, 1, 0] },
|
|
326
|
-
},
|
|
327
|
-
ended: { $sum: { $cond: [{ $eq: ["$status", "ended"] }, 1, 0] } },
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
$project: { _id: 0, totalCalls: 1, pending: 1, abandoned: 1, ended: 1 },
|
|
332
|
-
},
|
|
333
|
-
]);
|
|
334
|
-
|
|
335
|
-
// Always return a single object
|
|
336
|
-
return stats || { totalCalls: 0, pending: 0, abandoned: 0, ended: 0 };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Daily call summary
|
|
340
|
-
async getDailySummary(workspaceId, from, to, filters = {}, groupBy = null) {
|
|
341
|
-
const matchStage = {
|
|
342
|
-
workspaceId,
|
|
343
|
-
createdAt: { $gte: from, $lte: to },
|
|
344
|
-
...filters,
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
const groupId = {
|
|
348
|
-
day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// If admin wants to group by agentId / receiver / platformId
|
|
352
|
-
if (groupBy && ["agentId", "receiver", "platformId"].includes(groupBy)) {
|
|
353
|
-
groupId[groupBy] = `$${groupBy}`;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return Call.aggregate([
|
|
357
|
-
{ $match: matchStage },
|
|
358
|
-
{
|
|
359
|
-
$group: {
|
|
360
|
-
_id: groupId,
|
|
361
|
-
total: { $sum: 1 },
|
|
362
|
-
ended: { $sum: { $cond: [{ $eq: ["$status", "ended"] }, 1, 0] } },
|
|
363
|
-
abandoned: {
|
|
364
|
-
$sum: { $cond: [{ $eq: ["$status", "abandoned"] }, 1, 0] },
|
|
365
|
-
},
|
|
366
|
-
avgDuration: { $avg: "$duration" },
|
|
367
|
-
minDuration: { $min: "$duration" },
|
|
368
|
-
maxDuration: { $max: "$duration" },
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
$project: {
|
|
373
|
-
_id: 0,
|
|
374
|
-
day: "$_id.day",
|
|
375
|
-
groupBy: groupBy ? `$_id.${groupBy}` : null,
|
|
376
|
-
total: 1,
|
|
377
|
-
ended: 1,
|
|
378
|
-
abandoned: 1,
|
|
379
|
-
avgDuration: { $round: ["$avgDuration", 2] }, // round to 2 decimals
|
|
380
|
-
minDuration: 1,
|
|
381
|
-
maxDuration: 1,
|
|
382
|
-
},
|
|
383
|
-
},
|
|
384
|
-
{ $sort: { day: 1 } },
|
|
385
|
-
]);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Queue stats
|
|
389
|
-
async getQueueStats(workspaceId) {
|
|
390
|
-
return Call.aggregate([
|
|
391
|
-
{ $match: { workspaceId, status: "hold" } },
|
|
392
|
-
{
|
|
393
|
-
$group: {
|
|
394
|
-
_id: "$receiver",
|
|
395
|
-
count: { $sum: 1 },
|
|
396
|
-
avgQueuePosition: { $avg: "$queuePosition" },
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
]);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
module.exports = new CallRepository();
|
package/models/Call.js
DELETED
|
@@ -1,192 +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
|
-
details: String, // "Status changed to hold"
|
|
87
|
-
startTime: String, // platform start time
|
|
88
|
-
endTime: String, // platform endTime time
|
|
89
|
-
duration: Number, // platform call duration
|
|
90
|
-
timestamp: {
|
|
91
|
-
// system timestamp
|
|
92
|
-
type: Date,
|
|
93
|
-
default: Date.now,
|
|
94
|
-
},
|
|
95
|
-
platformTimestamp: String, // platformtimestamp
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
metadata: {},
|
|
99
|
-
tags: [],
|
|
100
|
-
connectionMetadata: {
|
|
101
|
-
browserConnectionState: {
|
|
102
|
-
type: String,
|
|
103
|
-
default: "new",
|
|
104
|
-
},
|
|
105
|
-
whatsappConnectionState: {
|
|
106
|
-
type: String,
|
|
107
|
-
default: "new",
|
|
108
|
-
},
|
|
109
|
-
lastHeartbeat: {
|
|
110
|
-
type: Date,
|
|
111
|
-
default: Date.now,
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
rating: {
|
|
115
|
-
type: Number,
|
|
116
|
-
default: 0,
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
timestamps: true,
|
|
121
|
-
collection: "calls",
|
|
122
|
-
}
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
// Indexes for performance
|
|
126
|
-
callSchema.index({ status: 1, createdAt: 1 });
|
|
127
|
-
callSchema.index({ agentId: 1, status: 1 });
|
|
128
|
-
callSchema.index({ callerNumber: 1, createdAt: -1 });
|
|
129
|
-
callSchema.index({ isOnHold: 1, status: 1 });
|
|
130
|
-
|
|
131
|
-
// Virtual for call duration calculation
|
|
132
|
-
callSchema.virtual("currentDuration").get(function () {
|
|
133
|
-
if (this.startTime) {
|
|
134
|
-
const endTime = this.endTime || new Date();
|
|
135
|
-
return Math.floor((endTime - this.startTime) / 1000);
|
|
136
|
-
}
|
|
137
|
-
return 0;
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Methods
|
|
141
|
-
callSchema.methods.addToHistory = function (
|
|
142
|
-
action,
|
|
143
|
-
agentId = null,
|
|
144
|
-
details = null
|
|
145
|
-
) {
|
|
146
|
-
this.callHistory.push({
|
|
147
|
-
action,
|
|
148
|
-
agentId,
|
|
149
|
-
details,
|
|
150
|
-
timestamp: new Date(),
|
|
151
|
-
});
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
callSchema.methods.updateHeartbeat = function () {
|
|
155
|
-
this.connectionMetadata.lastHeartbeat = new Date();
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
callSchema.methods.isStale = function (timeoutMinutes = 5) {
|
|
159
|
-
const timeout = timeoutMinutes * 60 * 1000; // Convert to milliseconds
|
|
160
|
-
return new Date() - this.connectionMetadata.lastHeartbeat > timeout;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// Static methods
|
|
164
|
-
callSchema.statics.getActiveCallsForAgent = function (agentId) {
|
|
165
|
-
return this.find({
|
|
166
|
-
agentId,
|
|
167
|
-
status: { $in: ["active", "hold"] },
|
|
168
|
-
}).sort({ createdAt: 1 });
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
callSchema.statics.getHoldQueue = function () {
|
|
172
|
-
return this.find({
|
|
173
|
-
status: "hold",
|
|
174
|
-
agentId: null,
|
|
175
|
-
}).sort({ createdAt: 1 });
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
callSchema.statics.getPendingCalls = function () {
|
|
179
|
-
return this.find({
|
|
180
|
-
status: "pending",
|
|
181
|
-
}).sort({ createdAt: 1 });
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
callSchema.statics.getStaleConnections = function (timeoutMinutes = 5) {
|
|
185
|
-
const timeout = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
|
186
|
-
return this.find({
|
|
187
|
-
status: { $in: ["active", "hold", "pending"] },
|
|
188
|
-
"connectionMetadata.lastHeartbeat": { $lt: timeout },
|
|
189
|
-
});
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
module.exports = mongoose.model("Call", callSchema);
|