bulltrackers-module 1.0.568 → 1.0.570

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.
@@ -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);
@@ -42,17 +42,47 @@ async function getUpdateTargets(userType, thresholds, config, dependencies) {
42
42
 
43
43
  /**
44
44
  * Sub-pipe: pipe.orchestrator.dispatchUpdates
45
+ * UPDATED: Added real-time validation to skip users already updated today.
45
46
  */
46
47
  async function dispatchUpdates(targets, userType, config, dependencies) {
47
- const { logger, pubsubUtils } = dependencies;
48
+ const { logger, pubsubUtils, db } = dependencies;
48
49
  const { dispatcherTopicName, taskBatchSize, pubsubBatchSize } = config;
49
50
 
50
- if (targets.length === 0) {
51
- logger.log('INFO', `[Orchestrator Helpers] No ${userType} update targets to dispatch.`);
52
- return;
53
- }
51
+ if (targets.length === 0) return;
52
+
53
+ // --- NEW VALIDATION BLOCK ---
54
+ const startOfToday = new Date();
55
+ startOfToday.setUTCHours(0, 0, 0, 0);
56
+
57
+ logger.log('INFO', `[Orchestrator Validation] Verifying ${targets.length} targets for ${userType}...`);
54
58
 
55
- logger.log('INFO', `[Orchestrator Helpers] Dispatching ${targets.length} update tasks for ${userType} to ${dispatcherTopicName}...`);
59
+ const validTargets = [];
60
+ for (const target of targets) {
61
+ const cid = target.userId || target.cid || (typeof target === 'string' ? target : null);
62
+ if (!cid) { validTargets.push(target); continue; }
63
+
64
+ // Check the actual Firestore record for today's completion
65
+ const collection = userType === 'popular_investor' ? 'PopularInvestors' :
66
+ userType === 'signed_in_user' ? 'SignedInUsers' : 'NormalUsers';
67
+
68
+ const doc = await db.collection(collection).doc(String(cid)).get();
69
+ const lastUpdated = doc.data()?.lastUpdated?.updatedAt?.toDate?.() ||
70
+ doc.data()?.lastUpdate?.toDate?.();
71
+
72
+ if (lastUpdated && lastUpdated >= startOfToday) {
73
+ logger.log('TRACE', `[Orchestrator Validation] Skipping ${cid} - Already updated today.`);
74
+ continue;
75
+ }
76
+ validTargets.push(target);
77
+ }
78
+
79
+ if (validTargets.length === 0) {
80
+ logger.log('INFO', `[Orchestrator Helpers] All ${targets.length} targets already updated. Skipping dispatch.`);
81
+ return;
82
+ }
83
+ // --- END VALIDATION BLOCK ---
84
+
85
+ logger.log('INFO', `[Orchestrator Helpers] Dispatching ${validTargets.length} validated update tasks for ${userType}...`);
56
86
 
57
87
  const individualTasks = targets.map(target => {
58
88
  let task = { userType };
@@ -133,7 +133,7 @@ async function handleRequest(message, context, configObj, dependencies) {
133
133
  if (updateTasksCount > 0) {
134
134
  try {
135
135
  // This batch counter is critical for triggering the Root Data Indexer
136
- batchCounterRef = db.collection('task_engine_batch_counters').doc(`${today}-${taskId}`);
136
+ batchCounterRef = db.collection('system_task_counts').doc(`${today}-${taskId}`);
137
137
  await batchCounterRef.set({
138
138
  totalTasks: updateTasksCount,
139
139
  remainingTasks: updateTasksCount,
@@ -308,7 +308,7 @@ async function handleGenericUserUpdate(taskData, config, dependencies, isPI) {
308
308
 
309
309
  // 5. Batch Counter Decrement (Critical for Scheduled Runs)
310
310
  if (batchCounterId) {
311
- const counterRef = db.collection('task_engine_batch_counters').doc(batchCounterId);
311
+ const counterRef = db.collection('system_task_counts').doc(batchCounterId);
312
312
  await counterRef.update({ remainingTasks: FieldValue.increment(-1) });
313
313
 
314
314
  // Check if we need to trigger root indexer for the batch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.568",
3
+ "version": "1.0.570",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [