@talkpilot/core-db 1.3.89 → 1.3.90
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/README.md +0 -72
- package/dist/talkpilot/calls/calls.constants.d.ts +1 -11
- package/dist/talkpilot/calls/calls.constants.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.constants.js +1 -11
- package/dist/talkpilot/calls/calls.constants.js.map +1 -1
- package/dist/talkpilot/calls/calls.getters.d.ts +1 -8
- package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.getters.js +1 -216
- package/dist/talkpilot/calls/calls.getters.js.map +1 -1
- package/dist/talkpilot/calls/calls.types.d.ts +0 -45
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.getters.d.ts +0 -6
- package/dist/talkpilot/clients/clients.getters.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.getters.js +1 -62
- package/dist/talkpilot/clients/clients.getters.js.map +1 -1
- package/dist/talkpilot/clients/clients.types.d.ts +0 -53
- package/dist/talkpilot/clients/clients.types.d.ts.map +1 -1
- package/dist/talkpilot/clients/index.d.ts +0 -2
- package/dist/talkpilot/clients/index.d.ts.map +1 -1
- package/dist/talkpilot/clients/index.js +0 -2
- package/dist/talkpilot/clients/index.js.map +1 -1
- package/dist/talkpilot/flows/flows.schema.d.ts +9 -0
- package/dist/talkpilot/flows/flows.schema.d.ts.map +1 -1
- package/dist/talkpilot/flows/flows.schema.js +7 -0
- package/dist/talkpilot/flows/flows.schema.js.map +1 -1
- package/dist/talkpilot/flows/flows.types.d.ts +3 -0
- package/dist/talkpilot/flows/flows.types.d.ts.map +1 -1
- package/dist/talkpilot/index.d.ts +0 -1
- package/dist/talkpilot/index.d.ts.map +1 -1
- package/dist/talkpilot/index.js +0 -1
- package/dist/talkpilot/index.js.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts +1 -9
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +1 -70
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts +0 -5
- package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts.map +1 -1
- package/dist/talkpilot/subscriptions/subscriptions.utils.d.ts +4 -0
- package/dist/talkpilot/subscriptions/subscriptions.utils.d.ts.map +1 -0
- package/dist/talkpilot/subscriptions/subscriptions.utils.js +20 -0
- package/dist/talkpilot/subscriptions/subscriptions.utils.js.map +1 -0
- package/dist/test-utils/factories/index.d.ts +0 -2
- package/dist/test-utils/factories/index.d.ts.map +1 -1
- package/dist/test-utils/factories/index.js +0 -2
- package/dist/test-utils/factories/index.js.map +1 -1
- package/dist/websitalk/scanResources/scanResources.getters.d.ts.map +1 -1
- package/dist/websitalk/scanResources/scanResources.getters.js +1 -2
- package/dist/websitalk/scanResources/scanResources.getters.js.map +1 -1
- package/dist/websitalk/scanResources/scanResources.utils.d.ts +1 -1
- package/dist/websitalk/scanResources/scanResources.utils.d.ts.map +1 -1
- package/dist/websitalk/scanResources/scanResources.utils.js +3 -3
- package/dist/websitalk/scanResources/scanResources.utils.js.map +1 -1
- package/package.json +1 -1
- package/src/talkpilot/calls/calls.constants.ts +0 -14
- package/src/talkpilot/calls/calls.getters.ts +0 -320
- package/src/talkpilot/calls/calls.types.ts +0 -52
- package/src/talkpilot/clients/clients.getters.ts +2 -91
- package/src/talkpilot/clients/clients.types.ts +0 -58
- package/src/talkpilot/clients/index.ts +0 -2
- package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +11 -0
- package/src/talkpilot/flows/flows.schema.ts +7 -0
- package/src/talkpilot/flows/flows.types.ts +1 -0
- package/src/talkpilot/index.ts +0 -1
- package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +13 -284
- package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +1 -89
- package/src/talkpilot/phone_numbers/phone_numbers.types.ts +0 -6
- package/src/test-utils/factories/index.ts +0 -2
- package/src/websitalk/scanResources/scanResources.getters.ts +6 -2
- package/src/websitalk/scanResources/scanResources.utils.ts +3 -2
- package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts +0 -8
- package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts.map +0 -1
- package/dist/talkpilot/auditLogs/auditLogs.getters.js +0 -26
- package/dist/talkpilot/auditLogs/auditLogs.getters.js.map +0 -1
- package/dist/talkpilot/auditLogs/auditLogs.types.d.ts +0 -11
- package/dist/talkpilot/auditLogs/auditLogs.types.d.ts.map +0 -1
- package/dist/talkpilot/auditLogs/auditLogs.types.js +0 -3
- package/dist/talkpilot/auditLogs/auditLogs.types.js.map +0 -1
- package/dist/talkpilot/auditLogs/index.d.ts +0 -3
- package/dist/talkpilot/auditLogs/index.d.ts.map +0 -1
- package/dist/talkpilot/auditLogs/index.js +0 -19
- package/dist/talkpilot/auditLogs/index.js.map +0 -1
- package/dist/talkpilot/clients/clients.constants.d.ts +0 -4
- package/dist/talkpilot/clients/clients.constants.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.constants.js +0 -7
- package/dist/talkpilot/clients/clients.constants.js.map +0 -1
- package/dist/talkpilot/clients/clients.lifecycle.d.ts +0 -10
- package/dist/talkpilot/clients/clients.lifecycle.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.lifecycle.js +0 -109
- package/dist/talkpilot/clients/clients.lifecycle.js.map +0 -1
- package/dist/test-utils/factories/talkpilot/clients.d.ts +0 -14
- package/dist/test-utils/factories/talkpilot/clients.d.ts.map +0 -1
- package/dist/test-utils/factories/talkpilot/clients.js +0 -19
- package/dist/test-utils/factories/talkpilot/clients.js.map +0 -1
- package/dist/test-utils/factories/talkpilot/results.d.ts +0 -16
- package/dist/test-utils/factories/talkpilot/results.d.ts.map +0 -1
- package/dist/test-utils/factories/talkpilot/results.js +0 -40
- package/dist/test-utils/factories/talkpilot/results.js.map +0 -1
- package/dist/utils/regex.utils.d.ts +0 -2
- package/dist/utils/regex.utils.d.ts.map +0 -1
- package/dist/utils/regex.utils.js +0 -6
- package/dist/utils/regex.utils.js.map +0 -1
- package/src/talkpilot/auditLogs/__tests__/auditLogs.spec.ts +0 -91
- package/src/talkpilot/auditLogs/auditLogs.getters.ts +0 -26
- package/src/talkpilot/auditLogs/auditLogs.types.ts +0 -12
- package/src/talkpilot/auditLogs/index.ts +0 -2
- package/src/talkpilot/calls/__tests__/calls.statistics.getters.spec.ts +0 -559
- package/src/talkpilot/clients/__tests__/clients.getters.spec.ts +0 -224
- package/src/talkpilot/clients/__tests__/clients.lifecycle.spec.ts +0 -194
- package/src/talkpilot/clients/clients.constants.ts +0 -4
- package/src/talkpilot/clients/clients.lifecycle.ts +0 -132
- package/src/test-utils/factories/talkpilot/clients.ts +0 -21
- package/src/test-utils/factories/talkpilot/results.ts +0 -43
- package/src/utils/regex.utils.ts +0 -2
|
@@ -1,559 +0,0 @@
|
|
|
1
|
-
import { ObjectId } from "mongodb";
|
|
2
|
-
import {
|
|
3
|
-
aggregateActiveCallsSummary,
|
|
4
|
-
aggregateTodayCallsSummary,
|
|
5
|
-
aggregateClientCallStats,
|
|
6
|
-
aggregateSessionCallStats,
|
|
7
|
-
findLastCallDatesByClientIds,
|
|
8
|
-
findClientIdsWithCalls,
|
|
9
|
-
findClientIdsByCallSids,
|
|
10
|
-
getCallsCollection,
|
|
11
|
-
} from "../calls.getters";
|
|
12
|
-
import { getClientsCollection } from "../../clients/clients.getters";
|
|
13
|
-
import { getSessionsCollection } from "../../sessions/sessions.getter";
|
|
14
|
-
import type { Call } from "../calls.types";
|
|
15
|
-
import { INCOMING_SESSION_ID, UNKNOWN_SESSION_ID } from "../calls.constants";
|
|
16
|
-
import {
|
|
17
|
-
createOutGoingCallDoc,
|
|
18
|
-
createIncomingCallDoc,
|
|
19
|
-
createSession,
|
|
20
|
-
createClient,
|
|
21
|
-
} from "../../../test-utils/factories";
|
|
22
|
-
|
|
23
|
-
const insertCall = (call: Call) =>
|
|
24
|
-
getCallsCollection().insertOne({ ...call, env: call.env ?? "test" });
|
|
25
|
-
|
|
26
|
-
const insertClients = (...clientIds: string[]) =>
|
|
27
|
-
getClientsCollection().insertMany(clientIds.map((clientId) => createClient({ clientId })));
|
|
28
|
-
|
|
29
|
-
describe("calls.getters statistics", () => {
|
|
30
|
-
describe("aggregateActiveCallsSummary", () => {
|
|
31
|
-
it("should count distinct active calls within the last 2 hours, split incoming/outgoing", async () => {
|
|
32
|
-
const now = new Date();
|
|
33
|
-
await Promise.all([
|
|
34
|
-
insertCall(createIncomingCallDoc({ clientId: "clientA", status: "in-progress", updatedAt: now })),
|
|
35
|
-
insertCall(createOutGoingCallDoc({ clientId: "clientB", status: "in-progress", updatedAt: now })),
|
|
36
|
-
insertCall(createIncomingCallDoc({ clientId: "clientC", status: "completed", updatedAt: now })),
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
const summary = await aggregateActiveCallsSummary();
|
|
40
|
-
|
|
41
|
-
expect(summary.total).toBe(2);
|
|
42
|
-
expect(summary.incoming).toBe(1);
|
|
43
|
-
expect(summary.outgoing).toBe(1);
|
|
44
|
-
expect(summary.activeClientIds.sort()).toEqual(["clientA", "clientB"]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("should filter by clientId when provided", async () => {
|
|
48
|
-
const now = new Date();
|
|
49
|
-
await Promise.all([
|
|
50
|
-
insertCall(createOutGoingCallDoc({ clientId: "onlyThisClient", status: "in-progress", updatedAt: now })),
|
|
51
|
-
insertCall(createOutGoingCallDoc({ clientId: "otherClient", status: "in-progress", updatedAt: now })),
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
const summary = await aggregateActiveCallsSummary("onlyThisClient");
|
|
55
|
-
expect(summary.total).toBe(1);
|
|
56
|
-
expect(summary.activeClientIds).toEqual(["onlyThisClient"]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("should exclude calls whose updatedAt is older than 2 hours", async () => {
|
|
60
|
-
const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000);
|
|
61
|
-
await insertCall(createOutGoingCallDoc({ status: "in-progress", updatedAt: threeHoursAgo }));
|
|
62
|
-
|
|
63
|
-
const summary = await aggregateActiveCallsSummary();
|
|
64
|
-
expect(summary.total).toBe(0);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe("aggregateTodayCallsSummary", () => {
|
|
69
|
-
it("should aggregate status/type counts and total minutes within the given date range", async () => {
|
|
70
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
71
|
-
await Promise.all([
|
|
72
|
-
insertCall(createIncomingCallDoc({ status: "completed", callLength: 120, createdAt: new Date() })),
|
|
73
|
-
insertCall(createOutGoingCallDoc({ status: "failed", callLength: 30, createdAt: new Date() })),
|
|
74
|
-
]);
|
|
75
|
-
|
|
76
|
-
const summary = await aggregateTodayCallsSummary({ dateFrom: today, dateTo: today });
|
|
77
|
-
|
|
78
|
-
expect(summary.total).toBe(2);
|
|
79
|
-
expect(summary.incoming).toBe(1);
|
|
80
|
-
expect(summary.outgoing).toBe(1);
|
|
81
|
-
expect(summary.completed).toBe(1);
|
|
82
|
-
expect(summary.failed).toBe(1);
|
|
83
|
-
expect(summary.totalMinutes).toBe(3); // ceil(150/60)
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("should respect the given timezone when resolving an explicit date", async () => {
|
|
87
|
-
// 23:30 UTC on 2026-01-01 is already 2026-01-02 in Asia/Jerusalem (+2).
|
|
88
|
-
const lateUtc = new Date("2026-01-01T23:30:00.000Z");
|
|
89
|
-
await insertCall(createOutGoingCallDoc({ status: "completed", createdAt: lateUtc }));
|
|
90
|
-
|
|
91
|
-
const summaryUtc = await aggregateTodayCallsSummary({
|
|
92
|
-
dateFrom: "2026-01-01",
|
|
93
|
-
dateTo: "2026-01-01",
|
|
94
|
-
timezone: "UTC",
|
|
95
|
-
});
|
|
96
|
-
expect(summaryUtc.total).toBe(1);
|
|
97
|
-
|
|
98
|
-
const summaryJerusalem = await aggregateTodayCallsSummary({
|
|
99
|
-
dateFrom: "2026-01-01",
|
|
100
|
-
dateTo: "2026-01-01",
|
|
101
|
-
timezone: "Asia/Jerusalem",
|
|
102
|
-
});
|
|
103
|
-
expect(summaryJerusalem.total).toBe(0);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("should include a call's full history (not just today) when no date range is given", async () => {
|
|
107
|
-
const sixMonthsAgo = new Date(Date.now() - 180 * 24 * 60 * 60 * 1000);
|
|
108
|
-
await insertCall(createOutGoingCallDoc({ status: "completed", createdAt: sixMonthsAgo }));
|
|
109
|
-
|
|
110
|
-
const summary = await aggregateTodayCallsSummary({});
|
|
111
|
-
expect(summary.total).toBe(1);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe("aggregateClientCallStats", () => {
|
|
116
|
-
beforeEach(async () => {
|
|
117
|
-
await insertClients(
|
|
118
|
-
"clientX",
|
|
119
|
-
"wantedClient",
|
|
120
|
-
"unwantedClient",
|
|
121
|
-
"longHistoryClient",
|
|
122
|
-
"lastCallDateClient",
|
|
123
|
-
"onlyThisClientNoRange",
|
|
124
|
-
"otherClientNoRange",
|
|
125
|
-
"statusBucketClient",
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("should group call counts and minutes by clientId", async () => {
|
|
130
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
131
|
-
await Promise.all([
|
|
132
|
-
insertCall(
|
|
133
|
-
createOutGoingCallDoc({ clientId: "clientX", status: "completed", callLength: 60, createdAt: new Date() }),
|
|
134
|
-
),
|
|
135
|
-
insertCall(
|
|
136
|
-
createOutGoingCallDoc({ clientId: "clientX", status: "failed", callLength: 60, createdAt: new Date() }),
|
|
137
|
-
),
|
|
138
|
-
]);
|
|
139
|
-
|
|
140
|
-
const { rows } = await aggregateClientCallStats({ dateFrom: today, dateTo: today });
|
|
141
|
-
const row = rows.find((r) => r.clientId === "clientX");
|
|
142
|
-
|
|
143
|
-
expect(row?.totalCalls).toBe(2);
|
|
144
|
-
expect(row?.statusCounts).toMatchObject({ completed: 1, failed: 1 });
|
|
145
|
-
expect(row?.totalMinutes).toBe(2);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("should not include other clients when filtered by clientId", async () => {
|
|
149
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
150
|
-
await Promise.all([
|
|
151
|
-
insertCall(createOutGoingCallDoc({ clientId: "wantedClient", status: "completed", createdAt: new Date() })),
|
|
152
|
-
insertCall(createOutGoingCallDoc({ clientId: "unwantedClient", status: "completed", createdAt: new Date() })),
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
const { rows } = await aggregateClientCallStats({
|
|
156
|
-
dateFrom: today,
|
|
157
|
-
dateTo: today,
|
|
158
|
-
clientId: "wantedClient",
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
expect(rows).toHaveLength(1);
|
|
162
|
-
expect(rows[0].clientId).toBe("wantedClient");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("should include a client's full history when no date range is given", async () => {
|
|
166
|
-
const sixMonthsAgo = new Date(Date.now() - 180 * 24 * 60 * 60 * 1000);
|
|
167
|
-
await Promise.all([
|
|
168
|
-
insertCall(createOutGoingCallDoc({ clientId: "longHistoryClient", status: "completed", createdAt: sixMonthsAgo })),
|
|
169
|
-
insertCall(createOutGoingCallDoc({ clientId: "longHistoryClient", status: "completed", createdAt: new Date() })),
|
|
170
|
-
]);
|
|
171
|
-
|
|
172
|
-
const { rows } = await aggregateClientCallStats({});
|
|
173
|
-
const row = rows.find((r) => r.clientId === "longHistoryClient");
|
|
174
|
-
|
|
175
|
-
expect(row?.totalCalls).toBe(2);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("should set lastCallDate to the most recent call", async () => {
|
|
179
|
-
const earlierCall = new Date("2026-01-01T00:00:00.000Z");
|
|
180
|
-
const laterCall = new Date("2026-03-10T00:00:00.000Z");
|
|
181
|
-
await Promise.all([
|
|
182
|
-
insertCall(createOutGoingCallDoc({ clientId: "lastCallDateClient", status: "completed", createdAt: earlierCall })),
|
|
183
|
-
insertCall(createOutGoingCallDoc({ clientId: "lastCallDateClient", status: "completed", createdAt: laterCall })),
|
|
184
|
-
]);
|
|
185
|
-
|
|
186
|
-
const { rows } = await aggregateClientCallStats({});
|
|
187
|
-
const row = rows.find((r) => r.clientId === "lastCallDateClient");
|
|
188
|
-
|
|
189
|
-
expect(row?.lastCallDate).toEqual(laterCall);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("should filter by clientId alone when no date range is given", async () => {
|
|
193
|
-
await Promise.all([
|
|
194
|
-
insertCall(createOutGoingCallDoc({ clientId: "onlyThisClientNoRange", status: "completed", createdAt: new Date() })),
|
|
195
|
-
insertCall(createOutGoingCallDoc({ clientId: "otherClientNoRange", status: "completed", createdAt: new Date() })),
|
|
196
|
-
]);
|
|
197
|
-
|
|
198
|
-
const { rows } = await aggregateClientCallStats({ clientId: "onlyThisClientNoRange" });
|
|
199
|
-
|
|
200
|
-
expect(rows).toHaveLength(1);
|
|
201
|
-
expect(rows[0].clientId).toBe("onlyThisClientNoRange");
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("should count every real status separately, including queued; a missing status only inflates totalCalls", async () => {
|
|
205
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
206
|
-
await Promise.all([
|
|
207
|
-
insertCall(createOutGoingCallDoc({ clientId: "statusBucketClient", status: "completed", createdAt: new Date() })),
|
|
208
|
-
insertCall(createOutGoingCallDoc({ clientId: "statusBucketClient", status: "canceled", createdAt: new Date() })),
|
|
209
|
-
insertCall(createOutGoingCallDoc({ clientId: "statusBucketClient", status: "queued" as any, createdAt: new Date() })),
|
|
210
|
-
insertCall(createOutGoingCallDoc({ clientId: "statusBucketClient", status: "ringing", createdAt: new Date() })),
|
|
211
|
-
insertCall(createOutGoingCallDoc({ clientId: "statusBucketClient", status: "initiated", createdAt: new Date() })),
|
|
212
|
-
insertCall(createOutGoingCallDoc({ clientId: "statusBucketClient", status: "answered", createdAt: new Date() })),
|
|
213
|
-
insertCall(createOutGoingCallDoc({ clientId: "statusBucketClient", status: undefined, createdAt: new Date() })),
|
|
214
|
-
]);
|
|
215
|
-
|
|
216
|
-
const { rows } = await aggregateClientCallStats({ dateFrom: today, dateTo: today });
|
|
217
|
-
const row = rows.find((r) => r.clientId === "statusBucketClient");
|
|
218
|
-
|
|
219
|
-
expect(row?.totalCalls).toBe(7);
|
|
220
|
-
expect(row?.statusCounts).toMatchObject({
|
|
221
|
-
completed: 1,
|
|
222
|
-
canceled: 1,
|
|
223
|
-
queued: 1,
|
|
224
|
-
ringing: 1,
|
|
225
|
-
initiated: 1,
|
|
226
|
-
answered: 1,
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("should leave out clients that no longer exist in the clients collection", async () => {
|
|
231
|
-
await Promise.all([
|
|
232
|
-
insertCall(createOutGoingCallDoc({ clientId: "clientX", status: "completed", createdAt: new Date() })),
|
|
233
|
-
insertCall(createOutGoingCallDoc({ clientId: "deletedClient", status: "completed", createdAt: new Date() })),
|
|
234
|
-
]);
|
|
235
|
-
|
|
236
|
-
const { rows, total } = await aggregateClientCallStats({});
|
|
237
|
-
|
|
238
|
-
expect(rows.map((r) => r.clientId)).toEqual(["clientX"]);
|
|
239
|
-
expect(total).toBe(1);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("should return one page sorted by call count, with the total across all pages", async () => {
|
|
243
|
-
await Promise.all([
|
|
244
|
-
insertCall(createOutGoingCallDoc({ clientId: "clientX", status: "completed", createdAt: new Date() })),
|
|
245
|
-
insertCall(createOutGoingCallDoc({ clientId: "wantedClient", status: "completed", createdAt: new Date() })),
|
|
246
|
-
insertCall(createOutGoingCallDoc({ clientId: "wantedClient", status: "completed", createdAt: new Date() })),
|
|
247
|
-
insertCall(createOutGoingCallDoc({ clientId: "unwantedClient", status: "completed", createdAt: new Date() })),
|
|
248
|
-
insertCall(createOutGoingCallDoc({ clientId: "unwantedClient", status: "completed", createdAt: new Date() })),
|
|
249
|
-
insertCall(createOutGoingCallDoc({ clientId: "unwantedClient", status: "completed", createdAt: new Date() })),
|
|
250
|
-
]);
|
|
251
|
-
|
|
252
|
-
const firstPage = await aggregateClientCallStats({ skip: 0, limit: 2 });
|
|
253
|
-
const secondPage = await aggregateClientCallStats({ skip: 2, limit: 2 });
|
|
254
|
-
|
|
255
|
-
expect(firstPage.rows.map((r) => r.clientId)).toEqual(["unwantedClient", "wantedClient"]);
|
|
256
|
-
expect(secondPage.rows.map((r) => r.clientId)).toEqual(["clientX"]);
|
|
257
|
-
expect(firstPage.total).toBe(3);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("should restrict to clientIds when given", async () => {
|
|
261
|
-
await Promise.all([
|
|
262
|
-
insertCall(createOutGoingCallDoc({ clientId: "wantedClient", status: "completed", createdAt: new Date() })),
|
|
263
|
-
insertCall(createOutGoingCallDoc({ clientId: "unwantedClient", status: "completed", createdAt: new Date() })),
|
|
264
|
-
]);
|
|
265
|
-
|
|
266
|
-
const { rows } = await aggregateClientCallStats({ clientIds: ["wantedClient"] });
|
|
267
|
-
|
|
268
|
-
expect(rows.map((r) => r.clientId)).toEqual(["wantedClient"]);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("should apply both clientId and clientIds when given together", async () => {
|
|
272
|
-
await Promise.all([
|
|
273
|
-
insertCall(createOutGoingCallDoc({ clientId: "wantedClient", status: "completed", createdAt: new Date() })),
|
|
274
|
-
insertCall(createOutGoingCallDoc({ clientId: "unwantedClient", status: "completed", createdAt: new Date() })),
|
|
275
|
-
]);
|
|
276
|
-
|
|
277
|
-
const inList = await aggregateClientCallStats({
|
|
278
|
-
clientId: "wantedClient",
|
|
279
|
-
clientIds: ["wantedClient", "unwantedClient"],
|
|
280
|
-
});
|
|
281
|
-
const notInList = await aggregateClientCallStats({
|
|
282
|
-
clientId: "wantedClient",
|
|
283
|
-
clientIds: ["unwantedClient"],
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
expect(inList.rows.map((r) => r.clientId)).toEqual(["wantedClient"]);
|
|
287
|
-
expect(notInList.rows).toEqual([]);
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
describe("aggregateSessionCallStats", () => {
|
|
292
|
-
it("should group by sessionId and resolve the session's current name via the sessions collection", async () => {
|
|
293
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
294
|
-
const sessionId = new ObjectId();
|
|
295
|
-
await getSessionsCollection().insertOne(createSession({ _id: sessionId, session_name: "My Session" }));
|
|
296
|
-
await insertCall(
|
|
297
|
-
createOutGoingCallDoc({ sessionId, status: "completed", createdAt: new Date() }),
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
301
|
-
const row = rows.find((r) => r.sessionId === sessionId.toString());
|
|
302
|
-
|
|
303
|
-
expect(row).toMatchObject({ sessionName: "My Session", totalCalls: 1 });
|
|
304
|
-
expect(row?.statusCounts).toMatchObject({ completed: 1 });
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("should always show the session's current name, even if it was renamed after some calls happened", async () => {
|
|
308
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
309
|
-
const sessionId = new ObjectId();
|
|
310
|
-
const earlierCall = new Date(Date.now() - 60 * 60 * 1000);
|
|
311
|
-
const laterCall = new Date();
|
|
312
|
-
|
|
313
|
-
await getSessionsCollection().insertOne(createSession({ _id: sessionId, session_name: "New Name" }));
|
|
314
|
-
await Promise.all([
|
|
315
|
-
insertCall(createOutGoingCallDoc({ sessionId, status: "completed", createdAt: earlierCall })),
|
|
316
|
-
insertCall(createOutGoingCallDoc({ sessionId, status: "completed", createdAt: laterCall })),
|
|
317
|
-
]);
|
|
318
|
-
|
|
319
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
320
|
-
const merged = rows.find((r) => r.sessionId === sessionId.toString());
|
|
321
|
-
|
|
322
|
-
expect(merged).toMatchObject({ sessionName: "New Name", totalCalls: 2 });
|
|
323
|
-
expect(merged?.statusCounts).toMatchObject({ completed: 2 });
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("should show 'Unknown' when the session no longer exists in the sessions collection", async () => {
|
|
327
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
328
|
-
const orphanedSessionId = new ObjectId();
|
|
329
|
-
await insertCall(createOutGoingCallDoc({ sessionId: orphanedSessionId, status: "completed", createdAt: new Date() }));
|
|
330
|
-
|
|
331
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
332
|
-
const row = rows.find((r) => r.sessionId === orphanedSessionId.toString());
|
|
333
|
-
|
|
334
|
-
expect(row?.sessionName).toBe("Unknown");
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("should count every call flagged isIncomingCall as 'Incoming Call', even if it carries a sessionId", async () => {
|
|
338
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
339
|
-
const sessionId = new ObjectId();
|
|
340
|
-
await getSessionsCollection().insertOne(createSession({ _id: sessionId, session_name: "Some Session" }));
|
|
341
|
-
await Promise.all([
|
|
342
|
-
insertCall(createIncomingCallDoc({ sessionId, status: "completed", createdAt: new Date() })),
|
|
343
|
-
insertCall(createIncomingCallDoc({ status: "completed", createdAt: new Date() })),
|
|
344
|
-
]);
|
|
345
|
-
|
|
346
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
347
|
-
|
|
348
|
-
expect(rows).toHaveLength(1);
|
|
349
|
-
expect(rows[0]).toMatchObject({ sessionId: INCOMING_SESSION_ID, sessionName: "Incoming Call", totalCalls: 2 });
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("should label a call with no session as 'Incoming Call' only when it is flagged isIncomingCall", async () => {
|
|
353
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
354
|
-
await Promise.all([
|
|
355
|
-
insertCall(createIncomingCallDoc({ status: "completed", createdAt: new Date() })),
|
|
356
|
-
insertCall(createIncomingCallDoc({ status: "failed", createdAt: new Date() })),
|
|
357
|
-
]);
|
|
358
|
-
|
|
359
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
360
|
-
const row = rows.find((r) => r.sessionName === "Incoming Call");
|
|
361
|
-
|
|
362
|
-
expect(rows).toHaveLength(1);
|
|
363
|
-
expect(row).toMatchObject({ sessionId: INCOMING_SESSION_ID, totalCalls: 2 });
|
|
364
|
-
expect(row?.statusCounts).toMatchObject({ completed: 1, failed: 1 });
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it("should put a call with no session that is not flagged incoming under 'Unknown', not 'Incoming Call'", async () => {
|
|
368
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
369
|
-
await insertCall(createOutGoingCallDoc({ sessionId: null, status: "completed", createdAt: new Date() }));
|
|
370
|
-
|
|
371
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
372
|
-
|
|
373
|
-
expect(rows).toHaveLength(1);
|
|
374
|
-
expect(rows[0]).toMatchObject({
|
|
375
|
-
sessionId: UNKNOWN_SESSION_ID,
|
|
376
|
-
sessionName: "Unknown",
|
|
377
|
-
totalCalls: 1,
|
|
378
|
-
});
|
|
379
|
-
expect(rows.find((r) => r.sessionName === "Incoming Call")).toBeUndefined();
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it("should treat a call with no isIncomingCall flag at all as unknown, not incoming", async () => {
|
|
383
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
384
|
-
const { isIncomingCall: _omitted, ...withoutFlag } = createOutGoingCallDoc({
|
|
385
|
-
sessionId: null,
|
|
386
|
-
status: "completed",
|
|
387
|
-
createdAt: new Date(),
|
|
388
|
-
});
|
|
389
|
-
await insertCall(withoutFlag as Call);
|
|
390
|
-
|
|
391
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
392
|
-
|
|
393
|
-
expect(rows.map((r) => r.sessionName)).toEqual(["Unknown"]);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it("should keep 'Incoming Call', calls without a session and a deleted session as separate rows", async () => {
|
|
397
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
398
|
-
const orphanedSessionId = new ObjectId();
|
|
399
|
-
await Promise.all([
|
|
400
|
-
insertCall(createIncomingCallDoc({ status: "completed", createdAt: new Date() })),
|
|
401
|
-
insertCall(createOutGoingCallDoc({ sessionId: null, status: "completed", createdAt: new Date() })),
|
|
402
|
-
insertCall(createOutGoingCallDoc({ sessionId: orphanedSessionId, status: "failed", createdAt: new Date() })),
|
|
403
|
-
]);
|
|
404
|
-
|
|
405
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
406
|
-
|
|
407
|
-
expect(rows).toHaveLength(3);
|
|
408
|
-
expect(rows.find((r) => r.sessionId === INCOMING_SESSION_ID)).toMatchObject({ sessionName: "Incoming Call", totalCalls: 1 });
|
|
409
|
-
expect(rows.find((r) => r.sessionId === UNKNOWN_SESSION_ID)).toMatchObject({ sessionName: "Unknown", totalCalls: 1 });
|
|
410
|
-
expect(rows.find((r) => r.sessionId === orphanedSessionId.toString())).toMatchObject({ sessionName: "Unknown", totalCalls: 1 });
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it("should show 'Unknown' for a session that exists but has no name, keeping its own sessionId", async () => {
|
|
414
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
415
|
-
const sessionId = new ObjectId();
|
|
416
|
-
await getSessionsCollection().insertOne(createSession({ _id: sessionId, session_name: "" }));
|
|
417
|
-
await insertCall(createOutGoingCallDoc({ sessionId, status: "completed", createdAt: new Date() }));
|
|
418
|
-
|
|
419
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
420
|
-
|
|
421
|
-
expect(rows).toHaveLength(1);
|
|
422
|
-
expect(rows[0]).toMatchObject({ sessionId: sessionId.toString(), sessionName: "Unknown" });
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it("should only include the given client's sessions when clientId is provided", async () => {
|
|
426
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
427
|
-
const sessionA = new ObjectId();
|
|
428
|
-
const sessionB = new ObjectId();
|
|
429
|
-
await getSessionsCollection().insertMany([
|
|
430
|
-
createSession({ _id: sessionA, session_name: "Session A" }),
|
|
431
|
-
createSession({ _id: sessionB, session_name: "Session B" }),
|
|
432
|
-
]);
|
|
433
|
-
await Promise.all([
|
|
434
|
-
insertCall(createOutGoingCallDoc({ clientId: "clientA", sessionId: sessionA, status: "completed", createdAt: new Date() })),
|
|
435
|
-
insertCall(createOutGoingCallDoc({ clientId: "clientB", sessionId: sessionB, status: "completed", createdAt: new Date() })),
|
|
436
|
-
]);
|
|
437
|
-
|
|
438
|
-
const rows = await aggregateSessionCallStats({
|
|
439
|
-
dateFrom: today,
|
|
440
|
-
dateTo: today,
|
|
441
|
-
clientId: "clientA",
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
expect(rows).toHaveLength(1);
|
|
445
|
-
expect(rows[0]).toMatchObject({ sessionId: sessionA.toString(), sessionName: "Session A" });
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it("should exclude a session with no calls in the given date range", async () => {
|
|
449
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
450
|
-
const longAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
451
|
-
const oldSessionId = new ObjectId();
|
|
452
|
-
await getSessionsCollection().insertOne(createSession({ _id: oldSessionId, session_name: "Old Session" }));
|
|
453
|
-
await insertCall(createOutGoingCallDoc({ sessionId: oldSessionId, status: "completed", createdAt: longAgo }));
|
|
454
|
-
|
|
455
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
456
|
-
|
|
457
|
-
expect(rows.find((r) => r.sessionId === oldSessionId.toString())).toBeUndefined();
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it("should include a session's full history when no date range is given", async () => {
|
|
461
|
-
const longAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
462
|
-
const sessionId = new ObjectId();
|
|
463
|
-
await getSessionsCollection().insertOne(createSession({ _id: sessionId, session_name: "All Time Session" }));
|
|
464
|
-
await insertCall(createOutGoingCallDoc({ sessionId, status: "completed", createdAt: longAgo }));
|
|
465
|
-
|
|
466
|
-
const rows = await aggregateSessionCallStats({});
|
|
467
|
-
const row = rows.find((r) => r.sessionId === sessionId.toString());
|
|
468
|
-
|
|
469
|
-
expect(row?.totalCalls).toBe(1);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it("should count each real status separately, including queued", async () => {
|
|
473
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
474
|
-
const sessionId = new ObjectId();
|
|
475
|
-
await getSessionsCollection().insertOne(createSession({ _id: sessionId, session_name: "Bucket Session" }));
|
|
476
|
-
await Promise.all([
|
|
477
|
-
insertCall(createOutGoingCallDoc({ sessionId, status: "completed", createdAt: new Date() })),
|
|
478
|
-
insertCall(createOutGoingCallDoc({ sessionId, status: "canceled", createdAt: new Date() })),
|
|
479
|
-
insertCall(createOutGoingCallDoc({ sessionId, status: "queued" as any, createdAt: new Date() })),
|
|
480
|
-
]);
|
|
481
|
-
|
|
482
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
483
|
-
const row = rows.find((r) => r.sessionId === sessionId.toString());
|
|
484
|
-
|
|
485
|
-
expect(row?.totalCalls).toBe(3);
|
|
486
|
-
expect(row?.statusCounts).toMatchObject({ completed: 1, canceled: 1, queued: 1 });
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
describe("findLastCallDatesByClientIds", () => {
|
|
491
|
-
it("should map each requested clientId to its most recent call", async () => {
|
|
492
|
-
const earlierCall = new Date("2026-01-01T00:00:00.000Z");
|
|
493
|
-
const laterCall = new Date("2026-03-10T00:00:00.000Z");
|
|
494
|
-
await Promise.all([
|
|
495
|
-
insertCall(createOutGoingCallDoc({ clientId: "lastCallMapClientA", status: "completed", createdAt: earlierCall })),
|
|
496
|
-
insertCall(createOutGoingCallDoc({ clientId: "lastCallMapClientA", status: "completed", createdAt: laterCall })),
|
|
497
|
-
insertCall(createOutGoingCallDoc({ clientId: "lastCallMapClientB", status: "completed", createdAt: earlierCall })),
|
|
498
|
-
]);
|
|
499
|
-
|
|
500
|
-
const map = await findLastCallDatesByClientIds(["lastCallMapClientA"]);
|
|
501
|
-
|
|
502
|
-
expect(map.get("lastCallMapClientA")).toEqual(laterCall);
|
|
503
|
-
expect(map.has("lastCallMapClientB")).toBe(false);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it("should return an empty map when given no clientIds", async () => {
|
|
507
|
-
const map = await findLastCallDatesByClientIds([]);
|
|
508
|
-
expect(map.size).toBe(0);
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
describe("findClientIdsWithCalls", () => {
|
|
513
|
-
it("should return each client with a call in the range once", async () => {
|
|
514
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
515
|
-
const sixMonthsAgo = new Date(Date.now() - 180 * 24 * 60 * 60 * 1000);
|
|
516
|
-
await Promise.all([
|
|
517
|
-
insertCall(createOutGoingCallDoc({ clientId: "recentCaller", status: "completed", createdAt: new Date() })),
|
|
518
|
-
insertCall(createOutGoingCallDoc({ clientId: "recentCaller", status: "completed", createdAt: new Date() })),
|
|
519
|
-
insertCall(createOutGoingCallDoc({ clientId: "oldCaller", status: "completed", createdAt: sixMonthsAgo })),
|
|
520
|
-
]);
|
|
521
|
-
|
|
522
|
-
const clientIds = await findClientIdsWithCalls({ dateFrom: today, dateTo: today });
|
|
523
|
-
|
|
524
|
-
expect(clientIds).toEqual(["recentCaller"]);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
it("should return every client with any call when no range is given", async () => {
|
|
528
|
-
const sixMonthsAgo = new Date(Date.now() - 180 * 24 * 60 * 60 * 1000);
|
|
529
|
-
await Promise.all([
|
|
530
|
-
insertCall(createOutGoingCallDoc({ clientId: "recentCaller", status: "completed", createdAt: new Date() })),
|
|
531
|
-
insertCall(createOutGoingCallDoc({ clientId: "oldCaller", status: "completed", createdAt: sixMonthsAgo })),
|
|
532
|
-
]);
|
|
533
|
-
|
|
534
|
-
const clientIds = await findClientIdsWithCalls({});
|
|
535
|
-
|
|
536
|
-
expect(clientIds.sort()).toEqual(["oldCaller", "recentCaller"]);
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
describe("findClientIdsByCallSids", () => {
|
|
541
|
-
it("should map each callSid to the clientId that owns it", async () => {
|
|
542
|
-
await Promise.all([
|
|
543
|
-
insertCall(createOutGoingCallDoc({ clientId: "clientA", callSid: "CAaaa" })),
|
|
544
|
-
insertCall(createOutGoingCallDoc({ clientId: "clientB", callSid: "CAbbb" })),
|
|
545
|
-
]);
|
|
546
|
-
|
|
547
|
-
const map = await findClientIdsByCallSids(["CAaaa", "CAbbb", "CAunknown"]);
|
|
548
|
-
|
|
549
|
-
expect(map.get("CAaaa")).toBe("clientA");
|
|
550
|
-
expect(map.get("CAbbb")).toBe("clientB");
|
|
551
|
-
expect(map.has("CAunknown")).toBe(false);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
it("should return an empty map when given no callSids", async () => {
|
|
555
|
-
const map = await findClientIdsByCallSids([]);
|
|
556
|
-
expect(map.size).toBe(0);
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
});
|