bulltrackers-module 1.0.403 → 1.0.405

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.
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Factory for creating the Computation Context.
3
3
  * UPDATED: Injects verification and rankings data into context globally and locally.
4
+ * UPDATED: Added support for historical ranking data in both Standard and Meta contexts.
4
5
  */
5
6
  const mathLayer = require('../layers/index');
6
7
  const { LEGACY_MAPPING } = require('../topology/HashManager');
@@ -21,8 +22,10 @@ class ContextFactory {
21
22
  todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory,
22
23
  userId, userType, dateStr, metadata, mappings, insights, socialData,
23
24
  computedDependencies, previousComputedDependencies, config, deps,
24
- verification, rankings, // User-specific
25
- allRankings, allVerifications // [NEW] Global context for comparative analysis
25
+ verification,
26
+ rankings, yesterdayRankings, // User-specific rank entries
27
+ allRankings, allRankingsYesterday, // Global rank lists
28
+ allVerifications
26
29
  } = options;
27
30
 
28
31
  return {
@@ -32,7 +35,8 @@ class ContextFactory {
32
35
  portfolio: { today: todayPortfolio, yesterday: yesterdayPortfolio },
33
36
  history: { today: todayHistory, yesterday: yesterdayHistory },
34
37
  verification: verification || null,
35
- rankEntry: rankings || null // Renamed for clarity (single entry)
38
+ rankEntry: rankings || null,
39
+ rankEntryYesterday: yesterdayRankings || null
36
40
  },
37
41
  date: { today: dateStr },
38
42
  insights: { today: insights?.today, yesterday: insights?.yesterday },
@@ -42,9 +46,9 @@ class ContextFactory {
42
46
  computed: computedDependencies || {},
43
47
  previousComputed: previousComputedDependencies || {},
44
48
  meta: metadata, config, deps,
45
- // [NEW] Global Data Injection for Comparative Logic (Similarity, Ranking position, etc.)
46
49
  globalData: {
47
50
  rankings: allRankings || [],
51
+ rankingsYesterday: allRankingsYesterday || [],
48
52
  verifications: allVerifications || {}
49
53
  }
50
54
  };
@@ -54,7 +58,8 @@ class ContextFactory {
54
58
  const {
55
59
  dateStr, metadata, mappings, insights, socialData, prices,
56
60
  computedDependencies, previousComputedDependencies, config, deps,
57
- allRankings, allVerifications
61
+ allRankings, allRankingsYesterday, // [UPDATED] Accepted here
62
+ allVerifications
58
63
  } = options;
59
64
 
60
65
  return {
@@ -69,6 +74,7 @@ class ContextFactory {
69
74
  meta: metadata, config, deps,
70
75
  globalData: {
71
76
  rankings: allRankings || [],
77
+ rankingsYesterday: allRankingsYesterday || [], // [UPDATED] Injected here
72
78
  verifications: allVerifications || {}
73
79
  }
74
80
  };
@@ -3,6 +3,7 @@
3
3
  * UPDATED: Uses CachedDataLoader for all data access.
4
4
  * UPDATED: Tracks processed shard/item counts.
5
5
  * UPDATED: Sends 'isInitialWrite: true' for robust cleanup.
6
+ * UPDATED: Support for historical rankings in Meta Context.
6
7
  */
7
8
  const { normalizeName } = require('../utils/utils');
8
9
  const { CachedDataLoader } = require('../data/CachedDataLoader');
@@ -13,9 +14,20 @@ class MetaExecutor {
13
14
  static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
14
15
  const dStr = date.toISOString().slice(0, 10);
15
16
  const { logger, db } = deps;
16
- const { CachedDataLoader } = require('../data/CachedDataLoader');
17
17
  const loader = new CachedDataLoader(config, deps);
18
18
 
19
+ // [FIX] Check if any meta calculation needs history
20
+ const needsHistory = calcs.some(c => c.isHistorical);
21
+ let rankingsYesterday = null;
22
+
23
+ if (needsHistory) {
24
+ const prevDate = new Date(date);
25
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
26
+ const prevStr = prevDate.toISOString().slice(0, 10);
27
+ logger.log('INFO', `[MetaExecutor] Loading historical rankings for ${prevStr}`);
28
+ rankingsYesterday = await loader.loadRankings(prevStr);
29
+ }
30
+
19
31
  // 1. Load Global Dependencies
20
32
  const [mappings, rankings, verifications] = await Promise.all([
21
33
  loader.loadMappings(),
@@ -38,11 +50,14 @@ class MetaExecutor {
38
50
  previousComputedDependencies: previousFetchedDeps,
39
51
  config, deps,
40
52
  allRankings: rankings,
53
+ allRankingsYesterday: rankingsYesterday, // [FIX] Injected
41
54
  allVerifications: verifications
42
55
  });
43
56
 
44
57
  try {
45
58
  const result = await inst.process(context);
59
+ // Meta results are usually wrapped in a global key or just the result object
60
+ // The structure below implies we store it under the date key
46
61
  inst.results = { [dStr]: { global: result } };
47
62
  state[c.name] = inst;
48
63
  } catch (e) {
@@ -61,6 +76,18 @@ class MetaExecutor {
61
76
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
62
77
  const social = metadata.rootDataDependencies?.includes('social') ? { today: await loader.loadSocial(dateStr) } : null;
63
78
 
79
+ // [FIX] Historical support for Batch/OncePerDay execution
80
+ let rankingsYesterday = null;
81
+ if (metadata.isHistorical) {
82
+ const prevDate = new Date(dateStr);
83
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
84
+ const prevStr = prevDate.toISOString().slice(0, 10);
85
+ rankingsYesterday = await loader.loadRankings(prevStr);
86
+ }
87
+
88
+ // Load current rankings (often needed for ContextFactory.buildMetaContext)
89
+ const rankings = await loader.loadRankings(dateStr);
90
+
64
91
  if (metadata.rootDataDependencies?.includes('price')) {
65
92
  logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
66
93
  const shardRefs = await loader.getPriceShardReferences();
@@ -72,7 +99,9 @@ class MetaExecutor {
72
99
  const partialContext = ContextFactory.buildMetaContext({
73
100
  dateStr, metadata, mappings, insights, socialData: social,
74
101
  prices: { history: shardData }, computedDependencies: computedDeps,
75
- previousComputedDependencies: prevDeps, config, deps
102
+ previousComputedDependencies: prevDeps, config, deps,
103
+ allRankings: rankings,
104
+ allRankingsYesterday: rankingsYesterday
76
105
  });
77
106
 
78
107
  await calcInstance.process(partialContext);
@@ -90,7 +119,9 @@ class MetaExecutor {
90
119
  const context = ContextFactory.buildMetaContext({
91
120
  dateStr, metadata, mappings, insights, socialData: social,
92
121
  prices: {}, computedDependencies: computedDeps,
93
- previousComputedDependencies: prevDeps, config, deps
122
+ previousComputedDependencies: prevDeps, config, deps,
123
+ allRankings: rankings,
124
+ allRankingsYesterday: rankingsYesterday
94
125
  });
95
126
  const res = await calcInstance.process(context);
96
127
 
@@ -102,4 +133,4 @@ class MetaExecutor {
102
133
  }
103
134
  }
104
135
 
105
- module.exports = { MetaExecutor };
136
+ module.exports = { MetaExecutor };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * {
3
3
  * type: uploaded file
4
- * fileName: bulltrackers-module/functions/computation-system/executors/StandardExecutor.js
4
+ * fileName: computation-system/executors/StandardExecutor.js
5
5
  * }
6
6
  */
7
7
  const { normalizeName, getEarliestDataDates } = require('../utils/utils');
@@ -141,6 +141,7 @@ class StandardExecutor {
141
141
  }
142
142
 
143
143
  static async flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, mode, skipStatusWrite, isInitialWrite = false) {
144
+ // ... (No changes to flushBuffer)
144
145
  const transformedState = {};
145
146
  for (const [name, inst] of Object.entries(state)) {
146
147
  const rawResult = inst.results || {};
@@ -178,6 +179,7 @@ class StandardExecutor {
178
179
  }
179
180
 
180
181
  static mergeReports(successAcc, failureAcc, newResult) {
182
+ // ... (No changes to mergeReports)
181
183
  if (!newResult) return;
182
184
  for (const [name, update] of Object.entries(newResult.successUpdates)) {
183
185
  if (!successAcc[name]) {
@@ -218,8 +220,15 @@ class StandardExecutor {
218
220
  const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.loadVerifications() : null;
219
221
  const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.loadRankings(dateStr) : null;
220
222
 
221
- // [UPDATED] Load the Partitioned Social Data Container
222
- // socialContainer = { generic: {}, pi: { userId: {} }, signedIn: { userId: {} } }
223
+ // [FIX] Load Yesterday's Rankings if isHistorical is true
224
+ let yesterdayRankings = null;
225
+ if (metadata.rootDataDependencies?.includes('rankings') && metadata.isHistorical) {
226
+ const prevDate = new Date(dateStr); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
227
+ const prevStr = prevDate.toISOString().slice(0, 10);
228
+ // Assuming CachedDataLoader handles caching for efficiency
229
+ yesterdayRankings = await loader.loadRankings(prevStr);
230
+ }
231
+
223
232
  const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.loadSocial(dateStr) : null;
224
233
 
225
234
  let chunkSuccess = 0;
@@ -229,7 +238,6 @@ class StandardExecutor {
229
238
  const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
230
239
  const todayHistory = historyData ? historyData[userId] : null;
231
240
 
232
- // 2. Identify User Type
233
241
  let actualUserType = todayPortfolio._userType;
234
242
  if (!actualUserType) {
235
243
  if (todayPortfolio.PublicPositions) {
@@ -240,7 +248,6 @@ class StandardExecutor {
240
248
  }
241
249
  }
242
250
 
243
- // 3. Strict User Type Filtering
244
251
  if (targetUserType && targetUserType !== 'all') {
245
252
  if (targetUserType !== actualUserType) {
246
253
  if (stats) stats.skippedUsers++;
@@ -248,22 +255,19 @@ class StandardExecutor {
248
255
  }
249
256
  }
250
257
 
251
- // 4. Resolve Contextual Data
252
258
  const userVerification = verifications ? verifications[userId] : null;
259
+
260
+ // [FIX] Extract current AND yesterday's rank entry for this user
253
261
  const userRanking = rankings ? (rankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
262
+ const userRankingYesterday = yesterdayRankings ? (yesterdayRankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
254
263
 
255
- // [CRITICAL FIX] Select Specific Social Data to Prevent Skew
256
264
  let effectiveSocialData = null;
257
265
  if (socialContainer) {
258
266
  if (actualUserType === 'POPULAR_INVESTOR') {
259
- // PI Route: Only gets PI-specific posts for this user
260
- // (PIs do not need generic noise usually, or you can merge it if required)
261
267
  effectiveSocialData = socialContainer.pi[userId] || {};
262
268
  } else if (actualUserType === 'SIGNED_IN_USER') {
263
- // Signed-In Route: Only gets Signed-In specific posts for this user
264
269
  effectiveSocialData = socialContainer.signedIn[userId] || {};
265
270
  } else {
266
- // Normal/Speculator/All Route: Gets Generic Market Posts
267
271
  effectiveSocialData = socialContainer.generic || {};
268
272
  }
269
273
  }
@@ -271,15 +275,19 @@ class StandardExecutor {
271
275
  const context = ContextFactory.buildPerUserContext({
272
276
  todayPortfolio, yesterdayPortfolio, todayHistory, userId,
273
277
  userType: actualUserType, dateStr, metadata, mappings, insights,
274
-
275
- // Inject the filtered subset
276
278
  socialData: effectiveSocialData ? { today: effectiveSocialData } : null,
277
-
278
279
  computedDependencies: computedDeps, previousComputedDependencies: prevDeps,
279
280
  config, deps,
280
281
  verification: userVerification,
282
+
283
+ // [FIX] Pass both ranking entries
281
284
  rankings: userRanking,
285
+ yesterdayRankings: userRankingYesterday,
286
+
287
+ // [FIX] Pass both global lists
282
288
  allRankings: rankings,
289
+ allRankingsYesterday: yesterdayRankings,
290
+
283
291
  allVerifications: verifications
284
292
  });
285
293
 
@@ -10,8 +10,53 @@ const { FieldValue } = require('@google-cloud/firestore');
10
10
  const pLimit = require('p-limit');
11
11
 
12
12
  const CANARY_BLOCK_ID = '19M';
13
- const CANARY_PART_ID = 'part_0';
14
- const PRICE_SHARD_ID = 'shard_0';
13
+ const PRICE_SHARD_ID = 'shard_0';
14
+
15
+ /**
16
+ * Helper function to check if any part document exists in a parts collection
17
+ * @param {Firestore.CollectionReference} partsCollectionRef - Reference to the parts collection
18
+ * @returns {Promise<boolean>} - True if any part_* document exists
19
+ */
20
+ async function checkAnyPartExists(partsCollectionRef) {
21
+ try {
22
+ // List all documents in the parts collection
23
+ const snapshot = await partsCollectionRef.limit(10).get();
24
+ if (snapshot.empty) return false;
25
+
26
+ // Check if any document ID starts with 'part_'
27
+ for (const doc of snapshot.docs) {
28
+ if (doc.id.startsWith('part_')) {
29
+ return true;
30
+ }
31
+ }
32
+ return false;
33
+ } catch (error) {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Helper function to check if any shard document exists in a collection
40
+ * @param {Firestore.CollectionReference} collectionRef - Reference to the collection
41
+ * @returns {Promise<boolean>} - True if any shard_* document exists
42
+ */
43
+ async function checkAnyShardExists(collectionRef) {
44
+ try {
45
+ // List all documents in the collection
46
+ const snapshot = await collectionRef.limit(10).get();
47
+ if (snapshot.empty) return false;
48
+
49
+ // Check if any document ID starts with 'shard_'
50
+ for (const doc of snapshot.docs) {
51
+ if (doc.id.startsWith('shard_')) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ } catch (error) {
57
+ return false;
58
+ }
59
+ }
15
60
 
16
61
  exports.runRootDataIndexer = async (config, dependencies) => {
17
62
  const { db, logger } = dependencies;
@@ -30,23 +75,33 @@ exports.runRootDataIndexer = async (config, dependencies) => {
30
75
  logger.log('INFO', '[RootDataIndexer] Starting Root Data Availability Scan...');
31
76
 
32
77
  // 1. Price Availability
78
+ // Sample up to 10 shards to extract date keys (efficient - doesn't read all shards)
33
79
  const priceAvailabilitySet = new Set();
34
80
  try {
35
- // Path: asset_prices/shard_0
36
- const priceShardRef = db.collection(PRICE_COLLECTION_NAME).doc(PRICE_SHARD_ID);
37
- const priceSnap = await priceShardRef.get();
38
- if (priceSnap.exists) {
39
- const data = priceSnap.data();
40
- Object.values(data).forEach(instrument => {
41
- if (instrument.prices) {
42
- Object.keys(instrument.prices).forEach(dateKey => {
43
- if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) { priceAvailabilitySet.add(dateKey); }
44
- });
45
- }
46
- });
81
+ // Path: asset_prices/shard_*
82
+ const priceCollectionRef = db.collection(PRICE_COLLECTION_NAME);
83
+ const priceShardsSnapshot = await priceCollectionRef.limit(10).get();
84
+
85
+ if (!priceShardsSnapshot.empty) {
86
+ // Sample up to 10 shards and extract date keys from them
87
+ // This gives us a representative sample without reading hundreds of shards
88
+ for (const shardDoc of priceShardsSnapshot.docs) {
89
+ if (shardDoc.id.startsWith('shard_')) {
90
+ const data = shardDoc.data();
91
+ Object.values(data).forEach(instrument => {
92
+ if (instrument && instrument.prices) {
93
+ Object.keys(instrument.prices).forEach(dateKey => {
94
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
95
+ priceAvailabilitySet.add(dateKey);
96
+ }
97
+ });
98
+ }
99
+ });
100
+ }
101
+ }
47
102
  }
48
103
  } catch (e) {
49
- logger.log('ERROR', '[RootDataIndexer] Failed to load price shard.', { error: e.message });
104
+ logger.log('ERROR', '[RootDataIndexer] Failed to sample price shards.', { error: e.message });
50
105
  }
51
106
 
52
107
  // 2. Determine Date Range
@@ -96,17 +151,17 @@ exports.runRootDataIndexer = async (config, dependencies) => {
96
151
  // --- Define Refs & Check Paths ---
97
152
 
98
153
  // 1. Standard Retail
99
- // Path: {normalPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
100
- const normPortRef = db.collection(collections.normalPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
154
+ // Path: {normalPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
155
+ const normPortPartsRef = db.collection(collections.normalPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
101
156
 
102
- // Path: {speculatorPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
103
- const specPortRef = db.collection(collections.speculatorPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
157
+ // Path: {speculatorPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
158
+ const specPortPartsRef = db.collection(collections.speculatorPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
104
159
 
105
- // Path: {normalHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
106
- const normHistRef = db.collection(collections.normalHistory).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
160
+ // Path: {normalHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
161
+ const normHistPartsRef = db.collection(collections.normalHistory).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
107
162
 
108
- // Path: {speculatorHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
109
- const specHistRef = db.collection(collections.speculatorHistory).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
163
+ // Path: {speculatorHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
164
+ const specHistPartsRef = db.collection(collections.speculatorHistory).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
110
165
 
111
166
  // Path: {insights}/{YYYY-MM-DD}
112
167
  const insightsRef = db.collection(collections.insights).doc(dateStr);
@@ -119,26 +174,26 @@ exports.runRootDataIndexer = async (config, dependencies) => {
119
174
  // Path: {piRankings}/{YYYY-MM-DD}
120
175
  const piRankingsRef = db.collection(collections.piRankings || 'popular_investor_rankings').doc(dateStr);
121
176
 
122
- // Path: {piPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
123
- const piPortfoliosRef = db.collection(collections.piPortfolios || 'pi_portfolios_overall')
124
- .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
177
+ // Path: {piPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
178
+ const piPortfoliosPartsRef = db.collection(collections.piPortfolios || 'pi_portfolios_overall')
179
+ .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
125
180
 
126
- // Path: {piDeepPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
127
- const piDeepRef = db.collection(collections.piDeepPortfolios || 'pi_portfolios_deep')
128
- .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
181
+ // Path: {piDeepPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
182
+ const piDeepPartsRef = db.collection(collections.piDeepPortfolios || 'pi_portfolios_deep')
183
+ .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
129
184
 
130
- // Path: {piHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
131
- const piHistoryRef = db.collection(collections.piHistory || 'pi_trade_history')
132
- .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
185
+ // Path: {piHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
186
+ const piHistoryPartsRef = db.collection(collections.piHistory || 'pi_trade_history')
187
+ .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
133
188
 
134
189
  // 3. Signed-In Users
135
- // Path: {signedInUsers}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
136
- const signedInPortRef = db.collection(collections.signedInUsers || 'signed_in_user_portfolios')
137
- .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
190
+ // Path: {signedInUsers}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
191
+ const signedInPortPartsRef = db.collection(collections.signedInUsers || 'signed_in_users')
192
+ .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
138
193
 
139
- // Path: {signedInHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_0
140
- const signedInHistRef = db.collection(collections.signedInHistory || 'signed_in_user_history')
141
- .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts').doc(CANARY_PART_ID);
194
+ // Path: {signedInHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
195
+ const signedInHistPartsRef = db.collection(collections.signedInHistory || 'signed_in_user_history')
196
+ .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
142
197
 
143
198
  // Path: {verifications} (Limit 1) - Checks if collection is non-empty generally
144
199
  const verificationsRef = db.collection(collections.verifications || 'user_verifications');
@@ -152,26 +207,29 @@ exports.runRootDataIndexer = async (config, dependencies) => {
152
207
 
153
208
  // --- Execute Checks ---
154
209
  const [
155
- normPortSnap, specPortSnap,
156
- normHistSnap, specHistSnap,
210
+ normPortExists, specPortExists,
211
+ normHistExists, specHistExists,
157
212
  insightsSnap, socialQuerySnap,
158
213
  piRankingsSnap,
159
- piPortSnap, // UPDATED: Now a DocumentSnapshot
160
- piDeepSnap, // UPDATED: Now a DocumentSnapshot
161
- piHistSnap, // UPDATED: Now a DocumentSnapshot
162
- signedInPortSnap, signedInHistSnap,
214
+ piPortExists, // UPDATED: Check for any part_* document
215
+ piDeepExists, // UPDATED: Check for any part_* document
216
+ piHistExists, // UPDATED: Check for any part_* document
217
+ signedInPortExists, signedInHistExists,
163
218
  verificationsQuery,
164
219
  universalSocialSnap
165
220
  ] = await Promise.all([
166
- normPortRef.get(), specPortRef.get(),
167
- normHistRef.get(), specHistRef.get(),
168
- insightsRef.get(), socialPostsRef.limit(1).get(),
221
+ checkAnyPartExists(normPortPartsRef),
222
+ checkAnyPartExists(specPortPartsRef),
223
+ checkAnyPartExists(normHistPartsRef),
224
+ checkAnyPartExists(specHistPartsRef),
225
+ insightsRef.get(),
226
+ socialPostsRef.limit(1).get(),
169
227
  piRankingsRef.get(),
170
- piPortfoliosRef.get(), // UPDATED: Direct Doc Get (no limit)
171
- piDeepRef.get(), // UPDATED: Direct Doc Get (no limit)
172
- piHistoryRef.get(), // UPDATED: Direct Doc Get (no limit)
173
- signedInPortRef.get(),
174
- signedInHistRef.get(),
228
+ checkAnyPartExists(piPortfoliosPartsRef),
229
+ checkAnyPartExists(piDeepPartsRef),
230
+ checkAnyPartExists(piHistoryPartsRef),
231
+ checkAnyPartExists(signedInPortPartsRef),
232
+ checkAnyPartExists(signedInHistPartsRef),
175
233
  verificationsRef.limit(1).get(),
176
234
  universalSocialQuery.get()
177
235
  ]);
@@ -189,16 +247,16 @@ exports.runRootDataIndexer = async (config, dependencies) => {
189
247
  }
190
248
 
191
249
  // --- Assign to Availability ---
192
- availability.details.normalPortfolio = normPortSnap.exists;
193
- availability.details.speculatorPortfolio = specPortSnap.exists;
194
- availability.details.normalHistory = normHistSnap.exists;
195
- availability.details.speculatorHistory = specHistSnap.exists;
250
+ availability.details.normalPortfolio = normPortExists;
251
+ availability.details.speculatorPortfolio = specPortExists;
252
+ availability.details.normalHistory = normHistExists;
253
+ availability.details.speculatorHistory = specHistExists;
196
254
  availability.details.piRankings = piRankingsSnap.exists;
197
255
 
198
- // UPDATED: Now checking .exists on DocumentSnapshots
199
- availability.details.piPortfolios = piPortSnap.exists;
200
- availability.details.piDeepPortfolios = piDeepSnap.exists;
201
- availability.details.piHistory = piHistSnap.exists;
256
+ // UPDATED: Now checking for any part_* document existence
257
+ availability.details.piPortfolios = piPortExists;
258
+ availability.details.piDeepPortfolios = piDeepExists;
259
+ availability.details.piHistory = piHistExists;
202
260
 
203
261
  // PI & Signed-In Social Flags (Strict)
204
262
  availability.details.piSocial = foundPISocial;
@@ -207,14 +265,14 @@ exports.runRootDataIndexer = async (config, dependencies) => {
207
265
  availability.details.hasSignedInSocial = foundSignedInSocial;
208
266
 
209
267
  // Signed-In Flags
210
- availability.details.signedInUserPortfolio = signedInPortSnap.exists;
211
- availability.details.signedInUserHistory = signedInHistSnap.exists;
268
+ availability.details.signedInUserPortfolio = signedInPortExists;
269
+ availability.details.signedInUserHistory = signedInHistExists;
212
270
  availability.details.signedInUserVerification = !verificationsQuery.empty;
213
271
 
214
272
  // Aggregates
215
- // UPDATED: piPortSnap.exists used in aggregate logic
216
- availability.hasPortfolio = normPortSnap.exists || specPortSnap.exists || piPortSnap.exists || signedInPortSnap.exists;
217
- availability.hasHistory = normHistSnap.exists || specHistSnap.exists || piHistSnap.exists || signedInHistSnap.exists;
273
+ // UPDATED: Using part existence checks
274
+ availability.hasPortfolio = normPortExists || specPortExists || piPortExists || signedInPortExists;
275
+ availability.hasHistory = normHistExists || specHistExists || piHistExists || signedInHistExists;
218
276
  availability.hasInsights = insightsSnap.exists;
219
277
 
220
278
  // [CRITICAL FIX]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.403",
3
+ "version": "1.0.405",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [