bulltrackers-module 1.0.592 → 1.0.593

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.
Files changed (36) hide show
  1. package/functions/old-generic-api/admin-api/index.js +895 -0
  2. package/functions/old-generic-api/helpers/api_helpers.js +457 -0
  3. package/functions/old-generic-api/index.js +204 -0
  4. package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +355 -0
  5. package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
  6. package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +212 -0
  7. package/functions/old-generic-api/user-api/helpers/collection_helpers.js +193 -0
  8. package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +68 -0
  9. package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +256 -0
  10. package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +640 -0
  11. package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +195 -0
  12. package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +503 -0
  13. package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
  14. package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +245 -0
  15. package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +174 -0
  16. package/functions/old-generic-api/user-api/helpers/data_helpers.js +87 -0
  17. package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +336 -0
  18. package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +615 -0
  19. package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +231 -0
  20. package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +641 -0
  21. package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +182 -0
  22. package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +137 -0
  23. package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +190 -0
  24. package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +66 -0
  25. package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +550 -0
  26. package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +378 -0
  27. package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +295 -0
  28. package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +162 -0
  29. package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +677 -0
  30. package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +323 -0
  31. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +96 -0
  32. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +141 -0
  33. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +310 -0
  34. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +829 -0
  35. package/functions/old-generic-api/user-api/index.js +109 -0
  36. package/package.json +2 -2
