bulltrackers-module 1.0.630 → 1.0.632

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.
@@ -6,7 +6,7 @@
6
6
  const { FieldValue } = require('@google-cloud/firestore');
7
7
  const zlib = require('zlib');
8
8
  const { getAlertTypeByComputation, generateAlertMessage } = require('./alert_type_registry');
9
- const { writeWithMigration } = require('../../old-generic-api/user-api/helpers/core/path_resolution_helpers');
9
+ // Migration helpers removed - write directly to new path
10
10
 
11
11
  /**
12
12
  * Process alerts for a specific PI from computation results
@@ -65,39 +65,30 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
65
65
  };
66
66
 
67
67
  // Write to alerts collection (not notifications) - alerts are separate from system notifications
68
- // Use writeWithMigration to write to new path (with legacy fallback)
69
- const writePromise = writeWithMigration(
70
- db,
71
- 'signedInUsers',
72
- 'alerts', // Write to alerts collection, not notifications
73
- { cid: String(userCid) },
74
- {
75
- // Alert-specific format (simplified from notification format)
76
- alertId: notificationId,
77
- piCid: Number(piCid),
78
- piUsername: piUsername,
79
- alertType: alertType.id,
80
- alertTypeName: alertType.name,
81
- message: alertMessage,
82
- severity: alertType.severity,
83
- watchlistId: subscription.watchlistId,
84
- watchlistName: subscription.watchlistName,
85
- read: false,
86
- createdAt: FieldValue.serverTimestamp(),
87
- computationDate: computationDate,
88
- computationName: alertType.computationName,
89
- ...(computationMetadata || {})
90
- },
91
- {
92
- isCollection: true,
93
- merge: false,
94
- dataType: 'alerts',
95
- documentId: notificationId,
96
- dualWrite: false, // Disable dual write - we're fully migrated to new path
97
- config,
98
- collectionRegistry
99
- }
100
- ).catch(err => {
68
+ // Write directly to new path: SignedInUsers/{userCid}/alerts/{alertId}
69
+ const alertData = {
70
+ alertId: notificationId,
71
+ piCid: Number(piCid),
72
+ piUsername: piUsername,
73
+ alertType: alertType.id,
74
+ alertTypeName: alertType.name,
75
+ message: alertMessage,
76
+ severity: alertType.severity,
77
+ watchlistId: subscription.watchlistId,
78
+ watchlistName: subscription.watchlistName,
79
+ read: false,
80
+ createdAt: FieldValue.serverTimestamp(),
81
+ computationDate: computationDate,
82
+ computationName: alertType.computationName,
83
+ ...(computationMetadata || {})
84
+ };
85
+
86
+ const writePromise = db.collection('SignedInUsers')
87
+ .doc(String(userCid))
88
+ .collection('alerts')
89
+ .doc(notificationId)
90
+ .set(alertData)
91
+ .catch(err => {
101
92
  logger.log('ERROR', `[processAlertForPI] Failed to write alert for CID ${userCid}: ${err.message}`, err);
102
93
  throw err; // Re-throw so we know if writes are failing
103
94
  });
@@ -114,9 +105,21 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
114
105
  // 6. Update global rootdata collection for computation system
115
106
  // (Wrap in try-catch to prevent crashing the alert if metrics fail)
116
107
  try {
117
- const { updateAlertHistoryRootData } = require('../../old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers');
108
+ const { runRootDataIndexer } = require('../../root-data-indexer/index');
118
109
  const triggeredUserCids = subscriptions.map(s => s.userCid);
119
- await updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids);
110
+
111
+ // Update alert history root data by running root data indexer for the specific date
112
+ // The indexer will detect and update PIAlertHistoryData availability
113
+ const indexerConfig = {
114
+ availabilityCollection: 'root_data_availability',
115
+ targetDate: computationDate,
116
+ collections: {
117
+ piAlertHistory: 'PIAlertHistoryData'
118
+ }
119
+ };
120
+
121
+ await runRootDataIndexer(indexerConfig, { db, logger });
122
+ logger.log('INFO', `[processAlertForPI] Updated root data indexer for date ${computationDate}`);
120
123
  } catch (e) {
121
124
  logger.log('WARN', `[processAlertForPI] Failed to update history rootdata: ${e.message}`);
122
125
  }
@@ -158,14 +161,18 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
158
161
  // Check for developer accounts with pretendSubscribedToAllAlerts flag enabled
159
162
  // This allows developers to test the alert system without manually configuring subscriptions
160
163
  try {
161
- const { getDevOverride, isDeveloperAccount } = require('../../old-generic-api/user-api/helpers/dev/dev_helpers');
162
- const { getPIMasterList } = require('../../old-generic-api/user-api/helpers/core/user_status_helpers');
164
+ const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
165
+ const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
163
166
  const config = dependencies.config || {};
164
- const collectionRegistry = dependencies.collectionRegistry || null;
165
167
 
166
- // Get all popular investors from master list for username lookup
167
- const allInvestors = await getPIMasterList(db, collectionRegistry, logger);
168
- const piUsername = allInvestors[String(piCid)]?.username || `PI-${piCid}`;
168
+ // Get PI username from master list
169
+ let piUsername = `PI-${piCid}`;
170
+ try {
171
+ const piData = await fetchPopularInvestorMasterList(db, String(piCid));
172
+ piUsername = piData.username || piUsername;
173
+ } catch (e) {
174
+ // PI not in master list, use fallback
175
+ }
169
176
 
170
177
  // Default alert config with all alert types enabled (for dev override)
171
178
  const allAlertsEnabledConfig = {
@@ -186,8 +193,9 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
186
193
  for (const devOverrideDoc of devOverridesSnapshot.docs) {
187
194
  const devUserCid = Number(devOverrideDoc.id);
188
195
 
189
- // Verify this is actually a developer account (security check)
190
- if (!isDeveloperAccount(devUserCid)) {
196
+ // Verify this is actually a developer account (security check) - using api-v2 helper
197
+ const isDev = await isDeveloper(db, String(devUserCid));
198
+ if (!isDev) {
191
199
  continue;
192
200
  }
193
201
 
@@ -244,44 +252,26 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
244
252
  }
245
253
 
246
254
  // Step 2: For each user, read their watchlists from SignedInUsers/{cid}/watchlists
247
- const { readWithMigration } = require('../../old-generic-api/user-api/helpers/core/path_resolution_helpers');
248
- // const { collectionRegistry } = dependencies; // Already destructured above
249
- // const config = dependencies.config || {}; // Already defined
250
-
255
+ // Read directly from new path (no migration needed)
251
256
  for (const userCidStr of userCids) {
252
257
  try {
253
258
  const userCid = Number(userCidStr);
254
259
 
255
260
  // Read all watchlists for this user from new path
256
- const watchlistsResult = await readWithMigration(
257
- db,
258
- 'signedInUsers',
259
- 'watchlists',
260
- { cid: userCid },
261
- {
262
- isCollection: true,
263
- dataType: 'watchlists',
264
- config,
265
- logger,
266
- collectionRegistry: collectionRegistry
267
- }
268
- );
261
+ const watchlistsSnapshot = await db.collection('SignedInUsers')
262
+ .doc(String(userCid))
263
+ .collection('watchlists')
264
+ .get();
269
265
 
270
- if (!watchlistsResult) {
266
+ if (watchlistsSnapshot.empty) {
271
267
  continue;
272
268
  }
273
269
 
274
- // Get watchlists from snapshot or data
275
- let watchlists = [];
276
- if (watchlistsResult.snapshot && !watchlistsResult.snapshot.empty) {
277
- watchlists = watchlistsResult.snapshot.docs.map(doc => ({
278
- id: doc.id,
279
- ...doc.data()
280
- }));
281
- } else if (watchlistsResult.data) {
282
- // If it's a single document, wrap it
283
- watchlists = [watchlistsResult.data];
284
- }
270
+ // Get watchlists from snapshot
271
+ const watchlists = watchlistsSnapshot.docs.map(doc => ({
272
+ id: doc.id,
273
+ ...doc.data()
274
+ }));
285
275
 
286
276
  // Step 3: Check each watchlist for the PI and alert config
287
277
  for (const watchlistData of watchlists) {
@@ -337,11 +327,13 @@ function shouldTriggerAlert(subscription, alertTypeId) {
337
327
  */
338
328
  async function getPIUsername(db, piCid) {
339
329
  try {
340
- // Try to get from master list first (single source of truth)
341
- const { getPIUsernameFromMasterList } = require('../../old-generic-api/user-api/helpers/core/user_status_helpers');
342
- const username = await getPIUsernameFromMasterList(db, piCid, null, null);
330
+ // Try to get from master list first (single source of truth) - using api-v2 helper
331
+ const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
332
+ const piData = await fetchPopularInvestorMasterList(db, String(piCid));
343
333
 
344
- if (username) return username;
334
+ if (piData && piData.username) {
335
+ return piData.username;
336
+ }
345
337
 
346
338
  // Fallback: try to get from any subscription
347
339
  const subscriptionsSnapshot = await db.collection('watchlist_subscriptions')
@@ -427,35 +427,25 @@ async function sendAllClearNotification(db, logger, userCid, piCid, piUsername,
427
427
  };
428
428
 
429
429
  // Write to alerts collection (not notifications) - alerts are separate from system notifications
430
- const { writeWithMigration } = require('../old-generic-api/user-api/helpers/core/path_resolution_helpers');
431
- await writeWithMigration(
432
- db,
433
- 'signedInUsers',
434
- 'alerts', // Write to alerts collection, not notifications
435
- { cid: String(userCid) },
436
- {
437
- // Alert-specific format
438
- alertId: notificationId,
439
- piCid: Number(piCid),
440
- piUsername: piUsername,
441
- alertType: 'all_clear',
442
- alertTypeName: 'All Clear',
443
- message: `${piUsername} was processed, all clear today!`,
444
- severity: 'info',
445
- read: false,
446
- createdAt: FieldValue.serverTimestamp(),
447
- computationDate: date
448
- },
449
- {
450
- isCollection: true,
451
- merge: false,
452
- dataType: 'alerts',
453
- documentId: notificationId,
454
- dualWrite: false, // Disable dual write - we're fully migrated to new path
455
- config,
456
- collectionRegistry
457
- }
458
- );
430
+ // Write directly to new path: SignedInUsers/{userCid}/alerts/{alertId}
431
+ const alertData = {
432
+ alertId: notificationId,
433
+ piCid: Number(piCid),
434
+ piUsername: piUsername,
435
+ alertType: 'all_clear',
436
+ alertTypeName: 'All Clear',
437
+ message: `${piUsername} was processed, all clear today!`,
438
+ severity: 'info',
439
+ read: false,
440
+ createdAt: FieldValue.serverTimestamp(),
441
+ computationDate: date
442
+ };
443
+
444
+ await db.collection('SignedInUsers')
445
+ .doc(String(userCid))
446
+ .collection('alerts')
447
+ .doc(notificationId)
448
+ .set(alertData);
459
449
 
460
450
  logger.log('INFO', `[sendAllClearNotification] Sent all-clear notification to user ${userCid} for PI ${piCid}`);
461
451
 
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Notification helpers for task engine and computation system
3
+ * Replaces old-generic-api notification helpers
4
+ */
5
+
6
+ const { FieldValue } = require('@google-cloud/firestore');
7
+
8
+ /**
9
+ * Notify task engine completion
10
+ * @param {Firestore} db - Firestore instance
11
+ * @param {Object} logger - Logger instance
12
+ * @param {string|number} userCid - User CID
13
+ * @param {string} requestId - Request ID
14
+ * @param {string} username - Username
15
+ * @param {boolean} success - Whether the task succeeded
16
+ * @param {string|null} errorMessage - Error message if failed
17
+ * @param {Object} options - Additional options (collectionRegistry, config, etc.)
18
+ */
19
+ async function notifyTaskEngineComplete(db, logger, userCid, requestId, username, success, errorMessage, options = {}) {
20
+ try {
21
+ const notificationData = {
22
+ type: 'sync',
23
+ subType: 'complete',
24
+ title: success ? 'Sync Complete' : 'Sync Failed',
25
+ message: success
26
+ ? `Data sync for ${username} completed successfully`
27
+ : `Data sync for ${username} failed: ${errorMessage || 'Unknown error'}`,
28
+ read: false,
29
+ timestamp: FieldValue.serverTimestamp(),
30
+ createdAt: FieldValue.serverTimestamp(),
31
+ metadata: {
32
+ requestId,
33
+ username,
34
+ success,
35
+ errorMessage: errorMessage || null
36
+ }
37
+ };
38
+
39
+ await db.collection('SignedInUsers')
40
+ .doc(String(userCid))
41
+ .collection('notifications')
42
+ .doc(requestId)
43
+ .set(notificationData);
44
+
45
+ logger?.log('INFO', `[notifyTaskEngineComplete] Notification sent for user ${userCid}, request ${requestId}`);
46
+ } catch (error) {
47
+ logger?.log('WARN', `[notifyTaskEngineComplete] Failed to send notification: ${error.message}`);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Notify task engine progress
53
+ * @param {Firestore} db - Firestore instance
54
+ * @param {Object} logger - Logger instance
55
+ * @param {string|number} userCid - User CID
56
+ * @param {string} requestId - Request ID
57
+ * @param {string} username - Username
58
+ * @param {string} stage - Progress stage (e.g., 'started', 'portfolio_complete')
59
+ * @param {string|null} dataType - Data type (e.g., 'portfolio', 'tradeHistory')
60
+ * @param {Object} options - Additional options
61
+ */
62
+ async function notifyTaskEngineProgress(db, logger, userCid, requestId, username, stage, dataType, options = {}) {
63
+ try {
64
+ const stageMessages = {
65
+ 'started': 'Data sync started',
66
+ 'portfolio_complete': 'Portfolio data fetched',
67
+ 'history_complete': 'Trade history fetched',
68
+ 'social_complete': 'Social posts fetched',
69
+ 'indexing': 'Indexing data',
70
+ 'computing': 'Running computations'
71
+ };
72
+
73
+ const notificationData = {
74
+ type: 'sync',
75
+ subType: 'progress',
76
+ title: 'Sync Progress',
77
+ message: stageMessages[stage] || `Sync ${stage}`,
78
+ read: false,
79
+ timestamp: FieldValue.serverTimestamp(),
80
+ createdAt: FieldValue.serverTimestamp(),
81
+ metadata: {
82
+ requestId,
83
+ username,
84
+ stage,
85
+ dataType: dataType || null
86
+ }
87
+ };
88
+
89
+ await db.collection('SignedInUsers')
90
+ .doc(String(userCid))
91
+ .collection('notifications')
92
+ .doc(`${requestId}_${stage}`)
93
+ .set(notificationData);
94
+
95
+ logger?.log('INFO', `[notifyTaskEngineProgress] Progress notification sent for user ${userCid}, stage ${stage}`);
96
+ } catch (error) {
97
+ logger?.log('WARN', `[notifyTaskEngineProgress] Failed to send notification: ${error.message}`);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Notify that PI data was refreshed
103
+ * @param {Firestore} db - Firestore instance
104
+ * @param {Object} logger - Logger instance
105
+ * @param {Object} collectionRegistry - Collection registry
106
+ * @param {string|number} piCid - PI CID
107
+ * @param {string} username - Username
108
+ * @param {Object} config - Config object
109
+ */
110
+ async function notifyPIDataRefreshed(db, logger, collectionRegistry, piCid, username, config) {
111
+ try {
112
+ // This is a non-critical notification - just log it
113
+ logger?.log('INFO', `[notifyPIDataRefreshed] PI ${piCid} (${username}) data refreshed`);
114
+ } catch (error) {
115
+ logger?.log('WARN', `[notifyPIDataRefreshed] Failed: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Notify computation completion
121
+ * @param {Firestore} db - Firestore instance
122
+ * @param {Object} logger - Logger instance
123
+ * @param {string|number} userCid - User CID
124
+ * @param {string} requestId - Request ID
125
+ * @param {string} computation - Computation name
126
+ * @param {string} displayName - Display name for computation
127
+ * @param {boolean} success - Whether computation succeeded
128
+ * @param {string|null} errorMessage - Error message if failed
129
+ * @param {Object} options - Additional options
130
+ */
131
+ async function notifyComputationComplete(db, logger, userCid, requestId, computation, displayName, success, errorMessage, options = {}) {
132
+ try {
133
+ const notificationData = {
134
+ type: 'computation',
135
+ subType: 'complete',
136
+ title: success ? 'Computation Complete' : 'Computation Failed',
137
+ message: success
138
+ ? `${displayName} completed successfully`
139
+ : `${displayName} failed: ${errorMessage || 'Unknown error'}`,
140
+ read: false,
141
+ timestamp: FieldValue.serverTimestamp(),
142
+ createdAt: FieldValue.serverTimestamp(),
143
+ metadata: {
144
+ requestId,
145
+ computation,
146
+ displayName,
147
+ success,
148
+ errorMessage: errorMessage || null
149
+ }
150
+ };
151
+
152
+ await db.collection('SignedInUsers')
153
+ .doc(String(userCid))
154
+ .collection('notifications')
155
+ .doc(`${requestId}_${computation}`)
156
+ .set(notificationData);
157
+
158
+ logger?.log('INFO', `[notifyComputationComplete] Notification sent for user ${userCid}, computation ${computation}`);
159
+ } catch (error) {
160
+ logger?.log('WARN', `[notifyComputationComplete] Failed to send notification: ${error.message}`);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Get computation display name
166
+ * @param {string} computation - Computation name
167
+ * @returns {string} Display name
168
+ */
169
+ function getComputationDisplayName(computation) {
170
+ const displayNames = {
171
+ 'SignedInUserProfileMetrics': 'Profile Metrics',
172
+ 'PopularInvestorProfileMetrics': 'PI Profile Metrics',
173
+ 'SignedInUserPIPersonalizedMetrics': 'Personalized PI Metrics',
174
+ 'SimilarInvestors': 'Similar Investors',
175
+ 'RecommendedHedges': 'Recommended Hedges',
176
+ 'PopularInvestorRankings': 'PI Rankings'
177
+ };
178
+ return displayNames[computation] || computation;
179
+ }
180
+
181
+ module.exports = {
182
+ notifyTaskEngineComplete,
183
+ notifyTaskEngineProgress,
184
+ notifyPIDataRefreshed,
185
+ notifyComputationComplete,
186
+ getComputationDisplayName
187
+ };
@@ -20,17 +20,32 @@ class ContextFactory {
20
20
 
21
21
  static buildPerUserContext(options) {
22
22
  const {
23
- todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory,
24
- userId, userType, dateStr, metadata, mappings, insights, socialData,
25
- computedDependencies, previousComputedDependencies, config, deps,
23
+ todayPortfolio,
24
+ yesterdayPortfolio,
25
+ todayHistory,
26
+ yesterdayHistory,
27
+ userId,
28
+ userType,
29
+ dateStr,
30
+ metadata,
31
+ mappings,
32
+ insights,
33
+ socialData,
34
+ computedDependencies,
35
+ previousComputedDependencies,
36
+ config,
37
+ deps,
26
38
  verification,
27
- rankings, yesterdayRankings, // User-specific rank entries
28
- allRankings, allRankingsYesterday, // Global rank lists
39
+ rankings,
40
+ yesterdayRankings,
41
+ allRankings,
42
+ allRankingsYesterday,
29
43
  allVerifications,
30
- // [NEW] New Root Data Types for Profile Metrics
31
- ratings, pageViews, watchlistMembership, alertHistory,
44
+ ratings,
45
+ pageViews,
46
+ watchlistMembership,
47
+ alertHistory,
32
48
  piMasterList,
33
- // [NEW] Series Data (Lookback for Root Data or Computation Results)
34
49
  seriesData
35
50
  } = options;
36
51
 
@@ -56,14 +71,11 @@ class ContextFactory {
56
71
  rankings: allRankings || [],
57
72
  rankingsYesterday: allRankingsYesterday || [],
58
73
  verifications: allVerifications || {},
59
- // [NEW] New Root Data Types for Profile Metrics
60
74
  ratings: ratings || {},
61
75
  pageViews: pageViews || {},
62
76
  watchlistMembership: watchlistMembership || {},
63
77
  alertHistory: alertHistory || {},
64
78
  piMasterList: piMasterList || {},
65
- // [NEW] Expose Series Data
66
- // Structure: { root: { [type]: { [date]: data } }, results: { [date]: { [calcName]: data } } }
67
79
  series: seriesData || {}
68
80
  }
69
81
  };
@@ -71,13 +83,23 @@ class ContextFactory {
71
83
 
72
84
  static buildMetaContext(options) {
73
85
  const {
74
- dateStr, metadata, mappings, insights, socialData, prices,
75
- computedDependencies, previousComputedDependencies, config, deps,
76
- allRankings, allRankingsYesterday,
86
+ dateStr,
87
+ metadata,
88
+ mappings,
89
+ insights,
90
+ socialData,
91
+ prices,
92
+ computedDependencies,
93
+ previousComputedDependencies,
94
+ config,
95
+ deps,
96
+ allRankings,
97
+ allRankingsYesterday,
77
98
  allVerifications,
78
- // [NEW] New Root Data Types
79
- ratings, pageViews, watchlistMembership, alertHistory,
80
- // [NEW] Series Data
99
+ ratings,
100
+ pageViews,
101
+ watchlistMembership,
102
+ alertHistory,
81
103
  seriesData
82
104
  } = options;
83
105
 
@@ -99,7 +121,6 @@ class ContextFactory {
99
121
  pageViews: pageViews || {},
100
122
  watchlistMembership: watchlistMembership || {},
101
123
  alertHistory: alertHistory || {},
102
- // [NEW] Expose Series Data
103
124
  series: seriesData || {}
104
125
  }
105
126
  };
@@ -30,11 +30,11 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
30
30
  let isAvailable = false;
31
31
 
32
32
  if (dep === 'portfolio') {
33
- if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
34
- else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
33
+ if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
34
+ else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
35
35
  else if (userType === 'popular_investor' && rootDataStatus.piPortfolios) isAvailable = true;
36
- else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
37
- else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
36
+ else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
37
+ else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
38
38
 
39
39
  if (!isAvailable) {
40
40
  // [OPTIMIZATION] Optimistic Series Check
@@ -284,12 +284,28 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
284
284
  logger.log('WARN', `[Availability] Index not found for ${dateStr}. Assuming NO data.`);
285
285
  return {
286
286
  status: {
287
- hasPortfolio: false, hasHistory: false, hasSocial: false, hasInsights: false, hasPrices: false,
288
- speculatorPortfolio: false, normalPortfolio: false, speculatorHistory: false, normalHistory: false,
289
- piRankings: false, piPortfolios: false, piDeepPortfolios: false, piHistory: false,
290
- signedInUserPortfolio: false, signedInUserHistory: false, signedInUserVerification: false,
291
- hasPISocial: false, hasSignedInSocial: false,
292
- piRatings: false, piPageViews: false, watchlistMembership: false, piAlertHistory: false
287
+ hasPortfolio: false,
288
+ hasHistory: false,
289
+ hasSocial: false,
290
+ hasInsights: false,
291
+ hasPrices: false,
292
+ speculatorPortfolio: false,
293
+ normalPortfolio: false,
294
+ speculatorHistory: false,
295
+ normalHistory: false,
296
+ piRankings: false,
297
+ piPortfolios: false,
298
+ piDeepPortfolios: false,
299
+ piHistory: false,
300
+ signedInUserPortfolio: false,
301
+ signedInUserHistory: false,
302
+ signedInUserVerification: false,
303
+ hasPISocial: false,
304
+ hasSignedInSocial: false,
305
+ piRatings: false,
306
+ piPageViews: false,
307
+ watchlistMembership: false,
308
+ piAlertHistory: false
293
309
  }
294
310
  };
295
311
  }
@@ -361,5 +377,5 @@ module.exports = {
361
377
  checkRootDependencies,
362
378
  checkRootDataAvailability,
363
379
  getViableCalculations,
364
- getAvailabilityWindow // [NEW] Exported
380
+ getAvailabilityWindow
365
381
  };
@@ -10,7 +10,7 @@ const { normalizeName } = require('../utils/utils');
10
10
  const { CachedDataLoader } = require('../data/CachedDataLoader');
11
11
  const { ContextFactory } = require('../context/ContextFactory');
12
12
  const { commitResults } = require('../persistence/ResultCommitter');
13
- const { fetchResultSeries } = require('../data/DependencyFetcher'); // [NEW] Import series fetcher
13
+ const { fetchResultSeries } = require('../data/DependencyFetcher');
14
14
 
15
15
  class MetaExecutor {
16
16
  static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
@@ -47,6 +47,8 @@ class MetaExecutor {
47
47
  let ratings = null, pageViews = null, watchlistMembership = null, alertHistory = null;
48
48
  if (needsNewRootData) {
49
49
  const loadPromises = [];
50
+
51
+ // Ratings
50
52
  if (calcs.some(c => c.rootDataDependencies?.includes('ratings'))) {
51
53
  loadPromises.push(loader.loadRatings(dStr).then(r => { ratings = r; }).catch(e => {
52
54
  // Only catch if ALL calcs allow missing roots
@@ -61,6 +63,8 @@ class MetaExecutor {
61
63
  }
62
64
  }));
63
65
  }
66
+
67
+ // PageViews
64
68
  if (calcs.some(c => c.rootDataDependencies?.includes('pageViews'))) {
65
69
  loadPromises.push(loader.loadPageViews(dStr).then(pv => { pageViews = pv; }).catch(e => {
66
70
  const allAllowMissing = calcs.every(c => {
@@ -74,6 +78,8 @@ class MetaExecutor {
74
78
  }
75
79
  }));
76
80
  }
81
+
82
+ // Watchlist
77
83
  if (calcs.some(c => c.rootDataDependencies?.includes('watchlist'))) {
78
84
  loadPromises.push(loader.loadWatchlistMembership(dStr).then(w => { watchlistMembership = w; }).catch(e => {
79
85
  const allAllowMissing = calcs.every(c => {
@@ -87,6 +93,8 @@ class MetaExecutor {
87
93
  }
88
94
  }));
89
95
  }
96
+
97
+ // Alerts
90
98
  if (calcs.some(c => c.rootDataDependencies?.includes('alerts'))) {
91
99
  loadPromises.push(loader.loadAlertHistory(dStr).then(a => { alertHistory = a; }).catch(e => {
92
100
  const allAllowMissing = calcs.every(c => {
@@ -100,6 +108,7 @@ class MetaExecutor {
100
108
  }
101
109
  }));
102
110
  }
111
+
103
112
  await Promise.all(loadPromises);
104
113
 
105
114
  // [FIX] Enforce canHaveMissingRoots - validate after loading
@@ -163,7 +172,8 @@ class MetaExecutor {
163
172
  else if (type === 'insights') loaderMethod = 'loadInsights';
164
173
  else if (type === 'ratings') loaderMethod = 'loadRatings';
165
174
  else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
166
- // Add other root types if needed
175
+ // [CRITICAL UPDATE] Add rankings support for Meta lookbacks
176
+ else if (type === 'rankings') loaderMethod = 'loadRankings';
167
177
 
168
178
  if (loaderMethod) {
169
179
  logger.log('INFO', `[MetaExecutor] Loading ${days}-day series for Root Data '${type}'...`);
@@ -222,7 +232,8 @@ class MetaExecutor {
222
232
  }
223
233
  }
224
234
 
225
- return await commitResults(state, dStr, passName, config, deps);
235
+ // CRITICAL FIX: Pass 'isInitialWrite: true' to ensure proper cleanup of old meta data
236
+ return await commitResults(state, dStr, passName, config, deps, false, { isInitialWrite: true });
226
237
  }
227
238
 
228
239
  static async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps, config, deps, loader) {
@@ -1,15 +1,8 @@
1
- /**
2
- * {
3
- * type: uploaded file
4
- * fileName: computation-system/executors/StandardExecutor.js
5
- * }
6
- */
7
1
  const { normalizeName, getEarliestDataDates } = require('../utils/utils');
8
2
  const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
9
3
  const { CachedDataLoader } = require('../data/CachedDataLoader');
10
4
  const { ContextFactory } = require('../context/ContextFactory');
11
5
  const { commitResults } = require('../persistence/ResultCommitter');
12
- // [NEW] Import series fetcher for computation results
13
6
  const { fetchResultSeries } = require('../data/DependencyFetcher');
14
7
  const mathLayer = require('../layers/index');
15
8
  const { performance } = require('perf_hooks');
@@ -167,7 +160,8 @@ class StandardExecutor {
167
160
  else if (type === 'insights') loaderMethod = 'loadInsights';
168
161
  else if (type === 'ratings') loaderMethod = 'loadRatings';
169
162
  else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
170
- // Add other root types as needed...
163
+ // [CRITICAL UPDATE] Add rankings support for AUM lookback
164
+ else if (type === 'rankings') loaderMethod = 'loadRankings';
171
165
 
172
166
  if (loaderMethod) {
173
167
  logger.log('INFO', `[StandardExecutor] Loading ${days}-day series for Root Data '${type}'...`);
@@ -487,4 +481,4 @@ class StandardExecutor {
487
481
  }
488
482
  }
489
483
 
490
- module.exports = { StandardExecutor };
484
+ module.exports = { StandardExecutor };
@@ -232,7 +232,7 @@ async function handleComputationTask(message, config, dependencies) {
232
232
  await db.doc(ledgerPath).update({ status: 'COMPLETED', completedAt: new Date() });
233
233
  await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics, triggerReason, resourceTier);
234
234
 
235
- const { notifyComputationComplete, getComputationDisplayName } = require('../../old-generic-api/user-api/helpers/notifications/notification_helpers.js');
235
+ const { notifyComputationComplete, getComputationDisplayName } = require('../../api-v2/helpers/notification_helpers.js');
236
236
  // Send notification if this was an on-demand computation
237
237
  if (metadata?.onDemand && metadata?.requestId && metadata?.requestingUserCid) {
238
238
  try {
@@ -4,6 +4,7 @@
4
4
  * UPDATED: Added support for 'isPage' mode to store per-user data in subcollections.
5
5
  * UPDATED: Implemented TTL retention policy. Defaults to 90 days from the computation date.
6
6
  * UPDATED: Fixed issue where switching to 'isPage' mode didn't clean up old sharded/raw data.
7
+ * CRITICAL FIX: Fixed sharding logic to prevent wiping existing shards during INTERMEDIATE flushes.
7
8
  */
8
9
  const { commitBatchInChunks, generateDataHash, FieldValue } = require('../utils/utils')
9
10
  const { updateComputationStatus } = require('./StatusRepository');
@@ -138,8 +139,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
138
139
  continue;
139
140
  }
140
141
 
141
- // [NEW] Page Computation Logic (Fan-Out) with TTL
142
- // Bypasses standard compression/sharding to write per-user documents
143
142
  // [NEW] Page Computation Logic (Fan-Out) with TTL
144
143
  // Bypasses standard compression/sharding to write per-user documents
145
144
  if (isPageComputation && !isEmpty) {
@@ -429,7 +428,9 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
429
428
  let finalStats = { totalSize: 0, isSharded: false, shardCount: 1, nextShardIndex: startShardIndex };
430
429
  let rootMergeOption = !isInitialWrite;
431
430
 
432
- let shouldWipeShards = wasSharded;
431
+ // CRITICAL FIX: Only wipe existing shards if this is the INITIAL write for this batch run.
432
+ // If we are flushing intermediate chunks, we should NOT wipe the shards created by previous chunks!
433
+ let shouldWipeShards = wasSharded && isInitialWrite;
433
434
 
434
435
  for (let attempt = 0; attempt < strategies.length; attempt++) {
435
436
  if (committed) break;
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @fileoverview Data loader sub-pipes for the Computation System.
3
3
  * REFACTORED: Now stateless and receive dependencies.
4
- * FIXED: Added strict userType filtering to prevent fetching unnecessary data (e.g. Normal users for PI calcs).
5
- * --- NEW: Updated to read PI/Signed-In data from SHARDS (Parts) instead of individual docs. ---
6
- * --- NEW: Logic to merge Overall and Deep PI data from corresponding shards. ---
7
- * --- UPDATED: Added loaders for Rankings and Verification data. ---
4
+ * FIXED: Added strict userType filtering to prevent fetching unnecessary data.
5
+ * UPDATED: Verification now uses CollectionGroup query due to per-user storage.
6
+ * UPDATED: Ratings now correctly handles flattened top-level schema (keys like "reviews.ID").
7
+ * REMOVED: Redundant Price Shard Indexing logic.
8
8
  */
9
9
  const zlib = require('zlib');
10
10
 
@@ -23,11 +23,9 @@ function tryDecompress(data) {
23
23
 
24
24
  /** --- Data Loader Sub-Pipes (Stateless, Dependency-Injection) --- */
25
25
 
26
- /** * Stage 1: Get portfolio part document references for a given date
27
- * [UPDATED] Accepts requiredUserTypes to filter collections.
28
- */
26
+ /** Stage 1: Get portfolio part document references for a given date */
29
27
  async function getPortfolioPartRefs(config, deps, dateString, requiredUserTypes = null) {
30
- const { db, logger, calculationUtils, collectionRegistry } = deps;
28
+ const { db, logger, calculationUtils } = deps;
31
29
  const { withRetry } = calculationUtils;
32
30
 
33
31
  // Normalize required types. If null/empty or contains 'ALL', fetch everything.
@@ -250,7 +248,6 @@ async function loadDailyInsights(config, deps, dateString) {
250
248
  async function loadDailySocialPostInsights(config, deps, dateString) {
251
249
  const { db, logger, calculationUtils, collectionRegistry } = deps;
252
250
  const { withRetry } = calculationUtils;
253
- const { getCollectionPath } = collectionRegistry || {};
254
251
 
255
252
  logger.log('INFO', `Loading and partitioning social data for ${dateString}`);
256
253
 
@@ -262,7 +259,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
262
259
  };
263
260
 
264
261
  // NEW STRUCTURE: Read from date-based collections
265
- // Structure: Collection/{date}/{cid}/{cid} for user social, Collection/{date}/posts/{postId} for instrument
266
262
  try {
267
263
  // Signed-In User Social: SignedInUserSocialPostData/{date}/{cid}/{cid}
268
264
  const signedInSocialCollectionName = 'SignedInUserSocialPostData';
@@ -279,7 +275,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
279
275
  const cidData = tryDecompress(cidDoc.data());
280
276
  if (cidData.posts && typeof cidData.posts === 'object') {
281
277
  if (!result.signedIn[cid]) result.signedIn[cid] = {};
282
- // Posts are stored as a map in the document
283
278
  Object.assign(result.signedIn[cid], cidData.posts);
284
279
  }
285
280
  }
@@ -328,13 +323,10 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
328
323
  const PI_COL_NAME = config.piSocialCollectionName || config.piSocialCollection || 'pi_social_posts';
329
324
  const SIGNED_IN_COL_NAME = config.signedInUserSocialCollection || 'signed_in_users_social';
330
325
 
331
- // 2. Define Time Range (UTC Day)
332
326
  const startDate = new Date(dateString + 'T00:00:00Z');
333
327
  const endDate = new Date(dateString + 'T23:59:59Z');
334
328
 
335
329
  try {
336
- // 3. Fetch ALL with CollectionGroup
337
- // NOTE: Requires Firestore Index: CollectionId 'posts', Field 'fetchedAt' (ASC/DESC)
338
330
  const postsQuery = db.collectionGroup('posts')
339
331
  .where('fetchedAt', '>=', startDate)
340
332
  .where('fetchedAt', '<=', endDate);
@@ -346,9 +338,7 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
346
338
  const data = tryDecompress(doc.data());
347
339
  const path = doc.ref.path;
348
340
 
349
- // 4. Partition Logic based on Path
350
341
  if (path.includes(PI_COL_NAME)) {
351
- // Path format: .../pi_social_posts/{userId}/posts/{postId}
352
342
  const parts = path.split('/');
353
343
  const colIndex = parts.indexOf(PI_COL_NAME);
354
344
  if (colIndex !== -1 && parts[colIndex + 1]) {
@@ -358,7 +348,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
358
348
  }
359
349
  }
360
350
  else if (path.includes(SIGNED_IN_COL_NAME)) {
361
- // Path format: .../signed_in_users_social/{userId}/posts/{postId}
362
351
  const parts = path.split('/');
363
352
  const colIndex = parts.indexOf(SIGNED_IN_COL_NAME);
364
353
  if (colIndex !== -1 && parts[colIndex + 1]) {
@@ -368,7 +357,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
368
357
  }
369
358
  }
370
359
  else {
371
- // Default: Generic Instrument Posts
372
360
  result.generic[doc.id] = data;
373
361
  }
374
362
  });
@@ -384,11 +372,9 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
384
372
  return result;
385
373
  }
386
374
 
387
- /** * Stage 6: Get history part references for a given date
388
- * [UPDATED] Accepts requiredUserTypes to filter collections.
389
- */
375
+ /** Stage 6: Get history part references for a given date */
390
376
  async function getHistoryPartRefs(config, deps, dateString, requiredUserTypes = null) {
391
- const { db, logger, calculationUtils, collectionRegistry } = deps;
377
+ const { db, logger, calculationUtils } = deps;
392
378
  const { withRetry } = calculationUtils;
393
379
 
394
380
  // Normalize required types
@@ -478,9 +464,7 @@ async function getHistoryPartRefs(config, deps, dateString, requiredUserTypes =
478
464
  return allPartRefs;
479
465
  }
480
466
 
481
- /** * Stage 7: Stream portfolio data in chunks
482
- * [UPDATED] Passes requiredUserTypes to getPortfolioPartRefs
483
- */
467
+ /** Stage 7: Stream portfolio data in chunks */
484
468
  async function* streamPortfolioData(config, deps, dateString, providedRefs = null, requiredUserTypes = null) {
485
469
  const { logger } = deps;
486
470
  const refs = providedRefs || (await getPortfolioPartRefs(config, deps, dateString, requiredUserTypes));
@@ -497,9 +481,7 @@ async function* streamPortfolioData(config, deps, dateString, providedRefs = nul
497
481
  logger.log('INFO', `[streamPortfolioData] Finished streaming for ${dateString}.`);
498
482
  }
499
483
 
500
- /** * Stage 8: Stream history data in chunks
501
- * [UPDATED] Passes requiredUserTypes to getHistoryPartRefs
502
- */
484
+ /** Stage 8: Stream history data in chunks */
503
485
  async function* streamHistoryData(config, deps, dateString, providedRefs = null, requiredUserTypes = null) {
504
486
  const { logger } = deps;
505
487
  const refs = providedRefs || (await getHistoryPartRefs(config, deps, dateString, requiredUserTypes));
@@ -531,80 +513,22 @@ async function getPriceShardRefs(config, deps) {
531
513
  }
532
514
  }
533
515
 
534
- /** Stage 10: Smart Shard Lookup System */
516
+ /** Stage 10: Smart Shard Lookup System (DEPRECATED/SIMPLIFIED) */
535
517
  async function ensurePriceShardIndex(config, deps) {
536
- const { db, logger } = deps;
537
- const metadataCol = config.metadataCollection || 'system_metadata';
538
- const indexDocRef = db.collection(metadataCol).doc('price_shard_index');
539
-
540
- // 1. Try to fetch existing index
541
- const snap = await indexDocRef.get();
542
- if (snap.exists) {
543
- const data = snap.data();
544
- const lastUpdated = data.lastUpdated ? new Date(data.lastUpdated).getTime() : 0;
545
- const now = Date.now();
546
- const oneDayMs = 24 * 60 * 60 * 1000;
547
- if ((now - lastUpdated) < oneDayMs) { return data.index || {}; }
548
- logger.log('INFO', '[ShardIndex] Index is stale (>24h). Rebuilding...');
549
- } else {
550
- logger.log('INFO', '[ShardIndex] Index not found. Building new Price Shard Index...');
551
- }
552
-
553
- // 2. Build Index
554
- const collection = config.priceCollection || 'asset_prices';
555
- const snapshot = await db.collection(collection).get();
556
-
557
- const index = {};
558
- let shardCount = 0;
559
-
560
- snapshot.forEach(doc => {
561
- shardCount++;
562
- const rawData = doc.data();
563
- const data = tryDecompress(rawData);
564
-
565
- if (data.history) {
566
- Object.keys(data.history).forEach(instId => {
567
- index[instId] = doc.id;
568
- });
569
- }
570
- });
571
-
572
- // 3. Save Index
573
- await indexDocRef.set({
574
- index: index,
575
- lastUpdated: new Date().toISOString(),
576
- shardCount: shardCount
577
- });
578
-
579
- logger.log('INFO', `[ShardIndex] Built index for ${Object.keys(index).length} instruments across ${shardCount} shards.`);
580
- return index;
518
+ // [DEPRECATED] This function previously built an index in 'system_metadata/price_shard_index'.
519
+ // It has been removed to avoid performing computation/indexing in the data loader.
520
+ // Use 'Fetch All' strategy in Stage 9 instead.
521
+ return {};
581
522
  }
582
523
 
583
524
  async function getRelevantShardRefs(config, deps, targetInstrumentIds) {
584
- const { db, logger } = deps;
585
-
586
- if (!targetInstrumentIds || targetInstrumentIds.length === 0) {
587
- return getPriceShardRefs(config, deps);
588
- }
589
-
590
- logger.log('INFO', `[ShardLookup] Resolving shards for ${targetInstrumentIds.length} specific instruments...`);
591
-
592
- const index = await ensurePriceShardIndex(config, deps);
593
- const uniqueShardIds = new Set();
594
- const collection = config.priceCollection || 'asset_prices';
595
-
596
- let foundCount = 0;
597
- for (const id of targetInstrumentIds) {
598
- const shardId = index[id];
599
- if (shardId) {
600
- uniqueShardIds.add(shardId);
601
- foundCount++;
602
- }
603
- }
604
-
605
- logger.log('INFO', `[ShardLookup] Mapped ${foundCount}/${targetInstrumentIds.length} instruments to ${uniqueShardIds.size} unique shards.`);
525
+ const { logger } = deps;
606
526
 
607
- return Array.from(uniqueShardIds).map(id => db.collection(collection).doc(id));
527
+ // [UPDATED] Smart shard lookup is disabled due to missing index infrastructure
528
+ // and to avoid computing indexes during load time.
529
+ // Falling back to Stage 9 (Fetch All Shards).
530
+ logger.log('INFO', `[ShardLookup] Smart indexing disabled. Fetching all price shards for ${targetInstrumentIds ? targetInstrumentIds.length : 'all'} instruments.`);
531
+ return getPriceShardRefs(config, deps);
608
532
  }
609
533
 
610
534
  /** Stage 11: Load Popular Investor Rankings */
@@ -625,40 +549,58 @@ async function loadPopularInvestorRankings(config, deps, dateString) {
625
549
  }
626
550
 
627
551
  const data = tryDecompress(docSnap.data());
628
- return data.Items || []; // Returns the array of PI objects
552
+ return data.Items || [];
629
553
  } catch (error) {
630
554
  logger.log('ERROR', `Failed to load Rankings for ${dateString}: ${error.message}`);
631
555
  return null;
632
556
  }
633
557
  }
634
558
 
635
- /** Stage 12: Load User Verification Profiles */
559
+ /** Stage 12: Load User Verification Profiles
560
+ * [UPDATED] Scans global verification data via CollectionGroup since it's now stored per-user.
561
+ */
636
562
  async function loadVerificationProfiles(config, deps) {
637
563
  const { db, logger, calculationUtils } = deps;
638
564
  const { withRetry } = calculationUtils;
639
- const collectionName = config.verificationCollection || 'verified_users';
640
565
 
641
- logger.log('INFO', `Loading Verification Profiles`);
566
+ // Verification is now stored at /SignedInUsers/{cid}/verification/data
567
+ // To fetch globally, we must use a CollectionGroup query on 'verification'
568
+ // and filter for the document ID 'data'.
569
+
570
+ logger.log('INFO', `Loading Verification Profiles (CollectionGroup: verification/data)`);
642
571
 
643
572
  try {
644
- const snapshot = await withRetry(() => db.collection(collectionName).get(), 'getVerifications');
573
+ // Warning: This requires a Firestore Index if used with complex filters, but basic get() usually works.
574
+ const snapshot = await withRetry(() => db.collectionGroup('verification').get(), 'getVerificationsGroup');
645
575
 
646
576
  if (snapshot.empty) return {};
647
577
 
648
578
  const profiles = {};
579
+ let count = 0;
580
+
649
581
  snapshot.forEach(doc => {
582
+ if (doc.id !== 'data') return; // Enforce specific document ID from schema
583
+
650
584
  const raw = tryDecompress(doc.data());
651
- // [FIX] Normalize Verification Data Structure
652
- profiles[doc.id] = {
653
- cid: raw.realCID || raw.cid,
654
- username: raw.username,
655
- aboutMe: raw.userBio?.aboutMe || raw.aboutMe || "",
656
- aboutMeShort: raw.userBio?.aboutMeShort || raw.aboutMeShort || "",
657
- isVerified: raw.isVerified === true,
658
- restrictions: raw.CustomerRestrictions || []
659
- };
585
+
586
+ // Map new schema fields to internal profile structure
587
+ // New Schema: { etoroCID, etoroUsername, verifiedAt, setupCompletedAt ... }
588
+ if (raw.etoroCID) {
589
+ profiles[raw.etoroCID] = {
590
+ cid: raw.etoroCID,
591
+ username: raw.etoroUsername,
592
+ // 'aboutMe' and 'restrictions' are NOT present in the new schema.
593
+ // Defaulting to empty values to preserve downstream compatibility.
594
+ aboutMe: "",
595
+ aboutMeShort: "",
596
+ isVerified: !!(raw.verifiedAt), // Using existence of verifiedAt as flag
597
+ restrictions: []
598
+ };
599
+ count++;
600
+ }
660
601
  });
661
602
 
603
+ logger.log('INFO', `Loaded ${count} verification profiles.`);
662
604
  return profiles;
663
605
  } catch (error) {
664
606
  logger.log('ERROR', `Failed to load Verification Profiles: ${error.message}`);
@@ -666,30 +608,60 @@ async function loadVerificationProfiles(config, deps) {
666
608
  }
667
609
  }
668
610
 
669
- /** Stage 13: Load PI Ratings Data */
611
+ /** Stage 13: Load PI Ratings Data
612
+ * [UPDATED] Reads from /PiReviews/{date}/shards/daily_log.
613
+ * [FIXED] Handles FLATTENED schema where keys like "reviews.ID" are at the top level.
614
+ * Returns RAW logs grouped by PI. NO COMPUTATION.
615
+ */
670
616
  async function loadPIRatings(config, deps, dateString) {
671
617
  const { db, logger, calculationUtils } = deps;
672
618
  const { withRetry } = calculationUtils;
673
- const collectionName = config.piRatingsCollection || 'PIRatingsData';
674
619
 
675
- logger.log('INFO', `Loading PI Ratings for ${dateString}`);
620
+ // New Path: /PiReviews/{date}/shards/daily_log
621
+
622
+ logger.log('INFO', `Loading PI Ratings (Raw Logs) for ${dateString}`);
676
623
 
677
624
  try {
678
- const docRef = db.collection(collectionName).doc(dateString);
679
- const docSnap = await withRetry(() => docRef.get(), `getPIRatings(${dateString})`);
680
-
681
- if (!docSnap.exists) {
682
- logger.log('WARN', `PI Ratings not found for ${dateString}`);
683
- return null;
625
+ const shardsColRef = db.collection('PiReviews').doc(dateString).collection('shards');
626
+ const shardDocs = await withRetry(() => shardsColRef.listDocuments(), `listRatingShards(${dateString})`);
627
+
628
+ if (!shardDocs || shardDocs.length === 0) {
629
+ logger.log('WARN', `No rating shards found for ${dateString} at ${shardsColRef.path}`);
630
+ return {};
684
631
  }
685
-
686
- const data = tryDecompress(docSnap.data());
687
- // Remove the date key and lastUpdated, return just the PI data
688
- const { date, lastUpdated, ...piRatings } = data;
689
- return piRatings; // Returns { piCid: { averageRating, totalRatings, ratingsByUser, ... } }
632
+
633
+ const rawReviewsByPi = {};
634
+
635
+ for (const docRef of shardDocs) {
636
+ const docSnap = await docRef.get();
637
+ if (!docSnap.exists) continue;
638
+
639
+ const rawData = tryDecompress(docSnap.data());
640
+
641
+ // SCHEMA HANDLING:
642
+ // Keys at the root of the document are the review IDs (e.g. "reviews.29312236_31075566").
643
+ // We iterate over all values and check if they look like review objects.
644
+
645
+ Object.values(rawData).forEach(entry => {
646
+ // Check for valid review object structure
647
+ if (entry && typeof entry === 'object' && entry.piCid && entry.rating !== undefined) {
648
+
649
+ if (!rawReviewsByPi[entry.piCid]) {
650
+ rawReviewsByPi[entry.piCid] = [];
651
+ }
652
+
653
+ // Store the raw entry directly.
654
+ rawReviewsByPi[entry.piCid].push(entry);
655
+ }
656
+ });
657
+ }
658
+
659
+ logger.log('INFO', `Loaded raw reviews for ${Object.keys(rawReviewsByPi).length} PIs.`);
660
+ return rawReviewsByPi;
661
+
690
662
  } catch (error) {
691
663
  logger.log('ERROR', `Failed to load PI Ratings for ${dateString}: ${error.message}`);
692
- return null;
664
+ return {};
693
665
  }
694
666
  }
695
667
 
@@ -711,9 +683,8 @@ async function loadPIPageViews(config, deps, dateString) {
711
683
  }
712
684
 
713
685
  const data = tryDecompress(docSnap.data());
714
- // Remove the date key and lastUpdated, return just the PI data
715
686
  const { date, lastUpdated, ...piPageViews } = data;
716
- return piPageViews; // Returns { piCid: { totalViews, uniqueViewers, viewsByUser, ... } }
687
+ return piPageViews;
717
688
  } catch (error) {
718
689
  logger.log('ERROR', `Failed to load PI Page Views for ${dateString}: ${error.message}`);
719
690
  return null;
@@ -738,9 +709,8 @@ async function loadWatchlistMembership(config, deps, dateString) {
738
709
  }
739
710
 
740
711
  const data = tryDecompress(docSnap.data());
741
- // Remove the date key and lastUpdated, return just the PI data
742
712
  const { date, lastUpdated, ...watchlistMembership } = data;
743
- return watchlistMembership; // Returns { piCid: { totalUsers, users, publicWatchlistCount, ... } }
713
+ return watchlistMembership;
744
714
  } catch (error) {
745
715
  logger.log('ERROR', `Failed to load Watchlist Membership for ${dateString}: ${error.message}`);
746
716
  return null;
@@ -765,19 +735,15 @@ async function loadPIAlertHistory(config, deps, dateString) {
765
735
  }
766
736
 
767
737
  const data = tryDecompress(docSnap.data());
768
- // Remove the date key and lastUpdated, return just the PI data
769
738
  const { date, lastUpdated, ...piAlertHistory } = data;
770
- return piAlertHistory; // Returns { piCid: { alertType: { triggered, count, triggeredFor, ... } } }
739
+ return piAlertHistory;
771
740
  } catch (error) {
772
741
  logger.log('ERROR', `Failed to load PI Alert History for ${dateString}: ${error.message}`);
773
742
  return null;
774
743
  }
775
744
  }
776
745
 
777
- /** Stage 17: Load PI-Centric Watchlist Data
778
- * Loads watchlist data from PopularInvestors/{piCid}/watchlistData/current
779
- * This provides time-series data of watchlist additions per PI
780
- */
746
+ /** Stage 17: Load PI-Centric Watchlist Data */
781
747
  async function loadPIWatchlistData(config, deps, piCid) {
782
748
  const { db, logger, calculationUtils } = deps;
783
749
  const { withRetry } = calculationUtils;
@@ -799,19 +765,18 @@ async function loadPIWatchlistData(config, deps, piCid) {
799
765
  }
800
766
 
801
767
  const data = tryDecompress(docSnap.data());
802
- return data; // Returns { totalUsers, userCids: [], dailyAdditions: { date: { count, userCids, timestamp } }, lastUpdated }
768
+ return data;
803
769
  } catch (error) {
804
770
  logger.log('ERROR', `Failed to load PI Watchlist Data for PI ${piCidStr}: ${error.message}`);
805
771
  return null;
806
772
  }
807
773
  }
808
774
 
809
- // [NEW] Load Popular Investor Master List
775
+ // Load Popular Investor Master List
810
776
  async function loadPopularInvestorMasterList(config, deps) {
811
777
  const { db, logger, calculationUtils } = deps;
812
778
  const { withRetry } = calculationUtils;
813
779
 
814
- // Default to 'system_state' collection, 'popular_investor_master_list' doc
815
780
  const collectionName = config.piMasterListCollection || 'system_state';
816
781
  const docId = config.piMasterListDocId || 'popular_investor_master_list';
817
782
 
@@ -827,9 +792,6 @@ async function loadPopularInvestorMasterList(config, deps) {
827
792
  }
828
793
 
829
794
  const data = tryDecompress(docSnap.data());
830
- // Structure is { investors: { cid: { username, ... } } } or direct map { cid: { ... } }
831
- // Based on user input, it looks like a direct map of CIDs or a field holding the map.
832
- // We return the raw object which acts as the map.
833
795
  return data.investors || data;
834
796
  } catch (error) {
835
797
  logger.log('ERROR', `Failed to load PI Master List: ${error.message}`);
@@ -855,6 +817,6 @@ module.exports = {
855
817
  loadPIPageViews,
856
818
  loadWatchlistMembership,
857
819
  loadPIAlertHistory,
858
- loadPopularInvestorMasterList, // [NEW]
820
+ loadPopularInvestorMasterList,
859
821
  loadPIWatchlistData,
860
822
  };
@@ -7,7 +7,7 @@
7
7
  const crypto = require('crypto');
8
8
  const { shouldTryProxy, recordProxyOutcome, getFailureCount, getMaxFailures } = require('../utils/proxy_circuit_breaker');
9
9
  const { FieldValue } = require('@google-cloud/firestore');
10
- const { notifyTaskEngineComplete, notifyTaskEngineProgress, notifyPIDataRefreshed } = require('../../old-generic-api/user-api/helpers/notifications/notification_helpers');
10
+ const { notifyTaskEngineComplete, notifyTaskEngineProgress, notifyPIDataRefreshed } = require('../../api-v2/helpers/notification_helpers');
11
11
  const { conditionallyRunRootDataIndexer } = require('./root_data_indexer_helpers');
12
12
 
13
13
  const {
@@ -405,7 +405,7 @@ async function finalizeOnDemandRequest(deps, config, taskData, isPI, success, to
405
405
  // 2. Trigger Computations (only if root data indexer completed successfully)
406
406
  if (indexerCompleted && pubsub && config.computationSystem) {
407
407
  const { triggerComputationWithDependencies } = require('../../computation-system/helpers/on_demand_helpers');
408
- const { checkIfUserIsPI } = require('../../old-generic-api/user-api/helpers/core/user_status_helpers');
408
+ const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
409
409
 
410
410
  // Use userType from metadata if available, otherwise fall back to isPI
411
411
  const userType = metadata?.userType || (isPI ? 'POPULAR_INVESTOR' : 'SIGNED_IN_USER');
@@ -429,11 +429,15 @@ async function finalizeOnDemandRequest(deps, config, taskData, isPI, success, to
429
429
 
430
430
  // IMPORTANT: Check if this signed-in user is also a Popular Investor
431
431
  // If they are, we need to run PI computations as well
432
- const rankEntry = await checkIfUserIsPI(db, cid, config, logger);
433
- if (rankEntry) {
434
- logger.log('INFO', `[On-Demand] Signed-in user ${cid} is also a Popular Investor. Adding PI computations.`);
435
- compsSet.add('PopularInvestorProfileMetrics');
436
- compsSet.add('SignedInUserPIPersonalizedMetrics');
432
+ try {
433
+ const piData = await fetchPopularInvestorMasterList(db, String(cid));
434
+ if (piData) {
435
+ logger.log('INFO', `[On-Demand] Signed-in user ${cid} is also a Popular Investor. Adding PI computations.`);
436
+ compsSet.add('PopularInvestorProfileMetrics');
437
+ compsSet.add('SignedInUserPIPersonalizedMetrics');
438
+ }
439
+ } catch (e) {
440
+ // User is not a PI, continue with signed-in user computations only
437
441
  }
438
442
  } else {
439
443
  // Fallback to isPI-based logic for backward compatibility
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.630",
3
+ "version": "1.0.632",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -9,7 +9,6 @@
9
9
  "functions/task-engine/",
10
10
  "functions/core/",
11
11
  "functions/computation-system/",
12
- "functions/old-generic-api/",
13
12
  "functions/api-v2/",
14
13
  "functions/dispatcher/",
15
14
  "functions/invalid-speculator-handler/",