bulltrackers-module 1.0.468 → 1.0.470

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.
@@ -397,10 +397,42 @@ async function getSignedInUsersToUpdate(dependencies, config) {
397
397
  targets.push({ cid: String(cid), username });
398
398
  });
399
399
 
400
- // Now filter out users who were updated in the last 18 hours
401
- // Check user_sync_requests for last completed sync
400
+ // Now filter out users who were updated today
401
+ // Check actual portfolio/history snapshot dates, not just sync request timestamps
402
402
  const filteredTargets = [];
403
+ const today = new Date().toISOString().split('T')[0];
403
404
  const checkPromises = targets.map(async (target) => {
405
+ // Check if user has portfolio or history data for today
406
+ const blockId = `${Math.floor(parseInt(target.cid) / 1000000)}M`;
407
+
408
+ // Check portfolio snapshot for today
409
+ const portfolioSnapshotRef = db.collection(collectionName)
410
+ .doc(blockId)
411
+ .collection('snapshots')
412
+ .doc(today)
413
+ .collection('parts')
414
+ .limit(1);
415
+
416
+ const portfolioSnapshot = await portfolioSnapshotRef.get();
417
+
418
+ // Check history snapshot for today
419
+ const historyCollection = config.signedInUserHistoryCollection || process.env.FIRESTORE_COLLECTION_SIGNED_IN_USER_HISTORY || 'signed_in_user_history';
420
+ const historySnapshotRef = db.collection(historyCollection)
421
+ .doc(blockId)
422
+ .collection('snapshots')
423
+ .doc(today)
424
+ .collection('parts')
425
+ .limit(1);
426
+
427
+ const historySnapshot = await historySnapshotRef.get();
428
+
429
+ // If user has data for today, skip them (they're already up-to-date)
430
+ if (!portfolioSnapshot.empty || !historySnapshot.empty) {
431
+ skippedCount++;
432
+ return null; // Skip this user - already updated today
433
+ }
434
+
435
+ // Also check sync requests as a fallback (in case data exists but snapshot check failed)
404
436
  const syncRef = db.collection('user_sync_requests')
405
437
  .doc(target.cid)
406
438
  .collection('global')
@@ -418,7 +450,7 @@ async function getSignedInUsersToUpdate(dependencies, config) {
418
450
  }
419
451
  }
420
452
 
421
- return target; // Include this user
453
+ return target; // Include this user - needs update
422
454
  });
423
455
 
424
456
  const results = await Promise.all(checkPromises);
@@ -49,6 +49,51 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
49
49
  const batchPromises = [];
50
50
  for (const shardId in shardUpdates) { const docRef = db.collection(priceCollectionName).doc(shardId); const payload = shardUpdates[shardId]; batchPromises.push(docRef.update(payload)); }
51
51
  await Promise.all(batchPromises);
52
+
53
+ // Extract all dates from the price data and create a date tracking document
54
+ const priceDatesSet = new Set();
55
+ const AUGUST_2025 = '2025-08-01';
56
+
57
+ for (const instrumentData of results) {
58
+ const dailyData = instrumentData?.ClosingPrices?.Daily;
59
+ const weeklyData = instrumentData?.ClosingPrices?.Weekly;
60
+ const monthlyData = instrumentData?.ClosingPrices?.Monthly;
61
+
62
+ if (dailyData?.Date) {
63
+ const dateKey = dailyData.Date.substring(0, 10);
64
+ if (dateKey >= AUGUST_2025) {
65
+ priceDatesSet.add(dateKey);
66
+ }
67
+ }
68
+ if (weeklyData?.Date) {
69
+ const dateKey = weeklyData.Date.substring(0, 10);
70
+ if (dateKey >= AUGUST_2025) {
71
+ priceDatesSet.add(dateKey);
72
+ }
73
+ }
74
+ if (monthlyData?.Date) {
75
+ const dateKey = monthlyData.Date.substring(0, 10);
76
+ if (dateKey >= AUGUST_2025) {
77
+ priceDatesSet.add(dateKey);
78
+ }
79
+ }
80
+ }
81
+
82
+ // Write date tracking document
83
+ const today = new Date().toISOString().split('T')[0];
84
+ const dateTrackingRef = db.collection('pricedatastoreddates').doc(today);
85
+ const priceDatesArray = Array.from(priceDatesSet).sort();
86
+
87
+ await dateTrackingRef.set({
88
+ fetchDate: today,
89
+ datesAvailable: priceDatesArray,
90
+ dateCount: priceDatesArray.length,
91
+ instrumentsProcessed: results.length,
92
+ storedAt: FieldValue.serverTimestamp()
93
+ });
94
+
95
+ logger.log('INFO', `[PriceFetcherHelpers] Wrote price date tracking document for ${today} with ${priceDatesArray.length} dates (from August 2025 onwards)`);
96
+
52
97
  const successMessage = `Successfully processed and saved daily prices for ${results.length} instruments to ${batchPromises.length} shards.`;
53
98
  logger.log('SUCCESS', `[PriceFetcherHelpers] ${successMessage}`);
54
99
  return { success: true, message: successMessage, instrumentsProcessed: results.length };
@@ -75,35 +75,61 @@ exports.runRootDataIndexer = async (config, dependencies) => {
75
75
  const scanMode = targetDate ? 'SINGLE_DATE' : 'FULL_SCAN';
76
76
  logger.log('INFO', `[RootDataIndexer] Starting Root Data Availability Scan... Mode: ${scanMode}`, { targetDate });
77
77
 
78
- // 1. Price Availability (Global Scan Mode Only)
79
- // If running for a single targetDate, we skip the global shard sampling to save time.
78
+ // 1. Price Availability - Read from date tracking documents
79
+ // Find the latest pricedatastoreddates document and extract available dates
80
80
  const priceAvailabilitySet = new Set();
81
81
 
82
- if (!targetDate) {
83
- try {
84
- // Path: asset_prices/shard_*
85
- const priceCollectionRef = db.collection(PRICE_COLLECTION_NAME);
86
- const priceShardsSnapshot = await priceCollectionRef.limit(10).get();
82
+ try {
83
+ // Get the latest price date tracking document
84
+ const dateTrackingRef = db.collection('pricedatastoreddates')
85
+ .orderBy('fetchDate', 'desc')
86
+ .limit(1);
87
+
88
+ const dateTrackingSnapshot = await dateTrackingRef.get();
89
+
90
+ if (!dateTrackingSnapshot.empty) {
91
+ const latestTrackingDoc = dateTrackingSnapshot.docs[0].data();
92
+ const datesAvailable = latestTrackingDoc.datesAvailable || [];
93
+
94
+ // Add all dates from the tracking document
95
+ datesAvailable.forEach(dateKey => {
96
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
97
+ priceAvailabilitySet.add(dateKey);
98
+ }
99
+ });
87
100
 
88
- if (!priceShardsSnapshot.empty) {
89
- // Sample up to 10 shards and extract date keys from them
90
- for (const shardDoc of priceShardsSnapshot.docs) {
91
- if (shardDoc.id.startsWith('shard_')) {
92
- const data = shardDoc.data();
93
- Object.values(data).forEach(instrument => {
94
- if (instrument && instrument.prices) {
95
- Object.keys(instrument.prices).forEach(dateKey => {
96
- if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
97
- priceAvailabilitySet.add(dateKey);
98
- }
99
- });
100
- }
101
- });
101
+ logger.log('INFO', `[RootDataIndexer] Loaded ${priceAvailabilitySet.size} price dates from tracking document (fetchDate: ${latestTrackingDoc.fetchDate})`);
102
+ } else {
103
+ logger.log('WARN', '[RootDataIndexer] No price date tracking documents found. Falling back to empty set.');
104
+ }
105
+ } catch (e) {
106
+ logger.log('ERROR', '[RootDataIndexer] Failed to load price date tracking document.', { error: e.message });
107
+ // Fallback: try to sample shards if tracking document fails
108
+ if (!targetDate) {
109
+ try {
110
+ const priceCollectionRef = db.collection(PRICE_COLLECTION_NAME);
111
+ const priceShardsSnapshot = await priceCollectionRef.limit(10).get();
112
+
113
+ if (!priceShardsSnapshot.empty) {
114
+ for (const shardDoc of priceShardsSnapshot.docs) {
115
+ if (shardDoc.id.startsWith('shard_')) {
116
+ const data = shardDoc.data();
117
+ Object.values(data).forEach(instrument => {
118
+ if (instrument && instrument.prices) {
119
+ Object.keys(instrument.prices).forEach(dateKey => {
120
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
121
+ priceAvailabilitySet.add(dateKey);
122
+ }
123
+ });
124
+ }
125
+ });
126
+ }
102
127
  }
128
+ logger.log('INFO', `[RootDataIndexer] Fallback: Loaded ${priceAvailabilitySet.size} price dates from shard sampling.`);
103
129
  }
130
+ } catch (fallbackError) {
131
+ logger.log('ERROR', '[RootDataIndexer] Fallback shard sampling also failed.', { error: fallbackError.message });
104
132
  }
105
- } catch (e) {
106
- logger.log('ERROR', '[RootDataIndexer] Failed to sample price shards.', { error: e.message });
107
133
  }
108
134
  }
109
135
 
@@ -130,9 +156,9 @@ exports.runRootDataIndexer = async (config, dependencies) => {
130
156
  const promises = datesToScan.map(dateStr => limit(async () => {
131
157
  try {
132
158
  // Define Time Range for Social Query (Full Day UTC)
133
- const dayStart = new Date(dateStr);
134
- const dayEnd = new Date(dateStr);
135
- dayEnd.setHours(23, 59, 59, 999);
159
+ // Use UTC methods to ensure correct timezone handling
160
+ const dayStart = new Date(dateStr + 'T00:00:00.000Z');
161
+ const dayEnd = new Date(dateStr + 'T23:59:59.999Z');
136
162
 
137
163
  const availability = {
138
164
  date: dateStr,
@@ -211,10 +237,14 @@ exports.runRootDataIndexer = async (config, dependencies) => {
211
237
 
212
238
  // 4. Universal Social Check via Collection Group
213
239
  // Path: Collection Group 'posts' where 'fetchedAt' is within the day
240
+ // This queries ALL 'posts' subcollections across:
241
+ // - signed_in_users_social/{userId}/posts/{postId}
242
+ // - pi_social_posts/{userId}/posts/{postId}
243
+ // - daily_social_insights/{date}/posts/{postId}
214
244
  const universalSocialQuery = db.collectionGroup('posts')
215
245
  .where('fetchedAt', '>=', dayStart)
216
246
  .where('fetchedAt', '<=', dayEnd)
217
- .limit(50);
247
+ .limit(100); // Increased limit to ensure we catch all posts
218
248
 
219
249
  // --- Execute Checks ---
220
250
  const [
@@ -249,18 +279,64 @@ exports.runRootDataIndexer = async (config, dependencies) => {
249
279
  let foundPISocial = false;
250
280
  let foundSignedInSocial = false;
251
281
 
282
+ logger.log('INFO', `[RootDataIndexer/${dateStr}] Checking social data availability (dayStart: ${dayStart.toISOString()}, dayEnd: ${dayEnd.toISOString()})`);
283
+ logger.log('INFO', `[RootDataIndexer/${dateStr}] Looking for PI social in: "${PI_SOCIAL_COLL_NAME}", Signed-in social in: "${SIGNED_IN_SOCIAL_COLL_NAME}"`);
284
+
252
285
  if (!universalSocialSnap.empty) {
286
+ logger.log('INFO', `[RootDataIndexer/${dateStr}] Collection group query returned ${universalSocialSnap.docs.length} posts`);
287
+
288
+ // Track all paths found for debugging
289
+ const allPaths = [];
290
+ const piPaths = [];
291
+ const signedInPaths = [];
292
+ const otherPaths = [];
293
+
253
294
  universalSocialSnap.docs.forEach(doc => {
254
295
  const path = doc.ref.path;
255
- // Use includes() to match collection name anywhere in path (more robust)
296
+ const data = doc.data();
297
+ const fetchedAt = data.fetchedAt;
298
+ allPaths.push(path);
299
+
300
+ // Convert fetchedAt to string for logging
301
+ let fetchedAtStr = 'missing';
302
+ if (fetchedAt) {
303
+ if (fetchedAt.toDate) {
304
+ fetchedAtStr = fetchedAt.toDate().toISOString();
305
+ } else if (fetchedAt.toMillis) {
306
+ fetchedAtStr = new Date(fetchedAt.toMillis()).toISOString();
307
+ } else {
308
+ fetchedAtStr = String(fetchedAt);
309
+ }
310
+ }
311
+
312
+ // Use includes() to match collection name anywhere in path
256
313
  // Path format: {collectionName}/{userId}/posts/{postId}
257
- if (path.includes(`/${PI_SOCIAL_COLL_NAME}/`) || path.startsWith(`${PI_SOCIAL_COLL_NAME}/`)) {
314
+ const piMatchPattern = `${PI_SOCIAL_COLL_NAME}/`;
315
+ const signedInMatchPattern = `${SIGNED_IN_SOCIAL_COLL_NAME}/`;
316
+
317
+ if (path.includes(piMatchPattern)) {
258
318
  foundPISocial = true;
259
- }
260
- if (path.includes(`/${SIGNED_IN_SOCIAL_COLL_NAME}/`) || path.startsWith(`${SIGNED_IN_SOCIAL_COLL_NAME}/`)) {
319
+ piPaths.push({ path, fetchedAt: fetchedAtStr });
320
+ logger.log('INFO', `[RootDataIndexer/${dateStr}] PI social: ${path} (fetchedAt: ${fetchedAtStr})`);
321
+ } else if (path.includes(signedInMatchPattern)) {
261
322
  foundSignedInSocial = true;
323
+ signedInPaths.push({ path, fetchedAt: fetchedAtStr });
324
+ logger.log('INFO', `[RootDataIndexer/${dateStr}] ✓ Signed-in social: ${path} (fetchedAt: ${fetchedAtStr})`);
325
+ } else {
326
+ otherPaths.push({ path, fetchedAt: fetchedAtStr });
327
+ logger.log('INFO', `[RootDataIndexer/${dateStr}] ? Other social: ${path} (fetchedAt: ${fetchedAtStr})`);
262
328
  }
263
329
  });
330
+
331
+ logger.log('INFO', `[RootDataIndexer/${dateStr}] Summary - Total: ${allPaths.length}, PI: ${piPaths.length}, Signed-in: ${signedInPaths.length}, Other: ${otherPaths.length}`);
332
+
333
+ if (signedInPaths.length === 0 && !foundSignedInSocial) {
334
+ logger.log('WARN', `[RootDataIndexer/${dateStr}] ⚠️ No signed-in social posts found! Expected pattern: "${signedInMatchPattern}"`);
335
+ logger.log('WARN', `[RootDataIndexer/${dateStr}] All paths found: ${allPaths.slice(0, 10).join(', ')}${allPaths.length > 10 ? '...' : ''}`);
336
+ }
337
+ } else {
338
+ logger.log('WARN', `[RootDataIndexer/${dateStr}] ⚠️ Collection group query returned NO posts! (dayStart: ${dayStart.toISOString()}, dayEnd: ${dayEnd.toISOString()})`);
339
+ logger.log('WARN', `[RootDataIndexer/${dateStr}] This might indicate: 1) No posts with fetchedAt in this range, 2) Missing Firestore index, or 3) Query error`);
264
340
  }
265
341
 
266
342
  // --- Assign to Availability ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.468",
3
+ "version": "1.0.470",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [