shuttlepro-shared 1.4.2 → 1.4.3

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.
@@ -8,10 +8,17 @@ class CallRepository {
8
8
  };
9
9
  }
10
10
 
11
- // Create a new call without transaction
11
+ // Create a new call
12
12
  async createCall(callData) {
13
13
  try {
14
- const call = new Call({
14
+ const historyEntry = {
15
+ timestamp: new Date(),
16
+ action: "created",
17
+ agentId: null,
18
+ details: "Call created in system",
19
+ };
20
+
21
+ const call = await Call.create({
15
22
  callId: callData.callId,
16
23
  callerName: callData.callerName,
17
24
  callerNumber: callData.callerNumber,
@@ -24,16 +31,16 @@ class CallRepository {
24
31
  platformId: callData.platformId || null,
25
32
  workspaceId: callData.workspaceId || null,
26
33
  profileId: callData.profileId || null,
34
+ platformTimestamp: callData.platformTimestamp || "",
35
+ receiver: callData.receiver || "",
36
+ callHistory: [historyEntry],
27
37
  });
28
38
 
29
- call.addToHistory("created", null, "Call created in system");
30
- await call.save();
31
-
32
39
  await this._updateCallMetrics("created");
33
40
  return call;
34
41
  } catch (error) {
35
42
  console.error(`Failed to create call ${callData.callId}:`, error);
36
- throw error;
43
+ return null;
37
44
  }
38
45
  }
39
46
 
@@ -59,7 +66,7 @@ class CallRepository {
59
66
  return await query.exec();
60
67
  } catch (error) {
61
68
  console.error(`Failed to get call ${callId}:`, error);
62
- throw error;
69
+ return null;
63
70
  }
64
71
  }
65
72
 
@@ -130,20 +137,16 @@ class CallRepository {
130
137
  };
131
138
  } catch (error) {
132
139
  console.error("Failed to get calls:", error);
133
- throw error;
140
+ return {
141
+ calls: [],
142
+ pagination: { page: 1, limit: 50, total: 0, pages: 0 },
143
+ };
134
144
  }
135
145
  }
136
146
 
137
147
  // Update call
138
148
  async updateCall(callId, updateData) {
139
149
  try {
140
- const call = await Call.findOne({ callId });
141
- if (!call) throw new Error(`Call ${callId} not found`);
142
-
143
- if (updateData.__v !== undefined && call.__v !== updateData.__v) {
144
- throw new Error(`Concurrent update detected for call ${callId}`);
145
- }
146
-
147
150
  const allowedFields = [
148
151
  "status",
149
152
  "agentId",
@@ -158,54 +161,73 @@ class CallRepository {
158
161
  "profileId",
159
162
  ];
160
163
 
164
+ const filteredUpdate = {};
161
165
  Object.keys(updateData).forEach((key) => {
162
166
  if (allowedFields.includes(key)) {
163
- call[key] = updateData[key];
167
+ filteredUpdate[key] = updateData[key];
164
168
  }
165
169
  });
166
170
 
167
- if (updateData.status && updateData.status !== call.status) {
168
- call.addToHistory(
169
- updateData.status,
170
- updateData.agentId || call.agentId,
171
- updateData.reason || `Status changed to ${updateData.status}`
172
- );
171
+ const updateOps = {
172
+ $set: {
173
+ ...filteredUpdate,
174
+ lastHeartbeat: new Date(),
175
+ },
176
+ };
177
+
178
+ if (updateData.status) {
179
+ updateOps.$push = {
180
+ callHistory: {
181
+ timestamp: new Date(),
182
+ action: updateData.status,
183
+ agentId: updateData.agentId || null,
184
+ details:
185
+ updateData.reason || `Status changed to ${updateData.status}`,
186
+ },
187
+ };
173
188
  }
174
189
 
175
- call.updateHeartbeat();
176
- await call.save();
177
- return call;
190
+ const call = await Call.findOneAndUpdate({ callId }, updateOps, {
191
+ new: true,
192
+ });
193
+ return call || null;
178
194
  } catch (error) {
179
195
  console.error(`Failed to update call ${callId}:`, error);
180
- throw error;
196
+ return null;
181
197
  }
182
198
  }
183
199
 
184
200
  // Assign call to agent
185
201
  async assignCallToAgent(callId, agentId) {
186
202
  try {
187
- const call = await Call.findOne({ callId });
188
- if (!call) throw new Error(`Call ${callId} not found`);
189
-
190
- if (call.status !== "pending" && call.status !== "hold") {
191
- throw new Error(`Call ${callId} is not available for assignment`);
192
- }
193
-
194
- call.agentId = agentId;
195
- call.status = "active";
196
- call.startTime = new Date();
197
- call.isOnHold = false;
198
- call.addToHistory("answered", agentId, "Call answered by agent");
199
- call.updateHeartbeat();
200
-
201
- await call.save();
202
- return call;
203
+ const call = await Call.findOneAndUpdate(
204
+ { callId, status: { $in: ["pending", "hold"] } },
205
+ {
206
+ $set: {
207
+ agentId,
208
+ status: "active",
209
+ startTime: new Date(),
210
+ isOnHold: false,
211
+ lastHeartbeat: new Date(),
212
+ },
213
+ $push: {
214
+ callHistory: {
215
+ timestamp: new Date(),
216
+ action: "answered",
217
+ agentId,
218
+ details: "Call answered by agent",
219
+ },
220
+ },
221
+ },
222
+ { new: true }
223
+ );
224
+ return call || null;
203
225
  } catch (error) {
204
226
  console.error(
205
227
  `Failed to assign call ${callId} to agent ${agentId}:`,
206
228
  error
207
229
  );
208
- throw error;
230
+ return null;
209
231
  }
210
232
  }
211
233
 
@@ -235,7 +257,7 @@ class CallRepository {
235
257
  return await Call.bulkWrite(bulkOps);
236
258
  } catch (error) {
237
259
  console.error("Failed to perform bulk update:", error);
238
- throw error;
260
+ return [];
239
261
  }
240
262
  }
241
263
 
@@ -262,7 +284,7 @@ class CallRepository {
262
284
  return await Call.aggregate(pipeline);
263
285
  } catch (error) {
264
286
  console.error("Failed to get hold queue:", error);
265
- throw error;
287
+ return [];
266
288
  }
267
289
  }
268
290
 
@@ -278,7 +300,7 @@ class CallRepository {
278
300
  .lean(options.lean !== false);
279
301
  } catch (error) {
280
302
  console.error("Failed to get available calls:", error);
281
- throw error;
303
+ return [];
282
304
  }
283
305
  }
284
306
 
@@ -338,7 +360,7 @@ class CallRepository {
338
360
  return result[0] || this._getEmptyStats();
339
361
  } catch (error) {
340
362
  console.error("Failed to get advanced statistics:", error);
341
- throw error;
363
+ return this._getEmptyStats();
342
364
  }
343
365
  }
344
366
 
@@ -356,11 +378,15 @@ class CallRepository {
356
378
 
357
379
  for (const call of staleCalls) {
358
380
  if (call.agentId) {
359
- await this.moveCallToHoldQueue(call.callId, "Connection timeout");
360
- results.push({ callId: call.callId, action: "moved_to_hold" });
381
+ const moved = await this.moveCallToHoldQueue(
382
+ call.callId,
383
+ "Connection timeout"
384
+ );
385
+ if (moved)
386
+ results.push({ callId: call.callId, action: "moved_to_hold" });
361
387
  } else {
362
- await this.endCall(call.callId);
363
- results.push({ callId: call.callId, action: "ended" });
388
+ const ended = await this.endCall(call.callId);
389
+ if (ended) results.push({ callId: call.callId, action: "ended" });
364
390
  }
365
391
  }
366
392
 
@@ -368,7 +394,73 @@ class CallRepository {
368
394
  return results;
369
395
  } catch (error) {
370
396
  console.error("Failed to cleanup stale connections:", error);
371
- throw error;
397
+ return [];
398
+ }
399
+ }
400
+
401
+ async moveCallToHoldQueue(callId, reason = "Agent disconnected") {
402
+ try {
403
+ const call = await Call.findOneAndUpdate(
404
+ { callId },
405
+ {
406
+ $set: {
407
+ agentId: null,
408
+ status: "hold",
409
+ isOnHold: true,
410
+ autoAccepted: true,
411
+ isHoldConnection: true,
412
+ lastHeartbeat: new Date(),
413
+ },
414
+ $inc: { priority: 1 },
415
+ $push: {
416
+ callHistory: {
417
+ timestamp: new Date(),
418
+ action: "disconnected",
419
+ agentId: null,
420
+ details: reason,
421
+ },
422
+ },
423
+ },
424
+ { new: true }
425
+ );
426
+
427
+ if (call) {
428
+ console.log(
429
+ `Call ${callId} moved back to hold queue due to: ${reason}`
430
+ );
431
+ }
432
+ return call || null;
433
+ } catch (error) {
434
+ console.error(`Failed to move call ${callId} to hold queue:`, error);
435
+ return null;
436
+ }
437
+ }
438
+
439
+ async endCall(callId, agentId = null) {
440
+ try {
441
+ const call = await Call.findOneAndUpdate(
442
+ { callId },
443
+ {
444
+ $set: {
445
+ status: "ended",
446
+ endTime: new Date(),
447
+ lastHeartbeat: new Date(),
448
+ },
449
+ $push: {
450
+ callHistory: {
451
+ timestamp: new Date(),
452
+ action: "ended",
453
+ agentId,
454
+ details: "Call ended",
455
+ },
456
+ },
457
+ },
458
+ { new: true }
459
+ );
460
+ return call || null;
461
+ } catch (error) {
462
+ console.error(`Failed to end call ${callId}:`, error);
463
+ return null;
372
464
  }
373
465
  }
374
466
 
@@ -432,49 +524,6 @@ class CallRepository {
432
524
  console.error("Failed to update metrics:", error);
433
525
  }
434
526
  }
435
-
436
- async moveCallToHoldQueue(callId, reason = "Agent disconnected") {
437
- try {
438
- const call = await Call.findOne({ callId });
439
- if (!call) throw new Error(`Call ${callId} not found`);
440
-
441
- const previousAgentId = call.agentId;
442
-
443
- call.agentId = null;
444
- call.status = "hold";
445
- call.isOnHold = true;
446
- call.autoAccepted = true;
447
- call.isHoldConnection = true;
448
- call.priority = Math.min(call.priority + 1, 10);
449
- call.addToHistory("disconnected", previousAgentId, reason);
450
- call.updateHeartbeat();
451
-
452
- await call.save();
453
- console.log(`Call ${callId} moved back to hold queue due to: ${reason}`);
454
- return call;
455
- } catch (error) {
456
- console.error(`Failed to move call ${callId} to hold queue:`, error);
457
- throw error;
458
- }
459
- }
460
-
461
- async endCall(callId, agentId = null) {
462
- try {
463
- const call = await Call.findOne({ callId });
464
- if (!call) throw new Error(`Call ${callId} not found`);
465
-
466
- call.status = "ended";
467
- call.endTime = new Date();
468
- call.duration = call.currentDuration;
469
- call.addToHistory("ended", agentId, "Call ended");
470
-
471
- await call.save();
472
- return call;
473
- } catch (error) {
474
- console.error(`Failed to end call ${callId}:`, error);
475
- throw error;
476
- }
477
- }
478
527
  }
479
528
 
480
529
  module.exports = new CallRepository();
@@ -16,6 +16,7 @@ const customerProfileRepository = require("./customerProfile.repository");
16
16
  const customerTimelineRepository = require("./customerTimeline.repository");
17
17
  const shopRepository = require("./shop.repository");
18
18
  const callRepository = require("./call.repository");
19
+ const interactiveRepository = require("./interactive.repository");
19
20
 
20
21
  exports.module = {
21
22
  workspaceRepository,
@@ -36,4 +37,5 @@ exports.module = {
36
37
  customerTimelineRepository,
37
38
  shopRepository,
38
39
  callRepository,
40
+ interactiveRepository,
39
41
  };
@@ -0,0 +1,124 @@
1
+ // repositories/interactive.repository.js
2
+ const Interactive = require("../../models/Interactive");
3
+ const { getRedisData, setRedisData } = require("../../config/redis");
4
+
5
+ const CACHE_KEY_ALL = "interactive_all";
6
+
7
+ /**
8
+ * Get cached interactive for all workspaces.
9
+ */
10
+ const getCachedAllInteractive = async () => {
11
+ let interactive = await getRedisData(CACHE_KEY_ALL);
12
+ if (!interactive) {
13
+ interactive = await Interactive.find({}).lean().exec();
14
+ await setRedisData(CACHE_KEY_ALL, interactive);
15
+ }
16
+ return interactive;
17
+ };
18
+
19
+ /**
20
+ * Update cached interactive for all workspaces.
21
+ */
22
+ const updateCachedAllInteractive = async () => {
23
+ const interactive = await Interactive.find({}).lean().exec();
24
+ await setRedisData(CACHE_KEY_ALL, interactive);
25
+ };
26
+
27
+ /**
28
+ * Get interactive for a specific workspace from cache.
29
+ */
30
+ const getWorkspaceInteractive = async (workspaceId) => {
31
+ if (!workspaceId) return [];
32
+ const allInteractive = await getCachedAllInteractive();
33
+ return allInteractive.filter(
34
+ (item) => item.workspaceId?.toString() === workspaceId.toString()
35
+ );
36
+ };
37
+ /**
38
+ * Get interactive by ID (cache first).
39
+ */
40
+ const getInteractiveById = async (id) => {
41
+ if (!id) return null;
42
+
43
+ const allInteractive = await getCachedAllInteractive();
44
+ let interactive = allInteractive.find(
45
+ (item) => item._id?.toString() === id.toString()
46
+ );
47
+
48
+ // If not found in cache, fetch from DB and update cache
49
+ if (!interactive) {
50
+ interactive = await Interactive.findById(id).lean().exec();
51
+ if (interactive) {
52
+ await updateCachedAllInteractive();
53
+ }
54
+ }
55
+
56
+ return interactive || null;
57
+ };
58
+
59
+ /**
60
+ * Create new interactive and refresh cache.
61
+ */
62
+ const createInteractive = async (data) => {
63
+ const newInteractive = await Interactive.create(data);
64
+ if (newInteractive) {
65
+ await updateCachedAllInteractive();
66
+ }
67
+ return newInteractive;
68
+ };
69
+
70
+ /**
71
+ * Update interactive by ID and refresh cache.
72
+ */
73
+ const updateInteractiveById = async (id, data) => {
74
+ const updated = await Interactive.findByIdAndUpdate(id, data, {
75
+ new: true,
76
+ }).exec();
77
+ if (updated) {
78
+ await updateCachedAllInteractive();
79
+ }
80
+ return updated;
81
+ };
82
+
83
+ /**
84
+ * Update interactive by filter and refresh cache.
85
+ */
86
+ const updateSingleInteractiveByFilter = async (filter, data) => {
87
+ const updated = await Interactive.findOneAndUpdate(filter, data, {
88
+ new: true,
89
+ }).exec();
90
+ if (updated) {
91
+ await updateCachedAllInteractive();
92
+ }
93
+ return updated;
94
+ };
95
+
96
+ /**
97
+ * Delete interactive by ID and refresh cache.
98
+ */
99
+ const deleteInteractiveById = async (id) => {
100
+ const deleted = await Interactive.findByIdAndDelete(id).exec();
101
+ if (deleted) {
102
+ await updateCachedAllInteractive();
103
+ }
104
+ return deleted;
105
+ };
106
+
107
+ /**
108
+ * Delete interactive by filter and refresh cache.
109
+ */
110
+ const deleteInteractiveByFilter = async (filter) => {
111
+ await Interactive.deleteMany(filter).exec();
112
+ await updateCachedAllInteractive();
113
+ };
114
+
115
+ module.exports = {
116
+ updateCachedAllInteractive,
117
+ getWorkspaceInteractive,
118
+ createInteractive,
119
+ updateInteractiveById,
120
+ updateSingleInteractiveByFilter,
121
+ deleteInteractiveById,
122
+ deleteInteractiveByFilter,
123
+ getInteractiveById,
124
+ };
@@ -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/Call.js CHANGED
@@ -55,7 +55,7 @@ const callSchema = new mongoose.Schema(
55
55
  default: null,
56
56
  },
57
57
  duration: {
58
- type: Number, // in seconds
58
+ type: Number,
59
59
  default: 0,
60
60
  },
61
61
  whatsappSdp: {
@@ -104,6 +104,14 @@ const callSchema = new mongoose.Schema(
104
104
  details: String,
105
105
  },
106
106
  ],
107
+ platformTimestamp: {
108
+ type: String,
109
+ default: "",
110
+ },
111
+ receiver: {
112
+ type: String,
113
+ default: "",
114
+ },
107
115
  platformId: {
108
116
  type: Schema.Types.ObjectId,
109
117
  ref: "Integration",
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
  );
package/models/Chatbot.js CHANGED
@@ -30,6 +30,7 @@ const ChatbotSchema = new Schema(
30
30
  },
31
31
  events: [
32
32
  {
33
+ type: { type: String, default: "" },
33
34
  value: { type: String, default: "" },
34
35
  },
35
36
  ],
@@ -0,0 +1,70 @@
1
+ const { Schema, Types, model } = require("mongoose");
2
+
3
+ const ActionSchema = new Schema({
4
+ type: {
5
+ type: String,
6
+ default: "",
7
+ },
8
+ interactiveId: { type: Types.ObjectId, ref: "Interactive", default: null },
9
+ value: { type: Schema.Types.Mixed, default: "" },
10
+ });
11
+ const InteractiveSchema = new Schema({
12
+ name: { type: String, default: "" },
13
+ workspace: { type: Types.ObjectId, ref: "Workspace", default: null },
14
+ type: {
15
+ type: String,
16
+ enum: ["button", "list"],
17
+ default: "list",
18
+ },
19
+ header: {
20
+ type: {
21
+ type: String,
22
+ enum: ["text", "image", "video"],
23
+ default: "text",
24
+ },
25
+ value: { type: String, default: "" },
26
+ },
27
+ body: {
28
+ type: {
29
+ type: String,
30
+ default: "text",
31
+ },
32
+ value: { type: String, default: "" },
33
+ },
34
+ footer: {
35
+ type: {
36
+ type: String,
37
+ default: "text",
38
+ },
39
+ value: { type: String, default: "" },
40
+ },
41
+ buttonTitle: {
42
+ type: String,
43
+ default: "Menu",
44
+ },
45
+ sections: [
46
+ {
47
+ title: { type: String, default: "" },
48
+ rows: [
49
+ {
50
+ id: { type: String, default: "" },
51
+ title: { type: String, default: "" },
52
+ description: { type: String, default: "" },
53
+ action: ActionSchema,
54
+ },
55
+ ],
56
+ },
57
+ ],
58
+ buttons: [
59
+ {
60
+ type: { type: String, default: "reply" },
61
+ reply: {
62
+ id: { type: String, default: "" },
63
+ title: { type: String, default: "" },
64
+ action: ActionSchema,
65
+ },
66
+ },
67
+ ],
68
+ });
69
+
70
+ module.exports = model("Interactive", InteractiveSchema);
package/models.js CHANGED
@@ -43,6 +43,7 @@ const FormTemplate = require("./models/FormTemplate");
43
43
  const GoogleClientInfo = require("./models/GoogleClientInfo");
44
44
  const GoogleCredentials = require("./models/GoogleCredentials");
45
45
  const Integration = require("./models/Integration");
46
+ const Interactive = require("./models/Interactive");
46
47
  const InternalComments = require("./models/InternalComments");
47
48
  const InternalThreads = require("./models/InternalThreads");
48
49
  const JobDesign = require("./models/JobDesign");
@@ -152,6 +153,7 @@ module.exports = {
152
153
  GoogleClientInfo,
153
154
  GoogleCredentials,
154
155
  Integration,
156
+ Interactive,
155
157
  InternalComments,
156
158
  InternalThreads,
157
159
  JobDesign,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shuttlepro-shared",
3
- "version": "1.4.02",
3
+ "version": "1.4.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {