bulltrackers-module 1.0.522 → 1.0.524
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/alert-system/helpers/alert_helpers.js +34 -73
- package/functions/alert-system/index.js +76 -50
- package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +50 -93
- package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +43 -14
- package/package.json +1 -1
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
7
|
const zlib = require('zlib');
|
|
8
8
|
const { getAlertTypeByComputation, generateAlertMessage } = require('./alert_type_registry');
|
|
9
|
+
const { writeWithMigration } = require('../../generic-api/user-api/helpers/core/path_resolution_helpers');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Process alerts for a specific PI from computation results
|
|
12
13
|
*/
|
|
13
|
-
async function processAlertForPI(db, logger, piCid, alertType, computationMetadata, computationDate) {
|
|
14
|
+
async function processAlertForPI(db, logger, piCid, alertType, computationMetadata, computationDate, dependencies = {}) {
|
|
14
15
|
try {
|
|
15
16
|
// 1. Get PI username from rankings or subscriptions
|
|
16
17
|
const piUsername = await getPIUsername(db, piCid);
|
|
@@ -29,52 +30,24 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
29
30
|
// 3. Generate alert message
|
|
30
31
|
const alertMessage = generateAlertMessage(alertType, piUsername, computationMetadata);
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.limit(1)
|
|
37
|
-
.get();
|
|
38
|
-
|
|
39
|
-
if (!signedInUsersSnapshot.empty) {
|
|
40
|
-
return signedInUsersSnapshot.docs[0].id; // Firebase UID is the document ID
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 4. Create notifications for each subscribed user (using user_notifications collection)
|
|
46
|
-
// Convert eToro CIDs to Firebase UIDs first
|
|
47
|
-
const batch = db.batch();
|
|
48
|
-
const notificationRefs = [];
|
|
49
|
-
const counterUpdates = {};
|
|
50
|
-
const uidMapping = {}; // Cache for CID -> UID mappings
|
|
33
|
+
// 4. Create notifications for each subscribed user (using CID and collection registry)
|
|
34
|
+
const { collectionRegistry } = dependencies;
|
|
35
|
+
const config = dependencies.config || {};
|
|
36
|
+
const notificationPromises = [];
|
|
51
37
|
|
|
52
38
|
for (const subscription of subscriptions) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!firebaseUid) {
|
|
56
|
-
firebaseUid = await getFirebaseUidFromCid(subscription.userCid);
|
|
57
|
-
if (!firebaseUid) {
|
|
58
|
-
logger.log('WARN', `[processAlertForPI] Could not find Firebase UID for user CID ${subscription.userCid}, skipping notification`);
|
|
59
|
-
continue; // Skip this user if we can't find their Firebase UID
|
|
60
|
-
}
|
|
61
|
-
uidMapping[subscription.userCid] = firebaseUid;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const notificationId = `alert_${Date.now()}_${subscription.userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
65
|
-
const notificationRef = db.collection('user_notifications')
|
|
66
|
-
.doc(firebaseUid) // Use Firebase UID, not eToro CID
|
|
67
|
-
.collection('notifications')
|
|
68
|
-
.doc(notificationId);
|
|
39
|
+
const userCid = subscription.userCid;
|
|
40
|
+
const notificationId = `alert_${Date.now()}_${userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
69
41
|
|
|
70
42
|
const notificationData = {
|
|
71
43
|
id: notificationId,
|
|
72
|
-
type: '
|
|
44
|
+
type: 'watchlistAlerts', // Use watchlistAlerts type for watchlist-based alerts
|
|
45
|
+
subType: 'alert',
|
|
73
46
|
title: alertType.name,
|
|
74
47
|
message: alertMessage,
|
|
75
48
|
read: false,
|
|
49
|
+
timestamp: FieldValue.serverTimestamp(),
|
|
76
50
|
createdAt: FieldValue.serverTimestamp(),
|
|
77
|
-
timestamp: FieldValue.serverTimestamp(), // Also include timestamp for ordering
|
|
78
51
|
metadata: {
|
|
79
52
|
piCid: Number(piCid),
|
|
80
53
|
piUsername: piUsername,
|
|
@@ -85,54 +58,42 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
85
58
|
severity: alertType.severity,
|
|
86
59
|
watchlistId: subscription.watchlistId,
|
|
87
60
|
watchlistName: subscription.watchlistName,
|
|
61
|
+
notificationType: 'watchlistAlerts',
|
|
62
|
+
userCid: Number(userCid),
|
|
88
63
|
...(computationMetadata || {})
|
|
89
64
|
}
|
|
90
65
|
};
|
|
91
66
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 5. Update notification counters (using Firebase UIDs)
|
|
112
|
-
for (const [firebaseUid, counter] of Object.entries(counterUpdates)) {
|
|
113
|
-
const counterRef = db.collection('user_notifications')
|
|
114
|
-
.doc(firebaseUid) // Use Firebase UID, not eToro CID
|
|
115
|
-
.collection('counters')
|
|
116
|
-
.doc(counter.date);
|
|
67
|
+
// Use writeWithMigration to write to new path (with legacy fallback)
|
|
68
|
+
const writePromise = writeWithMigration(
|
|
69
|
+
db,
|
|
70
|
+
'signedInUsers',
|
|
71
|
+
'notifications',
|
|
72
|
+
{ cid: String(userCid), notificationId },
|
|
73
|
+
notificationData,
|
|
74
|
+
{
|
|
75
|
+
isCollection: false,
|
|
76
|
+
merge: false,
|
|
77
|
+
dataType: 'notifications',
|
|
78
|
+
config,
|
|
79
|
+
collectionRegistry
|
|
80
|
+
}
|
|
81
|
+
).catch(err => {
|
|
82
|
+
logger.log('WARN', `[processAlertForPI] Failed to write notification for CID ${userCid}: ${err.message}`);
|
|
83
|
+
});
|
|
117
84
|
|
|
118
|
-
|
|
119
|
-
date: counter.date,
|
|
120
|
-
unreadCount: FieldValue.increment(counter.unreadCount),
|
|
121
|
-
totalCount: FieldValue.increment(counter.totalCount),
|
|
122
|
-
[`byType.${alertType.id}`]: FieldValue.increment(counter.byType[alertType.id] || 0),
|
|
123
|
-
lastUpdated: FieldValue.serverTimestamp()
|
|
124
|
-
}, { merge: true });
|
|
85
|
+
notificationPromises.push(writePromise);
|
|
125
86
|
}
|
|
126
87
|
|
|
127
|
-
//
|
|
128
|
-
await
|
|
88
|
+
// Wait for all notifications to be written
|
|
89
|
+
await Promise.all(notificationPromises);
|
|
129
90
|
|
|
130
91
|
// 7. Update global rootdata collection for computation system
|
|
131
92
|
const { updateAlertHistoryRootData } = require('../../generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers');
|
|
132
93
|
const triggeredUserCids = subscriptions.map(s => s.userCid);
|
|
133
94
|
await updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids);
|
|
134
95
|
|
|
135
|
-
logger.log('SUCCESS', `[processAlertForPI] Created ${
|
|
96
|
+
logger.log('SUCCESS', `[processAlertForPI] Created ${notificationPromises.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
|
|
136
97
|
|
|
137
98
|
} catch (error) {
|
|
138
99
|
logger.log('ERROR', `[processAlertForPI] Error processing alert for PI ${piCid}`, error);
|
|
@@ -100,7 +100,8 @@ async function handleAlertTrigger(message, context, config, dependencies) {
|
|
|
100
100
|
piCid,
|
|
101
101
|
alertType,
|
|
102
102
|
piMetadata,
|
|
103
|
-
date
|
|
103
|
+
date,
|
|
104
|
+
dependencies
|
|
104
105
|
);
|
|
105
106
|
processedCount++;
|
|
106
107
|
} catch (error) {
|
|
@@ -116,7 +117,7 @@ async function handleAlertTrigger(message, context, config, dependencies) {
|
|
|
116
117
|
// This runs after all alert computations are processed
|
|
117
118
|
if (processedCount > 0 || results.cids.length > 0) {
|
|
118
119
|
// Check if any PIs were processed but didn't trigger alerts
|
|
119
|
-
await checkAndSendAllClearNotifications(db, logger, results.cids, date, config);
|
|
120
|
+
await checkAndSendAllClearNotifications(db, logger, results.cids, date, config, dependencies);
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
} catch (error) {
|
|
@@ -179,7 +180,7 @@ async function handleComputationResultWrite(change, context, config, dependencie
|
|
|
179
180
|
const docData = change.after.data();
|
|
180
181
|
const results = await readComputationResultsWithShards(db, docData, change.after.ref);
|
|
181
182
|
if (results.cids && results.cids.length > 0) {
|
|
182
|
-
await checkAndSendAllClearNotifications(db, logger, results.cids, date, config);
|
|
183
|
+
await checkAndSendAllClearNotifications(db, logger, results.cids, date, config, dependencies);
|
|
183
184
|
}
|
|
184
185
|
return; // Don't process as alert computation
|
|
185
186
|
}
|
|
@@ -221,7 +222,8 @@ async function handleComputationResultWrite(change, context, config, dependencie
|
|
|
221
222
|
piCid,
|
|
222
223
|
alertType,
|
|
223
224
|
piMetadata,
|
|
224
|
-
date
|
|
225
|
+
date,
|
|
226
|
+
dependencies
|
|
225
227
|
);
|
|
226
228
|
processedCount++;
|
|
227
229
|
} catch (error) {
|
|
@@ -244,7 +246,7 @@ async function handleComputationResultWrite(change, context, config, dependencie
|
|
|
244
246
|
* Send "all clear" notifications to users who have these PIs in their static watchlists
|
|
245
247
|
* This should be called after PopularInvestorProfileMetrics is computed (not just alert computations)
|
|
246
248
|
*/
|
|
247
|
-
async function checkAndSendAllClearNotifications(db, logger, processedPICids, computationDate, config) {
|
|
249
|
+
async function checkAndSendAllClearNotifications(db, logger, processedPICids, computationDate, config, dependencies = {}) {
|
|
248
250
|
try {
|
|
249
251
|
const today = computationDate || new Date().toISOString().split('T')[0];
|
|
250
252
|
|
|
@@ -275,7 +277,7 @@ async function checkAndSendAllClearNotifications(db, logger, processedPICids, co
|
|
|
275
277
|
const piCid = Number(item.cid);
|
|
276
278
|
if (processedPICids.includes(piCid)) {
|
|
277
279
|
// Check if this PI triggered any alerts today for this user
|
|
278
|
-
const hasAlerts = await checkIfPIHasAlertsToday(db, logger, userCid, piCid, today);
|
|
280
|
+
const hasAlerts = await checkIfPIHasAlertsToday(db, logger, userCid, piCid, today, dependencies);
|
|
279
281
|
|
|
280
282
|
if (!hasAlerts) {
|
|
281
283
|
// No alerts triggered - send "all clear" notification
|
|
@@ -296,7 +298,7 @@ async function checkAndSendAllClearNotifications(db, logger, processedPICids, co
|
|
|
296
298
|
let totalNotifications = 0;
|
|
297
299
|
for (const [userCid, pisMap] of usersToNotify.entries()) {
|
|
298
300
|
for (const [piCid, pi] of pisMap.entries()) {
|
|
299
|
-
await sendAllClearNotification(db, logger, userCid, pi.cid, pi.username, today);
|
|
301
|
+
await sendAllClearNotification(db, logger, userCid, pi.cid, pi.username, today, dependencies);
|
|
300
302
|
totalNotifications++;
|
|
301
303
|
}
|
|
302
304
|
}
|
|
@@ -313,17 +315,27 @@ async function checkAndSendAllClearNotifications(db, logger, processedPICids, co
|
|
|
313
315
|
|
|
314
316
|
/**
|
|
315
317
|
* Check if a PI has any alerts for a user today
|
|
316
|
-
* Checks
|
|
318
|
+
* Checks SignedInUsers/{cid}/notifications collection for alert-type notifications
|
|
317
319
|
*/
|
|
318
|
-
async function checkIfPIHasAlertsToday(db, logger, userCid, piCid, date) {
|
|
320
|
+
async function checkIfPIHasAlertsToday(db, logger, userCid, piCid, date, dependencies = {}) {
|
|
319
321
|
try {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
const { collectionRegistry } = dependencies;
|
|
323
|
+
const config = dependencies.config || {};
|
|
324
|
+
|
|
325
|
+
// Use collection registry to get notifications path
|
|
326
|
+
let notificationsPath;
|
|
327
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
328
|
+
notificationsPath = collectionRegistry.getCollectionPath('signedInUsers', 'notifications', { cid: String(userCid) });
|
|
329
|
+
} else {
|
|
330
|
+
// Fallback to legacy path
|
|
331
|
+
notificationsPath = `user_notifications/${userCid}/notifications`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const notificationsRef = db.collection(notificationsPath);
|
|
323
335
|
|
|
324
336
|
// Check if there are any alert notifications for this PI today
|
|
325
337
|
const snapshot = await notificationsRef
|
|
326
|
-
.where('type', '==', '
|
|
338
|
+
.where('type', '==', 'watchlistAlerts')
|
|
327
339
|
.where('metadata.piCid', '==', Number(piCid))
|
|
328
340
|
.where('metadata.computationDate', '==', date)
|
|
329
341
|
.limit(1)
|
|
@@ -342,59 +354,73 @@ async function checkIfPIHasAlertsToday(db, logger, userCid, piCid, date) {
|
|
|
342
354
|
/**
|
|
343
355
|
* Send an "all clear" notification to a user
|
|
344
356
|
*/
|
|
345
|
-
async function sendAllClearNotification(db, logger, userCid, piCid, piUsername, date) {
|
|
357
|
+
async function sendAllClearNotification(db, logger, userCid, piCid, piUsername, date, dependencies = {}) {
|
|
346
358
|
try {
|
|
347
359
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
360
|
+
const { collectionRegistry } = dependencies;
|
|
361
|
+
const config = dependencies.config || {};
|
|
362
|
+
|
|
363
|
+
// Use collection registry to get notifications path
|
|
364
|
+
let notificationsPath;
|
|
365
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
366
|
+
notificationsPath = collectionRegistry.getCollectionPath('signedInUsers', 'notifications', { cid: String(userCid) });
|
|
367
|
+
} else {
|
|
368
|
+
// Fallback to legacy path
|
|
369
|
+
notificationsPath = `user_notifications/${userCid}/notifications`;
|
|
370
|
+
}
|
|
348
371
|
|
|
349
372
|
// Check if we already sent an all-clear notification for this PI today
|
|
350
|
-
const
|
|
351
|
-
.
|
|
352
|
-
.
|
|
353
|
-
.where('piCid', '==', Number(piCid))
|
|
354
|
-
.where('
|
|
355
|
-
.
|
|
356
|
-
.
|
|
357
|
-
|
|
358
|
-
const existingSnapshot = await existingRef.get();
|
|
373
|
+
const existingSnapshot = await db.collection(notificationsPath)
|
|
374
|
+
.where('type', '==', 'watchlistAlerts')
|
|
375
|
+
.where('subType', '==', 'all_clear')
|
|
376
|
+
.where('metadata.piCid', '==', Number(piCid))
|
|
377
|
+
.where('metadata.computationDate', '==', date)
|
|
378
|
+
.limit(1)
|
|
379
|
+
.get();
|
|
380
|
+
|
|
359
381
|
if (!existingSnapshot.empty) {
|
|
360
382
|
// Already sent notification for this PI today
|
|
361
383
|
return;
|
|
362
384
|
}
|
|
363
385
|
|
|
364
|
-
// Create the notification using
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
await notificationRef.set({
|
|
371
|
-
id: notificationRef.id,
|
|
372
|
-
type: 'alert',
|
|
386
|
+
// Create the notification using writeWithMigration
|
|
387
|
+
const notificationId = `all_clear_${Date.now()}_${userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
388
|
+
const notificationData = {
|
|
389
|
+
id: notificationId,
|
|
390
|
+
type: 'watchlistAlerts',
|
|
391
|
+
subType: 'all_clear',
|
|
373
392
|
title: 'All Clear',
|
|
374
393
|
message: `${piUsername} was processed, all clear today!`,
|
|
375
|
-
piCid: Number(piCid),
|
|
376
|
-
piUsername: piUsername,
|
|
377
|
-
alertType: 'all_clear',
|
|
378
|
-
computationDate: date,
|
|
379
394
|
read: false,
|
|
395
|
+
timestamp: FieldValue.serverTimestamp(),
|
|
380
396
|
createdAt: FieldValue.serverTimestamp(),
|
|
381
397
|
metadata: {
|
|
398
|
+
piCid: Number(piCid),
|
|
399
|
+
piUsername: piUsername,
|
|
400
|
+
alertType: 'all_clear',
|
|
401
|
+
computationDate: date,
|
|
402
|
+
notificationType: 'watchlistAlerts',
|
|
403
|
+
userCid: Number(userCid),
|
|
382
404
|
message: 'User was processed, all clear today!'
|
|
383
405
|
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
//
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Use writeWithMigration to write to new path (with legacy fallback)
|
|
409
|
+
const { writeWithMigration } = require('../generic-api/user-api/helpers/core/path_resolution_helpers');
|
|
410
|
+
await writeWithMigration(
|
|
411
|
+
db,
|
|
412
|
+
'signedInUsers',
|
|
413
|
+
'notifications',
|
|
414
|
+
{ cid: String(userCid), notificationId },
|
|
415
|
+
notificationData,
|
|
416
|
+
{
|
|
417
|
+
isCollection: false,
|
|
418
|
+
merge: false,
|
|
419
|
+
dataType: 'notifications',
|
|
420
|
+
config,
|
|
421
|
+
collectionRegistry
|
|
422
|
+
}
|
|
423
|
+
);
|
|
398
424
|
|
|
399
425
|
logger.log('INFO', `[sendAllClearNotification] Sent all-clear notification to user ${userCid} for PI ${piCid}`);
|
|
400
426
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
7
|
const { getAllAlertTypes, getAlertTypeByComputation } = require('../../../../alert-system/helpers/alert_type_registry');
|
|
8
8
|
const { isDeveloperAccount, getDevOverride } = require('../dev/dev_helpers');
|
|
9
|
+
const { getCidFromFirebaseUid } = require('../core/path_resolution_helpers');
|
|
10
|
+
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* POST /user/dev/test-alert
|
|
@@ -22,7 +24,7 @@ const { isDeveloperAccount, getDevOverride } = require('../dev/dev_helpers');
|
|
|
22
24
|
* }
|
|
23
25
|
*/
|
|
24
26
|
async function sendTestAlert(req, res, dependencies, config) {
|
|
25
|
-
const { db, logger } = dependencies;
|
|
27
|
+
const { db, logger, collectionRegistry } = dependencies;
|
|
26
28
|
const { userCid, alertTypeId, targetUsers = 'dev', piCid = 1, piUsername = 'TestPI', metadata = {} } = req.body;
|
|
27
29
|
|
|
28
30
|
if (!userCid) {
|
|
@@ -59,21 +61,8 @@ async function sendTestAlert(req, res, dependencies, config) {
|
|
|
59
61
|
logger.log('INFO', `[sendTestAlert] Using default alert type: ${alertType.id}`);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
const signedInUsersSnapshot = await db.collection('signedInUsers')
|
|
65
|
-
.where('etoroCID', '==', Number(etoroCid))
|
|
66
|
-
.limit(1)
|
|
67
|
-
.get();
|
|
68
|
-
|
|
69
|
-
if (!signedInUsersSnapshot.empty) {
|
|
70
|
-
return signedInUsersSnapshot.docs[0].id; // Firebase UID is the document ID
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Determine target users (as Firebase UIDs)
|
|
76
|
-
let targetFirebaseUids = [];
|
|
64
|
+
// Determine target users (as eToro CIDs)
|
|
65
|
+
let targetCids = [];
|
|
77
66
|
|
|
78
67
|
if (targetUsers === 'all') {
|
|
79
68
|
// Get all users from signedInUsers collection (who have etoroCID)
|
|
@@ -81,8 +70,10 @@ async function sendTestAlert(req, res, dependencies, config) {
|
|
|
81
70
|
.where('etoroCID', '!=', null)
|
|
82
71
|
.get();
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
targetCids = signedInUsersSnapshot.docs
|
|
74
|
+
.map(doc => doc.data().etoroCID)
|
|
75
|
+
.filter(cid => cid != null);
|
|
76
|
+
logger.log('INFO', `[sendTestAlert] Sending to all ${targetCids.length} users`);
|
|
86
77
|
} else if (targetUsers === 'dev') {
|
|
87
78
|
// Get all developer accounts with dev override enabled
|
|
88
79
|
const devOverridesCollection = config.devOverridesCollection || 'dev_overrides';
|
|
@@ -101,31 +92,12 @@ async function sendTestAlert(req, res, dependencies, config) {
|
|
|
101
92
|
devCids.push(Number(userCid));
|
|
102
93
|
}
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const firebaseUid = await getFirebaseUidFromCid(cid);
|
|
107
|
-
if (firebaseUid) {
|
|
108
|
-
targetFirebaseUids.push(firebaseUid);
|
|
109
|
-
} else {
|
|
110
|
-
logger.log('WARN', `[sendTestAlert] Could not find Firebase UID for developer CID ${cid}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
logger.log('INFO', `[sendTestAlert] Sending to ${targetFirebaseUids.length} developer accounts`);
|
|
95
|
+
targetCids = devCids;
|
|
96
|
+
logger.log('INFO', `[sendTestAlert] Sending to ${targetCids.length} developer accounts`);
|
|
115
97
|
} else if (Array.isArray(targetUsers)) {
|
|
116
|
-
// Specific user CIDs
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for (const cid of specificCids) {
|
|
120
|
-
const firebaseUid = await getFirebaseUidFromCid(cid);
|
|
121
|
-
if (firebaseUid) {
|
|
122
|
-
targetFirebaseUids.push(firebaseUid);
|
|
123
|
-
} else {
|
|
124
|
-
logger.log('WARN', `[sendTestAlert] Could not find Firebase UID for CID ${cid}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
logger.log('INFO', `[sendTestAlert] Sending to ${targetFirebaseUids.length} specific users (from ${specificCids.length} CIDs)`);
|
|
98
|
+
// Specific user CIDs
|
|
99
|
+
targetCids = targetUsers.map(cid => Number(cid)).filter(cid => !isNaN(cid) && cid > 0);
|
|
100
|
+
logger.log('INFO', `[sendTestAlert] Sending to ${targetCids.length} specific users`);
|
|
129
101
|
} else {
|
|
130
102
|
return res.status(400).json({
|
|
131
103
|
error: "Invalid targetUsers",
|
|
@@ -133,7 +105,7 @@ async function sendTestAlert(req, res, dependencies, config) {
|
|
|
133
105
|
});
|
|
134
106
|
}
|
|
135
107
|
|
|
136
|
-
if (
|
|
108
|
+
if (targetCids.length === 0) {
|
|
137
109
|
return res.status(400).json({
|
|
138
110
|
error: "No target users",
|
|
139
111
|
message: "No users found matching the target criteria"
|
|
@@ -149,27 +121,22 @@ async function sendTestAlert(req, res, dependencies, config) {
|
|
|
149
121
|
testSentAt: new Date().toISOString()
|
|
150
122
|
});
|
|
151
123
|
|
|
152
|
-
// Create notifications for each target user (using
|
|
153
|
-
const
|
|
154
|
-
const notificationRefs = [];
|
|
155
|
-
const counterUpdates = {};
|
|
124
|
+
// Create notifications for each target user (using eToro CIDs and collection registry)
|
|
125
|
+
const notificationPromises = [];
|
|
156
126
|
const today = new Date().toISOString().split('T')[0];
|
|
157
127
|
|
|
158
|
-
for (const
|
|
159
|
-
const notificationId = `test_alert_${Date.now()}_${
|
|
160
|
-
const notificationRef = db.collection('user_notifications')
|
|
161
|
-
.doc(firebaseUid) // Use Firebase UID, not eToro CID
|
|
162
|
-
.collection('notifications')
|
|
163
|
-
.doc(notificationId);
|
|
128
|
+
for (const targetCid of targetCids) {
|
|
129
|
+
const notificationId = `test_alert_${Date.now()}_${targetCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
164
130
|
|
|
165
131
|
const notificationData = {
|
|
166
132
|
id: notificationId,
|
|
167
|
-
type: '
|
|
133
|
+
type: 'testAlerts', // Use testAlerts type for test notifications
|
|
134
|
+
subType: 'alert',
|
|
168
135
|
title: `[TEST] ${alertType.name}`,
|
|
169
136
|
message: alertMessage,
|
|
170
137
|
read: false,
|
|
138
|
+
timestamp: FieldValue.serverTimestamp(),
|
|
171
139
|
createdAt: FieldValue.serverTimestamp(),
|
|
172
|
-
timestamp: FieldValue.serverTimestamp(), // Also include timestamp for ordering
|
|
173
140
|
metadata: {
|
|
174
141
|
piCid: Number(piCid),
|
|
175
142
|
piUsername: piUsername,
|
|
@@ -181,64 +148,54 @@ async function sendTestAlert(req, res, dependencies, config) {
|
|
|
181
148
|
isTest: true,
|
|
182
149
|
testSentBy: Number(userCid),
|
|
183
150
|
testSentAt: new Date().toISOString(),
|
|
151
|
+
notificationType: 'testAlerts',
|
|
152
|
+
userCid: Number(targetCid),
|
|
184
153
|
...metadata
|
|
185
154
|
}
|
|
186
155
|
};
|
|
187
156
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// Update notification counters (using Firebase UIDs)
|
|
207
|
-
for (const [firebaseUid, counter] of Object.entries(counterUpdates)) {
|
|
208
|
-
const counterRef = db.collection('user_notifications')
|
|
209
|
-
.doc(firebaseUid) // Use Firebase UID, not eToro CID
|
|
210
|
-
.collection('counters')
|
|
211
|
-
.doc(counter.date);
|
|
157
|
+
// Use writeWithMigration to write to new path (with legacy fallback)
|
|
158
|
+
const writePromise = writeWithMigration(
|
|
159
|
+
db,
|
|
160
|
+
'signedInUsers',
|
|
161
|
+
'notifications',
|
|
162
|
+
{ cid: String(targetCid), notificationId },
|
|
163
|
+
notificationData,
|
|
164
|
+
{
|
|
165
|
+
isCollection: false,
|
|
166
|
+
merge: false,
|
|
167
|
+
dataType: 'notifications',
|
|
168
|
+
config,
|
|
169
|
+
collectionRegistry
|
|
170
|
+
}
|
|
171
|
+
).catch(err => {
|
|
172
|
+
logger.log('WARN', `[sendTestAlert] Failed to write notification for CID ${targetCid}: ${err.message}`);
|
|
173
|
+
});
|
|
212
174
|
|
|
213
|
-
|
|
214
|
-
date: counter.date,
|
|
215
|
-
unreadCount: FieldValue.increment(counter.unreadCount),
|
|
216
|
-
totalCount: FieldValue.increment(counter.totalCount),
|
|
217
|
-
[`byType.${alertType.id}`]: FieldValue.increment(counter.byType[alertType.id] || 0),
|
|
218
|
-
lastUpdated: FieldValue.serverTimestamp()
|
|
219
|
-
}, { merge: true });
|
|
175
|
+
notificationPromises.push(writePromise);
|
|
220
176
|
}
|
|
221
177
|
|
|
222
|
-
//
|
|
223
|
-
await
|
|
178
|
+
// Wait for all notifications to be written
|
|
179
|
+
await Promise.all(notificationPromises);
|
|
224
180
|
|
|
225
|
-
|
|
181
|
+
const successCount = notificationPromises.length;
|
|
182
|
+
logger.log('SUCCESS', `[sendTestAlert] Created ${successCount} test notifications for alert type ${alertType.id}`);
|
|
226
183
|
|
|
227
184
|
return res.status(200).json({
|
|
228
185
|
success: true,
|
|
229
|
-
message: `Test alert sent to ${
|
|
186
|
+
message: `Test alert sent to ${targetCids.length} users`,
|
|
230
187
|
alertType: {
|
|
231
188
|
id: alertType.id,
|
|
232
189
|
name: alertType.name,
|
|
233
190
|
computationName: alertType.computationName
|
|
234
191
|
},
|
|
235
192
|
targetUsers: {
|
|
236
|
-
count:
|
|
237
|
-
|
|
193
|
+
count: targetCids.length,
|
|
194
|
+
cids: targetCids
|
|
238
195
|
},
|
|
239
196
|
piCid: Number(piCid),
|
|
240
197
|
piUsername: piUsername,
|
|
241
|
-
notificationsCreated:
|
|
198
|
+
notificationsCreated: successCount
|
|
242
199
|
});
|
|
243
200
|
|
|
244
201
|
} catch (error) {
|
|
@@ -406,6 +406,7 @@ async function getNotificationHistory(req, res, dependencies, config) {
|
|
|
406
406
|
notificationsPath = `user_notifications/${userCid}/notifications`;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
// Try to query from new path first
|
|
409
410
|
let query = db.collection(notificationsPath)
|
|
410
411
|
.orderBy('timestamp', 'desc')
|
|
411
412
|
.limit(parseInt(limit));
|
|
@@ -418,29 +419,57 @@ async function getNotificationHistory(req, res, dependencies, config) {
|
|
|
418
419
|
query = query.where('read', '==', read === 'true');
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
|
|
422
|
+
let snapshot;
|
|
423
|
+
try {
|
|
424
|
+
snapshot = await query.get();
|
|
425
|
+
} catch (queryError) {
|
|
426
|
+
// If query fails (e.g., missing index), try without filters
|
|
427
|
+
logger.log('WARN', `[getNotificationHistory] Query failed, trying without filters: ${queryError.message}`);
|
|
428
|
+
query = db.collection(notificationsPath)
|
|
429
|
+
.orderBy('timestamp', 'desc')
|
|
430
|
+
.limit(parseInt(limit));
|
|
431
|
+
snapshot = await query.get();
|
|
432
|
+
}
|
|
433
|
+
|
|
422
434
|
const notifications = [];
|
|
423
435
|
|
|
424
436
|
snapshot.forEach(doc => {
|
|
425
437
|
const data = doc.data();
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
438
|
+
|
|
439
|
+
// Apply filters in memory if query filters failed
|
|
440
|
+
let include = true;
|
|
441
|
+
if (type && data.type !== type) {
|
|
442
|
+
include = false;
|
|
443
|
+
}
|
|
444
|
+
if (read !== undefined && data.read !== (read === 'true')) {
|
|
445
|
+
include = false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (include) {
|
|
449
|
+
notifications.push({
|
|
450
|
+
id: doc.id,
|
|
451
|
+
type: data.type || 'other',
|
|
452
|
+
subType: data.subType,
|
|
453
|
+
title: data.title || '',
|
|
454
|
+
message: data.message || '',
|
|
455
|
+
read: data.read || false,
|
|
456
|
+
timestamp: data.timestamp?.toDate?.()?.toISOString() || data.createdAt?.toDate?.()?.toISOString() || new Date().toISOString(),
|
|
457
|
+
metadata: data.metadata || {}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
436
460
|
});
|
|
437
461
|
|
|
462
|
+
// Apply offset in memory
|
|
463
|
+
const offsetNum = parseInt(offset);
|
|
464
|
+
const paginatedNotifications = notifications.slice(offsetNum, offsetNum + parseInt(limit));
|
|
465
|
+
|
|
438
466
|
return res.status(200).json({
|
|
439
467
|
success: true,
|
|
440
|
-
notifications,
|
|
441
|
-
count:
|
|
468
|
+
notifications: paginatedNotifications,
|
|
469
|
+
count: paginatedNotifications.length,
|
|
470
|
+
total: notifications.length,
|
|
442
471
|
limit: parseInt(limit),
|
|
443
|
-
offset:
|
|
472
|
+
offset: offsetNum
|
|
444
473
|
});
|
|
445
474
|
} catch (error) {
|
|
446
475
|
logger.log('ERROR', `[getNotificationHistory] Error fetching notifications for ${userCid}`, error);
|