shuttlepro-shared 1.3.97 → 1.3.98

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
 
20
19
  exports.module = {
21
20
  workspaceRepository,
@@ -35,5 +34,4 @@ exports.module = {
35
34
  customerProfileRepository,
36
35
  customerTimelineRepository,
37
36
  shopRepository,
38
- callRepository,
39
37
  };
@@ -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 },
@@ -61,6 +61,9 @@ const AutomationAction = new Schema({
61
61
  deleteComment: {
62
62
  enabled: { type: Boolean, default: false },
63
63
  },
64
+ hideComment: {
65
+ enabled: { type: Boolean, default: false },
66
+ },
64
67
  closeChat: {
65
68
  enabled: { type: Boolean, default: false },
66
69
  },
@@ -96,6 +99,11 @@ const AutomationAction = new Schema({
96
99
  default: null,
97
100
  },
98
101
  members: [userJoin],
102
+ templateFormId: {
103
+ type: mongoose.Schema.Types.ObjectId,
104
+ ref: "FormTemplate",
105
+ default: null,
106
+ },
99
107
  },
100
108
  escalation: {
101
109
  enabled: { type: Boolean, default: false },
@@ -160,6 +168,7 @@ const AutomationCondition = new Schema({
160
168
  "post",
161
169
  "shift",
162
170
  "newCommentPost",
171
+ "profile",
163
172
  ],
164
173
  },
165
174
  keyValue: {
@@ -183,6 +192,7 @@ const AutomationCondition = new Schema({
183
192
  "orderConfirmation",
184
193
  "orderPublish",
185
194
  "newCommentPost",
195
+ "profile",
186
196
  ],
187
197
  },
188
198
  subKeyValue: {
package/models/Card.js CHANGED
@@ -156,6 +156,11 @@ const cardSchema = new mongoose.Schema(
156
156
  type: mongoose.Schema.Types.Mixed,
157
157
  default: {},
158
158
  },
159
+ templateFormId: {
160
+ type: mongoose.Schema.Types.ObjectId,
161
+ ref: "FormTemplate",
162
+ default: null,
163
+ },
159
164
  },
160
165
  { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
161
166
  );
@@ -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 },
@@ -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
  );
@@ -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,
@@ -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,
@@ -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
  },
@@ -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 },
@@ -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 },
@@ -17,6 +17,11 @@ const ProductVariantSchema = new Schema(
17
17
  ref: "Workspace",
18
18
  default: null,
19
19
  },
20
+ websiteId: {
21
+ type: Schema.Types.ObjectId,
22
+ ref: "Website",
23
+ default: null,
24
+ },
20
25
  webVariantId: { type: Number, default: null },
21
26
  inventoryPolicy: { type: String, default: "deny" }, //deny,continue
22
27
  taxable: { type: Boolean, default: false },
package/models/Tag.js CHANGED
@@ -8,6 +8,11 @@ const TagSchema = new Schema(
8
8
  ref: "Workspace",
9
9
  default: null,
10
10
  },
11
+ websiteId: {
12
+ type: Schema.Types.ObjectId,
13
+ ref: "Website",
14
+ default: null,
15
+ },
11
16
  oldId: { type: String, default: "" },
12
17
  webTagId: { type: Number, default: "" },
13
18
  createdBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
@@ -13,6 +13,11 @@ const VariantLocationSchema = new Schema(
13
13
  ref: "Workspace",
14
14
  default: null,
15
15
  },
16
+ websiteId: {
17
+ type: Schema.Types.ObjectId,
18
+ ref: "Website",
19
+ default: null,
20
+ },
16
21
  productId: { type: Schema.Types.ObjectId, ref: "Product", default: null },
17
22
  locationId: { type: Schema.Types.ObjectId, ref: "Location", default: null },
18
23
  webVariantLocationId: { type: Number, default: null },
package/models.js CHANGED
@@ -104,7 +104,6 @@ const WhatsappFlow = require("./models/WhatsappFlow");
104
104
  const Workflow = require("./models/Workflow");
105
105
  const Workspace = require("./models/Workspace");
106
106
  const Shop = require("./models/Shop");
107
- const Call = require("./models/Call");
108
107
 
109
108
  module.exports = {
110
109
  Activity,
@@ -213,5 +212,4 @@ module.exports = {
213
212
  Workflow,
214
213
  Workspace,
215
214
  Shop,
216
- Call,
217
215
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shuttlepro-shared",
3
- "version": "1.3.97",
3
+ "version": "1.3.98",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,540 +0,0 @@
1
- const Call = require("../../models/Call");
2
- const mongoose = require("mongoose");
3
-
4
- class CallRepository {
5
- constructor() {
6
- this.pipelines = {
7
- callStats: this._buildCallStatsPipeline(),
8
- holdQueue: this._buildHoldQueuePipeline(),
9
- };
10
- }
11
-
12
- // Create a new call with transaction support
13
- async createCall(callData) {
14
- const session = await mongoose.startSession();
15
-
16
- try {
17
- return await session.withTransaction(async () => {
18
- const call = new Call({
19
- callId: callData.callId,
20
- callerName: callData.callerName,
21
- callerNumber: callData.callerNumber,
22
- whatsappSdp: callData.whatsappSdp,
23
- status: callData.status || "pending",
24
- autoAccepted: callData.autoAccepted || false,
25
- metadata: callData.metadata || {},
26
- tags: callData.tags || [],
27
- priority: callData.priority || 0,
28
- platformId: callData.platformId || null,
29
- workspaceId: callData.workspaceId || null,
30
- profileId: callData.profileId || null,
31
- });
32
-
33
- call.addToHistory("created", null, "Call created in system");
34
- await call.save({ session });
35
-
36
- await this._updateCallMetrics("created", session);
37
-
38
- return call;
39
- });
40
- } catch (error) {
41
- console.error(`Failed to create call ${callData.callId}:`, error);
42
- throw error;
43
- } finally {
44
- await session.endSession();
45
- }
46
- }
47
-
48
- // Get call by ID
49
- async getCall(callId, options = {}) {
50
- try {
51
- const query = Call.findOne({ callId });
52
-
53
- if (options.lean) query.lean();
54
-
55
- // Auto-populate unless explicitly disabled
56
- if (options.populate !== false) {
57
- query.populate([
58
- { path: "platformId", model: "Integration" },
59
- { path: "workspaceId", model: "Workspace" },
60
- { path: "profileId", model: "Profile" },
61
- ]);
62
- } else if (options.populate) {
63
- query.populate(options.populate);
64
- }
65
-
66
- if (options.select) query.select(options.select);
67
-
68
- return await query.exec();
69
- } catch (error) {
70
- console.error(`Failed to get call ${callId}:`, error);
71
- throw error;
72
- }
73
- }
74
-
75
- // Bulk get calls with advanced filtering
76
- async getCalls(filters = {}, options = {}) {
77
- try {
78
- const {
79
- status,
80
- agentId,
81
- dateRange,
82
- priority,
83
- tags,
84
- platformId,
85
- workspaceId,
86
- profileId,
87
- page = 1,
88
- limit = 50,
89
- sort = { createdAt: -1 },
90
- } = filters;
91
-
92
- const query = {};
93
-
94
- if (status)
95
- query.status = Array.isArray(status) ? { $in: status } : status;
96
- if (agentId) query.agentId = agentId;
97
- if (platformId) query.platformId = platformId;
98
- if (workspaceId) query.workspaceId = workspaceId;
99
- if (profileId) query.profileId = profileId;
100
-
101
- if (dateRange) {
102
- query.createdAt = {
103
- $gte: new Date(dateRange.start),
104
- $lte: new Date(dateRange.end),
105
- };
106
- }
107
-
108
- if (priority !== undefined) query.priority = { $gte: priority };
109
- if (tags && tags.length > 0) query.tags = { $in: tags };
110
-
111
- const skip = (page - 1) * limit;
112
-
113
- const [calls, total] = await Promise.all([
114
- Call.find(query)
115
- .sort(sort)
116
- .skip(skip)
117
- .limit(limit)
118
- .lean(options.lean !== false)
119
- .populate(
120
- options.populate !== false
121
- ? [
122
- { path: "platformId", model: "Integration" },
123
- { path: "workspaceId", model: "Workspace" },
124
- { path: "profileId", model: "Profile" },
125
- ]
126
- : []
127
- )
128
- .exec(),
129
- Call.countDocuments(query),
130
- ]);
131
-
132
- return {
133
- calls,
134
- pagination: {
135
- page,
136
- limit,
137
- total,
138
- pages: Math.ceil(total / limit),
139
- },
140
- };
141
- } catch (error) {
142
- console.error("Failed to get calls:", error);
143
- throw error;
144
- }
145
- }
146
-
147
- // Update call with optimistic locking
148
- async updateCall(callId, updateData, options = {}) {
149
- const session = options.session || (await mongoose.startSession());
150
- const shouldCommitSession = !options.session;
151
-
152
- try {
153
- return await session.withTransaction(async () => {
154
- const call = await Call.findOne({ callId }).session(session);
155
- if (!call) throw new Error(`Call ${callId} not found`);
156
-
157
- if (updateData.__v !== undefined && call.__v !== updateData.__v) {
158
- throw new Error(`Concurrent update detected for call ${callId}`);
159
- }
160
-
161
- const allowedFields = [
162
- "status",
163
- "agentId",
164
- "callerName",
165
- "callerNumber",
166
- "priority",
167
- "tags",
168
- "metadata",
169
- "isOnHold",
170
- "platformId",
171
- "workspaceId",
172
- "profileId",
173
- ];
174
-
175
- Object.keys(updateData).forEach((key) => {
176
- if (allowedFields.includes(key)) {
177
- call[key] = updateData[key];
178
- }
179
- });
180
-
181
- if (updateData.status && updateData.status !== call.status) {
182
- call.addToHistory(
183
- updateData.status,
184
- updateData.agentId || call.agentId,
185
- updateData.reason || `Status changed to ${updateData.status}`
186
- );
187
- }
188
-
189
- call.updateHeartbeat();
190
- await call.save({ session });
191
-
192
- return call;
193
- });
194
- } catch (error) {
195
- console.error(`Failed to update call ${callId}:`, error);
196
- throw error;
197
- } finally {
198
- if (shouldCommitSession) await session.endSession();
199
- }
200
- }
201
-
202
- // Assign call to agent
203
- async assignCallToAgent(callId, agentId, options = {}) {
204
- const session = await mongoose.startSession();
205
-
206
- try {
207
- return await session.withTransaction(async () => {
208
- const call = await Call.findOne({ callId }).session(session);
209
- if (!call) throw new Error(`Call ${callId} not found`);
210
-
211
- if (call.status !== "pending" && call.status !== "hold") {
212
- throw new Error(`Call ${callId} is not available for assignment`);
213
- }
214
-
215
- call.agentId = agentId;
216
- call.status = "active";
217
- call.startTime = new Date();
218
- call.isOnHold = false;
219
- call.addToHistory("answered", agentId, "Call answered by agent");
220
- call.updateHeartbeat();
221
-
222
- await call.save({ session });
223
- return call;
224
- });
225
- } catch (error) {
226
- console.error(
227
- `Failed to assign call ${callId} to agent ${agentId}:`,
228
- error
229
- );
230
- throw error;
231
- } finally {
232
- await session.endSession();
233
- }
234
- }
235
-
236
- // Bulk operations for call management
237
- async bulkUpdateCalls(operations) {
238
- const session = await mongoose.startSession();
239
-
240
- try {
241
- return await session.withTransaction(async () => {
242
- const bulkOps = operations.map((op) => ({
243
- updateOne: {
244
- filter: { callId: op.callId },
245
- update: {
246
- $set: {
247
- ...op.updateData,
248
- lastHeartbeat: new Date(),
249
- },
250
- $push: {
251
- callHistory: {
252
- timestamp: new Date(),
253
- action: op.action || "bulk_update",
254
- agentId: op.agentId,
255
- details: op.details || "Bulk operation",
256
- },
257
- },
258
- },
259
- },
260
- }));
261
-
262
- return await Call.bulkWrite(bulkOps, { session });
263
- });
264
- } catch (error) {
265
- console.error("Failed to perform bulk update:", error);
266
- throw error;
267
- } finally {
268
- await session.endSession();
269
- }
270
- }
271
-
272
- // Hold queue with priority
273
- async getHoldQueue(options = {}) {
274
- try {
275
- const pipeline = [
276
- { $match: { status: "hold", isOnHold: true } },
277
- {
278
- $addFields: {
279
- waitingTime: {
280
- $divide: [{ $subtract: [new Date(), "$createdAt"] }, 1000 * 60],
281
- },
282
- priorityScore: {
283
- $add: ["$priority", { $multiply: ["$waitingTime", 0.1] }],
284
- },
285
- },
286
- },
287
- { $sort: { priorityScore: -1, createdAt: 1 } },
288
- ];
289
-
290
- if (options.limit) pipeline.push({ $limit: options.limit });
291
-
292
- return await Call.aggregate(pipeline);
293
- } catch (error) {
294
- console.error("Failed to get hold queue:", error);
295
- throw error;
296
- }
297
- }
298
-
299
- // Get available calls
300
- async getAvailableCalls(options = {}) {
301
- try {
302
- return await Call.find({
303
- status: { $in: ["pending", "hold"] },
304
- agentId: null,
305
- })
306
- .sort({ priority: -1, createdAt: 1 })
307
- .limit(options.limit || 20)
308
- .lean(options.lean !== false);
309
- } catch (error) {
310
- console.error("Failed to get available calls:", error);
311
- throw error;
312
- }
313
- }
314
-
315
- // Advanced stats
316
- async getAdvancedStatistics(timeframe = "today", groupBy = null) {
317
- try {
318
- const dateFilter = this._buildDateFilter(timeframe);
319
-
320
- const pipeline = [
321
- { $match: dateFilter },
322
- {
323
- $group: {
324
- _id: groupBy ? `$${groupBy}` : null,
325
- totalCalls: { $sum: 1 },
326
- activeCalls: {
327
- $sum: { $cond: [{ $in: ["$status", ["active", "hold"]] }, 1, 0] },
328
- },
329
- completedCalls: {
330
- $sum: { $cond: [{ $eq: ["$status", "ended"] }, 1, 0] },
331
- },
332
- abandonedCalls: {
333
- $sum: { $cond: [{ $eq: ["$status", "abandoned"] }, 1, 0] },
334
- },
335
- averageDuration: { $avg: { $ifNull: ["$duration", 0] } },
336
- totalDuration: { $sum: { $ifNull: ["$duration", 0] } },
337
- averageWaitTime: {
338
- $avg: {
339
- $divide: [
340
- { $subtract: ["$startTime", "$createdAt"] },
341
- 1000 * 60,
342
- ],
343
- },
344
- },
345
- },
346
- },
347
- {
348
- $addFields: {
349
- answerRate: {
350
- $cond: [
351
- { $gt: ["$totalCalls", 0] },
352
- { $divide: ["$completedCalls", "$totalCalls"] },
353
- 0,
354
- ],
355
- },
356
- abandonRate: {
357
- $cond: [
358
- { $gt: ["$totalCalls", 0] },
359
- { $divide: ["$abandonedCalls", "$totalCalls"] },
360
- 0,
361
- ],
362
- },
363
- },
364
- },
365
- ];
366
-
367
- const result = await Call.aggregate(pipeline);
368
- return result[0] || this._getEmptyStats();
369
- } catch (error) {
370
- console.error("Failed to get advanced statistics:", error);
371
- throw error;
372
- }
373
- }
374
-
375
- // Cleanup stale connections
376
- async cleanupStaleConnections(timeoutMinutes = 5) {
377
- const session = await mongoose.startSession();
378
-
379
- try {
380
- return await session.withTransaction(async () => {
381
- const cutoffTime = new Date(Date.now() - timeoutMinutes * 60 * 1000);
382
-
383
- const staleCalls = await Call.find({
384
- status: { $in: ["active", "hold", "pending"] },
385
- lastHeartbeat: { $lt: cutoffTime },
386
- }).session(session);
387
-
388
- const results = [];
389
-
390
- for (const call of staleCalls) {
391
- if (call.agentId) {
392
- await this.moveCallToHoldQueue(call.callId, "Connection timeout", {
393
- session,
394
- });
395
- results.push({ callId: call.callId, action: "moved_to_hold" });
396
- } else {
397
- await this.endCall(call.callId, null, { session });
398
- results.push({ callId: call.callId, action: "ended" });
399
- }
400
- }
401
-
402
- console.log(`Cleaned up ${results.length} stale connections`);
403
- return results;
404
- });
405
- } catch (error) {
406
- console.error("Failed to cleanup stale connections:", error);
407
- throw error;
408
- } finally {
409
- await session.endSession();
410
- }
411
- }
412
-
413
- // Helper methods
414
- _buildDateFilter(timeframe) {
415
- const now = new Date();
416
- switch (timeframe) {
417
- case "today":
418
- const today = new Date(now);
419
- today.setHours(0, 0, 0, 0);
420
- return { createdAt: { $gte: today } };
421
- case "week":
422
- const weekAgo = new Date(now);
423
- weekAgo.setDate(weekAgo.getDate() - 7);
424
- return { createdAt: { $gte: weekAgo } };
425
- case "month":
426
- const monthAgo = new Date(now);
427
- monthAgo.setMonth(monthAgo.getMonth() - 1);
428
- return { createdAt: { $gte: monthAgo } };
429
- default:
430
- return {};
431
- }
432
- }
433
-
434
- _buildCallStatsPipeline() {
435
- return [
436
- {
437
- $group: {
438
- _id: "$status",
439
- count: { $sum: 1 },
440
- avgDuration: { $avg: "$duration" },
441
- },
442
- },
443
- ];
444
- }
445
-
446
- _buildHoldQueuePipeline() {
447
- return [
448
- { $match: { status: "hold", isOnHold: true } },
449
- { $sort: { priority: -1, createdAt: 1 } },
450
- ];
451
- }
452
-
453
- _getEmptyStats() {
454
- return {
455
- totalCalls: 0,
456
- activeCalls: 0,
457
- completedCalls: 0,
458
- abandonedCalls: 0,
459
- averageDuration: 0,
460
- totalDuration: 0,
461
- averageWaitTime: 0,
462
- answerRate: 0,
463
- abandonRate: 0,
464
- };
465
- }
466
-
467
- async _updateCallMetrics(action, session = null) {
468
- try {
469
- console.log(`Updating metrics for action: ${action}`);
470
- } catch (error) {
471
- console.error("Failed to update metrics:", error);
472
- }
473
- }
474
-
475
- async moveCallToHoldQueue(
476
- callId,
477
- reason = "Agent disconnected",
478
- options = {}
479
- ) {
480
- const session = options.session || (await mongoose.startSession());
481
- const shouldCommitSession = !options.session;
482
-
483
- try {
484
- return await session.withTransaction(async () => {
485
- const call = await Call.findOne({ callId }).session(session);
486
- if (!call) throw new Error(`Call ${callId} not found`);
487
-
488
- const previousAgentId = call.agentId;
489
-
490
- call.agentId = null;
491
- call.status = "hold";
492
- call.isOnHold = true;
493
- call.autoAccepted = true;
494
- call.isHoldConnection = true;
495
- call.priority = Math.min(call.priority + 1, 10);
496
- call.addToHistory("disconnected", previousAgentId, reason);
497
- call.updateHeartbeat();
498
-
499
- await call.save({ session });
500
-
501
- console.log(
502
- `Call ${callId} moved back to hold queue due to: ${reason}`
503
- );
504
- return call;
505
- });
506
- } catch (error) {
507
- console.error(`Failed to move call ${callId} to hold queue:`, error);
508
- throw error;
509
- } finally {
510
- if (shouldCommitSession) await session.endSession();
511
- }
512
- }
513
-
514
- async endCall(callId, agentId = null, options = {}) {
515
- const session = options.session || (await mongoose.startSession());
516
- const shouldCommitSession = !options.session;
517
-
518
- try {
519
- return await session.withTransaction(async () => {
520
- const call = await Call.findOne({ callId }).session(session);
521
- if (!call) throw new Error(`Call ${callId} not found`);
522
-
523
- call.status = "ended";
524
- call.endTime = new Date();
525
- call.duration = call.currentDuration;
526
- call.addToHistory("ended", agentId, "Call ended");
527
-
528
- await call.save({ session });
529
- return call;
530
- });
531
- } catch (error) {
532
- console.error(`Failed to end call ${callId}:`, error);
533
- throw error;
534
- } finally {
535
- if (shouldCommitSession) await session.endSession();
536
- }
537
- }
538
- }
539
-
540
- module.exports = new CallRepository();
package/models/Call.js DELETED
@@ -1,188 +0,0 @@
1
- const mongoose = require("mongoose");
2
- const { Schema } = mongoose;
3
-
4
- const callSchema = new mongoose.Schema(
5
- {
6
- callId: {
7
- type: String,
8
- required: true,
9
- unique: true,
10
- index: true,
11
- },
12
- callerName: {
13
- type: String,
14
- required: true,
15
- },
16
- callerNumber: {
17
- type: String,
18
- required: true,
19
- index: true,
20
- },
21
- status: {
22
- type: String,
23
- enum: ["pending", "hold", "active", "ended", "disconnected"],
24
- default: "pending",
25
- index: true,
26
- },
27
- queuePosition: {
28
- type: Number,
29
- default: 0,
30
- },
31
- isOnHold: {
32
- type: Boolean,
33
- default: false,
34
- index: true,
35
- },
36
- autoAccepted: {
37
- type: Boolean,
38
- default: false,
39
- },
40
- isHoldConnection: {
41
- type: Boolean,
42
- default: false,
43
- },
44
- agentId: {
45
- type: String,
46
- default: null,
47
- index: true,
48
- },
49
- startTime: {
50
- type: Date,
51
- default: null,
52
- },
53
- endTime: {
54
- type: Date,
55
- default: null,
56
- },
57
- duration: {
58
- type: Number, // in seconds
59
- default: 0,
60
- },
61
- whatsappSdp: {
62
- type: String,
63
- default: null,
64
- },
65
- browserSdp: {
66
- type: String,
67
- default: null,
68
- },
69
- connectionMetadata: {
70
- browserConnectionState: {
71
- type: String,
72
- default: "new",
73
- },
74
- whatsappConnectionState: {
75
- type: String,
76
- default: "new",
77
- },
78
- lastHeartbeat: {
79
- type: Date,
80
- default: Date.now,
81
- },
82
- },
83
- type: { type: String, default: "inbound" },
84
- callHistory: [
85
- {
86
- action: {
87
- type: String,
88
- enum: [
89
- "created",
90
- "answered",
91
- "held",
92
- "unheld",
93
- "transferred",
94
- "ended",
95
- "disconnected",
96
- "reconnected",
97
- ],
98
- },
99
- timestamp: {
100
- type: Date,
101
- default: Date.now,
102
- },
103
- agentId: String,
104
- details: String,
105
- },
106
- ],
107
- platformId: {
108
- type: Schema.Types.ObjectId,
109
- ref: "Integration",
110
- default: null,
111
- },
112
- workspaceId: { type: Schema.Types.ObjectId, ref: "Workspace" },
113
- profileId: { type: Schema.Types.ObjectId, ref: "Profile" },
114
- },
115
- {
116
- timestamps: true,
117
- collection: "calls",
118
- }
119
- );
120
-
121
- // Indexes for performance
122
- callSchema.index({ status: 1, createdAt: 1 });
123
- callSchema.index({ agentId: 1, status: 1 });
124
- callSchema.index({ callerNumber: 1, createdAt: -1 });
125
- callSchema.index({ isOnHold: 1, status: 1 });
126
-
127
- // Virtual for call duration calculation
128
- callSchema.virtual("currentDuration").get(function () {
129
- if (this.startTime) {
130
- const endTime = this.endTime || new Date();
131
- return Math.floor((endTime - this.startTime) / 1000);
132
- }
133
- return 0;
134
- });
135
-
136
- // Methods
137
- callSchema.methods.addToHistory = function (
138
- action,
139
- agentId = null,
140
- details = null
141
- ) {
142
- this.callHistory.push({
143
- action,
144
- agentId,
145
- details,
146
- timestamp: new Date(),
147
- });
148
- };
149
-
150
- callSchema.methods.updateHeartbeat = function () {
151
- this.connectionMetadata.lastHeartbeat = new Date();
152
- };
153
-
154
- callSchema.methods.isStale = function (timeoutMinutes = 5) {
155
- const timeout = timeoutMinutes * 60 * 1000; // Convert to milliseconds
156
- return new Date() - this.connectionMetadata.lastHeartbeat > timeout;
157
- };
158
-
159
- // Static methods
160
- callSchema.statics.getActiveCallsForAgent = function (agentId) {
161
- return this.find({
162
- agentId,
163
- status: { $in: ["active", "hold"] },
164
- }).sort({ createdAt: 1 });
165
- };
166
-
167
- callSchema.statics.getHoldQueue = function () {
168
- return this.find({
169
- status: "hold",
170
- agentId: null,
171
- }).sort({ createdAt: 1 });
172
- };
173
-
174
- callSchema.statics.getPendingCalls = function () {
175
- return this.find({
176
- status: "pending",
177
- }).sort({ createdAt: 1 });
178
- };
179
-
180
- callSchema.statics.getStaleConnections = function (timeoutMinutes = 5) {
181
- const timeout = new Date(Date.now() - timeoutMinutes * 60 * 1000);
182
- return this.find({
183
- status: { $in: ["active", "hold", "pending"] },
184
- "connectionMetadata.lastHeartbeat": { $lt: timeout },
185
- });
186
- };
187
-
188
- module.exports = mongoose.model("Call", callSchema);