bulltrackers-module 1.0.569 → 1.0.571

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.
@@ -148,51 +148,70 @@ async function getUserDataStatus(req, res, dependencies, config) {
148
148
  const category = 'popular-investor';
149
149
  const computationName = 'SignedInUserProfileMetrics';
150
150
 
151
- // Find latest computation date
152
- const latestComputationDate = await findLatestComputationDate(
153
- db,
154
- insightsCollection,
155
- resultsSub,
156
- compsSub,
157
- category,
158
- computationName,
159
- effectiveCid,
160
- 30
161
- );
162
-
163
151
  let computationAvailable = false;
164
152
  let computationDate = null;
165
153
  let isComputationFallback = false;
166
154
 
167
- if (latestComputationDate) {
168
- // Check if this specific user exists in the computation
169
- const { found } = await checkPiInComputationDate(
170
- db,
171
- insightsCollection,
172
- resultsSub,
173
- compsSub,
174
- category,
175
- computationName,
176
- latestComputationDate,
177
- String(effectiveCid),
178
- logger
179
- );
155
+ // Search backwards from today to find the latest date where user exists in computation
156
+ // Priority: Today (T) -> Yesterday (T-1) -> Continue backwards up to 30 days
157
+ const maxDaysBack = 30;
158
+ let foundDate = null;
159
+
160
+ for (let daysBack = 0; daysBack < maxDaysBack; daysBack++) {
161
+ const checkDate = new Date();
162
+ checkDate.setDate(checkDate.getDate() - daysBack);
163
+ const dateStr = checkDate.toISOString().split('T')[0];
180
164
 
181
- if (found) {
182
- computationAvailable = true;
183
- computationDate = latestComputationDate;
184
- isComputationFallback = latestComputationDate !== today;
165
+ try {
166
+ // Check if computation document exists for this date
167
+ const computationRef = db.collection(insightsCollection)
168
+ .doc(dateStr)
169
+ .collection(resultsSub)
170
+ .doc(category)
171
+ .collection(compsSub)
172
+ .doc(computationName);
173
+
174
+ const computationDoc = await computationRef.get();
185
175
 
186
- if (isComputationFallback) {
187
- logger.log('INFO', `[getUserDataStatus] Using fallback computation date ${computationDate} for user ${userCid} (today: ${today})`);
188
- } else {
189
- logger.log('INFO', `[getUserDataStatus] Found computation for user ${userCid} on today's date`);
176
+ if (computationDoc.exists) {
177
+ // Computation exists for this date, check if user is in it
178
+ const { found } = await checkPiInComputationDate(
179
+ db,
180
+ insightsCollection,
181
+ resultsSub,
182
+ compsSub,
183
+ category,
184
+ computationName,
185
+ dateStr,
186
+ String(effectiveCid),
187
+ logger
188
+ );
189
+
190
+ if (found) {
191
+ foundDate = dateStr;
192
+ computationAvailable = true;
193
+ computationDate = dateStr;
194
+ isComputationFallback = daysBack > 0;
195
+
196
+ if (isComputationFallback) {
197
+ logger.log('INFO', `[getUserDataStatus] Found computation for user ${userCid} on date ${computationDate} (${daysBack} days back from today: ${today})`);
198
+ } else {
199
+ logger.log('INFO', `[getUserDataStatus] Found computation for user ${userCid} on today's date`);
200
+ }
201
+ break; // Found user, stop searching
202
+ } else {
203
+ logger.log('DEBUG', `[getUserDataStatus] Computation exists for date ${dateStr} but user ${userCid} not found in it, continuing search...`);
204
+ }
190
205
  }
191
- } else {
192
- logger.log('INFO', `[getUserDataStatus] Computation exists for date ${latestComputationDate} but user ${userCid} not found in it`);
206
+ } catch (error) {
207
+ // Continue to next date if error
208
+ logger.log('DEBUG', `[getUserDataStatus] Error checking date ${dateStr}:`, error.message);
209
+ continue;
193
210
  }
194
- } else {
195
- logger.log('INFO', `[getUserDataStatus] No computation found for user ${userCid} in last 30 days`);
211
+ }
212
+
213
+ if (!foundDate) {
214
+ logger.log('INFO', `[getUserDataStatus] No computation found for user ${userCid} in last ${maxDaysBack} days`);
196
215
  }
197
216
 
198
217
  // For backward compatibility, keep portfolioAvailable and historyAvailable
@@ -201,14 +220,22 @@ async function getUserDataStatus(req, res, dependencies, config) {
201
220
  portfolioAvailable: computationAvailable, // Profile page uses computation, not raw portfolio
202
221
  historyAvailable: computationAvailable, // Profile page uses computation, not raw history
203
222
  computationAvailable: computationAvailable, // Explicit computation check
204
- date: computationDate || today,
205
- computationDate: computationDate,
206
- isComputationFallback: isComputationFallback,
207
- requestedDate: today,
223
+ date: computationDate || today, // Use found date or today as fallback
224
+ computationDate: computationDate, // The actual date where computation was found
225
+ isComputationFallback: isComputationFallback, // True if using T-1 or older date
226
+ requestedDate: today, // What date was requested (today)
208
227
  userCid: String(userCid),
209
228
  effectiveCid: String(effectiveCid)
210
229
  };
211
230
 
231
+ if (computationAvailable && isComputationFallback) {
232
+ logger.log('INFO', `[getUserDataStatus] Using fallback date ${computationDate} for CID ${userCid} (requested: ${today})`);
233
+ } else if (computationAvailable) {
234
+ logger.log('INFO', `[getUserDataStatus] Using today's date ${computationDate} for CID ${userCid}`);
235
+ } else {
236
+ logger.log('WARN', `[getUserDataStatus] No computation found for CID ${userCid} in last ${maxDaysBack} days`);
237
+ }
238
+
212
239
  logger.log('INFO', `[getUserDataStatus] Result for CID ${userCid}:`, result);
213
240
 
214
241
  return res.status(200).json(result);
@@ -446,11 +446,21 @@ async function updateNotificationPreferences(req, res, dependencies, config) {
446
446
 
447
447
  /**
448
448
  * GET /user/me/notifications
449
- * Get user notification history
449
+ * Get user notification history with pagination, date range, and filtering
450
450
  */
451
451
  async function getNotificationHistory(req, res, dependencies, config) {
452
452
  const { db, logger, collectionRegistry } = dependencies;
453
- const { userCid, limit = 50, offset = 0, type, read } = req.query;
453
+ const {
454
+ userCid,
455
+ limit = 50,
456
+ offset = 0,
457
+ type,
458
+ read,
459
+ subType,
460
+ alertType,
461
+ startDate,
462
+ endDate
463
+ } = req.query;
454
464
 
455
465
  if (!userCid) {
456
466
  return res.status(400).json({ error: "Missing userCid" });
@@ -466,10 +476,31 @@ async function getNotificationHistory(req, res, dependencies, config) {
466
476
  notificationsPath = `user_notifications/${userCid}/notifications`;
467
477
  }
468
478
 
469
- // Try to query from new path first
479
+ // Parse date filters
480
+ let startDateObj = null;
481
+ let endDateObj = null;
482
+ if (startDate) {
483
+ startDateObj = new Date(startDate);
484
+ if (isNaN(startDateObj.getTime())) {
485
+ return res.status(400).json({ error: "Invalid startDate format" });
486
+ }
487
+ }
488
+ if (endDate) {
489
+ endDateObj = new Date(endDate);
490
+ if (isNaN(endDateObj.getTime())) {
491
+ return res.status(400).json({ error: "Invalid endDate format" });
492
+ }
493
+ // Set to end of day
494
+ endDateObj.setHours(23, 59, 59, 999);
495
+ }
496
+
497
+ // Build query - fetch more than needed to apply filters in memory
498
+ // Firestore has limitations on complex queries, so we'll fetch a larger batch
499
+ // and filter in memory for date range and subType
500
+ const maxFetchLimit = 1000; // Fetch up to 1000 to apply filters
470
501
  let query = db.collection(notificationsPath)
471
502
  .orderBy('timestamp', 'desc')
472
- .limit(parseInt(limit));
503
+ .limit(maxFetchLimit);
473
504
 
474
505
  if (type) {
475
506
  query = query.where('type', '==', type);
@@ -487,24 +518,74 @@ async function getNotificationHistory(req, res, dependencies, config) {
487
518
  logger.log('WARN', `[getNotificationHistory] Query failed, trying without filters: ${queryError.message}`);
488
519
  query = db.collection(notificationsPath)
489
520
  .orderBy('timestamp', 'desc')
490
- .limit(parseInt(limit));
521
+ .limit(maxFetchLimit);
491
522
  snapshot = await query.get();
492
523
  }
493
524
 
494
525
  const notifications = [];
526
+ const admin = require('firebase-admin');
495
527
 
496
528
  snapshot.forEach(doc => {
497
529
  const data = doc.data();
498
530
 
499
- // Apply filters in memory if query filters failed
531
+ // Convert timestamp to Date object
532
+ let timestamp = null;
533
+ if (data.timestamp) {
534
+ if (data.timestamp.toDate) {
535
+ timestamp = data.timestamp.toDate();
536
+ } else if (data.timestamp instanceof admin.firestore.Timestamp) {
537
+ timestamp = data.timestamp.toDate();
538
+ } else if (data.timestamp instanceof Date) {
539
+ timestamp = data.timestamp;
540
+ }
541
+ } else if (data.createdAt) {
542
+ if (data.createdAt.toDate) {
543
+ timestamp = data.createdAt.toDate();
544
+ } else if (data.createdAt instanceof admin.firestore.Timestamp) {
545
+ timestamp = data.createdAt.toDate();
546
+ } else if (data.createdAt instanceof Date) {
547
+ timestamp = data.createdAt;
548
+ }
549
+ }
550
+
551
+ if (!timestamp) {
552
+ timestamp = new Date();
553
+ }
554
+
555
+ // Apply filters in memory
500
556
  let include = true;
557
+
558
+ // Type filter (already applied in query if possible)
501
559
  if (type && data.type !== type) {
502
560
  include = false;
503
561
  }
562
+
563
+ // Read filter (already applied in query if possible)
504
564
  if (read !== undefined && data.read !== (read === 'true')) {
505
565
  include = false;
506
566
  }
507
567
 
568
+ // SubType filter
569
+ if (subType && data.subType !== subType) {
570
+ include = false;
571
+ }
572
+
573
+ // AlertType filter (for watchlist alerts, filters by metadata.alertType)
574
+ if (alertType) {
575
+ const metadata = data.metadata || {};
576
+ if (metadata.alertType !== alertType) {
577
+ include = false;
578
+ }
579
+ }
580
+
581
+ // Date range filter
582
+ if (startDateObj && timestamp < startDateObj) {
583
+ include = false;
584
+ }
585
+ if (endDateObj && timestamp > endDateObj) {
586
+ include = false;
587
+ }
588
+
508
589
  if (include) {
509
590
  notifications.push({
510
591
  id: doc.id,
@@ -513,23 +594,31 @@ async function getNotificationHistory(req, res, dependencies, config) {
513
594
  title: data.title || '',
514
595
  message: data.message || '',
515
596
  read: data.read || false,
516
- timestamp: data.timestamp?.toDate?.()?.toISOString() || data.createdAt?.toDate?.()?.toISOString() || new Date().toISOString(),
597
+ timestamp: timestamp.toISOString(),
517
598
  metadata: data.metadata || {}
518
599
  });
519
600
  }
520
601
  });
521
602
 
522
- // Apply offset in memory
603
+ // Sort by timestamp (descending) to ensure proper ordering
604
+ notifications.sort((a, b) => {
605
+ return new Date(b.timestamp) - new Date(a.timestamp);
606
+ });
607
+
608
+ // Apply pagination
523
609
  const offsetNum = parseInt(offset);
524
- const paginatedNotifications = notifications.slice(offsetNum, offsetNum + parseInt(limit));
610
+ const limitNum = parseInt(limit);
611
+ const totalCount = notifications.length;
612
+ const paginatedNotifications = notifications.slice(offsetNum, offsetNum + limitNum);
525
613
 
526
614
  return res.status(200).json({
527
615
  success: true,
528
616
  notifications: paginatedNotifications,
529
617
  count: paginatedNotifications.length,
530
- total: notifications.length,
531
- limit: parseInt(limit),
532
- offset: offsetNum
618
+ total: totalCount,
619
+ limit: limitNum,
620
+ offset: offsetNum,
621
+ hasMore: offsetNum + limitNum < totalCount
533
622
  });
534
623
  } catch (error) {
535
624
  logger.log('ERROR', `[getNotificationHistory] Error fetching notifications for ${userCid}`, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.569",
3
+ "version": "1.0.571",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [