@talkpilot/core-db 1.3.88 → 1.3.89
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 +65 -12
- 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.types.d.ts +11 -0
- package/dist/talkpilot/auditLogs/auditLogs.types.d.ts.map +1 -0
- package/dist/talkpilot/auditLogs/auditLogs.types.js +3 -0
- 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/auditLogs/index.js +19 -0
- package/dist/talkpilot/auditLogs/index.js.map +1 -0
- package/dist/talkpilot/calls/calls.constants.d.ts +11 -1
- package/dist/talkpilot/calls/calls.constants.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.constants.js +11 -1
- package/dist/talkpilot/calls/calls.constants.js.map +1 -1
- package/dist/talkpilot/calls/calls.getters.d.ts +8 -1
- package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.getters.js +216 -1
- package/dist/talkpilot/calls/calls.getters.js.map +1 -1
- package/dist/talkpilot/calls/calls.types.d.ts +45 -0
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.constants.d.ts +4 -0
- package/dist/talkpilot/clients/clients.constants.d.ts.map +1 -0
- package/dist/talkpilot/clients/clients.constants.js +7 -0
- package/dist/talkpilot/clients/clients.constants.js.map +1 -0
- package/dist/talkpilot/clients/clients.getters.d.ts +6 -0
- package/dist/talkpilot/clients/clients.getters.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.getters.js +62 -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 +109 -0
- package/dist/talkpilot/clients/clients.lifecycle.js.map +1 -0
- package/dist/talkpilot/clients/clients.types.d.ts +53 -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/contextNotes/contextNotes.getters.d.ts +1 -7
- package/dist/talkpilot/contextNotes/contextNotes.getters.d.ts.map +1 -1
- package/dist/talkpilot/contextNotes/contextNotes.getters.js +1 -37
- package/dist/talkpilot/contextNotes/contextNotes.getters.js.map +1 -1
- package/dist/talkpilot/contextNotes/contextNotes.types.d.ts +0 -6
- package/dist/talkpilot/contextNotes/contextNotes.types.d.ts.map +1 -1
- package/dist/talkpilot/index.d.ts +1 -0
- package/dist/talkpilot/index.d.ts.map +1 -1
- package/dist/talkpilot/index.js +1 -0
- 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 +70 -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/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/dist/utils/regex.utils.d.ts +2 -0
- package/dist/utils/regex.utils.d.ts.map +1 -0
- package/dist/utils/regex.utils.js +6 -0
- package/dist/utils/regex.utils.js.map +1 -0
- package/dist/websitalk/scanResources/scanResources.getters.d.ts.map +1 -1
- package/dist/websitalk/scanResources/scanResources.getters.js +2 -1
- 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/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 +559 -0
- package/src/talkpilot/calls/calls.constants.ts +14 -0
- package/src/talkpilot/calls/calls.getters.ts +320 -0
- package/src/talkpilot/calls/calls.types.ts +52 -0
- package/src/talkpilot/clients/__tests__/clients.getters.spec.ts +224 -0
- package/src/talkpilot/clients/__tests__/clients.lifecycle.spec.ts +194 -0
- package/src/talkpilot/clients/clients.constants.ts +4 -0
- package/src/talkpilot/clients/clients.getters.ts +91 -2
- package/src/talkpilot/clients/clients.lifecycle.ts +132 -0
- package/src/talkpilot/clients/clients.types.ts +58 -0
- package/src/talkpilot/clients/index.ts +2 -0
- package/src/talkpilot/contextNotes/__tests__/contextNotes.getters.spec.ts +0 -141
- package/src/talkpilot/contextNotes/contextNotes.getters.ts +1 -75
- package/src/talkpilot/contextNotes/contextNotes.types.ts +0 -7
- package/src/talkpilot/index.ts +1 -0
- package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +284 -13
- package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +89 -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/src/utils/regex.utils.ts +2 -0
- package/src/websitalk/scanResources/scanResources.getters.ts +2 -6
- package/src/websitalk/scanResources/scanResources.utils.ts +2 -3
- package/dist/configuration/gptLive.seed.d.ts +0 -2
- package/dist/configuration/gptLive.seed.d.ts.map +0 -1
- package/dist/configuration/gptLive.seed.js +0 -59
- package/dist/configuration/gptLive.seed.js.map +0 -1
- package/dist/talkpilot/clients/clients.quota.constants.d.ts +0 -2
- package/dist/talkpilot/clients/clients.quota.constants.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.quota.constants.js +0 -5
- package/dist/talkpilot/clients/clients.quota.constants.js.map +0 -1
- package/dist/talkpilot/clients/clients.quota.utils.d.ts +0 -8
- package/dist/talkpilot/clients/clients.quota.utils.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.quota.utils.js +0 -45
- package/dist/talkpilot/clients/clients.quota.utils.js.map +0 -1
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { Collection, ObjectId, WithId } from "mongodb";
|
|
2
2
|
import { getDb } from "../index";
|
|
3
|
-
import type {
|
|
4
|
-
ContextNote,
|
|
5
|
-
ContextNoteEntry,
|
|
6
|
-
ContextNoteEntryWrite,
|
|
7
|
-
} from "./contextNotes.types";
|
|
3
|
+
import type { ContextNote, ContextNoteEntry } from "./contextNotes.types";
|
|
8
4
|
|
|
9
5
|
export const getContextNotesCollection = (): Collection<ContextNote> => {
|
|
10
6
|
return getDb().collection<ContextNote>("contextNotes");
|
|
@@ -75,76 +71,6 @@ export const setContextNoteConfig = async (
|
|
|
75
71
|
return result.matchedCount > 0;
|
|
76
72
|
};
|
|
77
73
|
|
|
78
|
-
const findNoteEntryById = (
|
|
79
|
-
doc: WithId<ContextNote> | null,
|
|
80
|
-
noteId: ObjectId,
|
|
81
|
-
): ContextNoteEntry | null =>
|
|
82
|
-
doc?.notes.find((note) => note._id.equals(noteId)) ?? null;
|
|
83
|
-
|
|
84
|
-
/** Appends one note. Other notes on the document are left untouched. */
|
|
85
|
-
export const appendContextNoteEntry = async (
|
|
86
|
-
clientId: string,
|
|
87
|
-
product: string,
|
|
88
|
-
entry: ContextNoteEntryWrite,
|
|
89
|
-
now: Date = new Date(),
|
|
90
|
-
): Promise<ContextNoteEntry> => {
|
|
91
|
-
const note: ContextNoteEntry = {
|
|
92
|
-
_id: new ObjectId(),
|
|
93
|
-
title: entry.title,
|
|
94
|
-
content: entry.content,
|
|
95
|
-
activeFrom: entry.activeFrom,
|
|
96
|
-
expiresAt: entry.expiresAt,
|
|
97
|
-
createdAt: now,
|
|
98
|
-
updatedAt: now,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const saved = await getContextNotesCollection().findOneAndUpdate(
|
|
102
|
-
{ clientId, product },
|
|
103
|
-
{ $push: { notes: note } },
|
|
104
|
-
{ upsert: true, returnDocument: "after" },
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
return findNoteEntryById(saved, note._id) ?? note;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
/** Replaces one note's fields. Other notes on the document are left untouched. */
|
|
111
|
-
export const updateContextNoteEntryFields = async (
|
|
112
|
-
clientId: string,
|
|
113
|
-
product: string,
|
|
114
|
-
noteId: ObjectId,
|
|
115
|
-
entry: ContextNoteEntryWrite,
|
|
116
|
-
now: Date = new Date(),
|
|
117
|
-
): Promise<ContextNoteEntry | null> => {
|
|
118
|
-
const saved = await getContextNotesCollection().findOneAndUpdate(
|
|
119
|
-
{ clientId, product, "notes._id": noteId },
|
|
120
|
-
{
|
|
121
|
-
$set: {
|
|
122
|
-
"notes.$.title": entry.title,
|
|
123
|
-
"notes.$.content": entry.content,
|
|
124
|
-
"notes.$.activeFrom": entry.activeFrom,
|
|
125
|
-
"notes.$.expiresAt": entry.expiresAt,
|
|
126
|
-
"notes.$.updatedAt": now,
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
{ returnDocument: "after" },
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
return findNoteEntryById(saved, noteId);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
/** Removes one note by id. Other notes on the document are left untouched. */
|
|
136
|
-
export const pullContextNoteEntry = async (
|
|
137
|
-
clientId: string,
|
|
138
|
-
product: string,
|
|
139
|
-
noteId: ObjectId,
|
|
140
|
-
): Promise<boolean> => {
|
|
141
|
-
const result = await getContextNotesCollection().updateOne(
|
|
142
|
-
{ clientId, product, "notes._id": noteId },
|
|
143
|
-
{ $pull: { notes: { _id: noteId } } },
|
|
144
|
-
);
|
|
145
|
-
return result.modifiedCount > 0;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
74
|
/** Removes a single note entry from a document by its _id. */
|
|
149
75
|
export const deleteContextNoteEntry = async (
|
|
150
76
|
id: ObjectId,
|
|
@@ -10,13 +10,6 @@ export type ContextNoteEntry = {
|
|
|
10
10
|
updatedAt: Date;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export type ContextNoteEntryWrite = {
|
|
14
|
-
title: string;
|
|
15
|
-
content: string;
|
|
16
|
-
activeFrom: Date;
|
|
17
|
-
expiresAt: Date | null;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
13
|
export type ContextNote = {
|
|
21
14
|
_id: ObjectId;
|
|
22
15
|
clientId: string;
|
package/src/talkpilot/index.ts
CHANGED
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
createPhoneNumberEntity,
|
|
8
8
|
createPurchasedPhoneNumber,
|
|
9
9
|
getPrimaryPhoneFlowId,
|
|
10
|
+
getClientPhones,
|
|
11
|
+
findPhonesByClientIds,
|
|
12
|
+
getDistinctClientPhonePairs,
|
|
13
|
+
findClientsByPhoneNumbers,
|
|
14
|
+
deletePhoneNumberRecords,
|
|
10
15
|
findPhoneNumbersByClientId,
|
|
11
16
|
getOwnedPhoneNumber,
|
|
12
17
|
getPhoneNumberByClientAndFlow,
|
|
@@ -20,7 +25,7 @@ import { ObjectId } from "mongodb";
|
|
|
20
25
|
|
|
21
26
|
describe("db.phoneNumbers", () => {
|
|
22
27
|
describe("getPhoneDataByPhoneNumber", () => {
|
|
23
|
-
it("return phone number data with flow", async () => {
|
|
28
|
+
it("should return phone number data with flow", async () => {
|
|
24
29
|
const flow = createFlow({
|
|
25
30
|
clientId: "test-client-id",
|
|
26
31
|
conversationSettings: {
|
|
@@ -68,7 +73,7 @@ describe("db.phoneNumbers", () => {
|
|
|
68
73
|
});
|
|
69
74
|
|
|
70
75
|
describe("getClientPhoneNumber", () => {
|
|
71
|
-
it("return phone number by client id", async () => {
|
|
76
|
+
it("should return phone number by client id", async () => {
|
|
72
77
|
const flow = createFlow();
|
|
73
78
|
const phoneData = createPhoneNumber({
|
|
74
79
|
flow_id: flow._id,
|
|
@@ -84,7 +89,7 @@ describe("db.phoneNumbers", () => {
|
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
describe("getClientPhoneData", () => {
|
|
87
|
-
it("
|
|
92
|
+
it("should return primary phone data by default", async () => {
|
|
88
93
|
const clientId = "multiPhoneClient";
|
|
89
94
|
const phone1 = createPhoneNumber({
|
|
90
95
|
client_id: clientId,
|
|
@@ -106,7 +111,7 @@ describe("db.phoneNumbers", () => {
|
|
|
106
111
|
expect(result?.phone_number).toBe("2");
|
|
107
112
|
});
|
|
108
113
|
|
|
109
|
-
it("
|
|
114
|
+
it("should return most recent non-primary phone data if isPrimary is false", async () => {
|
|
110
115
|
const clientId = "multiPhoneClient2";
|
|
111
116
|
const phone1 = createPhoneNumber({
|
|
112
117
|
client_id: clientId,
|
|
@@ -137,7 +142,7 @@ describe("db.phoneNumbers", () => {
|
|
|
137
142
|
});
|
|
138
143
|
|
|
139
144
|
describe("getPhoneNumbersForFlows", () => {
|
|
140
|
-
it("
|
|
145
|
+
it("should return all phone numbers for the client with flow ObjectIds, newest first", async () => {
|
|
141
146
|
const clientId = "flowsClient";
|
|
142
147
|
const flow1 = createFlow({ clientId });
|
|
143
148
|
const flow2 = createFlow({ clientId });
|
|
@@ -175,12 +180,12 @@ describe("db.phoneNumbers", () => {
|
|
|
175
180
|
});
|
|
176
181
|
});
|
|
177
182
|
|
|
178
|
-
it("
|
|
183
|
+
it("should return an empty array when the client has no phone numbers", async () => {
|
|
179
184
|
const result = await getPhoneNumbersForFlows("noSuchClient");
|
|
180
185
|
expect(result).toEqual([]);
|
|
181
186
|
});
|
|
182
187
|
|
|
183
|
-
it("
|
|
188
|
+
it("should not include phone numbers for other clients", async () => {
|
|
184
189
|
const clientA = "clientA";
|
|
185
190
|
const clientB = "clientB";
|
|
186
191
|
const flow = createFlow();
|
|
@@ -197,7 +202,7 @@ describe("db.phoneNumbers", () => {
|
|
|
197
202
|
});
|
|
198
203
|
|
|
199
204
|
describe("createPhoneNumberEntity", () => {
|
|
200
|
-
it("
|
|
205
|
+
it("should create first phone number as primary", async () => {
|
|
201
206
|
const clientId = "newClient";
|
|
202
207
|
const flowId = new ObjectId().toHexString();
|
|
203
208
|
const phoneNumber = "+123456789";
|
|
@@ -211,7 +216,7 @@ describe("db.phoneNumbers", () => {
|
|
|
211
216
|
expect(result.is_primary).toBe(true);
|
|
212
217
|
});
|
|
213
218
|
|
|
214
|
-
it("
|
|
219
|
+
it("should create subsequent phone number as non-primary", async () => {
|
|
215
220
|
const clientId = "existingClient";
|
|
216
221
|
const flowId = new ObjectId().toHexString();
|
|
217
222
|
|
|
@@ -226,7 +231,7 @@ describe("db.phoneNumbers", () => {
|
|
|
226
231
|
});
|
|
227
232
|
|
|
228
233
|
describe("createPurchasedPhoneNumber", () => {
|
|
229
|
-
it("
|
|
234
|
+
it("should persist a phone_numbers document with provider payload and flow", async () => {
|
|
230
235
|
const clientId = "purchasedClientDb";
|
|
231
236
|
const flowId = new ObjectId().toHexString();
|
|
232
237
|
const phoneNumber = "+15551234567";
|
|
@@ -258,7 +263,7 @@ describe("db.phoneNumbers", () => {
|
|
|
258
263
|
});
|
|
259
264
|
|
|
260
265
|
describe("getPrimaryPhoneFlowId", () => {
|
|
261
|
-
it("
|
|
266
|
+
it("should return flow_id from primary phone as string", async () => {
|
|
262
267
|
const clientId = "primaryFlowClient";
|
|
263
268
|
const flowId = new ObjectId();
|
|
264
269
|
await getPhoneNumbersCollection().insertOne(
|
|
@@ -272,11 +277,11 @@ describe("db.phoneNumbers", () => {
|
|
|
272
277
|
expect(await getPrimaryPhoneFlowId(clientId)).toBe(flowId.toHexString());
|
|
273
278
|
});
|
|
274
279
|
|
|
275
|
-
it("
|
|
280
|
+
it("should return null when client has no primary phone", async () => {
|
|
276
281
|
expect(await getPrimaryPhoneFlowId("no-phone-client")).toBeNull();
|
|
277
282
|
});
|
|
278
283
|
|
|
279
|
-
it("
|
|
284
|
+
it("should return null when primary phone has no flow_id", async () => {
|
|
280
285
|
const clientId = "phoneNoFlow";
|
|
281
286
|
const phone = createPhoneNumber({
|
|
282
287
|
client_id: clientId,
|
|
@@ -289,6 +294,272 @@ describe("db.phoneNumbers", () => {
|
|
|
289
294
|
});
|
|
290
295
|
});
|
|
291
296
|
|
|
297
|
+
describe("getClientPhones", () => {
|
|
298
|
+
it("should return all phone numbers for the given client, projected to phoneNumber only", async () => {
|
|
299
|
+
const clientId = "phonesClientA";
|
|
300
|
+
await getPhoneNumbersCollection().insertMany([
|
|
301
|
+
createPhoneNumber({ client_id: clientId, phone_number: "+100" }),
|
|
302
|
+
createPhoneNumber({ client_id: clientId, phone_number: "+200" }),
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
const result = await getClientPhones(clientId);
|
|
306
|
+
|
|
307
|
+
expect(result).toHaveLength(2);
|
|
308
|
+
expect(result).toEqual(
|
|
309
|
+
expect.arrayContaining([
|
|
310
|
+
{ phoneNumber: "+100" },
|
|
311
|
+
{ phoneNumber: "+200" },
|
|
312
|
+
]),
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should not include phone numbers belonging to other clients", async () => {
|
|
317
|
+
await getPhoneNumbersCollection().insertOne(
|
|
318
|
+
createPhoneNumber({ client_id: "someOtherClient" }),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const result = await getClientPhones("phonesClientB");
|
|
322
|
+
expect(result).toEqual([]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("should remove exact duplicate phone records for the same client", async () => {
|
|
326
|
+
const clientId = "phonesClientDup";
|
|
327
|
+
await getPhoneNumbersCollection().insertMany([
|
|
328
|
+
createPhoneNumber({ client_id: clientId, phone_number: "+300" }),
|
|
329
|
+
createPhoneNumber({ client_id: clientId, phone_number: "+300" }),
|
|
330
|
+
createPhoneNumber({ client_id: clientId, phone_number: "+400" }),
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
const result = await getClientPhones(clientId);
|
|
334
|
+
|
|
335
|
+
expect(result).toHaveLength(2);
|
|
336
|
+
expect(result).toEqual(
|
|
337
|
+
expect.arrayContaining([
|
|
338
|
+
{ phoneNumber: "+300" },
|
|
339
|
+
{ phoneNumber: "+400" },
|
|
340
|
+
]),
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("findPhonesByClientIds", () => {
|
|
346
|
+
it("should return a Map of clientId to phone number for the requested clients only", async () => {
|
|
347
|
+
await getPhoneNumbersCollection().insertMany([
|
|
348
|
+
createPhoneNumber({ client_id: "mapClientA", phone_number: "+111" }),
|
|
349
|
+
createPhoneNumber({ client_id: "mapClientB", phone_number: "+222" }),
|
|
350
|
+
createPhoneNumber({ client_id: "mapClientC", phone_number: "+333" }),
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
const result = await findPhonesByClientIds(["mapClientA", "mapClientB"]);
|
|
354
|
+
|
|
355
|
+
expect(result).toBeInstanceOf(Map);
|
|
356
|
+
expect(result.get("mapClientA")).toBe("+111");
|
|
357
|
+
expect(result.get("mapClientB")).toBe("+222");
|
|
358
|
+
expect(result.has("mapClientC")).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should return an empty Map for an empty list of clients", async () => {
|
|
362
|
+
await getPhoneNumbersCollection().insertOne(
|
|
363
|
+
createPhoneNumber({ client_id: "mapClientA", phone_number: "+111" }),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const result = await findPhonesByClientIds([]);
|
|
367
|
+
|
|
368
|
+
expect(result.size).toBe(0);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("should skip clients that have no phone number", async () => {
|
|
372
|
+
const result = await findPhonesByClientIds(["clientWithoutPhone"]);
|
|
373
|
+
expect(result.size).toBe(0);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("should return the most recently updated phone number when a client has several", async () => {
|
|
377
|
+
const clientId = "mapClientRecent";
|
|
378
|
+
await getPhoneNumbersCollection().insertMany([
|
|
379
|
+
createPhoneNumber({
|
|
380
|
+
client_id: clientId,
|
|
381
|
+
phone_number: "+old",
|
|
382
|
+
updatedAt: new Date("2023-01-01"),
|
|
383
|
+
}),
|
|
384
|
+
createPhoneNumber({
|
|
385
|
+
client_id: clientId,
|
|
386
|
+
phone_number: "+new",
|
|
387
|
+
updatedAt: new Date("2023-06-01"),
|
|
388
|
+
}),
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
const result = await findPhonesByClientIds([clientId]);
|
|
392
|
+
|
|
393
|
+
expect(result.get(clientId)).toBe("+new");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("should prefer the is_primary number over a more recently updated non-primary one", async () => {
|
|
397
|
+
const clientId = "mapClientPrimary";
|
|
398
|
+
await getPhoneNumbersCollection().insertMany([
|
|
399
|
+
createPhoneNumber({
|
|
400
|
+
client_id: clientId,
|
|
401
|
+
phone_number: "+primary",
|
|
402
|
+
is_primary: true,
|
|
403
|
+
updatedAt: new Date("2023-01-01"),
|
|
404
|
+
}),
|
|
405
|
+
createPhoneNumber({
|
|
406
|
+
client_id: clientId,
|
|
407
|
+
phone_number: "+secondary",
|
|
408
|
+
is_primary: false,
|
|
409
|
+
updatedAt: new Date("2023-06-01"),
|
|
410
|
+
}),
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
const result = await findPhonesByClientIds([clientId]);
|
|
414
|
+
|
|
415
|
+
expect(result.get(clientId)).toBe("+primary");
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("getDistinctClientPhonePairs", () => {
|
|
420
|
+
it("should return unique client+phone pairs, collapsing exact duplicates", async () => {
|
|
421
|
+
await getPhoneNumbersCollection().insertMany([
|
|
422
|
+
createPhoneNumber({ client_id: "pairClientA", phone_number: "+500" }),
|
|
423
|
+
createPhoneNumber({ client_id: "pairClientA", phone_number: "+500" }),
|
|
424
|
+
createPhoneNumber({ client_id: "pairClientB", phone_number: "+600" }),
|
|
425
|
+
]);
|
|
426
|
+
|
|
427
|
+
const result = await getDistinctClientPhonePairs();
|
|
428
|
+
|
|
429
|
+
expect(result).toEqual(
|
|
430
|
+
expect.arrayContaining([
|
|
431
|
+
{ clientId: "pairClientA", phoneNumber: "+500", isPrimary: true },
|
|
432
|
+
{ clientId: "pairClientB", phoneNumber: "+600", isPrimary: true },
|
|
433
|
+
]),
|
|
434
|
+
);
|
|
435
|
+
expect(
|
|
436
|
+
result.filter(
|
|
437
|
+
(p) => p.clientId === "pairClientA" && p.phoneNumber === "+500",
|
|
438
|
+
),
|
|
439
|
+
).toHaveLength(1);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("should keep separate pairs when the same number is shared by different clients", async () => {
|
|
443
|
+
await getPhoneNumbersCollection().insertMany([
|
|
444
|
+
createPhoneNumber({ client_id: "sharedClientA", phone_number: "+700" }),
|
|
445
|
+
createPhoneNumber({ client_id: "sharedClientB", phone_number: "+700" }),
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
const result = await getDistinctClientPhonePairs();
|
|
449
|
+
|
|
450
|
+
expect(result).toEqual(
|
|
451
|
+
expect.arrayContaining([
|
|
452
|
+
{ clientId: "sharedClientA", phoneNumber: "+700", isPrimary: true },
|
|
453
|
+
{ clientId: "sharedClientB", phoneNumber: "+700", isPrimary: true },
|
|
454
|
+
]),
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("should report isPrimary: false for a non-primary number", async () => {
|
|
459
|
+
await getPhoneNumbersCollection().insertMany([
|
|
460
|
+
createPhoneNumber({ client_id: "mixedClient", phone_number: "+900", is_primary: true }),
|
|
461
|
+
createPhoneNumber({ client_id: "mixedClient", phone_number: "+901", is_primary: false }),
|
|
462
|
+
]);
|
|
463
|
+
|
|
464
|
+
const result = await getDistinctClientPhonePairs();
|
|
465
|
+
|
|
466
|
+
expect(result).toEqual(
|
|
467
|
+
expect.arrayContaining([
|
|
468
|
+
{ clientId: "mixedClient", phoneNumber: "+900", isPrimary: true },
|
|
469
|
+
{ clientId: "mixedClient", phoneNumber: "+901", isPrimary: false },
|
|
470
|
+
]),
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("should report isPrimary: false when no phone_numbers document for that client is marked primary", async () => {
|
|
475
|
+
await getPhoneNumbersCollection().insertOne(
|
|
476
|
+
createPhoneNumber({ client_id: "noPrimaryClient", phone_number: "+902", is_primary: false }),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const result = await getDistinctClientPhonePairs();
|
|
480
|
+
|
|
481
|
+
expect(result).toContainEqual({
|
|
482
|
+
clientId: "noPrimaryClient",
|
|
483
|
+
phoneNumber: "+902",
|
|
484
|
+
isPrimary: false,
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe("getDistinctClientPhonePairs with clientIds", () => {
|
|
490
|
+
it("should return only the pairs of the given clients", async () => {
|
|
491
|
+
await getPhoneNumbersCollection().insertMany([
|
|
492
|
+
createPhoneNumber({ client_id: "wantedPairClient", phone_number: "+111" }),
|
|
493
|
+
createPhoneNumber({ client_id: "otherPairClient", phone_number: "+222" }),
|
|
494
|
+
]);
|
|
495
|
+
|
|
496
|
+
const result = await getDistinctClientPhonePairs(["wantedPairClient"]);
|
|
497
|
+
|
|
498
|
+
expect(result).toEqual([
|
|
499
|
+
{ clientId: "wantedPairClient", phoneNumber: "+111", isPrimary: true },
|
|
500
|
+
]);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe("findClientsByPhoneNumbers", () => {
|
|
505
|
+
it("should return other clients sharing each phone number, excluding the given client", async () => {
|
|
506
|
+
await getPhoneNumbersCollection().insertMany([
|
|
507
|
+
createPhoneNumber({ client_id: "excludeMe", phone_number: "+800" }),
|
|
508
|
+
createPhoneNumber({ client_id: "otherClient1", phone_number: "+800" }),
|
|
509
|
+
createPhoneNumber({ client_id: "otherClient2", phone_number: "+800" }),
|
|
510
|
+
]);
|
|
511
|
+
|
|
512
|
+
const result = await findClientsByPhoneNumbers(["+800"], "excludeMe");
|
|
513
|
+
|
|
514
|
+
expect(result.get("+800")).toEqual(
|
|
515
|
+
expect.arrayContaining(["otherClient1", "otherClient2"]),
|
|
516
|
+
);
|
|
517
|
+
expect(result.get("+800")).not.toContain("excludeMe");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("should return an empty array for a phone number with no other clients", async () => {
|
|
521
|
+
await getPhoneNumbersCollection().insertOne(
|
|
522
|
+
createPhoneNumber({ client_id: "soleOwner", phone_number: "+900" }),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const result = await findClientsByPhoneNumbers(["+900"], "soleOwner");
|
|
526
|
+
|
|
527
|
+
expect(result.get("+900")).toEqual([]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("should return an empty map immediately when given no phone numbers", async () => {
|
|
531
|
+
const result = await findClientsByPhoneNumbers([]);
|
|
532
|
+
expect(result.size).toBe(0);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
describe("deletePhoneNumberRecords", () => {
|
|
537
|
+
it("should delete every record for the given phone number across all clients", async () => {
|
|
538
|
+
await getPhoneNumbersCollection().insertMany([
|
|
539
|
+
createPhoneNumber({ client_id: "delClientA", phone_number: "+1000" }),
|
|
540
|
+
createPhoneNumber({ client_id: "delClientB", phone_number: "+1000" }),
|
|
541
|
+
createPhoneNumber({ client_id: "delClientC", phone_number: "+2000" }),
|
|
542
|
+
]);
|
|
543
|
+
|
|
544
|
+
const deletedCount = await deletePhoneNumberRecords("+1000");
|
|
545
|
+
|
|
546
|
+
expect(deletedCount).toBe(2);
|
|
547
|
+
const remaining = await getPhoneNumbersCollection()
|
|
548
|
+
.find({ phone_number: "+1000" })
|
|
549
|
+
.toArray();
|
|
550
|
+
expect(remaining).toHaveLength(0);
|
|
551
|
+
const untouched = await getPhoneNumbersCollection().findOne({
|
|
552
|
+
phone_number: "+2000",
|
|
553
|
+
});
|
|
554
|
+
expect(untouched?.client_id).toBe("delClientC");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("should return 0 when no records match the phone number", async () => {
|
|
558
|
+
const deletedCount = await deletePhoneNumberRecords("+9999999");
|
|
559
|
+
expect(deletedCount).toBe(0);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
292
563
|
describe("findPhoneNumbersByClientId", () => {
|
|
293
564
|
it("returns only that client's numbers", async () => {
|
|
294
565
|
const clientId = "list-client";
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../index";
|
|
10
10
|
import {
|
|
11
11
|
ClientPhoneNumberForFlow,
|
|
12
|
+
ClientPhonePair,
|
|
12
13
|
PhoneNumber,
|
|
13
14
|
PhoneNumberWithFlow,
|
|
14
15
|
PurchasedPhoneProviderPayload,
|
|
@@ -158,7 +159,9 @@ export const findClientByPhoneNumber = async (
|
|
|
158
159
|
return client;
|
|
159
160
|
};
|
|
160
161
|
|
|
161
|
-
export const getPrimaryPhoneFlowId = async (
|
|
162
|
+
export const getPrimaryPhoneFlowId = async (
|
|
163
|
+
clientId: string,
|
|
164
|
+
): Promise<string | null> => {
|
|
162
165
|
const phone = await getClientPhoneData(clientId);
|
|
163
166
|
return phone?.flow_id ? String(phone.flow_id) : null;
|
|
164
167
|
};
|
|
@@ -176,6 +179,91 @@ export const bindClientPhoneToFlow = async (
|
|
|
176
179
|
}
|
|
177
180
|
};
|
|
178
181
|
|
|
182
|
+
export const getClientPhones = async (
|
|
183
|
+
clientId: string,
|
|
184
|
+
): Promise<{ phoneNumber: string }[]> => {
|
|
185
|
+
const docs = await getPhoneNumbersCollection()
|
|
186
|
+
.aggregate<{ _id: string }>([
|
|
187
|
+
{ $match: { client_id: clientId } },
|
|
188
|
+
{ $group: { _id: "$phone_number" } },
|
|
189
|
+
])
|
|
190
|
+
.toArray();
|
|
191
|
+
return docs.map((d) => ({ phoneNumber: d._id }));
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const findPhonesByClientIds = async (
|
|
195
|
+
clientIds: string[],
|
|
196
|
+
): Promise<Map<string, string>> => {
|
|
197
|
+
if (clientIds.length === 0) return new Map();
|
|
198
|
+
|
|
199
|
+
const docs = await getPhoneNumbersCollection()
|
|
200
|
+
.aggregate<{ _id: string; phone_number: string }>([
|
|
201
|
+
{ $match: { client_id: { $in: clientIds } } },
|
|
202
|
+
{ $sort: { is_primary: -1, updatedAt: -1 } },
|
|
203
|
+
{ $group: { _id: "$client_id", phone_number: { $first: "$phone_number" } } },
|
|
204
|
+
])
|
|
205
|
+
.toArray();
|
|
206
|
+
return new Map(docs.map((d) => [d._id, d.phone_number]));
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const getDistinctClientPhonePairs = async (
|
|
210
|
+
clientIds?: string[],
|
|
211
|
+
): Promise<ClientPhonePair[]> => {
|
|
212
|
+
const docs = await getPhoneNumbersCollection()
|
|
213
|
+
.aggregate<{
|
|
214
|
+
_id: { client_id: string; phone_number: string };
|
|
215
|
+
isPrimary: boolean;
|
|
216
|
+
}>([
|
|
217
|
+
...(clientIds ? [{ $match: { client_id: { $in: clientIds } } }] : []),
|
|
218
|
+
{
|
|
219
|
+
$group: {
|
|
220
|
+
_id: { client_id: "$client_id", phone_number: "$phone_number" },
|
|
221
|
+
isPrimary: { $max: "$is_primary" },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
])
|
|
225
|
+
.toArray();
|
|
226
|
+
return docs.map((d) => ({
|
|
227
|
+
clientId: d._id.client_id,
|
|
228
|
+
phoneNumber: d._id.phone_number,
|
|
229
|
+
isPrimary: Boolean(d.isPrimary),
|
|
230
|
+
}));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const findClientsByPhoneNumbers = async (
|
|
234
|
+
phoneNumbers: string[],
|
|
235
|
+
excludeClientId?: string,
|
|
236
|
+
): Promise<Map<string, string[]>> => {
|
|
237
|
+
const result = new Map<string, string[]>(
|
|
238
|
+
phoneNumbers.map((p) => [p, [] as string[]]),
|
|
239
|
+
);
|
|
240
|
+
if (phoneNumbers.length === 0) return result;
|
|
241
|
+
|
|
242
|
+
const docs = await getPhoneNumbersCollection()
|
|
243
|
+
.aggregate<{ _id: string; clientIds: string[] }>([
|
|
244
|
+
{
|
|
245
|
+
$match: {
|
|
246
|
+
phone_number: { $in: phoneNumbers },
|
|
247
|
+
...(excludeClientId ? { client_id: { $ne: excludeClientId } } : {}),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{ $group: { _id: "$phone_number", clientIds: { $addToSet: "$client_id" } } },
|
|
251
|
+
])
|
|
252
|
+
.toArray();
|
|
253
|
+
|
|
254
|
+
docs.forEach((d) => result.set(d._id, d.clientIds));
|
|
255
|
+
return result;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/** Deletes this number's record for every client that references it. Check `findClientsByPhoneNumbers` first to see who's affected. */
|
|
259
|
+
export const deletePhoneNumberRecords = async (
|
|
260
|
+
phoneNumber: string,
|
|
261
|
+
): Promise<number> => {
|
|
262
|
+
const result = await getPhoneNumbersCollection().deleteMany({
|
|
263
|
+
phone_number: phoneNumber,
|
|
264
|
+
});
|
|
265
|
+
return result.deletedCount;
|
|
266
|
+
};
|
|
179
267
|
export const findPhoneNumbersByClientId = (
|
|
180
268
|
clientId: string,
|
|
181
269
|
): Promise<PhoneNumber[]> =>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./talkpilot/agents";
|
|
2
|
+
export * from "./talkpilot/clients";
|
|
2
3
|
export * from "./talkpilot/calls";
|
|
3
4
|
export * from "./talkpilot/clientAudioBuffers";
|
|
4
5
|
export * from "./talkpilot/clientsConfig";
|
|
@@ -7,6 +8,7 @@ export * from "./talkpilot/contextNotes";
|
|
|
7
8
|
export * from "./talkpilot/groups";
|
|
8
9
|
export * from "./talkpilot/phone_numbers";
|
|
9
10
|
export * from "./talkpilot/sessions";
|
|
11
|
+
export * from "./talkpilot/results";
|
|
10
12
|
export * from "./municipal/cities";
|
|
11
13
|
export * from "./municipal/departmentsSubjects";
|
|
12
14
|
export * from "./municipal/muniIssues";
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|