bulltrackers-module 1.0.467 → 1.0.468
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/functions/alert-system/index.js +194 -4
- package/package.json +1 -1
|
@@ -112,6 +112,13 @@ async function handleAlertTrigger(message, context, config, dependencies) {
|
|
|
112
112
|
|
|
113
113
|
logger.log('SUCCESS', `[AlertTrigger] Completed processing ${computationName}: ${processedCount} successful, ${errorCount} errors`);
|
|
114
114
|
|
|
115
|
+
// After processing alerts, check for "all clear" notifications for static watchlists
|
|
116
|
+
// This runs after all alert computations are processed
|
|
117
|
+
if (processedCount > 0 || results.cids.length > 0) {
|
|
118
|
+
// Check if any PIs were processed but didn't trigger alerts
|
|
119
|
+
await checkAndSendAllClearNotifications(db, logger, results.cids, date, config);
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
} catch (error) {
|
|
116
123
|
logger.log('ERROR', '[AlertTrigger] Fatal error processing alert trigger', error);
|
|
117
124
|
throw error; // Re-throw for Pub/Sub retry
|
|
@@ -134,12 +141,23 @@ async function handleComputationResultWrite(change, context, config, dependencie
|
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
try {
|
|
137
|
-
// 1. Check if this is an alert computation
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
// 1. Check if this is an alert computation OR PopularInvestorProfileMetrics
|
|
145
|
+
const isProfileMetrics = computationName === 'PopularInvestorProfileMetrics';
|
|
146
|
+
if (!isAlertComputation(computationName) && !isProfileMetrics) {
|
|
147
|
+
logger.log('DEBUG', `[AlertTrigger] Not an alert computation or profile metrics: ${computationName}`);
|
|
140
148
|
return;
|
|
141
149
|
}
|
|
142
150
|
|
|
151
|
+
// If it's PopularInvestorProfileMetrics, check for all-clear notifications only
|
|
152
|
+
if (isProfileMetrics) {
|
|
153
|
+
const docData = change.after.data();
|
|
154
|
+
const results = readComputationResults(docData);
|
|
155
|
+
if (results.cids && results.cids.length > 0) {
|
|
156
|
+
await checkAndSendAllClearNotifications(db, logger, results.cids, date, config);
|
|
157
|
+
}
|
|
158
|
+
return; // Don't process as alert computation
|
|
159
|
+
}
|
|
160
|
+
|
|
143
161
|
const alertType = getAlertTypeByComputation(computationName);
|
|
144
162
|
if (!alertType) {
|
|
145
163
|
logger.log('WARN', `[AlertTrigger] Alert type not found for computation: ${computationName}`);
|
|
@@ -188,17 +206,189 @@ async function handleComputationResultWrite(change, context, config, dependencie
|
|
|
188
206
|
|
|
189
207
|
logger.log('SUCCESS', `[AlertTrigger] Completed processing ${computationName}: ${processedCount} successful, ${errorCount} errors`);
|
|
190
208
|
|
|
209
|
+
// After processing alerts, check for "all clear" notifications for static watchlists
|
|
210
|
+
if (processedCount > 0 || results.cids.length > 0) {
|
|
211
|
+
// Check if any PIs were processed but didn't trigger alerts
|
|
212
|
+
await checkAndSendAllClearNotifications(db, logger, results.cids, date, config);
|
|
213
|
+
}
|
|
214
|
+
|
|
191
215
|
} catch (error) {
|
|
192
216
|
logger.log('ERROR', `[AlertTrigger] Fatal error processing ${computationName}`, error);
|
|
193
217
|
// Don't throw - we don't want to retry the entire computation write
|
|
194
218
|
}
|
|
195
219
|
}
|
|
196
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Check for PIs in static watchlists that were processed but didn't trigger alerts
|
|
223
|
+
* Send "all clear" notifications to users who have these PIs in their static watchlists
|
|
224
|
+
* This should be called after PopularInvestorProfileMetrics is computed (not just alert computations)
|
|
225
|
+
*/
|
|
226
|
+
async function checkAndSendAllClearNotifications(db, logger, processedPICids, computationDate, config) {
|
|
227
|
+
try {
|
|
228
|
+
const today = computationDate || new Date().toISOString().split('T')[0];
|
|
229
|
+
|
|
230
|
+
if (!processedPICids || processedPICids.length === 0) {
|
|
231
|
+
return; // No PIs to check
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Get all static watchlists that contain any of the processed PIs
|
|
235
|
+
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
236
|
+
const watchlistsSnapshot = await db.collection(watchlistsCollection).get();
|
|
237
|
+
|
|
238
|
+
const usersToNotify = new Map(); // userCid -> Map of piCid -> {username, ...}
|
|
239
|
+
|
|
240
|
+
for (const userDoc of watchlistsSnapshot.docs) {
|
|
241
|
+
const userCid = userDoc.id;
|
|
242
|
+
const userListsSnapshot = await userDoc.ref.collection('lists').get();
|
|
243
|
+
|
|
244
|
+
for (const listDoc of userListsSnapshot.docs) {
|
|
245
|
+
const listData = listDoc.data();
|
|
246
|
+
|
|
247
|
+
// Only check static watchlists
|
|
248
|
+
if (listData.type !== 'static' || !listData.items || !Array.isArray(listData.items)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check if any processed PI is in this watchlist
|
|
253
|
+
for (const item of listData.items) {
|
|
254
|
+
const piCid = Number(item.cid);
|
|
255
|
+
if (processedPICids.includes(piCid)) {
|
|
256
|
+
// Check if this PI triggered any alerts today
|
|
257
|
+
const hasAlerts = await checkIfPIHasAlertsToday(db, userCid, piCid, today);
|
|
258
|
+
|
|
259
|
+
if (!hasAlerts) {
|
|
260
|
+
// No alerts triggered - send "all clear" notification
|
|
261
|
+
if (!usersToNotify.has(userCid)) {
|
|
262
|
+
usersToNotify.set(userCid, new Map());
|
|
263
|
+
}
|
|
264
|
+
usersToNotify.get(userCid).set(piCid, {
|
|
265
|
+
cid: piCid,
|
|
266
|
+
username: item.username || `PI-${piCid}`
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Send notifications to all users
|
|
275
|
+
let totalNotifications = 0;
|
|
276
|
+
for (const [userCid, pisMap] of usersToNotify.entries()) {
|
|
277
|
+
for (const [piCid, pi] of pisMap.entries()) {
|
|
278
|
+
await sendAllClearNotification(db, logger, userCid, pi.cid, pi.username, today);
|
|
279
|
+
totalNotifications++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (totalNotifications > 0) {
|
|
284
|
+
logger.log('INFO', `[checkAndSendAllClearNotifications] Sent ${totalNotifications} all-clear notifications to ${usersToNotify.size} users`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
} catch (error) {
|
|
288
|
+
logger.log('ERROR', `[checkAndSendAllClearNotifications] Error checking all-clear notifications`, error);
|
|
289
|
+
// Don't throw - this is a non-critical feature
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if a PI has any alerts for a user today
|
|
295
|
+
*/
|
|
296
|
+
async function checkIfPIHasAlertsToday(db, userCid, piCid, date) {
|
|
297
|
+
try {
|
|
298
|
+
const alertsRef = db.collection('user_alerts')
|
|
299
|
+
.doc(String(userCid))
|
|
300
|
+
.collection('alerts');
|
|
301
|
+
|
|
302
|
+
// Check if there are any alerts for this PI today
|
|
303
|
+
const todayStart = new Date(date);
|
|
304
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
305
|
+
const todayEnd = new Date(date);
|
|
306
|
+
todayEnd.setHours(23, 59, 59, 999);
|
|
307
|
+
|
|
308
|
+
const snapshot = await alertsRef
|
|
309
|
+
.where('piCid', '==', Number(piCid))
|
|
310
|
+
.where('computationDate', '==', date)
|
|
311
|
+
.limit(1)
|
|
312
|
+
.get();
|
|
313
|
+
|
|
314
|
+
return !snapshot.empty;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
// If we can't check, assume there are alerts (safer)
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Send an "all clear" notification to a user
|
|
323
|
+
*/
|
|
324
|
+
async function sendAllClearNotification(db, logger, userCid, piCid, piUsername, date) {
|
|
325
|
+
try {
|
|
326
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
327
|
+
|
|
328
|
+
// Check if we already sent an all-clear notification for this PI today
|
|
329
|
+
const existingRef = db.collection('user_alerts')
|
|
330
|
+
.doc(String(userCid))
|
|
331
|
+
.collection('alerts')
|
|
332
|
+
.where('piCid', '==', Number(piCid))
|
|
333
|
+
.where('alertType', '==', 'all_clear')
|
|
334
|
+
.where('computationDate', '==', date)
|
|
335
|
+
.limit(1);
|
|
336
|
+
|
|
337
|
+
const existingSnapshot = await existingRef.get();
|
|
338
|
+
if (!existingSnapshot.empty) {
|
|
339
|
+
// Already sent notification for this PI today
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create the notification using the notification system
|
|
344
|
+
const notificationRef = db.collection('user_notifications')
|
|
345
|
+
.doc(String(userCid))
|
|
346
|
+
.collection('notifications')
|
|
347
|
+
.doc();
|
|
348
|
+
|
|
349
|
+
await notificationRef.set({
|
|
350
|
+
id: notificationRef.id,
|
|
351
|
+
type: 'alert',
|
|
352
|
+
title: 'All Clear',
|
|
353
|
+
message: `${piUsername} was processed, all clear today!`,
|
|
354
|
+
piCid: Number(piCid),
|
|
355
|
+
piUsername: piUsername,
|
|
356
|
+
alertType: 'all_clear',
|
|
357
|
+
computationDate: date,
|
|
358
|
+
read: false,
|
|
359
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
360
|
+
metadata: {
|
|
361
|
+
message: 'User was processed, all clear today!'
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Update notification counter
|
|
366
|
+
const counterRef = db.collection('user_notifications')
|
|
367
|
+
.doc(String(userCid))
|
|
368
|
+
.collection('counters')
|
|
369
|
+
.doc(date);
|
|
370
|
+
|
|
371
|
+
await counterRef.set({
|
|
372
|
+
date: date,
|
|
373
|
+
unreadCount: FieldValue.increment(1),
|
|
374
|
+
totalCount: FieldValue.increment(1),
|
|
375
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
376
|
+
}, { merge: true });
|
|
377
|
+
|
|
378
|
+
logger.log('INFO', `[sendAllClearNotification] Sent all-clear notification to user ${userCid} for PI ${piCid}`);
|
|
379
|
+
|
|
380
|
+
} catch (error) {
|
|
381
|
+
logger.log('ERROR', `[sendAllClearNotification] Error sending all-clear notification`, error);
|
|
382
|
+
// Don't throw - this is non-critical
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
197
386
|
/**
|
|
198
387
|
* Export the trigger handlers
|
|
199
388
|
*/
|
|
200
389
|
module.exports = {
|
|
201
390
|
handleAlertTrigger,
|
|
202
|
-
handleComputationResultWrite
|
|
391
|
+
handleComputationResultWrite,
|
|
392
|
+
checkAndSendAllClearNotifications
|
|
203
393
|
};
|
|
204
394
|
|