bulltrackers-module 1.0.677 → 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
- const subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName, computationDate, dependencies);
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
- // If it's the probe OR the user explicitly enabled it
289
- if (isTestProbe || isEnabled) {
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',
@@ -8,6 +8,64 @@ const { IntelligentProxyManager } = require('../../core/utils/intelligent_proxy_
8
8
  const { IntelligentHeaderManager } = require('../../core/utils/intelligent_header_manager');
9
9
  const zlib = require('zlib');
10
10
 
11
+ /**
12
+ * Fetches individual user rankings data by CID
13
+ * @param {string} cid - Customer ID
14
+ * @param {object} headers - Request headers to use
15
+ * @param {object} proxyManager - ProxyManager instance
16
+ * @param {object} logger - Logger instance
17
+ * @returns {object|null} - User rankings data or null if failed
18
+ */
19
+ async function fetchIndividualUserRankings(cid, headers, proxyManager, logger) {
20
+ const individualUrl = `https://www.etoro.com/sapi/rankings/cid/${cid}/rankings/?Period=OneYearAgo`;
21
+
22
+ try {
23
+ logger.log('INFO', `[PopularInvestorFetch] Fetching individual rankings for CID: ${cid}`);
24
+
25
+ // Try with proxy first
26
+ try {
27
+ const response = await proxyManager.fetch(individualUrl, {
28
+ method: 'GET',
29
+ headers
30
+ });
31
+
32
+ if (response.ok) {
33
+ const data = await response.json();
34
+ if (data && data.Data) {
35
+ logger.log('SUCCESS', `[PopularInvestorFetch] Successfully fetched individual rankings for CID: ${cid} via proxy`);
36
+ return data.Data; // Return the Data object which matches the Items schema
37
+ }
38
+ }
39
+ } catch (proxyError) {
40
+ logger.log('WARN', `[PopularInvestorFetch] Proxy fetch failed for CID ${cid}: ${proxyError.message}`);
41
+ }
42
+
43
+ // Fallback to direct fetch
44
+ try {
45
+ const directResponse = await fetch(individualUrl, {
46
+ method: 'GET',
47
+ headers
48
+ });
49
+
50
+ if (directResponse.ok) {
51
+ const data = await directResponse.json();
52
+ if (data && data.Data) {
53
+ logger.log('SUCCESS', `[PopularInvestorFetch] Successfully fetched individual rankings for CID: ${cid} via direct fetch`);
54
+ return data.Data;
55
+ }
56
+ }
57
+ } catch (directError) {
58
+ logger.log('WARN', `[PopularInvestorFetch] Direct fetch failed for CID ${cid}: ${directError.message}`);
59
+ }
60
+
61
+ logger.log('ERROR', `[PopularInvestorFetch] Failed to fetch individual rankings for CID: ${cid} from all sources`);
62
+ return null;
63
+ } catch (error) {
64
+ logger.log('ERROR', `[PopularInvestorFetch] Error fetching individual rankings for CID ${cid}`, { errorMessage: error.message });
65
+ return null;
66
+ }
67
+ }
68
+
11
69
  /**
12
70
  * Fetches the top Popular Investors and stores the raw result in Firestore.
13
71
  * @param {object} dependencies - Contains db, logger.
@@ -117,6 +175,90 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
117
175
  await headerManager.flushPerformanceUpdates();
118
176
  }
119
177
 
178
+ // 5.5. Check for missing users from master list and fetch them individually
179
+ if (data && data.Items && Array.isArray(data.Items)) {
180
+ try {
181
+ logger.log('INFO', '[PopularInvestorFetch] Checking for missing users from master list...');
182
+
183
+ // Get master list path
184
+ let masterListPath = 'system_state/popular_investor_master_list';
185
+ if (collectionRegistry && collectionRegistry.getCollectionPath) {
186
+ try {
187
+ const registryPath = collectionRegistry.getCollectionPath('system', 'popularInvestorMasterList', {});
188
+ masterListPath = registryPath;
189
+ } catch (e) {
190
+ logger.log('WARN', `[PopularInvestorFetch] Failed to get master list path from registry, using default: ${e.message}`);
191
+ }
192
+ }
193
+
194
+ const masterListRef = db.doc(masterListPath);
195
+ const masterListDoc = await masterListRef.get();
196
+
197
+ if (masterListDoc.exists) {
198
+ const masterListData = masterListDoc.data();
199
+ const masterInvestors = masterListData.investors || {};
200
+
201
+ // Build a Set of CIDs from the fetched data for fast lookup
202
+ const fetchedCids = new Set(data.Items.map(item => String(item.CustomerId)));
203
+
204
+ // Identify missing CIDs
205
+ const masterCids = Object.keys(masterInvestors);
206
+ const missingCids = masterCids.filter(cid => !fetchedCids.has(cid));
207
+
208
+ if (missingCids.length > 0) {
209
+ logger.log('INFO', `[PopularInvestorFetch] Found ${missingCids.length} missing users from master list. Fetching individually...`);
210
+
211
+ // Prepare headers for individual fetches
212
+ const requestHeaders = {
213
+ 'Accept': 'application/json',
214
+ 'Referer': 'https://www.etoro.com/',
215
+ ...(await headerManager.selectHeader()).header
216
+ };
217
+
218
+ // Fetch missing users with rate limiting
219
+ const missingUserData = [];
220
+ let successCount = 0;
221
+ let failureCount = 0;
222
+
223
+ for (const cid of missingCids) {
224
+ const userData = await fetchIndividualUserRankings(cid, requestHeaders, proxyManager, logger);
225
+
226
+ if (userData) {
227
+ missingUserData.push(userData);
228
+ successCount++;
229
+ } else {
230
+ failureCount++;
231
+ logger.log('WARN', `[PopularInvestorFetch] Failed to fetch data for missing user CID: ${cid} (${masterInvestors[cid].username})`);
232
+ }
233
+
234
+ // Add small delay between requests to avoid rate limiting
235
+ if (missingCids.length > 10 && missingCids.indexOf(cid) < missingCids.length - 1) {
236
+ await new Promise(resolve => setTimeout(resolve, 200)); // 200ms delay
237
+ }
238
+ }
239
+
240
+ // Append successfully fetched missing users to the main data
241
+ if (missingUserData.length > 0) {
242
+ data.Items.push(...missingUserData);
243
+ data.TotalRows += missingUserData.length;
244
+ logger.log('SUCCESS', `[PopularInvestorFetch] Successfully fetched ${successCount}/${missingCids.length} missing users. Updated Items array from ${data.Items.length - missingUserData.length} to ${data.Items.length} users.`);
245
+ }
246
+
247
+ if (failureCount > 0) {
248
+ logger.log('WARN', `[PopularInvestorFetch] Failed to fetch ${failureCount}/${missingCids.length} missing users.`);
249
+ }
250
+ } else {
251
+ logger.log('INFO', '[PopularInvestorFetch] All users from master list are present in the main fetch. No missing users to fetch individually.');
252
+ }
253
+ } else {
254
+ logger.log('INFO', '[PopularInvestorFetch] Master list document does not exist yet. Skipping missing user check.');
255
+ }
256
+ } catch (missingUserError) {
257
+ logger.log('WARN', `[PopularInvestorFetch] Error while checking/fetching missing users: ${missingUserError.message}. Continuing with main fetch data.`);
258
+ // Non-critical error, continue with whatever data we have
259
+ }
260
+ }
261
+
120
262
  // 6. Final Validation & Storage
121
263
  if (data && data.Items && Array.isArray(data.Items)) {
122
264
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.677",
3
+ "version": "1.0.679",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [