bulltrackers-module 1.0.631 → 1.0.632

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.
@@ -20,17 +20,32 @@ class ContextFactory {
20
20
 
21
21
  static buildPerUserContext(options) {
22
22
  const {
23
- todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory,
24
- userId, userType, dateStr, metadata, mappings, insights, socialData,
25
- computedDependencies, previousComputedDependencies, config, deps,
23
+ todayPortfolio,
24
+ yesterdayPortfolio,
25
+ todayHistory,
26
+ yesterdayHistory,
27
+ userId,
28
+ userType,
29
+ dateStr,
30
+ metadata,
31
+ mappings,
32
+ insights,
33
+ socialData,
34
+ computedDependencies,
35
+ previousComputedDependencies,
36
+ config,
37
+ deps,
26
38
  verification,
27
- rankings, yesterdayRankings, // User-specific rank entries
28
- allRankings, allRankingsYesterday, // Global rank lists
39
+ rankings,
40
+ yesterdayRankings,
41
+ allRankings,
42
+ allRankingsYesterday,
29
43
  allVerifications,
30
- // [NEW] New Root Data Types for Profile Metrics
31
- ratings, pageViews, watchlistMembership, alertHistory,
44
+ ratings,
45
+ pageViews,
46
+ watchlistMembership,
47
+ alertHistory,
32
48
  piMasterList,
33
- // [NEW] Series Data (Lookback for Root Data or Computation Results)
34
49
  seriesData
35
50
  } = options;
36
51
 
@@ -56,14 +71,11 @@ class ContextFactory {
56
71
  rankings: allRankings || [],
57
72
  rankingsYesterday: allRankingsYesterday || [],
58
73
  verifications: allVerifications || {},
59
- // [NEW] New Root Data Types for Profile Metrics
60
74
  ratings: ratings || {},
61
75
  pageViews: pageViews || {},
62
76
  watchlistMembership: watchlistMembership || {},
63
77
  alertHistory: alertHistory || {},
64
78
  piMasterList: piMasterList || {},
65
- // [NEW] Expose Series Data
66
- // Structure: { root: { [type]: { [date]: data } }, results: { [date]: { [calcName]: data } } }
67
79
  series: seriesData || {}
68
80
  }
69
81
  };
@@ -71,13 +83,23 @@ class ContextFactory {
71
83
 
72
84
  static buildMetaContext(options) {
73
85
  const {
74
- dateStr, metadata, mappings, insights, socialData, prices,
75
- computedDependencies, previousComputedDependencies, config, deps,
76
- allRankings, allRankingsYesterday,
86
+ dateStr,
87
+ metadata,
88
+ mappings,
89
+ insights,
90
+ socialData,
91
+ prices,
92
+ computedDependencies,
93
+ previousComputedDependencies,
94
+ config,
95
+ deps,
96
+ allRankings,
97
+ allRankingsYesterday,
77
98
  allVerifications,
78
- // [NEW] New Root Data Types
79
- ratings, pageViews, watchlistMembership, alertHistory,
80
- // [NEW] Series Data
99
+ ratings,
100
+ pageViews,
101
+ watchlistMembership,
102
+ alertHistory,
81
103
  seriesData
82
104
  } = options;
83
105
 
@@ -99,7 +121,6 @@ class ContextFactory {
99
121
  pageViews: pageViews || {},
100
122
  watchlistMembership: watchlistMembership || {},
101
123
  alertHistory: alertHistory || {},
102
- // [NEW] Expose Series Data
103
124
  series: seriesData || {}
104
125
  }
105
126
  };
@@ -30,11 +30,11 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
30
30
  let isAvailable = false;
31
31
 
32
32
  if (dep === 'portfolio') {
33
- if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
34
- else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
33
+ if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
34
+ else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
35
35
  else if (userType === 'popular_investor' && rootDataStatus.piPortfolios) isAvailable = true;
36
- else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
37
- else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
36
+ else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
37
+ else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
38
38
 
39
39
  if (!isAvailable) {
40
40
  // [OPTIMIZATION] Optimistic Series Check
@@ -284,12 +284,28 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
284
284
  logger.log('WARN', `[Availability] Index not found for ${dateStr}. Assuming NO data.`);
285
285
  return {
286
286
  status: {
287
- hasPortfolio: false, hasHistory: false, hasSocial: false, hasInsights: false, hasPrices: false,
288
- speculatorPortfolio: false, normalPortfolio: false, speculatorHistory: false, normalHistory: false,
289
- piRankings: false, piPortfolios: false, piDeepPortfolios: false, piHistory: false,
290
- signedInUserPortfolio: false, signedInUserHistory: false, signedInUserVerification: false,
291
- hasPISocial: false, hasSignedInSocial: false,
292
- piRatings: false, piPageViews: false, watchlistMembership: false, piAlertHistory: false
287
+ hasPortfolio: false,
288
+ hasHistory: false,
289
+ hasSocial: false,
290
+ hasInsights: false,
291
+ hasPrices: false,
292
+ speculatorPortfolio: false,
293
+ normalPortfolio: false,
294
+ speculatorHistory: false,
295
+ normalHistory: false,
296
+ piRankings: false,
297
+ piPortfolios: false,
298
+ piDeepPortfolios: false,
299
+ piHistory: false,
300
+ signedInUserPortfolio: false,
301
+ signedInUserHistory: false,
302
+ signedInUserVerification: false,
303
+ hasPISocial: false,
304
+ hasSignedInSocial: false,
305
+ piRatings: false,
306
+ piPageViews: false,
307
+ watchlistMembership: false,
308
+ piAlertHistory: false
293
309
  }
294
310
  };
295
311
  }
@@ -361,5 +377,5 @@ module.exports = {
361
377
  checkRootDependencies,
362
378
  checkRootDataAvailability,
363
379
  getViableCalculations,
364
- getAvailabilityWindow // [NEW] Exported
380
+ getAvailabilityWindow
365
381
  };
@@ -10,7 +10,7 @@ const { normalizeName } = require('../utils/utils');
10
10
  const { CachedDataLoader } = require('../data/CachedDataLoader');
11
11
  const { ContextFactory } = require('../context/ContextFactory');
12
12
  const { commitResults } = require('../persistence/ResultCommitter');
13
- const { fetchResultSeries } = require('../data/DependencyFetcher'); // [NEW] Import series fetcher
13
+ const { fetchResultSeries } = require('../data/DependencyFetcher');
14
14
 
15
15
  class MetaExecutor {
16
16
  static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
@@ -47,6 +47,8 @@ class MetaExecutor {
47
47
  let ratings = null, pageViews = null, watchlistMembership = null, alertHistory = null;
48
48
  if (needsNewRootData) {
49
49
  const loadPromises = [];
50
+
51
+ // Ratings
50
52
  if (calcs.some(c => c.rootDataDependencies?.includes('ratings'))) {
51
53
  loadPromises.push(loader.loadRatings(dStr).then(r => { ratings = r; }).catch(e => {
52
54
  // Only catch if ALL calcs allow missing roots
@@ -61,6 +63,8 @@ class MetaExecutor {
61
63
  }
62
64
  }));
63
65
  }
66
+
67
+ // PageViews
64
68
  if (calcs.some(c => c.rootDataDependencies?.includes('pageViews'))) {
65
69
  loadPromises.push(loader.loadPageViews(dStr).then(pv => { pageViews = pv; }).catch(e => {
66
70
  const allAllowMissing = calcs.every(c => {
@@ -74,6 +78,8 @@ class MetaExecutor {
74
78
  }
75
79
  }));
76
80
  }
81
+
82
+ // Watchlist
77
83
  if (calcs.some(c => c.rootDataDependencies?.includes('watchlist'))) {
78
84
  loadPromises.push(loader.loadWatchlistMembership(dStr).then(w => { watchlistMembership = w; }).catch(e => {
79
85
  const allAllowMissing = calcs.every(c => {
@@ -87,6 +93,8 @@ class MetaExecutor {
87
93
  }
88
94
  }));
89
95
  }
96
+
97
+ // Alerts
90
98
  if (calcs.some(c => c.rootDataDependencies?.includes('alerts'))) {
91
99
  loadPromises.push(loader.loadAlertHistory(dStr).then(a => { alertHistory = a; }).catch(e => {
92
100
  const allAllowMissing = calcs.every(c => {
@@ -100,6 +108,7 @@ class MetaExecutor {
100
108
  }
101
109
  }));
102
110
  }
111
+
103
112
  await Promise.all(loadPromises);
104
113
 
105
114
  // [FIX] Enforce canHaveMissingRoots - validate after loading
@@ -163,7 +172,8 @@ class MetaExecutor {
163
172
  else if (type === 'insights') loaderMethod = 'loadInsights';
164
173
  else if (type === 'ratings') loaderMethod = 'loadRatings';
165
174
  else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
166
- // Add other root types if needed
175
+ // [CRITICAL UPDATE] Add rankings support for Meta lookbacks
176
+ else if (type === 'rankings') loaderMethod = 'loadRankings';
167
177
 
168
178
  if (loaderMethod) {
169
179
  logger.log('INFO', `[MetaExecutor] Loading ${days}-day series for Root Data '${type}'...`);
@@ -222,7 +232,8 @@ class MetaExecutor {
222
232
  }
223
233
  }
224
234
 
225
- return await commitResults(state, dStr, passName, config, deps);
235
+ // CRITICAL FIX: Pass 'isInitialWrite: true' to ensure proper cleanup of old meta data
236
+ return await commitResults(state, dStr, passName, config, deps, false, { isInitialWrite: true });
226
237
  }
227
238
 
228
239
  static async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps, config, deps, loader) {
@@ -1,15 +1,8 @@
1
- /**
2
- * {
3
- * type: uploaded file
4
- * fileName: computation-system/executors/StandardExecutor.js
5
- * }
6
- */
7
1
  const { normalizeName, getEarliestDataDates } = require('../utils/utils');
8
2
  const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
9
3
  const { CachedDataLoader } = require('../data/CachedDataLoader');
10
4
  const { ContextFactory } = require('../context/ContextFactory');
11
5
  const { commitResults } = require('../persistence/ResultCommitter');
12
- // [NEW] Import series fetcher for computation results
13
6
  const { fetchResultSeries } = require('../data/DependencyFetcher');
14
7
  const mathLayer = require('../layers/index');
15
8
  const { performance } = require('perf_hooks');
@@ -167,7 +160,8 @@ class StandardExecutor {
167
160
  else if (type === 'insights') loaderMethod = 'loadInsights';
168
161
  else if (type === 'ratings') loaderMethod = 'loadRatings';
169
162
  else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
170
- // Add other root types as needed...
163
+ // [CRITICAL UPDATE] Add rankings support for AUM lookback
164
+ else if (type === 'rankings') loaderMethod = 'loadRankings';
171
165
 
172
166
  if (loaderMethod) {
173
167
  logger.log('INFO', `[StandardExecutor] Loading ${days}-day series for Root Data '${type}'...`);
@@ -487,4 +481,4 @@ class StandardExecutor {
487
481
  }
488
482
  }
489
483
 
490
- module.exports = { StandardExecutor };
484
+ module.exports = { StandardExecutor };
@@ -4,6 +4,7 @@
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
6
  * UPDATED: Fixed issue where switching to 'isPage' mode didn't clean up old sharded/raw data.
7
+ * CRITICAL FIX: Fixed sharding logic to prevent wiping existing shards during INTERMEDIATE flushes.
7
8
  */
8
9
  const { commitBatchInChunks, generateDataHash, FieldValue } = require('../utils/utils')
9
10
  const { updateComputationStatus } = require('./StatusRepository');
@@ -138,8 +139,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
138
139
  continue;
139
140
  }
140
141
 
141
- // [NEW] Page Computation Logic (Fan-Out) with TTL
142
- // Bypasses standard compression/sharding to write per-user documents
143
142
  // [NEW] Page Computation Logic (Fan-Out) with TTL
144
143
  // Bypasses standard compression/sharding to write per-user documents
145
144
  if (isPageComputation && !isEmpty) {
@@ -429,7 +428,9 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
429
428
  let finalStats = { totalSize: 0, isSharded: false, shardCount: 1, nextShardIndex: startShardIndex };
430
429
  let rootMergeOption = !isInitialWrite;
431
430
 
432
- let shouldWipeShards = wasSharded;
431
+ // CRITICAL FIX: Only wipe existing shards if this is the INITIAL write for this batch run.
432
+ // If we are flushing intermediate chunks, we should NOT wipe the shards created by previous chunks!
433
+ let shouldWipeShards = wasSharded && isInitialWrite;
433
434
 
434
435
  for (let attempt = 0; attempt < strategies.length; attempt++) {
435
436
  if (committed) break;
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @fileoverview Data loader sub-pipes for the Computation System.
3
3
  * REFACTORED: Now stateless and receive dependencies.
4
- * FIXED: Added strict userType filtering to prevent fetching unnecessary data (e.g. Normal users for PI calcs).
5
- * --- NEW: Updated to read PI/Signed-In data from SHARDS (Parts) instead of individual docs. ---
6
- * --- NEW: Logic to merge Overall and Deep PI data from corresponding shards. ---
7
- * --- UPDATED: Added loaders for Rankings and Verification data. ---
4
+ * FIXED: Added strict userType filtering to prevent fetching unnecessary data.
5
+ * UPDATED: Verification now uses CollectionGroup query due to per-user storage.
6
+ * UPDATED: Ratings now correctly handles flattened top-level schema (keys like "reviews.ID").
7
+ * REMOVED: Redundant Price Shard Indexing logic.
8
8
  */
9
9
  const zlib = require('zlib');
10
10
 
@@ -23,11 +23,9 @@ function tryDecompress(data) {
23
23
 
24
24
  /** --- Data Loader Sub-Pipes (Stateless, Dependency-Injection) --- */
25
25
 
26
- /** * Stage 1: Get portfolio part document references for a given date
27
- * [UPDATED] Accepts requiredUserTypes to filter collections.
28
- */
26
+ /** Stage 1: Get portfolio part document references for a given date */
29
27
  async function getPortfolioPartRefs(config, deps, dateString, requiredUserTypes = null) {
30
- const { db, logger, calculationUtils, collectionRegistry } = deps;
28
+ const { db, logger, calculationUtils } = deps;
31
29
  const { withRetry } = calculationUtils;
32
30
 
33
31
  // Normalize required types. If null/empty or contains 'ALL', fetch everything.
@@ -250,7 +248,6 @@ async function loadDailyInsights(config, deps, dateString) {
250
248
  async function loadDailySocialPostInsights(config, deps, dateString) {
251
249
  const { db, logger, calculationUtils, collectionRegistry } = deps;
252
250
  const { withRetry } = calculationUtils;
253
- const { getCollectionPath } = collectionRegistry || {};
254
251
 
255
252
  logger.log('INFO', `Loading and partitioning social data for ${dateString}`);
256
253
 
@@ -262,7 +259,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
262
259
  };
263
260
 
264
261
  // NEW STRUCTURE: Read from date-based collections
265
- // Structure: Collection/{date}/{cid}/{cid} for user social, Collection/{date}/posts/{postId} for instrument
266
262
  try {
267
263
  // Signed-In User Social: SignedInUserSocialPostData/{date}/{cid}/{cid}
268
264
  const signedInSocialCollectionName = 'SignedInUserSocialPostData';
@@ -279,7 +275,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
279
275
  const cidData = tryDecompress(cidDoc.data());
280
276
  if (cidData.posts && typeof cidData.posts === 'object') {
281
277
  if (!result.signedIn[cid]) result.signedIn[cid] = {};
282
- // Posts are stored as a map in the document
283
278
  Object.assign(result.signedIn[cid], cidData.posts);
284
279
  }
285
280
  }
@@ -328,13 +323,10 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
328
323
  const PI_COL_NAME = config.piSocialCollectionName || config.piSocialCollection || 'pi_social_posts';
329
324
  const SIGNED_IN_COL_NAME = config.signedInUserSocialCollection || 'signed_in_users_social';
330
325
 
331
- // 2. Define Time Range (UTC Day)
332
326
  const startDate = new Date(dateString + 'T00:00:00Z');
333
327
  const endDate = new Date(dateString + 'T23:59:59Z');
334
328
 
335
329
  try {
336
- // 3. Fetch ALL with CollectionGroup
337
- // NOTE: Requires Firestore Index: CollectionId 'posts', Field 'fetchedAt' (ASC/DESC)
338
330
  const postsQuery = db.collectionGroup('posts')
339
331
  .where('fetchedAt', '>=', startDate)
340
332
  .where('fetchedAt', '<=', endDate);
@@ -346,9 +338,7 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
346
338
  const data = tryDecompress(doc.data());
347
339
  const path = doc.ref.path;
348
340
 
349
- // 4. Partition Logic based on Path
350
341
  if (path.includes(PI_COL_NAME)) {
351
- // Path format: .../pi_social_posts/{userId}/posts/{postId}
352
342
  const parts = path.split('/');
353
343
  const colIndex = parts.indexOf(PI_COL_NAME);
354
344
  if (colIndex !== -1 && parts[colIndex + 1]) {
@@ -358,7 +348,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
358
348
  }
359
349
  }
360
350
  else if (path.includes(SIGNED_IN_COL_NAME)) {
361
- // Path format: .../signed_in_users_social/{userId}/posts/{postId}
362
351
  const parts = path.split('/');
363
352
  const colIndex = parts.indexOf(SIGNED_IN_COL_NAME);
364
353
  if (colIndex !== -1 && parts[colIndex + 1]) {
@@ -368,7 +357,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
368
357
  }
369
358
  }
370
359
  else {
371
- // Default: Generic Instrument Posts
372
360
  result.generic[doc.id] = data;
373
361
  }
374
362
  });
@@ -384,11 +372,9 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
384
372
  return result;
385
373
  }
386
374
 
387
- /** * Stage 6: Get history part references for a given date
388
- * [UPDATED] Accepts requiredUserTypes to filter collections.
389
- */
375
+ /** Stage 6: Get history part references for a given date */
390
376
  async function getHistoryPartRefs(config, deps, dateString, requiredUserTypes = null) {
391
- const { db, logger, calculationUtils, collectionRegistry } = deps;
377
+ const { db, logger, calculationUtils } = deps;
392
378
  const { withRetry } = calculationUtils;
393
379
 
394
380
  // Normalize required types
@@ -478,9 +464,7 @@ async function getHistoryPartRefs(config, deps, dateString, requiredUserTypes =
478
464
  return allPartRefs;
479
465
  }
480
466
 
481
- /** * Stage 7: Stream portfolio data in chunks
482
- * [UPDATED] Passes requiredUserTypes to getPortfolioPartRefs
483
- */
467
+ /** Stage 7: Stream portfolio data in chunks */
484
468
  async function* streamPortfolioData(config, deps, dateString, providedRefs = null, requiredUserTypes = null) {
485
469
  const { logger } = deps;
486
470
  const refs = providedRefs || (await getPortfolioPartRefs(config, deps, dateString, requiredUserTypes));
@@ -497,9 +481,7 @@ async function* streamPortfolioData(config, deps, dateString, providedRefs = nul
497
481
  logger.log('INFO', `[streamPortfolioData] Finished streaming for ${dateString}.`);
498
482
  }
499
483
 
500
- /** * Stage 8: Stream history data in chunks
501
- * [UPDATED] Passes requiredUserTypes to getHistoryPartRefs
502
- */
484
+ /** Stage 8: Stream history data in chunks */
503
485
  async function* streamHistoryData(config, deps, dateString, providedRefs = null, requiredUserTypes = null) {
504
486
  const { logger } = deps;
505
487
  const refs = providedRefs || (await getHistoryPartRefs(config, deps, dateString, requiredUserTypes));
@@ -531,80 +513,22 @@ async function getPriceShardRefs(config, deps) {
531
513
  }
532
514
  }
533
515
 
534
- /** Stage 10: Smart Shard Lookup System */
516
+ /** Stage 10: Smart Shard Lookup System (DEPRECATED/SIMPLIFIED) */
535
517
  async function ensurePriceShardIndex(config, deps) {
536
- const { db, logger } = deps;
537
- const metadataCol = config.metadataCollection || 'system_metadata';
538
- const indexDocRef = db.collection(metadataCol).doc('price_shard_index');
539
-
540
- // 1. Try to fetch existing index
541
- const snap = await indexDocRef.get();
542
- if (snap.exists) {
543
- const data = snap.data();
544
- const lastUpdated = data.lastUpdated ? new Date(data.lastUpdated).getTime() : 0;
545
- const now = Date.now();
546
- const oneDayMs = 24 * 60 * 60 * 1000;
547
- if ((now - lastUpdated) < oneDayMs) { return data.index || {}; }
548
- logger.log('INFO', '[ShardIndex] Index is stale (>24h). Rebuilding...');
549
- } else {
550
- logger.log('INFO', '[ShardIndex] Index not found. Building new Price Shard Index...');
551
- }
552
-
553
- // 2. Build Index
554
- const collection = config.priceCollection || 'asset_prices';
555
- const snapshot = await db.collection(collection).get();
556
-
557
- const index = {};
558
- let shardCount = 0;
559
-
560
- snapshot.forEach(doc => {
561
- shardCount++;
562
- const rawData = doc.data();
563
- const data = tryDecompress(rawData);
564
-
565
- if (data.history) {
566
- Object.keys(data.history).forEach(instId => {
567
- index[instId] = doc.id;
568
- });
569
- }
570
- });
571
-
572
- // 3. Save Index
573
- await indexDocRef.set({
574
- index: index,
575
- lastUpdated: new Date().toISOString(),
576
- shardCount: shardCount
577
- });
578
-
579
- logger.log('INFO', `[ShardIndex] Built index for ${Object.keys(index).length} instruments across ${shardCount} shards.`);
580
- return index;
518
+ // [DEPRECATED] This function previously built an index in 'system_metadata/price_shard_index'.
519
+ // It has been removed to avoid performing computation/indexing in the data loader.
520
+ // Use 'Fetch All' strategy in Stage 9 instead.
521
+ return {};
581
522
  }
582
523
 
583
524
  async function getRelevantShardRefs(config, deps, targetInstrumentIds) {
584
- const { db, logger } = deps;
585
-
586
- if (!targetInstrumentIds || targetInstrumentIds.length === 0) {
587
- return getPriceShardRefs(config, deps);
588
- }
589
-
590
- logger.log('INFO', `[ShardLookup] Resolving shards for ${targetInstrumentIds.length} specific instruments...`);
591
-
592
- const index = await ensurePriceShardIndex(config, deps);
593
- const uniqueShardIds = new Set();
594
- const collection = config.priceCollection || 'asset_prices';
595
-
596
- let foundCount = 0;
597
- for (const id of targetInstrumentIds) {
598
- const shardId = index[id];
599
- if (shardId) {
600
- uniqueShardIds.add(shardId);
601
- foundCount++;
602
- }
603
- }
604
-
605
- logger.log('INFO', `[ShardLookup] Mapped ${foundCount}/${targetInstrumentIds.length} instruments to ${uniqueShardIds.size} unique shards.`);
525
+ const { logger } = deps;
606
526
 
607
- return Array.from(uniqueShardIds).map(id => db.collection(collection).doc(id));
527
+ // [UPDATED] Smart shard lookup is disabled due to missing index infrastructure
528
+ // and to avoid computing indexes during load time.
529
+ // Falling back to Stage 9 (Fetch All Shards).
530
+ logger.log('INFO', `[ShardLookup] Smart indexing disabled. Fetching all price shards for ${targetInstrumentIds ? targetInstrumentIds.length : 'all'} instruments.`);
531
+ return getPriceShardRefs(config, deps);
608
532
  }
609
533
 
610
534
  /** Stage 11: Load Popular Investor Rankings */
@@ -625,40 +549,58 @@ async function loadPopularInvestorRankings(config, deps, dateString) {
625
549
  }
626
550
 
627
551
  const data = tryDecompress(docSnap.data());
628
- return data.Items || []; // Returns the array of PI objects
552
+ return data.Items || [];
629
553
  } catch (error) {
630
554
  logger.log('ERROR', `Failed to load Rankings for ${dateString}: ${error.message}`);
631
555
  return null;
632
556
  }
633
557
  }
634
558
 
635
- /** Stage 12: Load User Verification Profiles */
559
+ /** Stage 12: Load User Verification Profiles
560
+ * [UPDATED] Scans global verification data via CollectionGroup since it's now stored per-user.
561
+ */
636
562
  async function loadVerificationProfiles(config, deps) {
637
563
  const { db, logger, calculationUtils } = deps;
638
564
  const { withRetry } = calculationUtils;
639
- const collectionName = config.verificationCollection || 'verified_users';
640
565
 
641
- logger.log('INFO', `Loading Verification Profiles`);
566
+ // Verification is now stored at /SignedInUsers/{cid}/verification/data
567
+ // To fetch globally, we must use a CollectionGroup query on 'verification'
568
+ // and filter for the document ID 'data'.
569
+
570
+ logger.log('INFO', `Loading Verification Profiles (CollectionGroup: verification/data)`);
642
571
 
643
572
  try {
644
- const snapshot = await withRetry(() => db.collection(collectionName).get(), 'getVerifications');
573
+ // Warning: This requires a Firestore Index if used with complex filters, but basic get() usually works.
574
+ const snapshot = await withRetry(() => db.collectionGroup('verification').get(), 'getVerificationsGroup');
645
575
 
646
576
  if (snapshot.empty) return {};
647
577
 
648
578
  const profiles = {};
579
+ let count = 0;
580
+
649
581
  snapshot.forEach(doc => {
582
+ if (doc.id !== 'data') return; // Enforce specific document ID from schema
583
+
650
584
  const raw = tryDecompress(doc.data());
651
- // [FIX] Normalize Verification Data Structure
652
- profiles[doc.id] = {
653
- cid: raw.realCID || raw.cid,
654
- username: raw.username,
655
- aboutMe: raw.userBio?.aboutMe || raw.aboutMe || "",
656
- aboutMeShort: raw.userBio?.aboutMeShort || raw.aboutMeShort || "",
657
- isVerified: raw.isVerified === true,
658
- restrictions: raw.CustomerRestrictions || []
659
- };
585
+
586
+ // Map new schema fields to internal profile structure
587
+ // New Schema: { etoroCID, etoroUsername, verifiedAt, setupCompletedAt ... }
588
+ if (raw.etoroCID) {
589
+ profiles[raw.etoroCID] = {
590
+ cid: raw.etoroCID,
591
+ username: raw.etoroUsername,
592
+ // 'aboutMe' and 'restrictions' are NOT present in the new schema.
593
+ // Defaulting to empty values to preserve downstream compatibility.
594
+ aboutMe: "",
595
+ aboutMeShort: "",
596
+ isVerified: !!(raw.verifiedAt), // Using existence of verifiedAt as flag
597
+ restrictions: []
598
+ };
599
+ count++;
600
+ }
660
601
  });
661
602
 
603
+ logger.log('INFO', `Loaded ${count} verification profiles.`);
662
604
  return profiles;
663
605
  } catch (error) {
664
606
  logger.log('ERROR', `Failed to load Verification Profiles: ${error.message}`);
@@ -666,30 +608,60 @@ async function loadVerificationProfiles(config, deps) {
666
608
  }
667
609
  }
668
610
 
669
- /** Stage 13: Load PI Ratings Data */
611
+ /** Stage 13: Load PI Ratings Data
612
+ * [UPDATED] Reads from /PiReviews/{date}/shards/daily_log.
613
+ * [FIXED] Handles FLATTENED schema where keys like "reviews.ID" are at the top level.
614
+ * Returns RAW logs grouped by PI. NO COMPUTATION.
615
+ */
670
616
  async function loadPIRatings(config, deps, dateString) {
671
617
  const { db, logger, calculationUtils } = deps;
672
618
  const { withRetry } = calculationUtils;
673
- const collectionName = config.piRatingsCollection || 'PIRatingsData';
674
619
 
675
- logger.log('INFO', `Loading PI Ratings for ${dateString}`);
620
+ // New Path: /PiReviews/{date}/shards/daily_log
621
+
622
+ logger.log('INFO', `Loading PI Ratings (Raw Logs) for ${dateString}`);
676
623
 
677
624
  try {
678
- const docRef = db.collection(collectionName).doc(dateString);
679
- const docSnap = await withRetry(() => docRef.get(), `getPIRatings(${dateString})`);
680
-
681
- if (!docSnap.exists) {
682
- logger.log('WARN', `PI Ratings not found for ${dateString}`);
683
- return null;
625
+ const shardsColRef = db.collection('PiReviews').doc(dateString).collection('shards');
626
+ const shardDocs = await withRetry(() => shardsColRef.listDocuments(), `listRatingShards(${dateString})`);
627
+
628
+ if (!shardDocs || shardDocs.length === 0) {
629
+ logger.log('WARN', `No rating shards found for ${dateString} at ${shardsColRef.path}`);
630
+ return {};
684
631
  }
685
-
686
- const data = tryDecompress(docSnap.data());
687
- // Remove the date key and lastUpdated, return just the PI data
688
- const { date, lastUpdated, ...piRatings } = data;
689
- return piRatings; // Returns { piCid: { averageRating, totalRatings, ratingsByUser, ... } }
632
+
633
+ const rawReviewsByPi = {};
634
+
635
+ for (const docRef of shardDocs) {
636
+ const docSnap = await docRef.get();
637
+ if (!docSnap.exists) continue;
638
+
639
+ const rawData = tryDecompress(docSnap.data());
640
+
641
+ // SCHEMA HANDLING:
642
+ // Keys at the root of the document are the review IDs (e.g. "reviews.29312236_31075566").
643
+ // We iterate over all values and check if they look like review objects.
644
+
645
+ Object.values(rawData).forEach(entry => {
646
+ // Check for valid review object structure
647
+ if (entry && typeof entry === 'object' && entry.piCid && entry.rating !== undefined) {
648
+
649
+ if (!rawReviewsByPi[entry.piCid]) {
650
+ rawReviewsByPi[entry.piCid] = [];
651
+ }
652
+
653
+ // Store the raw entry directly.
654
+ rawReviewsByPi[entry.piCid].push(entry);
655
+ }
656
+ });
657
+ }
658
+
659
+ logger.log('INFO', `Loaded raw reviews for ${Object.keys(rawReviewsByPi).length} PIs.`);
660
+ return rawReviewsByPi;
661
+
690
662
  } catch (error) {
691
663
  logger.log('ERROR', `Failed to load PI Ratings for ${dateString}: ${error.message}`);
692
- return null;
664
+ return {};
693
665
  }
694
666
  }
695
667
 
@@ -711,9 +683,8 @@ async function loadPIPageViews(config, deps, dateString) {
711
683
  }
712
684
 
713
685
  const data = tryDecompress(docSnap.data());
714
- // Remove the date key and lastUpdated, return just the PI data
715
686
  const { date, lastUpdated, ...piPageViews } = data;
716
- return piPageViews; // Returns { piCid: { totalViews, uniqueViewers, viewsByUser, ... } }
687
+ return piPageViews;
717
688
  } catch (error) {
718
689
  logger.log('ERROR', `Failed to load PI Page Views for ${dateString}: ${error.message}`);
719
690
  return null;
@@ -738,9 +709,8 @@ async function loadWatchlistMembership(config, deps, dateString) {
738
709
  }
739
710
 
740
711
  const data = tryDecompress(docSnap.data());
741
- // Remove the date key and lastUpdated, return just the PI data
742
712
  const { date, lastUpdated, ...watchlistMembership } = data;
743
- return watchlistMembership; // Returns { piCid: { totalUsers, users, publicWatchlistCount, ... } }
713
+ return watchlistMembership;
744
714
  } catch (error) {
745
715
  logger.log('ERROR', `Failed to load Watchlist Membership for ${dateString}: ${error.message}`);
746
716
  return null;
@@ -765,19 +735,15 @@ async function loadPIAlertHistory(config, deps, dateString) {
765
735
  }
766
736
 
767
737
  const data = tryDecompress(docSnap.data());
768
- // Remove the date key and lastUpdated, return just the PI data
769
738
  const { date, lastUpdated, ...piAlertHistory } = data;
770
- return piAlertHistory; // Returns { piCid: { alertType: { triggered, count, triggeredFor, ... } } }
739
+ return piAlertHistory;
771
740
  } catch (error) {
772
741
  logger.log('ERROR', `Failed to load PI Alert History for ${dateString}: ${error.message}`);
773
742
  return null;
774
743
  }
775
744
  }
776
745
 
777
- /** Stage 17: Load PI-Centric Watchlist Data
778
- * Loads watchlist data from PopularInvestors/{piCid}/watchlistData/current
779
- * This provides time-series data of watchlist additions per PI
780
- */
746
+ /** Stage 17: Load PI-Centric Watchlist Data */
781
747
  async function loadPIWatchlistData(config, deps, piCid) {
782
748
  const { db, logger, calculationUtils } = deps;
783
749
  const { withRetry } = calculationUtils;
@@ -799,19 +765,18 @@ async function loadPIWatchlistData(config, deps, piCid) {
799
765
  }
800
766
 
801
767
  const data = tryDecompress(docSnap.data());
802
- return data; // Returns { totalUsers, userCids: [], dailyAdditions: { date: { count, userCids, timestamp } }, lastUpdated }
768
+ return data;
803
769
  } catch (error) {
804
770
  logger.log('ERROR', `Failed to load PI Watchlist Data for PI ${piCidStr}: ${error.message}`);
805
771
  return null;
806
772
  }
807
773
  }
808
774
 
809
- // [NEW] Load Popular Investor Master List
775
+ // Load Popular Investor Master List
810
776
  async function loadPopularInvestorMasterList(config, deps) {
811
777
  const { db, logger, calculationUtils } = deps;
812
778
  const { withRetry } = calculationUtils;
813
779
 
814
- // Default to 'system_state' collection, 'popular_investor_master_list' doc
815
780
  const collectionName = config.piMasterListCollection || 'system_state';
816
781
  const docId = config.piMasterListDocId || 'popular_investor_master_list';
817
782
 
@@ -827,9 +792,6 @@ async function loadPopularInvestorMasterList(config, deps) {
827
792
  }
828
793
 
829
794
  const data = tryDecompress(docSnap.data());
830
- // Structure is { investors: { cid: { username, ... } } } or direct map { cid: { ... } }
831
- // Based on user input, it looks like a direct map of CIDs or a field holding the map.
832
- // We return the raw object which acts as the map.
833
795
  return data.investors || data;
834
796
  } catch (error) {
835
797
  logger.log('ERROR', `Failed to load PI Master List: ${error.message}`);
@@ -855,6 +817,6 @@ module.exports = {
855
817
  loadPIPageViews,
856
818
  loadWatchlistMembership,
857
819
  loadPIAlertHistory,
858
- loadPopularInvestorMasterList, // [NEW]
820
+ loadPopularInvestorMasterList,
859
821
  loadPIWatchlistData,
860
822
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.631",
3
+ "version": "1.0.632",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [