@talkpilot/core-db 1.3.26 → 1.3.28
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 +60 -0
- package/dist/bulkWrite/bulkWrite.ops.d.ts +17 -0
- package/dist/bulkWrite/bulkWrite.ops.d.ts.map +1 -0
- package/dist/bulkWrite/bulkWrite.ops.js +35 -0
- package/dist/bulkWrite/bulkWrite.ops.js.map +1 -0
- package/dist/bulkWrite/bulkWrite.types.d.ts +4 -0
- package/dist/bulkWrite/bulkWrite.types.d.ts.map +1 -0
- package/dist/{talkpilot/clients/clients.quota.types.js → bulkWrite/bulkWrite.types.js} +1 -1
- package/dist/bulkWrite/bulkWrite.types.js.map +1 -0
- package/dist/bulkWrite/index.d.ts +3 -0
- package/dist/bulkWrite/index.d.ts.map +1 -0
- package/dist/bulkWrite/index.js +19 -0
- package/dist/bulkWrite/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/municipal/departmentsSubjects/departmentsSubjects.getters.d.ts +15 -1
- package/dist/municipal/departmentsSubjects/departmentsSubjects.getters.d.ts.map +1 -1
- package/dist/municipal/departmentsSubjects/departmentsSubjects.getters.js +24 -1
- package/dist/municipal/departmentsSubjects/departmentsSubjects.getters.js.map +1 -1
- package/dist/municipal/departmentsSubjects/departmentsSubjects.setters.d.ts +10 -0
- package/dist/municipal/departmentsSubjects/departmentsSubjects.setters.d.ts.map +1 -0
- package/dist/municipal/departmentsSubjects/departmentsSubjects.setters.js +15 -0
- package/dist/municipal/departmentsSubjects/departmentsSubjects.setters.js.map +1 -0
- package/dist/municipal/departmentsSubjects/departmentsSubjects.types.d.ts +1 -0
- package/dist/municipal/departmentsSubjects/departmentsSubjects.types.d.ts.map +1 -1
- package/dist/municipal/streets/streets.getters.d.ts +2 -1
- package/dist/municipal/streets/streets.getters.d.ts.map +1 -1
- package/dist/municipal/streets/streets.getters.js +6 -1
- package/dist/municipal/streets/streets.getters.js.map +1 -1
- package/dist/municipal/streets/streets.setters.d.ts +10 -0
- package/dist/municipal/streets/streets.setters.d.ts.map +1 -0
- package/dist/municipal/streets/streets.setters.js +15 -0
- package/dist/municipal/streets/streets.setters.js.map +1 -0
- package/dist/municipal/streets/streets.types.d.ts +1 -0
- package/dist/municipal/streets/streets.types.d.ts.map +1 -1
- package/dist/reconcile/index.d.ts +5 -0
- package/dist/reconcile/index.d.ts.map +1 -0
- package/dist/reconcile/index.js +21 -0
- package/dist/reconcile/index.js.map +1 -0
- package/dist/reconcile/reconcile.diff.d.ts +4 -0
- package/dist/reconcile/reconcile.diff.d.ts.map +1 -0
- package/dist/reconcile/reconcile.diff.js +36 -0
- package/dist/reconcile/reconcile.diff.js.map +1 -0
- package/dist/reconcile/reconcile.execute.d.ts +7 -0
- package/dist/reconcile/reconcile.execute.d.ts.map +1 -0
- package/dist/reconcile/reconcile.execute.js +20 -0
- package/dist/reconcile/reconcile.execute.js.map +1 -0
- package/dist/reconcile/reconcile.ops.d.ts +12 -0
- package/dist/reconcile/reconcile.ops.d.ts.map +1 -0
- package/dist/reconcile/reconcile.ops.js +24 -0
- package/dist/reconcile/reconcile.ops.js.map +1 -0
- package/dist/reconcile/reconcile.types.d.ts +32 -0
- package/dist/reconcile/reconcile.types.d.ts.map +1 -0
- package/dist/reconcile/reconcile.types.js +3 -0
- package/dist/reconcile/reconcile.types.js.map +1 -0
- package/dist/talkpilot/calls/calls.constants.d.ts +14 -0
- package/dist/talkpilot/calls/calls.constants.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.constants.js +15 -1
- package/dist/talkpilot/calls/calls.constants.js.map +1 -1
- package/dist/talkpilot/calls/calls.getters.d.ts +3 -0
- package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.getters.js +12 -1
- package/dist/talkpilot/calls/calls.getters.js.map +1 -1
- package/dist/talkpilot/calls/calls.types.d.ts +11 -2
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.types.d.ts +0 -3
- package/dist/talkpilot/clients/clients.types.d.ts.map +1 -1
- package/dist/talkpilot/clients/index.d.ts +0 -2
- package/dist/talkpilot/clients/index.d.ts.map +1 -1
- package/dist/talkpilot/clients/index.js +0 -2
- package/dist/talkpilot/clients/index.js.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts +0 -3
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.js +1 -23
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.js.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts +0 -2
- package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bulkWrite/__tests__/bulkWrite.spec.ts +65 -0
- package/src/bulkWrite/bulkWrite.ops.ts +45 -0
- package/src/bulkWrite/bulkWrite.types.ts +4 -0
- package/src/bulkWrite/index.ts +2 -0
- package/src/index.ts +7 -0
- package/src/municipal/departmentsSubjects/__tests__/departmentsSubjects.spec.ts +272 -0
- package/src/municipal/departmentsSubjects/departmentsSubjects.getters.ts +38 -3
- package/src/municipal/departmentsSubjects/departmentsSubjects.types.ts +1 -0
- package/src/municipal/streets/__tests__/streets.spec.ts +253 -0
- package/src/municipal/streets/streets.getters.ts +16 -1
- package/src/municipal/streets/streets.types.ts +1 -0
- package/src/talkpilot/calls/__tests__/calls.spec.ts +189 -1
- package/src/talkpilot/calls/calls.constants.ts +17 -0
- package/src/talkpilot/calls/calls.getters.ts +19 -0
- package/src/talkpilot/calls/calls.types.ts +13 -10
- package/src/talkpilot/clients/clients.types.ts +0 -4
- package/src/talkpilot/clients/index.ts +0 -2
- package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +0 -36
- package/src/talkpilot/clientsConfig/clientsConfig.types.ts +0 -3
- package/dist/talkpilot/clients/clients.quota.getters.d.ts +0 -7
- package/dist/talkpilot/clients/clients.quota.getters.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.quota.getters.js +0 -80
- package/dist/talkpilot/clients/clients.quota.getters.js.map +0 -1
- package/dist/talkpilot/clients/clients.quota.types.d.ts +0 -22
- package/dist/talkpilot/clients/clients.quota.types.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.quota.types.js.map +0 -1
- package/src/talkpilot/clients/__tests__/clients.quota.spec.ts +0 -110
- package/src/talkpilot/clients/clients.quota.getters.ts +0 -119
- package/src/talkpilot/clients/clients.quota.types.ts +0 -25
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStreetsCollection,
|
|
3
|
+
getStreetsByCityName,
|
|
4
|
+
getStreetById,
|
|
5
|
+
getStreetByStreetId,
|
|
6
|
+
getStreetByStreetIdAndCity,
|
|
7
|
+
createStreet,
|
|
8
|
+
bulkWriteStreets,
|
|
9
|
+
} from '../streets.getters';
|
|
10
|
+
import { createStreet as streetFactory } from '../../../test-utils/factories/municipal/streets';
|
|
11
|
+
|
|
12
|
+
const CITY = 'tests' as const;
|
|
13
|
+
|
|
14
|
+
describe('Street — isActive field', () => {
|
|
15
|
+
describe('persistence', () => {
|
|
16
|
+
it('a document inserted without isActive is retrievable and the field is absent', async () => {
|
|
17
|
+
const street = streetFactory({ cityName: CITY });
|
|
18
|
+
delete (street as Partial<typeof street>).isActive;
|
|
19
|
+
|
|
20
|
+
await getStreetsCollection().insertOne(street);
|
|
21
|
+
|
|
22
|
+
const saved = await getStreetsCollection().findOne({ _id: street._id });
|
|
23
|
+
expect(saved).not.toBeNull();
|
|
24
|
+
expect(saved).not.toHaveProperty('isActive');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('isActive: true is persisted and returned', async () => {
|
|
28
|
+
const street = streetFactory({ cityName: CITY, isActive: true });
|
|
29
|
+
await getStreetsCollection().insertOne(street);
|
|
30
|
+
|
|
31
|
+
const saved = await getStreetsCollection().findOne({ _id: street._id });
|
|
32
|
+
expect(saved?.isActive).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('isActive: false is persisted and returned', async () => {
|
|
36
|
+
const street = streetFactory({ cityName: CITY, isActive: false });
|
|
37
|
+
await getStreetsCollection().insertOne(street);
|
|
38
|
+
|
|
39
|
+
const saved = await getStreetsCollection().findOne({ _id: street._id });
|
|
40
|
+
expect(saved?.isActive).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('createStreet', () => {
|
|
45
|
+
it('defaults isActive to true when not provided', async () => {
|
|
46
|
+
const data = streetFactory({ cityName: CITY });
|
|
47
|
+
delete (data as Partial<typeof data>).isActive;
|
|
48
|
+
|
|
49
|
+
const insertedId = await createStreet(data);
|
|
50
|
+
|
|
51
|
+
const saved = await getStreetsCollection().findOne({ _id: insertedId });
|
|
52
|
+
expect(saved?.isActive).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('persists isActive: false when explicitly passed', async () => {
|
|
56
|
+
const data = streetFactory({ cityName: CITY, isActive: false });
|
|
57
|
+
const insertedId = await createStreet(data);
|
|
58
|
+
|
|
59
|
+
const saved = await getStreetsCollection().findOne({ _id: insertedId });
|
|
60
|
+
expect(saved?.isActive).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('querying', () => {
|
|
65
|
+
it('getStreetsByCityName returns all streets regardless of isActive', async () => {
|
|
66
|
+
const active = streetFactory({ cityName: CITY, isActive: true });
|
|
67
|
+
const inactive = streetFactory({ cityName: CITY, isActive: false });
|
|
68
|
+
const noFlag = streetFactory({ cityName: CITY });
|
|
69
|
+
delete (noFlag as Partial<typeof noFlag>).isActive;
|
|
70
|
+
|
|
71
|
+
await getStreetsCollection().insertMany([active, inactive, noFlag]);
|
|
72
|
+
|
|
73
|
+
const all = await getStreetsByCityName(CITY);
|
|
74
|
+
expect(all.length).toBeGreaterThanOrEqual(3);
|
|
75
|
+
|
|
76
|
+
const returnedActive = all.find((s) => s._id.equals(active._id));
|
|
77
|
+
const returnedInactive = all.find((s) => s._id.equals(inactive._id));
|
|
78
|
+
const returnedNoFlag = all.find((s) => s._id.equals(noFlag._id));
|
|
79
|
+
|
|
80
|
+
expect(returnedActive?.isActive).toBe(true);
|
|
81
|
+
expect(returnedInactive?.isActive).toBe(false);
|
|
82
|
+
expect(returnedNoFlag?.isActive).toBeUndefined(); // absent in DB → getter is a pass-through, no normalization
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('getStreetById returns isActive undefined when absent in DB', async () => {
|
|
86
|
+
const street = streetFactory({ cityName: CITY });
|
|
87
|
+
delete (street as Partial<typeof street>).isActive;
|
|
88
|
+
await getStreetsCollection().insertOne(street);
|
|
89
|
+
|
|
90
|
+
const result = await getStreetById(street._id.toString());
|
|
91
|
+
expect(result?.isActive).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('getStreetById returns isActive: false unchanged', async () => {
|
|
95
|
+
const street = streetFactory({ cityName: CITY, isActive: false });
|
|
96
|
+
await getStreetsCollection().insertOne(street);
|
|
97
|
+
|
|
98
|
+
const result = await getStreetById(street._id.toString());
|
|
99
|
+
expect(result?.isActive).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('getStreetByStreetId returns isActive undefined when absent in DB', async () => {
|
|
103
|
+
const street = streetFactory({ cityName: CITY });
|
|
104
|
+
delete (street as Partial<typeof street>).isActive;
|
|
105
|
+
await getStreetsCollection().insertOne(street);
|
|
106
|
+
|
|
107
|
+
const result = await getStreetByStreetId(street.id);
|
|
108
|
+
expect(result?.isActive).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('getStreetByStreetIdAndCity returns isActive undefined when absent in DB', async () => {
|
|
112
|
+
const street = streetFactory({ cityName: CITY });
|
|
113
|
+
delete (street as Partial<typeof street>).isActive;
|
|
114
|
+
await getStreetsCollection().insertOne(street);
|
|
115
|
+
|
|
116
|
+
const result = await getStreetByStreetIdAndCity(street.id, CITY);
|
|
117
|
+
expect(result?.isActive).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('filtering with isActive !== false excludes inactive and includes active and unset documents', async () => {
|
|
121
|
+
const active = streetFactory({ cityName: CITY, isActive: true });
|
|
122
|
+
const inactive = streetFactory({ cityName: CITY, isActive: false });
|
|
123
|
+
const noFlag = streetFactory({ cityName: CITY });
|
|
124
|
+
delete (noFlag as Partial<typeof noFlag>).isActive;
|
|
125
|
+
|
|
126
|
+
await getStreetsCollection().insertMany([active, inactive, noFlag]);
|
|
127
|
+
|
|
128
|
+
// This is the exact filter pattern used by the sync script to skip already-inactive records
|
|
129
|
+
const notInactive = await getStreetsCollection()
|
|
130
|
+
.find({ cityName: CITY, isActive: { $ne: false } })
|
|
131
|
+
.toArray();
|
|
132
|
+
|
|
133
|
+
expect(notInactive.map((s) => s._id.toString())).toEqual(
|
|
134
|
+
expect.arrayContaining([active._id.toString(), noFlag._id.toString()])
|
|
135
|
+
);
|
|
136
|
+
expect(notInactive.map((s) => s._id.toString())).not.toContain(inactive._id.toString());
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('updating', () => {
|
|
141
|
+
it('isActive can be set to false on an existing active document (deactivation)', async () => {
|
|
142
|
+
const street = streetFactory({ cityName: CITY, isActive: true });
|
|
143
|
+
await getStreetsCollection().insertOne(street);
|
|
144
|
+
|
|
145
|
+
await getStreetsCollection().updateOne(
|
|
146
|
+
{ _id: street._id },
|
|
147
|
+
{ $set: { isActive: false, updatedAt: new Date() } }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const updated = await getStreetsCollection().findOne({ _id: street._id });
|
|
151
|
+
expect(updated?.isActive).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('isActive can be set to true on a deactivated document (reactivation)', async () => {
|
|
155
|
+
const street = streetFactory({ cityName: CITY, isActive: false });
|
|
156
|
+
await getStreetsCollection().insertOne(street);
|
|
157
|
+
|
|
158
|
+
await getStreetsCollection().updateOne(
|
|
159
|
+
{ _id: street._id },
|
|
160
|
+
{ $set: { isActive: true, updatedAt: new Date() } }
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const updated = await getStreetsCollection().findOne({ _id: street._id });
|
|
164
|
+
expect(updated?.isActive).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('deactivating a street does not touch other fields', async () => {
|
|
168
|
+
const street = streetFactory({
|
|
169
|
+
cityName: CITY,
|
|
170
|
+
isActive: true,
|
|
171
|
+
name: 'Herzl St',
|
|
172
|
+
hints: [{ id: '1', text: 'near the park' }],
|
|
173
|
+
});
|
|
174
|
+
await getStreetsCollection().insertOne(street);
|
|
175
|
+
|
|
176
|
+
await getStreetsCollection().updateOne(
|
|
177
|
+
{ _id: street._id },
|
|
178
|
+
{ $set: { isActive: false, updatedAt: new Date() } }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const updated = await getStreetsCollection().findOne({ _id: street._id });
|
|
182
|
+
expect(updated?.isActive).toBe(false);
|
|
183
|
+
expect(updated?.name).toBe('Herzl St');
|
|
184
|
+
expect(updated?.hints).toEqual([{ id: '1', text: 'near the park' }]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('bulkWriteStreets', () => {
|
|
189
|
+
it('applies upsert and update operations from a single batch', async () => {
|
|
190
|
+
const existing = streetFactory({ cityName: CITY, name: 'Old Name' });
|
|
191
|
+
await getStreetsCollection().insertOne(existing);
|
|
192
|
+
|
|
193
|
+
const result = await bulkWriteStreets(
|
|
194
|
+
[
|
|
195
|
+
{
|
|
196
|
+
updateOne: {
|
|
197
|
+
filter: { _id: existing._id },
|
|
198
|
+
update: { $set: { name: 'New Name' } },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
updateOne: {
|
|
203
|
+
filter: { id: 'brand-new-street', cityName: CITY },
|
|
204
|
+
update: {
|
|
205
|
+
$set: { name: 'Brand New St', cityName: CITY },
|
|
206
|
+
$setOnInsert: { id: 'brand-new-street', createdAt: new Date(), isActive: true },
|
|
207
|
+
},
|
|
208
|
+
upsert: true,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
{ ordered: false }
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(result.modifiedCount).toBe(1);
|
|
216
|
+
expect(result.upsertedCount).toBe(1);
|
|
217
|
+
|
|
218
|
+
const updated = await getStreetsCollection().findOne({ _id: existing._id });
|
|
219
|
+
expect(updated?.name).toBe('New Name');
|
|
220
|
+
|
|
221
|
+
const inserted = await getStreetsCollection().findOne({ id: 'brand-new-street', cityName: CITY });
|
|
222
|
+
expect(inserted?.name).toBe('Brand New St');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('propagates a MongoBulkWriteError for an invalid operation, preserving partial results', async () => {
|
|
226
|
+
const existing = streetFactory({ cityName: CITY });
|
|
227
|
+
await getStreetsCollection().insertOne(existing);
|
|
228
|
+
|
|
229
|
+
await expect(
|
|
230
|
+
bulkWriteStreets(
|
|
231
|
+
[
|
|
232
|
+
{
|
|
233
|
+
updateOne: {
|
|
234
|
+
filter: { _id: existing._id },
|
|
235
|
+
update: { $set: { name: 'Valid Update' } },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
insertOne: {
|
|
240
|
+
// Duplicate _id triggers a write error without aborting the ordered:false batch
|
|
241
|
+
document: { ...existing },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
{ ordered: false }
|
|
246
|
+
)
|
|
247
|
+
).rejects.toMatchObject({ name: 'MongoBulkWriteError' });
|
|
248
|
+
|
|
249
|
+
const updated = await getStreetsCollection().findOne({ _id: existing._id });
|
|
250
|
+
expect(updated?.name).toBe('Valid Update');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { CityName, getDb, ObjectId, Street } from "../index";
|
|
2
2
|
import type { StreetHint } from "./streets.types";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AnyBulkWriteOperation,
|
|
5
|
+
BulkWriteOptions,
|
|
6
|
+
BulkWriteResult,
|
|
7
|
+
Collection,
|
|
8
|
+
Filter,
|
|
9
|
+
ObjectId as MongoObjectId,
|
|
10
|
+
} from "mongodb";
|
|
4
11
|
|
|
5
12
|
export const getStreetsCollection = (): Collection<Street> => {
|
|
6
13
|
return getDb().collection<Street>("streets");
|
|
7
14
|
};
|
|
8
15
|
|
|
16
|
+
export const bulkWriteStreets = async (
|
|
17
|
+
operations: AnyBulkWriteOperation<Street>[],
|
|
18
|
+
options?: BulkWriteOptions,
|
|
19
|
+
): Promise<BulkWriteResult> => {
|
|
20
|
+
return getStreetsCollection().bulkWrite(operations, options);
|
|
21
|
+
};
|
|
22
|
+
|
|
9
23
|
export const findStreets = async (
|
|
10
24
|
filter: Filter<Street> = {},
|
|
11
25
|
): Promise<Street[]> => {
|
|
@@ -54,6 +68,7 @@ export const createStreet = async (
|
|
|
54
68
|
): Promise<MongoObjectId> => {
|
|
55
69
|
const street: Omit<Street, "_id"> = {
|
|
56
70
|
...streetData,
|
|
71
|
+
isActive: streetData.isActive ?? true,
|
|
57
72
|
createdAt: new Date(),
|
|
58
73
|
updatedAt: new Date(),
|
|
59
74
|
};
|
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
getCallsByClientAndDateRange,
|
|
10
10
|
getCallsCollection,
|
|
11
11
|
pushToolExecution,
|
|
12
|
+
getToolExecutionsByCallSid,
|
|
12
13
|
updateCallStatusByCallSid,
|
|
13
14
|
} from "../calls.getters";
|
|
14
|
-
import type { ToolExecution } from "../calls.types";
|
|
15
|
+
import type { ToolExecution, ToolExecutionStep } from "../calls.types";
|
|
15
16
|
import { ObjectId } from "mongodb";
|
|
16
17
|
import { createOutGoingCallDoc } from "../../../test-utils/factories";
|
|
17
18
|
|
|
@@ -242,6 +243,193 @@ describe("db.calls", () => {
|
|
|
242
243
|
});
|
|
243
244
|
});
|
|
244
245
|
|
|
246
|
+
describe("getToolExecutionsByCallSid()", () => {
|
|
247
|
+
const makeExecution = (
|
|
248
|
+
overrides?: Partial<ToolExecution>,
|
|
249
|
+
): ToolExecution => ({
|
|
250
|
+
toolName: "testTool",
|
|
251
|
+
executedAt: new Date(),
|
|
252
|
+
durationMs: 100,
|
|
253
|
+
args: { query: "test" },
|
|
254
|
+
meta: { kind: "http", url: "https://example.com", method: "POST" },
|
|
255
|
+
status: "success",
|
|
256
|
+
httpStatus: 200,
|
|
257
|
+
...overrides,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("returns all executions for a call", async () => {
|
|
261
|
+
const call = createOutGoingCallDoc();
|
|
262
|
+
await createCallDoc(call);
|
|
263
|
+
await pushToolExecution(call.callSid, makeExecution({ toolName: "first" }));
|
|
264
|
+
await pushToolExecution(call.callSid, makeExecution({ toolName: "second" }));
|
|
265
|
+
|
|
266
|
+
const result = await getToolExecutionsByCallSid(call.callSid);
|
|
267
|
+
|
|
268
|
+
expect(result).toHaveLength(2);
|
|
269
|
+
expect(result.map((e) => e.toolName)).toEqual(["first", "second"]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("returns an empty array when the call has no executions", async () => {
|
|
273
|
+
const call = createOutGoingCallDoc();
|
|
274
|
+
await createCallDoc(call);
|
|
275
|
+
|
|
276
|
+
const result = await getToolExecutionsByCallSid(call.callSid);
|
|
277
|
+
|
|
278
|
+
expect(result).toEqual([]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("returns an empty array for an unknown callSid", async () => {
|
|
282
|
+
const result = await getToolExecutionsByCallSid("nonexistent-sid");
|
|
283
|
+
|
|
284
|
+
expect(result).toEqual([]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("respects skip and limit for pagination", async () => {
|
|
288
|
+
const call = createOutGoingCallDoc();
|
|
289
|
+
await createCallDoc(call);
|
|
290
|
+
for (const name of ["a", "b", "c", "d", "e"]) {
|
|
291
|
+
await pushToolExecution(call.callSid, makeExecution({ toolName: name }));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const page1 = await getToolExecutionsByCallSid(call.callSid, { skip: 0, limit: 2 });
|
|
295
|
+
const page2 = await getToolExecutionsByCallSid(call.callSid, { skip: 2, limit: 2 });
|
|
296
|
+
const page3 = await getToolExecutionsByCallSid(call.callSid, { skip: 4, limit: 2 });
|
|
297
|
+
|
|
298
|
+
expect(page1.map((e) => e.toolName)).toEqual(["a", "b"]);
|
|
299
|
+
expect(page2.map((e) => e.toolName)).toEqual(["c", "d"]);
|
|
300
|
+
expect(page3.map((e) => e.toolName)).toEqual(["e"]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("includes steps when present", async () => {
|
|
304
|
+
const call = createOutGoingCallDoc();
|
|
305
|
+
await createCallDoc(call);
|
|
306
|
+
const steps: ToolExecutionStep[] = [
|
|
307
|
+
{ timestamp: new Date("2024-01-01T10:00:00Z"), label: "audio_enrichment", level: "info", data: { audioUsed: true } },
|
|
308
|
+
{ timestamp: new Date("2024-01-01T10:00:01Z"), label: "subject_classification", level: "warn" },
|
|
309
|
+
];
|
|
310
|
+
await pushToolExecution(call.callSid, makeExecution({ steps }));
|
|
311
|
+
|
|
312
|
+
const result = await getToolExecutionsByCallSid(call.callSid);
|
|
313
|
+
|
|
314
|
+
expect(result[0].steps).toHaveLength(2);
|
|
315
|
+
const s0 = result[0].steps![0];
|
|
316
|
+
expect(s0.label).toBe("audio_enrichment");
|
|
317
|
+
expect(s0.level).toBe("info");
|
|
318
|
+
expect(new Date(s0.timestamp).toISOString()).toBe("2024-01-01T10:00:00.000Z");
|
|
319
|
+
expect(s0.data).toEqual({ audioUsed: true });
|
|
320
|
+
const s1 = result[0].steps![1];
|
|
321
|
+
expect(s1.label).toBe("subject_classification");
|
|
322
|
+
expect(s1.level).toBe("warn");
|
|
323
|
+
expect(s1.data).toBeUndefined();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("includes step payload when present", async () => {
|
|
327
|
+
const call = createOutGoingCallDoc();
|
|
328
|
+
await createCallDoc(call);
|
|
329
|
+
const steps: ToolExecutionStep[] = [
|
|
330
|
+
{
|
|
331
|
+
timestamp: new Date("2024-01-01T10:00:00Z"),
|
|
332
|
+
label: "subject_classification",
|
|
333
|
+
level: "info",
|
|
334
|
+
data: "Classified as: ניקיון / אשפה",
|
|
335
|
+
payload: { subject: "ניקיון", subSubject: "אשפה", ids: { subject: "abc", subSubject: "def" } },
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
await pushToolExecution(call.callSid, makeExecution({ steps }));
|
|
339
|
+
|
|
340
|
+
const result = await getToolExecutionsByCallSid(call.callSid);
|
|
341
|
+
|
|
342
|
+
const s0 = result[0].steps![0];
|
|
343
|
+
expect(s0.data).toBe("Classified as: ניקיון / אשפה");
|
|
344
|
+
expect(s0.payload).toEqual({ subject: "ניקיון", subSubject: "אשפה", ids: { subject: "abc", subSubject: "def" } });
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("does not require step payload to be present", async () => {
|
|
348
|
+
const call = createOutGoingCallDoc();
|
|
349
|
+
await createCallDoc(call);
|
|
350
|
+
const steps: ToolExecutionStep[] = [
|
|
351
|
+
{ timestamp: new Date(), label: "user_query", level: "info", data: "Query: הרצל" },
|
|
352
|
+
];
|
|
353
|
+
await pushToolExecution(call.callSid, makeExecution({ steps }));
|
|
354
|
+
|
|
355
|
+
const result = await getToolExecutionsByCallSid(call.callSid);
|
|
356
|
+
|
|
357
|
+
expect(result[0].steps![0].payload).toBeUndefined();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("does not require steps to be present", async () => {
|
|
361
|
+
const call = createOutGoingCallDoc();
|
|
362
|
+
await createCallDoc(call);
|
|
363
|
+
await pushToolExecution(call.callSid, makeExecution());
|
|
364
|
+
|
|
365
|
+
const result = await getToolExecutionsByCallSid(call.callSid);
|
|
366
|
+
|
|
367
|
+
expect(result[0].steps).toBeUndefined();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("returns at most 50 executions by default", async () => {
|
|
371
|
+
const call = createOutGoingCallDoc();
|
|
372
|
+
await getCallsCollection().insertOne({
|
|
373
|
+
...call,
|
|
374
|
+
createdAt: new Date(),
|
|
375
|
+
updatedAt: new Date(),
|
|
376
|
+
env: "test",
|
|
377
|
+
toolExecutions: Array.from({ length: 51 }, (_, i) =>
|
|
378
|
+
makeExecution({ toolName: `tool${i}` }),
|
|
379
|
+
),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const result = await getToolExecutionsByCallSid(call.callSid);
|
|
383
|
+
|
|
384
|
+
expect(result).toHaveLength(50);
|
|
385
|
+
expect(result[0].toolName).toBe("tool0");
|
|
386
|
+
expect(result[49].toolName).toBe("tool49");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("clamps negative skip to 0", async () => {
|
|
390
|
+
const call = createOutGoingCallDoc();
|
|
391
|
+
await createCallDoc(call);
|
|
392
|
+
await pushToolExecution(call.callSid, makeExecution({ toolName: "only" }));
|
|
393
|
+
|
|
394
|
+
const result = await getToolExecutionsByCallSid(call.callSid, { skip: -5 });
|
|
395
|
+
|
|
396
|
+
expect(result).toHaveLength(1);
|
|
397
|
+
expect(result[0].toolName).toBe("only");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("clamps negative limit to 1", async () => {
|
|
401
|
+
const call = createOutGoingCallDoc();
|
|
402
|
+
await createCallDoc(call);
|
|
403
|
+
await pushToolExecution(call.callSid, makeExecution({ toolName: "first" }));
|
|
404
|
+
await pushToolExecution(call.callSid, makeExecution({ toolName: "second" }));
|
|
405
|
+
|
|
406
|
+
const result = await getToolExecutionsByCallSid(call.callSid, { limit: -1 });
|
|
407
|
+
|
|
408
|
+
expect(result).toHaveLength(1);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("returns executions when clientId matches the call's client", async () => {
|
|
412
|
+
const call = createOutGoingCallDoc({ clientId: "clientA" });
|
|
413
|
+
await createCallDoc(call);
|
|
414
|
+
await pushToolExecution(call.callSid, makeExecution({ toolName: "secret" }));
|
|
415
|
+
|
|
416
|
+
const result = await getToolExecutionsByCallSid(call.callSid, { clientId: "clientA" });
|
|
417
|
+
|
|
418
|
+
expect(result).toHaveLength(1);
|
|
419
|
+
expect(result[0].toolName).toBe("secret");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("returns empty when clientId does not match the call's client", async () => {
|
|
423
|
+
const call = createOutGoingCallDoc({ clientId: "clientA" });
|
|
424
|
+
await createCallDoc(call);
|
|
425
|
+
await pushToolExecution(call.callSid, makeExecution({ toolName: "secret" }));
|
|
426
|
+
|
|
427
|
+
const result = await getToolExecutionsByCallSid(call.callSid, { clientId: "clientB" });
|
|
428
|
+
|
|
429
|
+
expect(result).toEqual([]);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
245
433
|
describe("getCallsByClientAndDateRange()", () => {
|
|
246
434
|
it("should return calls within date range", async () => {
|
|
247
435
|
const clientId = "dateRangeClient";
|
|
@@ -30,3 +30,20 @@ export const STATS_MAX_TIME_MS = 30_000;
|
|
|
30
30
|
*/
|
|
31
31
|
export const TREND_MONTH_BUCKET_MIN_MONTHS = 6;
|
|
32
32
|
export const TREND_WEEK_BUCKET_MIN_MONTHS = 3;
|
|
33
|
+
|
|
34
|
+
export const TOOL_EXECUTIONS_DEFAULT_SKIP = 0;
|
|
35
|
+
export const TOOL_EXECUTIONS_DEFAULT_LIMIT = 50;
|
|
36
|
+
|
|
37
|
+
export const CallStatusValues = {
|
|
38
|
+
Initiated: "initiated",
|
|
39
|
+
Ringing: "ringing",
|
|
40
|
+
InProgress: "in-progress",
|
|
41
|
+
Answered: "answered",
|
|
42
|
+
Completed: "completed",
|
|
43
|
+
Busy: "busy",
|
|
44
|
+
NoAnswer: "no-answer",
|
|
45
|
+
Failed: "failed",
|
|
46
|
+
Canceled: "canceled",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
export const ALL_CALL_STATUSES = Object.values(CallStatusValues);
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
getDb,
|
|
7
7
|
} from "../index";
|
|
8
8
|
import type { CountOpts, DateRange, ToolExecution, CallStatus } from "./calls.types";
|
|
9
|
+
import {
|
|
10
|
+
TOOL_EXECUTIONS_DEFAULT_LIMIT,
|
|
11
|
+
TOOL_EXECUTIONS_DEFAULT_SKIP,
|
|
12
|
+
} from "./calls.constants";
|
|
9
13
|
import { Filter, ObjectId } from "mongodb";
|
|
10
14
|
import * as process from "node:process";
|
|
11
15
|
import { applyQueryOptions } from "../utils/query.utils";
|
|
@@ -60,6 +64,21 @@ export const updateCallByCallSid = async (
|
|
|
60
64
|
);
|
|
61
65
|
};
|
|
62
66
|
|
|
67
|
+
export const getToolExecutionsByCallSid = async (
|
|
68
|
+
callSid: string,
|
|
69
|
+
opts?: Pick<CallQueryOptions, "skip" | "limit"> & { clientId?: string },
|
|
70
|
+
): Promise<ToolExecution[]> => {
|
|
71
|
+
const skip = Math.max(0, opts?.skip ?? TOOL_EXECUTIONS_DEFAULT_SKIP);
|
|
72
|
+
const limit = Math.max(1, opts?.limit ?? TOOL_EXECUTIONS_DEFAULT_LIMIT);
|
|
73
|
+
const filter: Filter<Call> = { callSid };
|
|
74
|
+
if (opts?.clientId != null) filter.clientId = opts.clientId;
|
|
75
|
+
const call = await getCallsCollection().findOne(
|
|
76
|
+
filter,
|
|
77
|
+
{ projection: { toolExecutions: { $slice: [skip, limit] } } },
|
|
78
|
+
);
|
|
79
|
+
return call?.toolExecutions ?? [];
|
|
80
|
+
};
|
|
81
|
+
|
|
63
82
|
export const updateCallStatusByCallSid = async (
|
|
64
83
|
callSid: string,
|
|
65
84
|
status: CallStatus,
|
|
@@ -3,6 +3,7 @@ import { TranscriptionSegment } from "../results";
|
|
|
3
3
|
import { LeadProperty } from "../leads";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
CallStatusValues,
|
|
6
7
|
CONFERENCE_ROLE_CUSTOMER,
|
|
7
8
|
CONFERENCE_ROLE_SUPERVISOR,
|
|
8
9
|
} from "./calls.constants";
|
|
@@ -12,16 +13,7 @@ export type ConferenceRole =
|
|
|
12
13
|
| typeof CONFERENCE_ROLE_SUPERVISOR
|
|
13
14
|
| null;
|
|
14
15
|
|
|
15
|
-
export type CallStatus =
|
|
16
|
-
| "initiated"
|
|
17
|
-
| "ringing"
|
|
18
|
-
| "in-progress"
|
|
19
|
-
| "answered"
|
|
20
|
-
| "completed"
|
|
21
|
-
| "busy"
|
|
22
|
-
| "no-answer"
|
|
23
|
-
| "failed"
|
|
24
|
-
| "canceled";
|
|
16
|
+
export type CallStatus = (typeof CallStatusValues)[keyof typeof CallStatusValues];
|
|
25
17
|
|
|
26
18
|
export type Call = {
|
|
27
19
|
callSid: string;
|
|
@@ -64,6 +56,17 @@ export type ToolExecution = {
|
|
|
64
56
|
status: "success" | "error";
|
|
65
57
|
httpStatus?: number;
|
|
66
58
|
response?: Record<string, unknown>;
|
|
59
|
+
steps?: ToolExecutionStep[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type ToolExecutionStepLevel = "info" | "warn" | "error";
|
|
63
|
+
|
|
64
|
+
export type ToolExecutionStep = {
|
|
65
|
+
timestamp: Date;
|
|
66
|
+
label: string;
|
|
67
|
+
level: ToolExecutionStepLevel;
|
|
68
|
+
data?: Record<string, unknown> | string;
|
|
69
|
+
payload?: Record<string, unknown>;
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
export type ToolExecutionMeta =
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { ObjectId } from "mongodb";
|
|
2
|
-
import { ClientQuotaSettings } from "./clients.quota.types";
|
|
3
|
-
|
|
4
|
-
export type { QuotaThresholdAlert, ClientQuotaSettings, QuotaAlertInput, ClientQuotaUsage, ClientQuotaState } from "./clients.quota.types";
|
|
5
2
|
|
|
6
3
|
export type Client = {
|
|
7
4
|
_id?: ObjectId;
|
|
@@ -12,7 +9,6 @@ export type Client = {
|
|
|
12
9
|
apiKey?: string | null;
|
|
13
10
|
credits: number;
|
|
14
11
|
managedBy?: string;
|
|
15
|
-
quota?: ClientQuotaSettings;
|
|
16
12
|
};
|
|
17
13
|
|
|
18
14
|
export type ClientDoc = Client & { _id: ObjectId };
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
KnownProductKey,
|
|
8
8
|
} from "../index";
|
|
9
9
|
import { Collection } from "mongodb";
|
|
10
|
-
import { resetClientQuotaSettings } from "../clients/clients.quota.getters";
|
|
11
10
|
|
|
12
11
|
export const getClientsConfigCollection = (): Collection<ClientConfigDoc> => {
|
|
13
12
|
return getDb().collection<ClientConfigDoc>("clientsConfig");
|
|
@@ -78,38 +77,3 @@ export const clearActiveScanId = async (clientId: string): Promise<void> => {
|
|
|
78
77
|
throw new Error(`No client config found for clientId: ${clientId}`);
|
|
79
78
|
}
|
|
80
79
|
};
|
|
81
|
-
|
|
82
|
-
export async function incrementClientQuotaUsage(
|
|
83
|
-
clientId: string,
|
|
84
|
-
amount: number = 1,
|
|
85
|
-
): Promise<void> {
|
|
86
|
-
const result = await getClientsConfigCollection().updateOne(
|
|
87
|
-
{ clientId, "quota.totalQuota": { $exists: true } },
|
|
88
|
-
{ $inc: { "quota.usedQuota": amount } },
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
if (result.matchedCount === 0) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export async function resetClientQuotaUsage(clientId: string): Promise<void> {
|
|
97
|
-
const result = await getClientsConfigCollection().updateOne(
|
|
98
|
-
{ clientId },
|
|
99
|
-
{ $set: { "quota.usedQuota": 0 } },
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
if (result.matchedCount === 0) {
|
|
103
|
-
throw new Error(`No client config found for clientId: ${clientId}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
await resetClientQuotaSettings(clientId);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export {
|
|
110
|
-
getClientQuotaState,
|
|
111
|
-
updateQuotaAlertsConfig,
|
|
112
|
-
markQuotaAlertSent,
|
|
113
|
-
markQuotaExpiredSent,
|
|
114
|
-
resetClientQuotaSettings,
|
|
115
|
-
} from "../clients/clients.quota.getters";
|
|
@@ -111,8 +111,6 @@ export type CommunicationsConfig = {
|
|
|
111
111
|
whatsapp?: WhatsappConfig;
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
import type { ClientQuotaUsage } from "../clients/clients.quota.types";
|
|
115
|
-
|
|
116
114
|
export type ClientConfig<P extends Products = Products> = {
|
|
117
115
|
clientId: string;
|
|
118
116
|
phoneNumber?: string;
|
|
@@ -121,7 +119,6 @@ export type ClientConfig<P extends Products = Products> = {
|
|
|
121
119
|
timezone?: string;
|
|
122
120
|
communications?: CommunicationsConfig;
|
|
123
121
|
callLengthThresholds?: CallLengthThresholds;
|
|
124
|
-
quota?: ClientQuotaUsage;
|
|
125
122
|
};
|
|
126
123
|
|
|
127
124
|
export type ClientConfigDoc<P extends Products = Products> = WithId<
|