bulltrackers-module 1.0.514 → 1.0.515

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.
@@ -256,52 +256,85 @@ async function findLatestRankingsDate(db, rankingsCollection, maxDaysBack = 30)
256
256
 
257
257
  /**
258
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.
259
+ * UPDATED: Checks last 7 days of rankings data and builds a de-duplicated map.
260
+ * This solves the problem of a single day where a given popular investor is not in the rankings.
261
+ * UPDATED: Uses collection registry for path resolution.
261
262
  */
262
263
  async function getPopularInvestorsToUpdate(dependencies, config) {
263
- const { db, logger } = dependencies;
264
- const collectionName = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
264
+ const { db, logger, collectionRegistry } = dependencies;
265
+
266
+ // Get collection name from registry if available
267
+ let collectionName = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
268
+
269
+ if (collectionRegistry && collectionRegistry.getCollectionPath) {
270
+ try {
271
+ // Extract collection name from registry path: popular_investor_rankings/{date}
272
+ const samplePath = collectionRegistry.getCollectionPath('rootData', 'popularInvestorRankings', { date: '2025-01-01' });
273
+ collectionName = samplePath.split('/')[0];
274
+ } catch (err) {
275
+ logger.log('WARN', `[Core Utils] Failed to get collection from registry, using config: ${err.message}`);
276
+ }
277
+ }
265
278
 
266
- logger.log('INFO', `[Core Utils] Getting Popular Investors to update from ${collectionName}...`);
279
+ logger.log('INFO', `[Core Utils] Getting Popular Investors to update from ${collectionName} (checking last 7 days)...`);
267
280
 
268
- // Try today's date first
269
- const today = new Date().toISOString().split('T')[0];
270
- let rankingsDate = today;
281
+ // Check last 7 days of rankings data
282
+ const today = new Date();
283
+ const piMap = new Map(); // De-duplicated map: cid -> { cid, username, latestDate }
271
284
 
272
285
  try {
273
- let docRef = db.collection(collectionName).doc(today);
274
- let docSnap = await docRef.get();
275
-
276
- // If today's rankings don't exist, fallback to latest available date
277
- if (!docSnap.exists) {
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();
286
+ // Check each of the last 7 days
287
+ for (let i = 0; i < 7; i++) {
288
+ const checkDate = new Date(today);
289
+ checkDate.setDate(checkDate.getDate() - i);
290
+ const dateStr = checkDate.toISOString().split('T')[0];
289
291
 
290
- if (!docSnap.exists) {
291
- logger.log('WARN', `[Core Utils] Rankings doc does not exist for fallback date ${rankingsDate}`);
292
- return [];
292
+ try {
293
+ const docRef = db.collection(collectionName).doc(dateStr);
294
+ const docSnap = await docRef.get();
295
+
296
+ if (docSnap.exists) {
297
+ const data = docSnap.data();
298
+ const items = data.Items || [];
299
+
300
+ // Add all PIs from this date to the map (de-duplicated by CID)
301
+ for (const item of items) {
302
+ const cid = String(item.CustomerId);
303
+ const username = item.UserName;
304
+
305
+ // Only add if not already in map, or if this date is more recent
306
+ if (!piMap.has(cid)) {
307
+ piMap.set(cid, {
308
+ cid: cid,
309
+ username: username,
310
+ latestDate: dateStr
311
+ });
312
+ } else {
313
+ // Update if this date is more recent
314
+ const existing = piMap.get(cid);
315
+ if (dateStr > existing.latestDate) {
316
+ existing.latestDate = dateStr;
317
+ if (username) existing.username = username;
318
+ }
319
+ }
320
+ }
321
+ }
322
+ } catch (dateErr) {
323
+ logger.log('WARN', `[Core Utils] Error checking rankings for date ${dateStr}: ${dateErr.message}`);
324
+ continue; // Continue to next date
293
325
  }
294
326
  }
327
+
328
+ if (piMap.size === 0) {
329
+ logger.log('WARN', `[Core Utils] No Popular Investor rankings found in last 7 days. Returning empty array.`);
330
+ return [];
331
+ }
332
+
333
+ logger.log('INFO', `[Core Utils] Found ${piMap.size} unique Popular Investors across last 7 days of rankings.`);
295
334
 
296
- const data = docSnap.data();
297
- const items = data.Items || [];
298
-
299
- // Map to { cid, username }
300
- const allTargets = items.map(item => ({
301
- cid: String(item.CustomerId),
302
- username: item.UserName
303
- }));
304
-
335
+ // Convert map to array and filter out PIs updated in the last 18 hours
336
+ const allTargets = Array.from(piMap.values());
337
+
305
338
  // Filter out PIs updated in the last 18 hours
306
339
  const HOURS_THRESHOLD = 18;
307
340
  const thresholdMs = HOURS_THRESHOLD * 60 * 60 * 1000;
@@ -310,12 +343,29 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
310
343
  const filteredTargets = [];
311
344
  let skippedCount = 0;
312
345
 
313
- // Check each PI's last sync time
346
+ // Check each PI's last sync time using user-centric path
314
347
  for (const target of allTargets) {
315
- const syncRef = db.collection('user_sync_requests')
316
- .doc(target.cid)
317
- .collection('global')
318
- .doc('latest');
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
+ }
319
369
 
320
370
  const syncDoc = await syncRef.get();
321
371
  if (syncDoc.exists) {
@@ -336,7 +386,7 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
336
386
  logger.log('INFO', `[Core Utils Debug] First PI Target Mapped: ${JSON.stringify(filteredTargets[0])}`);
337
387
  }
338
388
 
339
- logger.log('INFO', `[Core Utils] Found ${allTargets.length} Popular Investors from ${rankingsDate} ranking${rankingsDate !== today ? ` (fallback from ${today})` : ''}. Skipped ${skippedCount} recently updated. ${filteredTargets.length} will be updated.`);
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.`);
340
390
  return filteredTargets;
341
391
 
342
392
  } catch (error) {
@@ -347,21 +397,37 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
347
397
 
348
398
  /**
349
399
  * [NEW] Fetches Signed-In Users for daily update.
400
+ * UPDATED: Uses user-centric collection model and collection registry.
350
401
  * Skips users updated in the last 18 hours to avoid duplicate refreshes.
351
402
  */
352
403
  async function getSignedInUsersToUpdate(dependencies, config) {
353
- const { db, logger } = dependencies;
354
- const collectionName = config.signedInUsersCollection || process.env.FIRESTORE_COLLECTION_SIGNED_IN_USER_PORTFOLIOS || 'signed_in_users';
404
+ const { db, logger, collectionRegistry } = dependencies;
405
+
406
+ // Get collection name from registry if available
407
+ let signedInUsersCollection = config.signedInUsersCollection || 'signedInUsers';
408
+
409
+ if (collectionRegistry && collectionRegistry.getCollectionPath) {
410
+ try {
411
+ // Extract collection name from registry path: signedInUsers/{firebaseUid}
412
+ const samplePath = collectionRegistry.getCollectionPath('signedInUsers', 'main', {});
413
+ signedInUsersCollection = samplePath.split('/')[0];
414
+ } catch (err) {
415
+ logger.log('WARN', `[Core Utils] Failed to get collection from registry, using config: ${err.message}`);
416
+ }
417
+ }
355
418
 
356
419
  // 18 hours threshold - skip users updated more recently than this
357
420
  const HOURS_THRESHOLD = 18;
358
421
  const thresholdMs = HOURS_THRESHOLD * 60 * 60 * 1000;
359
422
  const now = Date.now();
423
+ const today = new Date().toISOString().split('T')[0];
360
424
 
361
- logger.log('INFO', `[Core Utils] Getting Signed-In Users to update from ${collectionName} (skipping users updated in last ${HOURS_THRESHOLD} hours)...`);
425
+ logger.log('INFO', `[Core Utils] Getting Signed-In Users to update from ${signedInUsersCollection} (skipping users updated in last ${HOURS_THRESHOLD} hours)...`);
362
426
 
363
427
  try {
364
- const snapshot = await db.collection(collectionName).get();
428
+ // Get all signed-in users from the main collection
429
+ // This collection maps Firebase UID to eToro CID
430
+ const snapshot = await db.collection(signedInUsersCollection).get();
365
431
 
366
432
  if (snapshot.empty) {
367
433
  logger.log('INFO', '[Core Utils] No Signed-In Users found.');
@@ -369,74 +435,67 @@ async function getSignedInUsersToUpdate(dependencies, config) {
369
435
  }
370
436
 
371
437
  const targets = [];
372
- let skippedCount = 0;
373
438
 
439
+ // Extract CIDs from user documents
374
440
  snapshot.forEach(doc => {
375
441
  const data = doc.data();
376
- const cid = data.cid || doc.id;
377
- const username = data.username || 'Unknown';
442
+ const cid = data.etoroCID || data.cid || null;
443
+ const username = data.etoroUsername || data.username || 'Unknown';
378
444
 
379
445
  if (!cid) return;
380
446
 
381
- // Check if user was recently updated (check sync requests and portfolio timestamps)
382
- // First check sync requests for last update time
383
- let shouldSkip = false;
384
-
385
- // Check user_sync_requests for last completed sync
386
- // We'll check this asynchronously, but for now we'll check portfolio data timestamps
387
- // The portfolio data is stored in snapshots with dates, so we can check the latest snapshot date
388
- const portfolioCollection = collectionName; // Same collection
389
- const blockId = '19M';
390
- const today = new Date().toISOString().split('T')[0];
391
-
392
- // For efficiency, we'll check the sync requests collection for the last update time
393
- // This is a synchronous check we can do here
394
- // Actually, we need to make this async, so we'll do a batch check after
395
-
396
- // For now, include all users - we'll filter in the dispatcher
397
447
  targets.push({ cid: String(cid), username });
398
448
  });
399
449
 
400
- // Now filter out users who were updated today
401
- // Check actual portfolio/history snapshot dates, not just sync request timestamps
450
+ logger.log('INFO', `[Core Utils] Found ${targets.length} Signed-In Users. Checking update status...`);
451
+
452
+ // Filter out users who were updated recently
402
453
  const filteredTargets = [];
403
- const today = new Date().toISOString().split('T')[0];
454
+ let skippedCount = 0;
455
+
404
456
  const checkPromises = targets.map(async (target) => {
405
- // Check if user has portfolio or history data for today
406
- const blockId = `${Math.floor(parseInt(target.cid) / 1000000)}M`;
407
-
408
- // Check portfolio snapshot for today
409
- const portfolioSnapshotRef = db.collection(collectionName)
410
- .doc(blockId)
411
- .collection('snapshots')
412
- .doc(today)
413
- .collection('parts')
414
- .limit(1);
415
-
416
- const portfolioSnapshot = await portfolioSnapshotRef.get();
457
+ // Check if user has portfolio data for today (using root data format)
458
+ let hasTodayData = false;
417
459
 
418
- // Check history snapshot for today
419
- const historyCollection = config.signedInUserHistoryCollection || process.env.FIRESTORE_COLLECTION_SIGNED_IN_USER_HISTORY || 'signed_in_user_history';
420
- const historySnapshotRef = db.collection(historyCollection)
421
- .doc(blockId)
422
- .collection('snapshots')
423
- .doc(today)
424
- .collection('parts')
425
- .limit(1);
426
-
427
- const historySnapshot = await historySnapshotRef.get();
428
-
429
- // If user has data for today, skip them (they're already up-to-date)
430
- if (!portfolioSnapshot.empty || !historySnapshot.empty) {
431
- skippedCount++;
432
- return null; // Skip this user - already updated today
460
+ if (collectionRegistry && collectionRegistry.getCollectionPath) {
461
+ try {
462
+ // Check root data portfolio for today
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
+ }
473
+ } catch (err) {
474
+ logger.log('WARN', `[Core Utils] Error checking portfolio for ${target.cid}: ${err.message}`);
475
+ }
433
476
  }
434
477
 
435
- // Also check sync requests as a fallback (in case data exists but snapshot check failed)
436
- const syncRef = db.collection('user_sync_requests')
437
- .doc(target.cid)
438
- .collection('global')
439
- .doc('latest');
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');
491
+ }
492
+ } else {
493
+ // Fallback to legacy path
494
+ syncRef = db.collection('user_sync_requests')
495
+ .doc(target.cid)
496
+ .collection('global')
497
+ .doc('latest');
498
+ }
440
499
 
441
500
  const syncDoc = await syncRef.get();
442
501
  if (syncDoc.exists) {
@@ -446,10 +505,16 @@ async function getSignedInUsersToUpdate(dependencies, config) {
446
505
 
447
506
  if (lastRequestedAt && (now - lastRequestedAt) < thresholdMs) {
448
507
  skippedCount++;
449
- return null; // Skip this user
508
+ return null; // Skip this user - recently updated
450
509
  }
451
510
  }
452
511
 
512
+ // If user has data for today, skip them (they're already up-to-date)
513
+ if (hasTodayData) {
514
+ skippedCount++;
515
+ return null; // Skip this user - already updated today
516
+ }
517
+
453
518
  return target; // Include this user - needs update
454
519
  });
455
520
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.514",
3
+ "version": "1.0.515",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [