@talkpilot/core-db 1.3.52 → 1.3.53
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 +2 -21
- package/dist/talkpilot/calls/calls.constants.d.ts +1 -1
- package/dist/talkpilot/clients/clients.getters.d.ts +0 -1
- package/dist/talkpilot/clients/clients.getters.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.getters.js +1 -8
- package/dist/talkpilot/clients/clients.getters.js.map +1 -1
- package/dist/talkpilot/clients/clients.types.d.ts +0 -21
- 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.getter.d.ts +4 -1
- package/dist/talkpilot/flows/flows.getter.d.ts.map +1 -1
- package/dist/talkpilot/flows/flows.getter.js +12 -1
- package/dist/talkpilot/flows/flows.getter.js.map +1 -1
- package/dist/talkpilot/flows/flows.schema.d.ts +31 -0
- package/dist/talkpilot/flows/flows.schema.d.ts.map +1 -1
- package/dist/talkpilot/flows/flows.schema.js +25 -0
- package/dist/talkpilot/flows/flows.schema.js.map +1 -1
- package/dist/talkpilot/flows/flows.types.d.ts +14 -0
- package/dist/talkpilot/flows/flows.types.d.ts.map +1 -1
- package/dist/talkpilot/flows/index.d.ts +1 -1
- package/dist/talkpilot/flows/index.d.ts.map +1 -1
- package/dist/talkpilot/flows/index.js +3 -1
- package/dist/talkpilot/flows/index.js.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 +0 -11
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +1 -60
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
- package/dist/talkpilot/results/index.d.ts +0 -3
- package/dist/talkpilot/results/index.d.ts.map +1 -1
- package/dist/talkpilot/results/index.js +1 -22
- package/dist/talkpilot/results/index.js.map +1 -1
- package/dist/talkpilot/results/results.getter.d.ts.map +1 -1
- package/dist/talkpilot/results/results.getter.js +2 -3
- package/dist/talkpilot/results/results.getter.js.map +1 -1
- 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 +1 -0
- package/dist/websitalk/scanResources/scanResources.getters.d.ts.map +1 -1
- package/dist/websitalk/scanResources/scanResources.getters.js +6 -1
- package/dist/websitalk/scanResources/scanResources.getters.js.map +1 -1
- package/dist/websitalk/scans/scans.constants.d.ts +3 -1
- package/dist/websitalk/scans/scans.constants.d.ts.map +1 -1
- package/dist/websitalk/scans/scans.constants.js +3 -0
- package/dist/websitalk/scans/scans.constants.js.map +1 -1
- package/dist/websitalk/scans/scans.getters.d.ts +1 -0
- package/dist/websitalk/scans/scans.getters.d.ts.map +1 -1
- package/dist/websitalk/scans/scans.getters.js +6 -1
- package/dist/websitalk/scans/scans.getters.js.map +1 -1
- package/package.json +1 -1
- package/src/talkpilot/clients/clients.getters.ts +0 -7
- package/src/talkpilot/clients/clients.types.ts +0 -19
- package/src/talkpilot/clients/index.ts +0 -2
- package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +25 -0
- package/src/talkpilot/flows/flows.getter.ts +22 -2
- package/src/talkpilot/flows/flows.schema.ts +25 -0
- package/src/talkpilot/flows/flows.types.ts +22 -0
- package/src/talkpilot/flows/index.ts +6 -1
- package/src/talkpilot/index.ts +0 -1
- package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +13 -227
- package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +1 -76
- package/src/talkpilot/results/index.ts +0 -9
- package/src/talkpilot/results/results.getter.ts +2 -3
- package/src/test-utils/factories/index.ts +0 -2
- package/src/websitalk/scanResources/__tests__/scanResources.spec.ts +19 -0
- package/src/websitalk/scanResources/scanResources.getters.ts +7 -0
- package/src/websitalk/scans/__tests__/scans.spec.ts +28 -0
- package/src/websitalk/scans/scans.constants.ts +3 -0
- package/src/websitalk/scans/scans.getters.ts +5 -0
- 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.setters.d.ts +0 -3
- package/dist/talkpilot/auditLogs/auditLogs.setters.d.ts.map +0 -1
- package/dist/talkpilot/auditLogs/auditLogs.setters.js +0 -9
- package/dist/talkpilot/auditLogs/auditLogs.setters.js.map +0 -1
- package/dist/talkpilot/auditLogs/auditLogs.types.d.ts +0 -12
- 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/clientDeletionLogs/clientDeletionLogs.getters.d.ts +0 -4
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.d.ts.map +0 -1
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.js +0 -9
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.js.map +0 -1
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.d.ts +0 -3
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.d.ts.map +0 -1
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.js +0 -9
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.js.map +0 -1
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.d.ts +0 -10
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.d.ts.map +0 -1
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.js +0 -3
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.js.map +0 -1
- package/dist/talkpilot/clientDeletionLogs/index.d.ts +0 -4
- package/dist/talkpilot/clientDeletionLogs/index.d.ts.map +0 -1
- package/dist/talkpilot/clientDeletionLogs/index.js +0 -20
- package/dist/talkpilot/clientDeletionLogs/index.js.map +0 -1
- package/dist/talkpilot/clients/clients.constants.d.ts +0 -3
- package/dist/talkpilot/clients/clients.constants.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.constants.js +0 -6
- 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 -95
- package/dist/talkpilot/clients/clients.lifecycle.js.map +0 -1
- package/dist/talkpilot/results/results.constants.d.ts +0 -9
- package/dist/talkpilot/results/results.constants.d.ts.map +0 -1
- package/dist/talkpilot/results/results.constants.js +0 -12
- package/dist/talkpilot/results/results.constants.js.map +0 -1
- package/dist/talkpilot/results/results.statistics.getters.d.ts +0 -7
- package/dist/talkpilot/results/results.statistics.getters.d.ts.map +0 -1
- package/dist/talkpilot/results/results.statistics.getters.js +0 -244
- package/dist/talkpilot/results/results.statistics.getters.js.map +0 -1
- package/dist/talkpilot/results/results.statistics.types.d.ts +0 -42
- package/dist/talkpilot/results/results.statistics.types.d.ts.map +0 -1
- package/dist/talkpilot/results/results.statistics.types.js +0 -3
- package/dist/talkpilot/results/results.statistics.types.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/src/talkpilot/auditLogs/__tests__/auditLogs.spec.ts +0 -92
- package/src/talkpilot/auditLogs/auditLogs.getters.ts +0 -26
- package/src/talkpilot/auditLogs/auditLogs.types.ts +0 -13
- package/src/talkpilot/auditLogs/index.ts +0 -2
- package/src/talkpilot/clients/__tests__/clients.getters.spec.ts +0 -33
- package/src/talkpilot/clients/__tests__/clients.lifecycle.spec.ts +0 -166
- package/src/talkpilot/clients/clients.constants.ts +0 -2
- package/src/talkpilot/clients/clients.lifecycle.ts +0 -116
- package/src/talkpilot/results/__tests__/results.statistics.spec.ts +0 -226
- package/src/talkpilot/results/results.constants.ts +0 -11
- package/src/talkpilot/results/results.statistics.getters.ts +0 -329
- package/src/talkpilot/results/results.statistics.types.ts +0 -45
- package/src/test-utils/factories/talkpilot/clients.ts +0 -21
- package/src/test-utils/factories/talkpilot/results.ts +0 -43
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
aggregateActiveCallsSummary,
|
|
3
|
-
aggregateTodayCallsSummary,
|
|
4
|
-
aggregateClientCallStats,
|
|
5
|
-
aggregateSessionCallStats,
|
|
6
|
-
findClientIdsByCallSids,
|
|
7
|
-
} from "../results.statistics.getters";
|
|
8
|
-
import { getResultsCollection } from "../results.getter";
|
|
9
|
-
import { createResult, createCallResult } from "../../../test-utils/factories";
|
|
10
|
-
|
|
11
|
-
describe("results statistics getters", () => {
|
|
12
|
-
describe("aggregateActiveCallsSummary", () => {
|
|
13
|
-
it("should count distinct active calls within the last 2 hours, split incoming/outgoing", async () => {
|
|
14
|
-
const now = new Date();
|
|
15
|
-
await getResultsCollection().insertMany([
|
|
16
|
-
createResult({
|
|
17
|
-
clientId: "clientA",
|
|
18
|
-
sessionId: "incomingCall",
|
|
19
|
-
calls: [createCallResult({ status: "in-progress", date: now })],
|
|
20
|
-
}),
|
|
21
|
-
createResult({
|
|
22
|
-
clientId: "clientB",
|
|
23
|
-
sessionId: "someSessionId",
|
|
24
|
-
calls: [createCallResult({ status: "in-progress", date: now })],
|
|
25
|
-
}),
|
|
26
|
-
createResult({
|
|
27
|
-
clientId: "clientC",
|
|
28
|
-
sessionId: "incomingCall",
|
|
29
|
-
calls: [createCallResult({ status: "completed", date: now })],
|
|
30
|
-
}),
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
const summary = await aggregateActiveCallsSummary();
|
|
34
|
-
|
|
35
|
-
expect(summary.total).toBe(2);
|
|
36
|
-
expect(summary.incoming).toBe(1);
|
|
37
|
-
expect(summary.outgoing).toBe(1);
|
|
38
|
-
expect(summary.activeClientIds.sort()).toEqual(["clientA", "clientB"]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("should filter by clientId when provided", async () => {
|
|
42
|
-
const now = new Date();
|
|
43
|
-
await getResultsCollection().insertMany([
|
|
44
|
-
createResult({
|
|
45
|
-
clientId: "onlyThisClient",
|
|
46
|
-
calls: [createCallResult({ status: "in-progress", date: now })],
|
|
47
|
-
}),
|
|
48
|
-
createResult({
|
|
49
|
-
clientId: "otherClient",
|
|
50
|
-
calls: [createCallResult({ status: "in-progress", date: now })],
|
|
51
|
-
}),
|
|
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 older than 2 hours", async () => {
|
|
60
|
-
const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000);
|
|
61
|
-
await getResultsCollection().insertOne(
|
|
62
|
-
createResult({
|
|
63
|
-
calls: [
|
|
64
|
-
createCallResult({ status: "in-progress", date: threeHoursAgo }),
|
|
65
|
-
],
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const summary = await aggregateActiveCallsSummary();
|
|
70
|
-
expect(summary.total).toBe(0);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("aggregateTodayCallsSummary", () => {
|
|
75
|
-
it("should aggregate status/type counts and total minutes within the date range", async () => {
|
|
76
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
77
|
-
await getResultsCollection().insertMany([
|
|
78
|
-
createResult({
|
|
79
|
-
sessionId: "incomingCall",
|
|
80
|
-
calls: [
|
|
81
|
-
createCallResult({
|
|
82
|
-
status: "completed",
|
|
83
|
-
callLength: "120 seconds",
|
|
84
|
-
date: new Date(),
|
|
85
|
-
}),
|
|
86
|
-
],
|
|
87
|
-
}),
|
|
88
|
-
createResult({
|
|
89
|
-
sessionId: "someSession",
|
|
90
|
-
calls: [
|
|
91
|
-
createCallResult({
|
|
92
|
-
status: "failed",
|
|
93
|
-
callLength: "30 seconds",
|
|
94
|
-
date: new Date(),
|
|
95
|
-
}),
|
|
96
|
-
],
|
|
97
|
-
}),
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
const summary = await aggregateTodayCallsSummary({ dateFrom: today, dateTo: today });
|
|
101
|
-
|
|
102
|
-
expect(summary.total).toBe(2);
|
|
103
|
-
expect(summary.incoming).toBe(1);
|
|
104
|
-
expect(summary.outgoing).toBe(1);
|
|
105
|
-
expect(summary.completed).toBe(1);
|
|
106
|
-
expect(summary.failed).toBe(1);
|
|
107
|
-
expect(summary.totalMinutes).toBe(3); // ceil(150/60)
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("should respect the given timezone when resolving the default (today) range", async () => {
|
|
111
|
-
// 23:30 UTC on 2026-01-01 is already 2026-01-02 in Asia/Jerusalem (+2).
|
|
112
|
-
const lateUtc = new Date("2026-01-01T23:30:00.000Z");
|
|
113
|
-
await getResultsCollection().insertOne(
|
|
114
|
-
createResult({
|
|
115
|
-
calls: [createCallResult({ status: "completed", date: lateUtc })],
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const summaryUtc = await aggregateTodayCallsSummary({
|
|
120
|
-
dateFrom: "2026-01-01",
|
|
121
|
-
dateTo: "2026-01-01",
|
|
122
|
-
timezone: "UTC",
|
|
123
|
-
});
|
|
124
|
-
expect(summaryUtc.total).toBe(1);
|
|
125
|
-
|
|
126
|
-
const summaryJerusalem = await aggregateTodayCallsSummary({
|
|
127
|
-
dateFrom: "2026-01-01",
|
|
128
|
-
dateTo: "2026-01-01",
|
|
129
|
-
timezone: "Asia/Jerusalem",
|
|
130
|
-
});
|
|
131
|
-
expect(summaryJerusalem.total).toBe(0);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe("aggregateClientCallStats", () => {
|
|
136
|
-
it("should group call counts and minutes by clientId", async () => {
|
|
137
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
138
|
-
await getResultsCollection().insertOne(
|
|
139
|
-
createResult({
|
|
140
|
-
clientId: "clientX",
|
|
141
|
-
calls: [
|
|
142
|
-
createCallResult({
|
|
143
|
-
status: "completed",
|
|
144
|
-
callLength: "60 seconds",
|
|
145
|
-
date: new Date(),
|
|
146
|
-
}),
|
|
147
|
-
createCallResult({
|
|
148
|
-
status: "failed",
|
|
149
|
-
callLength: "60 seconds",
|
|
150
|
-
date: new Date(),
|
|
151
|
-
}),
|
|
152
|
-
],
|
|
153
|
-
}),
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const rows = await aggregateClientCallStats({ dateFrom: today, dateTo: today });
|
|
157
|
-
const row = rows.find((r) => r.clientId === "clientX");
|
|
158
|
-
|
|
159
|
-
expect(row).toMatchObject({ totalCalls: 2, completed: 1, failed: 1 });
|
|
160
|
-
expect(row?.totalMinutes).toBe(2);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("should not include other clients when filtered by clientId", async () => {
|
|
164
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
165
|
-
await getResultsCollection().insertMany([
|
|
166
|
-
createResult({
|
|
167
|
-
clientId: "wantedClient",
|
|
168
|
-
calls: [createCallResult({ status: "completed", date: new Date() })],
|
|
169
|
-
}),
|
|
170
|
-
createResult({
|
|
171
|
-
clientId: "unwantedClient",
|
|
172
|
-
calls: [createCallResult({ status: "completed", date: new Date() })],
|
|
173
|
-
}),
|
|
174
|
-
]);
|
|
175
|
-
|
|
176
|
-
const rows = await aggregateClientCallStats({
|
|
177
|
-
dateFrom: today,
|
|
178
|
-
dateTo: today,
|
|
179
|
-
clientId: "wantedClient",
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
expect(rows).toHaveLength(1);
|
|
183
|
-
expect(rows[0].clientId).toBe("wantedClient");
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe("aggregateSessionCallStats", () => {
|
|
188
|
-
it("should group call counts by sessionName", async () => {
|
|
189
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
190
|
-
await getResultsCollection().insertOne(
|
|
191
|
-
createResult({
|
|
192
|
-
sessionName: "My Session",
|
|
193
|
-
calls: [createCallResult({ status: "completed", date: new Date() })],
|
|
194
|
-
}),
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
const rows = await aggregateSessionCallStats({ dateFrom: today, dateTo: today });
|
|
198
|
-
expect(rows.find((r) => r.sessionName === "My Session")).toMatchObject({
|
|
199
|
-
totalCalls: 1,
|
|
200
|
-
completed: 1,
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe("findClientIdsByCallSids", () => {
|
|
206
|
-
it("should map each callSid to the clientId that owns it", async () => {
|
|
207
|
-
const callA = createCallResult({ callSid: "CAaaa" });
|
|
208
|
-
const callB = createCallResult({ callSid: "CAbbb" });
|
|
209
|
-
await getResultsCollection().insertMany([
|
|
210
|
-
createResult({ clientId: "clientA", calls: [callA] }),
|
|
211
|
-
createResult({ clientId: "clientB", calls: [callB] }),
|
|
212
|
-
]);
|
|
213
|
-
|
|
214
|
-
const map = await findClientIdsByCallSids(["CAaaa", "CAbbb", "CAunknown"]);
|
|
215
|
-
|
|
216
|
-
expect(map.get("CAaaa")).toBe("clientA");
|
|
217
|
-
expect(map.get("CAbbb")).toBe("clientB");
|
|
218
|
-
expect(map.has("CAunknown")).toBe(false);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("should return an empty map when given no callSids", async () => {
|
|
222
|
-
const map = await findClientIdsByCallSids([]);
|
|
223
|
-
expect(map.size).toBe(0);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
});
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** sessionId used for calls that arrived without an existing session (incoming calls). */
|
|
2
|
-
export const INCOMING_SESSION_ID = "incomingCall";
|
|
3
|
-
|
|
4
|
-
/** Twilio call statuses considered "currently active" for the live calls summary. */
|
|
5
|
-
export const ACTIVE_STATUSES = ["in-progress"];
|
|
6
|
-
|
|
7
|
-
/** Lookback window for the "active calls right now" summary. */
|
|
8
|
-
export const ACTIVE_CALLS_WINDOW_MS = 2 * 60 * 60 * 1000;
|
|
9
|
-
|
|
10
|
-
/** Max rows returned by the per-session call stats breakdown. */
|
|
11
|
-
export const SESSION_STATS_LIMIT = 20;
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { getResultsCollection } from "./results.getter";
|
|
2
|
-
import {
|
|
3
|
-
formatYmdInTz,
|
|
4
|
-
startOfCalendarDayInTz,
|
|
5
|
-
endOfCalendarDayInTz,
|
|
6
|
-
} from "../../utils/date.utils";
|
|
7
|
-
import {
|
|
8
|
-
ACTIVE_STATUSES,
|
|
9
|
-
ACTIVE_CALLS_WINDOW_MS,
|
|
10
|
-
SESSION_STATS_LIMIT,
|
|
11
|
-
INCOMING_SESSION_ID,
|
|
12
|
-
} from "./results.constants";
|
|
13
|
-
import type {
|
|
14
|
-
StatsDateRangeParams,
|
|
15
|
-
ActiveCallsSummary,
|
|
16
|
-
TodayCallsSummary,
|
|
17
|
-
ClientCallStats,
|
|
18
|
-
SessionCallStats,
|
|
19
|
-
} from "./results.statistics.types";
|
|
20
|
-
|
|
21
|
-
const dateRangeFilter = (
|
|
22
|
-
dateFrom: string | undefined,
|
|
23
|
-
dateTo: string | undefined,
|
|
24
|
-
timezone: string,
|
|
25
|
-
): { start: Date; end: Date } => {
|
|
26
|
-
const today = formatYmdInTz(new Date(), timezone);
|
|
27
|
-
return {
|
|
28
|
-
start: startOfCalendarDayInTz(dateFrom ?? today, timezone),
|
|
29
|
-
end: endOfCalendarDayInTz(dateTo ?? today, timezone),
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const buildDateRangeMatch = (
|
|
34
|
-
start: Date,
|
|
35
|
-
end: Date,
|
|
36
|
-
clientId: string | undefined,
|
|
37
|
-
) => {
|
|
38
|
-
const clientFilter = clientId ? { clientId } : {};
|
|
39
|
-
return { ...clientFilter, "calls.date": { $gte: start, $lte: end } };
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Filters on `clientId` (a document-level field) before `$unwind` explodes `calls[]`,
|
|
43
|
-
// so Mongo never has to unwind documents belonging to other clients.
|
|
44
|
-
const clientPreMatchStage = (clientId?: string) =>
|
|
45
|
-
clientId ? [{ $match: { clientId } }] : [];
|
|
46
|
-
|
|
47
|
-
// Converts a duration string like "120 seconds" into a number (120) that can be summed inside an aggregation.
|
|
48
|
-
const durationExpr = {
|
|
49
|
-
$convert: {
|
|
50
|
-
input: {
|
|
51
|
-
$arrayElemAt: [
|
|
52
|
-
{ $split: [{ $ifNull: ["$calls.callLength", "0 seconds"] }, " "] },
|
|
53
|
-
0,
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
to: "double",
|
|
57
|
-
onError: 0,
|
|
58
|
-
onNull: 0,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const statusFields = {
|
|
63
|
-
totalCalls: { $sum: 1 },
|
|
64
|
-
completed: {
|
|
65
|
-
$sum: { $cond: [{ $eq: ["$calls.status", "completed"] }, 1, 0] },
|
|
66
|
-
},
|
|
67
|
-
failed: { $sum: { $cond: [{ $eq: ["$calls.status", "failed"] }, 1, 0] } },
|
|
68
|
-
noAnswer: {
|
|
69
|
-
$sum: { $cond: [{ $eq: ["$calls.status", "no-answer"] }, 1, 0] },
|
|
70
|
-
},
|
|
71
|
-
busy: { $sum: { $cond: [{ $eq: ["$calls.status", "busy"] }, 1, 0] } },
|
|
72
|
-
inProgress: {
|
|
73
|
-
$sum: { $cond: [{ $eq: ["$calls.status", "in-progress"] }, 1, 0] },
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const aggregateActiveCallsSummary = async (
|
|
78
|
-
clientId?: string,
|
|
79
|
-
): Promise<ActiveCallsSummary> => {
|
|
80
|
-
const coll = getResultsCollection();
|
|
81
|
-
const twoHoursAgo = new Date(Date.now() - ACTIVE_CALLS_WINDOW_MS);
|
|
82
|
-
|
|
83
|
-
const activeMatch = {
|
|
84
|
-
"calls.status": { $in: ACTIVE_STATUSES },
|
|
85
|
-
"calls.date": { $gte: twoHoursAgo },
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const facetResult = await coll
|
|
89
|
-
.aggregate<{
|
|
90
|
-
counts: { total: number; incoming: number; outgoing: number }[];
|
|
91
|
-
activeClients: { _id: string }[];
|
|
92
|
-
}>([
|
|
93
|
-
...clientPreMatchStage(clientId),
|
|
94
|
-
{ $match: { "calls.date": { $gte: twoHoursAgo } } },
|
|
95
|
-
{ $unwind: "$calls" },
|
|
96
|
-
{ $match: activeMatch },
|
|
97
|
-
{
|
|
98
|
-
$facet: {
|
|
99
|
-
counts: [
|
|
100
|
-
{
|
|
101
|
-
$group: {
|
|
102
|
-
_id: null,
|
|
103
|
-
total: { $addToSet: "$calls.callSid" },
|
|
104
|
-
incoming: {
|
|
105
|
-
$addToSet: {
|
|
106
|
-
$cond: [
|
|
107
|
-
{ $eq: ["$sessionId", INCOMING_SESSION_ID] },
|
|
108
|
-
"$calls.callSid",
|
|
109
|
-
null,
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
outgoing: {
|
|
114
|
-
$addToSet: {
|
|
115
|
-
$cond: [
|
|
116
|
-
{ $ne: ["$sessionId", INCOMING_SESSION_ID] },
|
|
117
|
-
"$calls.callSid",
|
|
118
|
-
null,
|
|
119
|
-
],
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
$project: {
|
|
126
|
-
_id: 0,
|
|
127
|
-
total: { $size: "$total" },
|
|
128
|
-
incoming: {
|
|
129
|
-
$size: { $setDifference: ["$incoming", [null]] },
|
|
130
|
-
},
|
|
131
|
-
outgoing: {
|
|
132
|
-
$size: { $setDifference: ["$outgoing", [null]] },
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
activeClients: [{ $group: { _id: "$clientId" } }],
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
])
|
|
141
|
-
.next();
|
|
142
|
-
|
|
143
|
-
const counts = facetResult?.counts?.[0];
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
total: counts?.total ?? 0,
|
|
147
|
-
incoming: counts?.incoming ?? 0,
|
|
148
|
-
outgoing: counts?.outgoing ?? 0,
|
|
149
|
-
activeClientIds: (facetResult?.activeClients ?? []).map(
|
|
150
|
-
(d) => d._id || "Unknown",
|
|
151
|
-
),
|
|
152
|
-
};
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
export const aggregateTodayCallsSummary = async ({
|
|
156
|
-
dateFrom,
|
|
157
|
-
dateTo,
|
|
158
|
-
clientId,
|
|
159
|
-
timezone = "UTC",
|
|
160
|
-
}: StatsDateRangeParams): Promise<TodayCallsSummary> => {
|
|
161
|
-
const coll = getResultsCollection();
|
|
162
|
-
const { start, end } = dateRangeFilter(dateFrom, dateTo, timezone);
|
|
163
|
-
const match = buildDateRangeMatch(start, end, clientId);
|
|
164
|
-
|
|
165
|
-
const facetResult = await coll
|
|
166
|
-
.aggregate<{
|
|
167
|
-
byStatus: { _id: string; count: number }[];
|
|
168
|
-
byType: { _id: string; count: number }[];
|
|
169
|
-
total: { count: number }[];
|
|
170
|
-
totalSeconds: { seconds: number }[];
|
|
171
|
-
}>([
|
|
172
|
-
...clientPreMatchStage(clientId),
|
|
173
|
-
{ $match: { "calls.date": { $gte: start, $lte: end } } },
|
|
174
|
-
{ $unwind: "$calls" },
|
|
175
|
-
{ $match: match },
|
|
176
|
-
{
|
|
177
|
-
$facet: {
|
|
178
|
-
byStatus: [{ $group: { _id: "$calls.status", count: { $sum: 1 } } }],
|
|
179
|
-
byType: [
|
|
180
|
-
{
|
|
181
|
-
$group: {
|
|
182
|
-
_id: {
|
|
183
|
-
$cond: [
|
|
184
|
-
{ $eq: ["$sessionId", INCOMING_SESSION_ID] },
|
|
185
|
-
"incoming",
|
|
186
|
-
"outgoing",
|
|
187
|
-
],
|
|
188
|
-
},
|
|
189
|
-
count: { $sum: 1 },
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
total: [{ $group: { _id: null, count: { $sum: 1 } } }],
|
|
194
|
-
totalSeconds: [
|
|
195
|
-
{ $group: { _id: null, seconds: { $sum: durationExpr } } },
|
|
196
|
-
],
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
])
|
|
200
|
-
.next();
|
|
201
|
-
|
|
202
|
-
const byStatus: Record<string, number> = {};
|
|
203
|
-
(facetResult?.byStatus ?? []).forEach((s) => {
|
|
204
|
-
byStatus[s._id] = s.count;
|
|
205
|
-
});
|
|
206
|
-
const byType: Record<string, number> = {};
|
|
207
|
-
(facetResult?.byType ?? []).forEach((t) => {
|
|
208
|
-
byType[t._id] = t.count;
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const totalSeconds = facetResult?.totalSeconds?.[0]?.seconds ?? 0;
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
total: facetResult?.total?.[0]?.count ?? 0,
|
|
215
|
-
incoming: byType.incoming ?? 0,
|
|
216
|
-
outgoing: byType.outgoing ?? 0,
|
|
217
|
-
completed: byStatus.completed ?? 0,
|
|
218
|
-
failed: byStatus.failed ?? 0,
|
|
219
|
-
busy: byStatus.busy ?? 0,
|
|
220
|
-
noAnswer: byStatus["no-answer"] ?? 0,
|
|
221
|
-
totalMinutes: Math.ceil(totalSeconds / 60),
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
export const aggregateClientCallStats = async ({
|
|
226
|
-
dateFrom,
|
|
227
|
-
dateTo,
|
|
228
|
-
clientId,
|
|
229
|
-
timezone = "UTC",
|
|
230
|
-
}: StatsDateRangeParams): Promise<ClientCallStats[]> => {
|
|
231
|
-
const coll = getResultsCollection();
|
|
232
|
-
const { start, end } = dateRangeFilter(dateFrom, dateTo, timezone);
|
|
233
|
-
const match = buildDateRangeMatch(start, end, clientId);
|
|
234
|
-
|
|
235
|
-
const rows = await coll
|
|
236
|
-
.aggregate<{
|
|
237
|
-
_id: string;
|
|
238
|
-
totalCalls: number;
|
|
239
|
-
completed: number;
|
|
240
|
-
failed: number;
|
|
241
|
-
noAnswer: number;
|
|
242
|
-
busy: number;
|
|
243
|
-
inProgress: number;
|
|
244
|
-
totalSeconds: number;
|
|
245
|
-
}>([
|
|
246
|
-
...clientPreMatchStage(clientId),
|
|
247
|
-
{ $match: { "calls.date": { $gte: start, $lte: end } } },
|
|
248
|
-
{ $unwind: "$calls" },
|
|
249
|
-
{ $match: match },
|
|
250
|
-
{
|
|
251
|
-
$group: {
|
|
252
|
-
_id: "$clientId",
|
|
253
|
-
...statusFields,
|
|
254
|
-
totalSeconds: { $sum: durationExpr },
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
{ $sort: { totalCalls: -1 } },
|
|
258
|
-
])
|
|
259
|
-
.toArray();
|
|
260
|
-
|
|
261
|
-
return rows.map((r) => ({
|
|
262
|
-
clientId: r._id || "Unknown",
|
|
263
|
-
totalCalls: r.totalCalls,
|
|
264
|
-
completed: r.completed,
|
|
265
|
-
failed: r.failed,
|
|
266
|
-
noAnswer: r.noAnswer,
|
|
267
|
-
busy: r.busy,
|
|
268
|
-
inProgress: r.inProgress,
|
|
269
|
-
totalMinutes: Math.ceil((r.totalSeconds || 0) / 60),
|
|
270
|
-
}));
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
export const aggregateSessionCallStats = async ({
|
|
274
|
-
dateFrom,
|
|
275
|
-
dateTo,
|
|
276
|
-
clientId,
|
|
277
|
-
timezone = "UTC",
|
|
278
|
-
}: StatsDateRangeParams): Promise<SessionCallStats[]> => {
|
|
279
|
-
const coll = getResultsCollection();
|
|
280
|
-
const { start, end } = dateRangeFilter(dateFrom, dateTo, timezone);
|
|
281
|
-
const match = buildDateRangeMatch(start, end, clientId);
|
|
282
|
-
|
|
283
|
-
const rows = await coll
|
|
284
|
-
.aggregate<{
|
|
285
|
-
_id: string;
|
|
286
|
-
totalCalls: number;
|
|
287
|
-
completed: number;
|
|
288
|
-
failed: number;
|
|
289
|
-
noAnswer: number;
|
|
290
|
-
busy: number;
|
|
291
|
-
inProgress: number;
|
|
292
|
-
}>([
|
|
293
|
-
...clientPreMatchStage(clientId),
|
|
294
|
-
{ $match: { "calls.date": { $gte: start, $lte: end } } },
|
|
295
|
-
{ $unwind: "$calls" },
|
|
296
|
-
{ $match: match },
|
|
297
|
-
{ $group: { _id: "$sessionName", ...statusFields } },
|
|
298
|
-
{ $sort: { totalCalls: -1 } },
|
|
299
|
-
{ $limit: SESSION_STATS_LIMIT },
|
|
300
|
-
])
|
|
301
|
-
.toArray();
|
|
302
|
-
|
|
303
|
-
return rows.map((r) => ({
|
|
304
|
-
sessionName: r._id || "Unknown",
|
|
305
|
-
totalCalls: r.totalCalls,
|
|
306
|
-
completed: r.completed,
|
|
307
|
-
failed: r.failed,
|
|
308
|
-
noAnswer: r.noAnswer,
|
|
309
|
-
busy: r.busy,
|
|
310
|
-
inProgress: r.inProgress,
|
|
311
|
-
}));
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
export const findClientIdsByCallSids = async (
|
|
315
|
-
callSids: string[],
|
|
316
|
-
): Promise<Map<string, string>> => {
|
|
317
|
-
if (callSids.length === 0) return new Map();
|
|
318
|
-
|
|
319
|
-
const docs = await getResultsCollection()
|
|
320
|
-
.aggregate<{ clientId: string; callSid: string }>([
|
|
321
|
-
{ $match: { "calls.callSid": { $in: callSids } } },
|
|
322
|
-
{ $unwind: "$calls" },
|
|
323
|
-
{ $match: { "calls.callSid": { $in: callSids } } },
|
|
324
|
-
{ $project: { _id: 0, clientId: 1, callSid: "$calls.callSid" } },
|
|
325
|
-
])
|
|
326
|
-
.toArray();
|
|
327
|
-
|
|
328
|
-
return new Map(docs.map((d) => [d.callSid, d.clientId]));
|
|
329
|
-
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export type StatsDateRangeParams = {
|
|
2
|
-
dateFrom?: string;
|
|
3
|
-
dateTo?: string;
|
|
4
|
-
clientId?: string;
|
|
5
|
-
timezone?: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export type ActiveCallsSummary = {
|
|
9
|
-
total: number;
|
|
10
|
-
incoming: number;
|
|
11
|
-
outgoing: number;
|
|
12
|
-
activeClientIds: string[];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type TodayCallsSummary = {
|
|
16
|
-
total: number;
|
|
17
|
-
incoming: number;
|
|
18
|
-
outgoing: number;
|
|
19
|
-
completed: number;
|
|
20
|
-
failed: number;
|
|
21
|
-
busy: number;
|
|
22
|
-
noAnswer: number;
|
|
23
|
-
totalMinutes: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type ClientCallStats = {
|
|
27
|
-
clientId: string;
|
|
28
|
-
totalCalls: number;
|
|
29
|
-
completed: number;
|
|
30
|
-
failed: number;
|
|
31
|
-
noAnswer: number;
|
|
32
|
-
busy: number;
|
|
33
|
-
inProgress: number;
|
|
34
|
-
totalMinutes: number;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export type SessionCallStats = {
|
|
38
|
-
sessionName: string;
|
|
39
|
-
totalCalls: number;
|
|
40
|
-
completed: number;
|
|
41
|
-
failed: number;
|
|
42
|
-
noAnswer: number;
|
|
43
|
-
busy: number;
|
|
44
|
-
inProgress: number;
|
|
45
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Factory } from "fishery";
|
|
2
|
-
import { ObjectId } from "mongodb";
|
|
3
|
-
import { faker } from "@faker-js/faker";
|
|
4
|
-
import type { Client } from "../../../talkpilot";
|
|
5
|
-
|
|
6
|
-
export const clientFactory = Factory.define<Client & { _id?: ObjectId }>(
|
|
7
|
-
() => ({
|
|
8
|
-
_id: new ObjectId(),
|
|
9
|
-
clientId: faker.string.uuid(),
|
|
10
|
-
isActive: true,
|
|
11
|
-
credits: faker.number.int({ min: 0, max: 1000 }),
|
|
12
|
-
createdAt: new Date(),
|
|
13
|
-
updatedAt: new Date(),
|
|
14
|
-
}),
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
export function createClient(
|
|
18
|
-
overrides?: Partial<Client>,
|
|
19
|
-
): Client & { _id?: ObjectId } {
|
|
20
|
-
return clientFactory.build(overrides);
|
|
21
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Factory } from "fishery";
|
|
2
|
-
import { ObjectId } from "mongodb";
|
|
3
|
-
import { faker } from "@faker-js/faker";
|
|
4
|
-
import type { Result, CallResult } from "../../../talkpilot";
|
|
5
|
-
|
|
6
|
-
export const callResultFactory = Factory.define<CallResult>(() => ({
|
|
7
|
-
callSid: `CA${faker.string.alphanumeric(32)}`,
|
|
8
|
-
status: "completed",
|
|
9
|
-
date: new Date(),
|
|
10
|
-
statuses: ["completed"],
|
|
11
|
-
userAnswer: null,
|
|
12
|
-
color: null,
|
|
13
|
-
name: null,
|
|
14
|
-
callLength: "60 seconds",
|
|
15
|
-
answeredBy: null,
|
|
16
|
-
to: faker.phone.number(),
|
|
17
|
-
from: faker.phone.number(),
|
|
18
|
-
transcription: [],
|
|
19
|
-
flowId: new ObjectId().toString(),
|
|
20
|
-
recordingUrl: null,
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
export function createCallResult(overrides?: Partial<CallResult>): CallResult {
|
|
24
|
-
return callResultFactory.build(overrides);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const resultFactory = Factory.define<Result & { _id?: ObjectId }>(
|
|
28
|
-
() => ({
|
|
29
|
-
_id: new ObjectId(),
|
|
30
|
-
calls: [],
|
|
31
|
-
clientId: faker.string.uuid(),
|
|
32
|
-
date: new Date(),
|
|
33
|
-
runId: `run_${new ObjectId().toString()}`,
|
|
34
|
-
sessionId: "incomingCall",
|
|
35
|
-
sessionName: "incomingCall",
|
|
36
|
-
}),
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
export function createResult(
|
|
40
|
-
overrides?: Partial<Result>,
|
|
41
|
-
): Result & { _id?: ObjectId } {
|
|
42
|
-
return resultFactory.build(overrides);
|
|
43
|
-
}
|