bulltrackers-module 1.0.777 → 1.0.779
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/helpers/alert_helpers.js +114 -90
- package/functions/alert-system/helpers/alert_manifest_loader.js +88 -99
- package/functions/alert-system/index.js +81 -138
- package/functions/alert-system/tests/stage1-alert-manifest.test.js +94 -0
- package/functions/alert-system/tests/stage2-alert-metadata.test.js +93 -0
- package/functions/alert-system/tests/stage3-alert-handler.test.js +79 -0
- package/functions/api-v2/helpers/data-fetchers/firestore.js +613 -478
- package/functions/api-v2/routes/popular_investors.js +7 -7
- package/functions/api-v2/routes/profile.js +2 -1
- package/functions/api-v2/tests/stage4-profile-paths.test.js +52 -0
- package/functions/api-v2/tests/stage5-aum-bigquery.test.js +81 -0
- package/functions/api-v2/tests/stage7-pi-page-views.test.js +55 -0
- package/functions/api-v2/tests/stage8-watchlist-membership.test.js +49 -0
- package/functions/api-v2/tests/stage9-user-alert-settings.test.js +81 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +104 -81
- package/functions/computation-system-v2/computations/NewSectorExposure.js +7 -7
- package/functions/computation-system-v2/computations/NewSocialPost.js +6 -6
- package/functions/computation-system-v2/computations/PositionInvestedIncrease.js +11 -11
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +1 -1
- package/functions/computation-system-v2/config/bulltrackers.config.js +8 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +1 -0
- package/functions/computation-system-v2/handlers/scheduler.js +15 -24
- package/functions/core/utils/bigquery_utils.js +32 -0
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
23
23
|
// [FIX] Check if computation date is earlier than today (backfill protection)
|
|
24
24
|
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
25
25
|
const isHistoricalData = computationDate < today;
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
// If it's historical data, check if this alert already exists
|
|
28
28
|
if (isHistoricalData) {
|
|
29
29
|
// Check if we've already created alerts for this PI/date/alertType combination
|
|
@@ -35,26 +35,26 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
35
35
|
.where('alertType', '==', alertType.id)
|
|
36
36
|
.limit(1)
|
|
37
37
|
.get();
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
if (!existingAlertsSnapshot.empty) {
|
|
40
40
|
logger.log('INFO', `[processAlertForPI] Skipping duplicate alert for historical data: PI ${piCid}, date ${computationDate}, alert type ${alertType.id}`);
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
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.`);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
// 1. Get PI username from rankings or subscriptions
|
|
48
48
|
const piUsername = await getPIUsername(db, piCid);
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
// 2. Find all users subscribed to this PI and alert type
|
|
51
51
|
// Use computationName (e.g., 'RiskScoreIncrease') to map to alertConfig keys
|
|
52
52
|
let subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName, computationDate, dependencies);
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
// [FIX] If it's historical data, only send to developer accounts
|
|
55
55
|
if (isHistoricalData) {
|
|
56
56
|
const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
// Filter subscriptions to only include developers
|
|
59
59
|
const devSubscriptions = [];
|
|
60
60
|
for (const subscription of subscriptions) {
|
|
@@ -63,32 +63,32 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
63
63
|
devSubscriptions.push(subscription);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
subscriptions = devSubscriptions;
|
|
68
68
|
logger.log('INFO', `[processAlertForPI] Historical data: Filtered to ${subscriptions.length} developer subscriptions for PI ${piCid}, alert type ${alertType.id}`);
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
if (subscriptions.length === 0) {
|
|
72
72
|
logger.log('INFO', `[processAlertForPI] No subscriptions found for PI ${piCid}, alert type ${alertType.id}`);
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
logger.log('INFO', `[processAlertForPI] Processing ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertType.id}`);
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
// 3. Generate alert message
|
|
79
79
|
const alertMessage = generateAlertMessage(alertType, piUsername, computationMetadata);
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
// 4. Create notifications for each subscribed user (using CID and collection registry)
|
|
82
82
|
const { collectionRegistry } = dependencies;
|
|
83
83
|
const config = dependencies.config || {};
|
|
84
84
|
const notificationPromises = [];
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
// Check watchlistAlerts preference for each user
|
|
87
87
|
const { manageNotificationPreferences } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
for (const subscription of subscriptions) {
|
|
90
90
|
const userCid = subscription.userCid;
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
// [FIX] Check user's watchlistAlerts preference before creating alert
|
|
93
93
|
try {
|
|
94
94
|
const prefs = await manageNotificationPreferences(db, userCid, 'get');
|
|
@@ -100,16 +100,16 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
100
100
|
logger.log('WARN', `[processAlertForPI] Error checking watchlistAlerts preference for user ${userCid}: ${prefError.message}`);
|
|
101
101
|
// Continue anyway - fail open for important alerts
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
// [NEW] Evaluate dynamic conditions based on user's subscription mode
|
|
105
105
|
// Check if user is a developer for bypass
|
|
106
106
|
const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
107
107
|
const isDev = await isDeveloper(db, String(userCid));
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
// Get user's configuration from subscription
|
|
110
110
|
const userDynamicConfig = subscription.dynamicConfig?.[alertType.configKey] || {};
|
|
111
111
|
const userUseDynamic = subscription.useDynamic?.[alertType.configKey] === true;
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
// Evaluate conditions (will pass if static mode or no conditions)
|
|
114
114
|
const evaluation = evaluateDynamicConditions(
|
|
115
115
|
alertType,
|
|
@@ -119,20 +119,20 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
119
119
|
isDev,
|
|
120
120
|
logger
|
|
121
121
|
);
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
if (!evaluation.passes) {
|
|
124
124
|
logger.log('DEBUG', `[processAlertForPI] User ${userCid} dynamic conditions not met for ${alertType.id}: ${evaluation.reason}`);
|
|
125
125
|
continue; // Skip this user
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
if (evaluation.developerBypass) {
|
|
129
129
|
logger.log('INFO', `[processAlertForPI] Developer ${userCid} bypass: ${evaluation.reason}`);
|
|
130
130
|
} else {
|
|
131
131
|
logger.log('DEBUG', `[processAlertForPI] User ${userCid} alert ${alertType.id} in ${evaluation.mode} mode`);
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
const notificationId = `alert_${Date.now()}_${userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
const notificationData = {
|
|
137
137
|
id: notificationId,
|
|
138
138
|
type: 'watchlistAlerts', // Use watchlistAlerts type for watchlist-based alerts
|
|
@@ -157,7 +157,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
157
157
|
...(computationMetadata || {})
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
|
-
|
|
160
|
+
|
|
161
161
|
// Write to alerts collection (not notifications) - alerts are separate from system notifications
|
|
162
162
|
// Write directly to new path: SignedInUsers/{userCid}/alerts/{alertId}
|
|
163
163
|
const alertData = {
|
|
@@ -176,7 +176,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
176
176
|
computationName: alertType.computationName,
|
|
177
177
|
...(computationMetadata || {})
|
|
178
178
|
};
|
|
179
|
-
|
|
179
|
+
|
|
180
180
|
// Combined promise: write to Firestore AND send FCM push notification
|
|
181
181
|
const writeAndPushPromise = (async () => {
|
|
182
182
|
// 1. Write to Firestore first (this is the source of truth)
|
|
@@ -185,7 +185,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
185
185
|
.collection('alerts')
|
|
186
186
|
.doc(notificationId)
|
|
187
187
|
.set(alertData);
|
|
188
|
-
|
|
188
|
+
|
|
189
189
|
// 2. Send FCM push notification (non-blocking, don't fail if push fails)
|
|
190
190
|
// This enables notifications even when user is offline
|
|
191
191
|
try {
|
|
@@ -194,46 +194,70 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
194
194
|
// Log but don't throw - FCM failure shouldn't fail the alert
|
|
195
195
|
logger.log('WARN', `[processAlertForPI] FCM push failed for CID ${userCid}: ${fcmError.message}`);
|
|
196
196
|
}
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
return { userCid, success: true };
|
|
199
199
|
})().catch(err => {
|
|
200
200
|
logger.log('ERROR', `[processAlertForPI] Failed to write alert for CID ${userCid}: ${err.message}`, err);
|
|
201
201
|
throw err; // Re-throw so we know if writes are failing
|
|
202
202
|
});
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
notificationPromises.push(writeAndPushPromise);
|
|
205
205
|
}
|
|
206
|
-
|
|
206
|
+
|
|
207
207
|
// Wait for all notifications to be written and push notifications sent
|
|
208
208
|
await Promise.all(notificationPromises);
|
|
209
|
-
|
|
209
|
+
|
|
210
|
+
// 4b. Write to BigQuery pi_alert_history (for computation system and analytics)
|
|
211
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
212
|
+
try {
|
|
213
|
+
const { ensurePIAlertHistoryTable, insertRowsWithMerge } = require('../../core/utils/bigquery_utils');
|
|
214
|
+
await ensurePIAlertHistoryTable(logger);
|
|
215
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
216
|
+
const now = new Date().toISOString();
|
|
217
|
+
const row = {
|
|
218
|
+
date: computationDate,
|
|
219
|
+
pi_id: Number(piCid),
|
|
220
|
+
alert_type: alertType.computationName || alertType.id,
|
|
221
|
+
triggered: true,
|
|
222
|
+
trigger_count: subscriptions.length,
|
|
223
|
+
triggered_for: subscriptions.map(s => String(s.userCid)),
|
|
224
|
+
metadata: computationMetadata && typeof computationMetadata === 'object' ? computationMetadata : {},
|
|
225
|
+
last_triggered: now,
|
|
226
|
+
last_updated: now
|
|
227
|
+
};
|
|
228
|
+
await insertRowsWithMerge(datasetId, 'pi_alert_history', [row], ['date', 'pi_id', 'alert_type'], logger);
|
|
229
|
+
} catch (bqErr) {
|
|
230
|
+
logger.log('WARN', `[processAlertForPI] pi_alert_history write failed: ${bqErr.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
210
234
|
// 5. Notify the PI themselves if they are a signed-in user (Optional feature)
|
|
211
235
|
await notifyPIIfSignedIn(db, logger, piCid, alertType, subscriptions.length);
|
|
212
236
|
|
|
213
237
|
// 6. Update global rootdata collection for computation system
|
|
214
238
|
// (Wrap in try-catch to prevent crashing the alert if metrics fail)
|
|
215
239
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
const { runRootDataIndexer } = require('../../root-data-indexer/index');
|
|
241
|
+
const triggeredUserCids = subscriptions.map(s => s.userCid);
|
|
242
|
+
|
|
243
|
+
// Update alert history root data by running root data indexer for the specific date
|
|
244
|
+
// The indexer will detect and update PIAlertHistoryData availability
|
|
245
|
+
const indexerConfig = {
|
|
246
|
+
availabilityCollection: 'root_data_availability',
|
|
247
|
+
targetDate: computationDate,
|
|
248
|
+
collections: {
|
|
249
|
+
piAlertHistory: 'PIAlertHistoryData'
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await runRootDataIndexer(indexerConfig, { db, logger });
|
|
254
|
+
logger.log('INFO', `[processAlertForPI] Updated root data indexer for date ${computationDate}`);
|
|
231
255
|
} catch (e) {
|
|
232
|
-
|
|
256
|
+
logger.log('WARN', `[processAlertForPI] Failed to update history rootdata: ${e.message}`);
|
|
233
257
|
}
|
|
234
|
-
|
|
258
|
+
|
|
235
259
|
logger.log('SUCCESS', `[processAlertForPI] Created ${notificationPromises.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
|
|
236
|
-
|
|
260
|
+
|
|
237
261
|
// [FIX] Mark alert as processed to prevent duplicates on backfill
|
|
238
262
|
if (isHistoricalData) {
|
|
239
263
|
try {
|
|
@@ -255,7 +279,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
255
279
|
// Don't throw - this is non-critical
|
|
256
280
|
}
|
|
257
281
|
}
|
|
258
|
-
|
|
282
|
+
|
|
259
283
|
} catch (error) {
|
|
260
284
|
logger.log('ERROR', `[processAlertForPI] Error processing alert for PI ${piCid}`, error);
|
|
261
285
|
throw error;
|
|
@@ -269,24 +293,24 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
269
293
|
*/
|
|
270
294
|
async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computationDate, dependencies = {}) {
|
|
271
295
|
const subscriptions = [];
|
|
272
|
-
|
|
296
|
+
|
|
273
297
|
// [DYNAMIC] Get configKey from alertType metadata instead of hardcoded mapping
|
|
274
298
|
// The alertType is passed in dependencies and contains the configKey from computation metadata
|
|
275
299
|
const alertType = dependencies.alertType;
|
|
276
300
|
const configKey = alertType?.configKey;
|
|
277
|
-
|
|
301
|
+
|
|
278
302
|
if (!configKey) {
|
|
279
303
|
logger.log('WARN', `[findSubscriptionsForPI] No configKey found for alert type: ${alertTypeId}`);
|
|
280
304
|
return subscriptions;
|
|
281
305
|
}
|
|
282
|
-
|
|
306
|
+
|
|
283
307
|
// Check for developer accounts with pretendSubscribedToAllAlerts flag enabled
|
|
284
308
|
// This allows developers to test the alert system without manually configuring subscriptions
|
|
285
309
|
try {
|
|
286
310
|
const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
287
311
|
const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
288
312
|
const config = dependencies.config || {};
|
|
289
|
-
|
|
313
|
+
|
|
290
314
|
// Get PI username from master list
|
|
291
315
|
let piUsername = `PI-${piCid}`;
|
|
292
316
|
try {
|
|
@@ -295,7 +319,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
295
319
|
} catch (e) {
|
|
296
320
|
// PI not in master list, use fallback
|
|
297
321
|
}
|
|
298
|
-
|
|
322
|
+
|
|
299
323
|
// Default alert config with all alert types enabled (for dev override)
|
|
300
324
|
const allAlertsEnabledConfig = {
|
|
301
325
|
increasedRisk: true,
|
|
@@ -307,22 +331,22 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
307
331
|
behavioralAnomaly: true,
|
|
308
332
|
testSystemProbe: true // Test alerts for developers
|
|
309
333
|
};
|
|
310
|
-
|
|
334
|
+
|
|
311
335
|
// Check all developer accounts
|
|
312
336
|
const devOverridesCollection = config.devOverridesCollection || 'dev_overrides';
|
|
313
337
|
const devOverridesSnapshot = await db.collection(devOverridesCollection).get();
|
|
314
|
-
|
|
338
|
+
|
|
315
339
|
for (const devOverrideDoc of devOverridesSnapshot.docs) {
|
|
316
340
|
const devUserCid = Number(devOverrideDoc.id);
|
|
317
|
-
|
|
341
|
+
|
|
318
342
|
// Verify this is actually a developer account (security check) - using api-v2 helper
|
|
319
343
|
const isDev = await isDeveloper(db, String(devUserCid));
|
|
320
344
|
if (!isDev) {
|
|
321
345
|
continue;
|
|
322
346
|
}
|
|
323
|
-
|
|
347
|
+
|
|
324
348
|
const devOverrideData = devOverrideDoc.data();
|
|
325
|
-
|
|
349
|
+
|
|
326
350
|
// Check if this developer has the pretendSubscribedToAllAlerts flag enabled
|
|
327
351
|
if (devOverrideData.enabled === true && devOverrideData.pretendSubscribedToAllAlerts === true) {
|
|
328
352
|
// Add this developer as a subscription for this PI and alert type
|
|
@@ -334,7 +358,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
334
358
|
watchlistName: 'Dev Override - All Alerts',
|
|
335
359
|
alertConfig: allAlertsEnabledConfig
|
|
336
360
|
});
|
|
337
|
-
|
|
361
|
+
|
|
338
362
|
logger.log('INFO', `[findSubscriptionsForPI] DEV OVERRIDE: Added developer ${devUserCid} to subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
|
|
339
363
|
}
|
|
340
364
|
}
|
|
@@ -342,19 +366,19 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
342
366
|
// Don't fail the entire function if dev override check fails
|
|
343
367
|
logger.log('WARN', `[findSubscriptionsForPI] Error checking dev overrides: ${error.message}`);
|
|
344
368
|
}
|
|
345
|
-
|
|
369
|
+
|
|
346
370
|
// Step 1: Load WatchlistMembershipData/{date} to find which users have this PI in their watchlist
|
|
347
371
|
const piCidStr = String(piCid);
|
|
348
372
|
let userCids = [];
|
|
349
|
-
|
|
373
|
+
|
|
350
374
|
try {
|
|
351
375
|
const membershipRef = db.collection('WatchlistMembershipData').doc(computationDate);
|
|
352
376
|
const membershipDoc = await membershipRef.get();
|
|
353
|
-
|
|
377
|
+
|
|
354
378
|
if (membershipDoc.exists) {
|
|
355
379
|
const membershipData = membershipDoc.data();
|
|
356
380
|
const piMembership = membershipData[piCidStr];
|
|
357
|
-
|
|
381
|
+
|
|
358
382
|
if (piMembership && piMembership.users && Array.isArray(piMembership.users)) {
|
|
359
383
|
userCids = piMembership.users.map(cid => String(cid));
|
|
360
384
|
logger.log('INFO', `[findSubscriptionsForPI] Found ${userCids.length} users with PI ${piCid} in watchlist from WatchlistMembershipData/${computationDate}`);
|
|
@@ -372,29 +396,29 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
372
396
|
// Return subscriptions array which may contain dev overrides
|
|
373
397
|
return subscriptions;
|
|
374
398
|
}
|
|
375
|
-
|
|
399
|
+
|
|
376
400
|
// Step 2: For each user, read their watchlists from SignedInUsers/{cid}/watchlists
|
|
377
401
|
// Read directly from new path (no migration needed)
|
|
378
402
|
for (const userCidStr of userCids) {
|
|
379
403
|
try {
|
|
380
404
|
const userCid = Number(userCidStr);
|
|
381
|
-
|
|
405
|
+
|
|
382
406
|
// Read all watchlists for this user from new path
|
|
383
407
|
const watchlistsSnapshot = await db.collection('SignedInUsers')
|
|
384
408
|
.doc(String(userCid))
|
|
385
409
|
.collection('watchlists')
|
|
386
410
|
.get();
|
|
387
|
-
|
|
411
|
+
|
|
388
412
|
if (watchlistsSnapshot.empty) {
|
|
389
413
|
continue;
|
|
390
414
|
}
|
|
391
|
-
|
|
415
|
+
|
|
392
416
|
// Get watchlists from snapshot
|
|
393
417
|
const watchlists = watchlistsSnapshot.docs.map(doc => ({
|
|
394
418
|
id: doc.id,
|
|
395
419
|
...doc.data()
|
|
396
420
|
}));
|
|
397
|
-
|
|
421
|
+
|
|
398
422
|
// Step 3: Check each watchlist for the PI and alert config
|
|
399
423
|
for (const watchlistData of watchlists) {
|
|
400
424
|
if (watchlistData.type === 'static' && watchlistData.items && Array.isArray(watchlistData.items)) {
|
|
@@ -403,12 +427,12 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
403
427
|
// Check if this alert type is enabled
|
|
404
428
|
const isTestProbe = alertTypeId === 'TestSystemProbe';
|
|
405
429
|
const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
|
|
406
|
-
|
|
430
|
+
|
|
407
431
|
// [FIX] Check if this is a test alert (respect testAlerts preference)
|
|
408
432
|
// Load alert type from dependencies to check isTest flag
|
|
409
433
|
const alertType = dependencies.alertType;
|
|
410
434
|
const isTestAlert = alertType && alertType.isTest === true;
|
|
411
|
-
|
|
435
|
+
|
|
412
436
|
let shouldSendAlert = false;
|
|
413
437
|
if (isTestAlert) {
|
|
414
438
|
// Check if user has testAlerts enabled in their notification preferences
|
|
@@ -416,7 +440,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
416
440
|
const { manageNotificationPreferences } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
417
441
|
const prefs = await manageNotificationPreferences(db, userCid, 'get');
|
|
418
442
|
shouldSendAlert = prefs.testAlerts === true;
|
|
419
|
-
|
|
443
|
+
|
|
420
444
|
if (!shouldSendAlert) {
|
|
421
445
|
logger.log('DEBUG', `[findSubscriptionsForPI] User ${userCid} has testAlerts disabled, skipping test alert ${alertTypeId}`);
|
|
422
446
|
}
|
|
@@ -429,7 +453,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
429
453
|
// For non-test alerts, use normal alert config check
|
|
430
454
|
shouldSendAlert = isEnabled;
|
|
431
455
|
}
|
|
432
|
-
|
|
456
|
+
|
|
433
457
|
if (shouldSendAlert) {
|
|
434
458
|
subscriptions.push({
|
|
435
459
|
userCid: userCid,
|
|
@@ -451,7 +475,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
451
475
|
continue;
|
|
452
476
|
}
|
|
453
477
|
}
|
|
454
|
-
|
|
478
|
+
|
|
455
479
|
logger.log('INFO', `[findSubscriptionsForPI] Found ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
|
|
456
480
|
return subscriptions;
|
|
457
481
|
}
|
|
@@ -477,22 +501,22 @@ async function getPIUsername(db, piCid) {
|
|
|
477
501
|
// Try to get from master list first (single source of truth) - using api-v2 helper
|
|
478
502
|
const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
479
503
|
const piData = await fetchPopularInvestorMasterList(db, String(piCid));
|
|
480
|
-
|
|
504
|
+
|
|
481
505
|
if (piData && piData.username) {
|
|
482
506
|
return piData.username;
|
|
483
507
|
}
|
|
484
|
-
|
|
508
|
+
|
|
485
509
|
// Fallback: try to get from any subscription
|
|
486
510
|
const subscriptionsSnapshot = await db.collection('watchlist_subscriptions')
|
|
487
511
|
.where('piCid', '==', Number(piCid))
|
|
488
512
|
.limit(1)
|
|
489
513
|
.get();
|
|
490
|
-
|
|
514
|
+
|
|
491
515
|
if (!subscriptionsSnapshot.empty) {
|
|
492
516
|
const subData = subscriptionsSnapshot.docs[0].data();
|
|
493
517
|
return subData.piUsername || `PI-${piCid}`;
|
|
494
518
|
}
|
|
495
|
-
|
|
519
|
+
|
|
496
520
|
return `PI-${piCid}`;
|
|
497
521
|
} catch (error) {
|
|
498
522
|
return `PI-${piCid}`;
|
|
@@ -506,14 +530,14 @@ async function notifyPIIfSignedIn(db, logger, piCid, alertType, alertCount) {
|
|
|
506
530
|
try {
|
|
507
531
|
const userRef = db.collection('signedInUsers').doc(String(piCid));
|
|
508
532
|
const userDoc = await userRef.get();
|
|
509
|
-
|
|
510
|
-
if (!userDoc.exists) return;
|
|
533
|
+
|
|
534
|
+
if (!userDoc.exists) return;
|
|
511
535
|
|
|
512
536
|
const notificationRef = db.collection('signedInUsers')
|
|
513
537
|
.doc(String(piCid))
|
|
514
538
|
.collection('user_alerts_metrics') // Changed from user_alerts/triggered to avoid collision
|
|
515
539
|
.doc(`trigger_${Date.now()}_${alertType.id}`);
|
|
516
|
-
|
|
540
|
+
|
|
517
541
|
await notificationRef.set({
|
|
518
542
|
alertType: alertType.id,
|
|
519
543
|
alertTypeName: alertType.name,
|
|
@@ -522,7 +546,7 @@ async function notifyPIIfSignedIn(db, logger, piCid, alertType, alertCount) {
|
|
|
522
546
|
computationDate: new Date().toISOString().split('T')[0],
|
|
523
547
|
createdAt: FieldValue.serverTimestamp()
|
|
524
548
|
});
|
|
525
|
-
|
|
549
|
+
|
|
526
550
|
logger.log('INFO', `[notifyPIIfSignedIn] Notified PI ${piCid} that ${alertCount} users received ${alertType.id} alert`);
|
|
527
551
|
} catch (error) {
|
|
528
552
|
// Silent fail - non-critical
|
|
@@ -573,12 +597,12 @@ function tryDecompress(data) {
|
|
|
573
597
|
function readComputationResults(docData) {
|
|
574
598
|
try {
|
|
575
599
|
const decompressed = tryDecompress(docData);
|
|
576
|
-
|
|
600
|
+
|
|
577
601
|
if (decompressed && typeof decompressed === 'object') {
|
|
578
602
|
if (decompressed.cids && Array.isArray(decompressed.cids)) {
|
|
579
603
|
const userDataKeys = Object.keys(decompressed)
|
|
580
604
|
.filter(key => key !== 'cids' && key !== 'metadata' && /^\d+$/.test(key));
|
|
581
|
-
|
|
605
|
+
|
|
582
606
|
if (userDataKeys.length > 0) {
|
|
583
607
|
return {
|
|
584
608
|
cids: decompressed.cids,
|
|
@@ -595,11 +619,11 @@ function readComputationResults(docData) {
|
|
|
595
619
|
};
|
|
596
620
|
}
|
|
597
621
|
}
|
|
598
|
-
|
|
622
|
+
|
|
599
623
|
const cids = Object.keys(decompressed)
|
|
600
624
|
.filter(key => /^\d+$/.test(key))
|
|
601
625
|
.map(key => Number(key));
|
|
602
|
-
|
|
626
|
+
|
|
603
627
|
if (cids.length > 0) {
|
|
604
628
|
return {
|
|
605
629
|
cids: cids,
|
|
@@ -633,14 +657,14 @@ async function readComputationResultsWithShards(db, docData, docRef, logger = nu
|
|
|
633
657
|
try {
|
|
634
658
|
const bucketName = docData.gcsBucket || docData.gcsUri.split('/')[2];
|
|
635
659
|
const fileName = docData.gcsPath || docData.gcsUri.split('/').slice(3).join('/');
|
|
636
|
-
|
|
660
|
+
|
|
637
661
|
if (logger) {
|
|
638
662
|
logger.log('INFO', `[AlertSystem] Reading computation results from GCS: ${fileName}`);
|
|
639
663
|
}
|
|
640
|
-
|
|
664
|
+
|
|
641
665
|
// Stream download is memory efficient for large files
|
|
642
666
|
const [fileContent] = await storage.bucket(bucketName).file(fileName).download();
|
|
643
|
-
|
|
667
|
+
|
|
644
668
|
// Assume Gzip (as writer does it), if fails try plain
|
|
645
669
|
let decompressedData;
|
|
646
670
|
try {
|
|
@@ -649,7 +673,7 @@ async function readComputationResultsWithShards(db, docData, docRef, logger = nu
|
|
|
649
673
|
// Fallback for uncompressed GCS files
|
|
650
674
|
decompressedData = JSON.parse(fileContent.toString('utf8'));
|
|
651
675
|
}
|
|
652
|
-
|
|
676
|
+
|
|
653
677
|
// Process the decompressed data through readComputationResults
|
|
654
678
|
return readComputationResults(decompressedData);
|
|
655
679
|
} catch (gcsErr) {
|
|
@@ -666,7 +690,7 @@ async function readComputationResultsWithShards(db, docData, docRef, logger = nu
|
|
|
666
690
|
if (docData._sharded === true && docData._shardCount) {
|
|
667
691
|
const shardsCol = docRef.collection('_shards');
|
|
668
692
|
const shardsSnapshot = await shardsCol.get();
|
|
669
|
-
|
|
693
|
+
|
|
670
694
|
if (!shardsSnapshot.empty) {
|
|
671
695
|
let mergedData = {};
|
|
672
696
|
for (const shardDoc of shardsSnapshot.docs) {
|