bulltrackers-module 1.0.609 → 1.0.610
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.
|
@@ -64,24 +64,41 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
// Write to alerts collection (not notifications) - alerts are separate from system notifications
|
|
67
68
|
// Use writeWithMigration to write to new path (with legacy fallback)
|
|
68
69
|
const writePromise = writeWithMigration(
|
|
69
70
|
db,
|
|
70
71
|
'signedInUsers',
|
|
71
|
-
'
|
|
72
|
+
'alerts', // Write to alerts collection, not notifications
|
|
72
73
|
{ cid: String(userCid) },
|
|
73
|
-
|
|
74
|
+
{
|
|
75
|
+
// Alert-specific format (simplified from notification format)
|
|
76
|
+
alertId: notificationId,
|
|
77
|
+
piCid: Number(piCid),
|
|
78
|
+
piUsername: piUsername,
|
|
79
|
+
alertType: alertType.id,
|
|
80
|
+
alertTypeName: alertType.name,
|
|
81
|
+
message: alertMessage,
|
|
82
|
+
severity: alertType.severity,
|
|
83
|
+
watchlistId: subscription.watchlistId,
|
|
84
|
+
watchlistName: subscription.watchlistName,
|
|
85
|
+
read: false,
|
|
86
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
87
|
+
computationDate: computationDate,
|
|
88
|
+
computationName: alertType.computationName,
|
|
89
|
+
...(computationMetadata || {})
|
|
90
|
+
},
|
|
74
91
|
{
|
|
75
92
|
isCollection: true,
|
|
76
93
|
merge: false,
|
|
77
|
-
dataType: '
|
|
94
|
+
dataType: 'alerts',
|
|
78
95
|
documentId: notificationId,
|
|
79
96
|
dualWrite: false, // Disable dual write - we're fully migrated to new path
|
|
80
97
|
config,
|
|
81
98
|
collectionRegistry
|
|
82
99
|
}
|
|
83
100
|
).catch(err => {
|
|
84
|
-
logger.log('ERROR', `[processAlertForPI] Failed to write
|
|
101
|
+
logger.log('ERROR', `[processAlertForPI] Failed to write alert for CID ${userCid}: ${err.message}`, err);
|
|
85
102
|
throw err; // Re-throw so we know if writes are failing
|
|
86
103
|
});
|
|
87
104
|
|
|
@@ -343,11 +343,22 @@ async function checkIfPIHasAlertsToday(db, logger, userCid, piCid, date, depende
|
|
|
343
343
|
|
|
344
344
|
const notificationsRef = db.collection(notificationsPath);
|
|
345
345
|
|
|
346
|
-
// Check
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.
|
|
346
|
+
// Check alerts collection instead of notifications
|
|
347
|
+
// Use collection registry to get alerts path
|
|
348
|
+
let alertsPath;
|
|
349
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
350
|
+
alertsPath = collectionRegistry.getCollectionPath('signedInUsers', 'alerts', { cid: String(userCid) });
|
|
351
|
+
} else {
|
|
352
|
+
// Fallback to legacy path
|
|
353
|
+
alertsPath = `user_alerts/${userCid}/alerts`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const alertsRef = db.collection(alertsPath);
|
|
357
|
+
|
|
358
|
+
// Check if there are any alerts for this PI today
|
|
359
|
+
const snapshot = await alertsRef
|
|
360
|
+
.where('piCid', '==', Number(piCid))
|
|
361
|
+
.where('computationDate', '==', date)
|
|
351
362
|
.limit(1)
|
|
352
363
|
.get();
|
|
353
364
|
|
|
@@ -415,19 +426,30 @@ async function sendAllClearNotification(db, logger, userCid, piCid, piUsername,
|
|
|
415
426
|
}
|
|
416
427
|
};
|
|
417
428
|
|
|
418
|
-
//
|
|
419
|
-
// notifications is a subcollection, so we need isCollection: true and documentId
|
|
429
|
+
// Write to alerts collection (not notifications) - alerts are separate from system notifications
|
|
420
430
|
const { writeWithMigration } = require('../old-generic-api/user-api/helpers/core/path_resolution_helpers');
|
|
421
431
|
await writeWithMigration(
|
|
422
432
|
db,
|
|
423
433
|
'signedInUsers',
|
|
424
|
-
'
|
|
434
|
+
'alerts', // Write to alerts collection, not notifications
|
|
425
435
|
{ cid: String(userCid) },
|
|
426
|
-
|
|
436
|
+
{
|
|
437
|
+
// Alert-specific format
|
|
438
|
+
alertId: notificationId,
|
|
439
|
+
piCid: Number(piCid),
|
|
440
|
+
piUsername: piUsername,
|
|
441
|
+
alertType: 'all_clear',
|
|
442
|
+
alertTypeName: 'All Clear',
|
|
443
|
+
message: `${piUsername} was processed, all clear today!`,
|
|
444
|
+
severity: 'info',
|
|
445
|
+
read: false,
|
|
446
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
447
|
+
computationDate: date
|
|
448
|
+
},
|
|
427
449
|
{
|
|
428
450
|
isCollection: true,
|
|
429
451
|
merge: false,
|
|
430
|
-
dataType: '
|
|
452
|
+
dataType: 'alerts',
|
|
431
453
|
documentId: notificationId,
|
|
432
454
|
dualWrite: false, // Disable dual write - we're fully migrated to new path
|
|
433
455
|
config,
|
|
@@ -1112,13 +1112,26 @@ const getComputationResults = async (db, computationName, dateStr, userId = null
|
|
|
1112
1112
|
|
|
1113
1113
|
// 11. Fetch User Notifications
|
|
1114
1114
|
const fetchNotifications = async (firestore, userId, options = {}) => {
|
|
1115
|
-
const { limit = 20, unreadOnly = false } = options;
|
|
1115
|
+
const { limit = 20, unreadOnly = false, excludeTypes = ['watchlistAlerts'] } = options;
|
|
1116
1116
|
try {
|
|
1117
1117
|
let query = firestore.collection('SignedInUsers').doc(userId).collection('notifications');
|
|
1118
1118
|
if (unreadOnly) query = query.where('read', '==', false);
|
|
1119
1119
|
|
|
1120
|
-
const snapshot = await query.orderBy('createdAt', 'desc').limit(limit).get();
|
|
1121
|
-
|
|
1120
|
+
const snapshot = await query.orderBy('createdAt', 'desc').limit(limit * 2).get(); // Fetch more to filter
|
|
1121
|
+
|
|
1122
|
+
// Filter out excluded types (like watchlistAlerts which go to alerts bell)
|
|
1123
|
+
let notifications = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
|
1124
|
+
|
|
1125
|
+
if (excludeTypes && excludeTypes.length > 0) {
|
|
1126
|
+
notifications = notifications.filter(notif => {
|
|
1127
|
+
// Exclude by type field or metadata.notificationType
|
|
1128
|
+
const notifType = notif.type || notif.metadata?.notificationType;
|
|
1129
|
+
return !excludeTypes.includes(notifType);
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Limit after filtering
|
|
1134
|
+
return notifications.slice(0, limit);
|
|
1122
1135
|
} catch (error) {
|
|
1123
1136
|
console.error(`Error fetching notifications: ${error}`);
|
|
1124
1137
|
throw error;
|
|
@@ -1225,14 +1238,29 @@ const trackPopularInvestorView = async (db, piId, viewerId = null, viewerType =
|
|
|
1225
1238
|
// ==========================================
|
|
1226
1239
|
|
|
1227
1240
|
const fetchUserAlerts = async (db, userId, options = {}) => {
|
|
1228
|
-
const { limit = 50, unreadOnly = false, type } = options;
|
|
1229
|
-
|
|
1241
|
+
const { limit = 50, unreadOnly = false, type, piCid } = options;
|
|
1242
|
+
// Use new path: SignedInUsers/{cid}/alerts
|
|
1243
|
+
let query = db.collection('SignedInUsers').doc(String(userId)).collection('alerts').orderBy('createdAt', 'desc');
|
|
1230
1244
|
|
|
1231
1245
|
if (unreadOnly) query = query.where('read', '==', false);
|
|
1232
1246
|
if (type) query = query.where('alertType', '==', type);
|
|
1247
|
+
if (piCid) query = query.where('piCid', '==', Number(piCid));
|
|
1233
1248
|
|
|
1234
1249
|
const snapshot = await query.limit(limit).get();
|
|
1235
|
-
|
|
1250
|
+
// Convert Firestore timestamps to ISO strings for frontend
|
|
1251
|
+
return snapshot.docs.map(doc => {
|
|
1252
|
+
const data = doc.data();
|
|
1253
|
+
// Convert Firestore timestamps to ISO strings
|
|
1254
|
+
if (data.createdAt) {
|
|
1255
|
+
data.createdAt = data.createdAt.toDate ? data.createdAt.toDate().toISOString() :
|
|
1256
|
+
(data.createdAt.toISOString ? data.createdAt.toISOString() : data.createdAt);
|
|
1257
|
+
}
|
|
1258
|
+
if (data.readAt) {
|
|
1259
|
+
data.readAt = data.readAt.toDate ? data.readAt.toDate().toISOString() :
|
|
1260
|
+
(data.readAt.toISOString ? data.readAt.toISOString() : data.readAt);
|
|
1261
|
+
}
|
|
1262
|
+
return { id: doc.id, ...data };
|
|
1263
|
+
});
|
|
1236
1264
|
};
|
|
1237
1265
|
|
|
1238
1266
|
const subscribeToWatchlistAlerts = async (db, userId, watchlistId, piId, alertConfig) => {
|
|
@@ -14,11 +14,12 @@ const router = express.Router();
|
|
|
14
14
|
router.get('/history', async (req, res) => {
|
|
15
15
|
try {
|
|
16
16
|
const { db } = req.dependencies;
|
|
17
|
-
const { limit, unreadOnly, type } = req.query;
|
|
17
|
+
const { limit, unreadOnly, type, piCid } = req.query;
|
|
18
18
|
const alerts = await fetchUserAlerts(db, req.targetUserId, {
|
|
19
19
|
limit: parseInt(limit || 50),
|
|
20
20
|
unreadOnly: unreadOnly === 'true',
|
|
21
|
-
type
|
|
21
|
+
type: type || undefined,
|
|
22
|
+
piCid: piCid ? Number(piCid) : undefined
|
|
22
23
|
});
|
|
23
24
|
res.json({ success: true, count: alerts.length, data: alerts });
|
|
24
25
|
} catch (error) {
|
|
@@ -41,7 +42,7 @@ router.get('/count', async (req, res) => {
|
|
|
41
42
|
router.put('/:id/read', async (req, res) => {
|
|
42
43
|
try {
|
|
43
44
|
const { db } = req.dependencies;
|
|
44
|
-
await db.collection('
|
|
45
|
+
await db.collection('SignedInUsers').doc(String(req.targetUserId)).collection('alerts').doc(req.params.id)
|
|
45
46
|
.update({ read: true, readAt: new Date() });
|
|
46
47
|
res.json({ success: true });
|
|
47
48
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
@@ -52,7 +53,7 @@ router.put('/read-all', async (req, res) => {
|
|
|
52
53
|
try {
|
|
53
54
|
const { db } = req.dependencies;
|
|
54
55
|
const batch = db.batch();
|
|
55
|
-
const snaps = await db.collection('
|
|
56
|
+
const snaps = await db.collection('SignedInUsers').doc(String(req.targetUserId)).collection('alerts').where('read', '==', false).get();
|
|
56
57
|
snaps.docs.forEach(doc => batch.update(doc.ref, { read: true, readAt: new Date() }));
|
|
57
58
|
await batch.commit();
|
|
58
59
|
res.json({ success: true, updated: snaps.size });
|
|
@@ -63,7 +64,7 @@ router.put('/read-all', async (req, res) => {
|
|
|
63
64
|
router.delete('/:id', async (req, res) => {
|
|
64
65
|
try {
|
|
65
66
|
const { db } = req.dependencies;
|
|
66
|
-
await db.collection('
|
|
67
|
+
await db.collection('SignedInUsers').doc(String(req.targetUserId)).collection('alerts').doc(req.params.id).delete();
|
|
67
68
|
res.json({ success: true });
|
|
68
69
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
69
70
|
});
|