@talkpilot/core-db 1.3.51 → 1.3.52

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 (151) hide show
  1. package/README.md +21 -2
  2. package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts +8 -0
  3. package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts.map +1 -0
  4. package/dist/talkpilot/auditLogs/auditLogs.getters.js +26 -0
  5. package/dist/talkpilot/auditLogs/auditLogs.getters.js.map +1 -0
  6. package/dist/talkpilot/auditLogs/auditLogs.setters.d.ts +3 -0
  7. package/dist/talkpilot/auditLogs/auditLogs.setters.d.ts.map +1 -0
  8. package/dist/talkpilot/auditLogs/auditLogs.setters.js +9 -0
  9. package/dist/talkpilot/auditLogs/auditLogs.setters.js.map +1 -0
  10. package/dist/talkpilot/auditLogs/auditLogs.types.d.ts +12 -0
  11. package/dist/talkpilot/auditLogs/auditLogs.types.d.ts.map +1 -0
  12. package/dist/talkpilot/auditLogs/auditLogs.types.js +3 -0
  13. package/dist/talkpilot/auditLogs/auditLogs.types.js.map +1 -0
  14. package/dist/talkpilot/auditLogs/index.d.ts +3 -0
  15. package/dist/talkpilot/auditLogs/index.d.ts.map +1 -0
  16. package/dist/talkpilot/auditLogs/index.js +19 -0
  17. package/dist/talkpilot/auditLogs/index.js.map +1 -0
  18. package/dist/talkpilot/calls/calls.constants.d.ts +1 -1
  19. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.d.ts +4 -0
  20. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.d.ts.map +1 -0
  21. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.js +9 -0
  22. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.getters.js.map +1 -0
  23. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.d.ts +3 -0
  24. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.d.ts.map +1 -0
  25. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.js +9 -0
  26. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.setters.js.map +1 -0
  27. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.d.ts +10 -0
  28. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.d.ts.map +1 -0
  29. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.js +3 -0
  30. package/dist/talkpilot/clientDeletionLogs/clientDeletionLogs.types.js.map +1 -0
  31. package/dist/talkpilot/clientDeletionLogs/index.d.ts +4 -0
  32. package/dist/talkpilot/clientDeletionLogs/index.d.ts.map +1 -0
  33. package/dist/talkpilot/clientDeletionLogs/index.js +20 -0
  34. package/dist/talkpilot/clientDeletionLogs/index.js.map +1 -0
  35. package/dist/talkpilot/clients/clients.constants.d.ts +3 -0
  36. package/dist/talkpilot/clients/clients.constants.d.ts.map +1 -0
  37. package/dist/talkpilot/clients/clients.constants.js +6 -0
  38. package/dist/talkpilot/clients/clients.constants.js.map +1 -0
  39. package/dist/talkpilot/clients/clients.getters.d.ts +1 -0
  40. package/dist/talkpilot/clients/clients.getters.d.ts.map +1 -1
  41. package/dist/talkpilot/clients/clients.getters.js +8 -1
  42. package/dist/talkpilot/clients/clients.getters.js.map +1 -1
  43. package/dist/talkpilot/clients/clients.lifecycle.d.ts +10 -0
  44. package/dist/talkpilot/clients/clients.lifecycle.d.ts.map +1 -0
  45. package/dist/talkpilot/clients/clients.lifecycle.js +95 -0
  46. package/dist/talkpilot/clients/clients.lifecycle.js.map +1 -0
  47. package/dist/talkpilot/clients/clients.types.d.ts +21 -0
  48. package/dist/talkpilot/clients/clients.types.d.ts.map +1 -1
  49. package/dist/talkpilot/clients/index.d.ts +2 -0
  50. package/dist/talkpilot/clients/index.d.ts.map +1 -1
  51. package/dist/talkpilot/clients/index.js +2 -0
  52. package/dist/talkpilot/clients/index.js.map +1 -1
  53. package/dist/talkpilot/flows/flows.getter.d.ts +1 -4
  54. package/dist/talkpilot/flows/flows.getter.d.ts.map +1 -1
  55. package/dist/talkpilot/flows/flows.getter.js +1 -12
  56. package/dist/talkpilot/flows/flows.getter.js.map +1 -1
  57. package/dist/talkpilot/flows/flows.schema.d.ts +0 -31
  58. package/dist/talkpilot/flows/flows.schema.d.ts.map +1 -1
  59. package/dist/talkpilot/flows/flows.schema.js +0 -25
  60. package/dist/talkpilot/flows/flows.schema.js.map +1 -1
  61. package/dist/talkpilot/flows/flows.types.d.ts +0 -14
  62. package/dist/talkpilot/flows/flows.types.d.ts.map +1 -1
  63. package/dist/talkpilot/flows/index.d.ts +1 -1
  64. package/dist/talkpilot/flows/index.d.ts.map +1 -1
  65. package/dist/talkpilot/flows/index.js +1 -3
  66. package/dist/talkpilot/flows/index.js.map +1 -1
  67. package/dist/talkpilot/index.d.ts +1 -0
  68. package/dist/talkpilot/index.d.ts.map +1 -1
  69. package/dist/talkpilot/index.js +1 -0
  70. package/dist/talkpilot/index.js.map +1 -1
  71. package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts +11 -0
  72. package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
  73. package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +60 -1
  74. package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
  75. package/dist/talkpilot/results/index.d.ts +3 -0
  76. package/dist/talkpilot/results/index.d.ts.map +1 -1
  77. package/dist/talkpilot/results/index.js +22 -1
  78. package/dist/talkpilot/results/index.js.map +1 -1
  79. package/dist/talkpilot/results/results.constants.d.ts +9 -0
  80. package/dist/talkpilot/results/results.constants.d.ts.map +1 -0
  81. package/dist/talkpilot/results/results.constants.js +12 -0
  82. package/dist/talkpilot/results/results.constants.js.map +1 -0
  83. package/dist/talkpilot/results/results.getter.d.ts.map +1 -1
  84. package/dist/talkpilot/results/results.getter.js +3 -2
  85. package/dist/talkpilot/results/results.getter.js.map +1 -1
  86. package/dist/talkpilot/results/results.statistics.getters.d.ts +7 -0
  87. package/dist/talkpilot/results/results.statistics.getters.d.ts.map +1 -0
  88. package/dist/talkpilot/results/results.statistics.getters.js +244 -0
  89. package/dist/talkpilot/results/results.statistics.getters.js.map +1 -0
  90. package/dist/talkpilot/results/results.statistics.types.d.ts +42 -0
  91. package/dist/talkpilot/results/results.statistics.types.d.ts.map +1 -0
  92. package/dist/talkpilot/results/results.statistics.types.js +3 -0
  93. package/dist/talkpilot/results/results.statistics.types.js.map +1 -0
  94. package/dist/test-utils/factories/index.d.ts +2 -0
  95. package/dist/test-utils/factories/index.d.ts.map +1 -1
  96. package/dist/test-utils/factories/index.js +2 -0
  97. package/dist/test-utils/factories/index.js.map +1 -1
  98. package/dist/test-utils/factories/talkpilot/clients.d.ts +14 -0
  99. package/dist/test-utils/factories/talkpilot/clients.d.ts.map +1 -0
  100. package/dist/test-utils/factories/talkpilot/clients.js +19 -0
  101. package/dist/test-utils/factories/talkpilot/clients.js.map +1 -0
  102. package/dist/test-utils/factories/talkpilot/results.d.ts +16 -0
  103. package/dist/test-utils/factories/talkpilot/results.d.ts.map +1 -0
  104. package/dist/test-utils/factories/talkpilot/results.js +40 -0
  105. package/dist/test-utils/factories/talkpilot/results.js.map +1 -0
  106. package/dist/websitalk/scanResources/scanResources.getters.d.ts +0 -1
  107. package/dist/websitalk/scanResources/scanResources.getters.d.ts.map +1 -1
  108. package/dist/websitalk/scanResources/scanResources.getters.js +1 -6
  109. package/dist/websitalk/scanResources/scanResources.getters.js.map +1 -1
  110. package/dist/websitalk/scans/scans.constants.d.ts +0 -1
  111. package/dist/websitalk/scans/scans.constants.d.ts.map +1 -1
  112. package/dist/websitalk/scans/scans.constants.js +0 -1
  113. package/dist/websitalk/scans/scans.constants.js.map +1 -1
  114. package/dist/websitalk/scans/scans.getters.d.ts +0 -1
  115. package/dist/websitalk/scans/scans.getters.d.ts.map +1 -1
  116. package/dist/websitalk/scans/scans.getters.js +1 -6
  117. package/dist/websitalk/scans/scans.getters.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/talkpilot/auditLogs/__tests__/auditLogs.spec.ts +92 -0
  120. package/src/talkpilot/auditLogs/auditLogs.getters.ts +26 -0
  121. package/src/talkpilot/auditLogs/auditLogs.types.ts +13 -0
  122. package/src/talkpilot/auditLogs/index.ts +2 -0
  123. package/src/talkpilot/clients/__tests__/clients.getters.spec.ts +33 -0
  124. package/src/talkpilot/clients/__tests__/clients.lifecycle.spec.ts +166 -0
  125. package/src/talkpilot/clients/clients.constants.ts +2 -0
  126. package/src/talkpilot/clients/clients.getters.ts +7 -0
  127. package/src/talkpilot/clients/clients.lifecycle.ts +116 -0
  128. package/src/talkpilot/clients/clients.types.ts +19 -0
  129. package/src/talkpilot/clients/index.ts +2 -0
  130. package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +0 -25
  131. package/src/talkpilot/flows/flows.getter.ts +2 -22
  132. package/src/talkpilot/flows/flows.schema.ts +0 -25
  133. package/src/talkpilot/flows/flows.types.ts +0 -22
  134. package/src/talkpilot/flows/index.ts +1 -6
  135. package/src/talkpilot/index.ts +1 -0
  136. package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +227 -13
  137. package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +76 -1
  138. package/src/talkpilot/results/__tests__/results.statistics.spec.ts +226 -0
  139. package/src/talkpilot/results/index.ts +9 -0
  140. package/src/talkpilot/results/results.constants.ts +11 -0
  141. package/src/talkpilot/results/results.getter.ts +3 -2
  142. package/src/talkpilot/results/results.statistics.getters.ts +329 -0
  143. package/src/talkpilot/results/results.statistics.types.ts +45 -0
  144. package/src/test-utils/factories/index.ts +2 -0
  145. package/src/test-utils/factories/talkpilot/clients.ts +21 -0
  146. package/src/test-utils/factories/talkpilot/results.ts +43 -0
  147. package/src/websitalk/scanResources/__tests__/scanResources.spec.ts +0 -19
  148. package/src/websitalk/scanResources/scanResources.getters.ts +0 -7
  149. package/src/websitalk/scans/__tests__/scans.spec.ts +0 -17
  150. package/src/websitalk/scans/scans.constants.ts +0 -1
  151. package/src/websitalk/scans/scans.getters.ts +0 -5
@@ -0,0 +1,92 @@
1
+ import {
2
+ insertAuditLog,
3
+ getAuditLogsCollection,
4
+ findAuditLogs,
5
+ countAuditLogs,
6
+ } from "../auditLogs.getters";
7
+ import { AuditLogEntry } from "../auditLogs.types";
8
+
9
+ describe("db.auditLogs", () => {
10
+ describe("insertAuditLog", () => {
11
+ it("should write an entry that can be read back with all fields intact", async () => {
12
+ const entry: AuditLogEntry = {
13
+ clientId: "deletedClient1",
14
+ adminEmail: "admin@callai.co.il",
15
+ action: "delete",
16
+ timestamp: new Date("2026-07-15T10:00:00.000Z"),
17
+ env: "prod",
18
+ };
19
+
20
+ await insertAuditLog(entry);
21
+
22
+ const stored = await getAuditLogsCollection().findOne({
23
+ clientId: "deletedClient1",
24
+ });
25
+ expect(stored).toMatchObject(entry);
26
+ });
27
+ });
28
+
29
+ describe("findAuditLogs", () => {
30
+ const seed = async () => {
31
+ await insertAuditLog({
32
+ clientId: "clientA",
33
+ adminEmail: "admin@callai.co.il",
34
+ action: "delete",
35
+ timestamp: new Date("2026-01-01T00:00:00.000Z"),
36
+ });
37
+ await insertAuditLog({
38
+ clientId: "clientB",
39
+ adminEmail: "admin@callai.co.il",
40
+ action: "delete",
41
+ timestamp: new Date("2026-02-01T00:00:00.000Z"),
42
+ });
43
+ await insertAuditLog({
44
+ clientId: "clientC",
45
+ adminEmail: "admin@callai.co.il",
46
+ action: "delete",
47
+ timestamp: new Date("2026-03-01T00:00:00.000Z"),
48
+ });
49
+ };
50
+
51
+ it("should return a page of entries, newest first", async () => {
52
+ await seed();
53
+
54
+ const page = await findAuditLogs({ limit: 2 });
55
+
56
+ expect(page).toHaveLength(2);
57
+ expect(page.map((e) => e.clientId)).toEqual(["clientC", "clientB"]);
58
+ });
59
+
60
+ it("should skip entries for later pages", async () => {
61
+ await seed();
62
+
63
+ const page = await findAuditLogs({ skip: 2, limit: 2 });
64
+
65
+ expect(page).toHaveLength(1);
66
+ expect(page[0].clientId).toBe("clientA");
67
+ });
68
+ });
69
+
70
+ describe("countAuditLogs", () => {
71
+ it("should return the total number of entries regardless of pagination", async () => {
72
+ await insertAuditLog({
73
+ clientId: "clientA",
74
+ adminEmail: "admin@callai.co.il",
75
+ action: "delete",
76
+ timestamp: new Date(),
77
+ });
78
+ await insertAuditLog({
79
+ clientId: "clientB",
80
+ adminEmail: "admin@callai.co.il",
81
+ action: "delete",
82
+ timestamp: new Date(),
83
+ });
84
+
85
+ expect(await countAuditLogs()).toBe(2);
86
+ });
87
+
88
+ it("should return 0 when there are no entries", async () => {
89
+ expect(await countAuditLogs()).toBe(0);
90
+ });
91
+ });
92
+ });
@@ -0,0 +1,26 @@
1
+ import { Collection } from "mongodb";
2
+ import { getDb } from "../index";
3
+ import { applyQueryOptions, QueryOptions } from "../utils/query.utils";
4
+ import { AuditLogDoc, AuditLogEntry } from "./auditLogs.types";
5
+
6
+ export const getAuditLogsCollection = (): Collection<AuditLogEntry> => {
7
+ return getDb().collection<AuditLogEntry>("auditLogs");
8
+ };
9
+
10
+ export const insertAuditLog = async (entry: AuditLogEntry): Promise<void> => {
11
+ await getAuditLogsCollection().insertOne(entry);
12
+ };
13
+
14
+ export const findAuditLogs = async (
15
+ options: Pick<QueryOptions, "skip"> & Required<Pick<QueryOptions, "limit">>,
16
+ ): Promise<AuditLogDoc[]> => {
17
+ const cursor = getAuditLogsCollection().find({});
18
+ return applyQueryOptions(cursor, {
19
+ sort: { timestamp: -1 },
20
+ ...options,
21
+ }).toArray();
22
+ };
23
+
24
+ export const countAuditLogs = async (): Promise<number> => {
25
+ return getAuditLogsCollection().countDocuments({});
26
+ };
@@ -0,0 +1,13 @@
1
+ import { WithId } from "mongodb";
2
+
3
+ export type AuditLogEntry = {
4
+ clientId: string;
5
+ adminEmail: string;
6
+ action: "delete";
7
+ timestamp: Date;
8
+ env?: string;
9
+ allSucceeded?: boolean;
10
+ failedCollections?: string[];
11
+ };
12
+
13
+ export type AuditLogDoc = WithId<AuditLogEntry>;
@@ -0,0 +1,2 @@
1
+ export * from "./auditLogs.types";
2
+ export * from "./auditLogs.getters";
@@ -0,0 +1,33 @@
1
+ import { getClientsCollection, getAllClientCredits } 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
+ });
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ /** Collections to always skip during client deletion — e.g. auditLogs, which must survive it. */
2
+ export const DELETE_DENYLIST: string[] = ["auditLogs"];
@@ -14,3 +14,10 @@ export const getClientByAPIKey = async (
14
14
  });
15
15
  return client ? client : null;
16
16
  };
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
+ };
@@ -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
+ };
@@ -16,3 +16,22 @@ export type Client = {
16
16
  };
17
17
 
18
18
  export type ClientDoc = Client & { _id: ObjectId };
19
+
20
+ export type CollectionMatch = { collectionName: string; matched: boolean };
21
+
22
+ export type CollectionDeleteResult = {
23
+ collectionName: string;
24
+ deletedCount: number;
25
+ success: boolean;
26
+ error?: string;
27
+ };
28
+
29
+ /** Discriminated union: `deleted` tells you which shape you got without guessing. */
30
+ export type DeleteClientDataSummary =
31
+ | { clientId: string; deleted: false; reason: string }
32
+ | {
33
+ clientId: string;
34
+ deleted: true;
35
+ results: CollectionDeleteResult[];
36
+ allSucceeded: boolean;
37
+ };
@@ -1,4 +1,6 @@
1
+ export * from "./clients.constants";
1
2
  export * from "./clients.getters";
2
3
  export * from "./clients.types";
3
4
  export * from "./clients.quota.getters";
4
5
  export * from "./clients.quota.types";
6
+ export * from "./clients.lifecycle";
@@ -96,29 +96,4 @@ describe("flowMongoSchema", () => {
96
96
  toolItem.properties.backgroundContinuationInstructions.bsonType,
97
97
  ).toBe("string");
98
98
  });
99
-
100
- it("defines internalTools as an optional map of tool configs", () => {
101
- const schema: any = flowMongoSchema;
102
-
103
- expect(schema.properties).toHaveProperty("internalTools");
104
- expect(schema.required).not.toContain("internalTools");
105
- expect(schema.properties.internalTools.bsonType).toEqual([
106
- "object",
107
- "null",
108
- ]);
109
-
110
- const toolConfig = schema.properties.internalTools.additionalProperties;
111
- expect(toolConfig.required).toEqual(["enabled"]);
112
- expect(toolConfig.properties.enabled.bsonType).toBe("bool");
113
- expect(toolConfig.properties.maxCalls.bsonType).toEqual(["number", "null"]);
114
-
115
- const param = toolConfig.properties.params.additionalProperties;
116
- expect(param.properties.value.bsonType).toEqual([
117
- "string",
118
- "number",
119
- "bool",
120
- "array",
121
- "null",
122
- ]);
123
- });
124
99
  });
@@ -1,6 +1,6 @@
1
- import type { Collection, ObjectId, OptionalId, WithId } from "mongodb";
1
+ import type { Collection, ObjectId, WithId } from "mongodb";
2
2
  import { getDb } from "../index";
3
- import type { Flow, FlowUpdateParams } from "./flows.types";
3
+ import type { Flow } from "./flows.types";
4
4
 
5
5
  export const getFlowsCollection = (): Collection<Flow> => {
6
6
  return getDb().collection<Flow>("flows");
@@ -12,23 +12,3 @@ export const findFlowById = (
12
12
  ): Promise<WithId<Flow> | null> => {
13
13
  return getFlowsCollection().findOne({ _id: flowId, clientId });
14
14
  };
15
-
16
- export const createFlow = async (flow: Flow): Promise<WithId<Flow>> => {
17
- const { insertedId } = await getFlowsCollection().insertOne(
18
- flow as OptionalId<Flow>,
19
- );
20
- return { ...flow, _id: insertedId };
21
- };
22
-
23
- /** Returns false when no flow matches the id for this client. */
24
- export const updateFlowById = async (
25
- flowId: ObjectId,
26
- clientId: string,
27
- update: FlowUpdateParams,
28
- ): Promise<boolean> => {
29
- const { matchedCount } = await getFlowsCollection().updateOne(
30
- { _id: flowId, clientId },
31
- { $set: { ...update, updatedAt: new Date() } },
32
- );
33
- return matchedCount > 0;
34
- };
@@ -150,31 +150,6 @@ export const flowMongoSchema = {
150
150
  additionalProperties: false,
151
151
  },
152
152
  },
153
- internalTools: {
154
- bsonType: ["object", "null"],
155
- additionalProperties: {
156
- bsonType: "object",
157
- required: ["enabled"],
158
- properties: {
159
- enabled: { bsonType: "bool" },
160
- maxCalls: { bsonType: ["number", "null"] },
161
- params: {
162
- bsonType: ["object", "null"],
163
- additionalProperties: {
164
- bsonType: "object",
165
- properties: {
166
- description: { bsonType: ["string", "null"] },
167
- value: {
168
- bsonType: ["string", "number", "bool", "array", "null"],
169
- },
170
- },
171
- additionalProperties: false,
172
- },
173
- },
174
- },
175
- additionalProperties: false,
176
- },
177
- },
178
153
  },
179
154
  additionalProperties: false,
180
155
  } as const;
@@ -152,21 +152,6 @@ export type ConversationSettings = {
152
152
  liveTimeUpdates?: boolean;
153
153
  };
154
154
 
155
- /* ---------- Internal tools ---------- */
156
-
157
- export type InternalToolParamConfig = {
158
- description?: string;
159
- /** Pinned value. When set, the agent is not asked for this parameter. */
160
- value?: string | number | boolean | string[];
161
- };
162
-
163
- export type InternalToolConfig = {
164
- enabled: boolean;
165
- /** Max invocations per call. Unlimited when omitted. */
166
- maxCalls?: number;
167
- params?: Record<string, InternalToolParamConfig>;
168
- };
169
-
170
155
  /* ---------- The Flow object ---------- */
171
156
 
172
157
  export type Flow = {
@@ -198,14 +183,7 @@ export type Flow = {
198
183
  // AI provider configuration
199
184
  aiProvider?: any;
200
185
 
201
- /** Internal tool toggles and per-parameter overrides, keyed by tool name. */
202
- internalTools?: Record<string, InternalToolConfig>;
203
-
204
186
  // Timestamps
205
187
  createdAt?: Date;
206
188
  updatedAt?: Date;
207
189
  };
208
-
209
- export type FlowUpdateParams = Partial<
210
- Omit<Flow, "_id" | "clientId" | "createdAt">
211
- >;
@@ -1,7 +1,2 @@
1
- export {
2
- getFlowsCollection,
3
- findFlowById,
4
- createFlow,
5
- updateFlowById,
6
- } from "./flows.getter";
1
+ export { getFlowsCollection, findFlowById } from "./flows.getter";
7
2
  export type * from "./flows.types";
@@ -2,6 +2,7 @@ import { Db, ObjectId as MongoObjectId } from "mongodb";
2
2
 
3
3
  export * from "./flows";
4
4
  export * from "./contextNotes";
5
+ export * from "./auditLogs";
5
6
  export * from "./products";
6
7
  export * from "./results";
7
8
  export * from "./sessions";