bulltrackers-module 1.0.721 → 1.0.723

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,563 +1,256 @@
1
1
  /**
2
2
  * @fileoverview Root Data Indexer
3
3
  * Runs daily to index exactly what data is available for every date.
4
- * UPDATED: Includes 'targetDate' optimization for fast single-day updates.
5
- * UPDATED: Includes explicit checks for Signed-In User Portfolios, History, AND Social Data.
4
+ * REFACTORED: Simplifies checks by relying on BigQuery for migrated data types.
5
+ * RETAINS: Firestore checks for Verifications, Normal/Speculator users, and Generic Social.
6
6
  */
7
7
 
8
8
  const { FieldValue } = require('@google-cloud/firestore');
9
9
  const pLimit = require('p-limit');
10
10
 
11
11
  const CANARY_BLOCK_ID = '19M';
12
- const PRICE_SHARD_ID = 'shard_0';
13
12
 
14
13
  /**
15
14
  * Helper function to check if any part document exists in a parts collection
16
- * @param {Firestore.CollectionReference} partsCollectionRef - Reference to the parts collection
17
- * @returns {Promise<boolean>} - True if any part_* document exists
15
+ * Used only for legacy Retail (Normal/Speculator) data.
18
16
  */
19
17
  async function checkAnyPartExists(partsCollectionRef) {
20
18
  try {
21
- // List all documents in the parts collection
22
- const snapshot = await partsCollectionRef.limit(10).get();
23
- if (snapshot.empty) return false;
24
-
25
- // Check if any document ID starts with 'part_'
26
- for (const doc of snapshot.docs) {
27
- if (doc.id.startsWith('part_')) {
28
- return true;
29
- }
30
- }
31
- return false;
19
+ const snapshot = await partsCollectionRef.limit(1).get();
20
+ return !snapshot.empty;
32
21
  } catch (error) {
33
22
  return false;
34
23
  }
35
24
  }
36
25
 
37
- /**
38
- * Helper function to check if any shard document exists in a collection
39
- * @param {Firestore.CollectionReference} collectionRef - Reference to the collection
40
- * @returns {Promise<boolean>} - True if any shard_* document exists
41
- */
42
- async function checkAnyShardExists(collectionRef) {
43
- try {
44
- // List all documents in the collection
45
- const snapshot = await collectionRef.limit(10).get();
46
- if (snapshot.empty) return false;
47
-
48
- // Check if any document ID starts with 'shard_'
49
- for (const doc of snapshot.docs) {
50
- if (doc.id.startsWith('shard_')) {
51
- return true;
52
- }
53
- }
54
- return false;
55
- } catch (error) {
56
- return false;
57
- }
58
- }
59
-
60
26
  exports.runRootDataIndexer = async (config, dependencies) => {
61
27
  const { db, logger } = dependencies;
62
28
  const {
63
29
  availabilityCollection,
64
30
  earliestDate,
65
31
  collections = {},
66
- targetDate // [NEW] Optional parameter to scan a single specific date
32
+ targetDate
67
33
  } = config;
68
34
 
69
- const PRICE_COLLECTION_NAME = 'asset_prices';
70
-
71
- // Collection Names (Fail-safe defaults)
72
- const PI_SOCIAL_COLL_NAME = collections.piSocial || 'pi_social_posts';
73
- const SIGNED_IN_SOCIAL_COLL_NAME = collections.signedInUserSocialCollection || 'signed_in_users_social';
74
-
75
- // Ensure all required collections have defaults to prevent "collectionPath is not valid" errors
35
+ // Collection Config (Retained for Firestore-based types)
76
36
  const safeCollections = {
77
37
  normalPortfolios: collections.normalPortfolios || 'NormalUserPortfolios',
78
38
  speculatorPortfolios: collections.speculatorPortfolios || 'SpeculatorPortfolios',
79
39
  normalHistory: collections.normalHistory || 'NormalUserTradeHistory',
80
40
  speculatorHistory: collections.speculatorHistory || 'SpeculatorTradeHistory',
81
- insights: collections.insights || 'daily_instrument_insights',
82
- social: collections.social || 'daily_social_insights',
83
- prices: collections.prices || PRICE_COLLECTION_NAME,
84
- piRankings: collections.piRankings || 'popular_investor_rankings',
85
- piPortfolios: collections.piPortfolios || 'pi_portfolios_overall',
86
- piDeepPortfolios: collections.piDeepPortfolios || 'pi_portfolios_deep',
87
- piHistory: collections.piHistory || 'pi_trade_history',
88
- signedInUsers: collections.signedInUsers || 'signed_in_users',
89
- signedInHistory: collections.signedInHistory || 'signed_in_user_history',
41
+ social: collections.social || 'daily_social_insights', // Generic Social
90
42
  verifications: collections.verifications || 'user_verifications',
91
- // New Root Data Types for Profile Metrics
92
- piRatings: collections.piRatings || 'PIRatingsData',
93
- piPageViews: collections.piPageViews || 'PIPageViewsData',
94
- watchlistMembership: collections.watchlistMembership || 'WatchlistMembershipData',
95
- piAlertHistory: collections.piAlertHistory || 'PIAlertHistoryData',
96
- piMasterList: collections.piMasterList || 'system_state', // [NEW] Collection for master list
97
- ...collections // Allow overrides
43
+ ...collections
98
44
  };
99
45
 
100
46
  const scanMode = targetDate ? 'SINGLE_DATE' : 'FULL_SCAN';
101
- logger.log('INFO', `[RootDataIndexer] Starting Root Data Availability Scan... Mode: ${scanMode}`, { targetDate });
102
-
103
- // 1. Price Availability - Read from date tracking documents
104
- // Find the latest price tracking document and extract available dates
105
- const priceAvailabilitySet = new Set();
47
+ logger.log('INFO', `[RootDataIndexer] Starting Scan... Mode: ${scanMode}`, { targetDate });
106
48
 
107
- // Get price tracking collection name from registry if available
108
- let priceTrackingCollectionName = 'pricedatastoreddates';
109
- if (dependencies.collectionRegistry && dependencies.collectionRegistry.getCollectionPath) {
110
- try {
111
- const trackingPath = dependencies.collectionRegistry.getCollectionPath('rootData', 'priceTracking', { fetchDate: '2025-01-01' });
112
- priceTrackingCollectionName = trackingPath.split('/')[0];
113
- } catch (e) {
114
- logger.log('WARN', `[RootDataIndexer] Failed to get price tracking collection from registry, using default: ${e.message}`);
115
- }
116
- }
117
-
118
- try {
119
- // Get the latest price date tracking document
120
- const dateTrackingRef = db.collection(priceTrackingCollectionName)
121
- .orderBy('fetchDate', 'desc')
122
- .limit(1);
123
-
124
- const dateTrackingSnapshot = await dateTrackingRef.get();
125
-
126
- if (!dateTrackingSnapshot.empty) {
127
- const latestTrackingDoc = dateTrackingSnapshot.docs[0].data();
128
- const datesAvailable = latestTrackingDoc.datesAvailable || [];
129
- const fetchDate = latestTrackingDoc.fetchDate;
130
-
131
- // Add all dates from the tracking document
132
- datesAvailable.forEach(dateKey => {
133
- if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
134
- priceAvailabilitySet.add(dateKey);
135
- }
136
- });
137
-
138
- // IMPORTANT: If the tracking document was written for today (fetchDate matches targetDate),
139
- // we should consider prices available for that date even if the API didn't return that exact date.
140
- // This is because the price fetcher ran for that date and stored prices (even if they're historical).
141
- if (targetDate && fetchDate === targetDate) {
142
- priceAvailabilitySet.add(targetDate);
143
- logger.log('INFO', `[RootDataIndexer] Added fetchDate (${fetchDate}) to price availability set for target date check`);
144
- }
145
-
146
- logger.log('INFO', `[RootDataIndexer] Loaded ${priceAvailabilitySet.size} price dates from tracking document (fetchDate: ${fetchDate})`);
147
-
148
- // Debug: Log a sample of dates and check if target date is present
149
- if (targetDate) {
150
- const sampleDates = Array.from(priceAvailabilitySet).slice(0, 5);
151
- const hasTargetDate = priceAvailabilitySet.has(targetDate);
152
- logger.log('INFO', `[RootDataIndexer] Price availability check for ${targetDate}: ${hasTargetDate ? 'FOUND' : 'NOT FOUND'}. Sample dates: ${sampleDates.join(', ')}`);
153
- }
154
- } else {
155
- logger.log('WARN', '[RootDataIndexer] No price date tracking documents found. Falling back to empty set.');
156
- }
157
- } catch (e) {
158
- logger.log('ERROR', '[RootDataIndexer] Failed to load price date tracking document.', { error: e.message });
159
- // Fallback: try to sample shards if tracking document fails
160
- if (!targetDate) {
161
- try {
162
- const priceCollectionRef = db.collection(PRICE_COLLECTION_NAME);
163
- const priceShardsSnapshot = await priceCollectionRef.limit(10).get();
164
-
165
- if (!priceShardsSnapshot.empty) {
166
- for (const shardDoc of priceShardsSnapshot.docs) {
167
- if (shardDoc.id.startsWith('shard_')) {
168
- const data = shardDoc.data();
169
- Object.values(data).forEach(instrument => {
170
- if (instrument && instrument.prices) {
171
- Object.keys(instrument.prices).forEach(dateKey => {
172
- if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
173
- priceAvailabilitySet.add(dateKey);
174
- }
175
- });
176
- }
177
- });
178
- }
179
- }
180
- logger.log('INFO', `[RootDataIndexer] Fallback: Loaded ${priceAvailabilitySet.size} price dates from shard sampling.`);
181
- }
182
- } catch (fallbackError) {
183
- logger.log('ERROR', '[RootDataIndexer] Fallback shard sampling also failed.', { error: fallbackError.message });
184
- }
185
- }
186
- }
187
-
188
- // 2. Determine Date Range
49
+ // 1. Determine Date Range
189
50
  const datesToScan = [];
