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.
- package/common/repositories/call.repository.js +204 -217
- package/package.json +1 -1
|
@@ -2,19 +2,83 @@ const Call = require("../../models/Call");
|
|
|
2
2
|
const { Types } = require("mongoose");
|
|
3
3
|
|
|
4
4
|
class CallRepository {
|
|
5
|
-
|
|
5
|
+
/* ----------------- Internal Helpers ----------------- */
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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: [
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
95
|
+
callHistory: [entry],
|
|
96
|
+
}),
|
|
97
|
+
`Failed to create call ${callData.callId}`
|
|
98
|
+
);
|
|
37
99
|
}
|
|
38
100
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
115
|
+
),
|
|
116
|
+
`Failed to end call ${callId}`
|
|
117
|
+
);
|
|
70
118
|
}
|
|
71
119
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
176
|
+
),
|
|
177
|
+
`Failed to assign call ${callId}`
|
|
178
|
+
);
|
|
145
179
|
}
|
|
146
180
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return [];
|
|
172
|
-
}
|
|
198
|
+
},
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
return this._safeExec(Call.bulkWrite(bulkOps), "Bulk update failed", []);
|
|
173
202
|
}
|
|
174
203
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
223
|
+
),
|
|
224
|
+
`Failed to move call ${callId} to hold queue`
|
|
225
|
+
);
|
|
201
226
|
}
|
|
202
227
|
|
|
203
|
-
/* -----------------
|
|
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
|
|
212
|
-
skip
|
|
236
|
+
limit,
|
|
237
|
+
skip,
|
|
213
238
|
} = {}
|
|
214
239
|
) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
221
|
-
|
|
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
|
-
|
|
257
|
+
getCallsByAgent(
|
|
231
258
|
agentId,
|
|
232
259
|
{
|
|
233
260
|
filters = {},
|
|
234
261
|
select = null,
|
|
235
262
|
sortBy = { createdAt: -1 },
|
|
236
|
-
limit
|
|
237
|
-
skip
|
|
263
|
+
limit,
|
|
264
|
+
skip,
|
|
238
265
|
} = {}
|
|
239
266
|
) {
|
|
240
267
|
let query = Call.find({ agentId, ...filters })
|
|
241
|
-
.select(select || {})
|
|
242
|
-
.sort(sortBy)
|
|
268
|
+
.select(select || {})
|
|
269
|
+
.sort(sortBy)
|
|
243
270
|
.lean();
|
|
244
|
-
|
|
245
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
+
getPendingCalls(workspaceId) {
|
|
270
288
|
return Call.find({ workspaceId, status: "pending" });
|
|
271
289
|
}
|
|
272
290
|
|
|
273
|
-
|
|
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
|
|
282
|
-
|
|
283
|
-
// Stats grouped by agent
|
|
299
|
+
/* ----------------- Reporting ----------------- */
|
|
284
300
|
|
|
285
301
|
async getAgentStats({ from, to }, filters = {}) {
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] },
|
|
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
|
-
|
|
389
|
-
async getQueueStats(workspaceId) {
|
|
376
|
+
getQueueStats(workspaceId) {
|
|
390
377
|
return Call.aggregate([
|
|
391
378
|
{ $match: { workspaceId, status: "hold" } },
|
|
392
379
|
{
|