bulltrackers-module 1.0.573 → 1.0.575

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.
@@ -212,14 +212,14 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
212
212
  return subscriptions;
213
213
  }
214
214
  } else {
215
- logger.log('WARN', `[findSubscriptionsForPI] WatchlistMembershipData/${computationDate} not found, falling back to scanning all watchlists`);
216
- // Fallback to legacy method if membership data doesn't exist
217
- return await findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey);
215
+ logger.log('WARN', `[findSubscriptionsForPI] WatchlistMembershipData/${computationDate} not found, returning existing subscriptions (dev overrides only)`);
216
+ // Return subscriptions array which may contain dev overrides
217
+ return subscriptions;
218
218
  }
219
219
  } catch (error) {
220
220
  logger.log('ERROR', `[findSubscriptionsForPI] Error loading WatchlistMembershipData: ${error.message}`);
221
- // Fallback to legacy method on error
222
- return await findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey);
221
+ // Return subscriptions array which may contain dev overrides
222
+ return subscriptions;
223
223
  }
224
224
 
225
225
  // Step 2: For each user, read their watchlists from SignedInUsers/{cid}/watchlists
@@ -298,51 +298,6 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
298
298
  return subscriptions;
299
299
  }
300
300
 
301
- /**
302
- * Legacy fallback method - scans all watchlists (used if WatchlistMembershipData is not available)
303
- */
304
- async function findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey) {
305
- const subscriptions = [];
306
-
307
- logger.log('INFO', `[findSubscriptionsForPILegacy] Using legacy method to find subscriptions for PI ${piCid}`);
308
-
309
- // Get all watchlists from legacy path
310
- const watchlistsCollection = db.collection('watchlists');
311
- const watchlistsSnapshot = await watchlistsCollection.get();
312
-
313
- for (const userDoc of watchlistsSnapshot.docs) {
314
- const userCid = Number(userDoc.id);
315
- const userListsSnapshot = await userDoc.ref.collection('lists').get();
316
-
317
- for (const listDoc of userListsSnapshot.docs) {
318
- const listData = listDoc.data();
319
-
320
- // Check static watchlists
321
- if (listData.type === 'static' && listData.items && Array.isArray(listData.items)) {
322
- for (const item of listData.items) {
323
- if (Number(item.cid) === Number(piCid)) {
324
- const isTestProbe = alertTypeId === 'TestSystemProbe';
325
- const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
326
-
327
- if (isTestProbe || isEnabled) {
328
- subscriptions.push({
329
- userCid: userCid,
330
- piCid: piCid,
331
- piUsername: item.username || `PI-${piCid}`,
332
- watchlistId: listDoc.id,
333
- watchlistName: listData.name || 'Unnamed Watchlist',
334
- alertConfig: item.alertConfig
335
- });
336
- break;
337
- }
338
- }
339
- }
340
- }
341
- }
342
- }
343
-
344
- return subscriptions;
345
- }
346
301
 
347
302
  /**
348
303
  * Check if alert should trigger based on thresholds
@@ -9,6 +9,7 @@ const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
9
9
 
10
10
  /**
11
11
  * Check if a PI exists in a computation date
12
+ * Reads from the new path format: /unified_insights/YYYY-MM-DD/results/popular-investor/computations/PopularInvestorProfileMetrics/pages/{cid}
12
13
  * Returns { found: boolean, profileData: object | null, computationData: object | null }
13
14
  * @param {object} db - Firestore instance
14
15
  * @param {string} insightsCollection - Insights collection name
@@ -23,72 +24,45 @@ const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
23
24
  */
24
25
  async function checkPiInComputationDate(db, insightsCollection, resultsSub, compsSub, category, computationName, dateStr, cidStr, logger) {
25
26
  try {
26
- const computationRef = db.collection(insightsCollection)
27
+ // New path format: /unified_insights/YYYY-MM-DD/results/popular-investor/computations/PopularInvestorProfileMetrics/pages/{cid}
28
+ const pageRef = db.collection(insightsCollection)
27
29
  .doc(dateStr)
28
30
  .collection(resultsSub)
29
31
  .doc(category)
30
32
  .collection(compsSub)
31
- .doc(computationName);
33
+ .doc(computationName)
34
+ .collection('pages')
35
+ .doc(cidStr);
32
36
 
33
- const computationDoc = await computationRef.get();
37
+ const pageDoc = await pageRef.get();
34
38
 
35
- if (!computationDoc.exists) {
39
+ if (!pageDoc.exists) {
40
+ logger.log('INFO', `[checkPiInComputationDate] Page document not found for CID ${cidStr} in date ${dateStr}`);
36
41
  return { found: false, profileData: null, computationData: null };
37
42
  }
38
43
 
39
- const rawData = computationDoc.data();
40
- let computationData = null;
44
+ const rawData = pageDoc.data();
45
+ let profileData = null;
41
46
 
42
- // Check if data is sharded
43
- if (rawData._sharded === true && rawData._shardCount) {
44
- const shardsCol = computationRef.collection('_shards');
45
- const shardCount = rawData._shardCount;
46
-
47
- logger.log('INFO', `[checkPiInComputationDate] Reading ${shardCount} shards for date ${dateStr}`);
48
-
49
- computationData = {};
50
-
51
- // Read all shards (shard_0, shard_1, ..., shard_N-1)
52
- for (let i = 0; i < shardCount; i++) {
53
- const shardDoc = await shardsCol.doc(`shard_${i}`).get();
54
- if (shardDoc.exists) {
55
- const shardData = shardDoc.data();
56
- Object.assign(computationData, shardData);
57
- } else {
58
- logger.log('WARN', `[checkPiInComputationDate] Shard shard_${i} missing for date ${dateStr}`);
59
- }
60
- }
61
- } else {
62
- // Data is in the main document (compressed or raw)
63
- computationData = tryDecompress(rawData);
64
-
65
- // Handle string decompression result
66
- if (typeof computationData === 'string') {
67
- try {
68
- computationData = JSON.parse(computationData);
69
- } catch (e) {
70
- logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for date ${dateStr}:`, e.message);
71
- return { found: false, profileData: null, computationData: null };
72
- }
47
+ // Decompress if needed
48
+ profileData = tryDecompress(rawData);
49
+
50
+ // Handle string decompression result
51
+ if (typeof profileData === 'string') {
52
+ try {
53
+ profileData = JSON.parse(profileData);
54
+ } catch (e) {
55
+ logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for CID ${cidStr} on date ${dateStr}:`, e.message);
56
+ return { found: false, profileData: null, computationData: null };
73
57
  }
74
58
  }
75
59
 
76
- // Check if CID exists in the computation data
77
- if (computationData && typeof computationData === 'object' && !Array.isArray(computationData)) {
78
- // Filter out metadata keys that start with underscore
79
- const cids = Object.keys(computationData).filter(key => !key.startsWith('_'));
80
-
81
- // Check if the requested CID exists
82
- const profileData = computationData[cidStr];
83
- if (profileData) {
84
- logger.log('INFO', `[checkPiInComputationDate] Found CID ${cidStr} in date ${dateStr} (total CIDs: ${cids.length})`);
85
- return { found: true, profileData, computationData };
86
- } else {
87
- logger.log('INFO', `[checkPiInComputationDate] CID ${cidStr} not found in date ${dateStr} (total CIDs: ${cids.length})`);
88
- return { found: false, profileData: null, computationData };
89
- }
60
+ if (profileData && typeof profileData === 'object') {
61
+ logger.log('INFO', `[checkPiInComputationDate] Found profile data for CID ${cidStr} in date ${dateStr}`);
62
+ // Return the profile data - computationData is set to null since we're reading individual pages
63
+ return { found: true, profileData, computationData: null };
90
64
  } else {
91
- logger.log('WARN', `[checkPiInComputationDate] Computation data is not an object for date ${dateStr}`);
65
+ logger.log('WARN', `[checkPiInComputationDate] Profile data is not an object for CID ${cidStr} on date ${dateStr}`);
92
66
  return { found: false, profileData: null, computationData: null };
93
67
  }
94
68
  } catch (error) {
@@ -174,18 +148,38 @@ async function getUserComputations(req, res, dependencies, config) {
174
148
  const dateStr = checkDate.toISOString().split('T')[0];
175
149
 
176
150
  try {
177
- // Check if computation document exists for this date
178
- const computationRef = db.collection(insightsCollection)
179
- .doc(dateStr)
180
- .collection(resultsSub)
181
- .doc(category)
182
- .collection(compsSub)
183
- .doc(firstCompName);
151
+ // For PopularInvestorProfileMetrics, check pages subcollection directly
152
+ // For other computations, check main computation document
153
+ let shouldCheck = false;
184
154
 
185
- const computationDoc = await computationRef.get();
155
+ if (firstCompName === 'PopularInvestorProfileMetrics') {
156
+ // Check if page document exists for this user
157
+ const pageRef = db.collection(insightsCollection)
158
+ .doc(dateStr)
159
+ .collection(resultsSub)
160
+ .doc(category)
161
+ .collection(compsSub)
162
+ .doc(firstCompName)
163
+ .collection('pages')
164
+ .doc(String(effectiveCid));
165
+
166
+ const pageDoc = await pageRef.get();
167
+ shouldCheck = pageDoc.exists;
168
+ } else {
169
+ // Check if computation document exists for this date
170
+ const computationRef = db.collection(insightsCollection)
171
+ .doc(dateStr)
172
+ .collection(resultsSub)
173
+ .doc(category)
174
+ .collection(compsSub)
175
+ .doc(firstCompName);
176
+
177
+ const computationDoc = await computationRef.get();
178
+ shouldCheck = computationDoc.exists;
179
+ }
186
180
 
187
- if (computationDoc.exists) {
188
- // Computation exists, verify user is in it (same as data-status)
181
+ if (shouldCheck) {
182
+ // Computation/page exists, verify user is in it (same as data-status)
189
183
  const { found } = await checkPiInComputationDate(
190
184
  db,
191
185
  insightsCollection,
@@ -249,94 +243,128 @@ async function getUserComputations(req, res, dependencies, config) {
249
243
 
250
244
  for (const compName of computationNames) {
251
245
  try {
252
- const docRef = db.collection(insightsCollection)
253
- .doc(date)
254
- .collection(resultsSub)
255
- .doc(category)
256
- .collection(compsSub)
257
- .doc(compName);
258
-
259
- const doc = await docRef.get();
246
+ let userResult = null;
260
247
 
261
- if (doc.exists) {
262
- const rawData = doc.data();
263
- let data = tryDecompress(rawData);
248
+ // Special handling for PopularInvestorProfileMetrics - read from pages subcollection
249
+ if (compName === 'PopularInvestorProfileMetrics') {
250
+ const pageRef = db.collection(insightsCollection)
251
+ .doc(date)
252
+ .collection(resultsSub)
253
+ .doc(category)
254
+ .collection(compsSub)
255
+ .doc(compName)
256
+ .collection('pages')
257
+ .doc(String(effectiveCid));
264
258
 
265
- if (typeof data === 'string') {
266
- try {
267
- data = JSON.parse(data);
268
- } catch (e) {
269
- logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} on ${date}:`, e.message);
270
- data = null;
271
- }
272
- }
259
+ const pageDoc = await pageRef.get();
273
260
 
274
- if (data && data._sharded === true && data._shardCount) {
275
- const shardsCol = docRef.collection('_shards');
276
- const shardsSnapshot = await shardsCol.get();
261
+ if (pageDoc.exists) {
262
+ const rawData = pageDoc.data();
263
+ let data = tryDecompress(rawData);
277
264
 
278
- if (!shardsSnapshot.empty) {
279
- data = {};
280
- for (const shardDoc of shardsSnapshot.docs) {
281
- const shardData = shardDoc.data();
282
- Object.assign(data, shardData);
265
+ if (typeof data === 'string') {
266
+ try {
267
+ data = JSON.parse(data);
268
+ } catch (e) {
269
+ logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} page on ${date}:`, e.message);
270
+ data = null;
283
271
  }
284
- } else {
285
- data = null;
286
272
  }
287
- }
288
-
289
- // [FIX] Handle meta computations (global results) vs standard computations (user-specific)
290
- // Use metadata from schema collection to determine computation type
291
- let userResult = null;
292
- if (data && typeof data === 'object') {
293
- const metadata = computationMetadata[compName];
294
- const isMetaComputation = metadata && metadata.type === 'meta';
295
273
 
296
- if (isMetaComputation) {
297
- // Meta computation: return entire data object (global results)
274
+ if (data && typeof data === 'object') {
298
275
  userResult = data;
299
- } else {
300
- // Standard computation: extract user-specific result
301
- const userCidKey = String(effectiveCid);
302
- userResult = data.hasOwnProperty(userCidKey) ? data[userCidKey] : null;
303
276
  }
304
277
  }
278
+ } else {
279
+ // Standard path for other computations
280
+ const docRef = db.collection(insightsCollection)
281
+ .doc(date)
282
+ .collection(resultsSub)
283
+ .doc(category)
284
+ .collection(compsSub)
285
+ .doc(compName);
305
286
 
306
- if (isDevOverrideActive && (compName === 'SignedInUserProfileMetrics' || compName === 'SignedInUserCopiedPIs')) {
307
- if (compName === 'SignedInUserCopiedPIs') {
308
- userResult = {
309
- current: devOverride.fakeCopiedPIs,
310
- past: [],
311
- all: devOverride.fakeCopiedPIs
312
- };
313
- logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserCopiedPIs for user ${userCid}`);
314
- } else if (compName === 'SignedInUserProfileMetrics' && userResult && userResult.copiedPIs) {
315
- const fakeMirrors = devOverride.fakeCopiedPIs.map(cid => ({
316
- cid: Number(cid),
317
- username: `PI-${cid}`,
318
- invested: 0,
319
- netProfit: 0,
320
- value: 0,
321
- pendingClosure: false,
322
- isRanked: false
323
- }));
287
+ const doc = await docRef.get();
288
+
289
+ if (doc.exists) {
290
+ const rawData = doc.data();
291
+ let data = tryDecompress(rawData);
292
+
293
+ if (typeof data === 'string') {
294
+ try {
295
+ data = JSON.parse(data);
296
+ } catch (e) {
297
+ logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} on ${date}:`, e.message);
298
+ data = null;
299
+ }
300
+ }
301
+
302
+ if (data && data._sharded === true && data._shardCount) {
303
+ const shardsCol = docRef.collection('_shards');
304
+ const shardsSnapshot = await shardsCol.get();
324
305
 
325
- userResult = {
326
- ...userResult,
327
- copiedPIs: {
328
- chartType: 'cards',
329
- data: fakeMirrors
306
+ if (!shardsSnapshot.empty) {
307
+ data = {};
308
+ for (const shardDoc of shardsSnapshot.docs) {
309
+ const shardData = shardDoc.data();
310
+ Object.assign(data, shardData);
330
311
  }
331
- };
332
- logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserProfileMetrics.copiedPIs for user ${userCid}`);
312
+ } else {
313
+ data = null;
314
+ }
315
+ }
316
+
317
+ // [FIX] Handle meta computations (global results) vs standard computations (user-specific)
318
+ // Use metadata from schema collection to determine computation type
319
+ if (data && typeof data === 'object') {
320
+ const metadata = computationMetadata[compName];
321
+ const isMetaComputation = metadata && metadata.type === 'meta';
322
+
323
+ if (isMetaComputation) {
324
+ // Meta computation: return entire data object (global results)
325
+ userResult = data;
326
+ } else {
327
+ // Standard computation: extract user-specific result
328
+ const userCidKey = String(effectiveCid);
329
+ userResult = data.hasOwnProperty(userCidKey) ? data[userCidKey] : null;
330
+ }
333
331
  }
334
332
  }
335
-
336
- if (userResult) {
337
- results[date][compName] = userResult;
333
+ }
334
+
335
+ if (isDevOverrideActive && (compName === 'SignedInUserProfileMetrics' || compName === 'SignedInUserCopiedPIs')) {
336
+ if (compName === 'SignedInUserCopiedPIs') {
337
+ userResult = {
338
+ current: devOverride.fakeCopiedPIs,
339
+ past: [],
340
+ all: devOverride.fakeCopiedPIs
341
+ };
342
+ logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserCopiedPIs for user ${userCid}`);
343
+ } else if (compName === 'SignedInUserProfileMetrics' && userResult && userResult.copiedPIs) {
344
+ const fakeMirrors = devOverride.fakeCopiedPIs.map(cid => ({
345
+ cid: Number(cid),
346
+ username: `PI-${cid}`,
347
+ invested: 0,
348
+ netProfit: 0,
349
+ value: 0,
350
+ pendingClosure: false,
351
+ isRanked: false
352
+ }));
353
+
354
+ userResult = {
355
+ ...userResult,
356
+ copiedPIs: {
357
+ chartType: 'cards',
358
+ data: fakeMirrors
359
+ }
360
+ };
361
+ logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserProfileMetrics.copiedPIs for user ${userCid}`);
338
362
  }
339
363
  }
364
+
365
+ if (userResult) {
366
+ results[date][compName] = userResult;
367
+ }
340
368
  } catch (err) {
341
369
  logger.log('WARN', `[getUserComputations] Error fetching ${compName} for ${date}`, err);
342
370
  }
@@ -138,33 +138,14 @@ async function getPiProfile(req, res, dependencies, config) {
138
138
  if (!foundDate || !profileData) {
139
139
  logger.log('WARN', `[getPiProfile] CID ${cid} not found in any checked dates: ${checkedDates.join(', ')}`);
140
140
 
141
- // Try to get sample data from the latest date to show what CIDs are available
142
- const latestResult = await checkPiInComputationDate(
143
- db,
144
- insightsCollection,
145
- resultsSub,
146
- compsSub,
147
- category,
148
- computationName,
149
- latestDate,
150
- cidStr,
151
- logger
152
- );
153
-
154
- const allAvailableCids = latestResult.computationData && typeof latestResult.computationData === 'object' && !Array.isArray(latestResult.computationData)
155
- ? Object.keys(latestResult.computationData)
156
- .filter(key => !key.startsWith('_'))
157
- .sort()
158
- : [];
159
-
141
+ // Note: With the new pages subcollection structure, we can't enumerate all available CIDs
142
+ // from a single document, so we skip that debug information
160
143
  return res.status(404).json({
161
144
  error: "Profile data not found",
162
145
  message: `Popular Investor ${cid} does not exist in computation results for the last ${maxDaysBackForPi + 1} days. This PI may not have been processed recently.`,
163
146
  debug: {
164
147
  searchedCid: cidStr,
165
148
  checkedDates: checkedDates,
166
- totalCidsInLatestDocument: allAvailableCids.length,
167
- sampleAvailableCids: allAvailableCids.slice(0, 20),
168
149
  latestDate: latestDate
169
150
  }
170
151
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.573",
3
+ "version": "1.0.575",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [