bulltrackers-module 1.0.493 โ 1.0.495
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 -11
- package/functions/computation-system/helpers/computation_dispatcher.js +21 -1
- package/functions/computation-system/tools/FinalSweepReporter.js +10 -1
- package/functions/computation-system/workflows/datafeederpipelineinstructions.md +18 -20
- package/functions/generic-api/user-api/helpers/test_alert_helpers.js +251 -0
- package/functions/generic-api/user-api/index.js +2 -0
- package/functions/root-data-indexer/index.js +16 -2
- package/package.json +1 -1
|
@@ -29,15 +29,41 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
29
29
|
// 3. Generate alert message
|
|
30
30
|
const alertMessage = generateAlertMessage(alertType, piUsername, computationMetadata);
|
|
31
31
|
|
|
32
|
+
// Helper function to get Firebase UID from eToro CID
|
|
33
|
+
async function getFirebaseUidFromCid(etoroCid) {
|
|
34
|
+
const signedInUsersSnapshot = await db.collection('signedInUsers')
|
|
35
|
+
.where('etoroCID', '==', Number(etoroCid))
|
|
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
|
+
|
|
32
45
|
// 4. Create notifications for each subscribed user (using user_notifications collection)
|
|
46
|
+
// Convert eToro CIDs to Firebase UIDs first
|
|
33
47
|
const batch = db.batch();
|
|
34
48
|
const notificationRefs = [];
|
|
35
49
|
const counterUpdates = {};
|
|
50
|
+
const uidMapping = {}; // Cache for CID -> UID mappings
|
|
36
51
|
|
|
37
52
|
for (const subscription of subscriptions) {
|
|
53
|
+
// Get Firebase UID for this user's eToro CID
|
|
54
|
+
let firebaseUid = uidMapping[subscription.userCid];
|
|
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
|
+
|
|
38
64
|
const notificationId = `alert_${Date.now()}_${subscription.userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
39
65
|
const notificationRef = db.collection('user_notifications')
|
|
40
|
-
.doc(
|
|
66
|
+
.doc(firebaseUid) // Use Firebase UID, not eToro CID
|
|
41
67
|
.collection('notifications')
|
|
42
68
|
.doc(notificationId);
|
|
43
69
|
|
|
@@ -48,6 +74,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
48
74
|
message: alertMessage,
|
|
49
75
|
read: false,
|
|
50
76
|
createdAt: FieldValue.serverTimestamp(),
|
|
77
|
+
timestamp: FieldValue.serverTimestamp(), // Also include timestamp for ordering
|
|
51
78
|
metadata: {
|
|
52
79
|
piCid: Number(piCid),
|
|
53
80
|
piUsername: piUsername,
|
|
@@ -65,26 +92,26 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
65
92
|
batch.set(notificationRef, notificationData);
|
|
66
93
|
notificationRefs.push(notificationRef);
|
|
67
94
|
|
|
68
|
-
// Track counter updates
|
|
95
|
+
// Track counter updates (using Firebase UID as key)
|
|
69
96
|
const dateKey = computationDate || new Date().toISOString().split('T')[0];
|
|
70
|
-
if (!counterUpdates[
|
|
71
|
-
counterUpdates[
|
|
97
|
+
if (!counterUpdates[firebaseUid]) {
|
|
98
|
+
counterUpdates[firebaseUid] = {
|
|
72
99
|
date: dateKey,
|
|
73
100
|
unreadCount: 0,
|
|
74
101
|
totalCount: 0,
|
|
75
102
|
byType: {}
|
|
76
103
|
};
|
|
77
104
|
}
|
|
78
|
-
counterUpdates[
|
|
79
|
-
counterUpdates[
|
|
80
|
-
counterUpdates[
|
|
81
|
-
(counterUpdates[
|
|
105
|
+
counterUpdates[firebaseUid].unreadCount += 1;
|
|
106
|
+
counterUpdates[firebaseUid].totalCount += 1;
|
|
107
|
+
counterUpdates[firebaseUid].byType[alertType.id] =
|
|
108
|
+
(counterUpdates[firebaseUid].byType[alertType.id] || 0) + 1;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
// 5. Update notification counters
|
|
85
|
-
for (const [
|
|
111
|
+
// 5. Update notification counters (using Firebase UIDs)
|
|
112
|
+
for (const [firebaseUid, counter] of Object.entries(counterUpdates)) {
|
|
86
113
|
const counterRef = db.collection('user_notifications')
|
|
87
|
-
.doc(
|
|
114
|
+
.doc(firebaseUid) // Use Firebase UID, not eToro CID
|
|
88
115
|
.collection('counters')
|
|
89
116
|
.doc(counter.date);
|
|
90
117
|
|
|
@@ -345,7 +345,13 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
|
|
|
345
345
|
};
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
-
|
|
348
|
+
const taskNames = tasksPayload.map(t => t.computation || t.name).join(', ');
|
|
349
|
+
logger.log('WARN', `[Sweep] ๐งน Forcing ${tasksPayload.length} tasks to HIGH-MEM for ${date}.`, {
|
|
350
|
+
date: date,
|
|
351
|
+
pass: passToRun,
|
|
352
|
+
tasks: tasksPayload.map(t => ({ name: t.computation || t.name, reason: 'sweep' })),
|
|
353
|
+
topic: config.computationTopicHighMem || 'computation-tasks-highmem'
|
|
354
|
+
});
|
|
349
355
|
|
|
350
356
|
await pubsubUtils.batchPublishTasks(dependencies, {
|
|
351
357
|
topicName: config.computationTopicHighMem || 'computation-tasks-highmem',
|
|
@@ -503,6 +509,13 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
503
509
|
|
|
504
510
|
const pubPromises = [];
|
|
505
511
|
if (standardTasks.length > 0) {
|
|
512
|
+
const taskNames = standardTasks.map(t => t.computation || t.name).join(', ');
|
|
513
|
+
logger.log('INFO', `[Dispatcher] ๐ค Dispatching ${standardTasks.length} standard tasks: ${taskNames}`, {
|
|
514
|
+
date: selectedDate,
|
|
515
|
+
pass: passToRun,
|
|
516
|
+
tasks: standardTasks.map(t => ({ name: t.computation || t.name, reason: t.triggerReason || 'new' })),
|
|
517
|
+
topic: config.computationTopicStandard || 'computation-tasks'
|
|
518
|
+
});
|
|
506
519
|
pubPromises.push(pubsubUtils.batchPublishTasks(dependencies, {
|
|
507
520
|
topicName: config.computationTopicStandard || 'computation-tasks',
|
|
508
521
|
tasks: standardTasks,
|
|
@@ -510,6 +523,13 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
510
523
|
}));
|
|
511
524
|
}
|
|
512
525
|
if (highMemTasks.length > 0) {
|
|
526
|
+
const taskNames = highMemTasks.map(t => t.computation || t.name).join(', ');
|
|
527
|
+
logger.log('INFO', `[Dispatcher] ๐ค Dispatching ${highMemTasks.length} high-memory tasks: ${taskNames}`, {
|
|
528
|
+
date: selectedDate,
|
|
529
|
+
pass: passToRun,
|
|
530
|
+
tasks: highMemTasks.map(t => ({ name: t.computation || t.name, reason: t.triggerReason || 'retry' })),
|
|
531
|
+
topic: config.computationTopicHighMem || 'computation-tasks-highmem'
|
|
532
|
+
});
|
|
513
533
|
pubPromises.push(pubsubUtils.batchPublishTasks(dependencies, {
|
|
514
534
|
topicName: config.computationTopicHighMem || 'computation-tasks-highmem',
|
|
515
535
|
tasks: highMemTasks,
|
|
@@ -164,7 +164,16 @@ class FinalSweepReporter {
|
|
|
164
164
|
|
|
165
165
|
if (ledgerSnap.exists) {
|
|
166
166
|
const data = ledgerSnap.data();
|
|
167
|
-
|
|
167
|
+
// Filter out undefined values to prevent Firestore errors
|
|
168
|
+
const ledgerState = {
|
|
169
|
+
status: data.status,
|
|
170
|
+
workerId: data.workerId
|
|
171
|
+
};
|
|
172
|
+
// Only include error if it's defined
|
|
173
|
+
if (data.error !== undefined && data.error !== null) {
|
|
174
|
+
ledgerState.error = data.error;
|
|
175
|
+
}
|
|
176
|
+
forensics.ledgerState = ledgerState;
|
|
168
177
|
|
|
169
178
|
if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
|
|
170
179
|
const lastHb = data.telemetry?.lastHeartbeat ? new Date(data.telemetry.lastHeartbeat).getTime() : 0;
|
|
@@ -1,32 +1,30 @@
|
|
|
1
|
-
Below is a quick-reference guide for testing the **Data Feeder Pipeline** using the Google Cloud Console UI.
|
|
2
|
-
|
|
3
|
-
---
|
|
4
|
-
|
|
5
1
|
# ๐งช Workflow Testing Guide
|
|
6
2
|
|
|
7
|
-
|
|
3
|
+
Below is a quick-reference guide for manually triggering the **Data Feeder Pipeline** using the Google Cloud Console UI.
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
## How to Run a Test
|
|
10
6
|
|
|
11
|
-
1.
|
|
12
|
-
2.
|
|
7
|
+
1. Navigate to **Workflows** in your GCP Console.
|
|
8
|
+
2. Select `data-feeder-pipeline`.
|
|
13
9
|
3. Click the **Execute** button at the top.
|
|
14
|
-
4. Paste the
|
|
10
|
+
4. Paste the specific **JSON Input** from the table below into the input box to bypass schedules and target specific phases.
|
|
15
11
|
|
|
16
12
|
### Test Commands
|
|
17
13
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
| **Market Data** | `{"target_step": "market"}` | Runs
|
|
21
|
-
| **
|
|
22
|
-
| **Social
|
|
23
|
-
| **
|
|
24
|
-
| **Full Pipeline** | `{}` | Runs the entire 24-hour cycle from the beginning (Standard Run). |
|
|
14
|
+
| Phase to Test | JSON Input | Action & Description |
|
|
15
|
+
| :--- | :--- | :--- |
|
|
16
|
+
| **Market Data (Phase 1)** | `{"target_step": "market"}` | **Runs:** Price Fetcher & Insights Fetcher.<br>**Note:** Automatically triggers indexing after each fetch. Workflow will pause at "Wait for Midnight" upon completion. |
|
|
17
|
+
| **Midnight Phase (Phase 3)** | `{"target_step": "midnight"}` | **Runs:** Popular Investor Rankings, Midnight Social Orchestrator, and Global Index Verification.<br>**Note:** Use this to test the critical 00:00 UTC logic without waiting for the daily schedule. |
|
|
18
|
+
| **Social Loop (Phase 4)** | `{"target_step": "social"}` | **Runs:** Enters the recurring 3-hour social fetch loop.<br>**Warning:** Triggers `social_loop_start`, which begins with a 3-hour sleep (`wait_3_hours`) before the first execution. |
|
|
19
|
+
| **Standard Run** | `{}` | **Runs:** The full 24-hour cycle starting from 22:00 UTC (Market Close). |
|
|
25
20
|
|
|
26
21
|
---
|
|
27
22
|
|
|
28
|
-
### ๐ก Pro-Tips
|
|
23
|
+
### ๐ก Testing Notes & Pro-Tips
|
|
29
24
|
|
|
30
|
-
* **
|
|
31
|
-
* **
|
|
32
|
-
*
|
|
25
|
+
* **Automatic Indexing:** As of V3.2, you will not see explicit "Index" steps in the workflow visualization for Price, Insights, or Rankings. These functions now trigger the `root-data-indexer` automatically upon completion. The only visible index step is the **Global Verification** in the Midnight Phase.
|
|
26
|
+
* **Verification:** To confirm data was indexed during a test:
|
|
27
|
+
* Check the logs of the individual Cloud Functions (`price-fetcher`, etc.).
|
|
28
|
+
* Or, run the **Midnight Phase** test, which ends with the explicit `global_index_midnight` step.
|
|
29
|
+
* **Variable Checking:** After execution, check the **Variables** tab to view `sleep_midnight` calculations to ensure UTC alignment is functioning correctly.
|
|
30
|
+
* **Permissions:** Ensure the executor has `roles/workflows.invoker`.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Test Alert Helpers
|
|
3
|
+
* Allows developers to send test alerts for testing the alert system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
|
+
const { getAllAlertTypes, getAlertTypeByComputation } = require('../../../alert-system/helpers/alert_type_registry');
|
|
8
|
+
const { isDeveloperAccount, getDevOverride } = require('./dev_helpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* POST /user/dev/test-alert
|
|
12
|
+
* Send a test alert to users
|
|
13
|
+
*
|
|
14
|
+
* Request body:
|
|
15
|
+
* {
|
|
16
|
+
* userCid: number (required) - Developer account CID
|
|
17
|
+
* alertTypeId: string (optional) - Alert type ID, defaults to first available
|
|
18
|
+
* targetUsers: 'all' | 'dev' | number[] (optional) - Who to send to, defaults to 'dev'
|
|
19
|
+
* piCid: number (optional) - PI CID for the alert, defaults to 1
|
|
20
|
+
* piUsername: string (optional) - PI username, defaults to 'TestPI'
|
|
21
|
+
* metadata: object (optional) - Additional metadata for the alert
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
async function sendTestAlert(req, res, dependencies, config) {
|
|
25
|
+
const { db, logger } = dependencies;
|
|
26
|
+
const { userCid, alertTypeId, targetUsers = 'dev', piCid = 1, piUsername = 'TestPI', metadata = {} } = req.body;
|
|
27
|
+
|
|
28
|
+
if (!userCid) {
|
|
29
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// SECURITY CHECK: Only allow developer accounts
|
|
33
|
+
if (!isDeveloperAccount(userCid)) {
|
|
34
|
+
logger.log('WARN', `[sendTestAlert] Unauthorized attempt by user ${userCid}`);
|
|
35
|
+
return res.status(403).json({
|
|
36
|
+
error: "Forbidden",
|
|
37
|
+
message: "Test alerts are only available for authorized developer accounts"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Get alert type
|
|
43
|
+
let alertType;
|
|
44
|
+
if (alertTypeId) {
|
|
45
|
+
alertType = getAlertTypeByComputation(alertTypeId);
|
|
46
|
+
if (!alertType) {
|
|
47
|
+
const allTypes = getAllAlertTypes();
|
|
48
|
+
alertType = allTypes.find(t => t.id === alertTypeId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Default to first available alert type if not specified
|
|
53
|
+
if (!alertType) {
|
|
54
|
+
const allTypes = getAllAlertTypes();
|
|
55
|
+
if (allTypes.length === 0) {
|
|
56
|
+
return res.status(400).json({ error: "No alert types available" });
|
|
57
|
+
}
|
|
58
|
+
alertType = allTypes[0];
|
|
59
|
+
logger.log('INFO', `[sendTestAlert] Using default alert type: ${alertType.id}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Helper function to get Firebase UID from eToro CID
|
|
63
|
+
async function getFirebaseUidFromCid(etoroCid) {
|
|
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 = [];
|
|
77
|
+
|
|
78
|
+
if (targetUsers === 'all') {
|
|
79
|
+
// Get all users from signedInUsers collection (who have etoroCID)
|
|
80
|
+
const signedInUsersSnapshot = await db.collection('signedInUsers')
|
|
81
|
+
.where('etoroCID', '!=', null)
|
|
82
|
+
.get();
|
|
83
|
+
|
|
84
|
+
targetFirebaseUids = signedInUsersSnapshot.docs.map(doc => doc.id);
|
|
85
|
+
logger.log('INFO', `[sendTestAlert] Sending to all ${targetFirebaseUids.length} users`);
|
|
86
|
+
} else if (targetUsers === 'dev') {
|
|
87
|
+
// Get all developer accounts with dev override enabled
|
|
88
|
+
const devOverridesCollection = config.devOverridesCollection || 'dev_overrides';
|
|
89
|
+
const devOverridesSnapshot = await db.collection(devOverridesCollection).get();
|
|
90
|
+
|
|
91
|
+
const devCids = [];
|
|
92
|
+
for (const doc of devOverridesSnapshot.docs) {
|
|
93
|
+
const data = doc.data();
|
|
94
|
+
if (data.enabled === true) {
|
|
95
|
+
devCids.push(Number(doc.id));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Also include the requesting developer
|
|
100
|
+
if (!devCids.includes(Number(userCid))) {
|
|
101
|
+
devCids.push(Number(userCid));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Convert eToro CIDs to Firebase UIDs
|
|
105
|
+
for (const cid of devCids) {
|
|
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`);
|
|
115
|
+
} else if (Array.isArray(targetUsers)) {
|
|
116
|
+
// Specific user CIDs - convert to Firebase UIDs
|
|
117
|
+
const specificCids = targetUsers.map(cid => Number(cid)).filter(cid => !isNaN(cid) && cid > 0);
|
|
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)`);
|
|
129
|
+
} else {
|
|
130
|
+
return res.status(400).json({
|
|
131
|
+
error: "Invalid targetUsers",
|
|
132
|
+
message: "targetUsers must be 'all', 'dev', or an array of user CIDs"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (targetFirebaseUids.length === 0) {
|
|
137
|
+
return res.status(400).json({
|
|
138
|
+
error: "No target users",
|
|
139
|
+
message: "No users found matching the target criteria"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Generate alert message
|
|
144
|
+
const { generateAlertMessage } = require('../../../alert-system/helpers/alert_type_registry');
|
|
145
|
+
const alertMessage = generateAlertMessage(alertType, piUsername, {
|
|
146
|
+
...metadata,
|
|
147
|
+
isTest: true,
|
|
148
|
+
testSentBy: Number(userCid),
|
|
149
|
+
testSentAt: new Date().toISOString()
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Create notifications for each target user (using Firebase UIDs)
|
|
153
|
+
const batch = db.batch();
|
|
154
|
+
const notificationRefs = [];
|
|
155
|
+
const counterUpdates = {};
|
|
156
|
+
const today = new Date().toISOString().split('T')[0];
|
|
157
|
+
|
|
158
|
+
for (const firebaseUid of targetFirebaseUids) {
|
|
159
|
+
const notificationId = `test_alert_${Date.now()}_${firebaseUid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
160
|
+
const notificationRef = db.collection('user_notifications')
|
|
161
|
+
.doc(firebaseUid) // Use Firebase UID, not eToro CID
|
|
162
|
+
.collection('notifications')
|
|
163
|
+
.doc(notificationId);
|
|
164
|
+
|
|
165
|
+
const notificationData = {
|
|
166
|
+
id: notificationId,
|
|
167
|
+
type: 'alert',
|
|
168
|
+
title: `[TEST] ${alertType.name}`,
|
|
169
|
+
message: alertMessage,
|
|
170
|
+
read: false,
|
|
171
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
172
|
+
timestamp: FieldValue.serverTimestamp(), // Also include timestamp for ordering
|
|
173
|
+
metadata: {
|
|
174
|
+
piCid: Number(piCid),
|
|
175
|
+
piUsername: piUsername,
|
|
176
|
+
alertType: alertType.id,
|
|
177
|
+
alertTypeName: alertType.name,
|
|
178
|
+
computationName: alertType.computationName,
|
|
179
|
+
computationDate: today,
|
|
180
|
+
severity: alertType.severity,
|
|
181
|
+
isTest: true,
|
|
182
|
+
testSentBy: Number(userCid),
|
|
183
|
+
testSentAt: new Date().toISOString(),
|
|
184
|
+
...metadata
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
batch.set(notificationRef, notificationData);
|
|
189
|
+
notificationRefs.push(notificationRef);
|
|
190
|
+
|
|
191
|
+
// Track counter updates (using Firebase UID as key)
|
|
192
|
+
if (!counterUpdates[firebaseUid]) {
|
|
193
|
+
counterUpdates[firebaseUid] = {
|
|
194
|
+
date: today,
|
|
195
|
+
unreadCount: 0,
|
|
196
|
+
totalCount: 0,
|
|
197
|
+
byType: {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
counterUpdates[firebaseUid].unreadCount += 1;
|
|
201
|
+
counterUpdates[firebaseUid].totalCount += 1;
|
|
202
|
+
counterUpdates[firebaseUid].byType[alertType.id] =
|
|
203
|
+
(counterUpdates[firebaseUid].byType[alertType.id] || 0) + 1;
|
|
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);
|
|
212
|
+
|
|
213
|
+
batch.set(counterRef, {
|
|
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 });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Commit batch
|
|
223
|
+
await batch.commit();
|
|
224
|
+
|
|
225
|
+
logger.log('SUCCESS', `[sendTestAlert] Created ${notificationRefs.length} test notifications for alert type ${alertType.id}`);
|
|
226
|
+
|
|
227
|
+
return res.status(200).json({
|
|
228
|
+
success: true,
|
|
229
|
+
message: `Test alert sent to ${targetFirebaseUids.length} users`,
|
|
230
|
+
alertType: {
|
|
231
|
+
id: alertType.id,
|
|
232
|
+
name: alertType.name,
|
|
233
|
+
computationName: alertType.computationName
|
|
234
|
+
},
|
|
235
|
+
targetUsers: {
|
|
236
|
+
count: targetFirebaseUids.length,
|
|
237
|
+
firebaseUids: targetFirebaseUids
|
|
238
|
+
},
|
|
239
|
+
piCid: Number(piCid),
|
|
240
|
+
piUsername: piUsername,
|
|
241
|
+
notificationsCreated: notificationRefs.length
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
} catch (error) {
|
|
245
|
+
logger.log('ERROR', `[sendTestAlert] Error sending test alert:`, error);
|
|
246
|
+
return res.status(500).json({ error: error.message });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = { sendTestAlert };
|
|
251
|
+
|
|
@@ -12,6 +12,7 @@ const { setDevOverride, getDevOverrideStatus } = require('./helpers/dev_helpers'
|
|
|
12
12
|
const { getAlertTypes, getDynamicWatchlistComputations, getUserAlerts, getAlertCount, markAlertRead, markAllAlertsRead, deleteAlert } = require('./helpers/alert_helpers');
|
|
13
13
|
const { requestPiFetch, getPiFetchStatus } = require('./helpers/on_demand_fetch_helpers');
|
|
14
14
|
const { requestUserSync, getUserSyncStatus } = require('./helpers/user_sync_helpers');
|
|
15
|
+
const { sendTestAlert } = require('./helpers/test_alert_helpers');
|
|
15
16
|
|
|
16
17
|
module.exports = (dependencies, config) => {
|
|
17
18
|
const router = express.Router();
|
|
@@ -87,6 +88,7 @@ module.exports = (dependencies, config) => {
|
|
|
87
88
|
// --- Developer Mode (only for whitelisted developer accounts) ---
|
|
88
89
|
router.post('/dev/override', (req, res) => setDevOverride(req, res, dependencies, config));
|
|
89
90
|
router.get('/dev/override', (req, res) => getDevOverrideStatus(req, res, dependencies, config));
|
|
91
|
+
router.post('/dev/test-alert', (req, res) => sendTestAlert(req, res, dependencies, config));
|
|
90
92
|
|
|
91
93
|
// --- Alert Management ---
|
|
92
94
|
router.get('/me/alert-types', (req, res) => getAlertTypes(req, res, dependencies, config));
|
|
@@ -270,19 +270,33 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
270
270
|
]);
|
|
271
271
|
|
|
272
272
|
// Check if date exists in tracking documents
|
|
273
|
+
// The _dates document uses dot notation: fetchedDates.2025-12-29: true
|
|
274
|
+
// When read, this becomes: { fetchedDates: { "2025-12-29": true } }
|
|
273
275
|
let foundPISocial = false;
|
|
274
276
|
let foundSignedInSocial = false;
|
|
275
277
|
|
|
276
278
|
if (piSocialTrackingDoc.exists) {
|
|
277
279
|
const data = piSocialTrackingDoc.data();
|
|
278
|
-
|
|
280
|
+
// Check both nested structure and flat dot-notation structure
|
|
281
|
+
if (data.fetchedDates && typeof data.fetchedDates === 'object') {
|
|
282
|
+
if (data.fetchedDates[dateStr] === true) {
|
|
283
|
+
foundPISocial = true;
|
|
284
|
+
}
|
|
285
|
+
} else if (data[`fetchedDates.${dateStr}`] === true) {
|
|
286
|
+
// Handle flat dot-notation structure (if Firestore stores it that way)
|
|
279
287
|
foundPISocial = true;
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
290
|
|
|
283
291
|
if (signedInSocialTrackingDoc.exists) {
|
|
284
292
|
const data = signedInSocialTrackingDoc.data();
|
|
285
|
-
|
|
293
|
+
// Check both nested structure and flat dot-notation structure
|
|
294
|
+
if (data.fetchedDates && typeof data.fetchedDates === 'object') {
|
|
295
|
+
if (data.fetchedDates[dateStr] === true) {
|
|
296
|
+
foundSignedInSocial = true;
|
|
297
|
+
}
|
|
298
|
+
} else if (data[`fetchedDates.${dateStr}`] === true) {
|
|
299
|
+
// Handle flat dot-notation structure (if Firestore stores it that way)
|
|
286
300
|
foundSignedInSocial = true;
|
|
287
301
|
}
|
|
288
302
|
}
|