@talkpilot/core-db 1.2.0 → 1.2.1

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 (132) 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 +1 -2
  5. package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
  6. package/dist/talkpilot/calls/calls.getters.js +0 -176
  7. package/dist/talkpilot/calls/calls.getters.js.map +1 -1
  8. package/dist/talkpilot/calls/calls.types.d.ts +0 -48
  9. package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
  10. package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts +0 -1
  11. package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts.map +1 -1
  12. package/dist/talkpilot/clientsConfig/clientsConfig.getters.js +0 -13
  13. package/dist/talkpilot/clientsConfig/clientsConfig.getters.js.map +1 -1
  14. package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts +16 -8
  15. package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts.map +1 -1
  16. package/jest.config.js +19 -19
  17. package/package.json +45 -45
  18. package/src/__tests__/setup.ts +20 -20
  19. package/src/connection.ts +42 -42
  20. package/src/index.ts +16 -16
  21. package/src/municipal/__tests__/validation.spec.ts +62 -62
  22. package/src/municipal/cities/cities.getters.ts +50 -50
  23. package/src/municipal/cities/cities.types.ts +11 -11
  24. package/src/municipal/cities/index.ts +2 -2
  25. package/src/municipal/departmentsSubjects/departmentsSubjects.getters.ts +282 -282
  26. package/src/municipal/departmentsSubjects/departmentsSubjects.types.ts +72 -72
  27. package/src/municipal/departmentsSubjects/index.ts +9 -9
  28. package/src/municipal/index.ts +21 -21
  29. package/src/municipal/mongodb-client.ts +61 -61
  30. package/src/municipal/streets/index.ts +2 -2
  31. package/src/municipal/streets/streets.getters.ts +125 -125
  32. package/src/municipal/streets/streets.types.ts +18 -18
  33. package/src/municipal/systemInstructions/__tests__/getters.spec.ts +113 -113
  34. package/src/municipal/systemInstructions/__tests__/setters.spec.ts +274 -274
  35. package/src/municipal/systemInstructions/index.ts +7 -7
  36. package/src/municipal/systemInstructions/instructions.getters.ts +57 -57
  37. package/src/municipal/systemInstructions/instructions.setters.ts +119 -119
  38. package/src/municipal/systemInstructions/instructions.types.ts +30 -30
  39. package/src/municipal/tickets/__tests__/tickets.getters.spec.ts +66 -66
  40. package/src/municipal/tickets/index.ts +2 -2
  41. package/src/municipal/tickets/tickets.getters.ts +261 -261
  42. package/src/municipal/tickets/tickets.types.ts +43 -43
  43. package/src/municipal/utils/types.ts +11 -11
  44. package/src/talkpilot/__tests__/db.spec.ts +38 -38
  45. package/src/talkpilot/__tests__/mongodb-client.spec.ts +18 -18
  46. package/src/talkpilot/__tests__/validation.spec.ts +68 -68
  47. package/src/talkpilot/agents/__tests__/agents.getters.spec.ts +29 -29
  48. package/src/talkpilot/agents/agents.getters.ts +34 -34
  49. package/src/talkpilot/agents/agents.types.ts +14 -14
  50. package/src/talkpilot/agents/index.ts +2 -2
  51. package/src/talkpilot/backgroundToolResults/__tests__/backgroundToolResults.getters.spec.ts +147 -147
  52. package/src/talkpilot/backgroundToolResults/backgroundToolResults.getters.ts +65 -65
  53. package/src/talkpilot/backgroundToolResults/backgroundToolResults.types.ts +23 -23
  54. package/src/talkpilot/backgroundToolResults/index.ts +2 -2
  55. package/src/talkpilot/calls/__tests__/callStats.utils.spec.ts +128 -128
  56. package/src/talkpilot/calls/__tests__/calls.spec.ts +252 -252
  57. package/src/talkpilot/calls/calls.getters.ts +248 -446
  58. package/src/talkpilot/calls/calls.types.ts +115 -171
  59. package/src/talkpilot/calls/index.ts +2 -2
  60. package/src/talkpilot/clientAudioBuffers/__tests__/clientAudioBuffer.getters.spec.ts +160 -160
  61. package/src/talkpilot/clientAudioBuffers/clientAudioBuffer.getters.ts +117 -117
  62. package/src/talkpilot/clientAudioBuffers/clientsAudioBuffers.types.ts +25 -25
  63. package/src/talkpilot/clientAudioBuffers/index.ts +2 -2
  64. package/src/talkpilot/clients/clients.getters.ts +16 -16
  65. package/src/talkpilot/clients/clients.types.ts +14 -14
  66. package/src/talkpilot/clients/index.ts +2 -2
  67. package/src/talkpilot/clientsConfig/__tests__/clientsConfig.spec.ts +187 -106
  68. package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +22 -44
  69. package/src/talkpilot/clientsConfig/clientsConfig.types.ts +119 -94
  70. package/src/talkpilot/clientsConfig/index.ts +2 -2
  71. package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +67 -67
  72. package/src/talkpilot/flows/flows.getter.ts +14 -14
  73. package/src/talkpilot/flows/flows.schema.ts +153 -153
  74. package/src/talkpilot/flows/flows.types.ts +184 -184
  75. package/src/talkpilot/flows/index.ts +2 -2
  76. package/src/talkpilot/groups/__tests__/groups.spec.ts +90 -90
  77. package/src/talkpilot/groups/__tests__/phone.utils.spec.ts +32 -32
  78. package/src/talkpilot/groups/groups.getters.ts +30 -30
  79. package/src/talkpilot/groups/groups.types.ts +29 -29
  80. package/src/talkpilot/groups/index.ts +3 -3
  81. package/src/talkpilot/groups/phone.utils.ts +46 -46
  82. package/src/talkpilot/index.ts +29 -29
  83. package/src/talkpilot/leads/index.ts +2 -2
  84. package/src/talkpilot/leads/leads.getter.ts +6 -6
  85. package/src/talkpilot/leads/leads.schema.ts +33 -33
  86. package/src/talkpilot/leads/leads.types.ts +20 -20
  87. package/src/talkpilot/mongodb-client.ts +78 -78
  88. package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +247 -247
  89. package/src/talkpilot/phone_numbers/index.ts +2 -2
  90. package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +154 -154
  91. package/src/talkpilot/phone_numbers/phone_numbers.schema.ts +17 -17
  92. package/src/talkpilot/phone_numbers/phone_numbers.types.ts +30 -30
  93. package/src/talkpilot/plans/__tests__/plans.spec.ts +70 -70
  94. package/src/talkpilot/plans/index.ts +2 -2
  95. package/src/talkpilot/plans/plans.getters.ts +132 -132
  96. package/src/talkpilot/plans/plans.types.ts +89 -89
  97. package/src/talkpilot/results/index.ts +7 -7
  98. package/src/talkpilot/results/results.getter.ts +35 -35
  99. package/src/talkpilot/results/results.schema.ts +25 -25
  100. package/src/talkpilot/results/results.types.ts +34 -34
  101. package/src/talkpilot/retry_analyze/__tests__/retryAnalyze.getters.spec.ts +156 -156
  102. package/src/talkpilot/retry_analyze/index.ts +2 -2
  103. package/src/talkpilot/retry_analyze/retryAnalyze.getters.ts +75 -75
  104. package/src/talkpilot/retry_analyze/retryAnalyze.types.ts +13 -13
  105. package/src/talkpilot/sessions/__tests__/sessions.spec.ts +147 -147
  106. package/src/talkpilot/sessions/index.ts +2 -2
  107. package/src/talkpilot/sessions/sessions.getter.ts +92 -92
  108. package/src/talkpilot/sessions/sessions.schema.ts +34 -34
  109. package/src/talkpilot/sessions/sessions.types.ts +30 -30
  110. package/src/talkpilot/subscriptions/__tests__/subscriptions.getters.utils.spec.ts +45 -45
  111. package/src/talkpilot/subscriptions/index.ts +3 -3
  112. package/src/talkpilot/subscriptions/subscriptions.getters.ts +146 -146
  113. package/src/talkpilot/subscriptions/subscriptions.getters.utils.ts +33 -33
  114. package/src/talkpilot/subscriptions/subscriptions.types.ts +66 -66
  115. package/src/talkpilot/utils/__tests__/query.utils.spec.ts +49 -49
  116. package/src/talkpilot/utils/query.utils.ts +21 -21
  117. package/src/test-utils/db-utils.ts +24 -24
  118. package/src/test-utils/factories/index.ts +12 -12
  119. package/src/test-utils/factories/municipal/cities.ts +16 -16
  120. package/src/test-utils/factories/municipal/departmentsSubjects.ts +37 -37
  121. package/src/test-utils/factories/municipal/streets.ts +22 -22
  122. package/src/test-utils/factories/municipal/tickets.ts +39 -39
  123. package/src/test-utils/factories/talkpilot/agents.ts +19 -19
  124. package/src/test-utils/factories/talkpilot/calls.ts +37 -37
  125. package/src/test-utils/factories/talkpilot/clientAudioBuffers.ts +20 -20
  126. package/src/test-utils/factories/talkpilot/clientsConfig.ts +18 -18
  127. package/src/test-utils/factories/talkpilot/flows.ts +33 -33
  128. package/src/test-utils/factories/talkpilot/groups.ts +33 -33
  129. package/src/test-utils/factories/talkpilot/phone_numbers.ts +22 -22
  130. package/src/test-utils/factories/talkpilot/sessions.ts +35 -35
  131. package/src/utils/validation.ts +23 -23
  132. 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;