@talkpilot/core-db 1.1.14 → 1.1.16
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/.cursor/rules/development.mdc +65 -65
- package/DEVELOPMENT.md +141 -98
- package/README.md +297 -160
- package/README_OLD.md +160 -0
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +10 -0
- package/dist/connection.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/municipal/index.d.ts +1 -0
- package/dist/municipal/index.d.ts.map +1 -1
- package/dist/municipal/index.js +1 -0
- package/dist/municipal/index.js.map +1 -1
- package/dist/municipal/muniIssues/index.d.ts +7 -0
- package/dist/municipal/muniIssues/index.d.ts.map +1 -0
- package/dist/municipal/muniIssues/index.js +29 -0
- package/dist/municipal/muniIssues/index.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.constants.d.ts +14 -0
- package/dist/municipal/muniIssues/muniIssues.constants.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.constants.js +17 -0
- package/dist/municipal/muniIssues/muniIssues.constants.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.getters.d.ts +7 -0
- package/dist/municipal/muniIssues/muniIssues.getters.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.getters.js +24 -0
- package/dist/municipal/muniIssues/muniIssues.getters.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.schema.d.ts +4 -0
- package/dist/municipal/muniIssues/muniIssues.schema.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.schema.js +63 -0
- package/dist/municipal/muniIssues/muniIssues.schema.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.setters.d.ts +4 -0
- package/dist/municipal/muniIssues/muniIssues.setters.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.setters.js +22 -0
- package/dist/municipal/muniIssues/muniIssues.setters.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.setup.d.ts +3 -0
- package/dist/municipal/muniIssues/muniIssues.setup.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.setup.js +61 -0
- package/dist/municipal/muniIssues/muniIssues.setup.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.types.d.ts +30 -0
- package/dist/municipal/muniIssues/muniIssues.types.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.types.js +3 -0
- package/dist/municipal/muniIssues/muniIssues.types.js.map +1 -0
- package/dist/municipal/tickets/index.d.ts +2 -1
- package/dist/municipal/tickets/index.d.ts.map +1 -1
- package/dist/municipal/tickets/index.js +1 -0
- package/dist/municipal/tickets/index.js.map +1 -1
- package/dist/municipal/tickets/tickets.constants.d.ts +7 -0
- package/dist/municipal/tickets/tickets.constants.d.ts.map +1 -0
- package/dist/municipal/tickets/tickets.constants.js +10 -0
- package/dist/municipal/tickets/tickets.constants.js.map +1 -0
- package/dist/municipal/tickets/tickets.getters.d.ts +0 -11
- package/dist/municipal/tickets/tickets.getters.d.ts.map +1 -1
- package/dist/municipal/tickets/tickets.getters.js +0 -128
- package/dist/municipal/tickets/tickets.getters.js.map +1 -1
- package/dist/municipal/tickets/tickets.statistics.aggregation.d.ts +69 -0
- package/dist/municipal/tickets/tickets.statistics.aggregation.d.ts.map +1 -0
- package/dist/municipal/tickets/tickets.statistics.aggregation.js +99 -0
- package/dist/municipal/tickets/tickets.statistics.aggregation.js.map +1 -0
- package/dist/municipal/tickets/tickets.statistics.getters.d.ts +12 -0
- package/dist/municipal/tickets/tickets.statistics.getters.d.ts.map +1 -0
- package/dist/municipal/tickets/tickets.statistics.getters.js +80 -0
- package/dist/municipal/tickets/tickets.statistics.getters.js.map +1 -0
- package/dist/municipal/tickets/tickets.types.d.ts +15 -5
- package/dist/municipal/tickets/tickets.types.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.constants.d.ts +17 -0
- package/dist/talkpilot/calls/calls.constants.d.ts.map +1 -0
- package/dist/talkpilot/calls/calls.constants.js +20 -0
- package/dist/talkpilot/calls/calls.constants.js.map +1 -0
- package/dist/talkpilot/calls/calls.dashboard.d.ts +3 -0
- package/dist/talkpilot/calls/calls.dashboard.d.ts.map +1 -0
- package/dist/talkpilot/calls/calls.dashboard.js +191 -0
- package/dist/talkpilot/calls/calls.dashboard.js.map +1 -0
- package/dist/talkpilot/calls/calls.getters.d.ts +3 -2
- package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.getters.js +1 -2
- package/dist/talkpilot/calls/calls.getters.js.map +1 -1
- package/dist/talkpilot/calls/calls.statistics.getters.d.ts +28 -0
- package/dist/talkpilot/calls/calls.statistics.getters.d.ts.map +1 -0
- package/dist/talkpilot/calls/calls.statistics.getters.js +453 -0
- package/dist/talkpilot/calls/calls.statistics.getters.js.map +1 -0
- package/dist/talkpilot/calls/calls.statistics.types.d.ts +43 -0
- package/dist/talkpilot/calls/calls.statistics.types.d.ts.map +1 -0
- package/dist/talkpilot/calls/calls.statistics.types.js +3 -0
- package/dist/talkpilot/calls/calls.statistics.types.js.map +1 -0
- package/dist/talkpilot/calls/calls.types.d.ts +8 -7
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.types.js +0 -3
- package/dist/talkpilot/calls/calls.types.js.map +1 -1
- package/dist/talkpilot/calls/dashboard/calls.dashboard.d.ts +4 -0
- package/dist/talkpilot/calls/dashboard/calls.dashboard.d.ts.map +1 -0
- package/dist/talkpilot/calls/dashboard/calls.dashboard.js +223 -0
- package/dist/talkpilot/calls/dashboard/calls.dashboard.js.map +1 -0
- package/dist/talkpilot/calls/dashboard/calls.dashboard.types.d.ts +45 -0
- package/dist/talkpilot/calls/dashboard/calls.dashboard.types.d.ts.map +1 -0
- package/dist/talkpilot/calls/dashboard/calls.dashboard.types.js +3 -0
- package/dist/talkpilot/calls/dashboard/calls.dashboard.types.js.map +1 -0
- package/dist/talkpilot/calls/index.d.ts +4 -0
- package/dist/talkpilot/calls/index.d.ts.map +1 -1
- package/dist/talkpilot/calls/index.js +4 -0
- package/dist/talkpilot/calls/index.js.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts +2 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.js +14 -0
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.js.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts +20 -7
- package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.types.js +6 -0
- package/dist/talkpilot/clientsConfig/clientsConfig.types.js.map +1 -1
- package/dist/talkpilot/contextNotes/contextNotes.getters.d.ts +19 -0
- package/dist/talkpilot/contextNotes/contextNotes.getters.d.ts.map +1 -0
- package/dist/talkpilot/contextNotes/contextNotes.getters.js +55 -0
- package/dist/talkpilot/contextNotes/contextNotes.getters.js.map +1 -0
- package/dist/talkpilot/contextNotes/contextNotes.types.d.ts +19 -0
- package/dist/talkpilot/contextNotes/contextNotes.types.d.ts.map +1 -0
- package/dist/talkpilot/contextNotes/contextNotes.types.js +3 -0
- package/dist/talkpilot/contextNotes/contextNotes.types.js.map +1 -0
- package/dist/talkpilot/contextNotes/index.d.ts +3 -0
- package/dist/talkpilot/contextNotes/index.d.ts.map +1 -0
- package/dist/talkpilot/contextNotes/index.js +19 -0
- package/dist/talkpilot/contextNotes/index.js.map +1 -0
- package/dist/talkpilot/flows/flows.schema.d.ts +3 -0
- package/dist/talkpilot/flows/flows.schema.d.ts.map +1 -1
- package/dist/talkpilot/flows/flows.schema.js +2 -1
- package/dist/talkpilot/flows/flows.schema.js.map +1 -1
- package/dist/talkpilot/flows/flows.types.d.ts +2 -0
- package/dist/talkpilot/flows/flows.types.d.ts.map +1 -1
- package/dist/talkpilot/index.d.ts +3 -0
- package/dist/talkpilot/index.d.ts.map +1 -1
- package/dist/talkpilot/index.js +3 -0
- package/dist/talkpilot/index.js.map +1 -1
- package/dist/talkpilot/phone_numbers/index.d.ts +2 -2
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +5 -3
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.schema.js +12 -12
- package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts +4 -4
- package/dist/talkpilot/products/index.d.ts +3 -0
- package/dist/talkpilot/products/index.d.ts.map +1 -0
- package/dist/talkpilot/products/index.js +19 -0
- package/dist/talkpilot/products/index.js.map +1 -0
- package/dist/talkpilot/products/products.getters.d.ts +6 -0
- package/dist/talkpilot/products/products.getters.d.ts.map +1 -0
- package/dist/talkpilot/products/products.getters.js +14 -0
- package/dist/talkpilot/products/products.getters.js.map +1 -0
- package/dist/talkpilot/products/products.types.d.ts +9 -0
- package/dist/talkpilot/products/products.types.d.ts.map +1 -0
- package/dist/talkpilot/products/products.types.js +3 -0
- package/dist/talkpilot/products/products.types.js.map +1 -0
- package/dist/talkpilot/results/index.d.ts +1 -1
- package/dist/talkpilot/results/index.d.ts.map +1 -1
- package/dist/talkpilot/results/index.js +3 -1
- package/dist/talkpilot/results/index.js.map +1 -1
- package/dist/talkpilot/results/results.getter.d.ts +3 -1
- package/dist/talkpilot/results/results.getter.d.ts.map +1 -1
- package/dist/talkpilot/results/results.getter.js +11 -2
- package/dist/talkpilot/results/results.getter.js.map +1 -1
- package/dist/talkpilot/retry_analyze/index.d.ts +3 -0
- package/dist/talkpilot/retry_analyze/index.d.ts.map +1 -0
- package/dist/talkpilot/retry_analyze/index.js +19 -0
- package/dist/talkpilot/retry_analyze/index.js.map +1 -0
- package/dist/talkpilot/retry_analyze/retryAnalyze.getters.d.ts +10 -0
- package/dist/talkpilot/retry_analyze/retryAnalyze.getters.d.ts.map +1 -0
- package/dist/talkpilot/retry_analyze/retryAnalyze.getters.js +60 -0
- package/dist/talkpilot/retry_analyze/retryAnalyze.getters.js.map +1 -0
- package/dist/talkpilot/retry_analyze/retryAnalyze.types.d.ts +12 -0
- package/dist/talkpilot/retry_analyze/retryAnalyze.types.d.ts.map +1 -0
- package/dist/talkpilot/retry_analyze/retryAnalyze.types.js +3 -0
- package/dist/talkpilot/retry_analyze/retryAnalyze.types.js.map +1 -0
- package/dist/test-utils/db-utils.d.ts.map +1 -1
- package/dist/test-utils/db-utils.js +2 -0
- package/dist/test-utils/db-utils.js.map +1 -1
- package/dist/test-utils/factories/index.d.ts +3 -0
- package/dist/test-utils/factories/index.d.ts.map +1 -1
- package/dist/test-utils/factories/index.js +3 -0
- package/dist/test-utils/factories/index.js.map +1 -1
- package/dist/test-utils/factories/municipal/muniIssues.d.ts +10 -0
- package/dist/test-utils/factories/municipal/muniIssues.d.ts.map +1 -0
- package/dist/test-utils/factories/municipal/muniIssues.js +30 -0
- package/dist/test-utils/factories/municipal/muniIssues.js.map +1 -0
- package/dist/test-utils/factories/talkpilot/contextNotes.d.ts +16 -0
- package/dist/test-utils/factories/talkpilot/contextNotes.d.ts.map +1 -0
- package/dist/test-utils/factories/talkpilot/contextNotes.js +33 -0
- package/dist/test-utils/factories/talkpilot/contextNotes.js.map +1 -0
- package/dist/test-utils/factories/websitalk/scans.d.ts +5 -0
- package/dist/test-utils/factories/websitalk/scans.d.ts.map +1 -0
- package/dist/test-utils/factories/websitalk/scans.js +25 -0
- package/dist/test-utils/factories/websitalk/scans.js.map +1 -0
- package/dist/utils/date.utils.d.ts +49 -0
- package/dist/utils/date.utils.d.ts.map +1 -0
- package/dist/utils/date.utils.js +108 -0
- package/dist/utils/date.utils.js.map +1 -0
- package/dist/utils/shared.types.d.ts +5 -0
- package/dist/utils/shared.types.d.ts.map +1 -0
- package/dist/utils/shared.types.js +3 -0
- package/dist/utils/shared.types.js.map +1 -0
- package/dist/websitalk/index.d.ts +7 -0
- package/dist/websitalk/index.d.ts.map +1 -0
- package/dist/websitalk/index.js +34 -0
- package/dist/websitalk/index.js.map +1 -0
- package/dist/websitalk/mongodb-client.d.ts +13 -0
- package/dist/websitalk/mongodb-client.d.ts.map +1 -0
- package/dist/websitalk/mongodb-client.js +56 -0
- package/dist/websitalk/mongodb-client.js.map +1 -0
- package/dist/websitalk/scans/index.d.ts +3 -0
- package/dist/websitalk/scans/index.d.ts.map +1 -0
- package/dist/websitalk/scans/index.js +19 -0
- package/dist/websitalk/scans/index.js.map +1 -0
- package/dist/websitalk/scans/scans.getters.d.ts +12 -0
- package/dist/websitalk/scans/scans.getters.d.ts.map +1 -0
- package/dist/websitalk/scans/scans.getters.js +74 -0
- package/dist/websitalk/scans/scans.getters.js.map +1 -0
- package/dist/websitalk/scans/scans.types.d.ts +45 -0
- package/dist/websitalk/scans/scans.types.d.ts.map +1 -0
- package/dist/websitalk/scans/scans.types.js +3 -0
- package/dist/websitalk/scans/scans.types.js.map +1 -0
- package/jest.config.js +20 -19
- package/package.json +46 -45
- package/src/connection.ts +12 -0
- package/src/index.ts +10 -0
- package/src/municipal/index.ts +1 -0
- package/src/municipal/muniIssues/__tests__/muniIssues.setters.spec.ts +94 -0
- package/src/municipal/muniIssues/__tests__/muniIssues.setup.spec.ts +92 -0
- package/src/municipal/muniIssues/index.ts +17 -0
- package/src/municipal/muniIssues/muniIssues.constants.ts +18 -0
- package/src/municipal/muniIssues/muniIssues.getters.ts +24 -0
- package/src/municipal/muniIssues/muniIssues.schema.ts +66 -0
- package/src/municipal/muniIssues/muniIssues.setters.ts +24 -0
- package/src/municipal/muniIssues/muniIssues.setup.ts +65 -0
- package/src/municipal/muniIssues/muniIssues.types.ts +37 -0
- package/src/municipal/tickets/__tests__/tickets.getters.spec.ts +1 -37
- package/src/municipal/tickets/__tests__/tickets.statistics.spec.ts +99 -0
- package/src/municipal/tickets/index.ts +9 -2
- package/src/municipal/tickets/tickets.constants.ts +8 -0
- package/src/municipal/tickets/tickets.getters.ts +0 -140
- package/src/municipal/tickets/tickets.statistics.aggregation.ts +110 -0
- package/src/municipal/tickets/tickets.statistics.getters.ts +132 -0
- package/src/municipal/tickets/tickets.types.ts +54 -43
- package/src/talkpilot/calls/__tests__/calls.dashboard.spec.ts +149 -0
- package/src/talkpilot/calls/__tests__/calls.spec.ts +48 -30
- package/src/talkpilot/calls/__tests__/calls.statistics.spec.ts +481 -0
- package/src/talkpilot/calls/calls.constants.ts +20 -0
- package/src/talkpilot/calls/calls.getters.ts +6 -6
- package/src/talkpilot/calls/calls.statistics.getters.ts +668 -0
- package/src/talkpilot/calls/calls.statistics.types.ts +48 -0
- package/src/talkpilot/calls/calls.types.ts +18 -14
- package/src/talkpilot/calls/dashboard/calls.dashboard.ts +292 -0
- package/src/talkpilot/calls/dashboard/calls.dashboard.types.ts +57 -0
- package/src/talkpilot/calls/index.ts +4 -0
- package/src/talkpilot/clientsConfig/__tests__/clientsConfig.getters.spec.ts +53 -0
- package/src/talkpilot/clientsConfig/__tests__/clientsConfig.spec.ts +91 -7
- package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +34 -1
- package/src/talkpilot/clientsConfig/clientsConfig.types.ts +127 -92
- package/src/talkpilot/contextNotes/__tests__/contextNotes.getters.spec.ts +217 -0
- package/src/talkpilot/contextNotes/contextNotes.getters.ts +85 -0
- package/src/talkpilot/contextNotes/contextNotes.types.ts +20 -0
- package/src/talkpilot/contextNotes/index.ts +2 -0
- package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +31 -2
- package/src/talkpilot/flows/flows.schema.ts +2 -1
- package/src/talkpilot/flows/flows.types.ts +3 -0
- package/src/talkpilot/index.ts +3 -0
- package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +40 -35
- package/src/talkpilot/phone_numbers/index.ts +2 -2
- package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +10 -6
- package/src/talkpilot/phone_numbers/phone_numbers.schema.ts +12 -12
- package/src/talkpilot/phone_numbers/phone_numbers.types.ts +4 -4
- package/src/talkpilot/products/__tests__/products.getters.spec.ts +44 -0
- package/src/talkpilot/products/index.ts +2 -0
- package/src/talkpilot/products/products.getters.ts +12 -0
- package/src/talkpilot/products/products.types.ts +9 -0
- package/src/talkpilot/results/index.ts +2 -0
- package/src/talkpilot/results/results.getter.ts +14 -2
- package/src/talkpilot/retry_analyze/__tests__/retryAnalyze.getters.spec.ts +156 -0
- package/src/talkpilot/retry_analyze/index.ts +2 -0
- package/src/talkpilot/retry_analyze/retryAnalyze.getters.ts +84 -0
- package/src/talkpilot/retry_analyze/retryAnalyze.types.ts +13 -0
- package/src/test-utils/db-utils.ts +3 -1
- package/src/test-utils/factories/index.ts +3 -0
- package/src/test-utils/factories/municipal/muniIssues.ts +32 -0
- package/src/test-utils/factories/talkpilot/contextNotes.ts +40 -0
- package/src/test-utils/factories/websitalk/scans.ts +23 -0
- package/src/utils/date.utils.ts +126 -0
- package/src/utils/shared.types.ts +4 -0
- package/src/websitalk/index.ts +15 -0
- package/src/websitalk/mongodb-client.ts +61 -0
- package/src/websitalk/scans/__tests__/scans.spec.ts +218 -0
- package/src/websitalk/scans/index.ts +2 -0
- package/src/websitalk/scans/scans.getters.ts +113 -0
- package/src/websitalk/scans/scans.types.ts +53 -0
- package/tsconfig.json +23 -23
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ObjectId } from "mongodb";
|
|
2
|
+
import { CreateMuniIssueInput, MuniIssue } from "./muniIssues.types";
|
|
3
|
+
import { getMuniIssuesCollection } from "./muniIssues.getters";
|
|
4
|
+
import { ensureMuniIssuesCollectionOnce } from "./muniIssues.setup";
|
|
5
|
+
|
|
6
|
+
// Required `id`: MIS pre-generates it (generateMuniIssueId) and embeds it in the Jira ticket — the two-way link.
|
|
7
|
+
export const createMuniIssue = async (
|
|
8
|
+
issueData: CreateMuniIssueInput,
|
|
9
|
+
id: ObjectId,
|
|
10
|
+
): Promise<ObjectId> => {
|
|
11
|
+
await ensureMuniIssuesCollectionOnce();
|
|
12
|
+
|
|
13
|
+
const issue: MuniIssue = {
|
|
14
|
+
_id: id,
|
|
15
|
+
...issueData,
|
|
16
|
+
jira: {
|
|
17
|
+
...issueData.jira,
|
|
18
|
+
syncStatus: issueData.jira.syncStatus ?? "created",
|
|
19
|
+
},
|
|
20
|
+
createdAt: new Date(),
|
|
21
|
+
};
|
|
22
|
+
const { insertedId } = await getMuniIssuesCollection().insertOne(issue);
|
|
23
|
+
return insertedId;
|
|
24
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Db, Document, MongoServerError } from "mongodb";
|
|
2
|
+
import { getDb } from "../index";
|
|
3
|
+
import {
|
|
4
|
+
MUNI_ISSUES_COLLECTION,
|
|
5
|
+
NAMESPACE_EXISTS,
|
|
6
|
+
VALIDATION_LEVEL,
|
|
7
|
+
VALIDATION_ACTION,
|
|
8
|
+
} from "./muniIssues.constants";
|
|
9
|
+
import { muniIssuesIndexes, muniIssuesJsonSchema } from "./muniIssues.schema";
|
|
10
|
+
|
|
11
|
+
const applyValidator = (db: Db, validator: Document): Promise<unknown> =>
|
|
12
|
+
db.command({
|
|
13
|
+
collMod: MUNI_ISSUES_COLLECTION,
|
|
14
|
+
validator,
|
|
15
|
+
validationLevel: VALIDATION_LEVEL,
|
|
16
|
+
validationAction: VALIDATION_ACTION,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Provisions validator and indexes: createCollection first, collMod if already exists.
|
|
20
|
+
export const ensureMuniIssuesCollection = async (): Promise<void> => {
|
|
21
|
+
const db = getDb();
|
|
22
|
+
const validator = { $jsonSchema: muniIssuesJsonSchema };
|
|
23
|
+
|
|
24
|
+
const existing = await db
|
|
25
|
+
.listCollections({ name: MUNI_ISSUES_COLLECTION })
|
|
26
|
+
.toArray();
|
|
27
|
+
|
|
28
|
+
if (existing.length === 0) {
|
|
29
|
+
try {
|
|
30
|
+
await db.createCollection(MUNI_ISSUES_COLLECTION, {
|
|
31
|
+
validator,
|
|
32
|
+
validationLevel: VALIDATION_LEVEL,
|
|
33
|
+
validationAction: VALIDATION_ACTION,
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// NamespaceExists race: collection exists — apply validator via collMod.
|
|
37
|
+
if (err instanceof MongoServerError && err.code === NAMESPACE_EXISTS) {
|
|
38
|
+
await applyValidator(db, validator);
|
|
39
|
+
} else {
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
await applyValidator(db, validator);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await db.collection(MUNI_ISSUES_COLLECTION).createIndexes(muniIssuesIndexes);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Per-Db cache so each test's in-memory Db re-provisions.
|
|
51
|
+
const provisioningByDb = new WeakMap<Db, Promise<void>>();
|
|
52
|
+
|
|
53
|
+
// Once per Db; evicts failed run so the next call retries.
|
|
54
|
+
export const ensureMuniIssuesCollectionOnce = (): Promise<void> => {
|
|
55
|
+
const db = getDb();
|
|
56
|
+
let provisioning = provisioningByDb.get(db);
|
|
57
|
+
if (!provisioning) {
|
|
58
|
+
provisioning = ensureMuniIssuesCollection().catch((err) => {
|
|
59
|
+
provisioningByDb.delete(db);
|
|
60
|
+
throw err;
|
|
61
|
+
});
|
|
62
|
+
provisioningByDb.set(db, provisioning);
|
|
63
|
+
}
|
|
64
|
+
return provisioning;
|
|
65
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ObjectId } from "mongodb";
|
|
2
|
+
|
|
3
|
+
import { CityName } from "../utils/types";
|
|
4
|
+
import { MUNI_ISSUE_SYNC_STATUSES } from "./muniIssues.constants";
|
|
5
|
+
|
|
6
|
+
export type MuniIssueSyncStatus = (typeof MUNI_ISSUE_SYNC_STATUSES)[number];
|
|
7
|
+
|
|
8
|
+
export type MuniIssueContent = {
|
|
9
|
+
issuerName: string;
|
|
10
|
+
issuerRole: string;
|
|
11
|
+
issueDescription: string;
|
|
12
|
+
desiredBehavior: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type MuniIssueJira = {
|
|
16
|
+
key: string;
|
|
17
|
+
syncStatus: MuniIssueSyncStatus;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MuniIssue = {
|
|
21
|
+
_id: ObjectId;
|
|
22
|
+
callSid: string;
|
|
23
|
+
clientId: string;
|
|
24
|
+
cityName: CityName;
|
|
25
|
+
issueContent: MuniIssueContent;
|
|
26
|
+
jira: MuniIssueJira;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// _id/createdAt are set on insert; jira.syncStatus defaults to "created"
|
|
31
|
+
// because the issue is only stored after its Jira ticket exists (CP-1115).
|
|
32
|
+
export type CreateMuniIssueInput = Omit<
|
|
33
|
+
MuniIssue,
|
|
34
|
+
"_id" | "createdAt" | "jira"
|
|
35
|
+
> & {
|
|
36
|
+
jira: { key: string; syncStatus?: MuniIssueSyncStatus };
|
|
37
|
+
};
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
findTicketsByCallSid,
|
|
3
|
-
getTicketsCountByCityAndDateRange,
|
|
4
|
-
} from "../tickets.getters";
|
|
1
|
+
import { findTicketsByCallSid } from "../tickets.getters";
|
|
5
2
|
import { setDb } from "../../index";
|
|
6
3
|
import { Db, Collection } from "mongodb";
|
|
7
4
|
import { createTicket } from "../../../test-utils/factories";
|
|
@@ -30,37 +27,4 @@ describe("tickets getters", () => {
|
|
|
30
27
|
expect(mockCollection.find).toHaveBeenCalledWith({ callSid: "123" });
|
|
31
28
|
expect(result[0].callSid).toBe("123");
|
|
32
29
|
});
|
|
33
|
-
|
|
34
|
-
it("getTicketsCountByCityAndDateRange should not count tickets without callSid", async () => {
|
|
35
|
-
const aggregateNext = jest.fn().mockResolvedValue({ n: 1 });
|
|
36
|
-
mockCollection = {
|
|
37
|
-
...mockCollection,
|
|
38
|
-
aggregate: jest.fn().mockReturnValue({
|
|
39
|
-
next: aggregateNext,
|
|
40
|
-
}),
|
|
41
|
-
};
|
|
42
|
-
mockDb = {
|
|
43
|
-
collection: jest.fn().mockReturnValue(mockCollection),
|
|
44
|
-
};
|
|
45
|
-
setDb(mockDb as Db);
|
|
46
|
-
|
|
47
|
-
const result = await getTicketsCountByCityAndDateRange(
|
|
48
|
-
"ashdod",
|
|
49
|
-
"2026-01-01",
|
|
50
|
-
"2026-01-31",
|
|
51
|
-
"Asia/Jerusalem",
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
expect(mockCollection.aggregate).toHaveBeenCalledWith(
|
|
55
|
-
expect.arrayContaining([
|
|
56
|
-
{
|
|
57
|
-
$match: {
|
|
58
|
-
cityName: "ashdod",
|
|
59
|
-
callSid: { $exists: true, $nin: [null, ""] },
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
]),
|
|
63
|
-
);
|
|
64
|
-
expect(result).toBe(1);
|
|
65
|
-
});
|
|
66
30
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findCallSidDraftTicketCountsByCity,
|
|
3
|
+
findCallSidTicketCountsByCity,
|
|
4
|
+
getTicketsCollection,
|
|
5
|
+
ticketStatsDateScopeFromYmd,
|
|
6
|
+
} from "../../../index";
|
|
7
|
+
import { createTicket } from "../../../test-utils/factories";
|
|
8
|
+
|
|
9
|
+
const CITY = "tests" as const;
|
|
10
|
+
const TZ = "Asia/Jerusalem";
|
|
11
|
+
const mayScope = () =>
|
|
12
|
+
ticketStatsDateScopeFromYmd("2026-05-01", "2026-05-31", TZ);
|
|
13
|
+
|
|
14
|
+
describe("tickets statistics getters", () => {
|
|
15
|
+
it("should scope callSid lookups to the provided createdAt range", async () => {
|
|
16
|
+
await getTicketsCollection().insertMany([
|
|
17
|
+
createTicket({
|
|
18
|
+
cityName: CITY,
|
|
19
|
+
callSid: "CA-in-range",
|
|
20
|
+
createdAt: new Date("2026-05-15T10:00:00.000Z"),
|
|
21
|
+
updatedAt: new Date("2026-05-15T10:00:00.000Z"),
|
|
22
|
+
}),
|
|
23
|
+
createTicket({
|
|
24
|
+
cityName: CITY,
|
|
25
|
+
callSid: "CA-out-of-range",
|
|
26
|
+
createdAt: new Date("2024-01-01T10:00:00.000Z"),
|
|
27
|
+
updatedAt: new Date("2024-01-01T10:00:00.000Z"),
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
30
|
+
expect(await findCallSidTicketCountsByCity(CITY, mayScope())).toEqual(
|
|
31
|
+
new Map([["CA-in-range", 1]]),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should treat whitespace-only event_subject_id as draft for legacy tickets", async () => {
|
|
36
|
+
await getTicketsCollection().insertMany([
|
|
37
|
+
createTicket({
|
|
38
|
+
cityName: CITY,
|
|
39
|
+
callSid: "CA-draft-whitespace",
|
|
40
|
+
createdAt: new Date("2026-05-15T10:00:00.000Z"),
|
|
41
|
+
updatedAt: new Date("2026-05-15T10:00:00.000Z"),
|
|
42
|
+
externalCallFields: { event_subject_id: " \t " },
|
|
43
|
+
}),
|
|
44
|
+
createTicket({
|
|
45
|
+
cityName: CITY,
|
|
46
|
+
callSid: "CA-not-draft",
|
|
47
|
+
createdAt: new Date("2026-05-16T10:00:00.000Z"),
|
|
48
|
+
updatedAt: new Date("2026-05-16T10:00:00.000Z"),
|
|
49
|
+
externalCallFields: { event_subject_id: "100" },
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
expect(await findCallSidDraftTicketCountsByCity(CITY, mayScope())).toEqual(
|
|
53
|
+
new Map([["CA-draft-whitespace", 1]]),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should use isDraft when set, overriding legacy heuristics", async () => {
|
|
58
|
+
await getTicketsCollection().insertMany([
|
|
59
|
+
createTicket({
|
|
60
|
+
cityName: CITY,
|
|
61
|
+
callSid: "CA-explicit-draft",
|
|
62
|
+
isDraft: true,
|
|
63
|
+
createdAt: new Date("2026-05-15T10:00:00.000Z"),
|
|
64
|
+
updatedAt: new Date("2026-05-15T10:00:00.000Z"),
|
|
65
|
+
externalCallFields: { event_subject_id: "100" },
|
|
66
|
+
}),
|
|
67
|
+
createTicket({
|
|
68
|
+
cityName: CITY,
|
|
69
|
+
callSid: "CA-explicit-not-draft",
|
|
70
|
+
isDraft: false,
|
|
71
|
+
createdAt: new Date("2026-05-16T10:00:00.000Z"),
|
|
72
|
+
updatedAt: new Date("2026-05-16T10:00:00.000Z"),
|
|
73
|
+
externalCallFields: {},
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
expect(await findCallSidDraftTicketCountsByCity(CITY, mayScope())).toEqual(
|
|
77
|
+
new Map([["CA-explicit-draft", 1]]),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
it("should count multiple tickets on the same callSid", async () => {
|
|
81
|
+
await getTicketsCollection().insertMany([
|
|
82
|
+
createTicket({
|
|
83
|
+
cityName: CITY,
|
|
84
|
+
callSid: "CA-multi",
|
|
85
|
+
createdAt: new Date("2026-05-15T10:00:00.000Z"),
|
|
86
|
+
updatedAt: new Date("2026-05-15T10:00:00.000Z"),
|
|
87
|
+
}),
|
|
88
|
+
createTicket({
|
|
89
|
+
cityName: CITY,
|
|
90
|
+
callSid: "CA-multi",
|
|
91
|
+
createdAt: new Date("2026-05-15T11:00:00.000Z"),
|
|
92
|
+
updatedAt: new Date("2026-05-15T11:00:00.000Z"),
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
95
|
+
expect(await findCallSidTicketCountsByCity(CITY, mayScope())).toEqual(
|
|
96
|
+
new Map([["CA-multi", 2]]),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
export * from "./tickets.getters";
|
|
2
|
-
export
|
|
1
|
+
export * from "./tickets.getters";
|
|
2
|
+
export * from "./tickets.statistics.getters";
|
|
3
|
+
export type {
|
|
4
|
+
Ticket,
|
|
5
|
+
TicketDoc,
|
|
6
|
+
SubjectItem,
|
|
7
|
+
TicketStatsDateScope,
|
|
8
|
+
TicketSubjectRow,
|
|
9
|
+
} from "./tickets.types";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Label used when a ticket has no resolvable department/subject. */
|
|
2
|
+
export const UNCLASSIFIED = "ללא מחלקה";
|
|
3
|
+
|
|
4
|
+
/** Max execution time (ms) for ticket statistics aggregations. */
|
|
5
|
+
export const STATS_MAX_TIME_MS = 30_000;
|
|
6
|
+
|
|
7
|
+
/** Default look-back window (days) for ticket statistics date ranges. */
|
|
8
|
+
export const DEFAULT_LOOKBACK_DAYS = 30;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { CityName, getDb, ObjectId, Ticket } from "../index";
|
|
2
|
-
import type { SubjectStatsItem } from "./tickets.types";
|
|
3
2
|
import { Collection, Filter, ObjectId as MongoObjectId } from "mongodb";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -68,145 +67,6 @@ export const deleteTicket = async (ticketId: string): Promise<boolean> => {
|
|
|
68
67
|
return result.deletedCount > 0;
|
|
69
68
|
};
|
|
70
69
|
|
|
71
|
-
/**
|
|
72
|
-
* Count tickets by city and date range (createdAt converted to given timezone).
|
|
73
|
-
* Used as "open" tickets count when status is not available.
|
|
74
|
-
*/
|
|
75
|
-
export async function getTicketsCountByCityAndDateRange(
|
|
76
|
-
cityName: string,
|
|
77
|
-
startStr: string,
|
|
78
|
-
endStr: string,
|
|
79
|
-
timezone: string,
|
|
80
|
-
): Promise<number> {
|
|
81
|
-
const doc = await getTicketsCollection()
|
|
82
|
-
.aggregate<{ n: number }>([
|
|
83
|
-
{ $match: { cityName, callSid: { $exists: true, $nin: [null, ""] } } },
|
|
84
|
-
{
|
|
85
|
-
$addFields: {
|
|
86
|
-
dateLocal: {
|
|
87
|
-
$dateToString: { format: "%Y-%m-%d", date: "$createdAt", timezone },
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{ $match: { dateLocal: { $gte: startStr, $lte: endStr } } },
|
|
92
|
-
{ $count: "n" },
|
|
93
|
-
])
|
|
94
|
-
.next();
|
|
95
|
-
return doc?.n ?? 0;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Count of tickets by department (subject) from departmentsSubjects. Different departments (subject_id) are grouped, and sub-subjects are unified under their department.
|
|
100
|
-
* Date filter: createdAt in [startStr, endStr] (in the given timezone). Fallback to "Unclassified" when no match is found in the lookup.
|
|
101
|
-
*/
|
|
102
|
-
export async function getTicketsSubjectStats(
|
|
103
|
-
cityName: string,
|
|
104
|
-
startStr: string,
|
|
105
|
-
endStr: string,
|
|
106
|
-
timezone: string,
|
|
107
|
-
): Promise<SubjectStatsItem[]> {
|
|
108
|
-
const coll = getTicketsCollection();
|
|
109
|
-
const rows = await coll
|
|
110
|
-
.aggregate<{ _id: string; subject: string; count: number }>([
|
|
111
|
-
{ $match: { cityName } },
|
|
112
|
-
{
|
|
113
|
-
$addFields: {
|
|
114
|
-
dateLocal: {
|
|
115
|
-
$dateToString: { format: "%Y-%m-%d", date: "$createdAt", timezone },
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
{ $match: { dateLocal: { $gte: startStr, $lte: endStr } } },
|
|
120
|
-
{
|
|
121
|
-
$addFields: {
|
|
122
|
-
effectiveSubjectId: {
|
|
123
|
-
$ifNull: [
|
|
124
|
-
"$externalCallFields.event_subject_id",
|
|
125
|
-
{
|
|
126
|
-
$ifNull: [
|
|
127
|
-
"$externalCallFields.event_sub_subject_id",
|
|
128
|
-
"$externalCallFields.event_sub_subject_id2",
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
$lookup: {
|
|
137
|
-
from: "departmentsSubjects",
|
|
138
|
-
let: { sid: "$effectiveSubjectId", c: cityName },
|
|
139
|
-
pipeline: [
|
|
140
|
-
{
|
|
141
|
-
$match: {
|
|
142
|
-
$expr: {
|
|
143
|
-
$and: [
|
|
144
|
-
{ $eq: ["$cityName", "$$c"] },
|
|
145
|
-
{
|
|
146
|
-
$or: [
|
|
147
|
-
{ $eq: ["$subject_id", { $ifNull: ["$$sid", ""] }] },
|
|
148
|
-
{
|
|
149
|
-
$eq: ["$sub_subject_id", { $ifNull: ["$$sid", ""] }],
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
$eq: ["$sub_subject_id2", { $ifNull: ["$$sid", ""] }],
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
{ $limit: 1 },
|
|
161
|
-
],
|
|
162
|
-
as: "subj",
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
$addFields: {
|
|
167
|
-
subject_id: {
|
|
168
|
-
$cond: {
|
|
169
|
-
if: { $gt: [{ $size: "$subj" }, 0] },
|
|
170
|
-
then: {
|
|
171
|
-
$ifNull: [{ $arrayElemAt: ["$subj.subject_id", 0] }, ""],
|
|
172
|
-
},
|
|
173
|
-
else: "",
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
subject: {
|
|
177
|
-
$cond: {
|
|
178
|
-
if: { $gt: [{ $size: "$subj" }, 0] },
|
|
179
|
-
then: {
|
|
180
|
-
$ifNull: [
|
|
181
|
-
{ $arrayElemAt: ["$subj.subjectName", 0] },
|
|
182
|
-
"Unclassified",
|
|
183
|
-
],
|
|
184
|
-
},
|
|
185
|
-
else: "Unclassified",
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
$group: {
|
|
192
|
-
_id: "$subject_id",
|
|
193
|
-
subject: { $first: "$subject" },
|
|
194
|
-
count: { $sum: 1 },
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
{ $sort: { count: -1 } },
|
|
198
|
-
])
|
|
199
|
-
.toArray();
|
|
200
|
-
|
|
201
|
-
const total = rows.reduce((s: number, r: any) => s + r.count, 0);
|
|
202
|
-
return rows.map((r: any) => ({
|
|
203
|
-
subject_name: r.subject,
|
|
204
|
-
subject_id: r._id,
|
|
205
|
-
count: r.count,
|
|
206
|
-
percentage: total > 0 ? Math.round((r.count / total) * 100) : 0,
|
|
207
|
-
}));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
70
|
export const findTicketByQuery = async (query: Partial<Ticket>) => {
|
|
211
71
|
return await getTicketsCollection().findOne(query);
|
|
212
72
|
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { CityName } from "../utils/types";
|
|
2
|
+
import { STATS_MAX_TIME_MS, UNCLASSIFIED } from "./tickets.constants";
|
|
3
|
+
import { getTicketsCollection } from "./tickets.getters";
|
|
4
|
+
import type { TicketStatsDateRange } from "./tickets.types";
|
|
5
|
+
|
|
6
|
+
const effectiveSubjectIdExpr = {
|
|
7
|
+
$ifNull: [
|
|
8
|
+
"$externalCallFields.event_subject_id",
|
|
9
|
+
{
|
|
10
|
+
$ifNull: [
|
|
11
|
+
"$externalCallFields.event_sub_subject_id",
|
|
12
|
+
"$externalCallFields.event_sub_subject_id2",
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const departmentsSubjectsLookup = (cityName: CityName) => ({
|
|
19
|
+
$lookup: {
|
|
20
|
+
from: "departmentsSubjects",
|
|
21
|
+
let: { sid: "$effectiveSubjectId", c: cityName },
|
|
22
|
+
pipeline: [
|
|
23
|
+
{
|
|
24
|
+
$match: {
|
|
25
|
+
$expr: {
|
|
26
|
+
$and: [
|
|
27
|
+
{ $eq: ["$cityName", "$$c"] },
|
|
28
|
+
{
|
|
29
|
+
$or: [
|
|
30
|
+
{ $eq: ["$subject_id", { $ifNull: ["$$sid", ""] }] },
|
|
31
|
+
{ $eq: ["$sub_subject_id", { $ifNull: ["$$sid", ""] }] },
|
|
32
|
+
{ $eq: ["$sub_subject_id2", { $ifNull: ["$$sid", ""] }] },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{ $limit: 1 },
|
|
40
|
+
],
|
|
41
|
+
as: "subj",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const subjectLabelExpr = {
|
|
46
|
+
$cond: {
|
|
47
|
+
if: { $gt: [{ $size: "$subj" }, 0] },
|
|
48
|
+
then: {
|
|
49
|
+
$ifNull: [{ $arrayElemAt: ["$subj.subjectName", 0] }, UNCLASSIFIED],
|
|
50
|
+
},
|
|
51
|
+
else: UNCLASSIFIED,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const ticketDateMatch = (dateRange: TicketStatsDateRange) => ({
|
|
56
|
+
createdAt: { $gte: dateRange.from, $lte: dateRange.to },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Legacy draft detection for tickets created before isDraft existed. */
|
|
60
|
+
export const isDraftSubjectIdExpr = {
|
|
61
|
+
$or: [
|
|
62
|
+
{ $eq: [{ $type: "$externalCallFields.event_subject_id" }, "missing"] },
|
|
63
|
+
{ $eq: ["$externalCallFields.event_subject_id", null] },
|
|
64
|
+
{
|
|
65
|
+
$regexMatch: {
|
|
66
|
+
input: { $ifNull: ["$externalCallFields.event_subject_id", ""] },
|
|
67
|
+
regex: /^\s*$/,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Draft when isDraft is true, not draft when isDraft is false,
|
|
75
|
+
* otherwise fall back to legacy subject-id heuristic for older tickets.
|
|
76
|
+
*/
|
|
77
|
+
export const isDraftTicketExpr = {
|
|
78
|
+
$or: [
|
|
79
|
+
{ $eq: ["$isDraft", true] },
|
|
80
|
+
{
|
|
81
|
+
$and: [{ $ne: ["$isDraft", false] }, isDraftSubjectIdExpr],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const ticketsWithCallSidMatch = (
|
|
87
|
+
cityName: CityName,
|
|
88
|
+
dateRange: TicketStatsDateRange,
|
|
89
|
+
) => ({
|
|
90
|
+
cityName,
|
|
91
|
+
callSid: { $exists: true, $nin: [null, ""] },
|
|
92
|
+
...ticketDateMatch(dateRange),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const subjectResolutionStages = (
|
|
96
|
+
cityName: CityName,
|
|
97
|
+
): Record<string, unknown>[] => [
|
|
98
|
+
{ $addFields: { effectiveSubjectId: effectiveSubjectIdExpr } },
|
|
99
|
+
departmentsSubjectsLookup(cityName),
|
|
100
|
+
{ $addFields: { subject: subjectLabelExpr } },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export const runTicketStatsAggregate = async <T>(
|
|
104
|
+
pipeline: Record<string, unknown>[],
|
|
105
|
+
): Promise<T[]> => {
|
|
106
|
+
const rows = await getTicketsCollection()
|
|
107
|
+
.aggregate(pipeline, { maxTimeMS: STATS_MAX_TIME_MS })
|
|
108
|
+
.toArray();
|
|
109
|
+
return rows as T[];
|
|
110
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { CityName } from "../utils/types";
|
|
2
|
+
import {
|
|
3
|
+
DAY_MS,
|
|
4
|
+
endOfCalendarDayInTz,
|
|
5
|
+
startOfCalendarDayInTz,
|
|
6
|
+
} from "../../utils/date.utils";
|
|
7
|
+
import { DEFAULT_LOOKBACK_DAYS } from "./tickets.constants";
|
|
8
|
+
import {
|
|
9
|
+
isDraftTicketExpr,
|
|
10
|
+
runTicketStatsAggregate,
|
|
11
|
+
subjectResolutionStages,
|
|
12
|
+
ticketsWithCallSidMatch,
|
|
13
|
+
} from "./tickets.statistics.aggregation";
|
|
14
|
+
import type {
|
|
15
|
+
TicketStatsDateRange,
|
|
16
|
+
TicketStatsDateScope,
|
|
17
|
+
TicketSubjectRow,
|
|
18
|
+
} from "./tickets.types";
|
|
19
|
+
|
|
20
|
+
export const resolveTicketStatsDateRange = (
|
|
21
|
+
dateScope?: TicketStatsDateScope,
|
|
22
|
+
): TicketStatsDateRange => {
|
|
23
|
+
const to = dateScope?.to ?? new Date();
|
|
24
|
+
const from =
|
|
25
|
+
dateScope?.from ?? new Date(to.getTime() - DEFAULT_LOOKBACK_DAYS * DAY_MS);
|
|
26
|
+
return { from, to };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const ticketStatsDateScopeFromYmd = (
|
|
30
|
+
startStr: string,
|
|
31
|
+
endStr: string,
|
|
32
|
+
timezone: string,
|
|
33
|
+
): TicketStatsDateScope => ({
|
|
34
|
+
from: startOfCalendarDayInTz(startStr, timezone),
|
|
35
|
+
to: endOfCalendarDayInTz(endStr, timezone),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** @deprecated Use findCallSidTicketCountsByCity instead. */
|
|
39
|
+
export const findCallSidsWithTicketsByCity = async (
|
|
40
|
+
cityName: CityName,
|
|
41
|
+
dateScope?: TicketStatsDateScope,
|
|
42
|
+
): Promise<string[]> => {
|
|
43
|
+
const rows = await runTicketStatsAggregate<{ _id: unknown }>([
|
|
44
|
+
{
|
|
45
|
+
$match: ticketsWithCallSidMatch(
|
|
46
|
+
cityName,
|
|
47
|
+
resolveTicketStatsDateRange(dateScope),
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
{ $group: { _id: "$callSid" } },
|
|
51
|
+
]);
|
|
52
|
+
return rows
|
|
53
|
+
.filter((r) => r._id != null && r._id !== "")
|
|
54
|
+
.map((r) => String(r._id));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const findCallSidTicketCountsByCity = async (
|
|
58
|
+
cityName: CityName,
|
|
59
|
+
dateScope?: TicketStatsDateScope,
|
|
60
|
+
): Promise<Map<string, number>> => {
|
|
61
|
+
const rows = await runTicketStatsAggregate<{
|
|
62
|
+
_id: unknown;
|
|
63
|
+
ticketCount: number;
|
|
64
|
+
}>([
|
|
65
|
+
{
|
|
66
|
+
$match: ticketsWithCallSidMatch(
|
|
67
|
+
cityName,
|
|
68
|
+
resolveTicketStatsDateRange(dateScope),
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
{ $group: { _id: "$callSid", ticketCount: { $sum: 1 } } },
|
|
72
|
+
]);
|
|
73
|
+
return new Map(
|
|
74
|
+
rows
|
|
75
|
+
.filter((r) => r._id != null && r._id !== "")
|
|
76
|
+
.map((r) => [String(r._id), r.ticketCount]),
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** @deprecated Use findCallSidDraftTicketCountsByCity instead. */
|
|
81
|
+
export const findCallSidsWithDraftTicketsByCity = async (
|
|
82
|
+
cityName: CityName,
|
|
83
|
+
dateScope?: TicketStatsDateScope,
|
|
84
|
+
): Promise<string[]> => {
|
|
85
|
+
const rows = await runTicketStatsAggregate<{ _id: unknown }>([
|
|
86
|
+
{ $match: ticketsWithCallSidMatch(cityName, resolveTicketStatsDateRange(dateScope)) },
|
|
87
|
+
{ $match: { $expr: isDraftTicketExpr } },
|
|
88
|
+
{ $group: { _id: "$callSid" } },
|
|
89
|
+
]);
|
|
90
|
+
return rows
|
|
91
|
+
.filter((r) => r._id != null && r._id !== "")
|
|
92
|
+
.map((r) => String(r._id));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const findCallSidDraftTicketCountsByCity = async (
|
|
96
|
+
cityName: CityName,
|
|
97
|
+
dateScope?: TicketStatsDateScope,
|
|
98
|
+
): Promise<Map<string, number>> => {
|
|
99
|
+
const rows = await runTicketStatsAggregate<{ _id: unknown; ticketCount: number }>([
|
|
100
|
+
{ $match: ticketsWithCallSidMatch(cityName, resolveTicketStatsDateRange(dateScope)) },
|
|
101
|
+
{ $match: { $expr: isDraftTicketExpr } },
|
|
102
|
+
{ $group: { _id: "$callSid", ticketCount: { $sum: 1 } } },
|
|
103
|
+
]);
|
|
104
|
+
return new Map(
|
|
105
|
+
rows
|
|
106
|
+
.filter((r) => r._id != null && r._id !== "")
|
|
107
|
+
.map((r) => [String(r._id), r.ticketCount]),
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const findTicketSubjectRows = async (
|
|
112
|
+
cityName: CityName,
|
|
113
|
+
dateScope?: TicketStatsDateScope,
|
|
114
|
+
): Promise<TicketSubjectRow[]> => {
|
|
115
|
+
const rows = await runTicketStatsAggregate<{
|
|
116
|
+
callSid: unknown;
|
|
117
|
+
subject: unknown;
|
|
118
|
+
}>([
|
|
119
|
+
{
|
|
120
|
+
$match: ticketsWithCallSidMatch(
|
|
121
|
+
cityName,
|
|
122
|
+
resolveTicketStatsDateRange(dateScope),
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
...subjectResolutionStages(cityName),
|
|
126
|
+
{ $project: { _id: 0, callSid: 1, subject: 1 } },
|
|
127
|
+
]);
|
|
128
|
+
return rows.map((r) => ({
|
|
129
|
+
callSid: String(r.callSid),
|
|
130
|
+
subject: String(r.subject),
|
|
131
|
+
}));
|
|
132
|
+
};
|