190
51
  if (targetDate) {
191
- // [NEW] Single Date Optimization
192
52
  datesToScan.push(targetDate);
193
53
  } else {
194
- // [OLD] Full History
195
54
  const start = new Date(earliestDate || '2023-01-01');
196
55
  const end = new Date();
197
56
  end.setDate(end.getDate() + 1);
198
-
199
57
  for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
200
58
  datesToScan.push(d.toISOString().slice(0, 10));
201
59
  }
202
60
  }
203
61
 
204
- // 3. Scan in Parallel
205
62
  const limit = pLimit(20);
206
63
  let updatesCount = 0;
207
64
 
208
65
  const promises = datesToScan.map(dateStr => limit(async () => {
209
66
  try {
210
- // Define Time Range for Social Query (Full Day UTC)
211
- // Use UTC methods to ensure correct timezone handling
212
- const dayStart = new Date(dateStr + 'T00:00:00.000Z');
213
- const dayEnd = new Date(dateStr + 'T23:59:59.999Z');
214
-
215
67
  const availability = {
216
68
  date: dateStr,
217
69
  lastUpdated: FieldValue.serverTimestamp(),
70
+ // High-level aggregates
218
71
  hasPortfolio: false,
219
72
  hasHistory: false,
220
73
  hasSocial: false,
221
74
  hasInsights: false,
222
75
  hasPrices: false,
223
76
  details: {
77
+ // --- FIRESTORE DRIVEN (Not Migrated) ---
224
78
  normalPortfolio: false,
225
79
  speculatorPortfolio: false,
226
80
  normalHistory: false,
227
81
  speculatorHistory: false,
82
+ signedInUserVerification: false, // Explicitly Firestore
83
+
84
+ // --- BIGQUERY DRIVEN (Migrated) ---
228
85
  piRankings: false,
229
86
  piPortfolios: false,
230
- piDeepPortfolios: false,
87
+ piDeepPortfolios: false, // Deprecated/Merged into piPortfolios
231
88
  piHistory: false,
232
- piSocial: false, // Specific Flag for PI
89
+ piSocial: false,
233
90
  signedInUserPortfolio: false,
234
91
  signedInUserHistory: false,
235
- signedInUserVerification: false,
236
- signedInSocial: false, // Specific Flag for Signed-In
237
- // New Root Data Types for Profile Metrics
92
+ signedInSocial: false,
93
+
94
+ // Profile Metrics (BigQuery)
238
95
  piRatings: false,
239
96
  piPageViews: false,
240
97
  watchlistMembership: false,
241
98
  piAlertHistory: false,
242
- piMasterList: false // [NEW] Flag for Master List
99
+ piMasterList: false
243
100
  }
244
101
  };
245
102
 
246
- // --- Define Refs & Check Paths ---
247
-
248
103
  // =========================================================================
249
- // BIGQUERY FIRST: Check BigQuery tables for data availability
104
+ // 1. BIGQUERY CHECKS (Primary for most data)
250
105
  // =========================================================================
251
- let bigqueryHasPortfolio = false;
252
- let bigqueryHasHistory = false;
253
- let bigqueryHasSocial = false;
254
- let bigqueryHasInsights = false;
255
- let bigqueryHasPrices = false;
256
- let bigqueryHasPIRankings = false;
257
- let bigqueryHasPIMasterList = false;
258
-
106
+ let bqData = {
107
+ portfolio: {}, history: {}, social: {},
108
+ insights: [], prices: {}, rankings: [], masterList: {},
109
+ ratings: {}, pageViews: {}, watchlists: {}, alerts: {}
110
+ };
111
+
259
112
  if (process.env.BIGQUERY_ENABLED !== 'false') {
260
113
  try {
261
114
  const {
262
- queryPortfolioData,
263
- queryHistoryData,
264
- querySocialData,
265
- queryInstrumentInsights,
266
- queryAssetPrices,
267
- queryPIRankings,
268
- queryPIMasterList
115
+ queryPortfolioData, queryHistoryData, querySocialData,
116
+ queryInstrumentInsights, queryAssetPrices, queryPIRankings,
117
+ queryPIMasterList, queryPIRatings, queryPIPageViews,
118
+ queryWatchlistMembership, queryPIAlertHistory
269
119
  } = require('../core/utils/bigquery_utils');
270
120
 
271
- // Check BigQuery for today's data (or target date)
272
- const [portfolioData, historyData, socialData, insightsData, pricesData, rankingsData, masterListData] = await Promise.all([
273
- queryPortfolioData(dateStr, null, null, logger).catch(() => null),
274
- queryHistoryData(dateStr, null, null, logger).catch(() => null),
275
- querySocialData(dateStr, null, null, logger).catch(() => null),
276
- queryInstrumentInsights(dateStr, logger).catch(() => null),
277
- queryAssetPrices(dateStr, dateStr, null, logger).catch(() => null), // Check for specific date
278
- queryPIRankings(dateStr, logger).catch(() => null),
279
- queryPIMasterList(logger).catch(() => null)
121
+ const [p, h, s, i, pr, r, ml, rt, pv, wl, ah] = await Promise.all([
122
+ queryPortfolioData(dateStr, null, null, logger).catch(() => ({})),
123
+ queryHistoryData(dateStr, null, null, logger).catch(() => ({})),
124
+ querySocialData(dateStr, null, null, logger).catch(() => ({})),
125
+ queryInstrumentInsights(dateStr, logger).catch(() => []),
126
+ queryAssetPrices(dateStr, dateStr, null, logger).catch(() => ({})),
127
+ queryPIRankings(dateStr, logger).catch(() => []),
128
+ queryPIMasterList(logger).catch(() => ({})),
129
+ queryPIRatings(dateStr, logger).catch(() => ({})),
130
+ queryPIPageViews(dateStr, logger).catch(() => ({})),
131
+ queryWatchlistMembership(dateStr, logger).catch(() => ({})),
132
+ queryPIAlertHistory(dateStr, logger).catch(() => ({}))
280
133
  ]);
281
-
282
- bigqueryHasPortfolio = portfolioData && Object.keys(portfolioData).length > 0;
283
- bigqueryHasHistory = historyData && Object.keys(historyData).length > 0;
284
- bigqueryHasSocial = socialData && Object.keys(socialData).length > 0;
285
- bigqueryHasInsights = insightsData && Array.isArray(insightsData) && insightsData.length > 0;
286
- bigqueryHasPrices = pricesData && Object.keys(pricesData).length > 0;
287
- bigqueryHasPIRankings = rankingsData && Array.isArray(rankingsData) && rankingsData.length > 0;
288
- bigqueryHasPIMasterList = masterListData && Object.keys(masterListData).length > 0;
289
-
290
- if (bigqueryHasPortfolio || bigqueryHasHistory || bigqueryHasSocial || bigqueryHasInsights || bigqueryHasPrices || bigqueryHasPIRankings) {
291
- logger.log('INFO', `[RootDataIndexer/${dateStr}] ✅ Found data in BigQuery: portfolio=${bigqueryHasPortfolio}, history=${bigqueryHasHistory}, social=${bigqueryHasSocial}, insights=${bigqueryHasInsights}, prices=${bigqueryHasPrices}, rankings=${bigqueryHasPIRankings}`);
292
- }
293
- } catch (bqError) {
294
- logger.log('WARN', `[RootDataIndexer/${dateStr}] BigQuery check failed, using Firestore fallback: ${bqError.message}`);
134
+ bqData = {
135
+ portfolio: p || {}, history: h || {}, social: s || {},
136
+ insights: i || [], prices: pr || {}, rankings: r || [], masterList: ml || {},
137
+ ratings: rt || {}, pageViews: pv || {}, watchlists: wl || {}, alerts: ah || {}
138
+ };
139
+ } catch (e) {
140
+ logger.log('WARN', `[RootDataIndexer/${dateStr}] BigQuery check failed: ${e.message}`);
295
141
  }
296
142
  }
143
+
144
+ // Helper to check user types in BQ result maps
145
+ const hasType = (map, type) => Object.values(map).some(u => u.user_type === type);
146
+
147
+ // --- Populate Migrated Flags from BQ ---
148
+ availability.details.piPortfolios = hasType(bqData.portfolio, 'POPULAR_INVESTOR');
149
+ availability.details.signedInUserPortfolio = hasType(bqData.portfolio, 'SIGNED_IN_USER');
297
150
 
298
- // 1. Standard Retail
299
- // Path: {normalPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
300
- const normPortPartsRef = db.collection(safeCollections.normalPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
151
+ availability.details.piHistory = hasType(bqData.history, 'POPULAR_INVESTOR');
152
+ availability.details.signedInUserHistory = hasType(bqData.history, 'SIGNED_IN_USER');
301
153
 
302
- // Path: {speculatorPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
303
- const specPortPartsRef = db.collection(safeCollections.speculatorPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
154
+ availability.details.piSocial = hasType(bqData.social, 'POPULAR_INVESTOR');
155
+ availability.details.signedInSocial = hasType(bqData.social, 'SIGNED_IN_USER');
156
+
157
+ availability.details.piRankings = bqData.rankings.length > 0 || (bqData.rankings.Items && bqData.rankings.Items.length > 0);
158
+ availability.details.piMasterList = Object.keys(bqData.masterList).length > 0;
159
+
160
+ // Profile Metrics
161
+ availability.details.piRatings = Object.keys(bqData.ratings).length > 0;
162
+ availability.details.piPageViews = Object.keys(bqData.pageViews).length > 0;
163
+ availability.details.watchlistMembership = Object.keys(bqData.watchlists).length > 0;
164
+ availability.details.piAlertHistory = Object.keys(bqData.alerts).length > 0;
165
+
166
+ // Global Flags based on BQ
167
+ availability.hasInsights = bqData.insights.length > 0;
168
+ availability.hasPrices = Object.keys(bqData.prices).length > 0;
169
+
170
+ // =========================================================================
171
+ // 2. FIRESTORE CHECKS (Only for Non-Migrated Data)
172
+ // =========================================================================
304
173
 
305
- // Path: {normalHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
174
+ // A. Normal/Speculator Portfolios & History (Legacy Structure)
175
+ // Path: {collection}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
176
+ const normPortPartsRef = db.collection(safeCollections.normalPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
177
+ const specPortPartsRef = db.collection(safeCollections.speculatorPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
306
178
  const normHistPartsRef = db.collection(safeCollections.normalHistory).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
307
-
308
- // Path: {speculatorHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
309
179
  const specHistPartsRef = db.collection(safeCollections.speculatorHistory).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
310
-
311
- // Path: {insights}/{YYYY-MM-DD}
312
- const insightsRef = db.collection(safeCollections.insights).doc(dateStr);
313
-
314
- // Generic Asset Posts
180
+
181
+ // B. Generic Social (Instrument Feed)
315
182
  // Path: {social}/{YYYY-MM-DD}/posts (Limit 1)
316
183
  const socialPostsRef = db.collection(safeCollections.social).doc(dateStr).collection('posts');
317
184
 
318
- // 2. Popular Investors (NEW STRUCTURE)
319
- // Path: {piRankings}/{YYYY-MM-DD}
320
- const piRankingsRef = db.collection(safeCollections.piRankings).doc(dateStr);
321
-
322
- // Path: PopularInvestorPortfolioData/{YYYY-MM-DD}/{cid}
323
- const piPortfoliosCollectionRef = db.collection('PopularInvestorPortfolioData').doc(dateStr);
324
-
325
- // Path: PopularInvestorTradeHistoryData/{YYYY-MM-DD}/{cid}
326
- const piHistoryCollectionRef = db.collection('PopularInvestorTradeHistoryData').doc(dateStr);
327
-
328
- // Path: PopularInvestorSocialPostData/{YYYY-MM-DD}/{cid}
329
- const piSocialCollectionRef = db.collection('PopularInvestorSocialPostData').doc(dateStr);
330
-
331
- // Legacy paths (for backward compatibility during migration)
332
- const piPortfoliosPartsRef = db.collection(safeCollections.piPortfolios)
333
- .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
334
-
335
- const piDeepPartsRef = db.collection(safeCollections.piDeepPortfolios)
336
- .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
337
-
338
- const piHistoryPartsRef = db.collection(safeCollections.piHistory)
339
- .doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
340
-
341
- // 3. Signed-In Users (NEW STRUCTURE)
342
- // Path: SignedInUserPortfolioData/{YYYY-MM-DD}/{cid}
343
- const signedInPortCollectionRef = db.collection('SignedInUserPortfolioData').doc(dateStr);
344
-
345
- // Path: SignedInUserTradeHistoryData/{YYYY-MM-DD}/{cid}
346
- const signedInHistCollectionRef = db.collection('SignedInUserTradeHistoryData').doc(dateStr);
347
-
348
- // Path: SignedInUserSocialPostData/{YYYY-MM-DD}/{cid}
349
- const signedInSocialCollectionRef = db.collection('SignedInUserSocialPostData').doc(dateStr);
350
-
351
- // Path: {verifications} (Limit 1) - Checks if collection is non-empty generally
185
+ // C. Verifications
352
186
  const verificationsRef = db.collection(safeCollections.verifications);
353
-
354
- // 4. New Root Data Types for Profile Metrics (Global documents per date)
355
- // Path: PIRatingsData/{YYYY-MM-DD}
356
- const piRatingsRef = db.collection(safeCollections.piRatings).doc(dateStr);
357
-
358
- // Path: PIPageViewsData/{YYYY-MM-DD}
359
- const piPageViewsRef = db.collection(safeCollections.piPageViews).doc(dateStr);
360
-
361
- // Path: WatchlistMembershipData/{YYYY-MM-DD}
362
- const watchlistMembershipRef = db.collection(safeCollections.watchlistMembership).doc(dateStr);
363
-
364
- // Path: PIAlertHistoryData/{YYYY-MM-DD}
365
- const piAlertHistoryRef = db.collection(safeCollections.piAlertHistory).doc(dateStr);
366
187
 
367
- // [NEW] Master List Ref (Single Global Document)
368
- // Path: system_state/popular_investor_master_list
369
- const piMasterListRef = db.collection(safeCollections.piMasterList).doc('popular_investor_master_list');
370
-
371
- // 4. Social Data Checks - Use date tracking documents (NEW STRUCTURE)
372
- // Single tracking documents at root level:
373
- // - PopularInvestorSocialPostData/_dates -> fetchedDates.{date}
374
- // - SignedInUserSocialPostData/_dates -> fetchedDates.{date}
375
- // For generic social (InstrumentFeedSocialPostData), check the posts subcollection
376
- const [piSocialTrackingDoc, signedInSocialTrackingDoc, genericSocialSnap] = await Promise.all([
377
- db.collection('PopularInvestorSocialPostData').doc('_dates').get(),
378
- db.collection('SignedInUserSocialPostData').doc('_dates').get(),
379
- db.collection('InstrumentFeedSocialPostData').doc(dateStr).collection('posts').limit(1).get()
380
- ]);
381
-
382
- // Check if date exists in tracking documents
383
- // The _dates document uses dot notation: fetchedDates.2025-12-29: true
384
- // When read, this becomes: { fetchedDates: { "2025-12-29": true } }
385
- let foundPISocial = false;
386
- let foundSignedInSocial = false;
387
-
388
- if (piSocialTrackingDoc.exists) {
389
- const data = piSocialTrackingDoc.data();
390
- // Check both nested structure and flat dot-notation structure
391
- if (data.fetchedDates && typeof data.fetchedDates === 'object') {
392
- if (data.fetchedDates[dateStr] === true) {
393
- foundPISocial = true;
394
- }
395
- } else if (data[`fetchedDates.${dateStr}`] === true) {
396
- // Handle flat dot-notation structure (if Firestore stores it that way)
397
- foundPISocial = true;
398
- }
399
- }
400
-
401
- if (signedInSocialTrackingDoc.exists) {
402
- const data = signedInSocialTrackingDoc.data();
403
- // Check both nested structure and flat dot-notation structure
404
- if (data.fetchedDates && typeof data.fetchedDates === 'object') {
405
- if (data.fetchedDates[dateStr] === true) {
406
- foundSignedInSocial = true;
407
- }
408
- } else if (data[`fetchedDates.${dateStr}`] === true) {
409
- // Handle flat dot-notation structure (if Firestore stores it that way)
410
- foundSignedInSocial = true;
411
- }
412
- }
413
-
414
- // --- Execute Checks ---
415
- // Helper to check if any documents exist in a date collection (new structure)
416
- const checkDateCollectionHasDocs = async (collectionRef) => {
417
- try {
418
- // For new structure: collection/{date} is a document, check if it has subcollections (CIDs)
419
- const subcollections = await collectionRef.listCollections();
420
- if (subcollections.length === 0) return false;
421
- // Check if any subcollection (CID) has a document
422
- for (const subcol of subcollections) {
423
- const docs = await subcol.limit(1).get();
424
- if (!docs.empty) return true;
425
- }
426
- return false;
427
- } catch (e) {
428
- return false;
429
- }
430
- };
431
-
188
+ // Execute Firestore Queries
432
189
  const [
433
190
  normPortExists, specPortExists,
434
191
  normHistExists, specHistExists,
435
- insightsSnap, genericSocialExists,
436
- piRankingsSnap,
437
- piPortExists,
438
- piDeepExists,
439
- piHistExists,
440
- signedInPortExists, signedInHistExists,
441
- verificationsQuery,
442
- // New Root Data Types
443
- piRatingsSnap,
444
- piPageViewsSnap,
445
- watchlistMembershipSnap,
446
- piAlertHistorySnap,
447
- piMasterListSnap // [NEW]
192
+ genericSocialDocs,
193
+ verificationsQuery
448
194
  ] = await Promise.all([
449
195
  checkAnyPartExists(normPortPartsRef),
450
196
  checkAnyPartExists(specPortPartsRef),
451
197
  checkAnyPartExists(normHistPartsRef),
452
198
  checkAnyPartExists(specHistPartsRef),
453
- insightsRef.get(),
454
- Promise.resolve(!genericSocialSnap.empty),
455
- piRankingsRef.get(),
456
- // Check new structure first, fallback to legacy
457
- checkDateCollectionHasDocs(piPortfoliosCollectionRef).then(exists => exists || checkAnyPartExists(piPortfoliosPartsRef)),
458
- checkAnyPartExists(piDeepPartsRef), // Legacy only
459
- // Check new structure first, fallback to legacy
460
- checkDateCollectionHasDocs(piHistoryCollectionRef).then(exists => exists || checkAnyPartExists(piHistoryPartsRef)),
461
- // Check new structure for signed-in users
462
- checkDateCollectionHasDocs(signedInPortCollectionRef),
463
- checkDateCollectionHasDocs(signedInHistCollectionRef),
464
- verificationsRef.limit(1).get(),
465
- // New Root Data Types - check if document exists
466
- piRatingsRef.get(),
467
- piPageViewsRef.get(),
468
- watchlistMembershipRef.get(),
469
- piAlertHistoryRef.get(),
470
- piMasterListRef.get() // [NEW] - Moved to end to match destructuring order
471
- ]);
472
-
473
- // Also check social collections directly (new structure)
474
- const [piSocialExists, signedInSocialExists] = await Promise.all([
475
- checkDateCollectionHasDocs(piSocialCollectionRef),
476
- checkDateCollectionHasDocs(signedInSocialCollectionRef)
199
+ socialPostsRef.limit(1).get(),
200
+ verificationsRef.limit(1).get()
477
201
  ]);
478
-
479
- // Update social flags if found in new structure
480
- if (piSocialExists) foundPISocial = true;
481
- if (signedInSocialExists) foundSignedInSocial = true;
482
-
483
- // Social data checks are done above using tracking documents
484
- // foundPISocial and foundSignedInSocial are already set from the tracking document checks
485
- logger.log('INFO', `[RootDataIndexer/${dateStr}] Social check results - PI: ${foundPISocial}, Signed-in: ${foundSignedInSocial}`);
486
202
 
487
- // --- Assign to Availability ---
488
- // Use BigQuery results if available, otherwise fall back to Firestore
489
- availability.details.normalPortfolio = bigqueryHasPortfolio || normPortExists;
490
- availability.details.speculatorPortfolio = bigqueryHasPortfolio || specPortExists;
491
- availability.details.normalHistory = bigqueryHasHistory || normHistExists;
492
- availability.details.speculatorHistory = bigqueryHasHistory || specHistExists;
493
- availability.details.piRankings = bigqueryHasPIRankings || piRankingsSnap.exists;
494
-
495
- availability.details.piPortfolios = bigqueryHasPortfolio || piPortExists;
496
- availability.details.piDeepPortfolios = piDeepExists; // Legacy only, no BigQuery equivalent
497
- availability.details.piHistory = bigqueryHasHistory || piHistExists;
498
-
499
- // PI & Signed-In Social Flags (Strict)
500
- // Use BigQuery if available, otherwise use Firestore tracking documents
501
- const finalPISocial = bigqueryHasSocial || foundPISocial;
502
- const finalSignedInSocial = bigqueryHasSocial || foundSignedInSocial;
503
- availability.details.piSocial = finalPISocial;
504
- availability.details.hasPISocial = finalPISocial;
505
- availability.details.signedInSocial = finalSignedInSocial;
506
- availability.details.hasSignedInSocial = finalSignedInSocial;
507
-
508
- // Signed-In Flags
509
- availability.details.signedInUserPortfolio = bigqueryHasPortfolio || signedInPortExists;
510
- availability.details.signedInUserHistory = bigqueryHasHistory || signedInHistExists;
203
+ // --- Populate Non-Migrated Flags ---
204
+ availability.details.normalPortfolio = normPortExists;
205
+ availability.details.speculatorPortfolio = specPortExists;
206
+ availability.details.normalHistory = normHistExists;
207
+ availability.details.speculatorHistory = specHistExists;
511
208
  availability.details.signedInUserVerification = !verificationsQuery.empty;
512
-
513
- // New Root Data Types for Profile Metrics
514
- availability.details.piRatings = piRatingsSnap.exists;
515
- availability.details.piPageViews = piPageViewsSnap.exists;
516
- availability.details.watchlistMembership = watchlistMembershipSnap.exists;
517
- availability.details.piAlertHistory = piAlertHistorySnap.exists;
518
-
519
- // Aggregates (use BigQuery if available, otherwise Firestore)
520
- availability.hasPortfolio = bigqueryHasPortfolio || normPortExists || specPortExists || piPortExists || signedInPortExists;
521
- availability.hasHistory = bigqueryHasHistory || normHistExists || specHistExists || piHistExists || signedInHistExists;
522
- availability.hasInsights = bigqueryHasInsights || insightsSnap.exists;
523
- availability.hasSocial = bigqueryHasSocial || finalPISocial || finalSignedInSocial || genericSocialExists;
524
209
 
525
- // [NEW] Assign Master List Availability (BigQuery first, then Firestore)
526
- availability.details.piMasterList = bigqueryHasPIMasterList || piMasterListSnap.exists;
527
-
528
- // Price Check (BigQuery first, then Firestore tracking document)
529
- const hasPriceForDate = bigqueryHasPrices || priceAvailabilitySet.has(dateStr);
530
- availability.hasPrices = hasPriceForDate;
210
+ // =========================================================================
211
+ // 3. AGGREGATE FLAGS
212
+ // =========================================================================
531
213
 
532
- if (targetDate && !hasPriceForDate) {
533
- logger.log('WARN', `[RootDataIndexer/${dateStr}] Price data not found in tracking document. Set size: ${priceAvailabilitySet.size}, Date checked: ${dateStr}`);
534
- }
214
+ availability.hasPortfolio =
215
+ availability.details.piPortfolios ||
216
+ availability.details.signedInUserPortfolio ||
217
+ normPortExists ||
218
+ specPortExists;
219
+
220
+ availability.hasHistory =
221
+ availability.details.piHistory ||
222
+ availability.details.signedInUserHistory ||
223
+ normHistExists ||
224
+ specHistExists;
225
+
226
+ availability.hasSocial =
227
+ availability.details.piSocial ||
228
+ availability.details.signedInSocial ||
229
+ !genericSocialDocs.empty;
535
230
 
231
+ // Store Result
536
232
  await db.collection(availabilityCollection).doc(dateStr).set(availability);
537
233
  updatesCount++;
538
234
 
539
- // Log detailed results for this date
540
- const detailsLog = Object.entries(availability.details)
541
- .map(([key, value]) => `${key}: ${value ? '' : ''}`)
542
- .join(', ');
543
- logger.log('INFO', `[RootDataIndexer/${dateStr}] Data availability: ${detailsLog}`);
235
+ const summary = [
236
+ availability.hasPortfolio ? 'Port' : '',
237
+ availability.hasHistory ? 'Hist' : '',
238
+ availability.hasSocial ? 'Soc' : '',
239
+ availability.hasInsights ? 'Ins' : '',
240
+ availability.hasPrices ? 'Prc' : ''
241
+ ].filter(Boolean).join('+');
242
+
243
+ logger.log('INFO', `[RootDataIndexer/${dateStr}] Indexed. Found: [${summary || 'NONE'}]`);
544
244
 
545
245
  } catch (e) {
546
- logger.log('ERROR', `[RootDataIndexer] Failed to index ${dateStr}`, {
547
- message: e.message,
548
- code: e.code
549
- });
246
+ logger.log('ERROR', `[RootDataIndexer] Failed to index ${dateStr}`, { message: e.message });
550
247
  }
551
248
  }));
552
249
 
553
250
  await Promise.all(promises);
554
251
 
555
- // Log appropriately based on results
556
- if (updatesCount === 0) {
557
- logger.log('WARN', `[RootDataIndexer] Indexing complete but NO dates were updated. This may indicate a failure. Mode: ${scanMode}`, { targetDate });
558
- } else {
559
- logger.log('SUCCESS', `[RootDataIndexer] Indexing complete. Updated ${updatesCount} dates. Mode: ${scanMode}`, { targetDate, updatesCount });
560
- }
252
+ if (updatesCount === 0) logger.log('WARN', `[RootDataIndexer] No dates updated. Mode: ${scanMode}`);
253
+ else logger.log('SUCCESS', `[RootDataIndexer] Updated ${updatesCount} dates. Mode: ${scanMode}`);
561
254
 
562
- return { success: updatesCount > 0, count: updatesCount, mode: scanMode };
255
+ return { success: updatesCount > 0, count: updatesCount };
563
256
  };