shuttlepro-shared 1.4.21 → 1.4.23

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.
@@ -1,19 +1,84 @@
1
1
  const Call = require("../../models/Call");
2
+ const { Types } = require("mongoose");
2
3
 
3
4
  class CallRepository {
4
- constructor() {}
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 = "Agent Initiated",
49
+ platformTimestamp = "",
50
+ }) {
51
+ return {
52
+ timestamp: new Date(),
53
+ platformTimestamp,
54
+ action,
55
+ agentId,
56
+ details,
57
+ direction,
58
+ };
59
+ }
5
60
 
6
- async createCall(callData) {
61
+ async _safeExec(promise, errorMsg, fallback = null) {
7
62
  try {
8
- const historyEntry = {
9
- timestamp: new Date(),
10
- platformTimestamp: callData.platformTimestamp || "",
11
- action: "created",
12
- agentId: null,
13
- direction: "User Initiated",
14
- details: "Call created from webhook",
15
- };
16
- return await Call.create({
63
+ return await promise;
64
+ } catch (error) {
65
+ console.error(`❌ ${errorMsg}`, error);
66
+ return fallback;
67
+ }
68
+ }
69
+
70
+ /* ----------------- Core CRUD ----------------- */
71
+
72
+ createCall(callData) {
73
+ const entry = this._historyEntry({
74
+ action: "created",
75
+ direction: "User Initiated",
76
+ details: "Call created from webhook",
77
+ platformTimestamp: callData.platformTimestamp || "",
78
+ });
79
+
80
+ return this._safeExec(
81
+ Call.create({
17
82
  callId: callData.callId,
18
83
  callerName: callData.callerName,
19
84
  callerNumber: callData.callerNumber,
@@ -27,97 +92,76 @@ class CallRepository {
27
92
  whatsappSdp: callData.whatsappSdp || null,
28
93
  metadata: callData.metadata || {},
29
94
  tags: callData.tags || [],
30
- callHistory: [historyEntry],
31
- });
32
- } catch (error) {
33
- console.error(`❌ Failed to create call ${callData.callId}:`, error);
34
- return null;
35
- }
95
+ callHistory: [entry],
96
+ }),
97
+ `Failed to create call ${callData.callId}`
98
+ );
36
99
  }
37
100
 
38
- async getCall(callId) {
39
- try {
40
- return await Call.findOne({ callId }).lean().exec();
41
- } catch (error) {
42
- console.error(`❌ Failed to end call ${callId}:`, error);
43
- return null;
44
- }
101
+ getCall(callId) {
102
+ return this._safeExec(
103
+ Call.findOne({ callId }).lean().exec(),
104
+ `Failed to get call ${callId}`
105
+ );
45
106
  }
46
107
 
47
- async endCall(callId, agentId = null, updateObj = {}, callHistory = {}) {
48
- try {
49
- return await Call.findOneAndUpdate(
108
+ endCall(callId, agentId = null, updateObj = {}, history = {}) {
109
+ const entry = this._historyEntry({ ...history, agentId });
110
+ return this._safeExec(
111
+ Call.findOneAndUpdate(
50
112
  { callId },
51
- {
52
- $set: {
53
- ...updateObj,
54
- },
55
- $push: {
56
- callHistory: {
57
- ...callHistory,
58
- timestamp: new Date(),
59
- agentId,
60
- },
61
- },
62
- },
113
+ { $set: { ...updateObj }, $push: { callHistory: entry } },
63
114
  { new: true }
64
- );
65
- } catch (error) {
66
- console.error(`❌ Failed to end call ${callId}:`, error);
67
- return null;
68
- }
115
+ ),
116
+ `Failed to end call ${callId}`
117
+ );
69
118
  }
70
119
 
71
- async updateCall(callId, updateData) {
72
- try {
73
- const allowedFields = [
74
- "status",
75
- "agentId",
76
- "callerName",
77
- "callerNumber",
78
- "tags",
79
- "metadata",
80
- "platformId",
81
- "workspaceId",
82
- "profileId",
83
- "receiver",
84
- ];
85
-
86
- const filteredUpdate = {};
87
- Object.keys(updateData).forEach((key) => {
88
- if (allowedFields.includes(key)) filteredUpdate[key] = updateData[key];
89
- });
90
-
91
- const updateOps = {
92
- $set: {
93
- ...filteredUpdate,
94
- "connectionMetadata.lastHeartbeat": new Date(),
95
- },
96
- };
97
-
98
- if (updateData.status) {
99
- updateOps.$push = {
100
- callHistory: {
101
- timestamp: new Date(),
102
- action: updateData.status,
103
- agentId: updateData.agentId || null,
104
- details:
105
- updateData.reason || `Status changed to ${updateData.status}`,
106
- direction: "Agent Initiated",
107
- },
108
- };
109
- }
120
+ updateCall(callId, updateData) {
121
+ const allowed = [
122
+ "status",
123
+ "agentId",
124
+ "callerName",
125
+ "callerNumber",
126
+ "tags",
127
+ "metadata",
128
+ "platformId",
129
+ "workspaceId",
130
+ "profileId",
131
+ "receiver",
132
+ ];
133
+ const filtered = this._filterAllowedFields(updateData, allowed);
134
+
135
+ const updateOps = {
136
+ $set: { ...filtered, "connectionMetadata.lastHeartbeat": new Date() },
137
+ };
110
138
 
111
- return await Call.findOneAndUpdate({ callId }, updateOps, { new: true });
112
- } catch (error) {
113
- console.error(`❌ Failed to update call ${callId}:`, error);
114
- return null;
139
+ if (updateData.status) {
140
+ updateOps.$push = {
141
+ callHistory: this._historyEntry({
142
+ action: updateData.status,
143
+ agentId: updateData.agentId,
144
+ details:
145
+ updateData.reason || `Status changed to ${updateData.status}`,
146
+ }),
147
+ };
115
148
  }
149
+
150
+ return this._safeExec(
151
+ Call.findOneAndUpdate({ callId }, updateOps, { new: true }),
152
+ `Failed to update call ${callId}`
153
+ );
116
154
  }
117
155
 
118
- async assignCallToAgent(callId, agentId) {
119
- try {
120
- return await Call.findOneAndUpdate(
156
+ assignCallToAgent(callId, agentId) {
157
+ const entry = this._historyEntry({
158
+ action: "answered",
159
+ agentId,
160
+ details: "Call answered by agent",
161
+ });
162
+
163
+ return this._safeExec(
164
+ Call.findOneAndUpdate(
121
165
  { callId, status: { $in: ["pending", "hold"] } },
122
166
  {
123
167
  $set: {
@@ -126,54 +170,45 @@ class CallRepository {
126
170
  startTime: new Date(),
127
171
  "connectionMetadata.lastHeartbeat": new Date(),
128
172
  },
129
- $push: {
130
- callHistory: {
131
- timestamp: new Date(),
132
- action: "answered",
133
- agentId,
134
- details: "Call answered by agent",
135
- },
136
- },
173
+ $push: { callHistory: entry },
137
174
  },
138
175
  { new: true }
139
- );
140
- } catch (error) {
141
- console.error(`❌ Failed to assign call ${callId}:`, error);
142
- return null;
143
- }
176
+ ),
177
+ `Failed to assign call ${callId}`
178
+ );
144
179
  }
145
180
 
146
- async bulkUpdateCalls(operations) {
147
- try {
148
- const bulkOps = operations.map((op) => ({
149
- updateOne: {
150
- filter: { callId: op.callId },
151
- update: {
152
- $set: {
153
- ...op.updateData,
154
- "connectionMetadata.lastHeartbeat": new Date(),
155
- },
156
- $push: {
157
- callHistory: {
158
- timestamp: new Date(),
159
- action: op.action || "bulk_update",
160
- agentId: op.agentId,
161
- details: op.details || "Bulk operation",
162
- },
163
- },
181
+ bulkUpdateCalls(operations) {
182
+ const bulkOps = operations.map((op) => ({
183
+ updateOne: {
184
+ filter: { callId: op.callId },
185
+ update: {
186
+ $set: {
187
+ ...op.updateData,
188
+ "connectionMetadata.lastHeartbeat": new Date(),
189
+ },
190
+ $push: {
191
+ callHistory: this._historyEntry({
192
+ action: op.action || "bulk_update",
193
+ agentId: op.agentId,
194
+ details: op.details || "Bulk operation",
195
+ }),
164
196
  },
165
197
  },
166
- }));
167
- return await Call.bulkWrite(bulkOps);
168
- } catch (error) {
169
- console.error("Bulk update failed:", error);
170
- return [];
171
- }
198
+ },
199
+ }));
200
+
201
+ return this._safeExec(Call.bulkWrite(bulkOps), "Bulk update failed", []);
172
202
  }
173
203
 
174
- async moveCallToHoldQueue(callId, reason = "Agent disconnected") {
175
- try {
176
- return await Call.findOneAndUpdate(
204
+ moveCallToHoldQueue(callId, reason = "Agent disconnected") {
205
+ const entry = this._historyEntry({
206
+ action: "disconnected",
207
+ details: reason,
208
+ });
209
+
210
+ return this._safeExec(
211
+ Call.findOneAndUpdate(
177
212
  { callId },
178
213
  {
179
214
  $set: {
@@ -182,94 +217,78 @@ class CallRepository {
182
217
  autoAccepted: true,
183
218
  "connectionMetadata.lastHeartbeat": new Date(),
184
219
  },
185
- $push: {
186
- callHistory: {
187
- timestamp: new Date(),
188
- action: "disconnected",
189
- agentId: null,
190
- details: reason,
191
- },
192
- },
220
+ $push: { callHistory: entry },
193
221
  },
194
222
  { new: true }
195
- );
196
- } catch (error) {
197
- console.error(`❌ Failed to move call ${callId} to hold queue:`, error);
198
- return null;
199
- }
223
+ ),
224
+ `Failed to move call ${callId} to hold queue`
225
+ );
200
226
  }
201
227
 
202
- /* ----------------- Query Helpers ----------------- */
228
+ /* ----------------- Queries ----------------- */
203
229
 
204
230
  async getCallsByWorkspace(
205
- workspaceId,
231
+ { workspaceId, from, to },
206
232
  {
207
233
  filters = {},
208
234
  select = null,
209
235
  sortBy = { createdAt: -1 },
210
- limit = null,
211
- skip = null,
236
+ limit,
237
+ skip,
212
238
  } = {}
213
239
  ) {
214
- let query = Call.find({ workspaceId, ...filters })
215
- .select(select || {}) // if null → select all fields
216
- .sort(sortBy) // default newest first
240
+ const { from: fromDate, to: toDate } = this._utcDayBounds(from, to);
241
+
242
+ let query = Call.find({
243
+ workspaceId,
244
+ createdAt: { $gte: fromDate, $lte: toDate },
245
+ ...filters,
246
+ })
247
+ .select(select || {})
248
+ .sort(sortBy)
217
249
  .lean();
218
250
 
219
- if (limit !== null) {
220
- query = query.limit(limit);
221
- }
222
- if (skip !== null) {
223
- query = query.skip(skip);
224
- }
251
+ if (limit) query = query.limit(limit);
252
+ if (skip) query = query.skip(skip);
225
253
 
226
254
  return query.exec();
227
255
  }
228
256
 
229
- async getCallsByAgent(
257
+ getCallsByAgent(
230
258
  agentId,
231
259
  {
232
260
  filters = {},
233
261
  select = null,
234
262
  sortBy = { createdAt: -1 },
235
- limit = null,
236
- skip = null,
263
+ limit,
264
+ skip,
237
265
  } = {}
238
266
  ) {
239
267
  let query = Call.find({ agentId, ...filters })
240
- .select(select || {}) // if null → select all fields
241
- .sort(sortBy) // default newest first
268
+ .select(select || {})
269
+ .sort(sortBy)
242
270
  .lean();
243
-
244
- if (limit !== null) {
245
- query = query.limit(limit);
246
- }
247
- if (skip !== null) {
248
- query = query.skip(skip);
249
- }
250
-
271
+ if (limit) query = query.limit(limit);
272
+ if (skip) query = query.skip(skip);
251
273
  return query.exec();
252
274
  }
253
275
 
254
- async getCallsByReceiver(receiver, filters = {}) {
276
+ getCallsByReceiver(receiver, filters = {}) {
255
277
  return Call.find({ receiver, ...filters })
256
278
  .sort({ createdAt: -1 })
257
279
  .lean()
258
280
  .exec();
259
281
  }
260
282
 
261
- async getActiveCalls(workspaceId) {
262
- return Call.find({
263
- workspaceId,
264
- status: { $in: ["active", "hold"] },
265
- });
283
+ getActiveCalls(workspaceId) {
284
+ return Call.find({ workspaceId, status: { $in: ["active", "hold"] } });
266
285
  }
267
286
 
268
- async getPendingCalls(workspaceId) {
287
+ getPendingCalls(workspaceId) {
269
288
  return Call.find({ workspaceId, status: "pending" });
270
289
  }
271
290
 
272
- async getStaleCalls(timeoutMinutes = 5) {
291
+ getStaleCalls(timeoutMinutes = 5) {
273
292
  const timeout = new Date(Date.now() - timeoutMinutes * 60 * 1000);
274
293
  return Call.find({
275
294
  status: { $in: ["active", "hold", "pending"] },
@@ -277,29 +296,19 @@ class CallRepository {
277
296
  });
278
297
  }
279
298
 
280
- /* ----------------- Reporting & Analytics ----------------- */
299
+ /* ----------------- Reporting ----------------- */
281
300
 
282
- // Stats grouped by agent
283
301
  async getAgentStats({ from, to }, filters = {}) {
284
- // Pakistan offset (+5 hours)
285
- const PAK_OFFSET = 5 * 60 * 60 * 1000;
286
-
287
- // convert input to Date objects
288
- const fromDate = new Date(new Date(from).getTime() - PAK_OFFSET);
289
- const toDate = new Date(new Date(to).getTime() - PAK_OFFSET);
302
+ const { from: fromDate, to: toDate } = this._utcDayBounds(from, to);
290
303
 
291
- // set end of day in Pakistan time
292
- toDate.setHours(23, 59, 59, 999);
293
-
294
- console.log({ fromDate, toDate }, filters);
295
-
296
- const matchStage = {
297
- createdAt: { $gte: fromDate, $lte: toDate },
298
- ...filters,
299
- };
304
+ const match = { createdAt: { $gte: fromDate, $lte: toDate }, ...filters };
305
+ if (match.workspaceId && typeof match.workspaceId === "string")
306
+ match.workspaceId = new Types.ObjectId(match.workspaceId);
307
+ if (match.agentId && typeof match.agentId === "string")
308
+ match.agentId = new Types.ObjectId(match.agentId);
300
309
 
301
- const result = await Call.aggregate([
302
- { $match: matchStage },
310
+ const [stats] = await Call.aggregate([
311
+ { $match: match },
303
312
  {
304
313
  $group: {
305
314
  _id: filters.agentId ? "$agentId" : null,
@@ -312,25 +321,14 @@ class CallRepository {
312
321
  },
313
322
  },
314
323
  {
315
- $project: {
316
- _id: 0,
317
- totalCalls: 1,
318
- pending: 1,
319
- abandoned: 1,
320
- ended: 1,
321
- },
324
+ $project: { _id: 0, totalCalls: 1, pending: 1, abandoned: 1, ended: 1 },
322
325
  },
323
326
  ]);
324
327
 
325
- if (result.length === 0) {
326
- return { totalCalls: 0, pending: 0, abandoned: 0, ended: 0 };
327
- }
328
-
329
- return result[0];
328
+ return stats || { totalCalls: 0, pending: 0, abandoned: 0, ended: 0 };
330
329
  }
331
330
 
332
- // Daily call summary
333
- async getDailySummary(workspaceId, from, to, filters = {}, groupBy = null) {
331
+ getDailySummary(workspaceId, from, to, filters = {}, groupBy = null) {
334
332
  const matchStage = {
335
333
  workspaceId,
336
334
  createdAt: { $gte: from, $lte: to },
@@ -340,11 +338,8 @@ class CallRepository {
340
338
  const groupId = {
341
339
  day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
342
340
  };
343
-
344
- // If admin wants to group by agentId / receiver / platformId
345
- if (groupBy && ["agentId", "receiver", "platformId"].includes(groupBy)) {
341
+ if (groupBy && ["agentId", "receiver", "platformId"].includes(groupBy))
346
342
  groupId[groupBy] = `$${groupBy}`;
347
- }
348
343
 
349
344
  return Call.aggregate([
350
345
  { $match: matchStage },
@@ -369,7 +364,7 @@ class CallRepository {
369
364
  total: 1,
370
365
  ended: 1,
371
366
  abandoned: 1,
372
- avgDuration: { $round: ["$avgDuration", 2] }, // round to 2 decimals
367
+ avgDuration: { $round: ["$avgDuration", 2] },
373
368
  minDuration: 1,
374
369
  maxDuration: 1,
375
370
  },
@@ -378,8 +373,7 @@ class CallRepository {
378
373
  ]);
379
374
  }
380
375
 
381
- // Queue stats
382
- async getQueueStats(workspaceId) {
376
+ getQueueStats(workspaceId) {
383
377
  return Call.aggregate([
384
378
  { $match: { workspaceId, status: "hold" } },
385
379
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shuttlepro-shared",
3
- "version": "1.4.21",
3
+ "version": "1.4.23",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {