@astralibx/call-log-engine 0.2.0 → 0.3.0

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/dist/index.cjs CHANGED
@@ -3,18 +3,18 @@
3
3
  var zod = require('zod');
4
4
  var core = require('@astralibx/core');
5
5
  var callLogTypes = require('@astralibx/call-log-types');
6
- var crypto5 = require('crypto');
6
+ var crypto6 = require('crypto');
7
7
  var mongoose = require('mongoose');
8
8
  var express = require('express');
9
9
 
10
10
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
11
 
12
- var crypto5__default = /*#__PURE__*/_interopDefault(crypto5);
12
+ var crypto6__default = /*#__PURE__*/_interopDefault(crypto6);
13
13
 
14
14
  // src/index.ts
15
15
  var PipelineStageSchema = new mongoose.Schema(
16
16
  {
17
- stageId: { type: String, required: true, default: () => crypto5__default.default.randomUUID() },
17
+ stageId: { type: String, required: true, default: () => crypto6__default.default.randomUUID() },
18
18
  name: { type: String, required: true },
19
19
  color: { type: String, required: true },
20
20
  order: { type: Number, required: true },
@@ -25,7 +25,7 @@ var PipelineStageSchema = new mongoose.Schema(
25
25
  );
26
26
  var PipelineSchema = new mongoose.Schema(
27
27
  {
28
- pipelineId: { type: String, required: true, unique: true, default: () => crypto5__default.default.randomUUID() },
28
+ pipelineId: { type: String, required: true, unique: true, default: () => crypto6__default.default.randomUUID() },
29
29
  name: { type: String, required: true },
30
30
  description: { type: String },
31
31
  stages: { type: [PipelineStageSchema], required: true },
@@ -55,7 +55,7 @@ var ContactRefSchema = new mongoose.Schema(
55
55
  );
56
56
  var TimelineEntrySchema = new mongoose.Schema(
57
57
  {
58
- entryId: { type: String, required: true, default: () => crypto5__default.default.randomUUID() },
58
+ entryId: { type: String, required: true, default: () => crypto6__default.default.randomUUID() },
59
59
  type: {
60
60
  type: String,
61
61
  required: true,
@@ -90,7 +90,7 @@ var StageChangeSchema = new mongoose.Schema(
90
90
  );
91
91
  var CallLogSchema = new mongoose.Schema(
92
92
  {
93
- callLogId: { type: String, required: true, unique: true, default: () => crypto5__default.default.randomUUID() },
93
+ callLogId: { type: String, required: true, unique: true, default: () => crypto6__default.default.randomUUID() },
94
94
  pipelineId: { type: String, required: true, index: true },
95
95
  currentStageId: { type: String, required: true, index: true },
96
96
  contactRef: { type: ContactRefSchema, required: true },
@@ -115,6 +115,11 @@ var CallLogSchema = new mongoose.Schema(
115
115
  timeline: { type: [TimelineEntrySchema], default: [] },
116
116
  stageHistory: { type: [StageChangeSchema], default: [] },
117
117
  durationMinutes: { type: Number, min: 0 },
118
+ channel: { type: String, default: "phone", index: true },
119
+ outcome: { type: String, default: "pending", index: true },
120
+ isFollowUp: { type: Boolean, default: false, index: true },
121
+ isDeleted: { type: Boolean, default: false },
122
+ deletedAt: { type: Date },
118
123
  isClosed: { type: Boolean, default: false, index: true },
119
124
  closedAt: { type: Date },
120
125
  tenantId: { type: String, sparse: true },
@@ -127,6 +132,7 @@ var CallLogSchema = new mongoose.Schema(
127
132
  CallLogSchema.index({ "contactRef.externalId": 1 });
128
133
  CallLogSchema.index({ pipelineId: 1, currentStageId: 1 });
129
134
  CallLogSchema.index({ agentId: 1, isClosed: 1 });
135
+ CallLogSchema.index({ isDeleted: 1, isClosed: 1 });
130
136
  function createCallLogModel(connection, prefix) {
131
137
  const collectionName = prefix ? `${prefix}_call_logs` : "call_logs";
132
138
  return connection.model("CallLog", CallLogSchema, collectionName);
@@ -140,6 +146,7 @@ var ERROR_CODE = {
140
146
  StageInUse: "CALL_STAGE_IN_USE",
141
147
  CallLogNotFound: "CALL_LOG_NOT_FOUND",
142
148
  CallLogClosed: "CALL_LOG_CLOSED",
149
+ CallLogDeleted: "CALL_LOG_DELETED",
143
150
  ContactNotFound: "CALL_CONTACT_NOT_FOUND",
144
151
  AgentCapacityFull: "CALL_AGENT_CAPACITY_FULL",
145
152
  InvalidConfig: "CALL_INVALID_CONFIG",
@@ -154,6 +161,7 @@ var ERROR_MESSAGE = {
154
161
  StageInUse: "Cannot remove stage that has active calls",
155
162
  CallLogNotFound: "Call log not found",
156
163
  CallLogClosed: "Cannot modify a closed call log",
164
+ CallLogDeleted: "Call log has been deleted",
157
165
  ContactNotFound: "Contact not found",
158
166
  AgentCapacityFull: "Agent has reached maximum concurrent calls",
159
167
  AuthFailed: "Authentication failed"
@@ -179,7 +187,9 @@ var SYSTEM_TIMELINE_FN = {
179
187
  stageChanged: (from, to) => `Stage changed from "${from}" to "${to}"`,
180
188
  callAssigned: (agentName) => `Call assigned to ${agentName}`,
181
189
  callReassigned: (from, to) => `Call reassigned from ${from} to ${to}`,
182
- followUpSet: (date) => `Follow-up scheduled for ${date}`
190
+ followUpSet: (date) => `Follow-up scheduled for ${date}`,
191
+ callDeleted: (agentName) => `Call deleted by ${agentName}`,
192
+ followUpCallCreated: () => "This call is a follow-up to a previous interaction"
183
193
  };
184
194
 
185
195
  // src/schemas/call-log-settings.schema.ts
@@ -203,6 +213,8 @@ var CallLogSettingsSchema = new mongoose.Schema(
203
213
  key: { type: String, required: true, default: "global" },
204
214
  availableTags: { type: [String], default: [] },
205
215
  availableCategories: { type: [String], default: [] },
216
+ availableChannels: { type: [String], default: ["phone", "whatsapp", "telegram", "in_app_chat"] },
217
+ availableOutcomes: { type: [String], default: ["pending", "interested", "not_interested", "no_answer", "busy", "callback_requested", "subscribed", "complaint"] },
206
218
  priorityLevels: {
207
219
  type: [PriorityConfigSchema],
208
220
  default: () => DEFAULT_PRIORITY_LEVELS.map((p) => ({ ...p }))
@@ -465,10 +477,10 @@ var PipelineService = class {
465
477
  async create(data) {
466
478
  const stages = data.stages.map((s) => ({
467
479
  ...s,
468
- stageId: crypto5__default.default.randomUUID()
480
+ stageId: crypto6__default.default.randomUUID()
469
481
  }));
470
482
  validatePipelineStages(stages);
471
- const pipelineId = crypto5__default.default.randomUUID();
483
+ const pipelineId = crypto6__default.default.randomUUID();
472
484
  if (data.isDefault) {
473
485
  await this.Pipeline.updateMany(
474
486
  { ...this.tenantFilter, isDefault: true },
@@ -537,7 +549,7 @@ var PipelineService = class {
537
549
  const pipeline = await this.get(pipelineId);
538
550
  const newStage = {
539
551
  ...stage,
540
- stageId: crypto5__default.default.randomUUID()
552
+ stageId: crypto6__default.default.randomUUID()
541
553
  };
542
554
  const updatedStages = [...pipeline.stages, newStage];
543
555
  validatePipelineStages(updatedStages, pipelineId);
@@ -636,7 +648,7 @@ var TimelineService = class {
636
648
  if (!callLog) throw new CallLogNotFoundError(callLogId);
637
649
  if (callLog.isClosed) throw new CallLogClosedError(callLogId, "add note");
638
650
  const entry = {
639
- entryId: crypto5__default.default.randomUUID(),
651
+ entryId: crypto6__default.default.randomUUID(),
640
652
  type: callLogTypes.TimelineEntryType.Note,
641
653
  content,
642
654
  authorId,
@@ -661,7 +673,7 @@ var TimelineService = class {
661
673
  const callLog = await this.CallLog.findOne({ callLogId });
662
674
  if (!callLog) throw new CallLogNotFoundError(callLogId);
663
675
  const entry = {
664
- entryId: crypto5__default.default.randomUUID(),
676
+ entryId: crypto6__default.default.randomUUID(),
665
677
  type: callLogTypes.TimelineEntryType.System,
666
678
  content,
667
679
  createdAt: /* @__PURE__ */ new Date()
@@ -730,13 +742,23 @@ var CallLogService = class {
730
742
  if (!pipeline) throw new PipelineNotFoundError(data.pipelineId);
731
743
  const defaultStage = pipeline.stages.find((s) => s.isDefault);
732
744
  if (!defaultStage) throw new PipelineNotFoundError(data.pipelineId);
733
- const callLogId = crypto5__default.default.randomUUID();
745
+ const callLogId = crypto6__default.default.randomUUID();
734
746
  const initialEntry = {
735
- entryId: crypto5__default.default.randomUUID(),
747
+ entryId: crypto6__default.default.randomUUID(),
736
748
  type: callLogTypes.TimelineEntryType.System,
737
749
  content: SYSTEM_TIMELINE.CallCreated,
738
750
  createdAt: /* @__PURE__ */ new Date()
739
751
  };
752
+ const timelineEntries = [initialEntry];
753
+ if (data.isFollowUp) {
754
+ const followUpEntry = {
755
+ entryId: crypto6__default.default.randomUUID(),
756
+ type: callLogTypes.TimelineEntryType.System,
757
+ content: SYSTEM_TIMELINE_FN.followUpCallCreated(),
758
+ createdAt: /* @__PURE__ */ new Date()
759
+ };
760
+ timelineEntries.push(followUpEntry);
761
+ }
740
762
  const callLog = await this.CallLog.create({
741
763
  callLogId,
742
764
  pipelineId: data.pipelineId,
@@ -750,7 +772,10 @@ var CallLogService = class {
750
772
  category: data.category,
751
773
  nextFollowUpDate: data.nextFollowUpDate,
752
774
  durationMinutes: data.durationMinutes,
753
- timeline: [initialEntry],
775
+ channel: data.channel,
776
+ outcome: data.outcome,
777
+ isFollowUp: data.isFollowUp ?? false,
778
+ timeline: timelineEntries,
754
779
  stageHistory: [],
755
780
  isClosed: false,
756
781
  ...data.tenantId ? { tenantId: data.tenantId } : {},
@@ -772,6 +797,9 @@ var CallLogService = class {
772
797
  if (data.tags !== void 0) setFields.tags = data.tags;
773
798
  if (data.category !== void 0) setFields.category = data.category;
774
799
  if (data.durationMinutes !== void 0) setFields.durationMinutes = data.durationMinutes;
800
+ if (data.channel !== void 0) setFields.channel = data.channel;
801
+ if (data.outcome !== void 0) setFields.outcome = data.outcome;
802
+ if (data.isFollowUp !== void 0) setFields.isFollowUp = data.isFollowUp;
775
803
  if (data.nextFollowUpDate !== void 0) {
776
804
  const prevDate = callLog.nextFollowUpDate?.toISOString();
777
805
  const newDate = data.nextFollowUpDate?.toISOString();
@@ -779,7 +807,7 @@ var CallLogService = class {
779
807
  setFields.nextFollowUpDate = data.nextFollowUpDate;
780
808
  if (data.nextFollowUpDate) {
781
809
  const followUpEntry = {
782
- entryId: crypto5__default.default.randomUUID(),
810
+ entryId: crypto6__default.default.randomUUID(),
783
811
  type: callLogTypes.TimelineEntryType.FollowUpSet,
784
812
  content: SYSTEM_TIMELINE_FN.followUpSet(new Date(data.nextFollowUpDate).toISOString()),
785
813
  createdAt: /* @__PURE__ */ new Date()
@@ -806,6 +834,58 @@ var CallLogService = class {
806
834
  this.logger.info("Call log updated", { callLogId, fields: Object.keys(data) });
807
835
  return updated;
808
836
  }
837
+ async list(filter = {}) {
838
+ const query = {};
839
+ if (filter.pipelineId) query.pipelineId = filter.pipelineId;
840
+ if (filter.currentStageId) query.currentStageId = filter.currentStageId;
841
+ if (filter.agentId) query.agentId = filter.agentId;
842
+ if (filter.tags && filter.tags.length > 0) query.tags = { $in: filter.tags };
843
+ if (filter.category) query.category = filter.category;
844
+ if (filter.isClosed !== void 0) query.isClosed = filter.isClosed;
845
+ if (filter.contactExternalId) query["contactRef.externalId"] = filter.contactExternalId;
846
+ if (filter.contactName) query["contactRef.displayName"] = { $regex: filter.contactName, $options: "i" };
847
+ if (filter.contactPhone) query["contactRef.phone"] = { $regex: `^${filter.contactPhone}` };
848
+ if (filter.contactEmail) query["contactRef.email"] = filter.contactEmail;
849
+ if (filter.priority) query.priority = filter.priority;
850
+ if (filter.direction) query.direction = filter.direction;
851
+ if (filter.channel) query.channel = filter.channel;
852
+ if (filter.outcome) query.outcome = filter.outcome;
853
+ if (filter.isFollowUp !== void 0) query.isFollowUp = filter.isFollowUp;
854
+ if (!filter.includeDeleted) query.isDeleted = { $ne: true };
855
+ if (filter.dateRange?.from || filter.dateRange?.to) {
856
+ const dateFilter = {};
857
+ if (filter.dateRange.from) dateFilter.$gte = new Date(filter.dateRange.from);
858
+ if (filter.dateRange.to) dateFilter.$lte = new Date(filter.dateRange.to);
859
+ query.callDate = dateFilter;
860
+ }
861
+ const page = filter.page ?? 1;
862
+ const limit = filter.limit ?? 20;
863
+ const skip = (page - 1) * limit;
864
+ const sort = filter.sort ?? { callDate: -1 };
865
+ const [callLogs, total] = await Promise.all([
866
+ this.CallLog.find(query).sort(sort).skip(skip).limit(limit),
867
+ this.CallLog.countDocuments(query)
868
+ ]);
869
+ return { callLogs, total, page, limit };
870
+ }
871
+ async get(callLogId) {
872
+ const callLog = await this.CallLog.findOne({ callLogId, isDeleted: { $ne: true } });
873
+ if (!callLog) throw new CallLogNotFoundError(callLogId);
874
+ return callLog;
875
+ }
876
+ async getByContact(externalId) {
877
+ return this.CallLog.find({ "contactRef.externalId": externalId, isDeleted: { $ne: true } }).sort({ callDate: -1 });
878
+ }
879
+ };
880
+ var CallLogLifecycleService = class {
881
+ constructor(CallLog, Pipeline, timeline, logger, hooks, options) {
882
+ this.CallLog = CallLog;
883
+ this.Pipeline = Pipeline;
884
+ this.timeline = timeline;
885
+ this.logger = logger;
886
+ this.hooks = hooks;
887
+ this.options = options;
888
+ }
809
889
  async changeStage(callLogId, newStageId, agentId) {
810
890
  const callLog = await this.CallLog.findOne({ callLogId });
811
891
  if (!callLog) throw new CallLogNotFoundError(callLogId);
@@ -822,7 +902,7 @@ var CallLogService = class {
822
902
  const stageStartTime = lastHistory?.changedAt?.getTime() ?? callLog.createdAt?.getTime() ?? now.getTime();
823
903
  const timeInStageMs = now.getTime() - stageStartTime;
824
904
  const stageChangeEntry = {
825
- entryId: crypto5__default.default.randomUUID(),
905
+ entryId: crypto6__default.default.randomUUID(),
826
906
  type: callLogTypes.TimelineEntryType.StageChange,
827
907
  content: SYSTEM_TIMELINE_FN.stageChanged(currentStage.name, newStage.name),
828
908
  fromStageId: currentStageId,
@@ -871,7 +951,7 @@ var CallLogService = class {
871
951
  if (!callLog) throw new CallLogNotFoundError(callLogId);
872
952
  const previousAgentId = callLog.agentId?.toString();
873
953
  const assignmentEntry = {
874
- entryId: crypto5__default.default.randomUUID(),
954
+ entryId: crypto6__default.default.randomUUID(),
875
955
  type: callLogTypes.TimelineEntryType.Assignment,
876
956
  content: previousAgentId ? SYSTEM_TIMELINE_FN.callReassigned(previousAgentId, agentId) : SYSTEM_TIMELINE_FN.callAssigned(agentId),
877
957
  toAgentId: agentId,
@@ -899,88 +979,13 @@ var CallLogService = class {
899
979
  }
900
980
  return updated;
901
981
  }
902
- async list(filter = {}) {
903
- const query = {};
904
- if (filter.pipelineId) query.pipelineId = filter.pipelineId;
905
- if (filter.currentStageId) query.currentStageId = filter.currentStageId;
906
- if (filter.agentId) query.agentId = filter.agentId;
907
- if (filter.tags && filter.tags.length > 0) query.tags = { $in: filter.tags };
908
- if (filter.category) query.category = filter.category;
909
- if (filter.isClosed !== void 0) query.isClosed = filter.isClosed;
910
- if (filter.contactExternalId) query["contactRef.externalId"] = filter.contactExternalId;
911
- if (filter.contactName) query["contactRef.displayName"] = { $regex: filter.contactName, $options: "i" };
912
- if (filter.contactPhone) query["contactRef.phone"] = { $regex: `^${filter.contactPhone}` };
913
- if (filter.contactEmail) query["contactRef.email"] = filter.contactEmail;
914
- if (filter.priority) query.priority = filter.priority;
915
- if (filter.direction) query.direction = filter.direction;
916
- if (filter.dateRange?.from || filter.dateRange?.to) {
917
- const dateFilter = {};
918
- if (filter.dateRange.from) dateFilter.$gte = new Date(filter.dateRange.from);
919
- if (filter.dateRange.to) dateFilter.$lte = new Date(filter.dateRange.to);
920
- query.callDate = dateFilter;
921
- }
922
- const page = filter.page ?? 1;
923
- const limit = filter.limit ?? 20;
924
- const skip = (page - 1) * limit;
925
- const sort = filter.sort ?? { callDate: -1 };
926
- const [callLogs, total] = await Promise.all([
927
- this.CallLog.find(query).sort(sort).skip(skip).limit(limit),
928
- this.CallLog.countDocuments(query)
929
- ]);
930
- return { callLogs, total, page, limit };
931
- }
932
- async get(callLogId) {
933
- const callLog = await this.CallLog.findOne({ callLogId });
934
- if (!callLog) throw new CallLogNotFoundError(callLogId);
935
- return callLog;
936
- }
937
- async getByContact(externalId) {
938
- return this.CallLog.find({ "contactRef.externalId": externalId }).sort({ callDate: -1 });
939
- }
940
- async getFollowUpsDue(agentId, dateRange) {
941
- const now = /* @__PURE__ */ new Date();
942
- const query = {
943
- nextFollowUpDate: { $lte: now },
944
- isClosed: false,
945
- followUpNotifiedAt: null
946
- };
947
- if (agentId) query.agentId = agentId;
948
- if (dateRange?.from || dateRange?.to) {
949
- const dateFilter = {};
950
- if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
951
- if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
952
- query.nextFollowUpDate = { ...query.nextFollowUpDate, ...dateFilter };
953
- }
954
- return this.CallLog.find(query).sort({ nextFollowUpDate: 1 });
955
- }
956
- async bulkChangeStage(callLogIds, newStageId, agentId) {
957
- const succeeded = [];
958
- const failed = [];
959
- for (const callLogId of callLogIds) {
960
- try {
961
- await this.changeStage(callLogId, newStageId, agentId);
962
- succeeded.push(callLogId);
963
- } catch (err) {
964
- failed.push({
965
- callLogId,
966
- error: err instanceof Error ? err.message : String(err)
967
- });
968
- }
969
- }
970
- this.logger.info("Bulk stage change completed", {
971
- total: callLogIds.length,
972
- succeeded: succeeded.length,
973
- failed: failed.length
974
- });
975
- return { succeeded, failed, total: callLogIds.length };
976
- }
977
982
  async close(callLogId, agentId) {
978
983
  const callLog = await this.CallLog.findOne({ callLogId });
979
984
  if (!callLog) throw new CallLogNotFoundError(callLogId);
980
985
  if (callLog.isClosed) throw new CallLogClosedError(callLogId, "close");
981
986
  const now = /* @__PURE__ */ new Date();
982
987
  const closeEntry = {
983
- entryId: crypto5__default.default.randomUUID(),
988
+ entryId: crypto6__default.default.randomUUID(),
984
989
  type: callLogTypes.TimelineEntryType.System,
985
990
  content: SYSTEM_TIMELINE.CallClosed,
986
991
  authorId: agentId,
@@ -1012,7 +1017,7 @@ var CallLogService = class {
1012
1017
  if (!callLog.isClosed) throw new CallLogClosedError(callLogId, "reopen");
1013
1018
  const now = /* @__PURE__ */ new Date();
1014
1019
  const reopenEntry = {
1015
- entryId: crypto5__default.default.randomUUID(),
1020
+ entryId: crypto6__default.default.randomUUID(),
1016
1021
  type: callLogTypes.TimelineEntryType.System,
1017
1022
  content: SYSTEM_TIMELINE.CallReopened,
1018
1023
  authorId: agentId,
@@ -1035,6 +1040,72 @@ var CallLogService = class {
1035
1040
  this.logger.info("Call log reopened", { callLogId, agentId });
1036
1041
  return updated;
1037
1042
  }
1043
+ async bulkChangeStage(callLogIds, newStageId, agentId) {
1044
+ const succeeded = [];
1045
+ const failed = [];
1046
+ for (const callLogId of callLogIds) {
1047
+ try {
1048
+ await this.changeStage(callLogId, newStageId, agentId);
1049
+ succeeded.push(callLogId);
1050
+ } catch (err) {
1051
+ failed.push({
1052
+ callLogId,
1053
+ error: err instanceof Error ? err.message : String(err)
1054
+ });
1055
+ }
1056
+ }
1057
+ this.logger.info("Bulk stage change completed", {
1058
+ total: callLogIds.length,
1059
+ succeeded: succeeded.length,
1060
+ failed: failed.length
1061
+ });
1062
+ return { succeeded, failed, total: callLogIds.length };
1063
+ }
1064
+ async getFollowUpsDue(agentId, dateRange) {
1065
+ const now = /* @__PURE__ */ new Date();
1066
+ const query = {
1067
+ nextFollowUpDate: { $lte: now },
1068
+ isClosed: false,
1069
+ followUpNotifiedAt: null,
1070
+ isDeleted: { $ne: true }
1071
+ };
1072
+ if (agentId) query.agentId = agentId;
1073
+ if (dateRange?.from || dateRange?.to) {
1074
+ const dateFilter = {};
1075
+ if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
1076
+ if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
1077
+ query.nextFollowUpDate = { ...query.nextFollowUpDate, ...dateFilter };
1078
+ }
1079
+ return this.CallLog.find(query).sort({ nextFollowUpDate: 1 });
1080
+ }
1081
+ async softDelete(callLogId, agentId, agentName) {
1082
+ const callLog = await this.CallLog.findOne({ callLogId });
1083
+ if (!callLog) throw new CallLogNotFoundError(callLogId);
1084
+ const now = /* @__PURE__ */ new Date();
1085
+ const deleteEntry = {
1086
+ entryId: crypto6__default.default.randomUUID(),
1087
+ type: callLogTypes.TimelineEntryType.System,
1088
+ content: agentName ? SYSTEM_TIMELINE_FN.callDeleted(agentName) : SYSTEM_TIMELINE_FN.callDeleted(agentId ?? "unknown"),
1089
+ authorId: agentId,
1090
+ createdAt: now
1091
+ };
1092
+ const updated = await this.CallLog.findOneAndUpdate(
1093
+ { callLogId },
1094
+ {
1095
+ $set: { isDeleted: true, deletedAt: now },
1096
+ $push: {
1097
+ timeline: {
1098
+ $each: [deleteEntry],
1099
+ $slice: -this.options.maxTimelineEntries
1100
+ }
1101
+ }
1102
+ },
1103
+ { new: true }
1104
+ );
1105
+ if (!updated) throw new CallLogNotFoundError(callLogId);
1106
+ this.logger.info("Call log soft deleted", { callLogId });
1107
+ return updated;
1108
+ }
1038
1109
  };
1039
1110
 
1040
1111
  // src/services/analytics.service.ts
@@ -1065,6 +1136,7 @@ var AnalyticsService = class {
1065
1136
  const now = /* @__PURE__ */ new Date();
1066
1137
  const matchStage = {
1067
1138
  agentId,
1139
+ isDeleted: { $ne: true },
1068
1140
  ...this.buildDateMatch(dateRange)
1069
1141
  };
1070
1142
  const pipeline = [
@@ -1157,7 +1229,10 @@ var AnalyticsService = class {
1157
1229
  }
1158
1230
  async getAgentLeaderboard(dateRange) {
1159
1231
  const now = /* @__PURE__ */ new Date();
1160
- const matchStage = this.buildDateMatch(dateRange);
1232
+ const matchStage = {
1233
+ isDeleted: { $ne: true },
1234
+ ...this.buildDateMatch(dateRange)
1235
+ };
1161
1236
  const pipeline = [
1162
1237
  { $match: matchStage },
1163
1238
  {
@@ -1213,103 +1288,12 @@ var AnalyticsService = class {
1213
1288
  closeRate: stat.totalCalls > 0 ? Math.round(stat.callsClosed / stat.totalCalls * 1e4) / 100 : 0
1214
1289
  })));
1215
1290
  }
1216
- async getPipelineStats(pipelineId, dateRange) {
1217
- const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
1218
- const pipelineName = pipeline?.name ?? pipelineId;
1219
- const stages = pipeline?.stages ?? [];
1220
- const matchStage = {
1221
- pipelineId,
1222
- ...this.buildDateMatch(dateRange)
1223
- };
1224
- const [totalResult, stageAgg] = await Promise.all([
1225
- this.CallLog.countDocuments(matchStage),
1226
- this.CallLog.aggregate([
1227
- { $match: matchStage },
1228
- { $unwind: { path: "$stageHistory", preserveNullAndEmptyArrays: false } },
1229
- {
1230
- $group: {
1231
- _id: "$stageHistory.toStageId",
1232
- count: { $sum: 1 },
1233
- totalTimeMs: { $sum: "$stageHistory.timeInStageMs" }
1234
- }
1235
- },
1236
- {
1237
- $project: {
1238
- stageId: "$_id",
1239
- count: 1,
1240
- totalTimeMs: 1
1241
- }
1242
- }
1243
- ])
1244
- ]);
1245
- const totalCalls = totalResult;
1246
- const stageMap = new Map(stageAgg.map((s) => [s.stageId, s]));
1247
- let bottleneckStage = null;
1248
- let maxAvgTime = 0;
1249
- const stageStats = stages.map((s) => {
1250
- const agg = stageMap.get(s.stageId);
1251
- const count = agg?.count ?? 0;
1252
- const avgTimeMs = count > 0 ? Math.round((agg?.totalTimeMs ?? 0) / count) : 0;
1253
- const conversionRate = totalCalls > 0 ? Math.round(count / totalCalls * 1e4) / 100 : 0;
1254
- if (avgTimeMs > maxAvgTime) {
1255
- maxAvgTime = avgTimeMs;
1256
- bottleneckStage = s.stageId;
1257
- }
1258
- return {
1259
- stageId: s.stageId,
1260
- stageName: s.name,
1261
- count,
1262
- avgTimeMs,
1263
- conversionRate
1264
- };
1265
- });
1266
- this.logger.info("Pipeline stats computed", { pipelineId, totalCalls });
1267
- return {
1268
- pipelineId,
1269
- pipelineName,
1270
- totalCalls,
1271
- stages: stageStats,
1272
- bottleneckStage
1273
- };
1274
- }
1275
- async getPipelineFunnel(pipelineId, dateRange) {
1276
- const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
1277
- const pipelineName = pipeline?.name ?? pipelineId;
1278
- const stages = pipeline?.stages ?? [];
1291
+ async getTeamStats(teamId, dateRange = {}) {
1292
+ const now = /* @__PURE__ */ new Date();
1279
1293
  const matchStage = {
1280
- pipelineId,
1294
+ isDeleted: { $ne: true },
1281
1295
  ...this.buildDateMatch(dateRange)
1282
1296
  };
1283
- const [enteredAgg, exitedAgg] = await Promise.all([
1284
- this.CallLog.aggregate([
1285
- { $match: matchStage },
1286
- { $unwind: "$stageHistory" },
1287
- { $group: { _id: "$stageHistory.toStageId", count: { $sum: 1 } } }
1288
- ]),
1289
- this.CallLog.aggregate([
1290
- { $match: matchStage },
1291
- { $unwind: "$stageHistory" },
1292
- { $group: { _id: "$stageHistory.fromStageId", count: { $sum: 1 } } }
1293
- ])
1294
- ]);
1295
- const enteredMap = new Map(enteredAgg.map((r) => [String(r._id), r.count]));
1296
- const exitedMap = new Map(exitedAgg.map((r) => [String(r._id), r.count]));
1297
- const funnelStages = stages.map((s) => {
1298
- const entered = enteredMap.get(s.stageId) ?? 0;
1299
- const exited = exitedMap.get(s.stageId) ?? 0;
1300
- return {
1301
- stageId: s.stageId,
1302
- stageName: s.name,
1303
- entered,
1304
- exited,
1305
- dropOff: Math.max(0, entered - exited)
1306
- };
1307
- });
1308
- return { pipelineId, pipelineName, stages: funnelStages };
1309
- }
1310
- async getTeamStats(teamId, dateRange = {}) {
1311
- const now = /* @__PURE__ */ new Date();
1312
- const matchStage = this.buildDateMatch(dateRange);
1313
1297
  const pipeline = [
1314
1298
  { $match: matchStage },
1315
1299
  {
@@ -1366,8 +1350,12 @@ var AnalyticsService = class {
1366
1350
  const totalCalls = agentStats.reduce((sum, a) => sum + a.totalCalls, 0);
1367
1351
  return { teamId: teamId ?? null, agentStats, totalCalls };
1368
1352
  }
1369
- async getDailyReport(dateRange) {
1370
- const matchStage = this.buildDateMatch(dateRange);
1353
+ async getDailyReport(dateRange, agentId) {
1354
+ const matchStage = {
1355
+ isDeleted: { $ne: true },
1356
+ ...this.buildDateMatch(dateRange)
1357
+ };
1358
+ if (agentId) matchStage["agentId"] = agentId;
1371
1359
  const [dailyAgg, directionAgg, pipelineAgg, agentAgg] = await Promise.all([
1372
1360
  this.CallLog.aggregate([
1373
1361
  { $match: matchStage },
@@ -1447,13 +1435,14 @@ var AnalyticsService = class {
1447
1435
  const now = /* @__PURE__ */ new Date();
1448
1436
  const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1449
1437
  const [openCalls, closedToday, overdueFollowUps, callsToday] = await Promise.all([
1450
- this.CallLog.countDocuments({ isClosed: false }),
1451
- this.CallLog.countDocuments({ closedAt: { $gte: midnight } }),
1438
+ this.CallLog.countDocuments({ isClosed: false, isDeleted: { $ne: true } }),
1439
+ this.CallLog.countDocuments({ closedAt: { $gte: midnight }, isDeleted: { $ne: true } }),
1452
1440
  this.CallLog.countDocuments({
1453
1441
  nextFollowUpDate: { $lt: now },
1454
- isClosed: false
1442
+ isClosed: false,
1443
+ isDeleted: { $ne: true }
1455
1444
  }),
1456
- this.CallLog.countDocuments({ callDate: { $gte: midnight } })
1445
+ this.CallLog.countDocuments({ callDate: { $gte: midnight }, isDeleted: { $ne: true } })
1457
1446
  ]);
1458
1447
  return {
1459
1448
  openCalls,
@@ -1467,7 +1456,7 @@ var AnalyticsService = class {
1467
1456
  const now = /* @__PURE__ */ new Date();
1468
1457
  const from = new Date(now.getTime() - weeks * 7 * 24 * 60 * 60 * 1e3);
1469
1458
  const results = await this.CallLog.aggregate([
1470
- { $match: { callDate: { $gte: from, $lte: now } } },
1459
+ { $match: { callDate: { $gte: from, $lte: now }, isDeleted: { $ne: true } } },
1471
1460
  {
1472
1461
  $group: {
1473
1462
  _id: {
@@ -1494,8 +1483,11 @@ var AnalyticsService = class {
1494
1483
  return results;
1495
1484
  }
1496
1485
  async getOverallReport(dateRange) {
1497
- const matchStage = this.buildDateMatch(dateRange);
1498
- const [summaryAgg, tagAgg, categoryAgg, peakHoursAgg, followUpAgg] = await Promise.all([
1486
+ const matchStage = {
1487
+ isDeleted: { $ne: true },
1488
+ ...this.buildDateMatch(dateRange)
1489
+ };
1490
+ const [summaryAgg, tagAgg, categoryAgg, peakHoursAgg, followUpAgg, channelAgg, outcomeAgg] = await Promise.all([
1499
1491
  this.CallLog.aggregate([
1500
1492
  { $match: matchStage },
1501
1493
  {
@@ -1540,14 +1532,14 @@ var AnalyticsService = class {
1540
1532
  _id: null,
1541
1533
  total: { $sum: 1 },
1542
1534
  withFollowUp: {
1543
- $sum: { $cond: [{ $ne: ["$nextFollowUpDate", null] }, 1, 0] }
1535
+ $sum: { $cond: [{ $eq: ["$isFollowUp", true] }, 1, 0] }
1544
1536
  },
1545
1537
  completed: {
1546
1538
  $sum: {
1547
1539
  $cond: [
1548
1540
  {
1549
1541
  $and: [
1550
- { $ne: ["$nextFollowUpDate", null] },
1542
+ { $eq: ["$isFollowUp", true] },
1551
1543
  { $eq: ["$isClosed", true] }
1552
1544
  ]
1553
1545
  },
@@ -1558,11 +1550,31 @@ var AnalyticsService = class {
1558
1550
  }
1559
1551
  }
1560
1552
  }
1553
+ ]),
1554
+ this.CallLog.aggregate([
1555
+ { $match: matchStage },
1556
+ { $group: { _id: "$channel", count: { $sum: 1 } } },
1557
+ { $sort: { count: -1 } }
1558
+ ]),
1559
+ this.CallLog.aggregate([
1560
+ { $match: matchStage },
1561
+ { $group: { _id: "$outcome", count: { $sum: 1 } } },
1562
+ { $sort: { count: -1 } }
1561
1563
  ])
1562
1564
  ]);
1563
1565
  const summary = summaryAgg[0] ?? { totalCalls: 0, closedCalls: 0, avgTimeToCloseMs: 0 };
1564
- const followUp = followUpAgg[0] ?? { withFollowUp: 0, completed: 0 };
1566
+ const followUp = followUpAgg[0] ?? { total: 0, withFollowUp: 0, completed: 0 };
1565
1567
  const followUpComplianceRate = followUp.withFollowUp > 0 ? Math.round(followUp.completed / followUp.withFollowUp * 1e4) / 100 : 0;
1568
+ const followUpCalls = followUp.withFollowUp;
1569
+ const followUpRatio = followUp.total > 0 ? Math.round(followUp.withFollowUp / followUp.total * 1e4) / 100 : 0;
1570
+ const channelDistribution = channelAgg.map((r) => ({
1571
+ channel: r._id != null ? String(r._id) : "unknown",
1572
+ count: r.count
1573
+ }));
1574
+ const outcomeDistribution = outcomeAgg.map((r) => ({
1575
+ outcome: r._id != null ? String(r._id) : "unknown",
1576
+ count: r.count
1577
+ }));
1566
1578
  return {
1567
1579
  totalCalls: summary.totalCalls,
1568
1580
  closedCalls: summary.closedCalls,
@@ -1570,7 +1582,180 @@ var AnalyticsService = class {
1570
1582
  followUpComplianceRate,
1571
1583
  tagDistribution: tagAgg.map((r) => ({ tag: String(r._id), count: r.count })),
1572
1584
  categoryDistribution: categoryAgg.map((r) => ({ category: String(r._id), count: r.count })),
1573
- peakCallHours: peakHoursAgg.map((r) => ({ hour: Number(r._id), count: r.count }))
1585
+ peakCallHours: peakHoursAgg.map((r) => ({ hour: Number(r._id), count: r.count })),
1586
+ channelDistribution,
1587
+ outcomeDistribution,
1588
+ followUpCalls,
1589
+ followUpRatio
1590
+ };
1591
+ }
1592
+ };
1593
+
1594
+ // src/services/pipeline-analytics.service.ts
1595
+ var PipelineAnalyticsService = class {
1596
+ constructor(CallLog, Pipeline, logger, resolveAgent, tenantId) {
1597
+ this.CallLog = CallLog;
1598
+ this.Pipeline = Pipeline;
1599
+ this.logger = logger;
1600
+ this.resolveAgent = resolveAgent;
1601
+ this.tenantId = tenantId;
1602
+ }
1603
+ buildDateMatch(dateRange, field = "callDate") {
1604
+ if (!dateRange.from && !dateRange.to) return {};
1605
+ const dateFilter = {};
1606
+ if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
1607
+ if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
1608
+ return { [field]: dateFilter };
1609
+ }
1610
+ async getPipelineStats(pipelineId, dateRange) {
1611
+ const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
1612
+ const pipelineName = pipeline?.name ?? pipelineId;
1613
+ const stages = pipeline?.stages ?? [];
1614
+ const matchStage = {
1615
+ pipelineId,
1616
+ isDeleted: { $ne: true },
1617
+ ...this.buildDateMatch(dateRange)
1618
+ };
1619
+ const [totalResult, stageAgg] = await Promise.all([
1620
+ this.CallLog.countDocuments(matchStage),
1621
+ this.CallLog.aggregate([
1622
+ { $match: matchStage },
1623
+ { $unwind: { path: "$stageHistory", preserveNullAndEmptyArrays: false } },
1624
+ {
1625
+ $group: {
1626
+ _id: "$stageHistory.toStageId",
1627
+ count: { $sum: 1 },
1628
+ totalTimeMs: { $sum: "$stageHistory.timeInStageMs" }
1629
+ }
1630
+ },
1631
+ {
1632
+ $project: {
1633
+ stageId: "$_id",
1634
+ count: 1,
1635
+ totalTimeMs: 1
1636
+ }
1637
+ }
1638
+ ])
1639
+ ]);
1640
+ const totalCalls = totalResult;
1641
+ const stageMap = new Map(stageAgg.map((s) => [s.stageId, s]));
1642
+ let bottleneckStage = null;
1643
+ let maxAvgTime = 0;
1644
+ const stageStats = stages.map((s) => {
1645
+ const agg = stageMap.get(s.stageId);
1646
+ const count = agg?.count ?? 0;
1647
+ const avgTimeMs = count > 0 ? Math.round((agg?.totalTimeMs ?? 0) / count) : 0;
1648
+ const conversionRate = totalCalls > 0 ? Math.round(count / totalCalls * 1e4) / 100 : 0;
1649
+ if (avgTimeMs > maxAvgTime) {
1650
+ maxAvgTime = avgTimeMs;
1651
+ bottleneckStage = s.stageId;
1652
+ }
1653
+ return {
1654
+ stageId: s.stageId,
1655
+ stageName: s.name,
1656
+ count,
1657
+ avgTimeMs,
1658
+ conversionRate
1659
+ };
1660
+ });
1661
+ this.logger.info("Pipeline stats computed", { pipelineId, totalCalls });
1662
+ return {
1663
+ pipelineId,
1664
+ pipelineName,
1665
+ totalCalls,
1666
+ stages: stageStats,
1667
+ bottleneckStage
1668
+ };
1669
+ }
1670
+ async getPipelineFunnel(pipelineId, dateRange) {
1671
+ const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
1672
+ const pipelineName = pipeline?.name ?? pipelineId;
1673
+ const stages = pipeline?.stages ?? [];
1674
+ const matchStage = {
1675
+ pipelineId,
1676
+ isDeleted: { $ne: true },
1677
+ ...this.buildDateMatch(dateRange)
1678
+ };
1679
+ const [enteredAgg, exitedAgg] = await Promise.all([
1680
+ this.CallLog.aggregate([
1681
+ { $match: matchStage },
1682
+ { $unwind: "$stageHistory" },
1683
+ { $group: { _id: "$stageHistory.toStageId", count: { $sum: 1 } } }
1684
+ ]),
1685
+ this.CallLog.aggregate([
1686
+ { $match: matchStage },
1687
+ { $unwind: "$stageHistory" },
1688
+ { $group: { _id: "$stageHistory.fromStageId", count: { $sum: 1 } } }
1689
+ ])
1690
+ ]);
1691
+ const enteredMap = new Map(enteredAgg.map((r) => [String(r._id), r.count]));
1692
+ const exitedMap = new Map(exitedAgg.map((r) => [String(r._id), r.count]));
1693
+ const funnelStages = stages.map((s) => {
1694
+ const entered = enteredMap.get(s.stageId) ?? 0;
1695
+ const exited = exitedMap.get(s.stageId) ?? 0;
1696
+ return {
1697
+ stageId: s.stageId,
1698
+ stageName: s.name,
1699
+ entered,
1700
+ exited,
1701
+ dropOff: Math.max(0, entered - exited)
1702
+ };
1703
+ });
1704
+ return { pipelineId, pipelineName, stages: funnelStages };
1705
+ }
1706
+ async getChannelDistribution(dateRange) {
1707
+ const matchStage = {
1708
+ isDeleted: { $ne: true },
1709
+ ...this.buildDateMatch(dateRange)
1710
+ };
1711
+ const results = await this.CallLog.aggregate([
1712
+ { $match: matchStage },
1713
+ { $group: { _id: "$channel", count: { $sum: 1 } } },
1714
+ { $sort: { count: -1 } }
1715
+ ]);
1716
+ return results.map((r) => ({
1717
+ channel: r._id != null ? String(r._id) : "unknown",
1718
+ count: r.count
1719
+ }));
1720
+ }
1721
+ async getOutcomeDistribution(dateRange) {
1722
+ const matchStage = {
1723
+ isDeleted: { $ne: true },
1724
+ ...this.buildDateMatch(dateRange)
1725
+ };
1726
+ const results = await this.CallLog.aggregate([
1727
+ { $match: matchStage },
1728
+ { $group: { _id: "$outcome", count: { $sum: 1 } } },
1729
+ { $sort: { count: -1 } }
1730
+ ]);
1731
+ return results.map((r) => ({
1732
+ outcome: r._id != null ? String(r._id) : "unknown",
1733
+ count: r.count
1734
+ }));
1735
+ }
1736
+ async getFollowUpStats(dateRange) {
1737
+ const matchStage = {
1738
+ isDeleted: { $ne: true },
1739
+ ...this.buildDateMatch(dateRange)
1740
+ };
1741
+ const results = await this.CallLog.aggregate([
1742
+ { $match: matchStage },
1743
+ {
1744
+ $group: {
1745
+ _id: null,
1746
+ total: { $sum: 1 },
1747
+ followUpCalls: {
1748
+ $sum: { $cond: [{ $eq: ["$isFollowUp", true] }, 1, 0] }
1749
+ }
1750
+ }
1751
+ }
1752
+ ]);
1753
+ const stat = results[0] ?? { total: 0, followUpCalls: 0 };
1754
+ const followUpRatio = stat.total > 0 ? Math.round(stat.followUpCalls / stat.total * 1e4) / 100 : 0;
1755
+ return {
1756
+ followUpCalls: stat.followUpCalls,
1757
+ totalCalls: stat.total,
1758
+ followUpRatio
1574
1759
  };
1575
1760
  }
1576
1761
  };
@@ -1578,9 +1763,9 @@ var AnalyticsService = class {
1578
1763
  // src/services/export.service.ts
1579
1764
  var CSV_HEADER = "callLogId,contactName,contactPhone,contactEmail,direction,pipelineId,currentStageId,priority,agentId,callDate,isClosed,tags";
1580
1765
  var ExportService = class {
1581
- constructor(CallLog, analytics, logger) {
1766
+ constructor(CallLog, pipelineAnalytics, logger) {
1582
1767
  this.CallLog = CallLog;
1583
- this.analytics = analytics;
1768
+ this.pipelineAnalytics = pipelineAnalytics;
1584
1769
  this.logger = logger;
1585
1770
  }
1586
1771
  async exportCallLog(callLogId, format) {
@@ -1622,7 +1807,7 @@ var ExportService = class {
1622
1807
  );
1623
1808
  }
1624
1809
  async exportPipelineReport(pipelineId, dateRange, format) {
1625
- const report = await this.analytics.getPipelineStats(pipelineId, dateRange);
1810
+ const report = await this.pipelineAnalytics.getPipelineStats(pipelineId, dateRange);
1626
1811
  if (format === "csv") {
1627
1812
  const header = "pipelineId,pipelineName,totalCalls,stageId,stageName,count,avgTimeMs,conversionRate,bottleneckStage";
1628
1813
  const rows = [header];
@@ -1803,17 +1988,25 @@ function createPipelineRoutes(pipeline, logger) {
1803
1988
  });
1804
1989
  return router;
1805
1990
  }
1806
- function createCallLogRoutes(services, logger) {
1991
+ function getScopedAgentId(req, enableAgentScoping) {
1992
+ if (!enableAgentScoping) return void 0;
1993
+ const user = req.user;
1994
+ if (!user) return void 0;
1995
+ if (user.role === "owner") return void 0;
1996
+ return user.adminUserId;
1997
+ }
1998
+ function createCallLogRoutes(services, logger, enableAgentScoping = false) {
1807
1999
  const router = express.Router();
1808
- const { callLogs, timeline } = services;
2000
+ const { callLogs, lifecycle, timeline } = services;
1809
2001
  router.get("/follow-ups", async (req, res) => {
1810
2002
  try {
1811
- const { agentId } = req.query;
2003
+ const scopedAgentId = getScopedAgentId(req, enableAgentScoping);
2004
+ const agentId = scopedAgentId ?? req.query["agentId"];
1812
2005
  const dateRange = {
1813
2006
  from: req.query["from"],
1814
2007
  to: req.query["to"]
1815
2008
  };
1816
- const result = await callLogs.getFollowUpsDue(agentId, dateRange);
2009
+ const result = await lifecycle.getFollowUpsDue(agentId, dateRange);
1817
2010
  core.sendSuccess(res, result);
1818
2011
  } catch (error) {
1819
2012
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -1824,7 +2017,7 @@ function createCallLogRoutes(services, logger) {
1824
2017
  router.put("/-/bulk/stage", async (req, res) => {
1825
2018
  try {
1826
2019
  const { callLogIds, newStageId, agentId } = req.body;
1827
- const result = await callLogs.bulkChangeStage(callLogIds, newStageId, agentId);
2020
+ const result = await lifecycle.bulkChangeStage(callLogIds, newStageId, agentId);
1828
2021
  core.sendSuccess(res, result);
1829
2022
  } catch (error) {
1830
2023
  if (error instanceof AlxCallLogError) {
@@ -1842,7 +2035,12 @@ function createCallLogRoutes(services, logger) {
1842
2035
  const filter = {};
1843
2036
  if (query["pipelineId"]) filter["pipelineId"] = query["pipelineId"];
1844
2037
  if (query["currentStageId"]) filter["currentStageId"] = query["currentStageId"];
1845
- if (query["agentId"]) filter["agentId"] = query["agentId"];
2038
+ const scopedAgentId = getScopedAgentId(req, enableAgentScoping);
2039
+ if (scopedAgentId) {
2040
+ filter["agentId"] = scopedAgentId;
2041
+ } else if (query["agentId"]) {
2042
+ filter["agentId"] = query["agentId"];
2043
+ }
1846
2044
  if (query["category"]) filter["category"] = query["category"];
1847
2045
  if (query["isClosed"] !== void 0) filter["isClosed"] = query["isClosed"] === "true";
1848
2046
  if (query["contactExternalId"]) filter["contactExternalId"] = query["contactExternalId"];
@@ -1851,6 +2049,14 @@ function createCallLogRoutes(services, logger) {
1851
2049
  if (query["contactEmail"]) filter["contactEmail"] = query["contactEmail"];
1852
2050
  if (query["priority"]) filter["priority"] = query["priority"];
1853
2051
  if (query["direction"]) filter["direction"] = query["direction"];
2052
+ const channel = query["channel"];
2053
+ const outcome = query["outcome"];
2054
+ const isFollowUp = query["isFollowUp"] !== void 0 ? query["isFollowUp"] === "true" : void 0;
2055
+ const includeDeleted = query["includeDeleted"] === "true";
2056
+ if (channel) filter["channel"] = channel;
2057
+ if (outcome) filter["outcome"] = outcome;
2058
+ if (isFollowUp !== void 0) filter["isFollowUp"] = isFollowUp;
2059
+ if (includeDeleted) filter["includeDeleted"] = true;
1854
2060
  if (query["page"]) filter["page"] = parseInt(query["page"], 10);
1855
2061
  if (query["limit"]) filter["limit"] = parseInt(query["limit"], 10);
1856
2062
  if (query["from"] || query["to"]) {
@@ -1906,10 +2112,26 @@ function createCallLogRoutes(services, logger) {
1906
2112
  core.sendError(res, message, 500);
1907
2113
  }
1908
2114
  });
2115
+ router.delete("/:id", async (req, res) => {
2116
+ try {
2117
+ const agentId = req.user?.adminUserId;
2118
+ const agentName = req.user?.displayName;
2119
+ const result = await lifecycle.softDelete(req.params["id"], agentId, agentName);
2120
+ core.sendSuccess(res, result);
2121
+ } catch (error) {
2122
+ if (error instanceof AlxCallLogError) {
2123
+ core.sendError(res, error.message, 404);
2124
+ return;
2125
+ }
2126
+ const message = error instanceof Error ? error.message : "Unknown error";
2127
+ logger.error("Failed to soft delete call log", { id: req.params["id"], error: message });
2128
+ core.sendError(res, message, 500);
2129
+ }
2130
+ });
1909
2131
  router.put("/:id/stage", async (req, res) => {
1910
2132
  try {
1911
2133
  const { newStageId, agentId } = req.body;
1912
- const result = await callLogs.changeStage(req.params["id"], newStageId, agentId);
2134
+ const result = await lifecycle.changeStage(req.params["id"], newStageId, agentId);
1913
2135
  core.sendSuccess(res, result);
1914
2136
  } catch (error) {
1915
2137
  if (error instanceof AlxCallLogError) {
@@ -1924,7 +2146,7 @@ function createCallLogRoutes(services, logger) {
1924
2146
  router.put("/:id/assign", async (req, res) => {
1925
2147
  try {
1926
2148
  const { agentId, assignedBy } = req.body;
1927
- const result = await callLogs.assign(req.params["id"], agentId, assignedBy);
2149
+ const result = await lifecycle.assign(req.params["id"], agentId, assignedBy);
1928
2150
  core.sendSuccess(res, result);
1929
2151
  } catch (error) {
1930
2152
  if (error instanceof AlxCallLogError) {
@@ -1939,7 +2161,7 @@ function createCallLogRoutes(services, logger) {
1939
2161
  router.put("/:id/close", async (req, res) => {
1940
2162
  try {
1941
2163
  const { agentId } = req.body;
1942
- const result = await callLogs.close(req.params["id"], agentId);
2164
+ const result = await lifecycle.close(req.params["id"], agentId);
1943
2165
  core.sendSuccess(res, result);
1944
2166
  } catch (error) {
1945
2167
  if (error instanceof AlxCallLogError) {
@@ -1954,7 +2176,7 @@ function createCallLogRoutes(services, logger) {
1954
2176
  router.put("/:id/reopen", async (req, res) => {
1955
2177
  try {
1956
2178
  const { agentId } = req.body;
1957
- const result = await callLogs.reopen(req.params["id"], agentId);
2179
+ const result = await lifecycle.reopen(req.params["id"], agentId);
1958
2180
  core.sendSuccess(res, result);
1959
2181
  } catch (error) {
1960
2182
  if (error instanceof AlxCallLogError) {
@@ -2034,8 +2256,15 @@ function createContactRoutes(services, logger) {
2034
2256
  });
2035
2257
  return router;
2036
2258
  }
2037
- function createAnalyticsRoutes(analytics, logger) {
2259
+ function createAnalyticsRoutes(analytics, pipelineAnalytics, logger, enableAgentScoping = false) {
2038
2260
  const router = express.Router();
2261
+ function getScopedAgentId2(req) {
2262
+ if (!enableAgentScoping) return void 0;
2263
+ const user = req.user;
2264
+ if (!user) return void 0;
2265
+ if (user.role === "owner") return void 0;
2266
+ return user.adminUserId;
2267
+ }
2039
2268
  function parseDateRange(query) {
2040
2269
  return {
2041
2270
  from: query["from"],
@@ -2054,6 +2283,11 @@ function createAnalyticsRoutes(analytics, logger) {
2054
2283
  });
2055
2284
  router.get("/agent/:agentId", async (req, res) => {
2056
2285
  try {
2286
+ const scopedAgentId = getScopedAgentId2(req);
2287
+ if (scopedAgentId && scopedAgentId !== req.params["agentId"]) {
2288
+ core.sendError(res, "Forbidden: you may only view your own agent stats", 403);
2289
+ return;
2290
+ }
2057
2291
  const dateRange = parseDateRange(req.query);
2058
2292
  const result = await analytics.getAgentStats(req.params["agentId"], dateRange);
2059
2293
  core.sendSuccess(res, result);
@@ -2081,7 +2315,7 @@ function createAnalyticsRoutes(analytics, logger) {
2081
2315
  router.get("/pipeline/:id", async (req, res) => {
2082
2316
  try {
2083
2317
  const dateRange = parseDateRange(req.query);
2084
- const result = await analytics.getPipelineStats(req.params["id"], dateRange);
2318
+ const result = await pipelineAnalytics.getPipelineStats(req.params["id"], dateRange);
2085
2319
  core.sendSuccess(res, result);
2086
2320
  } catch (error) {
2087
2321
  if (error instanceof AlxCallLogError) {
@@ -2096,7 +2330,7 @@ function createAnalyticsRoutes(analytics, logger) {
2096
2330
  router.get("/pipeline/:id/funnel", async (req, res) => {
2097
2331
  try {
2098
2332
  const dateRange = parseDateRange(req.query);
2099
- const result = await analytics.getPipelineFunnel(req.params["id"], dateRange);
2333
+ const result = await pipelineAnalytics.getPipelineFunnel(req.params["id"], dateRange);
2100
2334
  core.sendSuccess(res, result);
2101
2335
  } catch (error) {
2102
2336
  if (error instanceof AlxCallLogError) {
@@ -2108,6 +2342,39 @@ function createAnalyticsRoutes(analytics, logger) {
2108
2342
  core.sendError(res, message, 500);
2109
2343
  }
2110
2344
  });
2345
+ router.get("/channel-distribution", async (req, res) => {
2346
+ try {
2347
+ const dateRange = parseDateRange(req.query);
2348
+ const result = await pipelineAnalytics.getChannelDistribution(dateRange);
2349
+ core.sendSuccess(res, result);
2350
+ } catch (error) {
2351
+ const message = error instanceof Error ? error.message : "Unknown error";
2352
+ logger.error("Failed to get channel distribution", { error: message });
2353
+ core.sendError(res, message, 500);
2354
+ }
2355
+ });
2356
+ router.get("/outcome-distribution", async (req, res) => {
2357
+ try {
2358
+ const dateRange = parseDateRange(req.query);
2359
+ const result = await pipelineAnalytics.getOutcomeDistribution(dateRange);
2360
+ core.sendSuccess(res, result);
2361
+ } catch (error) {
2362
+ const message = error instanceof Error ? error.message : "Unknown error";
2363
+ logger.error("Failed to get outcome distribution", { error: message });
2364
+ core.sendError(res, message, 500);
2365
+ }
2366
+ });
2367
+ router.get("/follow-up-stats", async (req, res) => {
2368
+ try {
2369
+ const dateRange = parseDateRange(req.query);
2370
+ const result = await pipelineAnalytics.getFollowUpStats(dateRange);
2371
+ core.sendSuccess(res, result);
2372
+ } catch (error) {
2373
+ const message = error instanceof Error ? error.message : "Unknown error";
2374
+ logger.error("Failed to get follow-up stats", { error: message });
2375
+ core.sendError(res, message, 500);
2376
+ }
2377
+ });
2111
2378
  router.get("/team", async (req, res) => {
2112
2379
  try {
2113
2380
  const { teamId } = req.query;
@@ -2122,8 +2389,9 @@ function createAnalyticsRoutes(analytics, logger) {
2122
2389
  });
2123
2390
  router.get("/daily", async (req, res) => {
2124
2391
  try {
2392
+ const scopedAgentId = getScopedAgentId2(req);
2125
2393
  const dateRange = parseDateRange(req.query);
2126
- const result = await analytics.getDailyReport(dateRange);
2394
+ const result = await analytics.getDailyReport(dateRange, scopedAgentId);
2127
2395
  core.sendSuccess(res, result);
2128
2396
  } catch (error) {
2129
2397
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -2248,7 +2516,7 @@ function createSettingsRoutes(settings, exportSvc, logger) {
2248
2516
  // src/routes/index.ts
2249
2517
  function createRoutes(services, options) {
2250
2518
  const router = express.Router();
2251
- const { logger, authenticateRequest } = options;
2519
+ const { logger, authenticateRequest, enableAgentScoping = false } = options;
2252
2520
  let authMiddleware;
2253
2521
  if (authenticateRequest) {
2254
2522
  authMiddleware = async (req, res, next) => {
@@ -2268,9 +2536,9 @@ function createRoutes(services, options) {
2268
2536
  }
2269
2537
  const protectedRouter = express.Router();
2270
2538
  protectedRouter.use("/pipelines", createPipelineRoutes(services.pipelines, logger));
2271
- protectedRouter.use("/calls", createCallLogRoutes({ callLogs: services.callLogs, timeline: services.timeline }, logger));
2539
+ protectedRouter.use("/calls", createCallLogRoutes({ callLogs: services.callLogs, lifecycle: services.lifecycle, timeline: services.timeline }, logger, enableAgentScoping));
2272
2540
  protectedRouter.use("/contacts", createContactRoutes({ callLogs: services.callLogs, timeline: services.timeline }, logger));
2273
- protectedRouter.use("/analytics", createAnalyticsRoutes(services.analytics, logger));
2541
+ protectedRouter.use("/analytics", createAnalyticsRoutes(services.analytics, services.pipelineAnalytics, logger, enableAgentScoping));
2274
2542
  protectedRouter.use("/", createSettingsRoutes(services.settings, services.export, logger));
2275
2543
  if (authMiddleware) {
2276
2544
  router.use(authMiddleware, protectedRouter);
@@ -2325,7 +2593,7 @@ var FollowUpWorker = class {
2325
2593
  $set: { followUpNotifiedAt: /* @__PURE__ */ new Date() },
2326
2594
  $push: {
2327
2595
  timeline: {
2328
- entryId: crypto5__default.default.randomUUID(),
2596
+ entryId: crypto6__default.default.randomUUID(),
2329
2597
  type: callLogTypes.TimelineEntryType.FollowUpCompleted,
2330
2598
  content: SYSTEM_TIMELINE.FollowUpCompleted,
2331
2599
  createdAt: /* @__PURE__ */ new Date()
@@ -2378,7 +2646,8 @@ var CallLogEngineConfigSchema = zod.z.object({
2378
2646
  }).optional(),
2379
2647
  options: zod.z.object({
2380
2648
  maxTimelineEntries: zod.z.number().int().positive().optional(),
2381
- followUpCheckIntervalMs: zod.z.number().int().positive().optional()
2649
+ followUpCheckIntervalMs: zod.z.number().int().positive().optional(),
2650
+ enableAgentScoping: zod.z.boolean().optional()
2382
2651
  }).optional()
2383
2652
  });
2384
2653
  function createCallLogEngine(config) {
@@ -2408,14 +2677,30 @@ function createCallLogEngine(config) {
2408
2677
  config.hooks ?? {},
2409
2678
  resolvedOptions
2410
2679
  );
2680
+ const callLogLifecycleService = new CallLogLifecycleService(
2681
+ CallLog,
2682
+ Pipeline,
2683
+ timelineService,
2684
+ logger,
2685
+ config.hooks ?? {},
2686
+ resolvedOptions
2687
+ );
2411
2688
  const analyticsService = new AnalyticsService(CallLog, Pipeline, logger, config.agents?.resolveAgent);
2412
- const exportService = new ExportService(CallLog, analyticsService, logger);
2689
+ const pipelineAnalyticsService = new PipelineAnalyticsService(
2690
+ CallLog,
2691
+ Pipeline,
2692
+ logger,
2693
+ config.agents?.resolveAgent
2694
+ );
2695
+ const exportService = new ExportService(CallLog, pipelineAnalyticsService, logger);
2413
2696
  const routes = createRoutes(
2414
2697
  {
2415
2698
  pipelines: pipelineService,
2416
2699
  callLogs: callLogService,
2700
+ lifecycle: callLogLifecycleService,
2417
2701
  timeline: timelineService,
2418
2702
  analytics: analyticsService,
2703
+ pipelineAnalytics: pipelineAnalyticsService,
2419
2704
  settings: settingsService,
2420
2705
  export: exportService
2421
2706
  },
@@ -2427,7 +2712,8 @@ function createCallLogEngine(config) {
2427
2712
  if (!token) return null;
2428
2713
  return config.adapters.authenticateAgent(token);
2429
2714
  } : void 0,
2430
- logger
2715
+ logger,
2716
+ enableAgentScoping: resolvedOptions.enableAgentScoping
2431
2717
  }
2432
2718
  );
2433
2719
  const followUpWorker = new FollowUpWorker({
@@ -2444,8 +2730,10 @@ function createCallLogEngine(config) {
2444
2730
  return {
2445
2731
  pipelines: pipelineService,
2446
2732
  callLogs: callLogService,
2733
+ lifecycle: callLogLifecycleService,
2447
2734
  timeline: timelineService,
2448
2735
  analytics: analyticsService,
2736
+ pipelineAnalytics: pipelineAnalyticsService,
2449
2737
  settings: settingsService,
2450
2738
  export: exportService,
2451
2739
  routes,
@@ -2465,6 +2753,7 @@ exports.AnalyticsService = AnalyticsService;
2465
2753
  exports.AuthFailedError = AuthFailedError;
2466
2754
  exports.CALL_LOG_DEFAULTS = CALL_LOG_DEFAULTS;
2467
2755
  exports.CallLogClosedError = CallLogClosedError;
2756
+ exports.CallLogLifecycleService = CallLogLifecycleService;
2468
2757
  exports.CallLogNotFoundError = CallLogNotFoundError;
2469
2758
  exports.CallLogSchema = CallLogSchema;
2470
2759
  exports.CallLogService = CallLogService;
@@ -2478,6 +2767,7 @@ exports.FollowUpWorker = FollowUpWorker;
2478
2767
  exports.InvalidConfigError = InvalidConfigError;
2479
2768
  exports.InvalidPipelineError = InvalidPipelineError;
2480
2769
  exports.PIPELINE_DEFAULTS = PIPELINE_DEFAULTS;
2770
+ exports.PipelineAnalyticsService = PipelineAnalyticsService;
2481
2771
  exports.PipelineNotFoundError = PipelineNotFoundError;
2482
2772
  exports.PipelineSchema = PipelineSchema;
2483
2773
  exports.PipelineService = PipelineService;