tabletcommand-location 2.4.2 → 2.4.3

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 (62) hide show
  1. package/.claude/worktrees/sc-36275/.buildkite/pipeline.yml +5 -0
  2. package/.claude/worktrees/sc-36275/README.md +3 -0
  3. package/.claude/worktrees/sc-36275/build/audience.js +38 -0
  4. package/.claude/worktrees/sc-36275/build/audience.js.map +1 -0
  5. package/.claude/worktrees/sc-36275/build/index.js +95 -0
  6. package/.claude/worktrees/sc-36275/build/index.js.map +1 -0
  7. package/.claude/worktrees/sc-36275/build/location.js +278 -0
  8. package/.claude/worktrees/sc-36275/build/location.js.map +1 -0
  9. package/.claude/worktrees/sc-36275/build/query.js +378 -0
  10. package/.claude/worktrees/sc-36275/build/query.js.map +1 -0
  11. package/.claude/worktrees/sc-36275/build/store.js +182 -0
  12. package/.claude/worktrees/sc-36275/build/store.js.map +1 -0
  13. package/.claude/worktrees/sc-36275/build/test/location.js +522 -0
  14. package/.claude/worktrees/sc-36275/build/test/location.js.map +1 -0
  15. package/.claude/worktrees/sc-36275/build/test/mock.js +165 -0
  16. package/.claude/worktrees/sc-36275/build/test/mock.js.map +1 -0
  17. package/.claude/worktrees/sc-36275/build/test/query.js +174 -0
  18. package/.claude/worktrees/sc-36275/build/test/query.js.map +1 -0
  19. package/.claude/worktrees/sc-36275/build/types.js +8 -0
  20. package/.claude/worktrees/sc-36275/build/types.js.map +1 -0
  21. package/.claude/worktrees/sc-36275/cspell.json +64 -0
  22. package/.claude/worktrees/sc-36275/definitions/audience.d.ts +18 -0
  23. package/.claude/worktrees/sc-36275/definitions/audience.d.ts.map +1 -0
  24. package/.claude/worktrees/sc-36275/definitions/index.d.ts +11 -0
  25. package/.claude/worktrees/sc-36275/definitions/index.d.ts.map +1 -0
  26. package/.claude/worktrees/sc-36275/definitions/location.d.ts +32 -0
  27. package/.claude/worktrees/sc-36275/definitions/location.d.ts.map +1 -0
  28. package/.claude/worktrees/sc-36275/definitions/query.d.ts +36 -0
  29. package/.claude/worktrees/sc-36275/definitions/query.d.ts.map +1 -0
  30. package/.claude/worktrees/sc-36275/definitions/store.d.ts +64 -0
  31. package/.claude/worktrees/sc-36275/definitions/store.d.ts.map +1 -0
  32. package/.claude/worktrees/sc-36275/definitions/test/location.d.ts +2 -0
  33. package/.claude/worktrees/sc-36275/definitions/test/location.d.ts.map +1 -0
  34. package/.claude/worktrees/sc-36275/definitions/test/mock.d.ts +346 -0
  35. package/.claude/worktrees/sc-36275/definitions/test/mock.d.ts.map +1 -0
  36. package/.claude/worktrees/sc-36275/definitions/test/query.d.ts +2 -0
  37. package/.claude/worktrees/sc-36275/definitions/test/query.d.ts.map +1 -0
  38. package/.claude/worktrees/sc-36275/definitions/types.d.ts +53 -0
  39. package/.claude/worktrees/sc-36275/definitions/types.d.ts.map +1 -0
  40. package/.claude/worktrees/sc-36275/eslint.config.mjs +95 -0
  41. package/.claude/worktrees/sc-36275/gulpfile.js +23 -0
  42. package/.claude/worktrees/sc-36275/package-lock.json +9404 -0
  43. package/.claude/worktrees/sc-36275/package.json +54 -0
  44. package/.claude/worktrees/sc-36275/src/audience.ts +54 -0
  45. package/.claude/worktrees/sc-36275/src/index.ts +81 -0
  46. package/.claude/worktrees/sc-36275/src/location.ts +315 -0
  47. package/.claude/worktrees/sc-36275/src/query.ts +497 -0
  48. package/.claude/worktrees/sc-36275/src/store.ts +204 -0
  49. package/.claude/worktrees/sc-36275/src/test/location.ts +485 -0
  50. package/.claude/worktrees/sc-36275/src/test/mock.ts +186 -0
  51. package/.claude/worktrees/sc-36275/src/test/query.ts +209 -0
  52. package/.claude/worktrees/sc-36275/src/tsconfig.json +30 -0
  53. package/.claude/worktrees/sc-36275/src/types.ts +74 -0
  54. package/.claude/worktrees/sc-36275/test.sh +32 -0
  55. package/build/query.js +1 -1
  56. package/build/query.js.map +1 -1
  57. package/build/test/query.js +2 -2
  58. package/build/test/query.js.map +1 -1
  59. package/cspell.json +1 -0
  60. package/package.json +1 -1
  61. package/src/query.ts +1 -1
  62. package/src/test/query.ts +2 -2
@@ -0,0 +1,485 @@
1
+ import { assert } from "chai";
2
+ import { describe, it, describe as context, before, after, beforeEach, afterEach } from "node:test";
3
+ import * as modelsModule from "tabletcommand-backend-models";
4
+ import storeModule, { StoreModule } from "../store";
5
+ import mockModule from "./mock";
6
+ import locationModule, { LocationModule } from "../location";
7
+ import { LocationPayload } from "../types";
8
+ import { AccountIndustry, BackendModels } from "tabletcommand-backend-models";
9
+
10
+ let location: LocationModule;
11
+ let store: StoreModule;
12
+
13
+ const mock = mockModule(modelsModule);
14
+ const mockDepartment = mock.department;
15
+ let models: BackendModels;
16
+
17
+ describe("location", () => {
18
+ before(async () => {
19
+ await mock.before();
20
+ });
21
+
22
+ beforeEach(async () => {
23
+ await mock.beforeEach();
24
+ models = mock.getModels();
25
+ store = storeModule(
26
+ models.Department,
27
+ models.Location,
28
+ models.CADVehicle,
29
+ models.DeviceMapping
30
+ );
31
+ location = locationModule(store);
32
+ });
33
+
34
+ afterEach(async () => {
35
+ await mock.afterEach();
36
+ });
37
+
38
+ after(async () => {
39
+ await mock.after();
40
+ });
41
+
42
+ context("Process Locations Items", () => {
43
+ it("Will save locations (long lat strings) and add CAD Vehicle records: ", async () => {
44
+ const atDate = new Date();
45
+ const department = mockDepartment;
46
+ /* cSpell:disable */
47
+ const items = [
48
+ {
49
+ Interface: "Fake Interface",
50
+ vehicleId: "BEEDOO1",
51
+ radioName: "BEEDOO1",
52
+ statusCode: "RED",
53
+ latitude: "34.35264",
54
+ longitude: "-119.0669",
55
+ heading: "",
56
+ speed: 5,
57
+ locationCurrent: "1st\\MAIN",
58
+ locationDestination: "Gru's House",
59
+ avlTime: "1602563494"
60
+ },
61
+ {
62
+ Interface: "Fake Interface",
63
+ vehicleId: "BEEDOO2",
64
+ radioName: "BEEDOO2",
65
+ statusCode: "BLUE",
66
+ latitude: "34.35264",
67
+ longitude: "-119.0669",
68
+ heading: "",
69
+ speed: 10,
70
+ locationCurrent: "2nd\\MAIN",
71
+ locationDestination: "Gru's House2",
72
+ avlTime: "1602563495"
73
+ }] as unknown as LocationPayload[];
74
+ await location.processItems(department, items, atDate);
75
+ const locationRecords = await models.Location.find({});
76
+ assert.equal(locationRecords[0].userId, "BEEDOO1-BEEDOO1");
77
+ assert.equal(locationRecords[0].departmentId, "5195426cc4e016a988000969");
78
+ assert.equal(locationRecords[0].username, "BEEDOO1");
79
+ assert.equal(locationRecords[0].locationGeoJSON.coordinates[0], -119.0669);
80
+ assert.equal(locationRecords[0].locationGeoJSON.coordinates[1], 34.35264);
81
+ assert.equal(locationRecords[0].speed, 5);
82
+ assert.equal(locationRecords[0].personnelUUID, "");
83
+ assert.equal(locationRecords.length, 2);
84
+ const vehicles = await models.CADVehicle.find({});
85
+ assert.equal(vehicles[0].vehicleId, "BEEDOO1");
86
+ assert.equal(vehicles[0].radioName, "BEEDOO1");
87
+ assert.equal(vehicles[0].mapHidden, false);
88
+ assert.equal(vehicles.length, 2);
89
+ });
90
+ it("Will save locations (long lat numbers) and add CAD Vehicle records: ", async () => {
91
+ const atDate = new Date();
92
+ const department = mockDepartment;
93
+ const items = [{
94
+ Interface: "Fake Interface",
95
+ vehicleId: "BEEDOO1",
96
+ radioName: "BEEDOO1",
97
+ statusCode: "RED",
98
+ latitude: 34.35264,
99
+ longitude: -119.0669,
100
+ heading: "",
101
+ locationCurrent: "1st\\MAIN",
102
+ locationDestination: "Gru's House",
103
+ avlTime: "1602563494"
104
+ }] as unknown as LocationPayload[];
105
+ await location.processItems(department, items, atDate);
106
+ const locationRecord = await models.Location.find({});
107
+ assert.equal(locationRecord[0]?.userId, "BEEDOO1-BEEDOO1");
108
+ assert.equal(locationRecord[0]?.departmentId, "5195426cc4e016a988000969");
109
+ assert.equal(locationRecord[0]?.username, "BEEDOO1");
110
+ assert.equal(locationRecord[0]?.locationGeoJSON.coordinates[0], -119.0669);
111
+ assert.equal(locationRecord[0]?.locationGeoJSON.coordinates[1], 34.35264);
112
+ assert.equal(locationRecord[0]?.speed, -1);
113
+ assert.equal(locationRecord[0]?.personnelUUID, "");
114
+ assert.equal(locationRecord[0]?.visibility.includes("visible"), true);
115
+ assert.equal(locationRecord[0]?.visibility.includes("shared"), true);
116
+ const vehicle = await models.CADVehicle.find({});
117
+ assert.equal(vehicle[0]?.vehicleId, "BEEDOO1");
118
+ assert.equal(vehicle[0]?.radioName, "BEEDOO1");
119
+ assert.equal(vehicle[0]?.mapHidden, false);
120
+ });
121
+ it("Will save geotab locations (long lat numbers) and add use device mappping data on record save: ", async () => {
122
+ const atDate = new Date();
123
+ const department = mockDepartment;
124
+ const items = [{
125
+ Interface: "Fake Interface",
126
+ vehicleId: "BEEDOO3",
127
+ radioName: "BEEDOO3",
128
+ statusCode: "RED",
129
+ latitude: 34.35264,
130
+ longitude: -119.0669,
131
+ heading: "",
132
+ locationCurrent: "1st\\MAIN",
133
+ locationDestination: "Gru's House",
134
+ source: "Geotab",
135
+ avlTime: "1602563494"
136
+ }] as unknown as LocationPayload[];
137
+ await location.processItemsDeviceMappingGeotab(department, items, atDate);
138
+ const locationRecord = await models.Location.find({});
139
+ assert.equal(locationRecord[0]?.userId, "BOOPBEEP99-BEEDOO3");
140
+ assert.equal(locationRecord[0]?.departmentId, "5195426cc4e016a988000969");
141
+ assert.equal(locationRecord[0]?.username, "BOOPBEEP99");
142
+ assert.equal(locationRecord[0]?.locationGeoJSON.coordinates[0], -119.0669);
143
+ assert.equal(locationRecord[0]?.locationGeoJSON.coordinates[1], 34.35264);
144
+ assert.equal(locationRecord[0]?.speed, -1);
145
+ assert.equal(locationRecord[0]?.personnelUUID, "");
146
+ assert.equal(locationRecord[0]?.sendToCAD, true);
147
+ assert.equal(locationRecord[0]?.shared, true);
148
+ assert.equal(locationRecord[0]?.visibility.includes("cad"), true);
149
+ assert.equal(locationRecord[0]?.visibility.includes("visible"), true);
150
+ assert.equal(locationRecord[0]?.visibility.includes("shared"), true);
151
+ /* cSpell:enable */
152
+ });
153
+ it("applies person invariants on the geotab path, ignoring device mapping", async () => {
154
+ const department = mockDepartment;
155
+ // BEEDOO3 has a device mapping with mapId BOOPBEEP99 and locationToCAD:
156
+ // true; a person payload must not pick either of those up.
157
+ const items = [{
158
+ vehicleId: "BEEDOO3",
159
+ radioName: "Smokey Bear",
160
+ latitude: 34.35264,
161
+ longitude: -119.0669,
162
+ avlTime: "2026-07-10T15:43:36.816Z",
163
+ kindType: "person",
164
+ locationType: "person",
165
+ source: "TXRX",
166
+ transponderUUID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
167
+ userId: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
168
+ session: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
169
+ username: "Smokey Bear",
170
+ visibility: [],
171
+ }] as unknown as LocationPayload[];
172
+ await location.processItemsDeviceMappingGeotab(department, items, new Date());
173
+ const recs = await models.Location.find({});
174
+ assert.equal(recs.length, 1);
175
+ assert.equal(recs[0]?.username, "Smokey Bear");
176
+ assert.equal(recs[0]?.session, "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
177
+ assert.equal(recs[0]?.transponderUUID, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
178
+ assert.equal(recs[0]?.active, true);
179
+ assert.equal(recs[0]?.shared, false);
180
+ assert.equal(recs[0]?.sendToCAD, false);
181
+ assert.deepEqual(recs[0]?.visibility, ["visible"]);
182
+ });
183
+ it("skips geotab person locations with neither personnelUUID nor transponderUUID", async () => {
184
+ const department = mockDepartment;
185
+ const items = [{
186
+ vehicleId: "BEEDOO3",
187
+ radioName: "Smokey Bear",
188
+ latitude: 34.35264,
189
+ longitude: -119.0669,
190
+ avlTime: "2026-07-10T15:43:36.816Z",
191
+ kindType: "person",
192
+ locationType: "person",
193
+ source: "TXRX",
194
+ visibility: [],
195
+ }] as unknown as LocationPayload[];
196
+ await location.processItemsDeviceMappingGeotab(department, items, new Date());
197
+ const recs = await models.Location.find({});
198
+ assert.equal(recs.length, 0);
199
+ });
200
+ it("saves person locations keyed by session with person invariants, no CAD vehicle", async () => {
201
+ const atDate = new Date();
202
+ const department = mockDepartment;
203
+ const items = [{
204
+ vehicleId: "12345",
205
+ radioName: "SmokeyBear",
206
+ latitude: 34.35264,
207
+ longitude: -119.0669,
208
+ avlTime: "2026-07-10T15:43:36.816Z",
209
+ kindType: "person",
210
+ locationType: "person",
211
+ kind: "",
212
+ type: "",
213
+ source: "TXRX",
214
+ personnelUUID: "11111111-2222-3333-4444-555555555555",
215
+ userId: "TXRX-12345",
216
+ session: "TXRX-12345",
217
+ username: "SmokeyBear",
218
+ visibility: [],
219
+ }] as unknown as LocationPayload[];
220
+ await location.processItems(department, items, atDate);
221
+ const recs = await models.Location.find({});
222
+ assert.equal(recs.length, 1);
223
+ assert.equal(recs[0]?.kindType, "person");
224
+ assert.equal(recs[0]?.locationType, "person");
225
+ assert.equal(recs[0]?.personnelUUID, "11111111-2222-3333-4444-555555555555");
226
+ assert.equal(recs[0]?.transponderUUID, "");
227
+ assert.equal(recs[0]?.username, "SmokeyBear");
228
+ assert.equal(recs[0]?.userId, "TXRX-12345");
229
+ assert.equal(recs[0]?.session, "TXRX-12345");
230
+ assert.equal(recs[0]?.active, true);
231
+ assert.equal(recs[0]?.shared, false);
232
+ assert.equal(recs[0]?.sendToCAD, false);
233
+ assert.deepEqual(recs[0]?.visibility, ["visible"]);
234
+ // Person records must not create phantom CAD vehicles
235
+ const vehicles = await models.CADVehicle.find({});
236
+ assert.equal(vehicles.length, 0);
237
+ });
238
+ it("stamps kindType on person locations classified by locationType alone", async () => {
239
+ const atDate = new Date();
240
+ const department = mockDepartment;
241
+ const items = [{
242
+ vehicleId: "12345",
243
+ radioName: "SmokeyBear",
244
+ latitude: 34.35264,
245
+ longitude: -119.0669,
246
+ avlTime: "2026-07-10T15:43:36.816Z",
247
+ locationType: "person",
248
+ source: "TXRX",
249
+ personnelUUID: "11111111-2222-3333-4444-555555555555",
250
+ userId: "TXRX-12345",
251
+ session: "TXRX-12345",
252
+ username: "SmokeyBear",
253
+ visibility: [],
254
+ }] as unknown as LocationPayload[];
255
+ await location.processItems(department, items, atDate);
256
+ const recs = await models.Location.find({});
257
+ assert.equal(recs.length, 1);
258
+ // The person feed queries kindType: "person", so the record must not
259
+ // rely on the payload having set it
260
+ assert.equal(recs[0]?.kindType, "person");
261
+ assert.equal(recs[0]?.locationType, "person");
262
+ assert.equal(recs[0]?.personnelUUID, "11111111-2222-3333-4444-555555555555");
263
+ const vehicles = await models.CADVehicle.find({});
264
+ assert.equal(vehicles.length, 0);
265
+ });
266
+ it("accepts transponder person locations on transponderUUID alone", async () => {
267
+ const atDate = new Date();
268
+ const department = mockDepartment;
269
+ const items = [{
270
+ vehicleId: "FB:9D:2C:3E:C9:E4",
271
+ radioName: "Fire Hose 1",
272
+ latitude: 34.35264,
273
+ longitude: -119.0669,
274
+ avlTime: "2026-07-10T15:43:36.816Z",
275
+ kindType: "person",
276
+ locationType: "person",
277
+ source: "TXRX",
278
+ transponderUUID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
279
+ userId: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
280
+ session: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
281
+ username: "Fire Hose 1",
282
+ visibility: [],
283
+ }] as unknown as LocationPayload[];
284
+ await location.processItems(department, items, atDate);
285
+ const recs = await models.Location.find({});
286
+ assert.equal(recs.length, 1);
287
+ assert.equal(recs[0]?.transponderUUID, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
288
+ assert.equal(recs[0]?.personnelUUID, "");
289
+ assert.equal(recs[0]?.session, "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
290
+ assert.equal(recs[0]?.active, true);
291
+ assert.equal(recs[0]?.shared, false);
292
+ assert.equal(recs[0]?.sendToCAD, false);
293
+ assert.deepEqual(recs[0]?.visibility, ["visible"]);
294
+ // Person records must not create phantom CAD vehicles
295
+ const vehicles = await models.CADVehicle.find({});
296
+ assert.equal(vehicles.length, 0);
297
+ });
298
+ it("updates person locations in place on rename, preserving person fields", async () => {
299
+ const department = mockDepartment;
300
+ const base = {
301
+ vehicleId: "FB:9D:2C:3E:C9:E4",
302
+ radioName: "William Jones",
303
+ latitude: 34.35264,
304
+ longitude: -119.0669,
305
+ avlTime: "2026-07-10T15:43:36.816Z",
306
+ kindType: "person",
307
+ locationType: "person",
308
+ source: "TXRX",
309
+ transponderUUID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
310
+ userId: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
311
+ session: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
312
+ username: "William Jones",
313
+ visibility: [],
314
+ };
315
+ await location.processItems(department, [base] as unknown as LocationPayload[], new Date());
316
+ const renamed = {
317
+ ...base,
318
+ radioName: "Bill Jones",
319
+ username: "Bill Jones",
320
+ latitude: 34.4,
321
+ };
322
+ await location.processItems(department, [renamed] as unknown as LocationPayload[], new Date());
323
+ const recs = await models.Location.find({});
324
+ assert.equal(recs.length, 1);
325
+ assert.equal(recs[0]?.username, "Bill Jones");
326
+ assert.equal(recs[0]?.session, "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
327
+ assert.equal(recs[0]?.locationGeoJSON.coordinates[1], 34.4);
328
+ // Update path must preserve the person fields
329
+ assert.equal(recs[0]?.kindType, "person");
330
+ assert.equal(recs[0]?.locationType, "person");
331
+ assert.equal(recs[0]?.transponderUUID, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
332
+ assert.deepEqual(recs[0]?.visibility, ["visible"]);
333
+ });
334
+ it("does not wipe transponderUUID when an update omits it", async () => {
335
+ const department = mockDepartment;
336
+ const base = {
337
+ vehicleId: "FB:9D:2C:3E:C9:E4",
338
+ radioName: "Fire Hose 1",
339
+ latitude: 34.35264,
340
+ longitude: -119.0669,
341
+ avlTime: "2026-07-10T15:43:36.816Z",
342
+ kindType: "person",
343
+ locationType: "person",
344
+ source: "TXRX",
345
+ transponderUUID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
346
+ userId: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
347
+ session: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
348
+ username: "Fire Hose 1",
349
+ visibility: [],
350
+ };
351
+ await location.processItems(department, [base] as unknown as LocationPayload[], new Date());
352
+ // Same session, but this update carries only a personnelUUID
353
+ const update = {
354
+ ...base,
355
+ transponderUUID: "",
356
+ personnelUUID: "11111111-2222-3333-4444-555555555555",
357
+ latitude: 34.4,
358
+ };
359
+ await location.processItems(department, [update] as unknown as LocationPayload[], new Date());
360
+ const recs = await models.Location.find({});
361
+ assert.equal(recs.length, 1);
362
+ assert.equal(recs[0]?.transponderUUID, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
363
+ assert.equal(recs[0]?.personnelUUID, "11111111-2222-3333-4444-555555555555");
364
+ assert.equal(recs[0]?.locationGeoJSON.coordinates[1], 34.4);
365
+ });
366
+ it("skips person locations with neither personnelUUID nor transponderUUID", async () => {
367
+ const department = mockDepartment;
368
+ const items = [{
369
+ vehicleId: "12345",
370
+ radioName: "William Jones",
371
+ latitude: 34.35264,
372
+ longitude: -119.0669,
373
+ avlTime: "2026-07-10T15:43:36.816Z",
374
+ kindType: "person",
375
+ locationType: "person",
376
+ source: "TXRX",
377
+ session: "TXRX-12345",
378
+ visibility: [],
379
+ }] as unknown as LocationPayload[];
380
+ await location.processItems(department, items, new Date());
381
+ const recs = await models.Location.find({});
382
+ assert.equal(recs.length, 0);
383
+ const vehicles = await models.CADVehicle.find({});
384
+ assert.equal(vehicles.length, 0);
385
+ });
386
+ it("continues processing the batch after an invalid item", async () => {
387
+ const department = mockDepartment;
388
+ const items = [{
389
+ vehicleId: "",
390
+ radioName: "",
391
+ latitude: 0,
392
+ longitude: 0,
393
+ avlTime: "1602563494",
394
+ }, {
395
+ vehicleId: "BEEDOO1",
396
+ radioName: "BEEDOO1",
397
+ latitude: 34.35264,
398
+ longitude: -119.0669,
399
+ avlTime: "1602563494",
400
+ }] as unknown as LocationPayload[];
401
+ await location.processItems(department, items, new Date());
402
+ const recs = await models.Location.find({});
403
+ assert.equal(recs.length, 1);
404
+ assert.equal(recs[0]?.username, "BEEDOO1");
405
+ });
406
+ it("carries kind/type/locationType/kindColor onto the saved Location", async () => {
407
+ const atDate = new Date();
408
+ const department = mockDepartment;
409
+ const items = [{
410
+ Interface: "Fake Interface",
411
+ vehicleId: "N777TC",
412
+ radioName: "COPTER-1",
413
+ latitude: 34.35264,
414
+ longitude: -119.0669,
415
+ heading: "",
416
+ avlTime: "1602563494",
417
+ kindType: "helicopter",
418
+ locationType: "helicopter",
419
+ kind: "helicopter",
420
+ type: "1",
421
+ kindColor: { background: "#0000FF", text: "#FFFFFF" },
422
+ }] as unknown as LocationPayload[];
423
+ await location.processItems(department, items, atDate);
424
+ const recs = await models.Location.find({});
425
+ assert.equal(recs[0]?.kind, "helicopter");
426
+ assert.equal(recs[0]?.type, "1");
427
+ assert.equal(recs[0]?.locationType, "helicopter");
428
+ assert.equal(recs[0]?.kindColor?.background, "#0000FF");
429
+ assert.equal(recs[0]?.kindColor?.text, "#FFFFFF");
430
+ assert.instanceOf(recs[0]?.kindColorChangedAt, Date);
431
+ });
432
+ it("stamps the department's industry on the saved location", async () => {
433
+ const atDate = new Date();
434
+ const session = "item-with-industry-utility";
435
+ const items = [{
436
+ vehicleId: "BEEDOO1",
437
+ radioName: "BEEDOO1",
438
+ latitude: 34.35264,
439
+ longitude: -119.0669,
440
+ avlTime: "1602563494",
441
+ session,
442
+ }] as unknown as LocationPayload[];
443
+ await location.processItems(mockDepartment, items, atDate);
444
+ const sut = await models.Location.findOne({ session });
445
+ assert.equal(sut?.industry, AccountIndustry.Utility);
446
+ });
447
+ it("falls back to fire industry when the department has none set", async () => {
448
+ const atDate = new Date();
449
+ const session = "item-with-industry-fallback";
450
+ const department = { ...mockDepartment, industry: undefined };
451
+ const items = [{
452
+ vehicleId: "BEEDOO1",
453
+ radioName: "BEEDOO1",
454
+ latitude: 34.35264,
455
+ longitude: -119.0669,
456
+ avlTime: "1602563494",
457
+ session,
458
+ }] as unknown as LocationPayload[];
459
+ await location.processItems(department, items, atDate);
460
+ const sut = await models.Location.findOne({ session });
461
+ assert.equal(sut?.industry, AccountIndustry.Fire);
462
+ });
463
+ it("updates industry in place and stamps propsChangedAt when it changes", async () => {
464
+ const base = {
465
+ vehicleId: "BEEDOO1",
466
+ radioName: "BEEDOO1",
467
+ latitude: 34.35264,
468
+ longitude: -119.0669,
469
+ avlTime: "1602563494",
470
+ } as unknown as LocationPayload;
471
+ await location.processItems(mockDepartment, [base], new Date());
472
+ const initial = await models.Location.find({});
473
+ assert.equal(initial[0]?.industry, AccountIndustry.Utility);
474
+ const initialPropsChangedAt = initial[0]?.propsChangedAt;
475
+ assert.instanceOf(initialPropsChangedAt, Date);
476
+
477
+ const lawDepartment = { ...mockDepartment, industry: AccountIndustry.Law };
478
+ await location.processItems(lawDepartment, [base], new Date());
479
+ const recs = await models.Location.find({});
480
+ assert.equal(recs.length, 1);
481
+ assert.equal(recs[0]?.industry, AccountIndustry.Law);
482
+ assert.isAbove(recs[0]!.propsChangedAt.valueOf(), initialPropsChangedAt!.valueOf());
483
+ });
484
+ });
485
+ });
@@ -0,0 +1,186 @@
1
+ import * as mongoose from "mongoose";
2
+ import {
3
+ BackendModels,
4
+ AccountIndustry,
5
+ Agency,
6
+ Department,
7
+ convertToObjectId as ObjectId
8
+ } from "tabletcommand-backend-models";
9
+
10
+ export default function(modelsModule: typeof import("tabletcommand-backend-models")) {
11
+ const departmentId = "5195426cc4e016a988000969";
12
+ const agencyId = "5f07b93bfb41951409d58493";
13
+ const apiKey = "secretApiKey1990";
14
+
15
+ const d: Partial<Department> = {
16
+ _id: ObjectId(departmentId),
17
+ department: "Test Department",
18
+ accountType: "production",
19
+ industry: AccountIndustry.Utility,
20
+ shareAVL: {
21
+ enabled: true,
22
+ opAreaName: "Test Area",
23
+ opAreaCode: "TEST",
24
+ fadeZoomLevel: 45000,
25
+ area: {
26
+ type: "Polygon",
27
+ coordinates: [],
28
+ },
29
+ },
30
+ signupDomains: [],
31
+ signupKey: "abcd",
32
+ incidentTypes: [],
33
+ rtsChannelPrefix: "rtsChannelPrefix123",
34
+ cadGetLocationEnabled: true,
35
+ rtsEnabled: true,
36
+ pushEnabled: true,
37
+ cadBidirectionalEnabled: false,
38
+ cadOneWayVehiclesEnabled: true,
39
+ cadEmailUsername: "",
40
+ apikey: apiKey,
41
+ active: true,
42
+ modified_unix_date: 1492743603.514,
43
+ state: "CO",
44
+ city: "Denver",
45
+ fdid: "06905",
46
+ uuid: "e4c5873c-1684-47a9-ad2d-81ded6e7ac15",
47
+ modified_date: "2017-04-21T03:00:03.514",
48
+ addressDetails: {
49
+ address: "123 Delta Way",
50
+ city: "Delta Junction",
51
+ state: "CA",
52
+ country: "US",
53
+ zipCode: "99999"
54
+ }
55
+ };
56
+ // cSpell:disable
57
+ const mappedDevice = {
58
+ departmentId,
59
+ remoteFamily: "IPv4",
60
+ uuid: "c01c192f-b71d-4bd6-b5a4-bab6e350a491",
61
+ remoteAddress: "166.140.104.203",
62
+ active: true,
63
+ modified_unix_date: 0,
64
+ deviceId: "BEEDOO3",
65
+ mapId: "BOOPBEEP99",
66
+ deviceType: "geotab",
67
+ userId: "",
68
+ locationToCAD: true,
69
+ mapHidden: false,
70
+ };
71
+ // cSpell:denable
72
+ const agency: Partial<Agency> = {
73
+ _id: ObjectId(agencyId),
74
+ active: true,
75
+ code: "OTHER",
76
+ departmentId: ObjectId(departmentId),
77
+ name: "Other",
78
+ personnelApiKey: "123",
79
+ crossStaffing: [
80
+ {
81
+ alwaysCrossStaff: true,
82
+ crossStaffedUnits: ["Z1"],
83
+ radioName: "D22"
84
+ },
85
+ {
86
+ alwaysCrossStaff: false,
87
+ crossStaffedUnits: ["M1"],
88
+ radioName: "K12"
89
+ },
90
+ {
91
+ alwaysCrossStaff: true,
92
+ crossStaffedUnits: ["A1", "A2"],
93
+ radioName: "Z12"
94
+ },
95
+ {
96
+ alwaysCrossStaff: false,
97
+ crossStaffedUnits: ["A1", "A2", "D22"],
98
+ radioName: "Z99"
99
+ }
100
+ ]
101
+ };
102
+
103
+ async function clearTestData(mods: BackendModels) {
104
+ const query = {};
105
+ await mods.Agency.deleteMany(query);
106
+ await mods.Department.deleteMany(query);
107
+ await mods.Location.deleteMany(query);
108
+ await mods.CADVehicle.deleteMany(query);
109
+ await mods.DeviceMapping.deleteMany(query);
110
+ }
111
+
112
+ async function prepareTestData(mods: BackendModels) {
113
+ const testDepartment = new mods.Department(d);
114
+ await testDepartment.save();
115
+ const testItem = new models.Agency(agency);
116
+ await testItem.save();
117
+ const deviceMapped = new models.DeviceMapping(mappedDevice);
118
+ await deviceMapped.save();
119
+ }
120
+
121
+ let models: BackendModels;
122
+ let connection: typeof mongoose;
123
+
124
+ async function afterEach() {
125
+ if (!shouldRun()) {
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ async function after() {
131
+ await getConnection().connection.close();
132
+ }
133
+
134
+ async function before() {
135
+ if (!shouldRun()) {
136
+ process.exit(1);
137
+ }
138
+
139
+ const {
140
+ models: lModels,
141
+ connection: lConnection
142
+ } = await modelsModule.connect(process.env.NODE_MONGO_URL || "");
143
+ models = lModels;
144
+ connection = lConnection;
145
+ return models;
146
+ }
147
+
148
+ async function beforeEach() {
149
+ if (!shouldRun()) {
150
+ process.exit(1);
151
+ }
152
+
153
+ await clearTestData(models);
154
+ await prepareTestData(models);
155
+ }
156
+
157
+ function shouldRun() {
158
+ const mongoUrl = process.env.NODE_MONGO_URL || "";
159
+ if (!/massive-test/i.exec(mongoUrl)) {
160
+ console.log(`Not a test database: ${mongoUrl}? Expecting database: massive-test.`);
161
+ return false;
162
+ }
163
+ return true;
164
+ }
165
+
166
+ function getModels(): BackendModels {
167
+ return models;
168
+ }
169
+
170
+ function getConnection(): typeof mongoose {
171
+ return connection;
172
+ }
173
+
174
+ return {
175
+ department: d,
176
+ agency,
177
+ clearTestData,
178
+ prepareTestData,
179
+ before,
180
+ beforeEach,
181
+ after,
182
+ afterEach,
183
+ getModels,
184
+ getConnection,
185
+ };
186
+ }