@talkpilot/core-db 1.3.64 → 1.3.66
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 +863 -816
- package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts +8 -0
- package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts.map +1 -0
- package/dist/talkpilot/auditLogs/auditLogs.getters.js +26 -0
- package/dist/talkpilot/auditLogs/auditLogs.getters.js.map +1 -0
- package/dist/talkpilot/auditLogs/auditLogs.setters.d.ts +3 -0
- package/dist/talkpilot/auditLogs/auditLogs.setters.d.ts.map +1 -0
- package/dist/talkpilot/auditLogs/auditLogs.setters.js +9 -0
- package/dist/talkpilot/auditLogs/auditLogs.setters.js.map +1 -0
- package/dist/talkpilot/auditLogs/auditLogs.types.d.ts +11 -0
- package/dist/talkpilot/auditLogs/auditLogs.types.d.ts.map +1 -0
- package/dist/talkpilot/{callTokenUsage/callTokenUsage.types.js → auditLogs/auditLogs.types.js} +1 -1
- package/dist/talkpilot/auditLogs/auditLogs.types.js.map +1 -0
- package/dist/talkpilot/auditLogs/index.d.ts +3 -0
- package/dist/talkpilot/auditLogs/index.d.ts.map +1 -0
- package/dist/talkpilot/{callTokenUsage → auditLogs}/index.js +2 -1
- package/dist/talkpilot/auditLogs/index.js.map +1 -0
- package/dist/talkpilot/calls/calls.constants.d.ts +9 -1
- package/dist/talkpilot/calls/calls.constants.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.constants.js +9 -1
- package/dist/talkpilot/calls/calls.constants.js.map +1 -1
- package/dist/talkpilot/calls/calls.getters.d.ts +7 -1
- package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.getters.js +181 -1
- package/dist/talkpilot/calls/calls.getters.js.map +1 -1
- package/dist/talkpilot/calls/calls.types.d.ts +35 -0
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.d.ts +4 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.d.ts.map +1 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.js +9 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.js.map +1 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.d.ts +3 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.d.ts.map +1 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.js +9 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.js.map +1 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.d.ts +10 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.d.ts.map +1 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.js +3 -0
- package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.js.map +1 -0
- package/dist/talkpilot/clientDeletionLogs/index.d.ts +4 -0
- package/dist/talkpilot/clientDeletionLogs/index.d.ts.map +1 -0
- package/dist/talkpilot/clientDeletionLogs/index.js +20 -0
- package/dist/talkpilot/clientDeletionLogs/index.js.map +1 -0
- package/dist/talkpilot/clients/clients.constants.d.ts +3 -0
- package/dist/talkpilot/clients/clients.constants.d.ts.map +1 -0
- package/dist/talkpilot/clients/clients.constants.js +6 -0
- package/dist/talkpilot/clients/clients.constants.js.map +1 -0
- package/dist/talkpilot/clients/clients.getters.d.ts +2 -0
- package/dist/talkpilot/clients/clients.getters.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.getters.js +15 -1
- package/dist/talkpilot/clients/clients.getters.js.map +1 -1
- package/dist/talkpilot/clients/clients.lifecycle.d.ts +10 -0
- package/dist/talkpilot/clients/clients.lifecycle.d.ts.map +1 -0
- package/dist/talkpilot/clients/clients.lifecycle.js +95 -0
- package/dist/talkpilot/clients/clients.lifecycle.js.map +1 -0
- package/dist/talkpilot/clients/clients.types.d.ts +21 -0
- package/dist/talkpilot/clients/clients.types.d.ts.map +1 -1
- package/dist/talkpilot/clients/index.d.ts +2 -0
- package/dist/talkpilot/clients/index.d.ts.map +1 -1
- package/dist/talkpilot/clients/index.js +2 -0
- package/dist/talkpilot/clients/index.js.map +1 -1
- package/dist/talkpilot/index.d.ts +1 -1
- package/dist/talkpilot/index.d.ts.map +1 -1
- package/dist/talkpilot/index.js +1 -1
- package/dist/talkpilot/index.js.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts +9 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +66 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts +5 -0
- package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts.map +1 -1
- package/dist/talkpilot/results/results.constants.d.ts +4 -0
- package/dist/talkpilot/results/results.constants.d.ts.map +1 -0
- package/dist/talkpilot/results/results.constants.js +10 -0
- package/dist/talkpilot/results/results.constants.js.map +1 -0
- package/dist/talkpilot/results/results.statistics.getters.d.ts +12 -0
- package/dist/talkpilot/results/results.statistics.getters.d.ts.map +1 -0
- package/dist/talkpilot/results/results.statistics.getters.js +258 -0
- package/dist/talkpilot/results/results.statistics.getters.js.map +1 -0
- package/dist/talkpilot/results/results.statistics.types.d.ts +22 -0
- package/dist/talkpilot/results/results.statistics.types.d.ts.map +1 -0
- package/dist/talkpilot/results/results.statistics.types.js +3 -0
- package/dist/talkpilot/results/results.statistics.types.js.map +1 -0
- package/dist/test-utils/factories/index.d.ts +2 -0
- package/dist/test-utils/factories/index.d.ts.map +1 -1
- package/dist/test-utils/factories/index.js +2 -0
- package/dist/test-utils/factories/index.js.map +1 -1
- package/dist/test-utils/factories/talkpilot/clients.d.ts +14 -0
- package/dist/test-utils/factories/talkpilot/clients.d.ts.map +1 -0
- package/dist/test-utils/factories/talkpilot/clients.js +19 -0
- package/dist/test-utils/factories/talkpilot/clients.js.map +1 -0
- package/dist/test-utils/factories/talkpilot/results.d.ts +16 -0
- package/dist/test-utils/factories/talkpilot/results.d.ts.map +1 -0
- package/dist/test-utils/factories/talkpilot/results.js +40 -0
- package/dist/test-utils/factories/talkpilot/results.js.map +1 -0
- package/package.json +1 -1
- package/src/talkpilot/auditLogs/__tests__/auditLogs.spec.ts +91 -0
- package/src/talkpilot/auditLogs/auditLogs.getters.ts +26 -0
- package/src/talkpilot/auditLogs/auditLogs.types.ts +12 -0
- package/src/talkpilot/auditLogs/index.ts +2 -0
- package/src/talkpilot/calls/__tests__/calls.statistics.getters.spec.ts +374 -0
- package/src/talkpilot/calls/calls.constants.ts +12 -0
- package/src/talkpilot/calls/calls.getters.ts +546 -277
- package/src/talkpilot/calls/calls.types.ts +189 -149
- package/src/talkpilot/clients/__tests__/clients.getters.spec.ts +51 -0
- package/src/talkpilot/clients/__tests__/clients.lifecycle.spec.ts +166 -0
- package/src/talkpilot/clients/clients.constants.ts +2 -0
- package/src/talkpilot/clients/clients.getters.ts +13 -0
- package/src/talkpilot/clients/clients.lifecycle.ts +116 -0
- package/src/talkpilot/clients/clients.types.ts +19 -0
- package/src/talkpilot/clients/index.ts +2 -0
- package/src/talkpilot/index.ts +1 -1
- package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +257 -13
- package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +85 -1
- package/src/talkpilot/phone_numbers/phone_numbers.types.ts +6 -0
- package/src/test-utils/factories/index.ts +2 -0
- package/src/test-utils/factories/talkpilot/clients.ts +21 -0
- package/src/test-utils/factories/talkpilot/results.ts +43 -0
- package/dist/talkpilot/callTokenUsage/callTokenUsage.getters.d.ts +0 -4
- package/dist/talkpilot/callTokenUsage/callTokenUsage.getters.d.ts.map +0 -1
- package/dist/talkpilot/callTokenUsage/callTokenUsage.getters.js +0 -30
- package/dist/talkpilot/callTokenUsage/callTokenUsage.getters.js.map +0 -1
- package/dist/talkpilot/callTokenUsage/callTokenUsage.types.d.ts +0 -28
- package/dist/talkpilot/callTokenUsage/callTokenUsage.types.d.ts.map +0 -1
- package/dist/talkpilot/callTokenUsage/callTokenUsage.types.js.map +0 -1
- package/dist/talkpilot/callTokenUsage/index.d.ts +0 -3
- package/dist/talkpilot/callTokenUsage/index.d.ts.map +0 -1
- package/dist/talkpilot/callTokenUsage/index.js.map +0 -1
- package/src/talkpilot/callTokenUsage/__tests__/callTokenUsage.getters.spec.ts +0 -57
- package/src/talkpilot/callTokenUsage/callTokenUsage.getters.ts +0 -42
- package/src/talkpilot/callTokenUsage/callTokenUsage.types.ts +0 -28
- package/src/talkpilot/callTokenUsage/index.ts +0 -2
|
@@ -1,149 +1,189 @@
|
|
|
1
|
-
import { ObjectId, Sort, WithId } from "mongodb";
|
|
2
|
-
import { TranscriptionSegment } from "../results";
|
|
3
|
-
import { LeadProperty } from "../leads";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
CallStatusValues,
|
|
7
|
-
CONFERENCE_ROLE_CUSTOMER,
|
|
8
|
-
CONFERENCE_ROLE_SUPERVISOR,
|
|
9
|
-
} from "./calls.constants";
|
|
10
|
-
|
|
11
|
-
export type ConferenceRole =
|
|
12
|
-
| typeof CONFERENCE_ROLE_CUSTOMER
|
|
13
|
-
| typeof CONFERENCE_ROLE_SUPERVISOR
|
|
14
|
-
| null;
|
|
15
|
-
|
|
16
|
-
export type CallStatus = (typeof CallStatusValues)[keyof typeof CallStatusValues];
|
|
17
|
-
|
|
18
|
-
export type Call = {
|
|
19
|
-
callSid: string;
|
|
20
|
-
flowId: ObjectId;
|
|
21
|
-
clientId: string;
|
|
22
|
-
sessionId: ObjectId | null;
|
|
23
|
-
runId?: string;
|
|
24
|
-
resultId: ObjectId;
|
|
25
|
-
customerPhoneNumber: string;
|
|
26
|
-
agentPhoneNumber: string;
|
|
27
|
-
isIncomingCall: boolean;
|
|
28
|
-
isOutgoingCall: boolean;
|
|
29
|
-
callLength: number;
|
|
30
|
-
transcription?: TranscriptionSegment[];
|
|
31
|
-
status?: CallStatus;
|
|
32
|
-
leads?: LeadProperty[];
|
|
33
|
-
summary?: string;
|
|
34
|
-
recordingUrl?: string;
|
|
35
|
-
env: string;
|
|
36
|
-
updatedAt: Date;
|
|
37
|
-
createdAt: Date;
|
|
38
|
-
isAnsweredByAnsweringMachine?: boolean;
|
|
39
|
-
agentHungUp?: boolean;
|
|
40
|
-
endReason?: string;
|
|
41
|
-
conferenceName?: string | null;
|
|
42
|
-
conferenceSid?: string | null;
|
|
43
|
-
conferenceParticipantSid?: string | null;
|
|
44
|
-
isConferenceCall?: boolean;
|
|
45
|
-
conferenceRole?: ConferenceRole;
|
|
46
|
-
redirectedCall?: boolean;
|
|
47
|
-
/** Present only on redirected calls: what the dialled leg did. */
|
|
48
|
-
redirectCallLog?: RedirectCallLog;
|
|
49
|
-
toolExecutions?: ToolExecution[];
|
|
50
|
-
transcriptionComparison?: Record<string, TranscriptionSegment[]>;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The outcome of the <Dial> that transferred a call, reported by Twilio when the
|
|
55
|
-
* dialled leg ends. `durationSeconds` is the bridged time, which is 0 when the
|
|
56
|
-
* destination never answered.
|
|
57
|
-
*/
|
|
58
|
-
export type RedirectCallLog = {
|
|
59
|
-
childCallSid?: string;
|
|
60
|
-
to?: string;
|
|
61
|
-
status: CallStatus;
|
|
62
|
-
durationSeconds: number;
|
|
63
|
-
endedAt: Date;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export type ToolExecution = {
|
|
67
|
-
toolName: string;
|
|
68
|
-
executedAt: Date;
|
|
69
|
-
durationMs: number;
|
|
70
|
-
args: Record<string, unknown> | { _redacted: true };
|
|
71
|
-
meta: ToolExecutionMeta;
|
|
72
|
-
status: "success" | "error";
|
|
73
|
-
httpStatus?: number;
|
|
74
|
-
response?: Record<string, unknown>;
|
|
75
|
-
steps?: ToolExecutionStep[];
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export type ToolExecutionStepLevel = "info" | "warn" | "error";
|
|
79
|
-
|
|
80
|
-
export type ToolExecutionStep = {
|
|
81
|
-
timestamp: Date;
|
|
82
|
-
label: string;
|
|
83
|
-
level: ToolExecutionStepLevel;
|
|
84
|
-
data?: Record<string, unknown> | string;
|
|
85
|
-
payload?: Record<string, unknown>;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export type ToolExecutionMeta =
|
|
89
|
-
| {
|
|
90
|
-
kind: "http";
|
|
91
|
-
url: string;
|
|
92
|
-
method: string;
|
|
93
|
-
flowToolId?: string;
|
|
94
|
-
runInBackground?: boolean;
|
|
95
|
-
sensitive?: boolean;
|
|
96
|
-
}
|
|
97
|
-
| { kind: "internal" };
|
|
98
|
-
|
|
99
|
-
export type CallQueryOptions = {
|
|
100
|
-
sort?: Sort;
|
|
101
|
-
skip?: number;
|
|
102
|
-
limit?: number;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
export type CallsFilterParams = {
|
|
106
|
-
clientId: string;
|
|
107
|
-
callSid?: string;
|
|
108
|
-
startDate?: Date;
|
|
109
|
-
endDate?: Date;
|
|
110
|
-
status?: CallStatus;
|
|
111
|
-
customerPhoneNumber?: string;
|
|
112
|
-
agentPhoneNumber?: string;
|
|
113
|
-
flowId?: string;
|
|
114
|
-
runId?: string;
|
|
115
|
-
sessionId?: string;
|
|
116
|
-
isIncoming?: boolean;
|
|
117
|
-
search?: string;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export type ImmutableCallFields =
|
|
121
|
-
| "customerPhoneNumber"
|
|
122
|
-
| "agentPhoneNumber"
|
|
123
|
-
| "isIncomingCall"
|
|
124
|
-
| "isOutgoingCall"
|
|
125
|
-
| "sessionId"
|
|
126
|
-
| "callSid"
|
|
127
|
-
| "flowId"
|
|
128
|
-
| "clientId"
|
|
129
|
-
| "runId"
|
|
130
|
-
| "resultId"
|
|
131
|
-
| "env"
|
|
132
|
-
| "createdAt";
|
|
133
|
-
|
|
134
|
-
export type CallUpdateParams = Partial<Omit<Call, ImmutableCallFields>>;
|
|
135
|
-
|
|
136
|
-
export type CallDoc = WithId<Call>;
|
|
137
|
-
|
|
138
|
-
/** @deprecated use DashboardHeatmapMetric instead */
|
|
139
|
-
export type CallsByHour = { hour: number; calls: number };
|
|
140
|
-
|
|
141
|
-
export type CountOpts = {
|
|
142
|
-
isOutgoingCall?: boolean;
|
|
143
|
-
isIncomingCall?: boolean;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export type DateRange = {
|
|
147
|
-
since?: Date;
|
|
148
|
-
until?: Date;
|
|
149
|
-
};
|
|
1
|
+
import { ObjectId, Sort, WithId } from "mongodb";
|
|
2
|
+
import { TranscriptionSegment } from "../results";
|
|
3
|
+
import { LeadProperty } from "../leads";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CallStatusValues,
|
|
7
|
+
CONFERENCE_ROLE_CUSTOMER,
|
|
8
|
+
CONFERENCE_ROLE_SUPERVISOR,
|
|
9
|
+
} from "./calls.constants";
|
|
10
|
+
|
|
11
|
+
export type ConferenceRole =
|
|
12
|
+
| typeof CONFERENCE_ROLE_CUSTOMER
|
|
13
|
+
| typeof CONFERENCE_ROLE_SUPERVISOR
|
|
14
|
+
| null;
|
|
15
|
+
|
|
16
|
+
export type CallStatus = (typeof CallStatusValues)[keyof typeof CallStatusValues];
|
|
17
|
+
|
|
18
|
+
export type Call = {
|
|
19
|
+
callSid: string;
|
|
20
|
+
flowId: ObjectId;
|
|
21
|
+
clientId: string;
|
|
22
|
+
sessionId: ObjectId | null;
|
|
23
|
+
runId?: string;
|
|
24
|
+
resultId: ObjectId;
|
|
25
|
+
customerPhoneNumber: string;
|
|
26
|
+
agentPhoneNumber: string;
|
|
27
|
+
isIncomingCall: boolean;
|
|
28
|
+
isOutgoingCall: boolean;
|
|
29
|
+
callLength: number;
|
|
30
|
+
transcription?: TranscriptionSegment[];
|
|
31
|
+
status?: CallStatus;
|
|
32
|
+
leads?: LeadProperty[];
|
|
33
|
+
summary?: string;
|
|
34
|
+
recordingUrl?: string;
|
|
35
|
+
env: string;
|
|
36
|
+
updatedAt: Date;
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
isAnsweredByAnsweringMachine?: boolean;
|
|
39
|
+
agentHungUp?: boolean;
|
|
40
|
+
endReason?: string;
|
|
41
|
+
conferenceName?: string | null;
|
|
42
|
+
conferenceSid?: string | null;
|
|
43
|
+
conferenceParticipantSid?: string | null;
|
|
44
|
+
isConferenceCall?: boolean;
|
|
45
|
+
conferenceRole?: ConferenceRole;
|
|
46
|
+
redirectedCall?: boolean;
|
|
47
|
+
/** Present only on redirected calls: what the dialled leg did. */
|
|
48
|
+
redirectCallLog?: RedirectCallLog;
|
|
49
|
+
toolExecutions?: ToolExecution[];
|
|
50
|
+
transcriptionComparison?: Record<string, TranscriptionSegment[]>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The outcome of the <Dial> that transferred a call, reported by Twilio when the
|
|
55
|
+
* dialled leg ends. `durationSeconds` is the bridged time, which is 0 when the
|
|
56
|
+
* destination never answered.
|
|
57
|
+
*/
|
|
58
|
+
export type RedirectCallLog = {
|
|
59
|
+
childCallSid?: string;
|
|
60
|
+
to?: string;
|
|
61
|
+
status: CallStatus;
|
|
62
|
+
durationSeconds: number;
|
|
63
|
+
endedAt: Date;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ToolExecution = {
|
|
67
|
+
toolName: string;
|
|
68
|
+
executedAt: Date;
|
|
69
|
+
durationMs: number;
|
|
70
|
+
args: Record<string, unknown> | { _redacted: true };
|
|
71
|
+
meta: ToolExecutionMeta;
|
|
72
|
+
status: "success" | "error";
|
|
73
|
+
httpStatus?: number;
|
|
74
|
+
response?: Record<string, unknown>;
|
|
75
|
+
steps?: ToolExecutionStep[];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type ToolExecutionStepLevel = "info" | "warn" | "error";
|
|
79
|
+
|
|
80
|
+
export type ToolExecutionStep = {
|
|
81
|
+
timestamp: Date;
|
|
82
|
+
label: string;
|
|
83
|
+
level: ToolExecutionStepLevel;
|
|
84
|
+
data?: Record<string, unknown> | string;
|
|
85
|
+
payload?: Record<string, unknown>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type ToolExecutionMeta =
|
|
89
|
+
| {
|
|
90
|
+
kind: "http";
|
|
91
|
+
url: string;
|
|
92
|
+
method: string;
|
|
93
|
+
flowToolId?: string;
|
|
94
|
+
runInBackground?: boolean;
|
|
95
|
+
sensitive?: boolean;
|
|
96
|
+
}
|
|
97
|
+
| { kind: "internal" };
|
|
98
|
+
|
|
99
|
+
export type CallQueryOptions = {
|
|
100
|
+
sort?: Sort;
|
|
101
|
+
skip?: number;
|
|
102
|
+
limit?: number;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type CallsFilterParams = {
|
|
106
|
+
clientId: string;
|
|
107
|
+
callSid?: string;
|
|
108
|
+
startDate?: Date;
|
|
109
|
+
endDate?: Date;
|
|
110
|
+
status?: CallStatus;
|
|
111
|
+
customerPhoneNumber?: string;
|
|
112
|
+
agentPhoneNumber?: string;
|
|
113
|
+
flowId?: string;
|
|
114
|
+
runId?: string;
|
|
115
|
+
sessionId?: string;
|
|
116
|
+
isIncoming?: boolean;
|
|
117
|
+
search?: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type ImmutableCallFields =
|
|
121
|
+
| "customerPhoneNumber"
|
|
122
|
+
| "agentPhoneNumber"
|
|
123
|
+
| "isIncomingCall"
|
|
124
|
+
| "isOutgoingCall"
|
|
125
|
+
| "sessionId"
|
|
126
|
+
| "callSid"
|
|
127
|
+
| "flowId"
|
|
128
|
+
| "clientId"
|
|
129
|
+
| "runId"
|
|
130
|
+
| "resultId"
|
|
131
|
+
| "env"
|
|
132
|
+
| "createdAt";
|
|
133
|
+
|
|
134
|
+
export type CallUpdateParams = Partial<Omit<Call, ImmutableCallFields>>;
|
|
135
|
+
|
|
136
|
+
export type CallDoc = WithId<Call>;
|
|
137
|
+
|
|
138
|
+
/** @deprecated use DashboardHeatmapMetric instead */
|
|
139
|
+
export type CallsByHour = { hour: number; calls: number };
|
|
140
|
+
|
|
141
|
+
export type CountOpts = {
|
|
142
|
+
isOutgoingCall?: boolean;
|
|
143
|
+
isIncomingCall?: boolean;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type DateRange = {
|
|
147
|
+
since?: Date;
|
|
148
|
+
until?: Date;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type StatsDateRangeParams = {
|
|
152
|
+
dateFrom?: string;
|
|
153
|
+
dateTo?: string;
|
|
154
|
+
clientId?: string;
|
|
155
|
+
timezone?: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type ActiveCallsSummary = {
|
|
159
|
+
total: number;
|
|
160
|
+
incoming: number;
|
|
161
|
+
outgoing: number;
|
|
162
|
+
activeClientIds: string[];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export type TodayCallsSummary = {
|
|
166
|
+
total: number;
|
|
167
|
+
incoming: number;
|
|
168
|
+
outgoing: number;
|
|
169
|
+
completed: number;
|
|
170
|
+
failed: number;
|
|
171
|
+
busy: number;
|
|
172
|
+
noAnswer: number;
|
|
173
|
+
totalMinutes: number;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export type ClientCallStats = {
|
|
177
|
+
clientId: string;
|
|
178
|
+
totalCalls: number;
|
|
179
|
+
statusCounts: Record<string, number>;
|
|
180
|
+
totalMinutes: number;
|
|
181
|
+
lastCallDate: Date;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export type SessionCallStats = {
|
|
185
|
+
sessionId: string;
|
|
186
|
+
sessionName: string;
|
|
187
|
+
totalCalls: number;
|
|
188
|
+
statusCounts: Record<string, number>;
|
|
189
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getClientsCollection, getAllClientCredits, getAllClientIds } from "../clients.getters";
|
|
2
|
+
import { createClient } from "../../../test-utils/factories";
|
|
3
|
+
|
|
4
|
+
describe("db.clients", () => {
|
|
5
|
+
describe("getAllClientCredits", () => {
|
|
6
|
+
it("should return a Map of clientId to credits across all clients", async () => {
|
|
7
|
+
await getClientsCollection().insertMany([
|
|
8
|
+
createClient({ clientId: "creditsClientA", credits: 100 }),
|
|
9
|
+
createClient({ clientId: "creditsClientB", credits: 250 }),
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const result = await getAllClientCredits();
|
|
13
|
+
|
|
14
|
+
expect(result).toBeInstanceOf(Map);
|
|
15
|
+
expect(result.get("creditsClientA")).toBe(100);
|
|
16
|
+
expect(result.get("creditsClientB")).toBe(250);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should default missing credits to 0", async () => {
|
|
20
|
+
const client = createClient({ clientId: "noCreditsClient" });
|
|
21
|
+
delete (client as { credits?: number }).credits;
|
|
22
|
+
await getClientsCollection().insertOne(client);
|
|
23
|
+
|
|
24
|
+
const result = await getAllClientCredits();
|
|
25
|
+
expect(result.get("noCreditsClient")).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return an empty Map when there are no clients", async () => {
|
|
29
|
+
const result = await getAllClientCredits();
|
|
30
|
+
expect(result.size).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("getAllClientIds", () => {
|
|
35
|
+
it("should return the ids of all clients", async () => {
|
|
36
|
+
await getClientsCollection().insertMany([
|
|
37
|
+
createClient({ clientId: "idClientA" }),
|
|
38
|
+
createClient({ clientId: "idClientB" }),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const result = await getAllClientIds();
|
|
42
|
+
|
|
43
|
+
expect(result.sort()).toEqual(["idClientA", "idClientB"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should return an empty array when there are no clients", async () => {
|
|
47
|
+
const result = await getAllClientIds();
|
|
48
|
+
expect(result).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clientExistsInAnyCollection,
|
|
3
|
+
deleteClientDataAcrossDomains,
|
|
4
|
+
} from "../clients.lifecycle";
|
|
5
|
+
import { getPhoneNumbersCollection } from "../../phone_numbers/phone_numbers.getter";
|
|
6
|
+
import {
|
|
7
|
+
createMuniIssue,
|
|
8
|
+
generateMuniIssueId,
|
|
9
|
+
getAuditLogsCollection,
|
|
10
|
+
getScansCollection,
|
|
11
|
+
} from "../../../index";
|
|
12
|
+
import {
|
|
13
|
+
createPhoneNumber,
|
|
14
|
+
createMuniIssueInput,
|
|
15
|
+
createScanFixture,
|
|
16
|
+
} from "../../../test-utils/factories";
|
|
17
|
+
|
|
18
|
+
describe("db.clients.lifecycle", () => {
|
|
19
|
+
describe("clientExistsInAnyCollection", () => {
|
|
20
|
+
it("should return true when the client exists only in phone_numbers (client_id field)", async () => {
|
|
21
|
+
const clientId = "existsPhoneOnly";
|
|
22
|
+
await getPhoneNumbersCollection().insertOne(
|
|
23
|
+
createPhoneNumber({ client_id: clientId }),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(await clientExistsInAnyCollection(clientId)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should return true when the client exists only in muniIssues (municipal DB)", async () => {
|
|
30
|
+
const clientId = "existsMuniOnly";
|
|
31
|
+
await createMuniIssue(
|
|
32
|
+
createMuniIssueInput({ clientId }),
|
|
33
|
+
generateMuniIssueId(),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(await clientExistsInAnyCollection(clientId)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return false when the client does not exist anywhere", async () => {
|
|
40
|
+
expect(await clientExistsInAnyCollection("noSuchClientAtAll")).toBe(
|
|
41
|
+
false,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should not consider websitalk data (scans/websiteUrls) at all", async () => {
|
|
46
|
+
const clientId = "onlyInWebsitalk";
|
|
47
|
+
await getScansCollection().insertOne(createScanFixture({ clientId }));
|
|
48
|
+
|
|
49
|
+
expect(await clientExistsInAnyCollection(clientId)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should not be fooled by unrelated collections lacking a clientId/client_id field", async () => {
|
|
53
|
+
await global.db
|
|
54
|
+
.collection("someUnrelatedCollection")
|
|
55
|
+
.insertOne({ someField: "someValue" });
|
|
56
|
+
|
|
57
|
+
expect(await clientExistsInAnyCollection("noSuchClientAtAll")).toBe(
|
|
58
|
+
false,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("deleteClientDataAcrossDomains", () => {
|
|
64
|
+
it("should return deleted:false with a reason and delete nothing when the client does not exist", async () => {
|
|
65
|
+
await getPhoneNumbersCollection().insertOne(
|
|
66
|
+
createPhoneNumber({ client_id: "unrelatedClient" }),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const summary = await deleteClientDataAcrossDomains(
|
|
70
|
+
"missingClient",
|
|
71
|
+
"admin@callai.co.il",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(summary.deleted).toBe(false);
|
|
75
|
+
if (!summary.deleted) {
|
|
76
|
+
expect(summary.reason).toBeTruthy();
|
|
77
|
+
}
|
|
78
|
+
expect(
|
|
79
|
+
await getPhoneNumbersCollection().findOne({
|
|
80
|
+
client_id: "unrelatedClient",
|
|
81
|
+
}),
|
|
82
|
+
).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should not write an audit log entry when the client does not exist", async () => {
|
|
86
|
+
await deleteClientDataAcrossDomains(
|
|
87
|
+
"missingClientNoAudit",
|
|
88
|
+
"admin@callai.co.il",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(
|
|
92
|
+
await getAuditLogsCollection().findOne({
|
|
93
|
+
clientId: "missingClientNoAudit",
|
|
94
|
+
}),
|
|
95
|
+
).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should delete the client's data across talkpilot and municipal domains", async () => {
|
|
99
|
+
const clientId = "deleteMeEverywhere";
|
|
100
|
+
await getPhoneNumbersCollection().insertOne(
|
|
101
|
+
createPhoneNumber({ client_id: clientId }),
|
|
102
|
+
);
|
|
103
|
+
await createMuniIssue(
|
|
104
|
+
createMuniIssueInput({ clientId }),
|
|
105
|
+
generateMuniIssueId(),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const adminEmail = "admin@callai.co.il";
|
|
109
|
+
const summary = await deleteClientDataAcrossDomains(
|
|
110
|
+
clientId,
|
|
111
|
+
adminEmail,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(summary.deleted).toBe(true);
|
|
115
|
+
if (summary.deleted) {
|
|
116
|
+
expect(summary.allSucceeded).toBe(true);
|
|
117
|
+
const byName = (name: string) =>
|
|
118
|
+
summary.results.find((r) => r.collectionName === name);
|
|
119
|
+
expect(byName("phone_numbers")?.deletedCount).toBe(1);
|
|
120
|
+
expect(byName("muniIssues")?.deletedCount).toBe(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
expect(await clientExistsInAnyCollection(clientId)).toBe(false);
|
|
124
|
+
|
|
125
|
+
// The audit entry itself must survive the client's own deletion.
|
|
126
|
+
const auditEntry = await getAuditLogsCollection().findOne({ clientId });
|
|
127
|
+
expect(auditEntry).toMatchObject({
|
|
128
|
+
clientId,
|
|
129
|
+
adminEmail,
|
|
130
|
+
action: "delete",
|
|
131
|
+
});
|
|
132
|
+
expect(auditEntry?.timestamp).toBeInstanceOf(Date);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should not touch websitalk data (scans/websiteUrls) — callers must call the external website-talk deletion API separately", async () => {
|
|
136
|
+
const clientId = "hasWebsitalkData";
|
|
137
|
+
await getPhoneNumbersCollection().insertOne(
|
|
138
|
+
createPhoneNumber({ client_id: clientId }),
|
|
139
|
+
);
|
|
140
|
+
await getScansCollection().insertOne(createScanFixture({ clientId }));
|
|
141
|
+
|
|
142
|
+
await deleteClientDataAcrossDomains(clientId, "admin@callai.co.il");
|
|
143
|
+
|
|
144
|
+
expect(
|
|
145
|
+
await getScansCollection().findOne({ clientId }),
|
|
146
|
+
).not.toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should not delete unrelated clients' data", async () => {
|
|
150
|
+
const targetClient = "deleteTarget";
|
|
151
|
+
const otherClient = "keepMeClient";
|
|
152
|
+
await getPhoneNumbersCollection().insertOne(
|
|
153
|
+
createPhoneNumber({ client_id: targetClient }),
|
|
154
|
+
);
|
|
155
|
+
await getPhoneNumbersCollection().insertOne(
|
|
156
|
+
createPhoneNumber({ client_id: otherClient }),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await deleteClientDataAcrossDomains(targetClient, "admin@callai.co.il");
|
|
160
|
+
|
|
161
|
+
expect(
|
|
162
|
+
await getPhoneNumbersCollection().findOne({ client_id: otherClient }),
|
|
163
|
+
).not.toBeNull();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -15,5 +15,18 @@ export const getClientByAPIKey = async (
|
|
|
15
15
|
return client ? client : null;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
export const getAllClientCredits = async (): Promise<Map<string, number>> => {
|
|
19
|
+
const docs = await getClientsCollection()
|
|
20
|
+
.find({}, { projection: { clientId: 1, credits: 1 } })
|
|
21
|
+
.toArray();
|
|
22
|
+
return new Map(docs.map((c) => [c.clientId, c.credits ?? 0]));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getAllClientIds = async (): Promise<string[]> => {
|
|
26
|
+
const docs = await getClientsCollection()
|
|
27
|
+
.find({}, { projection: { clientId: 1 } })
|
|
28
|
+
.toArray();
|
|
29
|
+
return docs.map((c) => c.clientId);
|
|
30
|
+
};
|
|
18
31
|
export const getClientByClientId = (clientId: string): Promise<Client | null> =>
|
|
19
32
|
getClientsCollection().findOne({ clientId });
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Db } from "mongodb";
|
|
2
|
+
import { getDb, insertAuditLog } from "../index";
|
|
3
|
+
import { getDb as getMunicipalDataDb } from "../../municipal";
|
|
4
|
+
import { DELETE_DENYLIST } from "./clients.constants";
|
|
5
|
+
import {
|
|
6
|
+
CollectionMatch,
|
|
7
|
+
CollectionDeleteResult,
|
|
8
|
+
DeleteClientDataSummary,
|
|
9
|
+
} from "./clients.types";
|
|
10
|
+
|
|
11
|
+
// Skips websitalk on purpose — see the warning on `deleteClientDataAcrossDomains` below.
|
|
12
|
+
const findClientMatchInDb = async (
|
|
13
|
+
db: Db,
|
|
14
|
+
clientId: string,
|
|
15
|
+
): Promise<CollectionMatch[]> => {
|
|
16
|
+
const collections = await db.listCollections().toArray();
|
|
17
|
+
return Promise.all(
|
|
18
|
+
collections
|
|
19
|
+
.filter((c) => !DELETE_DENYLIST.includes(c.name))
|
|
20
|
+
.map(async (c) => {
|
|
21
|
+
const doc = await db
|
|
22
|
+
.collection(c.name)
|
|
23
|
+
.findOne({ $or: [{ clientId }, { client_id: clientId }] });
|
|
24
|
+
return { collectionName: c.name, matched: !!doc };
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const clientExistsInAnyCollection = async (
|
|
30
|
+
clientId: string,
|
|
31
|
+
): Promise<boolean> => {
|
|
32
|
+
const [talkpilotResults, municipalResults] = await Promise.all([
|
|
33
|
+
findClientMatchInDb(getDb(), clientId),
|
|
34
|
+
findClientMatchInDb(getMunicipalDataDb(), clientId),
|
|
35
|
+
]);
|
|
36
|
+
return [...talkpilotResults, ...municipalResults].some((r) => r.matched);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const deleteClientFromDb = async (
|
|
40
|
+
db: Db,
|
|
41
|
+
clientId: string,
|
|
42
|
+
): Promise<CollectionDeleteResult[]> => {
|
|
43
|
+
const collections = await db.listCollections().toArray();
|
|
44
|
+
return Promise.all(
|
|
45
|
+
collections
|
|
46
|
+
.filter((c) => !DELETE_DENYLIST.includes(c.name))
|
|
47
|
+
.map(async (c) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await db
|
|
50
|
+
.collection(c.name)
|
|
51
|
+
.deleteMany({ $or: [{ clientId }, { client_id: clientId }] });
|
|
52
|
+
return {
|
|
53
|
+
collectionName: c.name,
|
|
54
|
+
deletedCount: result.deletedCount,
|
|
55
|
+
success: true,
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
collectionName: c.name,
|
|
60
|
+
deletedCount: 0,
|
|
61
|
+
success: false,
|
|
62
|
+
error: error instanceof Error ? error.message : String(error),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Deletes a client's data across talkpilot/municipal collections and logs the action.
|
|
71
|
+
*
|
|
72
|
+
* ⚠️ Does NOT delete website-talk data — `scans`/`websiteUrls` are the lookup key an
|
|
73
|
+
* external API needs to clean up that domain. Callers must invoke that API separately.
|
|
74
|
+
*/
|
|
75
|
+
export const deleteClientDataAcrossDomains = async (
|
|
76
|
+
clientId: string,
|
|
77
|
+
adminEmail: string,
|
|
78
|
+
): Promise<DeleteClientDataSummary> => {
|
|
79
|
+
const exists = await clientExistsInAnyCollection(clientId);
|
|
80
|
+
if (!exists) {
|
|
81
|
+
return {
|
|
82
|
+
clientId,
|
|
83
|
+
deleted: false,
|
|
84
|
+
reason: "Client does not exist in any collection",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const [talkpilotResults, municipalResults] = await Promise.all([
|
|
89
|
+
deleteClientFromDb(getDb(), clientId),
|
|
90
|
+
deleteClientFromDb(getMunicipalDataDb(), clientId),
|
|
91
|
+
]);
|
|
92
|
+
const results = [...talkpilotResults, ...municipalResults];
|
|
93
|
+
const allSucceeded = results.every((r) => r.success);
|
|
94
|
+
|
|
95
|
+
await insertAuditLog({
|
|
96
|
+
clientId,
|
|
97
|
+
adminEmail,
|
|
98
|
+
action: "delete",
|
|
99
|
+
timestamp: new Date(),
|
|
100
|
+
allSucceeded,
|
|
101
|
+
...(allSucceeded
|
|
102
|
+
? {}
|
|
103
|
+
: {
|
|
104
|
+
failedCollections: results
|
|
105
|
+
.filter((r) => !r.success)
|
|
106
|
+
.map((r) => r.collectionName),
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
clientId,
|
|
112
|
+
deleted: true,
|
|
113
|
+
results,
|
|
114
|
+
allSucceeded,
|
|
115
|
+
};
|
|
116
|
+
};
|