bulltrackers-module 1.0.468 → 1.0.469

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 || [];
87
93
 
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
- });
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
+ });
100
+
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,
@@ -250,17 +276,33 @@ exports.runRootDataIndexer = async (config, dependencies) => {
250
276
  let foundSignedInSocial = false;
251
277
 
252
278
  if (!universalSocialSnap.empty) {
279
+ logger.log('DEBUG', `[RootDataIndexer/${dateStr}] Found ${universalSocialSnap.docs.length} social posts in query`);
253
280
  universalSocialSnap.docs.forEach(doc => {
254
281
  const path = doc.ref.path;
282
+ const data = doc.data();
283
+ const fetchedAt = data.fetchedAt;
284
+
255
285
  // Use includes() to match collection name anywhere in path (more robust)
256
286
  // Path format: {collectionName}/{userId}/posts/{postId}
257
- if (path.includes(`/${PI_SOCIAL_COLL_NAME}/`) || path.startsWith(`${PI_SOCIAL_COLL_NAME}/`)) {
287
+ // Firestore paths don't have leading slash, so check both with and without
288
+ const piMatchPattern = `${PI_SOCIAL_COLL_NAME}/`;
289
+ const signedInMatchPattern = `${SIGNED_IN_SOCIAL_COLL_NAME}/`;
290
+
291
+ if (path.includes(piMatchPattern) || path.startsWith(piMatchPattern)) {
258
292
  foundPISocial = true;
293
+ logger.log('DEBUG', `[RootDataIndexer/${dateStr}] ✓ Found PI social: ${path}`);
259
294
  }
260
- if (path.includes(`/${SIGNED_IN_SOCIAL_COLL_NAME}/`) || path.startsWith(`${SIGNED_IN_SOCIAL_COLL_NAME}/`)) {
295
+ if (path.includes(signedInMatchPattern) || path.startsWith(signedInMatchPattern)) {
261
296
  foundSignedInSocial = true;
297
+ const fetchedAtStr = fetchedAt ? (fetchedAt.toDate ? fetchedAt.toDate().toISOString() : String(fetchedAt)) : 'missing';
298
+ logger.log('DEBUG', `[RootDataIndexer/${dateStr}] ✓ Found signed-in social: ${path}, fetchedAt: ${fetchedAtStr}`);
299
+ } else if (!path.includes(piMatchPattern)) {
300
+ // Log paths that don't match either pattern to help debug
301
+ logger.log('DEBUG', `[RootDataIndexer/${dateStr}] ✗ Path doesn't match: ${path} (looking for: "${signedInMatchPattern}" or "${piMatchPattern}")`);
262
302
  }
263
303
  });
304
+ } else {
305
+ logger.log('DEBUG', `[RootDataIndexer/${dateStr}] No social posts found in query (dayStart: ${dayStart.toISOString()}, dayEnd: ${dayEnd.toISOString()})`);
264
306
  }
265
307
 
266
308
  // --- 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.469",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [