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
|
|
401
|
-
// Check
|
|
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
|
|
79
|
-
//
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
134
|
-
const
|
|
135
|
-
dayEnd
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ---
|