bulltrackers-module 1.0.497 → 1.0.499

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.
@@ -24,52 +24,107 @@ function tryDecompress(data) {
24
24
 
25
25
  /** Stage 1: Get portfolio part document references for a given date */
26
26
  async function getPortfolioPartRefs(config, deps, dateString) {
27
- const { db, logger, calculationUtils } = deps;
27
+ const { db, logger, calculationUtils, collectionRegistry } = deps;
28
28
  const { withRetry } = calculationUtils;
29
+ const { getCollectionPath } = collectionRegistry || {};
29
30
 
30
31
  logger.log('INFO', `Getting portfolio part references for date: ${dateString}`);
31
32
 
32
- // We scan ALL portfolio collections that use the standard 'snapshots/{date}/parts' structure
33
+ const allPartRefs = [];
34
+
35
+ // NEW STRUCTURE: Read from date-based collections (per-user documents)
36
+ // Structure: Collection/{date}/{cid}/{cid} where {date} is document, {cid} is subcollection, {cid} is document
37
+ try {
38
+ // Signed-In User Portfolios: SignedInUserPortfolioData/{date}/{cid}/{cid}
39
+ const signedInPortCollectionName = 'SignedInUserPortfolioData';
40
+ const signedInPortDateDoc = db.collection(signedInPortCollectionName).doc(dateString);
41
+ const signedInPortSubcollections = await withRetry(
42
+ () => signedInPortDateDoc.listCollections(),
43
+ `listSignedInPortfolios(${dateString})`
44
+ );
45
+
46
+ signedInPortSubcollections.forEach(subcol => {
47
+ // Each subcollection is a CID, the document ID is also the CID
48
+ const cid = subcol.id;
49
+ const cidDocRef = subcol.doc(cid);
50
+ allPartRefs.push({
51
+ ref: cidDocRef,
52
+ type: 'SIGNED_IN_USER',
53
+ cid: cid,
54
+ collectionType: 'NEW_STRUCTURE'
55
+ });
56
+ });
57
+
58
+ // Popular Investor Portfolios: PopularInvestorPortfolioData/{date}/{cid}/{cid}
59
+ const piPortCollectionName = 'PopularInvestorPortfolioData';
60
+ const piPortDateDoc = db.collection(piPortCollectionName).doc(dateString);
61
+ const piPortSubcollections = await withRetry(
62
+ () => piPortDateDoc.listCollections(),
63
+ `listPIPortfolios(${dateString})`
64
+ );
65
+
66
+ piPortSubcollections.forEach(subcol => {
67
+ const cid = subcol.id;
68
+ const cidDocRef = subcol.doc(cid);
69
+ allPartRefs.push({
70
+ ref: cidDocRef,
71
+ type: 'POPULAR_INVESTOR',
72
+ cid: cid,
73
+ collectionType: 'NEW_STRUCTURE'
74
+ });
75
+ });
76
+
77
+ logger.log('INFO', `Found ${allPartRefs.length} portfolio refs from new structure for ${dateString}`);
78
+ } catch (newStructError) {
79
+ logger.log('WARN', `Failed to load from new structure, falling back to legacy: ${newStructError.message}`);
80
+ }
81
+
82
+ // LEGACY STRUCTURE: Read from block-based collections (for backward compatibility)
33
83
  const collectionsToQuery = [
34
84
  config.normalUserPortfolioCollection,
35
85
  config.speculatorPortfolioCollection,
36
- config.piPortfolioCollection, // New: PI Overall
37
- config.signedInUsersCollection // New: Signed-In Users
86
+ config.piPortfolioCollection, // Legacy: PI Overall
87
+ config.signedInUsersCollection // Legacy: Signed-In Users
38
88
  ].filter(Boolean);
39
89
 
40
- const allPartRefs = [];
41
-
42
90
  for (const collectionName of collectionsToQuery) {
43
- // Assume standard structure: Collection -> Block(e.g. 19M) -> snapshots -> date -> parts
44
- // List documents to find the Blocks
45
- const blockDocsQuery = db.collection(collectionName);
46
- const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
47
-
48
- if (!blockDocRefs.length) {
49
- continue;
50
- }
91
+ try {
92
+ // Assume standard structure: Collection -> Block(e.g. 19M) -> snapshots -> date -> parts
93
+ const blockDocsQuery = db.collection(collectionName);
94
+ const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
95
+
96
+ if (!blockDocRefs.length) {
97
+ continue;
98
+ }
51
99
 
52
- const partsPromises = blockDocRefs.map(blockDocRef => {
53
- const partsCollectionRef = blockDocRef
54
- .collection(config.snapshotsSubcollection || 'snapshots')
55
- .doc(dateString)
56
- .collection(config.partsSubcollection || 'parts');
57
- return withRetry(() => partsCollectionRef.listDocuments(), `listParts(${partsCollectionRef.path})`);
58
- });
100
+ const partsPromises = blockDocRefs.map(blockDocRef => {
101
+ const partsCollectionRef = blockDocRef
102
+ .collection(config.snapshotsSubcollection || 'snapshots')
103
+ .doc(dateString)
104
+ .collection(config.partsSubcollection || 'parts');
105
+ return withRetry(() => partsCollectionRef.listDocuments(), `listParts(${partsCollectionRef.path})`);
106
+ });
59
107
 
60
- const partDocArrays = await Promise.all(partsPromises);
61
-
62
- partDocArrays.forEach(partDocs => {
63
- // Tag them so loadDataByRefs knows how to handle them (especially PI Deep Fetch)
64
- let type = 'PART';
65
- if (collectionName === config.piPortfolioCollection) type = 'POPULAR_INVESTOR';
66
- if (collectionName === config.signedInUsersCollection) type = 'SIGNED_IN_USER';
108
+ const partDocArrays = await Promise.all(partsPromises);
67
109
 
68
- allPartRefs.push(...partDocs.map(ref => ({ ref, type })));
69
- });
110
+ partDocArrays.forEach(partDocs => {
111
+ // Tag them so loadDataByRefs knows how to handle them
112
+ let type = 'PART';
113
+ if (collectionName === config.piPortfolioCollection) type = 'POPULAR_INVESTOR';
114
+ if (collectionName === config.signedInUsersCollection) type = 'SIGNED_IN_USER';
115
+
116
+ allPartRefs.push(...partDocs.map(ref => ({
117
+ ref,
118
+ type,
119
+ collectionType: 'LEGACY'
120
+ })));
121
+ });
122
+ } catch (legacyError) {
123
+ logger.log('WARN', `Failed to load legacy collection ${collectionName}: ${legacyError.message}`);
124
+ }
70
125
  }
71
126
 
72
- logger.log('INFO', `Found ${allPartRefs.length} portfolio part refs for ${dateString}`);
127
+ logger.log('INFO', `Found ${allPartRefs.length} total portfolio refs for ${dateString}`);
73
128
  return allPartRefs;
74
129
  }
75
130
 
@@ -101,48 +156,73 @@ async function loadDataByRefs(config, deps, refObjects) {
101
156
  if (!doc.exists) continue;
102
157
 
103
158
  const rawData = doc.data();
104
- const chunkData = tryDecompress(rawData); // Map: { userId: data }
105
-
106
- if (meta.type === 'POPULAR_INVESTOR' && config.piDeepPortfolioCollection) {
107
- // Construct Deep Path
108
- // Current: pi_portfolios_overall/19M/snapshots/{date}/parts/{part_X}
109
- // Target: pi_portfolios_deep/19M/snapshots/{date}/parts/{part_X}
159
+ let chunkData;
160
+
161
+ // NEW STRUCTURE: Single user document per CID
162
+ if (meta.collectionType === 'NEW_STRUCTURE') {
163
+ const cid = meta.cid || doc.id;
164
+ // Data is stored directly in the document, not as a map
165
+ const userData = tryDecompress(rawData);
166
+ // Convert to map format: { cid: data }
167
+ chunkData = { [cid]: userData };
110
168
 
111
- const pathSegments = doc.ref.path.split('/'); // [col, block, snap, date, parts, partId]
112
- // Replace collection name with deep collection name
113
- const deepCollection = config.piDeepPortfolioCollection;
114
- const deepPath = `${deepCollection}/${pathSegments[1]}/${pathSegments[2]}/${pathSegments[3]}/${pathSegments[4]}/${pathSegments[5]}`;
169
+ // Tag user type
170
+ if (meta.type === 'POPULAR_INVESTOR') {
171
+ chunkData[cid]._userType = 'POPULAR_INVESTOR';
172
+ // Check for deep positions in the same document
173
+ if (chunkData[cid].deepPositions) {
174
+ chunkData[cid].DeepPositions = chunkData[cid].deepPositions;
175
+ }
176
+ } else if (meta.type === 'SIGNED_IN_USER') {
177
+ chunkData[cid]._userType = 'SIGNED_IN_USER';
178
+ }
115
179
 
116
- // Fetch deeply
117
- deepFetchPromises.push(
118
- db.doc(deepPath).get().then(deepSnap => {
119
- if (deepSnap.exists) {
120
- const deepChunk = tryDecompress(deepSnap.data());
121
- // Merge deep positions into overall data
122
- for (const [uid, pData] of Object.entries(chunkData)) {
123
- if (deepChunk[uid] && deepChunk[uid].positions) {
124
- pData.DeepPositions = deepChunk[uid].positions;
180
+ deepFetchPromises.push(Promise.resolve(chunkData));
181
+ } else {
182
+ // LEGACY STRUCTURE: Sharded parts with multiple users per document
183
+ chunkData = tryDecompress(rawData); // Map: { userId: data }
184
+
185
+ if (meta.type === 'POPULAR_INVESTOR' && config.piDeepPortfolioCollection) {
186
+ // Construct Deep Path
187
+ // Current: pi_portfolios_overall/19M/snapshots/{date}/parts/{part_X}
188
+ // Target: pi_portfolios_deep/19M/snapshots/{date}/parts/{part_X}
189
+
190
+ const pathSegments = doc.ref.path.split('/'); // [col, block, snap, date, parts, partId]
191
+ // Replace collection name with deep collection name
192
+ const deepCollection = config.piDeepPortfolioCollection;
193
+ const deepPath = `${deepCollection}/${pathSegments[1]}/${pathSegments[2]}/${pathSegments[3]}/${pathSegments[4]}/${pathSegments[5]}`;
194
+
195
+ // Fetch deeply
196
+ deepFetchPromises.push(
197
+ db.doc(deepPath).get().then(deepSnap => {
198
+ if (deepSnap.exists) {
199
+ const deepChunk = tryDecompress(deepSnap.data());
200
+ // Merge deep positions into overall data
201
+ for (const [uid, pData] of Object.entries(chunkData)) {
202
+ if (deepChunk[uid] && deepChunk[uid].positions) {
203
+ pData.DeepPositions = deepChunk[uid].positions;
204
+ }
125
205
  }
126
206
  }
127
- }
128
- // Tag internal type for ContextFactory
129
- for (const pData of Object.values(chunkData)) {
130
- pData._userType = 'POPULAR_INVESTOR';
131
- }
132
- return chunkData;
133
- }).catch(err => {
134
- // If deep fetch fails, return chunkData as is (graceful degradation)
135
- return chunkData;
136
- })
137
- );
138
- } else if (meta.type === 'SIGNED_IN_USER') {
139
- for (const pData of Object.values(chunkData)) {
140
- pData._userType = 'SIGNED_IN_USER';
207
+ // Tag internal type for ContextFactory
208
+ for (const pData of Object.values(chunkData)) {
209
+ pData._userType = 'POPULAR_INVESTOR';
210
+ }
211
+ return chunkData;
212
+ }).catch(err => {
213
+ // If deep fetch fails, return chunkData as is (graceful degradation)
214
+ return chunkData;
215
+ })
216
+ );
217
+ } else if (meta.type === 'SIGNED_IN_USER') {
218
+ for (const pData of Object.values(chunkData)) {
219
+ pData._userType = 'SIGNED_IN_USER';
220
+ }
221
+ deepFetchPromises.push(Promise.resolve(chunkData));
222
+ } else {
223
+ // Standard Part
224
+ deepFetchPromises.push(Promise.resolve(chunkData));
141
225
  }
142
- deepFetchPromises.push(Promise.resolve(chunkData));
143
- } else {
144
- // Standard Part
145
- deepFetchPromises.push(Promise.resolve(chunkData));
146
226
  }
147
227
  }
148
228
 
@@ -187,8 +267,9 @@ async function loadDailyInsights(config, deps, dateString) {
187
267
 
188
268
  /** Stage 5: Load and Partition Social Data */
189
269
  async function loadDailySocialPostInsights(config, deps, dateString) {
190
- const { db, logger, calculationUtils } = deps;
270
+ const { db, logger, calculationUtils, collectionRegistry } = deps;
191
271
  const { withRetry } = calculationUtils;
272
+ const { getCollectionPath } = collectionRegistry || {};
192
273
 
193
274
  logger.log('INFO', `Loading and partitioning social data for ${dateString}`);
194
275
 
@@ -199,6 +280,70 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
199
280
  signedIn: {} // Map<UserId, Map<PostId, Data>> - For Signed-In Users
200
281
  };
201
282
 
283
+ // NEW STRUCTURE: Read from date-based collections
284
+ // Structure: Collection/{date}/{cid}/{cid} for user social, Collection/{date}/posts/{postId} for instrument
285
+ try {
286
+ // Signed-In User Social: SignedInUserSocialPostData/{date}/{cid}/{cid}
287
+ const signedInSocialCollectionName = 'SignedInUserSocialPostData';
288
+ const signedInSocialDateDoc = db.collection(signedInSocialCollectionName).doc(dateString);
289
+ const signedInSocialSubcollections = await withRetry(
290
+ () => signedInSocialDateDoc.listCollections(),
291
+ `listSignedInSocial(${dateString})`
292
+ );
293
+
294
+ for (const subcol of signedInSocialSubcollections) {
295
+ const cid = subcol.id;
296
+ const cidDoc = await subcol.doc(cid).get();
297
+ if (cidDoc.exists) {
298
+ const cidData = tryDecompress(cidDoc.data());
299
+ if (cidData.posts && typeof cidData.posts === 'object') {
300
+ if (!result.signedIn[cid]) result.signedIn[cid] = {};
301
+ // Posts are stored as a map in the document
302
+ Object.assign(result.signedIn[cid], cidData.posts);
303
+ }
304
+ }
305
+ }
306
+
307
+ // Popular Investor Social: PopularInvestorSocialPostData/{date}/{cid}/{cid}
308
+ const piSocialCollectionName = 'PopularInvestorSocialPostData';
309
+ const piSocialDateDoc = db.collection(piSocialCollectionName).doc(dateString);
310
+ const piSocialSubcollections = await withRetry(
311
+ () => piSocialDateDoc.listCollections(),
312
+ `listPISocial(${dateString})`
313
+ );
314
+
315
+ for (const subcol of piSocialSubcollections) {
316
+ const cid = subcol.id;
317
+ const cidDoc = await subcol.doc(cid).get();
318
+ if (cidDoc.exists) {
319
+ const cidData = tryDecompress(cidDoc.data());
320
+ if (cidData.posts && typeof cidData.posts === 'object') {
321
+ if (!result.pi[cid]) result.pi[cid] = {};
322
+ Object.assign(result.pi[cid], cidData.posts);
323
+ }
324
+ }
325
+ }
326
+
327
+ // Instrument Social: InstrumentFeedSocialPostData/{date}/posts/{postId}
328
+ const instrumentSocialCollectionName = 'InstrumentFeedSocialPostData';
329
+ const instrumentSocialDateDoc = db.collection(instrumentSocialCollectionName).doc(dateString);
330
+ const instrumentSocialPostsCol = instrumentSocialDateDoc.collection('posts');
331
+ const instrumentSocialSnapshot = await withRetry(
332
+ () => instrumentSocialPostsCol.limit(1000).get(),
333
+ `getInstrumentSocial(${dateString})`
334
+ );
335
+
336
+ instrumentSocialSnapshot.forEach(doc => {
337
+ const data = tryDecompress(doc.data());
338
+ result.generic[doc.id] = data;
339
+ });
340
+
341
+ logger.log('INFO', `Loaded Social Data (NEW): ${Object.keys(result.generic).length} Generic, ${Object.keys(result.pi).length} PIs, ${Object.keys(result.signedIn).length} SignedIn.`);
342
+ } catch (newStructError) {
343
+ logger.log('WARN', `Failed to load from new structure, falling back to legacy: ${newStructError.message}`);
344
+ }
345
+
346
+ // LEGACY STRUCTURE: CollectionGroup query (for backward compatibility)
202
347
  const PI_COL_NAME = config.piSocialCollectionName || config.piSocialCollection || 'pi_social_posts';
203
348
  const SIGNED_IN_COL_NAME = config.signedInUserSocialCollection || 'signed_in_users_social';
204
349
 
@@ -246,7 +391,7 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
246
391
  result.generic[doc.id] = data;
247
392
  }
248
393
  });
249
- logger.log('INFO', `Loaded Social Data: ${Object.keys(result.generic).length} Generic, ${Object.keys(result.pi).length} PIs, ${Object.keys(result.signedIn).length} SignedIn.`);
394
+ logger.log('INFO', `Loaded Social Data (LEGACY): ${Object.keys(result.generic).length} Generic, ${Object.keys(result.pi).length} PIs, ${Object.keys(result.signedIn).length} SignedIn.`);
250
395
  } else {
251
396
  logger.log('WARN', `No social posts found for ${dateString} via CollectionGroup.`);
252
397
  }
@@ -260,39 +405,96 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
260
405
 
261
406
  /** Stage 6: Get history part references for a given date */
262
407
  async function getHistoryPartRefs(config, deps, dateString) {
263
- const { db, logger, calculationUtils } = deps;
408
+ const { db, logger, calculationUtils, collectionRegistry } = deps;
264
409
  const { withRetry } = calculationUtils;
410
+ const { getCollectionPath } = collectionRegistry || {};
411
+
265
412
  logger.log('INFO', `Getting history part references for ${dateString}`);
266
413
 
267
- // Include PI History
414
+ const allPartRefs = [];
415
+
416
+ // NEW STRUCTURE: Read from date-based collections
417
+ // Structure: Collection/{date}/{cid}/{cid}
418
+ try {
419
+ // Signed-In User History: SignedInUserTradeHistoryData/{date}/{cid}/{cid}
420
+ const signedInHistCollectionName = 'SignedInUserTradeHistoryData';
421
+ const signedInHistDateDoc = db.collection(signedInHistCollectionName).doc(dateString);
422
+ const signedInHistSubcollections = await withRetry(
423
+ () => signedInHistDateDoc.listCollections(),
424
+ `listSignedInHistory(${dateString})`
425
+ );
426
+
427
+ signedInHistSubcollections.forEach(subcol => {
428
+ const cid = subcol.id;
429
+ const cidDocRef = subcol.doc(cid);
430
+ allPartRefs.push({
431
+ ref: cidDocRef,
432
+ type: 'SIGNED_IN_USER',
433
+ cid: cid,
434
+ collectionType: 'NEW_STRUCTURE'
435
+ });
436
+ });
437
+
438
+ // Popular Investor History: PopularInvestorTradeHistoryData/{date}/{cid}/{cid}
439
+ const piHistCollectionName = 'PopularInvestorTradeHistoryData';
440
+ const piHistDateDoc = db.collection(piHistCollectionName).doc(dateString);
441
+ const piHistSubcollections = await withRetry(
442
+ () => piHistDateDoc.listCollections(),
443
+ `listPIHistory(${dateString})`
444
+ );
445
+
446
+ piHistSubcollections.forEach(subcol => {
447
+ const cid = subcol.id;
448
+ const cidDocRef = subcol.doc(cid);
449
+ allPartRefs.push({
450
+ ref: cidDocRef,
451
+ type: 'POPULAR_INVESTOR',
452
+ cid: cid,
453
+ collectionType: 'NEW_STRUCTURE'
454
+ });
455
+ });
456
+
457
+ logger.log('INFO', `Found ${allPartRefs.length} history refs from new structure for ${dateString}`);
458
+ } catch (newStructError) {
459
+ logger.log('WARN', `Failed to load from new structure, falling back to legacy: ${newStructError.message}`);
460
+ }
461
+
462
+ // LEGACY STRUCTURE: Read from block-based collections
268
463
  const collectionsToQuery = [
269
464
  config.normalUserHistoryCollection,
270
465
  config.speculatorHistoryCollection,
271
466
  config.piHistoryCollection,
272
- config.signedInHistoryCollection // if distinct
467
+ config.signedInHistoryCollection
273
468
  ].filter(Boolean);
274
469
 
275
- const allPartRefs = [];
276
-
277
470
  for (const collectionName of collectionsToQuery) {
278
- const blockDocsQuery = db.collection(collectionName);
279
- const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
280
-
281
- if (!blockDocRefs.length) { continue; }
282
-
283
- const partsPromises = blockDocRefs.map(blockDocRef => {
284
- const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection || 'snapshots')
285
- .doc(dateString).collection(config.partsSubcollection || 'parts');
286
- return withRetry(() => partsCollectionRef.listDocuments(), `listParts(${partsCollectionRef.path})`);
287
- });
288
-
289
- const partDocArrays = await Promise.all(partsPromises);
290
- partDocArrays.forEach(partDocs => {
291
- // History parts are standard, no deep merge needed usually
292
- allPartRefs.push(...partDocs.map(ref => ({ ref, type: 'PART' })));
293
- });
471
+ try {
472
+ const blockDocsQuery = db.collection(collectionName);
473
+ const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
474
+
475
+ if (!blockDocRefs.length) { continue; }
476
+
477
+ const partsPromises = blockDocRefs.map(blockDocRef => {
478
+ const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection || 'snapshots')
479
+ .doc(dateString).collection(config.partsSubcollection || 'parts');
480
+ return withRetry(() => partsCollectionRef.listDocuments(), `listParts(${partsCollectionRef.path})`);
481
+ });
482
+
483
+ const partDocArrays = await Promise.all(partsPromises);
484
+ partDocArrays.forEach(partDocs => {
485
+ // History parts are standard, no deep merge needed usually
486
+ allPartRefs.push(...partDocs.map(ref => ({
487
+ ref,
488
+ type: 'PART',
489
+ collectionType: 'LEGACY'
490
+ })));
491
+ });
492
+ } catch (legacyError) {
493
+ logger.log('WARN', `Failed to load legacy history collection ${collectionName}: ${legacyError.message}`);
494
+ }
294
495
  }
295
- logger.log('INFO', `Found ${allPartRefs.length} history part refs for ${dateString}`);
496
+
497
+ logger.log('INFO', `Found ${allPartRefs.length} total history refs for ${dateString}`);
296
498
  return allPartRefs;
297
499
  }
298
500
 
@@ -16,11 +16,29 @@ const SHARD_SIZE = 40;
16
16
  * @returns {Promise<{success: boolean, message: string, instrumentsProcessed?: number}>}
17
17
  */
18
18
  exports.fetchAndStorePrices = async (config, dependencies) => {
19
- const { db, logger, headerManager, proxyManager } = dependencies;
19
+ const { db, logger, headerManager, proxyManager, collectionRegistry } = dependencies;
20
20
  logger.log('INFO', '[PriceFetcherHelpers] Starting Daily Closing Price Update...');
21
21
  let selectedHeader = null;
22
22
  let wasSuccessful = false;
23
- const priceCollectionName = 'asset_prices';
23
+
24
+ // Get collection names from registry if available, fallback to hardcoded
25
+ const { getCollectionPath } = collectionRegistry || {};
26
+ let priceCollectionName = 'asset_prices';
27
+ let priceTrackingCollectionName = 'pricedatastoreddates';
28
+
29
+ if (getCollectionPath) {
30
+ try {
31
+ // Extract collection name from registry path: asset_prices/shard_{shardId}
32
+ const basePath = getCollectionPath('rootData', 'assetPrices', { shardId: '0' });
33
+ priceCollectionName = basePath.split('/')[0];
34
+
35
+ // Extract price tracking collection name
36
+ const trackingPath = getCollectionPath('rootData', 'priceTracking', { fetchDate: '2025-01-01' });
37
+ priceTrackingCollectionName = trackingPath.split('/')[0];
38
+ } catch (e) {
39
+ logger.log('WARN', `[PriceFetcherHelpers] Failed to get collections from registry, using defaults: ${e.message}`);
40
+ }
41
+ }
24
42
  try { if (!config.etoroApiUrl) { throw new Error("Missing required configuration: etoroApiUrl."); }
25
43
  selectedHeader = await headerManager.selectHeader();
26
44
  if (!selectedHeader || !selectedHeader.header) { throw new Error("Could not select a valid header for the request."); }
@@ -81,7 +99,7 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
81
99
 
82
100
  // Write date tracking document
83
101
  const today = new Date().toISOString().split('T')[0];
84
- const dateTrackingRef = db.collection('pricedatastoreddates').doc(today);
102
+ const dateTrackingRef = db.collection(priceTrackingCollectionName).doc(today);
85
103
  const priceDatesArray = Array.from(priceDatesSet).sort();
86
104
 
87
105
  await dateTrackingRef.set({
@@ -13,12 +13,27 @@ const zlib = require('zlib'); // [NEW] Required for compression
13
13
  * @returns {Promise<{success: boolean, message: string, instrumentCount?: number}>}
14
14
  */
15
15
  exports.fetchAndStoreInsights = async (config, dependencies) => {
16
- const { db, logger, headerManager, proxyManager } = dependencies;
16
+ const { db, logger, headerManager, proxyManager, collectionRegistry } = dependencies;
17
17
  logger.log('INFO', '[FetchInsightsHelpers] Starting eToro insights data fetch...');
18
18
  let selectedHeader = null; let wasSuccessful = false;
19
19
 
20
+ // Get collection name from registry if available, fallback to config
21
+ const { getCollectionPath } = collectionRegistry || {};
22
+ let insightsCollectionName = config.insightsCollectionName;
23
+
24
+ if (getCollectionPath) {
25
+ try {
26
+ // Extract collection name from registry path: daily_instrument_insights/{date}
27
+ const basePath = getCollectionPath('rootData', 'instrumentInsights', { date: '2025-01-01' });
28
+ // Path is like "daily_instrument_insights/2025-01-01", extract collection name
29
+ insightsCollectionName = basePath.split('/')[0];
30
+ } catch (e) {
31
+ logger.log('WARN', `[FetchInsightsHelpers] Failed to get collection from registry, using config: ${e.message}`);
32
+ }
33
+ }
34
+
20
35
  try {
21
- if (!config.etoroInsightsUrl || !config.insightsCollectionName) {
36
+ if (!config.etoroInsightsUrl || !insightsCollectionName) {
22
37
  throw new Error("Missing required configuration: etoroInsightsUrl or insightsCollectionName.");
23
38
  }
24
39
 
@@ -71,7 +86,7 @@ exports.fetchAndStoreInsights = async (config, dependencies) => {
71
86
  }
72
87
 
73
88
  const today = new Date().toISOString().slice(0, 10);
74
- const docRef = db.collection(config.insightsCollectionName).doc(today);
89
+ const docRef = db.collection(insightsCollectionName).doc(today);
75
90
 
76
91
  // [FIX] --- COMPRESSION LOGIC START ---
77
92
 
@@ -130,7 +145,7 @@ exports.fetchAndStoreInsights = async (config, dependencies) => {
130
145
  ...config.rootDataIndexer,
131
146
  collections: {
132
147
  ...config.rootDataIndexer.collections,
133
- insights: config.insightsCollectionName // Override with actual collection name used
148
+ insights: insightsCollectionName // Override with actual collection name used
134
149
  },
135
150
  targetDate: today // Index only today's date for speed
136
151
  };
@@ -16,10 +16,25 @@ const { IntelligentHeaderManager } = require('../../core/utils/intelligent_heade
16
16
  * @param {object} config.headerConfig - Configuration for the IntelligentHeaderManager.
17
17
  */
18
18
  async function fetchAndStorePopularInvestors(config, dependencies) {
19
- const { db, logger } = dependencies;
19
+ const { db, logger, collectionRegistry } = dependencies;
20
20
  const { rankingsApiUrl, rankingsCollectionName, proxyConfig, headerConfig } = config;
21
21
 
22
- if (!rankingsApiUrl || !rankingsCollectionName || !proxyConfig || !headerConfig) {
22
+ // Get collection name from registry if available, fallback to config
23
+ const { getCollectionPath } = collectionRegistry || {};
24
+ let finalRankingsCollectionName = rankingsCollectionName;
25
+
26
+ if (getCollectionPath) {
27
+ try {
28
+ // Extract collection name from registry path: popular_investor_rankings/{date}
29
+ const basePath = getCollectionPath('rootData', 'popularInvestorRankings', { date: '2025-01-01' });
30
+ // Path is like "popular_investor_rankings/2025-01-01", extract collection name
31
+ finalRankingsCollectionName = basePath.split('/')[0];
32
+ } catch (e) {
33
+ logger.log('WARN', `[PopularInvestorFetch] Failed to get collection from registry, using config: ${e.message}`);
34
+ }
35
+ }
36
+
37
+ if (!rankingsApiUrl || !finalRankingsCollectionName || !proxyConfig || !headerConfig) {
23
38
  throw new Error("[PopularInvestorFetch] Missing required config (rankingsApiUrl, rankingsCollectionName, proxyConfig, headerConfig).");
24
39
  }
25
40
 
@@ -103,7 +118,7 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
103
118
  // 6. Final Validation & Storage
104
119
  if (data && data.Items && Array.isArray(data.Items)) {
105
120
  try {
106
- const docRef = db.collection(rankingsCollectionName).doc(today);
121
+ const docRef = db.collection(finalRankingsCollectionName).doc(today);
107
122
 
108
123
  await docRef.set({
109
124
  fetchedAt: new Date(),
@@ -112,7 +127,7 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
112
127
  ...data
113
128
  });
114
129
 
115
- logger.log('SUCCESS', `[PopularInvestorFetch] Stored ${data.TotalRows} rankings into ${rankingsCollectionName}/${today}`);
130
+ logger.log('SUCCESS', `[PopularInvestorFetch] Stored ${data.TotalRows} rankings into ${finalRankingsCollectionName}/${today}`);
116
131
 
117
132
  // Update root data indexer for today's date after rankings data is stored
118
133
  try {
@@ -132,7 +147,7 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
132
147
  ...rootDataIndexerConfig,
133
148
  collections: {
134
149
  ...rootDataIndexerConfig.collections,
135
- piRankings: rankingsCollectionName // Override with actual collection name used
150
+ piRankings: finalRankingsCollectionName // Override with actual collection name used
136
151
  },
137
152
  targetDate: today // Index only today's date for speed
138
153
  };