bulltrackers-module 1.0.516 → 1.0.518
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.
- package/functions/core/utils/firestore_utils.js +168 -122
- package/functions/fetch-popular-investors/helpers/fetch_helpers.js +58 -0
- package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +86 -5
- package/functions/task-engine/handler_creator.js +15 -14
- package/functions/task-engine/helpers/popular_investor_helpers.js +170 -13
- package/package.json +1 -1
|
@@ -255,14 +255,129 @@ async function findLatestRankingsDate(db, rankingsCollection, maxDaysBack = 30)
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
* [NEW] Fetches Popular Investors from the
|
|
259
|
-
* UPDATED:
|
|
260
|
-
*
|
|
258
|
+
* [NEW] Fetches Popular Investors from the master list and filters by last updated times.
|
|
259
|
+
* UPDATED: Uses the master list as single source of truth, then checks last updated timestamps
|
|
260
|
+
* for each data type (portfolio, tradeHistory, socialPosts) to determine who needs updating.
|
|
261
261
|
* UPDATED: Uses collection registry for path resolution.
|
|
262
262
|
*/
|
|
263
263
|
async function getPopularInvestorsToUpdate(dependencies, config) {
|
|
264
264
|
const { db, logger, collectionRegistry } = dependencies;
|
|
265
265
|
|
|
266
|
+
// 24 hours threshold - users not updated in the last 24 hours need updating
|
|
267
|
+
const HOURS_THRESHOLD = 24;
|
|
268
|
+
const thresholdMs = HOURS_THRESHOLD * 60 * 60 * 1000;
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
|
|
271
|
+
logger.log('INFO', `[Core Utils] Getting Popular Investors to update (checking master list and last updated times, threshold: ${HOURS_THRESHOLD} hours)...`);
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// Get the master list of Popular Investors
|
|
275
|
+
let masterListPath = 'system_state/popular_investor_master_list';
|
|
276
|
+
|
|
277
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
278
|
+
try {
|
|
279
|
+
masterListPath = collectionRegistry.getCollectionPath('system', 'popularInvestorMasterList', {});
|
|
280
|
+
} catch (err) {
|
|
281
|
+
logger.log('WARN', `[Core Utils] Failed to get master list path from registry, using default: ${err.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const masterListRef = db.doc(masterListPath);
|
|
286
|
+
const masterListDoc = await masterListRef.get();
|
|
287
|
+
|
|
288
|
+
if (!masterListDoc.exists) {
|
|
289
|
+
logger.log('WARN', `[Core Utils] Master list not found. Falling back to rankings-based approach.`);
|
|
290
|
+
// Fallback to old method if master list doesn't exist yet
|
|
291
|
+
return await getPopularInvestorsToUpdateLegacy(dependencies, config);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const masterListData = masterListDoc.data();
|
|
295
|
+
const investors = masterListData.investors || {};
|
|
296
|
+
|
|
297
|
+
if (Object.keys(investors).length === 0) {
|
|
298
|
+
logger.log('WARN', `[Core Utils] Master list is empty. Returning empty array.`);
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
logger.log('INFO', `[Core Utils] Found ${Object.keys(investors).length} Popular Investors in master list. Checking last updated times...`);
|
|
303
|
+
|
|
304
|
+
const targets = [];
|
|
305
|
+
let skippedCount = 0;
|
|
306
|
+
|
|
307
|
+
// Check each PI's last updated times for each data type
|
|
308
|
+
for (const [cid, piData] of Object.entries(investors)) {
|
|
309
|
+
const username = piData.username || String(cid);
|
|
310
|
+
|
|
311
|
+
// Get last updated document for this PI
|
|
312
|
+
let lastUpdatedPath;
|
|
313
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
314
|
+
try {
|
|
315
|
+
lastUpdatedPath = collectionRegistry.getCollectionPath('popularInvestors', 'lastUpdated', { piCid: cid });
|
|
316
|
+
} catch (err) {
|
|
317
|
+
logger.log('WARN', `[Core Utils] Failed to get lastUpdated path for ${cid}: ${err.message}`);
|
|
318
|
+
// Include this PI if we can't check (better to update than skip)
|
|
319
|
+
targets.push({ cid, username });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Include this PI if registry not available
|
|
324
|
+
targets.push({ cid, username });
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const lastUpdatedRef = db.doc(lastUpdatedPath);
|
|
329
|
+
const lastUpdatedDoc = await lastUpdatedRef.get();
|
|
330
|
+
|
|
331
|
+
let needsUpdate = false;
|
|
332
|
+
|
|
333
|
+
if (!lastUpdatedDoc.exists) {
|
|
334
|
+
// No last updated data - needs update
|
|
335
|
+
needsUpdate = true;
|
|
336
|
+
} else {
|
|
337
|
+
const lastUpdatedData = lastUpdatedDoc.data();
|
|
338
|
+
|
|
339
|
+
// Check each data type - if any is missing or older than threshold, needs update
|
|
340
|
+
const dataTypes = ['portfolio', 'tradeHistory', 'socialPosts'];
|
|
341
|
+
for (const dataType of dataTypes) {
|
|
342
|
+
const lastUpdated = lastUpdatedData[dataType];
|
|
343
|
+
if (!lastUpdated) {
|
|
344
|
+
needsUpdate = true;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const lastUpdatedMs = lastUpdated.toDate?.()?.getTime() || lastUpdated.toMillis?.() || null;
|
|
349
|
+
if (lastUpdatedMs && (now - lastUpdatedMs) >= thresholdMs) {
|
|
350
|
+
needsUpdate = true;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (needsUpdate) {
|
|
357
|
+
targets.push({ cid, username });
|
|
358
|
+
} else {
|
|
359
|
+
skippedCount++;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
logger.log('INFO', `[Core Utils] Found ${Object.keys(investors).length} Popular Investors in master list. Skipped ${skippedCount} recently updated. ${targets.length} will be updated.`);
|
|
364
|
+
return targets;
|
|
365
|
+
|
|
366
|
+
} catch (error) {
|
|
367
|
+
logger.log('ERROR', '[Core Utils] Error getting Popular Investors', { errorMessage: error.message });
|
|
368
|
+
// Fallback to legacy method on error
|
|
369
|
+
logger.log('INFO', '[Core Utils] Falling back to legacy rankings-based approach due to error.');
|
|
370
|
+
return await getPopularInvestorsToUpdateLegacy(dependencies, config);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Legacy method: Fetches Popular Investors from the daily rankings document.
|
|
376
|
+
* Used as fallback when master list is not available.
|
|
377
|
+
*/
|
|
378
|
+
async function getPopularInvestorsToUpdateLegacy(dependencies, config) {
|
|
379
|
+
const { db, logger, collectionRegistry } = dependencies;
|
|
380
|
+
|
|
266
381
|
// Get collection name from registry if available
|
|
267
382
|
let collectionName = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
|
|
268
383
|
|
|
@@ -276,7 +391,7 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
|
|
|
276
391
|
}
|
|
277
392
|
}
|
|
278
393
|
|
|
279
|
-
logger.log('INFO', `[Core Utils] Getting Popular Investors
|
|
394
|
+
logger.log('INFO', `[Core Utils] Using legacy method: Getting Popular Investors from ${collectionName} (checking last 7 days)...`);
|
|
280
395
|
|
|
281
396
|
// Check last 7 days of rankings data
|
|
282
397
|
const today = new Date();
|
|
@@ -331,66 +446,10 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
|
|
|
331
446
|
}
|
|
332
447
|
|
|
333
448
|
logger.log('INFO', `[Core Utils] Found ${piMap.size} unique Popular Investors across last 7 days of rankings.`);
|
|
334
|
-
|
|
335
|
-
// Convert map to array and filter out PIs updated in the last 18 hours
|
|
336
|
-
const allTargets = Array.from(piMap.values());
|
|
337
|
-
|
|
338
|
-
// Filter out PIs updated in the last 18 hours
|
|
339
|
-
const HOURS_THRESHOLD = 18;
|
|
340
|
-
const thresholdMs = HOURS_THRESHOLD * 60 * 60 * 1000;
|
|
341
|
-
const now = Date.now();
|
|
342
|
-
|
|
343
|
-
const filteredTargets = [];
|
|
344
|
-
let skippedCount = 0;
|
|
345
|
-
|
|
346
|
-
// Check each PI's last sync time using user-centric path
|
|
347
|
-
for (const target of allTargets) {
|
|
348
|
-
// Use collection registry for sync status path if available
|
|
349
|
-
let syncRef;
|
|
350
|
-
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
351
|
-
try {
|
|
352
|
-
// syncStatus path is: SignedInUsers/{cid}/syncStatus/latest (document path)
|
|
353
|
-
const syncStatusPath = collectionRegistry.getCollectionPath('signedInUsers', 'syncStatus', { cid: target.cid });
|
|
354
|
-
syncRef = db.doc(syncStatusPath);
|
|
355
|
-
} catch (err) {
|
|
356
|
-
// Fallback to legacy path
|
|
357
|
-
syncRef = db.collection('user_sync_requests')
|
|
358
|
-
.doc(target.cid)
|
|
359
|
-
.collection('global')
|
|
360
|
-
.doc('latest');
|
|
361
|
-
}
|
|
362
|
-
} else {
|
|
363
|
-
// Fallback to legacy path
|
|
364
|
-
syncRef = db.collection('user_sync_requests')
|
|
365
|
-
.doc(target.cid)
|
|
366
|
-
.collection('global')
|
|
367
|
-
.doc('latest');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const syncDoc = await syncRef.get();
|
|
371
|
-
if (syncDoc.exists) {
|
|
372
|
-
const syncData = syncDoc.data();
|
|
373
|
-
const lastRequestedAt = syncData.lastRequestedAt?.toDate?.()?.getTime() ||
|
|
374
|
-
syncData.lastRequestedAt?.toMillis?.() || null;
|
|
375
|
-
|
|
376
|
-
if (lastRequestedAt && (now - lastRequestedAt) < thresholdMs) {
|
|
377
|
-
skippedCount++;
|
|
378
|
-
continue; // Skip this PI
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
filteredTargets.push(target); // Include this PI
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (filteredTargets.length > 0) {
|
|
386
|
-
logger.log('INFO', `[Core Utils Debug] First PI Target Mapped: ${JSON.stringify(filteredTargets[0])}`);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
logger.log('INFO', `[Core Utils] Found ${allTargets.length} Popular Investors from last 7 days of rankings. Skipped ${skippedCount} recently updated. ${filteredTargets.length} will be updated.`);
|
|
390
|
-
return filteredTargets;
|
|
449
|
+
return Array.from(piMap.values());
|
|
391
450
|
|
|
392
451
|
} catch (error) {
|
|
393
|
-
logger.log('ERROR', '[Core Utils] Error getting Popular Investors', { errorMessage: error.message });
|
|
452
|
+
logger.log('ERROR', '[Core Utils] Error getting Popular Investors (legacy)', { errorMessage: error.message });
|
|
394
453
|
throw error;
|
|
395
454
|
}
|
|
396
455
|
}
|
|
@@ -398,7 +457,8 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
|
|
|
398
457
|
/**
|
|
399
458
|
* [NEW] Fetches Signed-In Users for daily update.
|
|
400
459
|
* UPDATED: Uses user-centric collection model and collection registry.
|
|
401
|
-
*
|
|
460
|
+
* UPDATED: Checks last updated timestamps for each data type (portfolio, tradeHistory, socialPosts)
|
|
461
|
+
* to determine who needs updating. Users not updated in the last 24 hours need updating.
|
|
402
462
|
*/
|
|
403
463
|
async function getSignedInUsersToUpdate(dependencies, config) {
|
|
404
464
|
const { db, logger, collectionRegistry } = dependencies;
|
|
@@ -416,13 +476,12 @@ async function getSignedInUsersToUpdate(dependencies, config) {
|
|
|
416
476
|
}
|
|
417
477
|
}
|
|
418
478
|
|
|
419
|
-
//
|
|
420
|
-
const HOURS_THRESHOLD =
|
|
479
|
+
// 24 hours threshold - users not updated in the last 24 hours need updating
|
|
480
|
+
const HOURS_THRESHOLD = 24;
|
|
421
481
|
const thresholdMs = HOURS_THRESHOLD * 60 * 60 * 1000;
|
|
422
482
|
const now = Date.now();
|
|
423
|
-
const today = new Date().toISOString().split('T')[0];
|
|
424
483
|
|
|
425
|
-
logger.log('INFO', `[Core Utils] Getting Signed-In Users to update from ${signedInUsersCollection} (
|
|
484
|
+
logger.log('INFO', `[Core Utils] Getting Signed-In Users to update from ${signedInUsersCollection} (threshold: ${HOURS_THRESHOLD} hours)...`);
|
|
426
485
|
|
|
427
486
|
try {
|
|
428
487
|
// Get all signed-in users from the main collection
|
|
@@ -434,7 +493,7 @@ async function getSignedInUsersToUpdate(dependencies, config) {
|
|
|
434
493
|
return [];
|
|
435
494
|
}
|
|
436
495
|
|
|
437
|
-
const
|
|
496
|
+
const allUsers = [];
|
|
438
497
|
|
|
439
498
|
// Extract CIDs from user documents
|
|
440
499
|
snapshot.forEach(doc => {
|
|
@@ -444,84 +503,71 @@ async function getSignedInUsersToUpdate(dependencies, config) {
|
|
|
444
503
|
|
|
445
504
|
if (!cid) return;
|
|
446
505
|
|
|
447
|
-
|
|
506
|
+
allUsers.push({ cid: String(cid), username });
|
|
448
507
|
});
|
|
449
508
|
|
|
450
|
-
logger.log('INFO', `[Core Utils] Found ${
|
|
509
|
+
logger.log('INFO', `[Core Utils] Found ${allUsers.length} Signed-In Users. Checking last updated times...`);
|
|
451
510
|
|
|
452
511
|
// Filter out users who were updated recently
|
|
453
|
-
const
|
|
512
|
+
const targets = [];
|
|
454
513
|
let skippedCount = 0;
|
|
455
514
|
|
|
456
|
-
const checkPromises =
|
|
457
|
-
//
|
|
458
|
-
let
|
|
459
|
-
|
|
515
|
+
const checkPromises = allUsers.map(async (target) => {
|
|
516
|
+
// Get last updated document for this user
|
|
517
|
+
let lastUpdatedPath;
|
|
460
518
|
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
461
519
|
try {
|
|
462
|
-
|
|
463
|
-
const portfolioPath = collectionRegistry.getCollectionPath('rootData', 'signedInUserPortfolio', {
|
|
464
|
-
date: today,
|
|
465
|
-
cid: target.cid
|
|
466
|
-
});
|
|
467
|
-
const portfolioRef = db.doc(portfolioPath);
|
|
468
|
-
const portfolioDoc = await portfolioRef.get();
|
|
469
|
-
|
|
470
|
-
if (portfolioDoc.exists) {
|
|
471
|
-
hasTodayData = true;
|
|
472
|
-
}
|
|
520
|
+
lastUpdatedPath = collectionRegistry.getCollectionPath('signedInUsers', 'lastUpdated', { cid: target.cid });
|
|
473
521
|
} catch (err) {
|
|
474
|
-
logger.log('WARN', `[Core Utils]
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// Also check sync status using user-centric path
|
|
479
|
-
let syncRef;
|
|
480
|
-
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
481
|
-
try {
|
|
482
|
-
// syncStatus path is: SignedInUsers/{cid}/syncStatus/latest (document path)
|
|
483
|
-
const syncStatusPath = collectionRegistry.getCollectionPath('signedInUsers', 'syncStatus', { cid: target.cid });
|
|
484
|
-
syncRef = db.doc(syncStatusPath);
|
|
485
|
-
} catch (err) {
|
|
486
|
-
// Fallback to legacy path
|
|
487
|
-
syncRef = db.collection('user_sync_requests')
|
|
488
|
-
.doc(target.cid)
|
|
489
|
-
.collection('global')
|
|
490
|
-
.doc('latest');
|
|
522
|
+
logger.log('WARN', `[Core Utils] Failed to get lastUpdated path for ${target.cid}: ${err.message}`);
|
|
523
|
+
// Include this user if we can't check (better to update than skip)
|
|
524
|
+
return target;
|
|
491
525
|
}
|
|
492
526
|
} else {
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
.doc(target.cid)
|
|
496
|
-
.collection('global')
|
|
497
|
-
.doc('latest');
|
|
527
|
+
// Include this user if registry not available
|
|
528
|
+
return target;
|
|
498
529
|
}
|
|
499
530
|
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
531
|
+
const lastUpdatedRef = db.doc(lastUpdatedPath);
|
|
532
|
+
const lastUpdatedDoc = await lastUpdatedRef.get();
|
|
533
|
+
|
|
534
|
+
let needsUpdate = false;
|
|
535
|
+
|
|
536
|
+
if (!lastUpdatedDoc.exists) {
|
|
537
|
+
// No last updated data - needs update
|
|
538
|
+
needsUpdate = true;
|
|
539
|
+
} else {
|
|
540
|
+
const lastUpdatedData = lastUpdatedDoc.data();
|
|
505
541
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
542
|
+
// Check each data type - if any is missing or older than threshold, needs update
|
|
543
|
+
const dataTypes = ['portfolio', 'tradeHistory', 'socialPosts'];
|
|
544
|
+
for (const dataType of dataTypes) {
|
|
545
|
+
const lastUpdated = lastUpdatedData[dataType];
|
|
546
|
+
if (!lastUpdated) {
|
|
547
|
+
needsUpdate = true;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const lastUpdatedMs = lastUpdated.toDate?.()?.getTime() || lastUpdated.toMillis?.() || null;
|
|
552
|
+
if (lastUpdatedMs && (now - lastUpdatedMs) >= thresholdMs) {
|
|
553
|
+
needsUpdate = true;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
509
556
|
}
|
|
510
557
|
}
|
|
511
558
|
|
|
512
|
-
|
|
513
|
-
|
|
559
|
+
if (needsUpdate) {
|
|
560
|
+
return target;
|
|
561
|
+
} else {
|
|
514
562
|
skippedCount++;
|
|
515
|
-
return null;
|
|
563
|
+
return null;
|
|
516
564
|
}
|
|
517
|
-
|
|
518
|
-
return target; // Include this user - needs update
|
|
519
565
|
});
|
|
520
566
|
|
|
521
567
|
const results = await Promise.all(checkPromises);
|
|
522
568
|
const finalTargets = results.filter(t => t !== null);
|
|
523
569
|
|
|
524
|
-
logger.log('INFO', `[Core Utils] Found ${
|
|
570
|
+
logger.log('INFO', `[Core Utils] Found ${allUsers.length} Signed-In Users. Skipped ${skippedCount} recently updated. ${finalTargets.length} will be updated.`);
|
|
525
571
|
return finalTargets;
|
|
526
572
|
|
|
527
573
|
} catch (error) {
|
|
@@ -129,6 +129,64 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
|
|
|
129
129
|
|
|
130
130
|
logger.log('SUCCESS', `[PopularInvestorFetch] Stored ${data.TotalRows} rankings into ${finalRankingsCollectionName}/${today}`);
|
|
131
131
|
|
|
132
|
+
// Update the master list of Popular Investors
|
|
133
|
+
try {
|
|
134
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
135
|
+
let masterListPath = 'system_state/popular_investor_master_list';
|
|
136
|
+
|
|
137
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
138
|
+
try {
|
|
139
|
+
// Get the path from registry
|
|
140
|
+
const registryPath = collectionRegistry.getCollectionPath('system', 'popularInvestorMasterList', {});
|
|
141
|
+
masterListPath = registryPath;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
logger.log('WARN', `[PopularInvestorFetch] Failed to get master list path from registry, using default: ${e.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const masterListRef = db.doc(masterListPath);
|
|
148
|
+
const masterListDoc = await masterListRef.get();
|
|
149
|
+
|
|
150
|
+
const now = new Date();
|
|
151
|
+
const investorsMap = masterListDoc.exists ? (masterListDoc.data().investors || {}) : {};
|
|
152
|
+
|
|
153
|
+
// Update the master list with all PIs from this fetch
|
|
154
|
+
for (const item of data.Items) {
|
|
155
|
+
const cid = String(item.CustomerId);
|
|
156
|
+
const username = item.UserName;
|
|
157
|
+
|
|
158
|
+
if (!cid || !username) continue;
|
|
159
|
+
|
|
160
|
+
if (!investorsMap[cid]) {
|
|
161
|
+
// New PI discovered
|
|
162
|
+
investorsMap[cid] = {
|
|
163
|
+
cid: cid,
|
|
164
|
+
username: username,
|
|
165
|
+
firstSeenAt: FieldValue.serverTimestamp(),
|
|
166
|
+
lastSeenAt: FieldValue.serverTimestamp()
|
|
167
|
+
};
|
|
168
|
+
} else {
|
|
169
|
+
// Existing PI - update lastSeenAt and username if changed
|
|
170
|
+
investorsMap[cid].lastSeenAt = FieldValue.serverTimestamp();
|
|
171
|
+
if (username && investorsMap[cid].username !== username) {
|
|
172
|
+
investorsMap[cid].username = username;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Write the updated master list
|
|
178
|
+
await masterListRef.set({
|
|
179
|
+
investors: investorsMap,
|
|
180
|
+
lastUpdated: FieldValue.serverTimestamp(),
|
|
181
|
+
totalInvestors: Object.keys(investorsMap).length
|
|
182
|
+
}, { merge: true });
|
|
183
|
+
|
|
184
|
+
logger.log('SUCCESS', `[PopularInvestorFetch] Updated master list with ${data.Items.length} PIs. Total unique PIs: ${Object.keys(investorsMap).length}`);
|
|
185
|
+
} catch (masterListError) {
|
|
186
|
+
logger.log('WARN', `[PopularInvestorFetch] Failed to update master list: ${masterListError.message}`);
|
|
187
|
+
// Non-critical, continue
|
|
188
|
+
}
|
|
189
|
+
|
|
132
190
|
// Update root data indexer for today's date after rankings data is stored
|
|
133
191
|
try {
|
|
134
192
|
const { runRootDataIndexer } = require('../../root-data-indexer/index');
|
|
@@ -424,22 +424,102 @@ async function getUserSyncStatus(req, res, dependencies, config) {
|
|
|
424
424
|
// No request found, check if user can request
|
|
425
425
|
let canRequest = true;
|
|
426
426
|
let rateLimitInfo = null;
|
|
427
|
+
let lastUpdatedInfo = null;
|
|
427
428
|
|
|
428
|
-
|
|
429
|
+
// Check last updated times for the target user
|
|
430
|
+
try {
|
|
431
|
+
const { collectionRegistry } = dependencies;
|
|
432
|
+
const HOURS_THRESHOLD = 24;
|
|
433
|
+
const thresholdMs = HOURS_THRESHOLD * 60 * 60 * 1000;
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
|
|
436
|
+
// Determine user type (PI or signed-in user)
|
|
437
|
+
const referrer = req.headers.referer || req.headers.referrer || '';
|
|
438
|
+
const sourcePage = req.query?.sourcePage || '';
|
|
439
|
+
const isPI = referrer.includes('/popular-investors/') || sourcePage === 'popular-investor';
|
|
440
|
+
|
|
441
|
+
let lastUpdatedPath;
|
|
442
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
443
|
+
try {
|
|
444
|
+
if (isPI) {
|
|
445
|
+
lastUpdatedPath = collectionRegistry.getCollectionPath('popularInvestors', 'lastUpdated', { piCid: String(targetCidNum) });
|
|
446
|
+
} else {
|
|
447
|
+
lastUpdatedPath = collectionRegistry.getCollectionPath('signedInUsers', 'lastUpdated', { cid: String(targetCidNum) });
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
logger.log('WARN', `[getUserSyncStatus] Failed to get lastUpdated path: ${err.message}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (lastUpdatedPath) {
|
|
455
|
+
const lastUpdatedRef = db.doc(lastUpdatedPath);
|
|
456
|
+
const lastUpdatedDoc = await lastUpdatedRef.get();
|
|
457
|
+
|
|
458
|
+
if (lastUpdatedDoc.exists) {
|
|
459
|
+
const lastUpdatedData = lastUpdatedDoc.data();
|
|
460
|
+
const dataTypes = ['portfolio', 'tradeHistory', 'socialPosts'];
|
|
461
|
+
const allRecent = dataTypes.every(dataType => {
|
|
462
|
+
const lastUpdated = lastUpdatedData[dataType];
|
|
463
|
+
if (!lastUpdated) return false;
|
|
464
|
+
|
|
465
|
+
const lastUpdatedMs = lastUpdated.toDate?.()?.getTime() || lastUpdated.toMillis?.() || null;
|
|
466
|
+
return lastUpdatedMs && (now - lastUpdatedMs) < thresholdMs;
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (allRecent) {
|
|
470
|
+
// All data types were updated recently - disable sync
|
|
471
|
+
canRequest = false;
|
|
472
|
+
const oldestUpdate = Math.min(...dataTypes.map(dataType => {
|
|
473
|
+
const lastUpdated = lastUpdatedData[dataType];
|
|
474
|
+
const lastUpdatedMs = lastUpdated.toDate?.()?.getTime() || lastUpdated.toMillis?.() || null;
|
|
475
|
+
return lastUpdatedMs || 0;
|
|
476
|
+
}).filter(ms => ms > 0));
|
|
477
|
+
|
|
478
|
+
const canRequestAgainAt = new Date(oldestUpdate + thresholdMs).toISOString();
|
|
479
|
+
lastUpdatedInfo = {
|
|
480
|
+
allDataTypesRecent: true,
|
|
481
|
+
canRequestAgainAt,
|
|
482
|
+
lastUpdated: {
|
|
483
|
+
portfolio: lastUpdatedData.portfolio?.toDate?.()?.toISOString() || null,
|
|
484
|
+
tradeHistory: lastUpdatedData.tradeHistory?.toDate?.()?.toISOString() || null,
|
|
485
|
+
socialPosts: lastUpdatedData.socialPosts?.toDate?.()?.toISOString() || null
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
} else {
|
|
489
|
+
// Some data types need updating - allow sync
|
|
490
|
+
lastUpdatedInfo = {
|
|
491
|
+
allDataTypesRecent: false,
|
|
492
|
+
lastUpdated: {
|
|
493
|
+
portfolio: lastUpdatedData.portfolio?.toDate?.()?.toISOString() || null,
|
|
494
|
+
tradeHistory: lastUpdatedData.tradeHistory?.toDate?.()?.toISOString() || null,
|
|
495
|
+
socialPosts: lastUpdatedData.socialPosts?.toDate?.()?.toISOString() || null
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch (lastUpdatedError) {
|
|
502
|
+
logger.log('WARN', `[getUserSyncStatus] Error checking last updated times: ${lastUpdatedError.message}`);
|
|
503
|
+
// Non-critical, continue with rate limit check
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (requestingUserCid) {
|
|
429
507
|
// Check if this is a developer account (bypass rate limits for developers)
|
|
430
508
|
const { isDeveloperAccount } = require('../dev/dev_helpers');
|
|
431
509
|
const isDeveloper = isDeveloperAccount(requestingUserCid);
|
|
432
510
|
|
|
433
511
|
if (isDeveloper) {
|
|
434
|
-
// Developer accounts bypass rate limits
|
|
435
|
-
canRequest = true;
|
|
512
|
+
// Developer accounts bypass rate limits (but still respect last updated check)
|
|
436
513
|
rateLimitInfo = {
|
|
437
514
|
bypassed: true,
|
|
438
515
|
reason: 'developer_account'
|
|
439
516
|
};
|
|
440
517
|
} else {
|
|
441
518
|
const rateLimitCheck = await checkRateLimits(db, targetCidNum, logger);
|
|
442
|
-
|
|
519
|
+
// Combine rate limit and last updated checks - both must pass
|
|
520
|
+
if (!rateLimitCheck.allowed) {
|
|
521
|
+
canRequest = false;
|
|
522
|
+
}
|
|
443
523
|
rateLimitInfo = {
|
|
444
524
|
canRequestAgainAt: rateLimitCheck.canRequestAgainAt
|
|
445
525
|
};
|
|
@@ -450,7 +530,8 @@ async function getUserSyncStatus(req, res, dependencies, config) {
|
|
|
450
530
|
success: true,
|
|
451
531
|
status: 'not_requested',
|
|
452
532
|
canRequest,
|
|
453
|
-
rateLimit: rateLimitInfo
|
|
533
|
+
rateLimit: rateLimitInfo,
|
|
534
|
+
lastUpdated: lastUpdatedInfo
|
|
454
535
|
});
|
|
455
536
|
|
|
456
537
|
} catch (error) {
|
|
@@ -228,22 +228,23 @@ async function handleRequest(message, context, configObj, dependencies) {
|
|
|
228
228
|
await handleUpdate(data, 'single-update', dependencies, config);
|
|
229
229
|
break;
|
|
230
230
|
case 'POPULAR_INVESTOR_UPDATE':
|
|
231
|
-
// For POPULAR_INVESTOR_UPDATE,
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
231
|
+
// For POPULAR_INVESTOR_UPDATE, fields are at the payload root level
|
|
232
|
+
// Merge data object with payload root fields
|
|
233
|
+
const taskData = {
|
|
234
|
+
cid: payload.cid || data?.cid,
|
|
235
|
+
username: payload.username || data?.username,
|
|
236
|
+
requestId: payload.requestId || data?.requestId,
|
|
237
|
+
source: payload.source || data?.source,
|
|
238
|
+
requestedBy: payload.requestedBy || data?.requestedBy,
|
|
239
|
+
effectiveRequestedBy: payload.effectiveRequestedBy || data?.effectiveRequestedBy,
|
|
240
|
+
metadata: payload.metadata || data?.metadata || {},
|
|
241
|
+
priority: payload.priority || data?.priority,
|
|
242
|
+
// Merge nested data object if it exists
|
|
243
|
+
...(data || {})
|
|
243
244
|
};
|
|
244
245
|
|
|
245
|
-
if (!taskData
|
|
246
|
-
logger.log('ERROR', '[TaskEngine] POPULAR_INVESTOR_UPDATE missing required fields (cid or username)', { payload, taskData });
|
|
246
|
+
if (!taskData.cid && !taskData.username) {
|
|
247
|
+
logger.log('ERROR', '[TaskEngine] POPULAR_INVESTOR_UPDATE missing required fields (cid or username)', { payload, data, taskData });
|
|
247
248
|
return;
|
|
248
249
|
}
|
|
249
250
|
|
|
@@ -27,6 +27,50 @@ const {
|
|
|
27
27
|
storeSignedInUserSocialPosts
|
|
28
28
|
} = require('./data_storage_helpers');
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Helper function to update last updated timestamp for a user's data type
|
|
32
|
+
* @param {object} db - Firestore database instance
|
|
33
|
+
* @param {object} collectionRegistry - Collection registry helper
|
|
34
|
+
* @param {string} cid - User CID
|
|
35
|
+
* @param {string} userType - 'popularInvestor' or 'signedInUser'
|
|
36
|
+
* @param {string} dataType - 'portfolio', 'tradeHistory', or 'socialPosts'
|
|
37
|
+
* @param {object} logger - Logger instance
|
|
38
|
+
*/
|
|
39
|
+
async function updateLastUpdatedTimestamp(db, collectionRegistry, cid, userType, dataType, logger) {
|
|
40
|
+
try {
|
|
41
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
42
|
+
let lastUpdatedPath;
|
|
43
|
+
|
|
44
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
45
|
+
try {
|
|
46
|
+
if (userType === 'popularInvestor') {
|
|
47
|
+
lastUpdatedPath = collectionRegistry.getCollectionPath('popularInvestors', 'lastUpdated', { piCid: cid });
|
|
48
|
+
} else {
|
|
49
|
+
lastUpdatedPath = collectionRegistry.getCollectionPath('signedInUsers', 'lastUpdated', { cid: cid });
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
logger.log('WARN', `[LastUpdated] Failed to get path from registry: ${e.message}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
logger.log('WARN', '[LastUpdated] Collection registry not available');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lastUpdatedRef = db.doc(lastUpdatedPath);
|
|
61
|
+
const updateData = {
|
|
62
|
+
[dataType]: FieldValue.serverTimestamp(),
|
|
63
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await lastUpdatedRef.set(updateData, { merge: true });
|
|
67
|
+
logger.log('INFO', `[LastUpdated] Updated ${dataType} timestamp for ${userType} ${cid}`);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
logger.log('WARN', `[LastUpdated] Failed to update timestamp for ${userType} ${cid} ${dataType}: ${error.message}`);
|
|
70
|
+
// Non-critical, continue
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
30
74
|
async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
31
75
|
const { logger, proxyManager, batchManager, headerManager, db, pubsub, collectionRegistry } = dependencies;
|
|
32
76
|
|
|
@@ -37,9 +81,13 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
37
81
|
}
|
|
38
82
|
|
|
39
83
|
const { cid, username, requestId, source, metadata } = taskData;
|
|
84
|
+
const data = taskData.data || {}; // Extract data object
|
|
40
85
|
// Extract targetCid from metadata if present (for optimization)
|
|
41
86
|
const targetCid = metadata?.targetCid || cid;
|
|
42
87
|
|
|
88
|
+
// Always fetch social data (for both on-demand and scheduled updates)
|
|
89
|
+
const includeSocial = data.includeSocial !== false; // Default to true, only skip if explicitly false
|
|
90
|
+
|
|
43
91
|
// Validate required fields
|
|
44
92
|
if (!cid && !username) {
|
|
45
93
|
logger.log('ERROR', '[PI Update] Missing required fields: cid or username', { taskData });
|
|
@@ -192,6 +240,9 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
192
240
|
portfolioData
|
|
193
241
|
});
|
|
194
242
|
|
|
243
|
+
// Update last updated timestamp for portfolio
|
|
244
|
+
await updateLastUpdatedTimestamp(db, collectionRegistry, cid, 'popularInvestor', 'portfolio', logger);
|
|
245
|
+
|
|
195
246
|
// Mark success for header reporting (at least partial)
|
|
196
247
|
fetchSuccess = true;
|
|
197
248
|
|
|
@@ -354,7 +405,99 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
354
405
|
logger.log('WARN', `[PI Update] History fetch failed for ${username}. Status: ${historyRes?.status || 'unknown'}`);
|
|
355
406
|
}
|
|
356
407
|
|
|
357
|
-
|
|
408
|
+
// Fetch social data (always for all PI updates)
|
|
409
|
+
let socialFetched = false;
|
|
410
|
+
if (includeSocial && username) {
|
|
411
|
+
try {
|
|
412
|
+
logger.log('INFO', `[PI Update] Fetching social data for ${username} (${cid})...`);
|
|
413
|
+
|
|
414
|
+
// Import social helper functions
|
|
415
|
+
const { getGcidForUser } = require('../../social-task-handler/helpers/handler_helpers');
|
|
416
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
417
|
+
|
|
418
|
+
// Get GCID for user
|
|
419
|
+
const gcid = await getGcidForUser(dependencies, config.social || {}, cid, username);
|
|
420
|
+
|
|
421
|
+
// Fetch social posts
|
|
422
|
+
const userFeedApiUrl = config.social?.userFeedApiUrl || 'https://www.etoro.com/api/edm-streams/v1/feed/user/top/';
|
|
423
|
+
const fetchUrl = `${userFeedApiUrl}${gcid}?take=10&offset=0&reactionsPageSize=20&badgesExperimentIsEnabled=false&client_request_id=${uuid}`;
|
|
424
|
+
|
|
425
|
+
const socialResponse = await proxyManager.fetch(fetchUrl, requestOptions);
|
|
426
|
+
if (socialResponse.ok) {
|
|
427
|
+
const socialData = await socialResponse.json();
|
|
428
|
+
const discussions = socialData?.discussions || [];
|
|
429
|
+
|
|
430
|
+
// Process and store social posts using new structure
|
|
431
|
+
const processedPostsCollection = config.social?.processedPostsCollectionName || 'social_processed_registry';
|
|
432
|
+
const processedRef = db.collection(processedPostsCollection);
|
|
433
|
+
|
|
434
|
+
const MAX_POSTS = 30;
|
|
435
|
+
const postsToStore = [];
|
|
436
|
+
|
|
437
|
+
for (const discussion of discussions.slice(0, MAX_POSTS)) {
|
|
438
|
+
const post = discussion.post;
|
|
439
|
+
if (!post || !post.id) continue;
|
|
440
|
+
|
|
441
|
+
// Check if already processed
|
|
442
|
+
const existing = await processedRef.doc(post.id).get();
|
|
443
|
+
if (existing.exists) continue;
|
|
444
|
+
|
|
445
|
+
// Prepare post data
|
|
446
|
+
const postData = {
|
|
447
|
+
id: post.id,
|
|
448
|
+
postId: post.id,
|
|
449
|
+
text: post.message?.text || '',
|
|
450
|
+
ownerId: post.owner?.id,
|
|
451
|
+
username: post.owner?.username,
|
|
452
|
+
createdAt: post.created,
|
|
453
|
+
fetchedAt: FieldValue.serverTimestamp(),
|
|
454
|
+
snippet: (post.message?.text || '').substring(0, 500),
|
|
455
|
+
stats: {
|
|
456
|
+
likes: discussion.emotionsData?.like?.paging?.totalCount || 0,
|
|
457
|
+
comments: discussion.summary?.totalCommentsAndReplies || 0
|
|
458
|
+
},
|
|
459
|
+
aiAnalysis: {
|
|
460
|
+
overallSentiment: "Neutral",
|
|
461
|
+
topics: [],
|
|
462
|
+
isSpam: false,
|
|
463
|
+
qualityScore: 0.5
|
|
464
|
+
},
|
|
465
|
+
tags: post.tags?.map(t => t.market?.symbolName).filter(Boolean) || []
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
postsToStore.push(postData);
|
|
469
|
+
|
|
470
|
+
// Mark as processed
|
|
471
|
+
await processedRef.doc(post.id).set({ processedAt: FieldValue.serverTimestamp() });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Store posts using new structure
|
|
475
|
+
if (postsToStore.length > 0) {
|
|
476
|
+
await storePopularInvestorSocialPosts({
|
|
477
|
+
db,
|
|
478
|
+
logger,
|
|
479
|
+
collectionRegistry,
|
|
480
|
+
cid: String(cid),
|
|
481
|
+
date: today,
|
|
482
|
+
posts: postsToStore
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Update last updated timestamp for social posts
|
|
486
|
+
await updateLastUpdatedTimestamp(db, collectionRegistry, String(cid), 'popularInvestor', 'socialPosts', logger);
|
|
487
|
+
|
|
488
|
+
socialFetched = true;
|
|
489
|
+
logger.log('INFO', `[PI Update] Fetched and stored ${postsToStore.length} social posts for ${username}`);
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
logger.log('WARN', `[PI Update] Social fetch failed for ${username} (status: ${socialResponse.status})`);
|
|
493
|
+
}
|
|
494
|
+
} catch (socialError) {
|
|
495
|
+
logger.log('WARN', `[PI Update] Social fetch failed for ${username}`, socialError);
|
|
496
|
+
// Continue - social failure shouldn't block portfolio/history processing
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
logger.log('SUCCESS', `[PI Update] Completed full update for ${username} (Portfolio: ✓, History: ✓, Social: ${socialFetched ? '✓' : '✗'})`);
|
|
358
501
|
|
|
359
502
|
// Update request status and trigger computation if this is an on-demand request (PI fetch or sync)
|
|
360
503
|
if (requestId && (source === 'on_demand' || source === 'on_demand_sync') && db) {
|
|
@@ -395,6 +538,10 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
395
538
|
// Get rootDataIndexer config from injected config object
|
|
396
539
|
const rootDataIndexerConfig = config.rootDataIndexer || {};
|
|
397
540
|
|
|
541
|
+
// Determine which data types ran
|
|
542
|
+
const dataTypesRun = ['piPortfolios', 'piHistory'];
|
|
543
|
+
if (socialFetched) dataTypesRun.push('piSocial');
|
|
544
|
+
|
|
398
545
|
const wasIndexed = await conditionallyRunRootDataIndexer({
|
|
399
546
|
db,
|
|
400
547
|
logger,
|
|
@@ -402,7 +549,7 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
402
549
|
rootDataIndexerConfig,
|
|
403
550
|
dependencies,
|
|
404
551
|
counterRef: null, // No counter for on-demand PI updates
|
|
405
|
-
dataTypesRun:
|
|
552
|
+
dataTypesRun: dataTypesRun
|
|
406
553
|
});
|
|
407
554
|
|
|
408
555
|
// Update status to indicate indexing is done (or skipped), computation is being triggered
|
|
@@ -522,7 +669,8 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
522
669
|
async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
523
670
|
const { cid, username, requestId, source, metadata } = taskData;
|
|
524
671
|
const data = taskData.data || {}; // Extract data object
|
|
525
|
-
|
|
672
|
+
// All on-demand requests should fetch social data
|
|
673
|
+
const includeSocial = data.includeSocial !== false; // Default to true, only skip if explicitly false
|
|
526
674
|
const since = data.since || new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString(); // Default to 7 days
|
|
527
675
|
const portfolioOnly = data.portfolioOnly === false ? false : true; // Default to true (fetch portfolio)
|
|
528
676
|
|
|
@@ -701,6 +849,7 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
701
849
|
date: today,
|
|
702
850
|
portfolioData
|
|
703
851
|
});
|
|
852
|
+
await updateLastUpdatedTimestamp(db, collectionRegistry, String(cid), 'popularInvestor', 'portfolio', logger);
|
|
704
853
|
} else {
|
|
705
854
|
await storeSignedInUserPortfolio({
|
|
706
855
|
db,
|
|
@@ -710,6 +859,7 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
710
859
|
date: today,
|
|
711
860
|
portfolioData
|
|
712
861
|
});
|
|
862
|
+
await updateLastUpdatedTimestamp(db, collectionRegistry, String(cid), 'signedInUser', 'portfolio', logger);
|
|
713
863
|
}
|
|
714
864
|
} catch (batchErr) {
|
|
715
865
|
const errorMsg = `Failed to store portfolio data: ${batchErr.message}`;
|
|
@@ -783,14 +933,17 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
783
933
|
try {
|
|
784
934
|
// Use correct storage function based on user type
|
|
785
935
|
if (userType === 'POPULAR_INVESTOR') {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
936
|
+
await storePopularInvestorTradeHistory({
|
|
937
|
+
db,
|
|
938
|
+
logger,
|
|
939
|
+
collectionRegistry,
|
|
940
|
+
cid: String(cid),
|
|
941
|
+
date: today,
|
|
942
|
+
historyData
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// Update last updated timestamp for trade history
|
|
946
|
+
await updateLastUpdatedTimestamp(db, collectionRegistry, String(cid), 'popularInvestor', 'tradeHistory', logger);
|
|
794
947
|
} else {
|
|
795
948
|
await storeSignedInUserTradeHistory({
|
|
796
949
|
db,
|
|
@@ -800,6 +953,7 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
800
953
|
date: today,
|
|
801
954
|
historyData
|
|
802
955
|
});
|
|
956
|
+
await updateLastUpdatedTimestamp(db, collectionRegistry, String(cid), 'signedInUser', 'tradeHistory', logger);
|
|
803
957
|
}
|
|
804
958
|
historyFetched = true;
|
|
805
959
|
} catch (batchErr) {
|
|
@@ -832,9 +986,9 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
832
986
|
logger.log('INFO', `[On-Demand Update] Skipping history fetch (portfolioOnly=false) for ${username}`);
|
|
833
987
|
}
|
|
834
988
|
|
|
835
|
-
// Fetch social data
|
|
989
|
+
// Fetch social data for on-demand syncs
|
|
836
990
|
let socialFetched = false;
|
|
837
|
-
if (username) {
|
|
991
|
+
if (includeSocial && username) {
|
|
838
992
|
try {
|
|
839
993
|
logger.log('INFO', `[On-Demand Update] Fetching social data for ${username} (${cid})...`);
|
|
840
994
|
|
|
@@ -919,6 +1073,9 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
919
1073
|
date: today,
|
|
920
1074
|
posts: postsToStore
|
|
921
1075
|
});
|
|
1076
|
+
|
|
1077
|
+
// Update last updated timestamp for social posts
|
|
1078
|
+
await updateLastUpdatedTimestamp(db, collectionRegistry, String(cid), 'signedInUser', 'socialPosts', logger);
|
|
922
1079
|
}
|
|
923
1080
|
|
|
924
1081
|
// Update date tracking document (for root data indexer)
|