bulltrackers-module 1.0.678 → 1.0.679
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.
|
@@ -16,12 +16,53 @@ const storage = new Storage(); // Singleton GCS Client
|
|
|
16
16
|
*/
|
|
17
17
|
async function processAlertForPI(db, logger, piCid, alertType, computationMetadata, computationDate, dependencies = {}) {
|
|
18
18
|
try {
|
|
19
|
+
// [FIX] Check if computation date is earlier than today (backfill protection)
|
|
20
|
+
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
21
|
+
const isHistoricalData = computationDate < today;
|
|
22
|
+
|
|
23
|
+
// If it's historical data, check if this alert already exists
|
|
24
|
+
if (isHistoricalData) {
|
|
25
|
+
// Check if we've already created alerts for this PI/date/alertType combination
|
|
26
|
+
const existingAlertsSnapshot = await db.collection('SignedInUsers')
|
|
27
|
+
.doc('_metadata') // Use metadata doc to track processed alerts
|
|
28
|
+
.collection('processed_alerts')
|
|
29
|
+
.where('piCid', '==', Number(piCid))
|
|
30
|
+
.where('computationDate', '==', computationDate)
|
|
31
|
+
.where('alertType', '==', alertType.id)
|
|
32
|
+
.limit(1)
|
|
33
|
+
.get();
|
|
34
|
+
|
|
35
|
+
if (!existingAlertsSnapshot.empty) {
|
|
36
|
+
logger.log('INFO', `[processAlertForPI] Skipping duplicate alert for historical data: PI ${piCid}, date ${computationDate}, alert type ${alertType.id}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.log('WARN', `[processAlertForPI] Processing alert for historical data (backfill): PI ${piCid}, date ${computationDate}, alert type ${alertType.id}. This alert will only be sent to developers.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
// 1. Get PI username from rankings or subscriptions
|
|
20
44
|
const piUsername = await getPIUsername(db, piCid);
|
|
21
45
|
|
|
22
46
|
// 2. Find all users subscribed to this PI and alert type
|
|
23
47
|
// Use computationName (e.g., 'RiskScoreIncrease') to map to alertConfig keys
|
|
24
|
-
|
|
48
|
+
let subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName, computationDate, dependencies);
|
|
49
|
+
|
|
50
|
+
// [FIX] If it's historical data, only send to developer accounts
|
|
51
|
+
if (isHistoricalData) {
|
|
52
|
+
const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
53
|
+
|
|
54
|
+
// Filter subscriptions to only include developers
|
|
55
|
+
const devSubscriptions = [];
|
|
56
|
+
for (const subscription of subscriptions) {
|
|
57
|
+
const isDev = await isDeveloper(db, String(subscription.userCid));
|
|
58
|
+
if (isDev) {
|
|
59
|
+
devSubscriptions.push(subscription);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
subscriptions = devSubscriptions;
|
|
64
|
+
logger.log('INFO', `[processAlertForPI] Historical data: Filtered to ${subscriptions.length} developer subscriptions for PI ${piCid}, alert type ${alertType.id}`);
|
|
65
|
+
}
|
|
25
66
|
|
|
26
67
|
if (subscriptions.length === 0) {
|
|
27
68
|
logger.log('INFO', `[processAlertForPI] No subscriptions found for PI ${piCid}, alert type ${alertType.id}`);
|
|
@@ -38,8 +79,24 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
38
79
|
const config = dependencies.config || {};
|
|
39
80
|
const notificationPromises = [];
|
|
40
81
|
|
|
82
|
+
// Check watchlistAlerts preference for each user
|
|
83
|
+
const { manageNotificationPreferences } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
84
|
+
|
|
41
85
|
for (const subscription of subscriptions) {
|
|
42
86
|
const userCid = subscription.userCid;
|
|
87
|
+
|
|
88
|
+
// [FIX] Check user's watchlistAlerts preference before creating alert
|
|
89
|
+
try {
|
|
90
|
+
const prefs = await manageNotificationPreferences(db, userCid, 'get');
|
|
91
|
+
if (!prefs.watchlistAlerts) {
|
|
92
|
+
logger.log('DEBUG', `[processAlertForPI] User ${userCid} has watchlistAlerts disabled, skipping alert`);
|
|
93
|
+
continue; // Skip this user
|
|
94
|
+
}
|
|
95
|
+
} catch (prefError) {
|
|
96
|
+
logger.log('WARN', `[processAlertForPI] Error checking watchlistAlerts preference for user ${userCid}: ${prefError.message}`);
|
|
97
|
+
// Continue anyway - fail open for important alerts
|
|
98
|
+
}
|
|
99
|
+
|
|
43
100
|
const notificationId = `alert_${Date.now()}_${userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
44
101
|
|
|
45
102
|
const notificationData = {
|
|
@@ -129,6 +186,28 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
129
186
|
|
|
130
187
|
logger.log('SUCCESS', `[processAlertForPI] Created ${notificationPromises.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
|
|
131
188
|
|
|
189
|
+
// [FIX] Mark alert as processed to prevent duplicates on backfill
|
|
190
|
+
if (isHistoricalData) {
|
|
191
|
+
try {
|
|
192
|
+
await db.collection('SignedInUsers')
|
|
193
|
+
.doc('_metadata')
|
|
194
|
+
.collection('processed_alerts')
|
|
195
|
+
.doc(`${piCid}_${computationDate}_${alertType.id}`)
|
|
196
|
+
.set({
|
|
197
|
+
piCid: Number(piCid),
|
|
198
|
+
computationDate: computationDate,
|
|
199
|
+
alertType: alertType.id,
|
|
200
|
+
alertTypeName: alertType.name,
|
|
201
|
+
processedAt: FieldValue.serverTimestamp(),
|
|
202
|
+
notificationsSent: notificationPromises.length
|
|
203
|
+
});
|
|
204
|
+
logger.log('INFO', `[processAlertForPI] Marked historical alert as processed: PI ${piCid}, date ${computationDate}, alert type ${alertType.id}`);
|
|
205
|
+
} catch (markError) {
|
|
206
|
+
logger.log('WARN', `[processAlertForPI] Failed to mark alert as processed: ${markError.message}`);
|
|
207
|
+
// Don't throw - this is non-critical
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
132
211
|
} catch (error) {
|
|
133
212
|
logger.log('ERROR', `[processAlertForPI] Error processing alert for PI ${piCid}`, error);
|
|
134
213
|
throw error;
|
|
@@ -285,8 +364,29 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
285
364
|
const isTestProbe = alertTypeId === 'TestSystemProbe';
|
|
286
365
|
const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
|
|
287
366
|
|
|
288
|
-
//
|
|
289
|
-
|
|
367
|
+
// [FIX] For TestSystemProbe, check user's testAlerts preference
|
|
368
|
+
let shouldSendAlert = false;
|
|
369
|
+
if (isTestProbe) {
|
|
370
|
+
// Check if user has testAlerts enabled in their notification preferences
|
|
371
|
+
try {
|
|
372
|
+
const { manageNotificationPreferences } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
373
|
+
const prefs = await manageNotificationPreferences(db, userCid, 'get');
|
|
374
|
+
shouldSendAlert = prefs.testAlerts === true;
|
|
375
|
+
|
|
376
|
+
if (!shouldSendAlert) {
|
|
377
|
+
logger.log('DEBUG', `[findSubscriptionsForPI] User ${userCid} has testAlerts disabled, skipping TestSystemProbe alert`);
|
|
378
|
+
}
|
|
379
|
+
} catch (prefError) {
|
|
380
|
+
logger.log('WARN', `[findSubscriptionsForPI] Error checking testAlerts preference for user ${userCid}: ${prefError.message}`);
|
|
381
|
+
// Default to not sending test alerts if we can't check preferences
|
|
382
|
+
shouldSendAlert = false;
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
// For non-test alerts, use normal alert config check
|
|
386
|
+
shouldSendAlert = isEnabled;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (shouldSendAlert) {
|
|
290
390
|
subscriptions.push({
|
|
291
391
|
userCid: userCid,
|
|
292
392
|
piCid: piCid,
|
|
@@ -18,6 +18,15 @@ const { FieldValue } = require('@google-cloud/firestore');
|
|
|
18
18
|
*/
|
|
19
19
|
async function notifyTaskEngineComplete(db, logger, userCid, requestId, username, success, errorMessage, options = {}) {
|
|
20
20
|
try {
|
|
21
|
+
// [FIX] Check user's notification preferences before sending
|
|
22
|
+
const { manageNotificationPreferences } = require('./data-fetchers/firestore.js');
|
|
23
|
+
const prefs = await manageNotificationPreferences(db, userCid, 'get');
|
|
24
|
+
|
|
25
|
+
if (!prefs.syncProcesses) {
|
|
26
|
+
logger?.log('DEBUG', `[notifyTaskEngineComplete] User ${userCid} has syncProcesses disabled, skipping notification`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
const notificationData = {
|
|
22
31
|
type: 'sync',
|
|
23
32
|
subType: 'complete',
|
|
@@ -42,6 +51,29 @@ async function notifyTaskEngineComplete(db, logger, userCid, requestId, username
|
|
|
42
51
|
.doc(requestId)
|
|
43
52
|
.set(notificationData);
|
|
44
53
|
|
|
54
|
+
// [FIX] Clean up progress notifications for this sync to stop showing loading icons
|
|
55
|
+
try {
|
|
56
|
+
const progressNotificationsQuery = await db.collection('SignedInUsers')
|
|
57
|
+
.doc(String(userCid))
|
|
58
|
+
.collection('notifications')
|
|
59
|
+
.where('metadata.requestId', '==', requestId)
|
|
60
|
+
.where('subType', '==', 'progress')
|
|
61
|
+
.get();
|
|
62
|
+
|
|
63
|
+
const batch = db.batch();
|
|
64
|
+
progressNotificationsQuery.docs.forEach(doc => {
|
|
65
|
+
batch.delete(doc.ref);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (progressNotificationsQuery.size > 0) {
|
|
69
|
+
await batch.commit();
|
|
70
|
+
logger?.log('INFO', `[notifyTaskEngineComplete] Cleaned up ${progressNotificationsQuery.size} progress notifications for request ${requestId}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (cleanupError) {
|
|
73
|
+
logger?.log('WARN', `[notifyTaskEngineComplete] Failed to clean up progress notifications: ${cleanupError.message}`);
|
|
74
|
+
// Don't throw - cleanup is non-critical
|
|
75
|
+
}
|
|
76
|
+
|
|
45
77
|
logger?.log('INFO', `[notifyTaskEngineComplete] Notification sent for user ${userCid}, request ${requestId}`);
|
|
46
78
|
} catch (error) {
|
|
47
79
|
logger?.log('WARN', `[notifyTaskEngineComplete] Failed to send notification: ${error.message}`);
|
|
@@ -61,6 +93,15 @@ async function notifyTaskEngineComplete(db, logger, userCid, requestId, username
|
|
|
61
93
|
*/
|
|
62
94
|
async function notifyTaskEngineProgress(db, logger, userCid, requestId, username, stage, dataType, options = {}) {
|
|
63
95
|
try {
|
|
96
|
+
// [FIX] Check user's notification preferences before sending
|
|
97
|
+
const { manageNotificationPreferences } = require('./data-fetchers/firestore.js');
|
|
98
|
+
const prefs = await manageNotificationPreferences(db, userCid, 'get');
|
|
99
|
+
|
|
100
|
+
if (!prefs.syncProcesses) {
|
|
101
|
+
logger?.log('DEBUG', `[notifyTaskEngineProgress] User ${userCid} has syncProcesses disabled, skipping notification`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
const stageMessages = {
|
|
65
106
|
'started': 'Data sync started',
|
|
66
107
|
'portfolio_complete': 'Portfolio data fetched',
|
|
@@ -130,6 +171,15 @@ async function notifyPIDataRefreshed(db, logger, collectionRegistry, piCid, user
|
|
|
130
171
|
*/
|
|
131
172
|
async function notifyComputationComplete(db, logger, userCid, requestId, computation, displayName, success, errorMessage, options = {}) {
|
|
132
173
|
try {
|
|
174
|
+
// [FIX] Check user's notification preferences before sending
|
|
175
|
+
const { manageNotificationPreferences } = require('./data-fetchers/firestore.js');
|
|
176
|
+
const prefs = await manageNotificationPreferences(db, userCid, 'get');
|
|
177
|
+
|
|
178
|
+
if (!prefs.userActionCompletions) {
|
|
179
|
+
logger?.log('DEBUG', `[notifyComputationComplete] User ${userCid} has userActionCompletions disabled, skipping notification`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
133
183
|
const notificationData = {
|
|
134
184
|
type: 'computation',
|
|
135
185
|
subType: 'complete',
|