@talkpilot/core-db 1.1.19 → 1.2.0

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 (130) hide show
  1. package/.cursor/rules/development.mdc +65 -65
  2. package/DEVELOPMENT.md +98 -98
  3. package/README.md +160 -160
  4. package/dist/talkpilot/calls/calls.getters.d.ts +2 -1
  5. package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
  6. package/dist/talkpilot/calls/calls.getters.js +176 -0
  7. package/dist/talkpilot/calls/calls.getters.js.map +1 -1
  8. package/dist/talkpilot/calls/calls.types.d.ts +48 -0
  9. package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
  10. package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts +1 -0
  11. package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts.map +1 -1
  12. package/dist/talkpilot/clientsConfig/clientsConfig.getters.js +13 -0
  13. package/dist/talkpilot/clientsConfig/clientsConfig.getters.js.map +1 -1
  14. package/jest.config.js +19 -19
  15. package/package.json +45 -45
  16. package/src/__tests__/setup.ts +20 -20
  17. package/src/connection.ts +42 -42
  18. package/src/index.ts +16 -16
  19. package/src/municipal/__tests__/validation.spec.ts +62 -62
  20. package/src/municipal/cities/cities.getters.ts +50 -50
  21. package/src/municipal/cities/cities.types.ts +11 -11
  22. package/src/municipal/cities/index.ts +2 -2
  23. package/src/municipal/departmentsSubjects/departmentsSubjects.getters.ts +282 -282
  24. package/src/municipal/departmentsSubjects/departmentsSubjects.types.ts +72 -72
  25. package/src/municipal/departmentsSubjects/index.ts +9 -9
  26. package/src/municipal/index.ts +21 -21
  27. package/src/municipal/mongodb-client.ts +61 -61
  28. package/src/municipal/streets/index.ts +2 -2
  29. package/src/municipal/streets/streets.getters.ts +125 -125
  30. package/src/municipal/streets/streets.types.ts +18 -18
  31. package/src/municipal/systemInstructions/__tests__/getters.spec.ts +113 -113
  32. package/src/municipal/systemInstructions/__tests__/setters.spec.ts +274 -274
  33. package/src/municipal/systemInstructions/index.ts +7 -7
  34. package/src/municipal/systemInstructions/instructions.getters.ts +57 -57
  35. package/src/municipal/systemInstructions/instructions.setters.ts +119 -119
  36. package/src/municipal/systemInstructions/instructions.types.ts +30 -30
  37. package/src/municipal/tickets/__tests__/tickets.getters.spec.ts +66 -66
  38. package/src/municipal/tickets/index.ts +2 -2
  39. package/src/municipal/tickets/tickets.getters.ts +261 -261
  40. package/src/municipal/tickets/tickets.types.ts +43 -43
  41. package/src/municipal/utils/types.ts +11 -11
  42. package/src/talkpilot/__tests__/db.spec.ts +38 -38
  43. package/src/talkpilot/__tests__/mongodb-client.spec.ts +18 -18
  44. package/src/talkpilot/__tests__/validation.spec.ts +68 -68
  45. package/src/talkpilot/agents/__tests__/agents.getters.spec.ts +29 -29
  46. package/src/talkpilot/agents/agents.getters.ts +34 -34
  47. package/src/talkpilot/agents/agents.types.ts +14 -14
  48. package/src/talkpilot/agents/index.ts +2 -2
  49. package/src/talkpilot/backgroundToolResults/__tests__/backgroundToolResults.getters.spec.ts +147 -147
  50. package/src/talkpilot/backgroundToolResults/backgroundToolResults.getters.ts +65 -65
  51. package/src/talkpilot/backgroundToolResults/backgroundToolResults.types.ts +23 -23
  52. package/src/talkpilot/backgroundToolResults/index.ts +2 -2
  53. package/src/talkpilot/calls/__tests__/callStats.utils.spec.ts +128 -128
  54. package/src/talkpilot/calls/__tests__/calls.spec.ts +252 -252
  55. package/src/talkpilot/calls/calls.getters.ts +446 -248
  56. package/src/talkpilot/calls/calls.types.ts +171 -115
  57. package/src/talkpilot/calls/index.ts +2 -2
  58. package/src/talkpilot/clientAudioBuffers/__tests__/clientAudioBuffer.getters.spec.ts +160 -160
  59. package/src/talkpilot/clientAudioBuffers/clientAudioBuffer.getters.ts +117 -117
  60. package/src/talkpilot/clientAudioBuffers/clientsAudioBuffers.types.ts +25 -25
  61. package/src/talkpilot/clientAudioBuffers/index.ts +2 -2
  62. package/src/talkpilot/clients/clients.getters.ts +16 -16
  63. package/src/talkpilot/clients/clients.types.ts +14 -14
  64. package/src/talkpilot/clients/index.ts +2 -2
  65. package/src/talkpilot/clientsConfig/__tests__/clientsConfig.spec.ts +106 -106
  66. package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +44 -22
  67. package/src/talkpilot/clientsConfig/clientsConfig.types.ts +94 -94
  68. package/src/talkpilot/clientsConfig/index.ts +2 -2
  69. package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +67 -67
  70. package/src/talkpilot/flows/flows.getter.ts +14 -14
  71. package/src/talkpilot/flows/flows.schema.ts +153 -153
  72. package/src/talkpilot/flows/flows.types.ts +184 -184
  73. package/src/talkpilot/flows/index.ts +2 -2
  74. package/src/talkpilot/groups/__tests__/groups.spec.ts +90 -90
  75. package/src/talkpilot/groups/__tests__/phone.utils.spec.ts +32 -32
  76. package/src/talkpilot/groups/groups.getters.ts +30 -30
  77. package/src/talkpilot/groups/groups.types.ts +29 -29
  78. package/src/talkpilot/groups/index.ts +3 -3
  79. package/src/talkpilot/groups/phone.utils.ts +46 -46
  80. package/src/talkpilot/index.ts +29 -29
  81. package/src/talkpilot/leads/index.ts +2 -2
  82. package/src/talkpilot/leads/leads.getter.ts +6 -6
  83. package/src/talkpilot/leads/leads.schema.ts +33 -33
  84. package/src/talkpilot/leads/leads.types.ts +20 -20
  85. package/src/talkpilot/mongodb-client.ts +78 -78
  86. package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +247 -247
  87. package/src/talkpilot/phone_numbers/index.ts +2 -2
  88. package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +154 -154
  89. package/src/talkpilot/phone_numbers/phone_numbers.schema.ts +17 -17
  90. package/src/talkpilot/phone_numbers/phone_numbers.types.ts +30 -30
  91. package/src/talkpilot/plans/__tests__/plans.spec.ts +70 -70
  92. package/src/talkpilot/plans/index.ts +2 -2
  93. package/src/talkpilot/plans/plans.getters.ts +132 -132
  94. package/src/talkpilot/plans/plans.types.ts +89 -89
  95. package/src/talkpilot/results/index.ts +7 -7
  96. package/src/talkpilot/results/results.getter.ts +35 -35
  97. package/src/talkpilot/results/results.schema.ts +25 -25
  98. package/src/talkpilot/results/results.types.ts +34 -34
  99. package/src/talkpilot/retry_analyze/__tests__/retryAnalyze.getters.spec.ts +156 -156
  100. package/src/talkpilot/retry_analyze/index.ts +2 -2
  101. package/src/talkpilot/retry_analyze/retryAnalyze.getters.ts +75 -75
  102. package/src/talkpilot/retry_analyze/retryAnalyze.types.ts +13 -13
  103. package/src/talkpilot/sessions/__tests__/sessions.spec.ts +147 -147
  104. package/src/talkpilot/sessions/index.ts +2 -2
  105. package/src/talkpilot/sessions/sessions.getter.ts +92 -92
  106. package/src/talkpilot/sessions/sessions.schema.ts +34 -34
  107. package/src/talkpilot/sessions/sessions.types.ts +30 -30
  108. package/src/talkpilot/subscriptions/__tests__/subscriptions.getters.utils.spec.ts +45 -45
  109. package/src/talkpilot/subscriptions/index.ts +3 -3
  110. package/src/talkpilot/subscriptions/subscriptions.getters.ts +146 -146
  111. package/src/talkpilot/subscriptions/subscriptions.getters.utils.ts +33 -33
  112. package/src/talkpilot/subscriptions/subscriptions.types.ts +66 -66
  113. package/src/talkpilot/utils/__tests__/query.utils.spec.ts +49 -49
  114. package/src/talkpilot/utils/query.utils.ts +21 -21
  115. package/src/test-utils/db-utils.ts +24 -24
  116. package/src/test-utils/factories/index.ts +12 -12
  117. package/src/test-utils/factories/municipal/cities.ts +16 -16
  118. package/src/test-utils/factories/municipal/departmentsSubjects.ts +37 -37
  119. package/src/test-utils/factories/municipal/streets.ts +22 -22
  120. package/src/test-utils/factories/municipal/tickets.ts +39 -39
  121. package/src/test-utils/factories/talkpilot/agents.ts +19 -19
  122. package/src/test-utils/factories/talkpilot/calls.ts +37 -37
  123. package/src/test-utils/factories/talkpilot/clientAudioBuffers.ts +20 -20
  124. package/src/test-utils/factories/talkpilot/clientsConfig.ts +18 -18
  125. package/src/test-utils/factories/talkpilot/flows.ts +33 -33
  126. package/src/test-utils/factories/talkpilot/groups.ts +33 -33
  127. package/src/test-utils/factories/talkpilot/phone_numbers.ts +22 -22
  128. package/src/test-utils/factories/talkpilot/sessions.ts +35 -35
  129. package/src/utils/validation.ts +23 -23
  130. package/tsconfig.json +23 -23
@@ -1,261 +1,261 @@
1
- import { CityName, getDb, ObjectId, Ticket } from "../index";
2
- import type { SubjectStatsItem } from "./tickets.types";
3
- import { Collection, Filter, ObjectId as MongoObjectId } from "mongodb";
4
-
5
- /**
6
- * Generate a new ticket ID as a string
7
- * @returns A new ObjectId as a string
8
- */
9
- export const generateTicketId = (): string => {
10
- return new ObjectId().toString();
11
- };
12
-
13
- export const getTicketsCollection = (): Collection<Ticket> => {
14
- return getDb().collection<Ticket>("tickets");
15
- };
16
-
17
- export const findTickets = async (
18
- filter: Filter<Ticket> = {},
19
- ): Promise<Ticket[]> => {
20
- return await getTicketsCollection().find(filter).toArray();
21
- };
22
-
23
- export const findTicketsByCallSid = async (
24
- callSid: string,
25
- ): Promise<Ticket[]> => {
26
- return await getTicketsCollection().find({ callSid }).toArray();
27
- };
28
-
29
- export const getTicketById = async (
30
- ticketId: string,
31
- ): Promise<Ticket | null> => {
32
- const ticket = await getTicketsCollection().findOne({
33
- _id: new ObjectId(ticketId),
34
- });
35
- return ticket ? ticket : null;
36
- };
37
-
38
- export const createTicket = async (
39
- ticketData: Omit<Ticket, "_id" | "createdAt" | "updatedAt">,
40
- ticketId?: string,
41
- ): Promise<MongoObjectId> => {
42
- const ticket: Ticket = {
43
- _id: ticketId ? new ObjectId(ticketId) : new ObjectId(),
44
- ...ticketData,
45
- createdAt: new Date(),
46
- updatedAt: new Date(),
47
- };
48
- const { insertedId } = await getTicketsCollection().insertOne(ticket);
49
- return insertedId;
50
- };
51
-
52
- export const updateTicket = async (
53
- ticketId: string,
54
- data: Partial<Omit<Ticket, "_id" | "createdAt" | "updatedAt">>,
55
- ): Promise<Ticket | null> => {
56
- const result = await getTicketsCollection().findOneAndUpdate(
57
- { _id: new ObjectId(ticketId) },
58
- { $set: { ...data, updatedAt: new Date() } },
59
- { returnDocument: "after" },
60
- );
61
- return result || null;
62
- };
63
-
64
- export const deleteTicket = async (ticketId: string): Promise<boolean> => {
65
- const result = await getTicketsCollection().deleteOne({
66
- _id: new ObjectId(ticketId),
67
- });
68
- return result.deletedCount > 0;
69
- };
70
-
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
- export const findTicketByQuery = async (query: Partial<Ticket>) => {
211
- return await getTicketsCollection().findOne(query);
212
- };
213
- /**
214
- * Update ticket files by external call number
215
- */
216
- export const updateTicketFilesByCallNumber = async (params: {
217
- cityName: CityName;
218
- callNumber: string;
219
- image1?: string;
220
- image2?: string;
221
- image3?: string;
222
- }): Promise<boolean> => {
223
- const { cityName, callNumber, image1, image2, image3 } = params;
224
-
225
- const update: any = {};
226
- if (image1) update["externalCallFields.image1"] = image1;
227
- if (image2) update["externalCallFields.image2"] = image2;
228
- if (image3) update["externalCallFields.image3"] = image3;
229
-
230
- const result = await getTicketsCollection().updateOne(
231
- {
232
- cityName: cityName,
233
- "externalCallFields.call_number": callNumber,
234
- },
235
- {
236
- $set: {
237
- ...update,
238
- updatedAt: new Date(),
239
- },
240
- },
241
- );
242
-
243
- return result.matchedCount > 0;
244
- };
245
-
246
- /**
247
- * Remove guestJwt from ticket by external call number
248
- */
249
- export const clearGuestJwtByCallNumber = async (params: {
250
- cityName: CityName;
251
- callNumber: string;
252
- }): Promise<boolean> => {
253
- const { cityName, callNumber } = params;
254
-
255
- const result = await getTicketsCollection().updateOne(
256
- { cityName, "externalCallFields.call_number": callNumber },
257
- { $unset: { guestJwt: "" }, $set: { updatedAt: new Date() } },
258
- );
259
-
260
- return result.matchedCount > 0;
261
- };
1
+ import { CityName, getDb, ObjectId, Ticket } from "../index";
2
+ import type { SubjectStatsItem } from "./tickets.types";
3
+ import { Collection, Filter, ObjectId as MongoObjectId } from "mongodb";
4
+
5
+ /**
6
+ * Generate a new ticket ID as a string
7
+ * @returns A new ObjectId as a string
8
+ */
9
+ export const generateTicketId = (): string => {
10
+ return new ObjectId().toString();
11
+ };
12
+
13
+ export const getTicketsCollection = (): Collection<Ticket> => {
14
+ return getDb().collection<Ticket>("tickets");
15
+ };
16
+
17
+ export const findTickets = async (
18
+ filter: Filter<Ticket> = {},
19
+ ): Promise<Ticket[]> => {
20
+ return await getTicketsCollection().find(filter).toArray();
21
+ };
22
+
23
+ export const findTicketsByCallSid = async (
24
+ callSid: string,
25
+ ): Promise<Ticket[]> => {
26
+ return await getTicketsCollection().find({ callSid }).toArray();
27
+ };
28
+
29
+ export const getTicketById = async (
30
+ ticketId: string,
31
+ ): Promise<Ticket | null> => {
32
+ const ticket = await getTicketsCollection().findOne({
33
+ _id: new ObjectId(ticketId),
34
+ });
35
+ return ticket ? ticket : null;
36
+ };
37
+
38
+ export const createTicket = async (
39
+ ticketData: Omit<Ticket, "_id" | "createdAt" | "updatedAt">,
40
+ ticketId?: string,
41
+ ): Promise<MongoObjectId> => {
42
+ const ticket: Ticket = {
43
+ _id: ticketId ? new ObjectId(ticketId) : new ObjectId(),
44
+ ...ticketData,
45
+ createdAt: new Date(),
46
+ updatedAt: new Date(),
47
+ };
48
+ const { insertedId } = await getTicketsCollection().insertOne(ticket);
49
+ return insertedId;
50
+ };
51
+
52
+ export const updateTicket = async (
53
+ ticketId: string,
54
+ data: Partial<Omit<Ticket, "_id" | "createdAt" | "updatedAt">>,
55
+ ): Promise<Ticket | null> => {
56
+ const result = await getTicketsCollection().findOneAndUpdate(
57
+ { _id: new ObjectId(ticketId) },
58
+ { $set: { ...data, updatedAt: new Date() } },
59
+ { returnDocument: "after" },
60
+ );
61
+ return result || null;
62
+ };
63
+
64
+ export const deleteTicket = async (ticketId: string): Promise<boolean> => {
65
+ const result = await getTicketsCollection().deleteOne({
66
+ _id: new ObjectId(ticketId),
67
+ });
68
+ return result.deletedCount > 0;
69
+ };
70
+
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
+ export const findTicketByQuery = async (query: Partial<Ticket>) => {
211
+ return await getTicketsCollection().findOne(query);
212
+ };
213
+ /**
214
+ * Update ticket files by external call number
215
+ */
216
+ export const updateTicketFilesByCallNumber = async (params: {
217
+ cityName: CityName;
218
+ callNumber: string;
219
+ image1?: string;
220
+ image2?: string;
221
+ image3?: string;
222
+ }): Promise<boolean> => {
223
+ const { cityName, callNumber, image1, image2, image3 } = params;
224
+
225
+ const update: any = {};
226
+ if (image1) update["externalCallFields.image1"] = image1;
227
+ if (image2) update["externalCallFields.image2"] = image2;
228
+ if (image3) update["externalCallFields.image3"] = image3;
229
+
230
+ const result = await getTicketsCollection().updateOne(
231
+ {
232
+ cityName: cityName,
233
+ "externalCallFields.call_number": callNumber,
234
+ },
235
+ {
236
+ $set: {
237
+ ...update,
238
+ updatedAt: new Date(),
239
+ },
240
+ },
241
+ );
242
+
243
+ return result.matchedCount > 0;
244
+ };
245
+
246
+ /**
247
+ * Remove guestJwt from ticket by external call number
248
+ */
249
+ export const clearGuestJwtByCallNumber = async (params: {
250
+ cityName: CityName;
251
+ callNumber: string;
252
+ }): Promise<boolean> => {
253
+ const { cityName, callNumber } = params;
254
+
255
+ const result = await getTicketsCollection().updateOne(
256
+ { cityName, "externalCallFields.call_number": callNumber },
257
+ { $unset: { guestJwt: "" }, $set: { updatedAt: new Date() } },
258
+ );
259
+
260
+ return result.matchedCount > 0;
261
+ };
@@ -1,43 +1,43 @@
1
- import { ObjectId, WithId } from "mongodb";
2
-
3
- import { CityName } from "../utils/types";
4
-
5
- export type Ticket = {
6
- _id: ObjectId;
7
- createdAt: Date;
8
- updatedAt: Date;
9
- callSid?: string;
10
- cityName: CityName;
11
- externalCallFields: {
12
- // Request fields
13
- first_name?: string;
14
- last_name?: string;
15
- event_description?: string;
16
- street?: string;
17
- house_number?: string;
18
- event_subject_id?: string;
19
- event_sub_subject_id?: string;
20
- event_sub_subject_id2?: string | null;
21
- // Response fields (only if external call was successful)
22
- call_number?: string;
23
- status?: number; // Response status from Bina API (1 = success)
24
- error?: string; // Error message from Bina API if any
25
- image1?: string;
26
- image2?: string;
27
- image3?: string;
28
- file1?: string;
29
- file2?: string;
30
- file3?: string;
31
- };
32
- guestJwt?: string;
33
- };
34
-
35
- export type TicketDoc = WithId<Ticket>;
36
-
37
- /** Call count by department (subject). */
38
- export type SubjectStatsItem = {
39
- subject_name: string;
40
- subject_id: string;
41
- count: number;
42
- percentage: number;
43
- };
1
+ import { ObjectId, WithId } from "mongodb";
2
+
3
+ import { CityName } from "../utils/types";
4
+
5
+ export type Ticket = {
6
+ _id: ObjectId;
7
+ createdAt: Date;
8
+ updatedAt: Date;
9
+ callSid?: string;
10
+ cityName: CityName;
11
+ externalCallFields: {
12
+ // Request fields
13
+ first_name?: string;
14
+ last_name?: string;
15
+ event_description?: string;
16
+ street?: string;
17
+ house_number?: string;
18
+ event_subject_id?: string;
19
+ event_sub_subject_id?: string;
20
+ event_sub_subject_id2?: string | null;
21
+ // Response fields (only if external call was successful)
22
+ call_number?: string;
23
+ status?: number; // Response status from Bina API (1 = success)
24
+ error?: string; // Error message from Bina API if any
25
+ image1?: string;
26
+ image2?: string;
27
+ image3?: string;
28
+ file1?: string;
29
+ file2?: string;
30
+ file3?: string;
31
+ };
32
+ guestJwt?: string;
33
+ };
34
+
35
+ export type TicketDoc = WithId<Ticket>;
36
+
37
+ /** Call count by department (subject). */
38
+ export type SubjectStatsItem = {
39
+ subject_name: string;
40
+ subject_id: string;
41
+ count: number;
42
+ percentage: number;
43
+ };
@@ -1,11 +1,11 @@
1
- /**
2
- * City name type for municipal data
3
- */
4
- export type CityName =
5
- | "ashdod"
6
- | "maltar"
7
- | "billit"
8
- | "hashkelon"
9
- | "eilat"
10
- | "tests"
11
- | string;
1
+ /**
2
+ * City name type for municipal data
3
+ */
4
+ export type CityName =
5
+ | "ashdod"
6
+ | "maltar"
7
+ | "billit"
8
+ | "hashkelon"
9
+ | "eilat"
10
+ | "tests"
11
+ | string;