@talkpilot/core-db 1.3.89 → 1.3.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +0 -72
  2. package/dist/talkpilot/calls/calls.constants.d.ts +1 -11
  3. package/dist/talkpilot/calls/calls.constants.d.ts.map +1 -1
  4. package/dist/talkpilot/calls/calls.constants.js +1 -11
  5. package/dist/talkpilot/calls/calls.constants.js.map +1 -1
  6. package/dist/talkpilot/calls/calls.getters.d.ts +1 -8
  7. package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
  8. package/dist/talkpilot/calls/calls.getters.js +1 -216
  9. package/dist/talkpilot/calls/calls.getters.js.map +1 -1
  10. package/dist/talkpilot/calls/calls.types.d.ts +0 -45
  11. package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
  12. package/dist/talkpilot/clients/clients.getters.d.ts +0 -6
  13. package/dist/talkpilot/clients/clients.getters.d.ts.map +1 -1
  14. package/dist/talkpilot/clients/clients.getters.js +1 -62
  15. package/dist/talkpilot/clients/clients.getters.js.map +1 -1
  16. package/dist/talkpilot/clients/clients.types.d.ts +0 -53
  17. package/dist/talkpilot/clients/clients.types.d.ts.map +1 -1
  18. package/dist/talkpilot/clients/index.d.ts +0 -2
  19. package/dist/talkpilot/clients/index.d.ts.map +1 -1
  20. package/dist/talkpilot/clients/index.js +0 -2
  21. package/dist/talkpilot/clients/index.js.map +1 -1
  22. package/dist/talkpilot/flows/flows.schema.d.ts +9 -0
  23. package/dist/talkpilot/flows/flows.schema.d.ts.map +1 -1
  24. package/dist/talkpilot/flows/flows.schema.js +7 -0
  25. package/dist/talkpilot/flows/flows.schema.js.map +1 -1
  26. package/dist/talkpilot/flows/flows.types.d.ts +3 -0
  27. package/dist/talkpilot/flows/flows.types.d.ts.map +1 -1
  28. package/dist/talkpilot/index.d.ts +0 -1
  29. package/dist/talkpilot/index.d.ts.map +1 -1
  30. package/dist/talkpilot/index.js +0 -1
  31. package/dist/talkpilot/index.js.map +1 -1
  32. package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts +1 -9
  33. package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
  34. package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +1 -70
  35. package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
  36. package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts +0 -5
  37. package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts.map +1 -1
  38. package/dist/talkpilot/subscriptions/subscriptions.utils.d.ts +4 -0
  39. package/dist/talkpilot/subscriptions/subscriptions.utils.d.ts.map +1 -0
  40. package/dist/talkpilot/subscriptions/subscriptions.utils.js +20 -0
  41. package/dist/talkpilot/subscriptions/subscriptions.utils.js.map +1 -0
  42. package/dist/test-utils/factories/index.d.ts +0 -2
  43. package/dist/test-utils/factories/index.d.ts.map +1 -1
  44. package/dist/test-utils/factories/index.js +0 -2
  45. package/dist/test-utils/factories/index.js.map +1 -1
  46. package/dist/websitalk/scanResources/scanResources.getters.d.ts.map +1 -1
  47. package/dist/websitalk/scanResources/scanResources.getters.js +1 -2
  48. package/dist/websitalk/scanResources/scanResources.getters.js.map +1 -1
  49. package/dist/websitalk/scanResources/scanResources.utils.d.ts +1 -1
  50. package/dist/websitalk/scanResources/scanResources.utils.d.ts.map +1 -1
  51. package/dist/websitalk/scanResources/scanResources.utils.js +3 -3
  52. package/dist/websitalk/scanResources/scanResources.utils.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/talkpilot/calls/calls.constants.ts +0 -14
  55. package/src/talkpilot/calls/calls.getters.ts +0 -320
  56. package/src/talkpilot/calls/calls.types.ts +0 -52
  57. package/src/talkpilot/clients/clients.getters.ts +2 -91
  58. package/src/talkpilot/clients/clients.types.ts +0 -58
  59. package/src/talkpilot/clients/index.ts +0 -2
  60. package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +11 -0
  61. package/src/talkpilot/flows/flows.schema.ts +7 -0
  62. package/src/talkpilot/flows/flows.types.ts +1 -0
  63. package/src/talkpilot/index.ts +0 -1
  64. package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +13 -284
  65. package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +1 -89
  66. package/src/talkpilot/phone_numbers/phone_numbers.types.ts +0 -6
  67. package/src/test-utils/factories/index.ts +0 -2
  68. package/src/websitalk/scanResources/scanResources.getters.ts +6 -2
  69. package/src/websitalk/scanResources/scanResources.utils.ts +3 -2
  70. package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts +0 -8
  71. package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts.map +0 -1
  72. package/dist/talkpilot/auditLogs/auditLogs.getters.js +0 -26
  73. package/dist/talkpilot/auditLogs/auditLogs.getters.js.map +0 -1
  74. package/dist/talkpilot/auditLogs/auditLogs.types.d.ts +0 -11
  75. package/dist/talkpilot/auditLogs/auditLogs.types.d.ts.map +0 -1
  76. package/dist/talkpilot/auditLogs/auditLogs.types.js +0 -3
  77. package/dist/talkpilot/auditLogs/auditLogs.types.js.map +0 -1
  78. package/dist/talkpilot/auditLogs/index.d.ts +0 -3
  79. package/dist/talkpilot/auditLogs/index.d.ts.map +0 -1
  80. package/dist/talkpilot/auditLogs/index.js +0 -19
  81. package/dist/talkpilot/auditLogs/index.js.map +0 -1
  82. package/dist/talkpilot/clients/clients.constants.d.ts +0 -4
  83. package/dist/talkpilot/clients/clients.constants.d.ts.map +0 -1
  84. package/dist/talkpilot/clients/clients.constants.js +0 -7
  85. package/dist/talkpilot/clients/clients.constants.js.map +0 -1
  86. package/dist/talkpilot/clients/clients.lifecycle.d.ts +0 -10
  87. package/dist/talkpilot/clients/clients.lifecycle.d.ts.map +0 -1
  88. package/dist/talkpilot/clients/clients.lifecycle.js +0 -109
  89. package/dist/talkpilot/clients/clients.lifecycle.js.map +0 -1
  90. package/dist/test-utils/factories/talkpilot/clients.d.ts +0 -14
  91. package/dist/test-utils/factories/talkpilot/clients.d.ts.map +0 -1
  92. package/dist/test-utils/factories/talkpilot/clients.js +0 -19
  93. package/dist/test-utils/factories/talkpilot/clients.js.map +0 -1
  94. package/dist/test-utils/factories/talkpilot/results.d.ts +0 -16
  95. package/dist/test-utils/factories/talkpilot/results.d.ts.map +0 -1
  96. package/dist/test-utils/factories/talkpilot/results.js +0 -40
  97. package/dist/test-utils/factories/talkpilot/results.js.map +0 -1
  98. package/dist/utils/regex.utils.d.ts +0 -2
  99. package/dist/utils/regex.utils.d.ts.map +0 -1
  100. package/dist/utils/regex.utils.js +0 -6
  101. package/dist/utils/regex.utils.js.map +0 -1
  102. package/src/talkpilot/auditLogs/__tests__/auditLogs.spec.ts +0 -91
  103. package/src/talkpilot/auditLogs/auditLogs.getters.ts +0 -26
  104. package/src/talkpilot/auditLogs/auditLogs.types.ts +0 -12
  105. package/src/talkpilot/auditLogs/index.ts +0 -2
  106. package/src/talkpilot/calls/__tests__/calls.statistics.getters.spec.ts +0 -559
  107. package/src/talkpilot/clients/__tests__/clients.getters.spec.ts +0 -224
  108. package/src/talkpilot/clients/__tests__/clients.lifecycle.spec.ts +0 -194
  109. package/src/talkpilot/clients/clients.constants.ts +0 -4
  110. package/src/talkpilot/clients/clients.lifecycle.ts +0 -132
  111. package/src/test-utils/factories/talkpilot/clients.ts +0 -21
  112. package/src/test-utils/factories/talkpilot/results.ts +0 -43
  113. package/src/utils/regex.utils.ts +0 -2
@@ -1,224 +0,0 @@
1
- import {
2
- getClientsCollection,
3
- getAllClientIds,
4
- getBackofficeRole,
5
- setBackofficeRole,
6
- countBackofficeAdmins,
7
- findClientsPage,
8
- } from "../clients.getters";
9
- import { getPhoneNumbersCollection } from "../../phone_numbers/phone_numbers.getter";
10
- import { createClient, createPhoneNumber } from "../../../test-utils/factories";
11
-
12
- describe("db.clients", () => {
13
- describe("getAllClientIds", () => {
14
- it("should return the ids of all clients", async () => {
15
- await getClientsCollection().insertMany([
16
- createClient({ clientId: "idClientA" }),
17
- createClient({ clientId: "idClientB" }),
18
- ]);
19
-
20
- const result = await getAllClientIds();
21
-
22
- expect(result.sort()).toEqual(["idClientA", "idClientB"]);
23
- });
24
-
25
- it("should return an empty array when there are no clients", async () => {
26
- const result = await getAllClientIds();
27
- expect(result).toEqual([]);
28
- });
29
- });
30
-
31
- describe("getBackofficeRole", () => {
32
- it("returns 'admin' when backoffice.admin is set", async () => {
33
- await getClientsCollection().insertOne(
34
- createClient({
35
- clientId: "roleAdmin",
36
- roles: { backoffice: { admin: true } },
37
- }),
38
- );
39
- expect(await getBackofficeRole("roleAdmin")).toBe("admin");
40
- });
41
-
42
- it("returns 'viewer' when only backoffice.viewer is set", async () => {
43
- await getClientsCollection().insertOne(
44
- createClient({
45
- clientId: "roleViewer",
46
- roles: { backoffice: { viewer: true } },
47
- }),
48
- );
49
- expect(await getBackofficeRole("roleViewer")).toBe("viewer");
50
- });
51
-
52
- it("lets admin win when both flags are set", async () => {
53
- await getClientsCollection().insertOne(
54
- createClient({
55
- clientId: "roleBoth",
56
- roles: { backoffice: { admin: true, viewer: true } },
57
- }),
58
- );
59
- expect(await getBackofficeRole("roleBoth")).toBe("admin");
60
- });
61
-
62
- it("ignores the legacy roles.admin flag", async () => {
63
- await getClientsCollection().insertOne(
64
- createClient({ clientId: "legacyAdmin", roles: { admin: true } }),
65
- );
66
- expect(await getBackofficeRole("legacyAdmin")).toBeNull();
67
- });
68
-
69
- it("returns null for false flags, a client without roles, and an unknown client", async () => {
70
- await getClientsCollection().insertMany([
71
- createClient({
72
- clientId: "roleFalse",
73
- roles: { backoffice: { admin: false, viewer: false } },
74
- }),
75
- createClient({ clientId: "noRoles" }),
76
- ]);
77
- expect(await getBackofficeRole("roleFalse")).toBeNull();
78
- expect(await getBackofficeRole("noRoles")).toBeNull();
79
- expect(await getBackofficeRole("missing")).toBeNull();
80
- });
81
- });
82
-
83
- describe("setBackofficeRole", () => {
84
- it("sets one role, clears the other, and leaves roles.admin alone", async () => {
85
- await getClientsCollection().insertOne(
86
- createClient({
87
- clientId: "switchRole",
88
- roles: { admin: true, backoffice: { admin: true } },
89
- }),
90
- );
91
-
92
- expect(await setBackofficeRole("switchRole", "viewer")).toBe(true);
93
- expect(await getBackofficeRole("switchRole")).toBe("viewer");
94
-
95
- await setBackofficeRole("switchRole", null);
96
- expect(await getBackofficeRole("switchRole")).toBeNull();
97
-
98
- const doc = await getClientsCollection().findOne({
99
- clientId: "switchRole",
100
- });
101
- expect(doc?.roles?.admin).toBe(true);
102
- });
103
-
104
- it("returns false for an unknown client", async () => {
105
- expect(await setBackofficeRole("missing", "admin")).toBe(false);
106
- });
107
- });
108
-
109
- describe("countBackofficeAdmins", () => {
110
- it("counts only clients with the backoffice admin flag", async () => {
111
- await getClientsCollection().insertMany([
112
- createClient({
113
- clientId: "adminOne",
114
- roles: { backoffice: { admin: true } },
115
- }),
116
- createClient({
117
- clientId: "viewerOne",
118
- roles: { backoffice: { viewer: true } },
119
- }),
120
- createClient({ clientId: "legacyOnly", roles: { admin: true } }),
121
- ]);
122
- expect(await countBackofficeAdmins()).toBe(1);
123
- });
124
- });
125
-
126
- describe("findClientsPage", () => {
127
- const insertClientsCreatedAt = (...clientIds: string[]) =>
128
- getClientsCollection().insertMany(
129
- clientIds.map((clientId, index) =>
130
- createClient({
131
- clientId,
132
- createdAt: new Date(Date.UTC(2026, 0, index + 1)),
133
- }),
134
- ),
135
- );
136
-
137
- it("returns one page, newest client first, with the total across all pages", async () => {
138
- await insertClientsCreatedAt("pageOldest", "pageMiddle", "pageNewest");
139
-
140
- const firstPage = await findClientsPage({ skip: 0, limit: 2 });
141
- const secondPage = await findClientsPage({ skip: 2, limit: 2 });
142
-
143
- expect(firstPage.clients.map((c) => c.clientId)).toEqual([
144
- "pageNewest",
145
- "pageMiddle",
146
- ]);
147
- expect(firstPage.total).toBe(3);
148
- expect(secondPage.clients.map((c) => c.clientId)).toEqual(["pageOldest"]);
149
- expect(secondPage.total).toBe(3);
150
- });
151
-
152
- it("searches the clientId case-insensitively", async () => {
153
- await insertClientsCreatedAt("SearchMe-1", "other-client");
154
-
155
- const page = await findClientsPage({
156
- skip: 0,
157
- limit: 10,
158
- search: "searchme",
159
- });
160
-
161
- expect(page.clients.map((c) => c.clientId)).toEqual(["SearchMe-1"]);
162
- expect(page.total).toBe(1);
163
- });
164
-
165
- it("searches phone numbers, treating regex characters like + as plain text", async () => {
166
- await insertClientsCreatedAt("phoneOwner", "noPhoneMatch");
167
- await getPhoneNumbersCollection().insertMany([
168
- createPhoneNumber({
169
- client_id: "phoneOwner",
170
- phone_number: "+97233823446",
171
- }),
172
- createPhoneNumber({
173
- client_id: "noPhoneMatch",
174
- phone_number: "+97250000000",
175
- }),
176
- ]);
177
-
178
- const page = await findClientsPage({
179
- skip: 0,
180
- limit: 10,
181
- search: "+9723382",
182
- });
183
-
184
- expect(page.clients.map((c) => c.clientId)).toEqual(["phoneOwner"]);
185
- });
186
-
187
- it("filters to one exact clientId", async () => {
188
- await insertClientsCreatedAt("exactClient", "exactClient-2");
189
-
190
- const page = await findClientsPage({
191
- skip: 0,
192
- limit: 10,
193
- clientId: "exactClient",
194
- });
195
-
196
- expect(page.clients.map((c) => c.clientId)).toEqual(["exactClient"]);
197
- });
198
-
199
- it("restricts to clientIds and leaves out excludeClientIds", async () => {
200
- await insertClientsCreatedAt("listA", "listB", "listC");
201
-
202
- const included = await findClientsPage({
203
- skip: 0,
204
- limit: 10,
205
- clientIds: ["listA", "listC"],
206
- });
207
- const excluded = await findClientsPage({
208
- skip: 0,
209
- limit: 10,
210
- excludeClientIds: ["listA"],
211
- });
212
-
213
- expect(included.clients.map((c) => c.clientId)).toEqual([
214
- "listC",
215
- "listA",
216
- ]);
217
- expect(excluded.clients.map((c) => c.clientId)).toEqual([
218
- "listC",
219
- "listB",
220
- ]);
221
- expect(excluded.total).toBe(2);
222
- });
223
- });
224
- });
@@ -1,194 +0,0 @@
1
- import {
2
- clientExistsInAnyCollection,
3
- deleteClientDataAcrossDomains,
4
- } from "../clients.lifecycle";
5
- import { getPhoneNumbersCollection } from "../../phone_numbers/phone_numbers.getter";
6
- import * as auditLogsGetters from "../../auditLogs/auditLogs.getters";
7
- import {
8
- createMuniIssue,
9
- generateMuniIssueId,
10
- getAuditLogsCollection,
11
- getScansCollection,
12
- } from "../../../index";
13
- import {
14
- createPhoneNumber,
15
- createMuniIssueInput,
16
- createScanFixture,
17
- } from "../../../test-utils/factories";
18
-
19
- describe("db.clients.lifecycle", () => {
20
- describe("clientExistsInAnyCollection", () => {
21
- it("should return true when the client exists only in phone_numbers (client_id field)", async () => {
22
- const clientId = "existsPhoneOnly";
23
- await getPhoneNumbersCollection().insertOne(
24
- createPhoneNumber({ client_id: clientId }),
25
- );
26
-
27
- expect(await clientExistsInAnyCollection(clientId)).toBe(true);
28
- });
29
-
30
- it("should return true when the client exists only in muniIssues (municipal DB)", async () => {
31
- const clientId = "existsMuniOnly";
32
- await createMuniIssue(
33
- createMuniIssueInput({ clientId }),
34
- generateMuniIssueId(),
35
- );
36
-
37
- expect(await clientExistsInAnyCollection(clientId)).toBe(true);
38
- });
39
-
40
- it("should return false when the client does not exist anywhere", async () => {
41
- expect(await clientExistsInAnyCollection("noSuchClientAtAll")).toBe(
42
- false,
43
- );
44
- });
45
-
46
- it("should not consider websitalk data (scans/websiteUrls) at all", async () => {
47
- const clientId = "onlyInWebsitalk";
48
- await getScansCollection().insertOne(createScanFixture({ clientId }));
49
-
50
- expect(await clientExistsInAnyCollection(clientId)).toBe(false);
51
- });
52
-
53
- it("should not be fooled by unrelated collections lacking a clientId/client_id field", async () => {
54
- await global.db
55
- .collection("someUnrelatedCollection")
56
- .insertOne({ someField: "someValue" });
57
-
58
- expect(await clientExistsInAnyCollection("noSuchClientAtAll")).toBe(
59
- false,
60
- );
61
- });
62
- });
63
-
64
- describe("deleteClientDataAcrossDomains", () => {
65
- it("should return deleted:false with a reason and delete nothing when the client does not exist", async () => {
66
- await getPhoneNumbersCollection().insertOne(
67
- createPhoneNumber({ client_id: "unrelatedClient" }),
68
- );
69
-
70
- const summary = await deleteClientDataAcrossDomains(
71
- "missingClient",
72
- "admin@callai.co.il",
73
- );
74
-
75
- expect(summary.deleted).toBe(false);
76
- if (!summary.deleted) {
77
- expect(summary.reason).toBeTruthy();
78
- }
79
- expect(
80
- await getPhoneNumbersCollection().findOne({
81
- client_id: "unrelatedClient",
82
- }),
83
- ).not.toBeNull();
84
- });
85
-
86
- it("should not write an audit log entry when the client does not exist", async () => {
87
- await deleteClientDataAcrossDomains(
88
- "missingClientNoAudit",
89
- "admin@callai.co.il",
90
- );
91
-
92
- expect(
93
- await getAuditLogsCollection().findOne({
94
- clientId: "missingClientNoAudit",
95
- }),
96
- ).toBeNull();
97
- });
98
-
99
- it("should delete the client's data across talkpilot and municipal domains", async () => {
100
- const clientId = "deleteMeEverywhere";
101
- await getPhoneNumbersCollection().insertOne(
102
- createPhoneNumber({ client_id: clientId }),
103
- );
104
- await createMuniIssue(
105
- createMuniIssueInput({ clientId }),
106
- generateMuniIssueId(),
107
- );
108
-
109
- const adminEmail = "admin@callai.co.il";
110
- const summary = await deleteClientDataAcrossDomains(
111
- clientId,
112
- adminEmail,
113
- );
114
-
115
- expect(summary.deleted).toBe(true);
116
- if (summary.deleted) {
117
- expect(summary.allSucceeded).toBe(true);
118
- expect(summary.auditLogged).toBe(true);
119
- expect(summary.auditError).toBeUndefined();
120
- const byName = (name: string) =>
121
- summary.results.find((r) => r.collectionName === name);
122
- expect(byName("phone_numbers")?.deletedCount).toBe(1);
123
- expect(byName("muniIssues")?.deletedCount).toBe(1);
124
- }
125
-
126
- expect(await clientExistsInAnyCollection(clientId)).toBe(false);
127
-
128
- // The audit entry itself must survive the client's own deletion.
129
- const auditEntry = await getAuditLogsCollection().findOne({ clientId });
130
- expect(auditEntry).toMatchObject({
131
- clientId,
132
- adminEmail,
133
- action: "delete",
134
- });
135
- expect(auditEntry?.timestamp).toBeInstanceOf(Date);
136
- });
137
-
138
- it("should still delete the data and report auditLogged: false when the audit write fails", async () => {
139
- const clientId = "auditWriteFails";
140
- await getPhoneNumbersCollection().insertOne(
141
- createPhoneNumber({ client_id: clientId }),
142
- );
143
- const insertAuditLog = jest
144
- .spyOn(auditLogsGetters, "insertAuditLog")
145
- .mockRejectedValueOnce(new Error("audit write failed"));
146
-
147
- const summary = await deleteClientDataAcrossDomains(
148
- clientId,
149
- "admin@callai.co.il",
150
- );
151
-
152
- expect(summary).toMatchObject({
153
- deleted: true,
154
- allSucceeded: true,
155
- auditLogged: false,
156
- auditError: "audit write failed",
157
- });
158
- expect(await clientExistsInAnyCollection(clientId)).toBe(false);
159
-
160
- insertAuditLog.mockRestore();
161
- });
162
-
163
- it("should not touch websitalk data (scans/websiteUrls) — callers must call the external website-talk deletion API separately", async () => {
164
- const clientId = "hasWebsitalkData";
165
- await getPhoneNumbersCollection().insertOne(
166
- createPhoneNumber({ client_id: clientId }),
167
- );
168
- await getScansCollection().insertOne(createScanFixture({ clientId }));
169
-
170
- await deleteClientDataAcrossDomains(clientId, "admin@callai.co.il");
171
-
172
- expect(
173
- await getScansCollection().findOne({ clientId }),
174
- ).not.toBeNull();
175
- });
176
-
177
- it("should not delete unrelated clients' data", async () => {
178
- const targetClient = "deleteTarget";
179
- const otherClient = "keepMeClient";
180
- await getPhoneNumbersCollection().insertOne(
181
- createPhoneNumber({ client_id: targetClient }),
182
- );
183
- await getPhoneNumbersCollection().insertOne(
184
- createPhoneNumber({ client_id: otherClient }),
185
- );
186
-
187
- await deleteClientDataAcrossDomains(targetClient, "admin@callai.co.il");
188
-
189
- expect(
190
- await getPhoneNumbersCollection().findOne({ client_id: otherClient }),
191
- ).not.toBeNull();
192
- });
193
- });
194
- });
@@ -1,4 +0,0 @@
1
- /** Collections to always skip during client deletion — e.g. auditLogs, which must survive it. */
2
- export const DELETE_DENYLIST: string[] = ["auditLogs"];
3
-
4
- export const BACKOFFICE_ROLES = ["admin", "viewer"] as const;
@@ -1,132 +0,0 @@
1
- import { Db } from "mongodb";
2
- import { getDb, insertAuditLog } from "../index";
3
- import type { AuditLogEntry } from "../auditLogs/auditLogs.types";
4
- import { getDb as getMunicipalDataDb } from "../../municipal";
5
- import { DELETE_DENYLIST } from "./clients.constants";
6
- import {
7
- AuditLogResult,
8
- CollectionMatch,
9
- CollectionDeleteResult,
10
- DeleteClientDataSummary,
11
- } from "./clients.types";
12
-
13
- // Skips websitalk on purpose — see the warning on `deleteClientDataAcrossDomains` below.
14
- const findClientMatchInDb = async (
15
- db: Db,
16
- clientId: string,
17
- ): Promise<CollectionMatch[]> => {
18
- const collections = await db.listCollections().toArray();
19
- return Promise.all(
20
- collections
21
- .filter((c) => !DELETE_DENYLIST.includes(c.name))
22
- .map(async (c) => {
23
- const doc = await db
24
- .collection(c.name)
25
- .findOne({ $or: [{ clientId }, { client_id: clientId }] });
26
- return { collectionName: c.name, matched: !!doc };
27
- }),
28
- );
29
- };
30
-
31
- export const clientExistsInAnyCollection = async (
32
- clientId: string,
33
- ): Promise<boolean> => {
34
- const [talkpilotResults, municipalResults] = await Promise.all([
35
- findClientMatchInDb(getDb(), clientId),
36
- findClientMatchInDb(getMunicipalDataDb(), clientId),
37
- ]);
38
- return [...talkpilotResults, ...municipalResults].some((r) => r.matched);
39
- };
40
-
41
- const deleteClientFromDb = async (
42
- db: Db,
43
- clientId: string,
44
- ): Promise<CollectionDeleteResult[]> => {
45
- const collections = await db.listCollections().toArray();
46
- return Promise.all(
47
- collections
48
- .filter((c) => !DELETE_DENYLIST.includes(c.name))
49
- .map(async (c) => {
50
- try {
51
- const result = await db
52
- .collection(c.name)
53
- .deleteMany({ $or: [{ clientId }, { client_id: clientId }] });
54
- return {
55
- collectionName: c.name,
56
- deletedCount: result.deletedCount,
57
- success: true,
58
- };
59
- } catch (error) {
60
- return {
61
- collectionName: c.name,
62
- deletedCount: 0,
63
- success: false,
64
- error: error instanceof Error ? error.message : String(error),
65
- };
66
- }
67
- }),
68
- );
69
- };
70
-
71
- // The data is already deleted at this point — a failed audit write must not abort the caller.
72
- const tryInsertAuditLog = async (entry: AuditLogEntry): Promise<AuditLogResult> => {
73
- try {
74
- await insertAuditLog(entry);
75
- return { auditLogged: true };
76
- } catch (error) {
77
- return {
78
- auditLogged: false,
79
- auditError: error instanceof Error ? error.message : String(error),
80
- };
81
- }
82
- };
83
-
84
- /**
85
- * Deletes a client's data across talkpilot/municipal collections and logs the action.
86
- *
87
- * ⚠️ Does NOT delete website-talk data — `scans`/`websiteUrls` are the lookup key an
88
- * external API needs to clean up that domain. Callers must invoke that API separately.
89
- */
90
- export const deleteClientDataAcrossDomains = async (
91
- clientId: string,
92
- adminEmail: string,
93
- ): Promise<DeleteClientDataSummary> => {
94
- const exists = await clientExistsInAnyCollection(clientId);
95
- if (!exists) {
96
- return {
97
- clientId,
98
- deleted: false,
99
- reason: "Client does not exist in any collection",
100
- };
101
- }
102
-
103
- const [talkpilotResults, municipalResults] = await Promise.all([
104
- deleteClientFromDb(getDb(), clientId),
105
- deleteClientFromDb(getMunicipalDataDb(), clientId),
106
- ]);
107
- const results = [...talkpilotResults, ...municipalResults];
108
- const allSucceeded = results.every((r) => r.success);
109
-
110
- const auditResult = await tryInsertAuditLog({
111
- clientId,
112
- adminEmail,
113
- action: "delete",
114
- timestamp: new Date(),
115
- allSucceeded,
116
- ...(allSucceeded
117
- ? {}
118
- : {
119
- failedCollections: results
120
- .filter((r) => !r.success)
121
- .map((r) => r.collectionName),
122
- }),
123
- });
124
-
125
- return {
126
- clientId,
127
- deleted: true,
128
- results,
129
- allSucceeded,
130
- ...auditResult,
131
- };
132
- };
@@ -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
- }
@@ -1,2 +0,0 @@
1
- export const escapeRegex = (text: string): string =>
2
- text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");