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.
- package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +101 -12
- package/functions/orchestrator/helpers/update_helpers.js +36 -6
- package/functions/task-engine/handler_creator.js +1 -1
- package/functions/task-engine/helpers/popular_investor_helpers.js +1 -1
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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:
|
|
597
|
+
timestamp: timestamp.toISOString(),
|
|
517
598
|
metadata: data.metadata || {}
|
|
518
599
|
});
|
|
519
600
|
}
|
|
520
601
|
});
|
|
521
602
|
|
|
522
|
-
//
|
|
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
|
|
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:
|
|
531
|
-
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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
|