bulltrackers-module 1.0.589 → 1.0.591

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.
@@ -3,6 +3,7 @@
3
3
  * UPDATED: Fixed bug where Alert Computations failed to trigger Pub/Sub on empty FINAL flush.
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
+ * UPDATED: Fixed issue where switching to 'isPage' mode didn't clean up old sharded/raw data.
6
7
  */
7
8
  const { commitBatchInChunks, generateDataHash } = require('../utils/utils');
8
9
  const { updateComputationStatus } = require('./StatusRepository');
@@ -26,7 +27,9 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
26
27
  const schemas = [];
27
28
  const cleanupTasks = [];
28
29
  const alertTriggers = [];
29
- const { logger, db } = deps;
30
+ const { logger, db, calculationUtils } = deps; // Extract calculationUtils if available
31
+ const withRetry = calculationUtils?.withRetry || (fn => fn());
32
+
30
33
  const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
31
34
 
32
35
  const flushMode = options.flushMode || 'STANDARD';
@@ -141,6 +144,30 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
141
144
  .collection(config.resultsSubcollection).doc(calc.manifest.category)
142
145
  .collection(config.computationsSubcollection).doc(name);
143
146
 
147
+ // --- CLEANUP START: Remove old storage formats (Sharded/Compressed) ---
148
+ // If the computation previously ran in Standard mode, we must remove shards
149
+ // and clear the main document data to avoid conflicting flags.
150
+ try {
151
+ const docSnap = await mainDocRef.get();
152
+ if (docSnap.exists) {
153
+ const dData = docSnap.data();
154
+ if (dData._sharded) {
155
+ const shardCol = mainDocRef.collection('_shards');
156
+ const shardDocs = await withRetry(() => shardCol.listDocuments());
157
+
158
+ if (shardDocs.length > 0) {
159
+ const cleanupOps = shardDocs.map(d => ({ type: 'DELETE', ref: d }));
160
+ await commitBatchInChunks(config, deps, cleanupOps, `${name}::PageModeCleanup`);
161
+ runMetrics.io.deletes += cleanupOps.length;
162
+ logger.log('INFO', `[PageMode] ${name}: Cleaned up ${cleanupOps.length} old shard documents.`);
163
+ }
164
+ }
165
+ }
166
+ } catch (cleanupErr) {
167
+ logger.log('WARN', `[PageMode] ${name}: Cleanup warning: ${cleanupErr.message}`);
168
+ }
169
+ // --- CLEANUP END ---
170
+
144
171
  // Calculate expiration based on computation date
145
172
  const expireAt = calculateExpirationDate(dStr, ttlDays);
146
173
 
@@ -181,7 +208,9 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
181
208
  _expireAt: expireAt // Ensure the header also gets deleted
182
209
  };
183
210
 
184
- await mainDocRef.set(headerData, { merge: true });
211
+ // CHANGED: Use merge: false to FORCE overwrite.
212
+ // This clears old "payload" (compressed), raw data keys, and old flags like "_sharded".
213
+ await mainDocRef.set(headerData, { merge: false });
185
214
  runMetrics.io.writes += 1;
186
215
 
