bulltrackers-module 1.0.435 → 1.0.437

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.
@@ -226,8 +226,38 @@ async function getSpeculatorsToUpdate(dependencies, config) {
226
226
  } catch (error) { logger.log('ERROR','[Core Utils] Error getting speculators to update', { errorMessage: error.message }); throw error; }
227
227
  }
228
228
 
229
+ /**
230
+ * Helper function to find the latest available date for Popular Investor rankings
231
+ * Searches backwards from today up to 30 days
232
+ */
233
+ async function findLatestRankingsDate(db, rankingsCollection, maxDaysBack = 30) {
234
+ const today = new Date();
235
+
236
+ for (let i = 0; i < maxDaysBack; i++) {
237
+ const checkDate = new Date(today);
238
+ checkDate.setDate(checkDate.getDate() - i);
239
+ const dateStr = checkDate.toISOString().split('T')[0];
240
+
241
+ try {
242
+ const rankingsRef = db.collection(rankingsCollection).doc(dateStr);
243
+ const rankingsDoc = await rankingsRef.get();
244
+
245
+ if (rankingsDoc.exists) {
246
+ return dateStr; // Found rankings for this date
247
+ }
248
+ } catch (error) {
249
+ // Continue to next date if error
250
+ continue;
251
+ }
252
+ }
253
+
254
+ return null; // No rankings found in the last maxDaysBack days
255
+ }
256
+
229
257
  /**
230
258
  * [NEW] Fetches Popular Investors from the daily rankings document.
259
+ * UPDATED: Uses latest available rankings date if today's doesn't exist (fallback for edge cases).
260
+ * This ensures the dispatcher can still queue tasks even if today's rankings haven't been fetched yet.
231
261
  */
232
262
  async function getPopularInvestorsToUpdate(dependencies, config) {
233
263
  const { db, logger } = dependencies;
@@ -235,17 +265,32 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
235
265
 
236
266
  logger.log('INFO', `[Core Utils] Getting Popular Investors to update from ${collectionName}...`);
237
267
 
238
- // Construct today's document ID (YYYY-MM-DD)
268
+ // Try today's date first
239
269
  const today = new Date().toISOString().split('T')[0];
240
- const docPath = `${collectionName}/${today}`;
241
-
270
+ let rankingsDate = today;
271
+
242
272
  try {
243
- const docRef = db.doc(docPath);
244
- const docSnap = await docRef.get();
273
+ let docRef = db.collection(collectionName).doc(today);
274
+ let docSnap = await docRef.get();
245
275
 
276
+ // If today's rankings don't exist, fallback to latest available date
246
277
  if (!docSnap.exists) {
247
- logger.log('WARN', `[Core Utils] No Popular Investor rankings found for date: ${today} at ${docPath}`);
248
- return [];
278
+ logger.log('WARN', `[Core Utils] No Popular Investor rankings found for date: ${today}. Looking for latest available date...`);
279
+ rankingsDate = await findLatestRankingsDate(db, collectionName, 30);
280
+
281
+ if (!rankingsDate) {
282
+ logger.log('WARN', `[Core Utils] No Popular Investor rankings found in last 30 days. Returning empty array.`);
283
+ return [];
284
+ }
285
+
286
+ logger.log('INFO', `[Core Utils] Using fallback rankings date: ${rankingsDate} (today: ${today})`);
287
+ docRef = db.collection(collectionName).doc(rankingsDate);
288
+ docSnap = await docRef.get();
289
+
290
+ if (!docSnap.exists) {
291
+ logger.log('WARN', `[Core Utils] Rankings doc does not exist for fallback date ${rankingsDate}`);
292
+ return [];
293
+ }
249
294
  }
250
295
 
251
296
  const data = docSnap.data();
@@ -261,7 +306,7 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
261
306
  logger.log('INFO', `[Core Utils Debug] First PI Target Mapped: ${JSON.stringify(targets[0])}`);
262
307
  }
263
308
 
264
- logger.log('INFO', `[Core Utils] Found ${targets.length} Popular Investors from ${today} ranking.`);
309
+ logger.log('INFO', `[Core Utils] Found ${targets.length} Popular Investors from ${rankingsDate} ranking${rankingsDate !== today ? ` (fallback from ${today})` : ''}.`);
265
310
  return targets;
266
311
 
267
312
  } catch (error) {
@@ -2465,7 +2465,7 @@ async function getSignedInUserPIPersonalizedMetrics(req, res, dependencies, conf
2465
2465
 
2466
2466
  module.exports = {
2467
2467
  getPiAnalytics,
2468
- getUserRecommendations,
2468
+ getUserRecommendations,
2469
2469
  getWatchlist,
2470
2470
  updateWatchlist,
2471
2471
  autoGenerateWatchlist,
@@ -2481,6 +2481,8 @@ module.exports = {
2481
2481
  checkPisInRankings,
2482
2482
  getPiProfile,
2483
2483
  checkIfUserIsPopularInvestor,
2484
+ checkIfUserIsPI, // Export for use in review_helpers
2485
+ findLatestRankingsDate, // Export for use in on_demand_fetch_helpers
2484
2486
  trackProfileView,
2485
2487
  getSignedInUserPIPersonalizedMetrics
2486
2488
  };
@@ -16,16 +16,27 @@ const RATE_LIMIT_MS = RATE_LIMIT_HOURS * 60 * 60 * 1000;
16
16
  async function requestPiFetch(req, res, dependencies, config) {
17
17
  const { db, logger, pubsub } = dependencies;
18
18
  const { piCid } = req.params;
19
- const userCid = req.user?.etoroCID || req.query?.userCid;
19
+
20
+ // Get userCid from query (same pattern as other endpoints)
21
+ const userCid = req.query?.userCid;
20
22
 
21
23
  if (!userCid) {
22
- return res.status(401).json({
24
+ return res.status(400).json({
23
25
  success: false,
24
- error: "Authentication required",
25
- message: "You must be signed in to request data fetching"
26
+ error: "Missing userCid",
27
+ message: "Please provide userCid as a query parameter"
26
28
  });
27
29
  }
28
30
 
31
+ // Check for dev override impersonation (same pattern as other endpoints)
32
+ const { getEffectiveCid, getDevOverride } = require('./dev_helpers');
33
+ const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
34
+ const devOverride = await getDevOverride(db, userCid, config, logger);
35
+ const isImpersonating = devOverride && devOverride.impersonateCid && effectiveCid !== Number(userCid);
36
+
37
+ // Use effective CID for the request (impersonated or actual)
38
+ const requestUserCid = Number(effectiveCid);
39
+
29
40
  const piCidNum = Number(piCid);
30
41
  if (isNaN(piCidNum) || piCidNum <= 0) {
31
42
  return res.status(400).json({
@@ -36,8 +47,8 @@ async function requestPiFetch(req, res, dependencies, config) {
36
47
  }
37
48
 
38
49
  try {
39
- // Check rate limits
40
- const rateLimitCheck = await checkRateLimits(db, userCid, piCidNum, logger);
50
+ // Check rate limits (use effective CID)
51
+ const rateLimitCheck = await checkRateLimits(db, requestUserCid, piCidNum, logger);
41
52
 
42
53
  if (!rateLimitCheck.allowed) {
43
54
  return res.status(429).json({
@@ -54,7 +65,7 @@ async function requestPiFetch(req, res, dependencies, config) {
54
65
  }
55
66
 
56
67
  // Get PI username from rankings (needed for task engine)
57
- const piUsername = await getPiUsername(db, piCidNum, logger);
68
+ const piUsername = await getPiUsername(db, piCidNum, config, logger);
58
69
  if (!piUsername) {
59
70
  return res.status(404).json({
60
71
  success: false,
@@ -75,22 +86,24 @@ async function requestPiFetch(req, res, dependencies, config) {
75
86
 
76
87
  await requestRef.set({
77
88
  requestId,
78
- userCid: Number(userCid),
89
+ userCid: requestUserCid, // Use effective CID
90
+ actualUserCid: Number(userCid), // Track actual developer CID for audit
79
91
  piCid: piCidNum,
80
92
  piUsername,
81
93
  status: 'queued',
94
+ isImpersonating: isImpersonating || false,
82
95
  createdAt: FieldValue.serverTimestamp(),
83
96
  updatedAt: FieldValue.serverTimestamp()
84
97
  });
85
98
 
86
- // Update user rate limit
99
+ // Update user rate limit (use effective CID)
87
100
  const userRequestRef = db.collection('pi_fetch_requests')
88
101
  .doc(String(piCidNum))
89
102
  .collection('user_requests')
90
- .doc(String(userCid));
103
+ .doc(String(requestUserCid));
91
104
 
92
105
  await userRequestRef.set({
93
- userCid: Number(userCid),
106
+ userCid: requestUserCid,
94
107
  piCid: piCidNum,
95
108
  lastRequestedAt: FieldValue.serverTimestamp(),
96
109
  requestCount: FieldValue.increment(1),
@@ -106,7 +119,7 @@ async function requestPiFetch(req, res, dependencies, config) {
106
119
  await globalRef.set({
107
120
  piCid: piCidNum,
108
121
  lastRequestedAt: FieldValue.serverTimestamp(),
109
- lastRequestedBy: Number(userCid),
122
+ lastRequestedBy: requestUserCid, // Use effective CID
110
123
  totalRequests: FieldValue.increment(1),
111
124
  updatedAt: FieldValue.serverTimestamp()
112
125
  }, { merge: true });
@@ -122,10 +135,12 @@ async function requestPiFetch(req, res, dependencies, config) {
122
135
  priority: 'high',
123
136
  source: 'on_demand',
124
137
  requestId,
125
- requestedBy: Number(userCid),
138
+ requestedBy: requestUserCid, // Use effective CID
139
+ actualRequestedBy: Number(userCid), // Track actual developer CID
126
140
  metadata: {
127
141
  onDemand: true,
128
- requestedAt: now.toISOString()
142
+ requestedAt: now.toISOString(),
143
+ isImpersonating: isImpersonating || false
129
144
  }
130
145
  };
131
146
 
@@ -139,7 +154,7 @@ async function requestPiFetch(req, res, dependencies, config) {
139
154
  updatedAt: FieldValue.serverTimestamp()
140
155
  });
141
156
 
142
- logger.log('INFO', `[requestPiFetch] User ${userCid} requested fetch for PI ${piCidNum} (${piUsername}). Request ID: ${requestId}`);
157
+ logger.log('INFO', `[requestPiFetch] User ${userCid}${isImpersonating ? ` (impersonating ${requestUserCid})` : ''} requested fetch for PI ${piCidNum} (${piUsername}). Request ID: ${requestId}`);
143
158
 
144
159
  return res.status(200).json({
145
160
  success: true,
@@ -170,7 +185,7 @@ async function requestPiFetch(req, res, dependencies, config) {
170
185
  async function getPiFetchStatus(req, res, dependencies, config) {
171
186
  const { db, logger } = dependencies;
172
187
  const { piCid } = req.params;
173
- const userCid = req.user?.etoroCID || req.query?.userCid;
188
+ const userCid = req.query?.userCid; // Optional - for rate limit info
174
189
 
175
190
  const piCidNum = Number(piCid);
176
191
  if (isNaN(piCidNum) || piCidNum <= 0) {
@@ -380,20 +395,46 @@ async function checkRateLimits(db, userCid, piCid, logger) {
380
395
 
381
396
  /**
382
397
  * Get PI username from rankings
398
+ * Uses latest available rankings date (with fallback)
383
399
  */
384
- async function getPiUsername(db, piCid, logger) {
400
+ async function getPiUsername(db, piCid, config, logger) {
385
401
  try {
386
- const rankingsRef = db.collection('rankings')
387
- .doc('latest')
388
- .collection('popular_investors')
389
- .doc(String(piCid));
390
-
391
- const doc = await rankingsRef.get();
392
- if (doc.exists) {
393
- const data = doc.data();
394
- return data.username || data.Username || null;
402
+ // Import the helper function from data_helpers
403
+ const { findLatestRankingsDate } = require('./data_helpers');
404
+
405
+ const rankingsCollection = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
406
+ const rankingsDate = await findLatestRankingsDate(db, rankingsCollection, 30);
407
+
408
+ if (!rankingsDate) {
409
+ logger.log('WARN', `[getPiUsername] No rankings data found (checked last 30 days) for PI ${piCid}`);
410
+ return null;
411
+ }
412
+
413
+ // Fetch rankings document for the latest available date
414
+ const rankingsRef = db.collection(rankingsCollection).doc(rankingsDate);
415
+ const rankingsDoc = await rankingsRef.get();
416
+
417
+ if (!rankingsDoc.exists) {
418
+ logger.log('WARN', `[getPiUsername] Rankings doc does not exist for date ${rankingsDate}`);
419
+ return null;
420
+ }
421
+
422
+ const rankingsData = rankingsDoc.data();
423
+ const rankingsItems = rankingsData.Items || [];
424
+
425
+ // Search for the PI in the rankings items
426
+ const piCidNum = Number(piCid);
427
+ const rankingEntry = rankingsItems.find(item => Number(item.CustomerId) === piCidNum);
428
+
429
+ if (rankingEntry) {
430
+ const username = rankingEntry.UserName || rankingEntry.username || null;
431
+ if (username) {
432
+ logger.log('INFO', `[getPiUsername] Found username "${username}" for PI ${piCid} in rankings date ${rankingsDate}`);
433
+ }
434
+ return username;
395
435
  }
396
436
 
437
+ logger.log('WARN', `[getPiUsername] PI ${piCid} not found in rankings date ${rankingsDate}`);
397
438
  return null;
398
439
  } catch (error) {
399
440
  logger.log('ERROR', `[getPiUsername] Error fetching username for PI ${piCid}`, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.435",
3
+ "version": "1.0.437",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [