bulltrackers-module 1.0.535 → 1.0.537

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.
@@ -27,7 +27,8 @@ class ContextFactory {
27
27
  allRankings, allRankingsYesterday, // Global rank lists
28
28
  allVerifications,
29
29
  // [NEW] New Root Data Types for Profile Metrics
30
- ratings, pageViews, watchlistMembership, alertHistory
30
+ ratings, pageViews, watchlistMembership, alertHistory,
31
+ piMasterList, // [NEW]
31
32
  } = options;
32
33
 
33
34
  return {
@@ -56,7 +57,8 @@ class ContextFactory {
56
57
  ratings: ratings || {},
57
58
  pageViews: pageViews || {},
58
59
  watchlistMembership: watchlistMembership || {},
59
- alertHistory: alertHistory || {}
60
+ alertHistory: alertHistory || {},
61
+ piMasterList: piMasterList || {}, // [NEW]
60
62
  }
61
63
  };
62
64
  }
@@ -229,7 +229,10 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
229
229
  piRatings: !!details.piRatings,
230
230
  piPageViews: !!details.piPageViews,
231
231
  watchlistMembership: !!details.watchlistMembership,
232
- piAlertHistory: !!details.piAlertHistory
232
+ piAlertHistory: !!details.piAlertHistory,
233
+
234
+ // [NEW] Global Helper Data
235
+ piMasterList: !!details.piMasterList
233
236
  },
234
237
  portfolioRefs: null,
235
238
  historyRefs: null,
@@ -14,7 +14,8 @@ const {
14
14
  loadPIRatings, // [NEW]
15
15
  loadPIPageViews, // [NEW]
16
16
  loadWatchlistMembership: loadWatchlistMembershipData, // [NEW] Renamed to avoid conflict
17
- loadPIAlertHistory // [NEW]
17
+ loadPIAlertHistory, // [NEW]
18
+ loadPopularInvestorMasterList // [NEW]
18
19
  } = require('../utils/data_loader');
19
20
  const zlib = require('zlib');
20
21
 
@@ -31,7 +32,8 @@ class CachedDataLoader {
31
32
  ratings: new Map(), // [NEW]
32
33
  pageViews: new Map(), // [NEW]
33
34
  watchlistMembership: new Map(), // [NEW]
34
- alertHistory: new Map() // [NEW]
35
+ alertHistory: new Map(),// [NEW]
36
+ piMasterList: null // [NEW] Singleton cache (not date dependent)
35
37
  };
36
38
  }
37
39
 
@@ -141,6 +143,13 @@ class CachedDataLoader {
141
143
  return {};
142
144
  }
143
145
  }
146
+ // [NEW] Load PI Master List (Global, Cached)
147
+ async loadPIMasterList() {
148
+ if (this.cache.piMasterList) return this.cache.piMasterList;
149
+ const data = await loadPopularInvestorMasterList(this.config, this.deps);
150
+ this.cache.piMasterList = data;
151
+ return data;
152
+ }
144
153
  }
145
154
 
146
155
  module.exports = { CachedDataLoader };
@@ -5,7 +5,7 @@
5
5
  * }
6
6
  */
7
7
  const { normalizeName, getEarliestDataDates } = require('../utils/utils');
8
- const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs } = require('../utils/data_loader');
8
+ const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
9
9
  const { CachedDataLoader } = require('../data/CachedDataLoader');
10
10
  const { ContextFactory } = require('../context/ContextFactory');
11
11
  const { commitResults } = require('../persistence/ResultCommitter');
@@ -24,15 +24,31 @@ class StandardExecutor {
24
24
  const type = (c.userType || 'ALL').toUpperCase();
25
25
  requiredUserTypes.add(type);
26
26
  });
27
- // If any calc requires 'ALL' (or has no userType), we fetch everything
28
27
  const userTypeArray = requiredUserTypes.has('ALL') ? null : Array.from(requiredUserTypes);
29
28
 
29
+ // [OPTIMIZATION] Check for Target CID in manifests (On-Demand Optimization)
30
+ // If present, we will filter all data streams to strictly this user
31
+ const targetCid = calcs.find(c => c.targetCid)?.targetCid || calcs.find(c => c.manifest?.targetCid)?.manifest?.targetCid;
32
+ if (targetCid) {
33
+ logger.log('INFO', `[StandardExecutor] Running in Targeted Mode for CID: ${targetCid}`);
34
+ }
35
+
30
36
  const fullRoot = { ...rootData };
31
37
  if (calcs.some(c => c.isHistorical)) {
32
38
  const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
33
39
  const prevStr = prev.toISOString().slice(0, 10);
34
- // [FIX] Pass userTypeArray to filter yesterday's data loading
35
- fullRoot.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr, userTypeArray);
40
+
41
+ // Fetch yesterday's refs
42
+ let yRefs = await getPortfolioPartRefs(config, deps, prevStr, userTypeArray);
43
+
44
+ // [OPTIMIZATION] Filter Yesterday's Refs if targetCid is set
45
+ if (targetCid && yRefs) {
46
+ const originalCount = yRefs.length;
47
+ yRefs = yRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
48
+ logger.log('INFO', `[StandardExecutor] Filtered Yesterday's Refs: ${originalCount} -> ${yRefs.length}`);
49
+ }
50
+
51
+ fullRoot.yesterdayPortfolioRefs = yRefs;
36
52
  }
37
53
 
38
54
  const state = {};
@@ -46,18 +62,48 @@ class StandardExecutor {
46
62
  } catch (e) { logger.log('WARN', `Failed to init ${c.name}`); }
47
63
  }
48
64
 
49
- return await StandardExecutor.streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, userTypeArray);
65
+ // Pass targetCid to streamAndProcess
66
+ return await StandardExecutor.streamAndProcess(
67
+ dStr, state, passName, config, deps, fullRoot,
68
+ rootData.portfolioRefs, rootData.historyRefs,
69
+ fetchedDeps, previousFetchedDeps, skipStatusWrite,
70
+ userTypeArray, targetCid
71
+ );
50
72
  }
51
73
 
52
- // [UPDATED] Accepts requiredUserTypes param
53
- static async streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, requiredUserTypes = null) {
74
+ // [UPDATED] Added targetCid param
75
+ static async streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, requiredUserTypes = null, targetCid = null) {
54
76
  const { logger } = deps;
55
77
  const calcs = Object.values(state).filter(c => c && c.manifest);
56
78
  const streamingCalcs = calcs.filter(c => c.manifest.rootDataDependencies.includes('portfolio') || c.manifest.rootDataDependencies.includes('history'));
57
79
 
58
80
  if (streamingCalcs.length === 0) return { successUpdates: {}, failureReport: [] };
81
+
82
+ // --- 1. Resolve and Filter Portfolio Refs (Today) ---
83
+ let effectivePortfolioRefs = portfolioRefs;
84
+ if (!effectivePortfolioRefs) {
85
+ // If refs weren't provided by AvailabilityChecker, fetch them now
86
+ effectivePortfolioRefs = await getPortfolioPartRefs(config, deps, dateStr, requiredUserTypes);
87
+ }
88
+ if (targetCid && effectivePortfolioRefs) {
89
+ // Filter: Keep only refs that match the CID (or Legacy refs without CID)
90
+ effectivePortfolioRefs = effectivePortfolioRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
91
+ }
92
+
93
+ // --- 2. Resolve and Filter History Refs ---
94
+ let effectiveHistoryRefs = historyRefs;
95
+ const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
59
96
 
60
- let totalReadOps = (portfolioRefs?.length || 0) + (historyRefs?.length || 0);
97
+ if (needsTradingHistory) {
98
+ if (!effectiveHistoryRefs) {
99
+ effectiveHistoryRefs = await getHistoryPartRefs(config, deps, dateStr, requiredUserTypes);
100
+ }
101
+ if (targetCid && effectiveHistoryRefs) {
102
+ effectiveHistoryRefs = effectiveHistoryRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
103
+ }
104
+ }
105
+
106
+ let totalReadOps = (effectivePortfolioRefs?.length || 0) + (effectiveHistoryRefs?.length || 0);
61
107
  if (rootData.yesterdayPortfolioRefs) totalReadOps += rootData.yesterdayPortfolioRefs.length;
62
108
  totalReadOps += 2;
63
109
 
@@ -83,6 +129,7 @@ class StandardExecutor {
83
129
  const setupDuration = performance.now() - startSetup;
84
130
  Object.keys(executionStats).forEach(name => executionStats[name].timings.setup += setupDuration);
85
131
 
132
+ // Yesterday's Refs are already filtered in run()
86
133
  const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
87
134
  const prevDateStr = prevDate.toISOString().slice(0, 10);
88
135
 
@@ -91,17 +138,13 @@ class StandardExecutor {
91
138
  earliestDates = await getEarliestDataDates(config, deps);
92
139
  }
93
140
 
94
- // [FIX] Pass requiredUserTypes to streamPortfolioData
95
- // Note: portfolioRefs (for today) might be null if not pre-fetched, allowing streamPortfolioData to fetch filtered refs
96
- const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs, requiredUserTypes);
141
+ // [FIX] Use effective/filtered refs
142
+ const tP_iter = streamPortfolioData(config, deps, dateStr, effectivePortfolioRefs, requiredUserTypes);
97
143
 
98
144
  const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
99
- // Yesterday's refs were already filtered in run(), so we pass them directly
100
145
  const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs) ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
101
146
 
102
- const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
103
- // [FIX] Pass requiredUserTypes to streamHistoryData
104
- const tH_iter = (needsTradingHistory) ? streamHistoryData(config, deps, dateStr, historyRefs, requiredUserTypes) : null;
147
+ const tH_iter = (needsTradingHistory) ? streamHistoryData(config, deps, dateStr, effectiveHistoryRefs, requiredUserTypes) : null;
105
148
 
106
149
  let yP_chunk = {}, tH_chunk = {};
107
150
  let usersSinceLastFlush = 0;
@@ -159,7 +202,7 @@ class StandardExecutor {
159
202
 
160
203
  // ... rest of the file (flushBuffer, mergeReports, executePerUser) ...
161
204
  static async flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, mode, skipStatusWrite, isInitialWrite = false) {
162
- // ... (No changes to flushBuffer)
205
+ const { logger } = deps;
163
206
  const transformedState = {};
164
207
  for (const [name, inst] of Object.entries(state)) {
165
208
  const rawResult = inst.results || {};
@@ -197,7 +240,6 @@ class StandardExecutor {
197
240
  }
198
241
 
199
242
  static mergeReports(successAcc, failureAcc, newResult) {
200
- // ... (No changes to mergeReports)
201
243
  if (!newResult) return;
202
244
  for (const [name, update] of Object.entries(newResult.successUpdates)) {
203
245
  if (!successAcc[name]) {
@@ -230,24 +272,25 @@ class StandardExecutor {
230
272
  static async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps, config, deps, loader, stats, earliestDates) {
231
273
  const { logger } = deps;
232
274
  const targetUserType = metadata.userType;
275
+ // [NEW] Always load Global Helpers
233
276
  const mappings = await loader.loadMappings();
277
+ const piMasterList = await loader.loadPopularInvestorMasterList(config, deps); // [NEW] Loaded globally
234
278
  const SCHEMAS = mathLayer.SCHEMAS;
235
279
 
236
280
  // 1. Load Root Data
237
281
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
238
- const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.loadVerifications() : null;
239
- const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.loadRankings(dateStr) : null;
282
+ const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.loadVerificationProfiles(config, deps) : null;
283
+ const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.loadPopularInvestorRankings(config, deps, dateStr) : null;
240
284
 
241
285
  // [FIX] Load Yesterday's Rankings if isHistorical is true
242
286
  let yesterdayRankings = null;
243
287
  if (metadata.rootDataDependencies?.includes('rankings') && metadata.isHistorical) {
244
288
  const prevDate = new Date(dateStr); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
245
289
  const prevStr = prevDate.toISOString().slice(0, 10);
246
- // Assuming CachedDataLoader handles caching for efficiency
247
- yesterdayRankings = await loader.loadRankings(prevStr);
290
+ yesterdayRankings = await loader.loadPopularInvestorRankings(config, deps, prevStr);
248
291
  }
249
292
 
250
- const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.loadSocial(dateStr) : null;
293
+ const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.loadDailySocialPostInsights(config, deps, dateStr) : null;
251
294
 
252
295
  // [NEW] Load New Root Data Types for Profile Metrics
253
296
  // [FIX] Enforce canHaveMissingRoots
@@ -256,14 +299,13 @@ class StandardExecutor {
256
299
  let ratings = null;
257
300
  if (metadata.rootDataDependencies?.includes('ratings')) {
258
301
  try {
259
- ratings = await loader.loadRatings(dateStr);
302
+ ratings = await loader.loadPIRatings(config, deps, dateStr);
260
303
  } catch (e) {
261
304
  if (!allowMissing) {
262
305
  throw new Error(`[StandardExecutor] Required root 'ratings' failed to load for ${metadata.name}: ${e.message}`);
263
306
  }
264
307
  ratings = null;
265
308
  }
266
- // Check if result is null (clean fetch but empty/missing doc)
267
309
  if (!ratings && !allowMissing) {
268
310
  throw new Error(`[StandardExecutor] Required root 'ratings' is missing for ${metadata.name}`);
269
311
  }
@@ -272,7 +314,7 @@ class StandardExecutor {
272
314
  let pageViews = null;
273
315
  if (metadata.rootDataDependencies?.includes('pageViews')) {
274
316
  try {
275
- pageViews = await loader.loadPageViews(dateStr);
317
+ pageViews = await loader.loadPIPageViews(config, deps, dateStr);
276
318
  } catch (e) {
277
319
  if (!allowMissing) {
278
320
  throw new Error(`[StandardExecutor] Required root 'pageViews' failed to load for ${metadata.name}: ${e.message}`);
@@ -287,7 +329,7 @@ class StandardExecutor {
287
329
  let watchlistMembership = null;
288
330
  if (metadata.rootDataDependencies?.includes('watchlist')) {
289
331
  try {
290
- watchlistMembership = await loader.loadWatchlistMembership(dateStr);
332
+ watchlistMembership = await loader.loadWatchlistMembership(config, deps, dateStr);
291
333
  } catch (e) {
292
334
  if (!allowMissing) {
293
335
  throw new Error(`[StandardExecutor] Required root 'watchlist' failed to load for ${metadata.name}: ${e.message}`);
@@ -302,7 +344,7 @@ class StandardExecutor {
302
344
  let alertHistory = null;
303
345
  if (metadata.rootDataDependencies?.includes('alerts')) {
304
346
  try {
305
- alertHistory = await loader.loadAlertHistory(dateStr);
347
+ alertHistory = await loader.loadPIAlertHistory(config, deps, dateStr);
306
348
  } catch (e) {
307
349
  if (!allowMissing) {
308
350
  throw new Error(`[StandardExecutor] Required root 'alerts' failed to load for ${metadata.name}: ${e.message}`);
@@ -348,7 +390,6 @@ class StandardExecutor {
348
390
 
349
391
  const userVerification = verifications ? verifications[userId] : null;
350
392
 
351
- // [FIX] Extract current AND yesterday's rank entry for this user
352
393
  const userRanking = rankings ? (rankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
353
394
  const userRankingYesterday = yesterdayRankings ? (yesterdayRankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
354
395
 
@@ -371,21 +412,20 @@ class StandardExecutor {
371
412
  config, deps,
372
413
  verification: userVerification,
373
414
 
374
- // [FIX] Pass both ranking entries
375
415
  rankings: userRanking,
376
416
  yesterdayRankings: userRankingYesterday,
377
417
 
378
- // [FIX] Pass both global lists
379
418
  allRankings: rankings,
380
419
  allRankingsYesterday: yesterdayRankings,
381
420
 
382
421
  allVerifications: verifications,
383
422
 
384
- // [NEW] Pass New Root Data Types for Profile Metrics
385
423
  ratings: ratings || {},
386
424
  pageViews: pageViews || {},
387
425
  watchlistMembership: watchlistMembership || {},
388
- alertHistory: alertHistory || {}
426
+ alertHistory: alertHistory || {},
427
+
428
+ piMasterList,
389
429
  });
390
430
 
391
431
  if (metadata.requiresEarliestDataDate && earliestDates) {
@@ -947,6 +947,39 @@ class AlertHistoryExtractor {
947
947
  }
948
948
  }
949
949
 
950
+ /**
951
+ * [NEW] Extractor for Popular Investor Master List (Global Backup)
952
+ * Access via: context.globalData.piMasterList
953
+ */
954
+ class PIMasterListExtractor {
955
+ /**
956
+ * Get the master record for a PI
957
+ * @param {Object} masterList - context.globalData.piMasterList
958
+ * @param {string} cid - The User CID
959
+ */
960
+ static getRecord(masterList, cid) {
961
+ if (!masterList || !cid) return null;
962
+ return masterList[String(cid)] || null;
963
+ }
964
+
965
+ /**
966
+ * Resolve Username from Master List
967
+ * @param {Object} masterList
968
+ * @param {string} cid
969
+ */
970
+ static getUsername(masterList, cid) {
971
+ const record = this.getRecord(masterList, cid);
972
+ return record ? record.username : null;
973
+ }
974
+
975
+ /**
976
+ * Check if CID exists in master list
977
+ */
978
+ static exists(masterList, cid) {
979
+ return !!this.getRecord(masterList, cid);
980
+ }
981
+ }
982
+
950
983
  module.exports = {
951
984
  DataExtractor,
952
985
  priceExtractor,
@@ -962,5 +995,6 @@ module.exports = {
962
995
  RatingsExtractor,
963
996
  PageViewsExtractor,
964
997
  WatchlistMembershipExtractor,
965
- AlertHistoryExtractor
998
+ AlertHistoryExtractor,
999
+ PIMasterListExtractor
966
1000
  };
@@ -774,6 +774,37 @@ async function loadPIAlertHistory(config, deps, dateString) {
774
774
  }
775
775
  }
776
776
 
777
+ // [NEW] Load Popular Investor Master List
778
+ async function loadPopularInvestorMasterList(config, deps) {
779
+ const { db, logger, calculationUtils } = deps;
780
+ const { withRetry } = calculationUtils;
781
+
782
+ // Default to 'system_state' collection, 'popular_investor_master_list' doc
783
+ const collectionName = config.piMasterListCollection || 'system_state';
784
+ const docId = config.piMasterListDocId || 'popular_investor_master_list';
785
+
786
+ logger.log('INFO', `Loading Popular Investor Master List from ${collectionName}/${docId}`);
787
+
788
+ try {
789
+ const docRef = db.collection(collectionName).doc(docId);
790
+ const docSnap = await withRetry(() => docRef.get(), 'getPIMasterList');
791
+
792
+ if (!docSnap.exists) {
793
+ logger.log('WARN', 'Popular Investor Master List not found.');
794
+ return {};
795
+ }
796
+
797
+ const data = tryDecompress(docSnap.data());
798
+ // Structure is { investors: { cid: { username, ... } } } or direct map { cid: { ... } }
799
+ // Based on user input, it looks like a direct map of CIDs or a field holding the map.
800
+ // We return the raw object which acts as the map.
801
+ return data.investors || data;
802
+ } catch (error) {
803
+ logger.log('ERROR', `Failed to load PI Master List: ${error.message}`);
804
+ return {};
805
+ }
806
+ }
807
+
777
808
  module.exports = {
778
809
  getPortfolioPartRefs,
779
810
  loadDataByRefs,
@@ -791,5 +822,6 @@ module.exports = {
791
822
  loadPIRatings,
792
823
  loadPIPageViews,
793
824
  loadWatchlistMembership,
794
- loadPIAlertHistory
825
+ loadPIAlertHistory,
826
+ loadPopularInvestorMasterList // [NEW]
795
827
  };
@@ -93,6 +93,7 @@ exports.runRootDataIndexer = async (config, dependencies) => {
93
93
  piPageViews: collections.piPageViews || 'PIPageViewsData',
94
94
  watchlistMembership: collections.watchlistMembership || 'WatchlistMembershipData',
95
95
  piAlertHistory: collections.piAlertHistory || 'PIAlertHistoryData',
96
+ piMasterList: collections.piMasterList || 'system_state', // [NEW] Collection for master list
96
97
  ...collections // Allow overrides
97
98
  };
98
99
 
@@ -237,7 +238,8 @@ exports.runRootDataIndexer = async (config, dependencies) => {
237
238
  piRatings: false,
238
239
  piPageViews: false,
239
240
  watchlistMembership: false,
240
- piAlertHistory: false
241
+ piAlertHistory: false,
242
+ piMasterList: false // [NEW] Flag for Master List
241
243
  }
242
244
  };
243
245
 
@@ -312,6 +314,10 @@ exports.runRootDataIndexer = async (config, dependencies) => {
312
314
  // Path: PIAlertHistoryData/{YYYY-MM-DD}
313
315
  const piAlertHistoryRef = db.collection(safeCollections.piAlertHistory).doc(dateStr);
314
316
 
317
+ // [NEW] Master List Ref (Single Global Document)
318
+ // Path: system_state/popular_investor_master_list
319
+ const piMasterListRef = db.collection(safeCollections.piMasterList).doc('popular_investor_master_list');
320
+
315
321
  // 4. Social Data Checks - Use date tracking documents (NEW STRUCTURE)
316
322
  // Single tracking documents at root level:
317
323
  // - PopularInvestorSocialPostData/_dates -> fetchedDates.{date}
@@ -387,7 +393,8 @@ exports.runRootDataIndexer = async (config, dependencies) => {
387
393
  piRatingsSnap,
388
394
  piPageViewsSnap,
389
395
  watchlistMembershipSnap,
390
- piAlertHistorySnap
396
+ piAlertHistorySnap,
397
+ piMasterListSnap // [NEW]
391
398
  ] = await Promise.all([
392
399
  checkAnyPartExists(normPortPartsRef),
393
400
  checkAnyPartExists(specPortPartsRef),
@@ -396,6 +403,7 @@ exports.runRootDataIndexer = async (config, dependencies) => {
396
403
  insightsRef.get(),
397
404
  Promise.resolve(!genericSocialSnap.empty),
398
405
  piRankingsRef.get(),
406
+ piMasterListRef.get(), // [NEW]
399
407
  // Check new structure first, fallback to legacy
400
408
  checkDateCollectionHasDocs(piPortfoliosCollectionRef).then(exists => exists || checkAnyPartExists(piPortfoliosPartsRef)),
401
409
  checkAnyPartExists(piDeepPartsRef), // Legacy only
@@ -459,6 +467,9 @@ exports.runRootDataIndexer = async (config, dependencies) => {
459
467
  availability.hasHistory = normHistExists || specHistExists || piHistExists || signedInHistExists;
460
468
  availability.hasInsights = insightsSnap.exists;
461
469
  availability.hasSocial = foundPISocial || foundSignedInSocial || genericSocialExists;
470
+
471
+ // [NEW] Assign Master List Availability
472
+ availability.details.piMasterList = piMasterListSnap.exists;
462
473
 
463
474
  // Price Check
464
475
  // Check if the target date exists in the price availability set
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.535",
3
+ "version": "1.0.537",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [