bulltrackers-module 1.0.523 → 1.0.525
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 +38 -73
- package/functions/alert-system/index.js +79 -50
- package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +54 -93
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +15 -4
- package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +48 -16
- 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,46 @@ 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
|
-
|
|
112
|
-
|
|
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
|
+
// notifications is a subcollection, so we need isCollection: true and documentId
|
|
69
|
+
const writePromise = writeWithMigration(
|
|
70
|
+
db,
|
|
71
|
+
'signedInUsers',
|
|
72
|
+
'notifications',
|
|
73
|
+
{ cid: String(userCid) },
|
|
74
|
+
notificationData,
|
|
75
|
+
{
|
|
76
|
+
isCollection: true,
|
|
77
|
+
merge: false,
|
|
78
|
+
dataType: 'notifications',
|
|
79
|
+
documentId: notificationId,
|
|
80
|
+
dualWrite: false, // Disable dual write - we're fully migrated to new path
|
|
81
|
+
config,
|
|
82
|
+
collectionRegistry
|
|
83
|
+
}
|
|
84
|
+
).catch(err => {
|
|
85
|
+
logger.log('ERROR', `[processAlertForPI] Failed to write notification for CID ${userCid}: ${err.message}`, err);
|
|
86
|
+
throw err; // Re-throw so we know if writes are failing
|
|
87
|
+
});
|
|
117
88
|
|
|
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 });
|
|
89
|
+
notificationPromises.push(writePromise);
|
|
125
90
|
}
|
|
126
91
|
|
|
127
|
-
//
|
|
128
|
-
await
|
|
92
|
+
// Wait for all notifications to be written
|
|
93
|
+
await Promise.all(notificationPromises);
|
|
129
94
|
|
|
130
95
|
// 7. Update global rootdata collection for computation system
|
|
131
96
|
const { updateAlertHistoryRootData } = require('../../generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers');
|
|
132
97
|
const triggeredUserCids = subscriptions.map(s => s.userCid);
|
|
133
98
|
await updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids);
|
|
134
99
|
|
|
135
|
-
logger.log('SUCCESS', `[processAlertForPI] Created ${
|
|
100
|
+
logger.log('SUCCESS', `[processAlertForPI] Created ${notificationPromises.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
|
|
136
101
|
|
|
137
102
|
} catch (error) {
|
|
138
103
|
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,76 @@ 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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Use writeWithMigration to write to new path (with legacy fallback)
|
|
409
|
+
// notifications is a subcollection, so we need isCollection: true and documentId
|
|
410
|
+
const { writeWithMigration } = require('../generic-api/user-api/helpers/core/path_resolution_helpers');
|
|
411
|
+
await writeWithMigration(
|
|
412
|
+
db,
|
|
413
|
+
'signedInUsers',
|
|
414
|
+
'notifications',
|
|
415
|
+
{ cid: String(userCid) },
|
|
416
|
+
notificationData,
|
|
417
|
+
{
|
|
418
|
+
isCollection: true,
|
|
419
|
+
merge: false,
|
|
420
|
+
dataType: 'notifications',
|
|
421
|
+
documentId: notificationId,
|
|
422
|
+
dualWrite: false, // Disable dual write - we're fully migrated to new path
|
|
423
|
+
config,
|
|
424
|
+
collectionRegistry
|
|
425
|
+
}
|
|
426
|
+
);
|
|
398
427
|
|
|
399
428
|
logger.log('INFO', `[sendAllClearNotification] Sent all-clear notification to user ${userCid} for PI ${piCid}`);
|
|
400
429
|
|
|
@@ -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,58 @@ 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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
// notifications is a subcollection, so we need isCollection: true and documentId
|
|
159
|
+
const writePromise = writeWithMigration(
|
|
160
|
+
db,
|
|
161
|
+
'signedInUsers',
|
|
162
|
+
'notifications',
|
|
163
|
+
{ cid: String(targetCid) },
|
|
164
|
+
notificationData,
|
|
165
|
+
{
|
|
166
|
+
isCollection: true,
|
|
167
|
+
merge: false,
|
|
168
|
+
dataType: 'notifications',
|
|
169
|
+
documentId: notificationId,
|
|
170
|
+
dualWrite: false, // Disable dual write - we're fully migrated to new path
|
|
171
|
+
config,
|
|
172
|
+
collectionRegistry
|
|
173
|
+
}
|
|
174
|
+
).catch(err => {
|
|
175
|
+
logger.log('ERROR', `[sendTestAlert] Failed to write notification for CID ${targetCid}: ${err.message}`, err);
|
|
176
|
+
throw err; // Re-throw so we know if writes are failing
|
|
177
|
+
});
|
|
212
178
|
|
|
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 });
|
|
179
|
+
notificationPromises.push(writePromise);
|
|
220
180
|
}
|
|
221
181
|
|
|
222
|
-
//
|
|
223
|
-
await
|
|
182
|
+
// Wait for all notifications to be written
|
|
183
|
+
await Promise.all(notificationPromises);
|
|
224
184
|
|
|
225
|
-
|
|
185
|
+
const successCount = notificationPromises.length;
|
|
186
|
+
logger.log('SUCCESS', `[sendTestAlert] Created ${successCount} test notifications for alert type ${alertType.id}`);
|
|
226
187
|
|
|
227
188
|
return res.status(200).json({
|
|
228
189
|
success: true,
|
|
229
|
-
message: `Test alert sent to ${
|
|
190
|
+
message: `Test alert sent to ${targetCids.length} users`,
|
|
230
191
|
alertType: {
|
|
231
192
|
id: alertType.id,
|
|
232
193
|
name: alertType.name,
|
|
233
194
|
computationName: alertType.computationName
|
|
234
195
|
},
|
|
235
196
|
targetUsers: {
|
|
236
|
-
count:
|
|
237
|
-
|
|
197
|
+
count: targetCids.length,
|
|
198
|
+
cids: targetCids
|
|
238
199
|
},
|
|
239
200
|
piCid: Number(piCid),
|
|
240
201
|
piUsername: piUsername,
|
|
241
|
-
notificationsCreated:
|
|
202
|
+
notificationsCreated: successCount
|
|
242
203
|
});
|
|
243
204
|
|
|
244
205
|
} catch (error) {
|
|
@@ -469,10 +469,21 @@ async function writeWithMigration(db, category, subcategory, params, data, optio
|
|
|
469
469
|
// Get legacy path if dual write is enabled
|
|
470
470
|
let legacyPath = null;
|
|
471
471
|
if (dualWrite && dataType && userCid) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
legacyPath =
|
|
472
|
+
try {
|
|
473
|
+
const registryFuncs = getRegistryFunctions(registryOptions);
|
|
474
|
+
const resolve = registryFuncs?.resolvePath;
|
|
475
|
+
legacyPath = getLegacyPath(dataType, userCid, config, params, null, null, registryOptions);
|
|
476
|
+
if (legacyPath && resolve && typeof resolve === 'function') {
|
|
477
|
+
legacyPath = resolve(legacyPath, params);
|
|
478
|
+
} else if (legacyPath) {
|
|
479
|
+
// If resolve is not available, manually replace placeholders
|
|
480
|
+
for (const [key, value] of Object.entries(params)) {
|
|
481
|
+
legacyPath = legacyPath.replace(`{${key}}`, String(value));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} catch (error) {
|
|
485
|
+
console.warn('[writeWithMigration] Could not resolve legacy path, skipping dual write:', error.message);
|
|
486
|
+
legacyPath = null; // Skip dual write if we can't resolve legacy path
|
|
476
487
|
}
|
|
477
488
|
}
|
|
478
489
|
|
|
@@ -127,17 +127,20 @@ async function sendOnDemandNotification(db, logger, userCid, type, title, messag
|
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
// Write using collection registry
|
|
130
|
+
// notifications is a subcollection, so we need isCollection: true and documentId
|
|
130
131
|
if (collectionRegistry) {
|
|
131
132
|
await writeWithMigration(
|
|
132
133
|
db,
|
|
133
134
|
'signedInUsers',
|
|
134
135
|
'notifications',
|
|
135
|
-
{ cid: String(userCid)
|
|
136
|
+
{ cid: String(userCid) },
|
|
136
137
|
notificationData,
|
|
137
138
|
{
|
|
138
|
-
isCollection:
|
|
139
|
+
isCollection: true,
|
|
139
140
|
merge: false,
|
|
140
141
|
dataType: 'notifications',
|
|
142
|
+
documentId: notificationId,
|
|
143
|
+
dualWrite: false, // Disable dual write - we're fully migrated to new path
|
|
141
144
|
config,
|
|
142
145
|
collectionRegistry
|
|
143
146
|
}
|
|
@@ -406,6 +409,7 @@ async function getNotificationHistory(req, res, dependencies, config) {
|
|
|
406
409
|
notificationsPath = `user_notifications/${userCid}/notifications`;
|
|
407
410
|
}
|
|
408
411
|
|
|
412
|
+
// Try to query from new path first
|
|
409
413
|
let query = db.collection(notificationsPath)
|
|
410
414
|
.orderBy('timestamp', 'desc')
|
|
411
415
|
.limit(parseInt(limit));
|
|
@@ -418,29 +422,57 @@ async function getNotificationHistory(req, res, dependencies, config) {
|
|
|
418
422
|
query = query.where('read', '==', read === 'true');
|
|
419
423
|
}
|
|
420
424
|
|
|
421
|
-
|
|
425
|
+
let snapshot;
|
|
426
|
+
try {
|
|
427
|
+
snapshot = await query.get();
|
|
428
|
+
} catch (queryError) {
|
|
429
|
+
// If query fails (e.g., missing index), try without filters
|
|
430
|
+
logger.log('WARN', `[getNotificationHistory] Query failed, trying without filters: ${queryError.message}`);
|
|
431
|
+
query = db.collection(notificationsPath)
|
|
432
|
+
.orderBy('timestamp', 'desc')
|
|
433
|
+
.limit(parseInt(limit));
|
|
434
|
+
snapshot = await query.get();
|
|
435
|
+
}
|
|
436
|
+
|
|
422
437
|
const notifications = [];
|
|
423
438
|
|
|
424
439
|
snapshot.forEach(doc => {
|
|
425
440
|
const data = doc.data();
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
441
|
+
|
|
442
|
+
// Apply filters in memory if query filters failed
|
|
443
|
+
let include = true;
|
|
444
|
+
if (type && data.type !== type) {
|
|
445
|
+
include = false;
|
|
446
|
+
}
|
|
447
|
+
if (read !== undefined && data.read !== (read === 'true')) {
|
|
448
|
+
include = false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (include) {
|
|
452
|
+
notifications.push({
|
|
453
|
+
id: doc.id,
|
|
454
|
+
type: data.type || 'other',
|
|
455
|
+
subType: data.subType,
|
|
456
|
+
title: data.title || '',
|
|
457
|
+
message: data.message || '',
|
|
458
|
+
read: data.read || false,
|
|
459
|
+
timestamp: data.timestamp?.toDate?.()?.toISOString() || data.createdAt?.toDate?.()?.toISOString() || new Date().toISOString(),
|
|
460
|
+
metadata: data.metadata || {}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
436
463
|
});
|
|
437
464
|
|
|
465
|
+
// Apply offset in memory
|
|
466
|
+
const offsetNum = parseInt(offset);
|
|
467
|
+
const paginatedNotifications = notifications.slice(offsetNum, offsetNum + parseInt(limit));
|
|
468
|
+
|
|
438
469
|
return res.status(200).json({
|
|
439
470
|
success: true,
|
|
440
|
-
notifications,
|
|
441
|
-
count:
|
|
471
|
+
notifications: paginatedNotifications,
|
|
472
|
+
count: paginatedNotifications.length,
|
|
473
|
+
total: notifications.length,
|
|
442
474
|
limit: parseInt(limit),
|
|
443
|
-
offset:
|
|
475
|
+
offset: offsetNum
|
|
444
476
|
});
|
|
445
477
|
} catch (error) {
|
|
446
478
|
logger.log('ERROR', `[getNotificationHistory] Error fetching notifications for ${userCid}`, error);
|