@@ -0,0 +1,503 @@
1
+ /**
2
+ * @fileoverview Computation Data Helpers
3
+ * Handles computation results endpoints with migration support
4
+ */
5
+
6
+ const { findLatestComputationDate } = require('../core/data_lookup_helpers');
7
+ const { tryDecompress } = require('../core/compression_helpers');
8
+ const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
9
+
10
+ /**
11
+ * Check if a PI exists in a computation date
12
+ * For PopularInvestorProfileMetrics and SignedInUserProfileMetrics: reads from pages subcollection
13
+ * For other computations: reads from main computation document
14
+ * Returns { found: boolean, profileData: object | null, computationData: object | null }
15
+ * @param {object} db - Firestore instance
16
+ * @param {string} insightsCollection - Insights collection name
17
+ * @param {string} resultsSub - Results subcollection name
18
+ * @param {string} compsSub - Computations subcollection name
19
+ * @param {string} category - Category name
20
+ * @param {string} computationName - Computation name
21
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
22
+ * @param {string} cidStr - CID as string
23
+ * @param {object} logger - Logger instance
24
+ * @returns {Promise<{found: boolean, profileData: object|null, computationData: object|null}>}
25
+ */
26
+ async function checkPiInComputationDate(db, insightsCollection, resultsSub, compsSub, category, computationName, dateStr, cidStr, logger) {
27
+ try {
28
+ // Check if this computation uses the pages subcollection structure
29
+ const usesPagesStructure = computationName === 'PopularInvestorProfileMetrics' ||
30
+ computationName === 'SignedInUserProfileMetrics' ||
31
+ computationName === 'SignedInUserPIPersonalizedMetrics';
32
+
33
+ if (usesPagesStructure) {
34
+ // New path format: /unified_insights/YYYY-MM-DD/results/popular-investor/computations/{computationName}/pages/{cid}
35
+ const pageRef = db.collection(insightsCollection)
36
+ .doc(dateStr)
37
+ .collection(resultsSub)
38
+ .doc(category)
39
+ .collection(compsSub)
40
+ .doc(computationName)
41
+ .collection('pages')
42
+ .doc(cidStr);
43
+
44
+ const pageDoc = await pageRef.get();
45
+
46
+ if (!pageDoc.exists) {
47
+ logger.log('INFO', `[checkPiInComputationDate] Page document not found for CID ${cidStr} in date ${dateStr}`);
48
+ return { found: false, profileData: null, computationData: null };
49
+ }
50
+
51
+ const rawData = pageDoc.data();
52
+ let profileData = null;
53
+
54
+ // Decompress if needed
55
+ profileData = tryDecompress(rawData);
56
+
57
+ // Handle string decompression result
58
+ if (typeof profileData === 'string') {
59
+ try {
60
+ profileData = JSON.parse(profileData);
61
+ } catch (e) {
62
+ logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for CID ${cidStr} on date ${dateStr}:`, e.message);
63
+ return { found: false, profileData: null, computationData: null };
64
+ }
65
+ }
66
+
67
+ if (profileData && typeof profileData === 'object') {
68
+ logger.log('INFO', `[checkPiInComputationDate] Found profile data for CID ${cidStr} in date ${dateStr}`);
69
+ // Return the profile data - computationData is set to null since we're reading individual pages
70
+ return { found: true, profileData, computationData: null };
71
+ } else {
72
+ logger.log('WARN', `[checkPiInComputationDate] Profile data is not an object for CID ${cidStr} on date ${dateStr}`);
73
+ return { found: false, profileData: null, computationData: null };
74
+ }
75
+ } else {
76
+ // Standard path for other computations: read from main computation document
77
+ const computationRef = db.collection(insightsCollection)
78
+ .doc(dateStr)
79
+ .collection(resultsSub)
80
+ .doc(category)
81
+ .collection(compsSub)
82
+ .doc(computationName);
83
+
84
+ const computationDoc = await computationRef.get();
85
+
86
+ if (!computationDoc.exists) {
87
+ return { found: false, profileData: null, computationData: null };
88
+ }
89
+
90
+ const rawData = computationDoc.data();
91
+ let computationData = null;
92
+
93
+ // Check if data is sharded
94
+ if (rawData._sharded === true && rawData._shardCount) {
95
+ const shardsCol = computationRef.collection('_shards');
96
+ const shardCount = rawData._shardCount;
97
+
98
+ logger.log('INFO', `[checkPiInComputationDate] Reading ${shardCount} shards for date ${dateStr}`);
99
+
100
+ computationData = {};
101
+
102
+ // Read all shards (shard_0, shard_1, ..., shard_N-1)
103
+ for (let i = 0; i < shardCount; i++) {
104
+ const shardDoc = await shardsCol.doc(`shard_${i}`).get();
105
+ if (shardDoc.exists) {
106
+ const shardData = shardDoc.data();
107
+ Object.assign(computationData, shardData);
108
+ } else {
109
+ logger.log('WARN', `[checkPiInComputationDate] Shard shard_${i} missing for date ${dateStr}`);
110
+ }
111
+ }
112
+ } else {
113
+ // Data is in the main document (compressed or raw)
114
+ computationData = tryDecompress(rawData);
115
+
116
+ // Handle string decompression result
117
+ if (typeof computationData === 'string') {
118
+ try {
119
+ computationData = JSON.parse(computationData);
120
+ } catch (e) {
121
+ logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for date ${dateStr}:`, e.message);
122
+ return { found: false, profileData: null, computationData: null };
123
+ }
124
+ }
125
+ }
126
+
127
+ // Check if CID exists in the computation data
128
+ if (computationData && typeof computationData === 'object' && !Array.isArray(computationData)) {
129
+ // Filter out metadata keys that start with underscore
130
+ const cids = Object.keys(computationData).filter(key => !key.startsWith('_'));
131
+
132
+ // Check if the requested CID exists
133
+ const profileData = computationData[cidStr];
134
+ if (profileData) {
135
+ logger.log('INFO', `[checkPiInComputationDate] Found CID ${cidStr} in date ${dateStr} (total CIDs: ${cids.length})`);
136
+ return { found: true, profileData, computationData };
137
+ } else {
138
+ logger.log('INFO', `[checkPiInComputationDate] CID ${cidStr} not found in date ${dateStr} (total CIDs: ${cids.length})`);
139
+ return { found: false, profileData: null, computationData };
140
+ }
141
+ } else {
142
+ logger.log('WARN', `[checkPiInComputationDate] Computation data is not an object for date ${dateStr}`);
143
+ return { found: false, profileData: null, computationData: null };
144
+ }
145
+ }
146
+ } catch (error) {
147
+ logger.log('ERROR', `[checkPiInComputationDate] Error checking PI in computation:`, error);
148
+ return { found: false, profileData: null, computationData: null };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Helper: Get computation metadata from schema collection
154
+ * @param {object} db - Firestore instance
155
+ * @param {string} computationName - Computation name
156
+ * @param {object} config - Config object
157
+ * @returns {Promise<object|null>} Metadata object or null if not found
158
+ */
159
+ async function getComputationMetadata(db, computationName, config) {
160
+ try {
161
+ const schemaCollection = config.schemaCollection || 'computation_schemas';
162
+ const schemaDoc = await db.collection(schemaCollection).doc(computationName).get();
163
+ if (schemaDoc.exists) {
164
+ return schemaDoc.data().metadata || null;
165
+ }
166
+ } catch (err) {
167
+ // Non-critical, return null
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * GET /user/me/computations
174
+ * Fetches computation results for a specific signed-in user
175
+ */
176
+ async function getUserComputations(req, res, dependencies, config) {
177
+ const { db, logger } = dependencies;
178
+ const { userCid, computation, mode = 'latest', limit = 30 } = req.query;
179
+
180
+ if (!userCid) {
181
+ return res.status(400).json({ error: "Missing userCid" });
182
+ }
183
+
184
+ try {
185
+ // Check for dev override impersonation
186
+ const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
187
+ const devOverride = await getDevOverride(db, userCid, config, logger);
188
+ const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
189
+
190
+ const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
191
+ const resultsSub = config.resultsSubcollection || 'results';
192
+ const compsSub = config.computationsSubcollection || 'computations';
193
+
194
+ const category = 'popular-investor';
195
+ const today = new Date().toISOString().split('T')[0];
196
+
197
+ const computationNames = computation ? computation.split(',') : [];
198
+
199
+ if (computationNames.length === 0) {
200
+ return res.status(400).json({ error: "Please specify at least one computation name" });
201
+ }
202
+
203
+ // [NEW] Pre-fetch metadata for all computations to determine which are meta vs standard
204
+ const computationMetadata = {};
205
+ for (const compName of computationNames) {
206
+ const metadata = await getComputationMetadata(db, compName, config);
207
+ if (metadata) {
208
+ computationMetadata[compName] = metadata;
209
+ }
210
+ }
211
+
212
+ const isDevOverrideActive = devOverride && devOverride.enabled && devOverride.fakeCopiedPIs.length > 0;
213
+
214
+ // Check if any of the requested computations are meta computations
215
+ const firstCompName = computationNames[0];
216
+ const firstCompMetadata = computationMetadata[firstCompName];
217
+ const isMetaComputation = firstCompMetadata && firstCompMetadata.type === 'meta';
218
+
219
+ let datesToCheck = [today];
220
+
221
+ if (mode === 'latest') {
222
+ let foundDate = null;
223
+
224
+ if (isMetaComputation) {
225
+ // For meta computations: check today first, then look back 7 days
226
+ const maxDaysBack = 7;
227
+
228
+ for (let daysBack = 0; daysBack < maxDaysBack; daysBack++) {
229
+ const checkDate = new Date();
230
+ checkDate.setDate(checkDate.getDate() - daysBack);
231
+ const dateStr = checkDate.toISOString().split('T')[0];
232
+
233
+ try {
234
+ // For meta computations, just check if the document exists
235
+ const computationRef = db.collection(insightsCollection)
236
+ .doc(dateStr)
237
+ .collection(resultsSub)
238
+ .doc(category)
239
+ .collection(compsSub)
240
+ .doc(firstCompName);
241
+
242
+ const computationDoc = await computationRef.get();
243
+
244
+ if (computationDoc.exists) {
245
+ foundDate = dateStr;
246
+ if (dateStr !== today) {
247
+ logger.log('INFO', `[getUserComputations] Meta computation ${firstCompName} found on fallback date ${foundDate} (today: ${today})`);
248
+ } else {
249
+ logger.log('INFO', `[getUserComputations] Meta computation ${firstCompName} found on today's date`);
250
+ }
251
+ break; // Found document, stop searching
252
+ }
253
+ } catch (error) {
254
+ // Continue to next date if error
255
+ logger.log('DEBUG', `[getUserComputations] Error checking date ${dateStr} for meta computation:`, error.message);
256
+ continue;
257
+ }
258
+ }
259
+ } else {
260
+ // For user-specific computations: use same logic as data-status
261
+ // Search backwards and verify user exists
262
+ const maxDaysBack = 30; // Match data-status search window
263
+
264
+ for (let daysBack = 0; daysBack < maxDaysBack; daysBack++) {
265
+ const checkDate = new Date();
266
+ checkDate.setDate(checkDate.getDate() - daysBack);
267
+ const dateStr = checkDate.toISOString().split('T')[0];
268
+
269
+ try {
270
+ // Check if page/computation document exists for this user and date
271
+ // For computations using pages subcollection, checkPiInComputationDate handles it
272
+ // For other computations, check the main document first
273
+ const { found } = await checkPiInComputationDate(
274
+ db,
275
+ insightsCollection,
276
+ resultsSub,
277
+ compsSub,
278
+ category,
279
+ firstCompName,
280
+ dateStr,
281
+ String(effectiveCid),
282
+ logger
283
+ );
284
+
285
+ if (found) {
286
+ foundDate = dateStr;
287
+ if (dateStr !== today) {
288
+ logger.log('INFO', `[getUserComputations] Using fallback date ${foundDate} for effective CID ${effectiveCid} (today: ${today})`);
289
+ } else {
290
+ logger.log('INFO', `[getUserComputations] Found computation for effective CID ${effectiveCid} on today's date`);
291
+ }
292
+ break; // Found user, stop searching
293
+ }
294
+ } catch (error) {
295
+ // Continue to next date if error
296
+ logger.log('DEBUG', `[getUserComputations] Error checking date ${dateStr}:`, error.message);
297
+ continue;
298
+ }
299
+ }
300
+ }
301
+
302
+ if (foundDate) {
303
+ datesToCheck = [foundDate];
304
+ } else {
305
+ const maxDaysBack = isMetaComputation ? 7 : 30;
306
+ logger.log('WARN', `[getUserComputations] No computation data found for ${isMetaComputation ? 'meta computation' : `CID ${effectiveCid}`} in last ${maxDaysBack} days. Frontend will use fallback.`);
307
+ return res.status(200).json({
308
+ status: 'success',
309
+ userCid: String(effectiveCid),
310
+ mode,
311
+ computations: computationNames,
312
+ data: {},
313
+ isFallback: true,
314
+ requestedDate: today,
315
+ isImpersonating: isImpersonating || false,
316
+ actualCid: Number(userCid)
317
+ });
318
+ }
319
+ } else if (mode === 'series') {
320
+ const limitNum = parseInt(limit) || 30;
321
+ datesToCheck = [];
322
+ for (let i = 0; i < limitNum; i++) {
323
+ const date = new Date();
324
+ date.setDate(date.getDate() - i);
325
+ datesToCheck.push(date.toISOString().split('T')[0]);
326
+ }
327
+ }
328
+
329
+ const results = {};
330
+ let isFallback = false;
331
+
332
+ for (const date of datesToCheck) {
333
+ results[date] = {};
334
+
335
+ for (const compName of computationNames) {
336
+ try {
337
+ let userResult = null;
338
+
339
+ // Special handling for computations that use pages subcollection structure
340
+ // PopularInvestorProfileMetrics, SignedInUserProfileMetrics, and SignedInUserPIPersonalizedMetrics use pages subcollection
341
+ if (compName === 'PopularInvestorProfileMetrics' ||
342
+ compName === 'SignedInUserProfileMetrics' ||
343
+ compName === 'SignedInUserPIPersonalizedMetrics') {
344
+ const pageRef = db.collection(insightsCollection)
345
+ .doc(date)
346
+ .collection(resultsSub)
347
+ .doc(category)
348
+ .collection(compsSub)
349
+ .doc(compName)
350
+ .collection('pages')
351
+ .doc(String(effectiveCid));
352
+
353
+ const pageDoc = await pageRef.get();
354
+
355
+ if (pageDoc.exists) {
356
+ const rawData = pageDoc.data();
357
+ let data = tryDecompress(rawData);
358
+
359
+ if (typeof data === 'string') {
360
+ try {
361
+ data = JSON.parse(data);
362
+ } catch (e) {
363
+ logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} page on ${date}:`, e.message);
364
+ data = null;
365
+ }
366
+ }
367
+
368
+ if (data && typeof data === 'object') {
369
+ userResult = data;
370
+ }
371
+ }
372
+ } else {
373
+ // Standard path for other computations
374
+ const docRef = db.collection(insightsCollection)
375
+ .doc(date)
376
+ .collection(resultsSub)
377
+ .doc(category)
378
+ .collection(compsSub)
379
+ .doc(compName);
380
+
381
+ const doc = await docRef.get();
382
+
383
+ if (doc.exists) {
384
+ const rawData = doc.data();
385
+ let data = tryDecompress(rawData);
386
+
387
+ if (typeof data === 'string') {
388
+ try {
389
+ data = JSON.parse(data);
390
+ } catch (e) {
391
+ logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} on ${date}:`, e.message);
392
+ data = null;
393
+ }
394
+ }
395
+
396
+ if (data && data._sharded === true && data._shardCount) {
397
+ const shardsCol = docRef.collection('_shards');
398
+ const shardsSnapshot = await shardsCol.get();
399
+
400
+ if (!shardsSnapshot.empty) {
401
+ data = {};
402
+ for (const shardDoc of shardsSnapshot.docs) {
403
+ const shardData = shardDoc.data();
404
+ Object.assign(data, shardData);
405
+ }
406
+ } else {
407
+ data = null;
408
+ }
409
+ }
410
+
411
+ // [FIX] Handle meta computations (global results) vs standard computations (user-specific)
412
+ // Use metadata from schema collection to determine computation type
413
+ if (data && typeof data === 'object') {
414
+ const metadata = computationMetadata[compName];
415
+ const isMetaComputation = metadata && metadata.type === 'meta';
416
+
417
+ if (isMetaComputation) {
418
+ // Meta computation: return entire data object (global results)
419
+ userResult = data;
420
+ } else {
421
+ // Standard computation: extract user-specific result
422
+ const userCidKey = String(effectiveCid);
423
+ userResult = data.hasOwnProperty(userCidKey) ? data[userCidKey] : null;
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ if (isDevOverrideActive && (compName === 'SignedInUserProfileMetrics' || compName === 'SignedInUserCopiedPIs')) {
430
+ if (compName === 'SignedInUserCopiedPIs') {
431
+ userResult = {
432
+ current: devOverride.fakeCopiedPIs,
433
+ past: [],
434
+ all: devOverride.fakeCopiedPIs
435
+ };
436
+ logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserCopiedPIs for user ${userCid}`);
437
+ } else if (compName === 'SignedInUserProfileMetrics' && userResult && userResult.copiedPIs) {
438
+ const fakeMirrors = devOverride.fakeCopiedPIs.map(cid => ({
439
+ cid: Number(cid),
440
+ username: `PI-${cid}`,
441
+ invested: 0,
442
+ netProfit: 0,
443
+ value: 0,
444
+ pendingClosure: false,
445
+ isRanked: false
446
+ }));
447
+
448
+ userResult = {
449
+ ...userResult,
450
+ copiedPIs: {
451
+ chartType: 'cards',
452
+ data: fakeMirrors
453
+ }
454
+ };
455
+ logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserProfileMetrics.copiedPIs for user ${userCid}`);
456
+ }
457
+ }
458
+
459
+ if (userResult) {
460
+ results[date][compName] = userResult;
461
+ }
462
+ } catch (err) {
463
+ logger.log('WARN', `[getUserComputations] Error fetching ${compName} for ${date}`, err);
464
+ }
465
+ }
466
+
467
+ if (mode === 'latest' && Object.keys(results[date]).length > 0) {
468
+ isFallback = date !== today;
469
+ break;
470
+ }
471
+ }
472
+
473
+ const cleanedResults = {};
474
+ for (const [date, data] of Object.entries(results)) {
475
+ if (Object.keys(data).length > 0) {
476
+ cleanedResults[date] = data;
477
+ }
478
+ }
479
+
480
+ return res.status(200).json({
481
+ status: 'success',
482
+ userCid: String(effectiveCid),
483
+ mode,
484
+ computations: computationNames,
485
+ data: cleanedResults,
486
+ isFallback: isFallback,
487
+ requestedDate: today,
488
+ devOverrideActive: isDevOverrideActive,
489
+ isImpersonating: isImpersonating || false,
490
+ actualCid: Number(userCid)
491
+ });
492
+
493
+ } catch (error) {
494
+ logger.log('ERROR', `[getUserComputations] Error fetching computations for ${userCid}`, error);
495
+ return res.status(500).json({ error: error.message });
496
+ }
497
+ }
498
+
499
+ module.exports = {
500
+ getUserComputations,
501
+ checkPiInComputationDate
502
+ };
503
+
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Instrument Mappings Helpers
3
+ * Handles instrument ID to ticker and sector mappings
4
+ */
5
+
6
+ /**
7
+ * GET /user/me/instrument-mappings
8
+ * Fetches instrument ID to ticker and sector mappings for the frontend
9
+ */
10
+ async function getInstrumentMappings(req, res, dependencies, config) {
11
+ const { db, logger } = dependencies;
12
+
13
+ try {
14
+ // Fetch from Firestore (same source as computation system)
15
+ const [tickerToIdDoc, tickerToSectorDoc] = await Promise.all([
16
+ db.collection('instrument_mappings').doc('etoro_to_ticker').get(),
17
+ db.collection('instrument_sector_mappings').doc('sector_mappings').get()
18
+ ]);
19
+
20
+ if (!tickerToIdDoc.exists) {
21
+ return res.status(404).json({ error: "Instrument mappings not found" });
22
+ }
23
+
24
+ const tickerToId = tickerToIdDoc.data();
25
+ const tickerToSector = tickerToSectorDoc.exists ? tickerToSectorDoc.data() : {};
26
+
27
+ // Convert to ID -> Ticker mapping (reverse the mapping)
28
+ const idToTicker = {};
29
+ const idToSector = {};
30
+
31
+ for (const [id, ticker] of Object.entries(tickerToId)) {
32
+ idToTicker[String(id)] = ticker;
33
+ // Map ID -> Sector via ticker
34
+ if (tickerToSector[ticker]) {
35
+ idToSector[String(id)] = tickerToSector[ticker];
36
+ }
37
+ }
38
+
39
+ return res.status(200).json({
40
+ instrumentToTicker: idToTicker,
41
+ instrumentToSector: idToSector,
42
+ count: Object.keys(idToTicker).length,
43
+ sectorCount: Object.keys(idToSector).length
44
+ });
45
+
46
+ } catch (error) {
47
+ logger.log('ERROR', `[getInstrumentMappings] Error fetching mappings`, error);
48
+ return res.status(500).json({ error: error.message });
49
+ }
50
+ }
51
+
52
+ module.exports = {
53
+ getInstrumentMappings
54
+ };
55
+