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
|
|
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 || [];
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
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(
|
|
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 ---
|