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.
@@ -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(String(subscription.userCid))
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[subscription.userCid]) {
71
- counterUpdates[subscription.userCid] = {
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[subscription.userCid].unreadCount += 1;
79
- counterUpdates[subscription.userCid].totalCount += 1;
80
- counterUpdates[subscription.userCid].byType[alertType.id] =
81
- (counterUpdates[subscription.userCid].byType[alertType.id] || 0) + 1;
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 [userCid, counter] of Object.entries(counterUpdates)) {
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(String(userCid))
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
- logger.log('WARN', `[Sweep] ๐Ÿงน Forcing ${tasksPayload.length} tasks to HIGH-MEM for ${date}.`);
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
- forensics.ledgerState = { status: data.status, workerId: data.workerId, error: data.error };
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
- When triggering a manual execution in the **GCP Workflows Console**, use the following JSON objects in the **Input** field to bypass the schedule and test specific components.
3
+ Below is a quick-reference guide for manually triggering the **Data Feeder Pipeline** using the Google Cloud Console UI.
8
4
 
9
- ### How to Run a Test
5
+ ## How to Run a Test
10
6
 
11
- 1. Go to the **Workflows** page in your GCP Console.
12
- 2. Click on `data-feeder-pipeline`.
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 relevant **JSON Input** from the table below into the input box.
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
- | Component to Test | JSON Input | Description |
19
- | --- | --- | --- |
20
- | **Market Data** | `{"target_step": "market"}` | Runs `price-fetcher`, `insights-fetcher`, and `market-data-indexer`. |
21
- | **Rankings** | `{"target_step": "rankings"}` | Runs the `fetch-popular-investors` function specifically. |
22
- | **Social Orchestrator** | `{"target_step": "social"}` | Runs the midnight social task and the midnight data indexer. |
23
- | **Global Sync** | `{"target_step": "global"}` | Triggers the `root-data-indexer` with no target date (Full Scan). |
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 for Testing
23
+ ### ๐Ÿ’ก Testing Notes & Pro-Tips
29
24
 
30
- * **Variable Checking:** After an execution finishes, click the **Details** tab in the execution logs to see the final values of variables like `sleep_midnight` or `sleep_loop` to verify the UTC alignment math worked correctly.
31
- * **Permissions:** Ensure your user account or the service account running the workflow has the `roles/workflows.invoker` role to trigger these tests.
32
- * **Timeouts:** If testing the full loop, remember that the workflow will "pause" at the `sys.sleep` steps. You can see the status as **Active** while it waits for the next 3-hour window.
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
- if (data.fetchedDates && data.fetchedDates[dateStr] === true) {
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
- if (data.fetchedDates && data.fetchedDates[dateStr] === true) {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.493",
3
+ "version": "1.0.495",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [