shuttlepro-shared 1.4.22 → 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.
@@ -2,19 +2,83 @@ const Call = require("../../models/Call");
2
2
  const { Types } = require("mongoose");
3
3
 
4
4
  class CallRepository {
5
- constructor() {}
5
+ /* ----------------- Internal Helpers ----------------- */
6
6
 
7
- async createCall(callData) {
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
+ }
60
+
61
+ async _safeExec(promise, errorMsg, fallback = null) {
8
62
  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({
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({
18
82
  callId: callData.callId,
19
83
  callerName: callData.callerName,
20
84
  callerNumber: callData.callerNumber,
@@ -28,97 +92,76 @@ class CallRepository {
28
92
  whatsappSdp: callData.whatsappSdp || null,
29
93
  metadata: callData.metadata || {},
30
94
  tags: callData.tags || [],
31
- callHistory: [historyEntry],
32
- });
33
- } catch (error) {
34
- console.error(`❌ Failed to create call ${callData.callId}:`, error);
35
- return null;
36
- }
95
+ callHistory: [entry],
96
+ }),
97
+ `Failed to create call ${callData.callId}`
98
+ );
37
99
  }
38
100
 
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
- }
101
+ getCall(callId) {
102
+ return this._safeExec(
103
+ Call.findOne({ callId }).lean().exec(),
104
+ `Failed to get call ${callId}`
105
+ );
46
106
  }
47
107
 
48
- async endCall(callId, agentId = null, updateObj = {}, callHistory = {}) {
49
- try {
50
- 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(
51
112
  { callId },
52
- {
53
- $set: {
54
- ...updateObj,
55
- },
56
- $push: {
57
- callHistory: {
58
- ...callHistory,
59
- timestamp: new Date(),
60
- agentId,
61
- },
62
- },
63
- },
113
+ { $set: { ...updateObj }, $push: { callHistory: entry } },
64
114
  { new: true }
65
- );
66
- } catch (error) {
67
- console.error(`❌ Failed to end call ${callId}:`, error);
68
- return null;
69
- }
115
+ ),
116
+ `Failed to end call ${callId}`
117
+ );
70
118
  }
71
119
 
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
- }
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
+ };
111
138
 
112
- return await Call.findOneAndUpdate({ callId }, updateOps, { new: true });
113
- } catch (error) {
114
- console.error(`❌ Failed to update call ${callId}:`, error);
115
- 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
+ };
116
148
  }
149
+
150
+ return this._safeExec(
151
+ Call.findOneAndUpdate({ callId }, updateOps, { new: true }),
152
+ `Failed to update call ${callId}`
153
+ );
117
154
  }
118
155
 
119
- async assignCallToAgent(callId, agentId) {
120
- try {
121
- 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(
122
165
  { callId, status: { $in: ["pending", "hold"] } },
123
166
  {
124
167
  $set: {
@@ -127,54 +170,45 @@ class CallRepository {
127
170
  startTime: new Date(),
128
171
  "connectionMetadata.lastHeartbeat": new Date(),
129
172
  },
130
- $push: {
131
- callHistory: {
132
- timestamp: new Date(),
133
- action: "answered",
134
- agentId,
135
- details: "Call answered by agent",
136
- },
137
- },
173
+ $push: { callHistory: entry },
138
174
  },
139
175
  { new: true }
140
- );
141
- } catch (error) {
142
- console.error(`❌ Failed to assign call ${callId}:`, error);
143
- return null;
144
- }
176
+ ),
177
+ `Failed to assign call ${callId}`
178
+ );
145
179
  }
146
180
 
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
- },
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
+ }),
165
196
  },
166
197
  },
167
- }));
168
- return await Call.bulkWrite(bulkOps);
169
- } catch (error) {
170
- console.error("Bulk update failed:", error);
171
- return [];
172
- }
198
+ },
199
+ }));
200
+
201
+ return this._safeExec(Call.bulkWrite(bulkOps), "Bulk update failed", []);
173
202
  }
174
203
 
175
- async moveCallToHoldQueue(callId, reason = "Agent disconnected") {
176
- try {
177
- 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(
178
212
  { callId },
179
213
  {
180
214
  $set: {
@@ -183,94 +217,78 @@ class CallRepository {
183
217
  autoAccepted: true,
184
218
  "connectionMetadata.lastHeartbeat": new Date(),
185
219
  },
186
- $push: {
187
- callHistory: {
188
- timestamp: new Date(),
189
- action: "disconnected",
190
- agentId: null,
191
- details: reason,
192
- },
193
- },
220
+ $push: { callHistory: entry },
194
221
  },
195
222
  { new: true }
196
- );
197
- } catch (error) {
198
- console.error(`❌ Failed to move call ${callId} to hold queue:`, error);
199
- return null;
200
- }
223
+ ),
224
+ `Failed to move call ${callId} to hold queue`
225
+ );
201
226
  }
202
227
 
203
- /* ----------------- Query Helpers ----------------- */
228
+ /* ----------------- Queries ----------------- */
204
229
 
205
230
  async getCallsByWorkspace(
206
- workspaceId,
231
+ { workspaceId, from, to },
207
232
  {
208
233
  filters = {},
209
234
  select = null,
210
235
  sortBy = { createdAt: -1 },
211
- limit = null,
212
- skip = null,
236
+ limit,
237
+ skip,
213
238
  } = {}
214
239
  ) {
215
- let query = Call.find({ workspaceId, ...filters })
216
- .select(select || {}) // if null → select all fields
217
- .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)
218
249
  .lean();
219
250
 
220
- if (limit !== null) {
221
- query = query.limit(limit);
222
- }
223
- if (skip !== null) {
224
- query = query.skip(skip);
225
- }
251
+ if (limit) query = query.limit(limit);
252
+ if (skip) query = query.skip(skip);
226
253
 
227
254
  return query.exec();
228
255
  }
229
256
 
230
- async getCallsByAgent(
257
+ getCallsByAgent(
231
258
  agentId,
232
259
  {
233
260
  filters = {},
234
261
  select = null,
235
262
  sortBy = { createdAt: -1 },
236
- limit = null,
237
- skip = null,
263
+ limit,
264
+ skip,
238
265
  } = {}
239
266
  ) {
240
267
  let query = Call.find({ agentId, ...filters })
241
- .select(select || {}) // if null → select all fields
242
- .sort(sortBy) // default newest first
268
+ .select(select || {})
269
+ .sort(sortBy)
243
270
  .lean();
244
-
245
- if (limit !== null) {
246
- query = query.limit(limit);
247
- }
248
- if (skip !== null) {
249
- query = query.skip(skip);
250
- }
251
-
271
+ if (limit) query = query.limit(limit);
272
+ if (skip) query = query.skip(skip);
252
273
  return query.exec();
253
274
  }
254
275
 
255
- async getCallsByReceiver(receiver, filters = {}) {
276
+ getCallsByReceiver(receiver, filters = {}) {
256
277
  return Call.find({ receiver, ...filters })
257
278
  .sort({ createdAt: -1 })
258
279
  .lean()
259
280
  .exec();
260
281
  }
261
282
 
262
- async getActiveCalls(workspaceId) {
263
- return Call.find({
264
- workspaceId,
265
- status: { $in: ["active", "hold"] },
266
- });
283
+ getActiveCalls(workspaceId) {
284
+ return Call.find({ workspaceId, status: { $in: ["active", "hold"] } });
267
285
  }
268
286
 
269
- async getPendingCalls(workspaceId) {
287
+ getPendingCalls(workspaceId) {
270
288
  return Call.find({ workspaceId, status: "pending" });
271
289
  }
272
290
 
273
- async getStaleCalls(timeoutMinutes = 5) {
291
+ getStaleCalls(timeoutMinutes = 5) {
274
292
  const timeout = new Date(Date.now() - timeoutMinutes * 60 * 1000);
275
293
  return Call.find({
276
294
  status: { $in: ["active", "hold", "pending"] },
@@ -278,41 +296,16 @@ class CallRepository {
278
296
  });
279
297
  }
280
298
 
281
- /* ----------------- Reporting & Analytics ----------------- */
282
-
283
- // Stats grouped by agent
299
+ /* ----------------- Reporting ----------------- */
284
300
 
285
301
  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
- );
302
+ const { from: fromDate, to: toDate } = this._utcDayBounds(from, to);
303
303
 
304
- const match = {
305
- createdAt: { $gte: fromDate, $lte: toDate },
306
- ...filters,
307
- };
308
- if (match.workspaceId && typeof match.workspaceId === "string") {
304
+ const match = { createdAt: { $gte: fromDate, $lte: toDate }, ...filters };
305
+ if (match.workspaceId && typeof match.workspaceId === "string")
309
306
  match.workspaceId = new Types.ObjectId(match.workspaceId);
310
- }
311
- if (match.agentId && typeof match.agentId === "string") {
307
+ if (match.agentId && typeof match.agentId === "string")
312
308
  match.agentId = new Types.ObjectId(match.agentId);
313
- }
314
-
315
- console.log(match, "match");
316
309
 
317
310
  const [stats] = await Call.aggregate([
318
311
  { $match: match },
@@ -332,12 +325,10 @@ class CallRepository {
332
325
  },
333
326
  ]);
334
327
 
335
- // Always return a single object
336
328
  return stats || { totalCalls: 0, pending: 0, abandoned: 0, ended: 0 };
337
329
  }
338
330
 
339
- // Daily call summary
340
- async getDailySummary(workspaceId, from, to, filters = {}, groupBy = null) {
331
+ getDailySummary(workspaceId, from, to, filters = {}, groupBy = null) {
341
332
  const matchStage = {
342
333
  workspaceId,
343
334
  createdAt: { $gte: from, $lte: to },
@@ -347,11 +338,8 @@ class CallRepository {
347
338
  const groupId = {
348
339
  day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
349
340
  };
350
-
351
- // If admin wants to group by agentId / receiver / platformId
352
- if (groupBy && ["agentId", "receiver", "platformId"].includes(groupBy)) {
341
+ if (groupBy && ["agentId", "receiver", "platformId"].includes(groupBy))
353
342
  groupId[groupBy] = `$${groupBy}`;
354
- }
355
343
 
356
344
  return Call.aggregate([
357
345
  { $match: matchStage },
@@ -376,7 +364,7 @@ class CallRepository {
376
364
  total: 1,
377
365
  ended: 1,
378
366
  abandoned: 1,
379
- avgDuration: { $round: ["$avgDuration", 2] }, // round to 2 decimals
367
+ avgDuration: { $round: ["$avgDuration", 2] },
380
368
  minDuration: 1,
381
369
  maxDuration: 1,
382
370
  },
@@ -385,8 +373,7 @@ class CallRepository {
385
373
  ]);
386
374
  }
387
375
 
388
- // Queue stats
389
- async getQueueStats(workspaceId) {
376
+ getQueueStats(workspaceId) {
390
377
  return Call.aggregate([
391
378
  { $match: { workspaceId, status: "hold" } },
392
379
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shuttlepro-shared",
3
- "version": "1.4.22",
3
+ "version": "1.4.23",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {