@talkpilot/core-db 1.3.12 → 1.3.13
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 -141
- package/README.md +307 -297
- package/dist/municipal/tickets/tickets.statistics.getters.d.ts.map +1 -1
- package/dist/municipal/tickets/tickets.statistics.getters.js +6 -2
- package/dist/municipal/tickets/tickets.statistics.getters.js.map +1 -1
- 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 +1 -0
- 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/subscriptions/subscriptions.utils.d.ts +4 -0
- package/dist/talkpilot/subscriptions/subscriptions.utils.d.ts.map +1 -0
- package/dist/talkpilot/subscriptions/subscriptions.utils.js +20 -0
- package/dist/talkpilot/subscriptions/subscriptions.utils.js.map +1 -0
- package/dist/test-utils/factories/talkpilot/flows.d.ts.map +1 -1
- package/dist/test-utils/factories/talkpilot/flows.js +1 -0
- package/dist/test-utils/factories/talkpilot/flows.js.map +1 -1
- package/jest.config.js +20 -20
- package/package.json +46 -46
- package/src/municipal/muniIssues/__tests__/muniIssues.setters.spec.ts +94 -94
- package/src/municipal/tickets/__tests__/tickets.statistics.spec.ts +99 -99
- package/src/municipal/tickets/index.ts +9 -9
- package/src/municipal/tickets/tickets.statistics.aggregation.ts +110 -110
- package/src/municipal/tickets/tickets.statistics.getters.ts +145 -132
- package/src/municipal/tickets/tickets.types.ts +54 -54
- package/src/talkpilot/calls/__tests__/calls.statistics.spec.ts +483 -481
- package/src/talkpilot/calls/calls.statistics.getters.ts +668 -668
- package/src/talkpilot/calls/calls.statistics.types.ts +48 -48
- package/src/talkpilot/clientsConfig/clientsConfig.types.ts +127 -127
- package/src/talkpilot/contextNotes/__tests__/contextNotes.getters.spec.ts +217 -217
- package/src/talkpilot/contextNotes/contextNotes.getters.ts +85 -85
- package/src/talkpilot/flows/flows.schema.ts +1 -0
- package/src/talkpilot/flows/flows.types.ts +2 -0
- package/src/talkpilot/products/__tests__/products.getters.spec.ts +44 -44
- package/src/test-utils/factories/talkpilot/contextNotes.ts +40 -40
- package/src/test-utils/factories/talkpilot/flows.ts +1 -0
- package/src/utils/date.utils.ts +126 -126
- package/tsconfig.json +23 -23
- package/README_OLD.md +0 -160
- package/dist/talkpilot/calls/calls.dashboard.d.ts +0 -3
- package/dist/talkpilot/calls/calls.dashboard.d.ts.map +0 -1
- package/dist/talkpilot/calls/calls.dashboard.js +0 -191
- package/dist/talkpilot/calls/calls.dashboard.js.map +0 -1
|
@@ -1,99 +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
|
+
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,9 +1,9 @@
|
|
|
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";
|
|
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";
|
|
@@ -1,110 +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
|
-
};
|
|
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
|
+
};
|