187
216
  if (calc.manifest.hash) {
@@ -26,7 +26,9 @@ const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
26
26
  async function checkPiInComputationDate(db, insightsCollection, resultsSub, compsSub, category, computationName, dateStr, cidStr, logger) {
27
27
  try {
28
28
  // Check if this computation uses the pages subcollection structure
29
- const usesPagesStructure = computationName === 'PopularInvestorProfileMetrics' || computationName === 'SignedInUserProfileMetrics';
29
+ const usesPagesStructure = computationName === 'PopularInvestorProfileMetrics' ||
30
+ computationName === 'SignedInUserProfileMetrics' ||
31
+ computationName === 'SignedInUserPIPersonalizedMetrics';
30
32
 
31
33
  if (usesPagesStructure) {
32
34
  // New path format: /unified_insights/YYYY-MM-DD/results/popular-investor/computations/{computationName}/pages/{cid}
@@ -335,8 +337,10 @@ async function getUserComputations(req, res, dependencies, config) {
335
337
  let userResult = null;
336
338
 
337
339
  // Special handling for computations that use pages subcollection structure
338
- // Both PopularInvestorProfileMetrics and SignedInUserProfileMetrics use pages subcollection
339
- if (compName === 'PopularInvestorProfileMetrics' || compName === 'SignedInUserProfileMetrics') {
340
+ // PopularInvestorProfileMetrics, SignedInUserProfileMetrics, and SignedInUserPIPersonalizedMetrics use pages subcollection
341
+ if (compName === 'PopularInvestorProfileMetrics' ||
342
+ compName === 'SignedInUserProfileMetrics' ||
343
+ compName === 'SignedInUserPIPersonalizedMetrics') {
340
344
  const pageRef = db.collection(insightsCollection)
341
345
  .doc(date)
342
346
  .collection(resultsSub)
@@ -8,84 +8,10 @@ const { tryDecompress } = require('../core/compression_helpers');
8
8
  const { checkIfUserIsPI } = require('../core/user_status_helpers');
9
9
  const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
10
10
 
11
- /**
12
- * Generate sample PI personalized metrics data for dev testing
13
- * Creates realistic sample data that matches the computation structure 1:1
14
- */
15
- function generateSamplePIPersonalizedMetrics(userCid, rankEntry) {
16
- const today = new Date().toISOString().split('T')[0];
17
- const yesterday = new Date();
18
- yesterday.setDate(yesterday.getDate() - 1);
19
- const yesterdayStr = yesterday.toISOString().split('T')[0];
20
-
21
- // Generate sample base metrics (from PopularInvestorProfileMetrics)
22
- const baseMetrics = {
23
- socialEngagement: {
24
- chartType: 'line',
25
- data: Array.from({ length: 30 }, (_, i) => {
26
- const date = new Date();
27
- date.setDate(date.getDate() - (29 - i));
28
- return {
29
- date: date.toISOString().split('T')[0],
30
- likes: Math.floor(Math.random() * 50) + 10,
31
- comments: Math.floor(Math.random() * 20) + 5
32
- };
33
- })
34
- },
35
- profitablePositions: {
36
- chartType: 'bar',
37
- data: Array.from({ length: 30 }, (_, i) => {
38
- const date = new Date();
39
- date.setDate(date.getDate() - (29 - i));
40
- return {
41
- date: date.toISOString().split('T')[0],
42
- profitableCount: Math.floor(Math.random() * 10) + 5,
43
- totalCount: Math.floor(Math.random() * 15) + 10
44
- };
45
- })
46
- },
47
- topWinningPositions: {
48
- chartType: 'table',
49
- data: Array.from({ length: 5 }, (_, i) => ({
50
- instrumentId: 1000 + i,
51
- instrumentName: `Stock ${i + 1}`,
52
- netProfit: Math.floor(Math.random() * 1000) + 100,
53
- invested: Math.floor(Math.random() * 500) + 50
54
- }))
55
- }
56
- };
57
-
58
- return {
59
- baseMetrics,
60
- reviewMetrics: {
61
- overTime: [],
62
- currentStats: {
63
- averageRating: 4.5,
64
- totalReviews: 10,
65
- distribution: { 1: 0, 2: 0, 3: 1, 4: 3, 5: 6 }
66
- }
67
- },
68
- profileViews: {
69
- overTime: [],
70
- totalViews: 150,
71
- totalUniqueViews: 50,
72
- averageDailyViews: 5
73
- },
74
- engagementScore: {
75
- current: 75.5,
76
- components: {
77
- reviewScore: 90,
78
- viewScore: 50,
79
- copierScore: 80,
80
- socialScore: 60
81
- }
82
- }
83
- };
84
- }
85
-
86
11
  /**
87
12
  * GET /user/me/pi-personalized-metrics
88
13
  * Fetches personalized metrics for a signed-in user who is also a Popular Investor
14
+ * Reads from pages subcollection: /unified_insights/YYYY-MM-DD/results/popular-investor/computations/SignedInUserPIPersonalizedMetrics/pages/{cid}
89
15
  */
90
16
  async function getSignedInUserPIPersonalizedMetrics(req, res, dependencies, config) {
91
17
  const { db, logger } = dependencies;
@@ -165,36 +91,19 @@ async function getSignedInUserPIPersonalizedMetrics(req, res, dependencies, conf
165
91
  });
166
92
  }
167
93
 
168
- const isDevOverride = devOverride && devOverride.enabled && devOverride.pretendToBePI;
169
-
170
- // If dev override, generate sample data
171
- if (isDevOverride) {
172
- logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] DEV OVERRIDE: Generating sample PI metrics for effective CID ${effectiveCid}`);
173
- const sampleData = generateSamplePIPersonalizedMetrics(effectiveCid, rankEntry);
174
- const today = new Date().toISOString().split('T')[0];
175
-
176
- return res.status(200).json({
177
- status: 'success',
178
- userCid: String(effectiveCid),
179
- data: sampleData,
180
- dataDate: today,
181
- requestedDate: today,
182
- isFallback: false,
183
- isDevOverride: true,
184
- isImpersonating: isImpersonating || false,
185
- actualCid: Number(userCid)
186
- });
187
- }
188
-
189
94
  const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
190
95
  const resultsSub = config.resultsSubcollection || 'results';
191
96
  const compsSub = config.computationsSubcollection || 'computations';
192
97
  const category = 'popular-investor';
193
98
  const computationName = 'SignedInUserPIPersonalizedMetrics';
194
99
  const today = new Date().toISOString().split('T')[0];
100
+ const cidStr = String(effectiveCid);
101
+ const maxDaysBack = 30;
102
+
103
+ logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] Starting search for PI CID: ${effectiveCid}`);
195
104
 
196
- // Find latest computation date
197
- const latestComputationDate = await findLatestComputationDate(
105
+ // Find latest available computation date
106
+ const latestDate = await findLatestComputationDate(
198
107
  db,
199
108
  insightsCollection,
200
109
  resultsSub,
@@ -205,189 +114,97 @@ async function getSignedInUserPIPersonalizedMetrics(req, res, dependencies, conf
205
114
  30
206
115
  );
207
116
 
208
- if (!latestComputationDate) {
117
+ logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] Latest computation date found: ${latestDate || 'NONE'}`);
118
+
119
+ if (!latestDate) {
120
+ logger.log('WARN', `[getSignedInUserPIPersonalizedMetrics] No computation document found for ${computationName} in last 30 days`);
209
121
  return res.status(200).json({
210
- status: 'fallback',
211
- message: 'Computation not available, use frontend fallback',
122
+ status: 'not_found',
123
+ message: 'No computation data available. Please sync your account to generate personalized metrics.',
212
124
  data: null,
213
- useFrontendFallback: true,
125
+ dataDate: null,
126
+ requestedDate: today,
127
+ fallbackWindowExhausted: true,
214
128
  effectiveCid: effectiveCid,
215
- isImpersonating: isImpersonating || false
129
+ isImpersonating: isImpersonating || false,
130
+ actualCid: Number(userCid)
216
131
  });
217
132
  }
218
133
 
219
- // Check if user exists in computation (look back 7 days from latest)
134
+ // Try to find the user's page starting from the latest date, then going back up to 30 days
220
135
  let foundDate = null;
221
136
  let metricsData = null;
222
137
  let checkedDates = [];
223
138
 
224
- const latestDateObj = new Date(latestComputationDate + 'T00:00:00Z');
139
+ const latestDateObj = new Date(latestDate + 'T00:00:00Z');
225
140
 
226
- for (let daysBack = 0; daysBack <= 7; daysBack++) {
141
+ for (let daysBack = 0; daysBack <= maxDaysBack; daysBack++) {
227
142
  const checkDate = new Date(latestDateObj);
228
143
  checkDate.setUTCDate(latestDateObj.getUTCDate() - daysBack);
229
144
  const dateStr = checkDate.toISOString().split('T')[0];
230
145
  checkedDates.push(dateStr);
231
146
 
232
- const computationRef = db.collection(insightsCollection)
147
+ logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] Checking date ${dateStr} for CID ${effectiveCid} (${daysBack} days back from latest)`);
148
+
149
+ // Read from pages subcollection: /unified_insights/YYYY-MM-DD/results/popular-investor/computations/SignedInUserPIPersonalizedMetrics/pages/{cid}
150
+ const pageRef = db.collection(insightsCollection)
233
151
  .doc(dateStr)
234
152
  .collection(resultsSub)
235
153
  .doc(category)
236
154
  .collection(compsSub)
237
- .doc(computationName);
155
+ .doc(computationName)
156
+ .collection('pages')
157
+ .doc(cidStr);
238
158
 
239
- const computationDoc = await computationRef.get();
159
+ const pageDoc = await pageRef.get();
240
160
 
241
- if (computationDoc.exists) {
242
- const rawData = computationDoc.data();
243
- let computationData = tryDecompress(rawData);
161
+ if (pageDoc.exists) {
162
+ const rawData = pageDoc.data();
163
+ let data = tryDecompress(rawData);
244
164
 
245
- if (typeof computationData === 'string') {
165
+ // Handle string decompression result
166
+ if (typeof data === 'string') {
246
167
  try {
247
- computationData = JSON.parse(computationData);
168
+ data = JSON.parse(data);
248
169
  } catch (e) {
170
+ logger.log('WARN', `[getSignedInUserPIPersonalizedMetrics] Failed to parse decompressed string for CID ${effectiveCid} on date ${dateStr}:`, e.message);
249
171
  continue;
250
172
  }
251
173
  }
252
174
 
253
- if (computationData && typeof computationData === 'object' && !Array.isArray(computationData)) {
254
- const userData = computationData[String(effectiveCid)];
255
- if (userData) {
256
- foundDate = dateStr;
257
- metricsData = userData;
258
- logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] Found effective CID ${effectiveCid} in computation date ${dateStr}`);
259
- break;
260
- }
175
+ if (data && typeof data === 'object') {
176
+ foundDate = dateStr;
177
+ metricsData = data;
178
+ logger.log('SUCCESS', `[getSignedInUserPIPersonalizedMetrics] Found personalized metrics for CID ${effectiveCid} in date ${dateStr} (${daysBack} days back from latest)`);
179
+ break;
180
+ } else {
181
+ logger.log('WARN', `[getSignedInUserPIPersonalizedMetrics] Page data is not an object for CID ${effectiveCid} on date ${dateStr}`);
261
182
  }
183
+ } else {
184
+ logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] Page document not found for CID ${effectiveCid} in date ${dateStr}`);
262
185
  }
263
186
  }
264
187
 
188
+ // If not found in any checked date, return response indicating sync is needed
265
189
  if (!foundDate || !metricsData) {
190
+ logger.log('WARN', `[getSignedInUserPIPersonalizedMetrics] CID ${effectiveCid} not found in any checked dates: ${checkedDates.join(', ')}`);
191
+
266
192
  return res.status(200).json({
267
- status: 'fallback',
268
- message: 'User not found in computation, use frontend fallback',
193
+ status: 'not_found',
194
+ message: 'No personalized metrics found for this user. Please sync your account to generate personalized metrics.',
269
195
  data: null,
270
- useFrontendFallback: true,
196
+ dataDate: null,
197
+ requestedDate: today,
198
+ latestComputationDate: latestDate,
271
199
  checkedDates: checkedDates,
200
+ fallbackWindowExhausted: true,
272
201
  effectiveCid: effectiveCid,
273
- isImpersonating: isImpersonating || false
274
- });
275
- }
276
-
277
- // Enhance with review metrics and profile views
278
- const reviewsCollection = config.reviewsCollection || 'pi_reviews';
279
- const reviewsSnapshot = await db.collection(reviewsCollection)
280
- .where('piCid', '==', Number(effectiveCid))
281
- .orderBy('createdAt', 'desc')
282
- .limit(1000)
283
- .get();
284
-
285
- const reviewTimeMap = new Map();
286
- let totalReviews = 0;
287
- let totalRating = 0;
288
- const ratingDistribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
289
-
290
- reviewsSnapshot.forEach(doc => {
291
- const review = doc.data();
292
- const rating = review.rating || 0;
293
- totalReviews++;
294
- totalRating += rating;
295
- ratingDistribution[rating] = (ratingDistribution[rating] || 0) + 1;
296
-
297
- if (review.createdAt) {
298
- const reviewDate = review.createdAt.toDate ? review.createdAt.toDate().toISOString().split('T')[0] : review.createdAt;
299
- const existing = reviewTimeMap.get(reviewDate) || { date: reviewDate, count: 0, totalRating: 0 };
300
- existing.count++;
301
- existing.totalRating += rating;
302
- reviewTimeMap.set(reviewDate, existing);
303
- }
304
- });
305
-
306
- const reviewMetricsOverTime = Array.from(reviewTimeMap.values())
307
- .map(entry => ({
308
- date: entry.date,
309
- averageRating: entry.count > 0 ? Number((entry.totalRating / entry.count).toFixed(2)) : 0,
310
- count: entry.count
311
- }))
312
- .sort((a, b) => a.date.localeCompare(b.date));
313
-
314
- metricsData.reviewMetrics = {
315
- overTime: reviewMetricsOverTime,
316
- currentStats: {
317
- averageRating: totalReviews > 0 ? Number((totalRating / totalReviews).toFixed(2)) : 0,
318
- totalReviews: totalReviews,
319
- distribution: ratingDistribution
320
- },
321
- sentimentTrend: []
322
- };
323
-
324
- // Fetch profile views
325
- const profileViewsCollection = config.profileViewsCollection || 'profile_views';
326
- const viewsSnapshot = await db.collection(profileViewsCollection)
327
- .where('piCid', '==', Number(effectiveCid))
328
- .limit(90)
329
- .get();
330
-
331
- const viewsOverTime = [];
332
- let totalViews = 0;
333
- let totalUniqueViews = 0;
334
- const uniqueViewersSet = new Set();
335
- const viewsByDate = new Map();
336
-
337
- viewsSnapshot.forEach(doc => {
338
- const viewData = doc.data();
339
- const date = viewData.date;
340
- if (!date) return;
341
-
342
- const views = typeof viewData.totalViews === 'number' ? viewData.totalViews : 0;
343
- const uniqueViewers = Array.isArray(viewData.uniqueViewers) ? viewData.uniqueViewers : [];
344
-
345
- const existing = viewsByDate.get(date) || { date, views: 0, uniqueViewers: [] };
346
- existing.views += views;
347
- existing.uniqueViewers = [...new Set([...existing.uniqueViewers, ...uniqueViewers])];
348
- viewsByDate.set(date, existing);
349
- });
350
-
351
- for (const [date, data] of viewsByDate.entries()) {
352
- totalViews += data.views;
353
- data.uniqueViewers.forEach(v => uniqueViewersSet.add(v));
354
-
355
- viewsOverTime.push({
356
- date: date,
357
- views: data.views,
358
- uniqueViews: data.uniqueViewers.length
202
+ isImpersonating: isImpersonating || false,
203
+ actualCid: Number(userCid)
359
204
  });
360
205
  }
361
206
 
362
- totalUniqueViews = uniqueViewersSet.size;
363
- const averageDailyViews = viewsOverTime.length > 0 ? Number((totalViews / viewsOverTime.length).toFixed(2)) : 0;
364
-
365
- metricsData.profileViews = {
366
- overTime: viewsOverTime.sort((a, b) => a.date.localeCompare(b.date)),
367
- totalViews: totalViews,
368
- totalUniqueViews: totalUniqueViews,
369
- averageDailyViews: averageDailyViews
370
- };
371
-
372
- // Calculate engagement score
373
- const reviewScore = metricsData.reviewMetrics.currentStats.averageRating * 20;
374
- const viewScore = Math.min(100, (averageDailyViews / 10) * 100);
375
- const copierScore = rankEntry.Copiers ? Math.min(100, (rankEntry.Copiers / 1000) * 100) : 0;
376
- const socialScore = metricsData.baseMetrics?.socialEngagement?.data?.length > 0 ? Math.min(100, (metricsData.baseMetrics.socialEngagement.data.length / 10) * 100) : 0;
377
-
378
- const engagementScore = (reviewScore * 0.3 + viewScore * 0.2 + copierScore * 0.3 + socialScore * 0.2);
379
-
380
- metricsData.engagementScore = {
381
- current: Number(engagementScore.toFixed(2)),
382
- components: {
383
- reviewScore: Number(reviewScore.toFixed(2)),
384
- viewScore: Number(viewScore.toFixed(2)),
385
- copierScore: Number(copierScore.toFixed(2)),
386
- socialScore: Number(socialScore.toFixed(2))
387
- }
388
- };
389
-
390
- logger.log('SUCCESS', `[getSignedInUserPIPersonalizedMetrics] Returning personalized metrics for effective CID ${effectiveCid} from date ${foundDate}`);
207
+ logger.log('SUCCESS', `[getSignedInUserPIPersonalizedMetrics] Returning personalized metrics for CID ${effectiveCid} from date ${foundDate} (requested: ${today}, latest available: ${latestDate})`);
391
208
 
392
209
  return res.status(200).json({
393
210
  status: 'success',
@@ -395,7 +212,8 @@ async function getSignedInUserPIPersonalizedMetrics(req, res, dependencies, conf
395
212
  data: metricsData,
396
213
  dataDate: foundDate,
397
214
  requestedDate: today,
398
- isFallback: foundDate !== today,
215
+ latestComputationDate: latestDate,
216
+ isFallback: foundDate !== latestDate || foundDate !== today,
399
217
  daysBackFromLatest: checkedDates.indexOf(foundDate),
400
218
  isImpersonating: isImpersonating || false,
401
219
  actualCid: Number(userCid),
@@ -409,7 +227,5 @@ async function getSignedInUserPIPersonalizedMetrics(req, res, dependencies, conf
409
227
  }
410
228
 
411
229
  module.exports = {
412
- getSignedInUserPIPersonalizedMetrics,
413
- generateSamplePIPersonalizedMetrics
230
+ getSignedInUserPIPersonalizedMetrics
414
231
  };
415
-
@@ -405,32 +405,52 @@ 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('../../generic-api/user-api/helpers/core/user_status_helpers');
408
409
 
409
410
  // Use userType from metadata if available, otherwise fall back to isPI
410
411
  const userType = metadata?.userType || (isPI ? 'POPULAR_INVESTOR' : 'SIGNED_IN_USER');
411
412
 
412
413
  // Determine which computations to run based on userType
413
- const comps = [];
414
+ // Use a Set to avoid duplicate computation names
415
+ const compsSet = new Set();
414
416
  if (userType === 'POPULAR_INVESTOR') {
415
- comps.push('PopularInvestorProfileMetrics');
417
+ compsSet.add('PopularInvestorProfileMetrics');
416
418
  // PIs might also be signed-in users
417
419
  const userDoc = await db.collection('signed_in_users').doc(String(cid)).get();
418
420
  if (userDoc.exists) {
419
- comps.push('SignedInUserPIPersonalizedMetrics');
421
+ compsSet.add('SignedInUserPIPersonalizedMetrics');
420
422
  }
421
423
  } else if (userType === 'SIGNED_IN_USER') {
422
- comps.push('SignedInUserProfileMetrics', 'SignedInUserCopiedList', 'SignedInUserCopiedPIs', 'SignedInUserPastCopies');
424
+ // Signed-in users get standard signed-in user computations
425
+ compsSet.add('SignedInUserProfileMetrics');
426
+ compsSet.add('SignedInUserCopiedList');
427
+ compsSet.add('SignedInUserCopiedPIs');
428
+ compsSet.add('SignedInUserPastCopies');
429
+
430
+ // IMPORTANT: Check if this signed-in user is also a Popular Investor
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');
437
+ }
423
438
  } else {
424
439
  // Fallback to isPI-based logic for backward compatibility
425
440
  if (isPI) {
426
- comps.push('PopularInvestorProfileMetrics');
441
+ compsSet.add('PopularInvestorProfileMetrics');
427
442
  const userDoc = await db.collection('signed_in_users').doc(String(cid)).get();
428
- if (userDoc.exists) comps.push('SignedInUserPIPersonalizedMetrics');
443
+ if (userDoc.exists) compsSet.add('SignedInUserPIPersonalizedMetrics');
429
444
  } else {
430
- comps.push('SignedInUserProfileMetrics', 'SignedInUserCopiedList', 'SignedInUserCopiedPIs', 'SignedInUserPastCopies');
445
+ compsSet.add('SignedInUserProfileMetrics');
446
+ compsSet.add('SignedInUserCopiedList');
447
+ compsSet.add('SignedInUserCopiedPIs');
448
+ compsSet.add('SignedInUserPastCopies');
431
449
  }
432
450
  }
433
451
 
452
+ // Convert Set to Array for logging and iteration
453
+ const comps = Array.from(compsSet);
434
454
  logger.log('INFO', `[On-Demand] Triggering ${comps.length} computations for ${userType} (${username}): ${comps.join(', ')}`);
435
455
 
436
456
  for (const comp of comps) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.589",
3
+ "version": "1.0.591",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [