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
|
-
//
|
|
268
|
+
// Try today's date first
|
|
239
269
|
const today = new Date().toISOString().split('T')[0];
|
|
240
|
-
|
|
241
|
-
|
|
270
|
+
let rankingsDate = today;
|
|
271
|
+
|
|
242
272
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
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}
|
|
248
|
-
|
|
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}
|
|
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
|
-
|
|
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(
|
|
24
|
+
return res.status(400).json({
|
|
23
25
|
success: false,
|
|
24
|
-
error: "
|
|
25
|
-
message: "
|
|
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,
|
|
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:
|
|
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(
|
|
103
|
+
.doc(String(requestUserCid));
|
|
91
104
|
|
|
92
105
|
await userRequestRef.set({
|
|
93
|
-
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:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
return
|
|
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);
|