bulltrackers-module 1.0.127 → 1.0.129

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.
@@ -12,231 +12,164 @@
12
12
  * @param {string} dateString - The date in YYYY-MM-DD format.
13
13
  * @returns {Promise<Firestore.DocumentReference[]>} An array of DocumentReferences.
14
14
  */
15
- async function getPortfolioPartRefs(config, dependencies, dateString) {
16
- // <<< FIX: Destructure all dependencies here, inside the function >>>
17
- const { db, logger, calculationUtils } = dependencies;
15
+ /** --- Data Loader Sub-Pipes (Stateless, Dependency-Injection) --- */
16
+
17
+ /** Stage 1: Get portfolio part document references for a given date */
18
+ async function getPortfolioPartRefs(config, deps, dateString) {
19
+ const { db, logger, calculationUtils } = deps;
18
20
  const { withRetry } = calculationUtils;
19
- // <<< END FIX >>>
20
21
 
21
22
  logger.log('INFO', `Getting portfolio part references for date: ${dateString}`);
22
23
  const allPartRefs = [];
23
24
  const collectionsToQuery = [config.normalUserPortfolioCollection, config.speculatorPortfolioCollection];
24
25
 
25
26
  for (const collectionName of collectionsToQuery) {
26
- const blockDocsQuery = db.collection(collectionName); // Use db
27
- const blockDocRefs = await withRetry(
28
- () => blockDocsQuery.listDocuments(),
29
- `listDocuments(${collectionName})`
30
- );
31
-
32
- if (blockDocRefs.length === 0) {
33
- logger.log('WARN', `No block documents found in collection: ${collectionName}`);
34
- continue;
35
- }
36
-
37
- for (const blockDocRef of blockDocRefs) {
27
+ const blockDocsQuery = db.collection(collectionName);
28
+ const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
29
+ if (!blockDocRefs.length) { logger.log('WARN', `No block documents in ${collectionName}`); continue; }
30
+
31
+ // --- START MODIFICATION ---
32
+ // Run all "listDocuments" calls in parallel instead of a sequential loop
33
+ const partsPromises = blockDocRefs.map(blockDocRef => {
38
34
  const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection).doc(dateString).collection(config.partsSubcollection);
39
- const partDocs = await withRetry(
40
- () => partsCollectionRef.listDocuments(),
41
- `listDocuments(${partsCollectionRef.path})`
42
- );
35
+ // Each call is individually retried
36
+ return withRetry(() => partsCollectionRef.listDocuments(), `listDocuments(${partsCollectionRef.path})`);
37
+ });
38
+
39
+ // Wait for all parallel queries to finish
40
+ const partDocArrays = await Promise.all(partsPromises);
41
+
42
+ // Flatten the arrays of arrays into the final list
43
+ partDocArrays.forEach(partDocs => {
43
44
  allPartRefs.push(...partDocs);
44
- }
45
+ });
46
+ // --- END MODIFICATION ---
45
47
  }
46
48
 
47
- logger.log('INFO', `Found ${allPartRefs.length} part document references for ${dateString}.`);
49
+ logger.log('INFO', `Found ${allPartRefs.length} portfolio part refs for ${dateString}`);
48
50
  return allPartRefs;
49
51
  }
50
52
 
51
- /**
52
- * Sub-pipe: pipe.computationSystem.dataLoader.loadDataByRefs
53
- * @param {object} config - The computation system configuration object.
54
- * @param {object} dependencies - Contains db, logger, calculationUtils.
55
- * @param {Firestore.DocumentReference[]} refs - An array of DocumentReferences to load.
56
- * @returns {Promise<object>} A single map of { [userId]: portfolioData }.
57
- */
58
- async function loadDataByRefs(config, dependencies, refs) {
59
- // <<< FIX: Destructure all dependencies here, inside the function >>>
60
- const { db, logger, calculationUtils } = dependencies;
53
+ /** Stage 2: Load data from an array of document references */
54
+ async function loadDataByRefs(config, deps, refs) {
55
+ const { db, logger, calculationUtils } = deps;
61
56
  const { withRetry } = calculationUtils;
62
- // <<< END FIX >>>
63
57
 
64
- if (!refs || refs.length === 0) { return {}; }
58
+ if (!refs || !refs.length) return {};
65
59
  const mergedPortfolios = {};
66
60
  const batchSize = config.partRefBatchSize || 50;
67
61
 
68
62
  for (let i = 0; i < refs.length; i += batchSize) {
69
63
  const batchRefs = refs.slice(i, i + batchSize);
70
- // Use db from dependencies
71
- const snapshots = await withRetry(
72
- () => db.getAll(...batchRefs),
73
- `getAll(batch ${Math.floor(i / batchSize)})`
74
- );
64
+ const snapshots = await withRetry(() => db.getAll(...batchRefs), `getAll(batch ${Math.floor(i / batchSize)})`);
65
+
75
66
  for (const doc of snapshots) {
76
- if (doc.exists) {
77
- const data = doc.data();
78
- if (data && typeof data === 'object') {
79
- Object.assign(mergedPortfolios, data);
80
- } else {
81
- logger.log('WARN', `Document ${doc.id} exists but data is not an object. Data:`, data);
82
- }
83
- }
67
+ if (!doc.exists) continue;
68
+ const data = doc.data();
69
+ if (data && typeof data === 'object') Object.assign(mergedPortfolios, data);
70
+ else logger.log('WARN', `Doc ${doc.id} exists but data is not an object`, data);
84
71
  }
85
72
  }
73
+
86
74
  return mergedPortfolios;
87
75
  }
88
76
 
89
- /**
90
- * Sub-pipe: pipe.computationSystem.dataLoader.loadFullDayMap
91
- * @param {object} config - The computation system configuration object.
92
- * @param {object} dependencies - Contains db, logger.
93
- * @param {Firestore.DocumentReference[]} partRefs - Array of part document references.
94
- * @returns {Promise<object>} A single map of { [userId]: portfolioData }.
95
- */
96
- async function loadFullDayMap(config, dependencies, partRefs) {
97
- // <<< FIX: Destructure only what's needed for this specific function >>>
98
- const { logger } = dependencies;
99
- // <<< END FIX >>>
77
+ /** Stage 3: Load a full day map by delegating to loadDataByRefs */
78
+ async function loadFullDayMap(config, deps, partRefs) {
79
+ const { logger } = deps;
80
+ if (!partRefs.length) return {};
100
81
 
101
- if (partRefs.length === 0) return {};
102
82
  logger.log('TRACE', `Loading full day map from ${partRefs.length} references...`);
103
-
104
- // Pass config and dependencies to sub-pipe
105
- const fullMap = await loadDataByRefs(config, dependencies, partRefs);
106
- logger.log('TRACE', `Full day map loaded with ${Object.keys(fullMap).length} users.`);
83
+ const fullMap = await loadDataByRefs(config, deps, partRefs);
84
+ logger.log('TRACE', `Full day map loaded with ${Object.keys(fullMap).length} users`);
107
85
  return fullMap;
108
86
  }
109
87
 
110
- /**
111
- * Sub-pipe: pipe.computationSystem.dataLoader.loadDailyInsights
112
- * Fetches the daily instrument insights document for a specific date.
113
- * @param {object} config - The computation system configuration object.
114
- * @param {object} dependencies - Contains db, logger, calculationUtils.
115
- * @param {string} dateString - The date in YYYY-MM-DD format.
116
- * @returns {Promise<object|null>} The insights data object or null if not found/error.
117
- */
118
- async function loadDailyInsights(config, dependencies, dateString) {
119
- // <<< FIX: Destructure all dependencies here, inside the function >>>
120
- const { db, logger, calculationUtils } = dependencies;
88
+ /** Stage 4: Load daily instrument insights */
89
+ async function loadDailyInsights(config, deps, dateString) {
90
+ const { db, logger, calculationUtils } = deps;
121
91
  const { withRetry } = calculationUtils;
122
- // <<< END FIX >>>
123
92
 
124
- const insightsCollectionName = config.insightsCollectionName || 'daily_instrument_insights'; // Use config or default
125
- logger.log('INFO', `Loading daily insights for date: ${dateString} from ${insightsCollectionName}`);
93
+ const insightsCollectionName = config.insightsCollectionName || 'daily_instrument_insights';
94
+ logger.log('INFO', `Loading daily insights for ${dateString} from ${insightsCollectionName}`);
95
+
126
96
  try {
127
97
  const docRef = db.collection(insightsCollectionName).doc(dateString);
128
98
  const docSnap = await withRetry(() => docRef.get(), `getInsights(${dateString})`);
129
-
130
- if (!docSnap.exists) {
131
- logger.log('WARN', `Daily insights document not found for ${dateString}`);
132
- return null;
133
- }
134
- logger.log('TRACE', `Successfully loaded insights for ${dateString}.`);
99
+ if (!docSnap.exists) { logger.log('WARN', `Insights not found for ${dateString}`); return null; }
100
+ logger.log('TRACE', `Successfully loaded insights for ${dateString}`);
135
101
  return docSnap.data();
136
102
  } catch (error) {
137
103
  logger.log('ERROR', `Failed to load daily insights for ${dateString}`, { errorMessage: error.message });
138
- return null; // Return null on error to allow computations to proceed partially if possible
104
+ return null;
139
105
  }
140
106
  }
141
107
 
142
- /**
143
- * --- NEW ---
144
- * Sub-pipe: pipe.computationSystem.dataLoader.loadDailySocialPostInsights
145
- * Fetches all analyzed social post documents for a specific date.
146
- * @param {object} config - The computation system configuration object.
147
- * @param {object} dependencies - Contains db, logger, calculationUtils.
148
- * @param {string} dateString - The date in YYYY-MM-DD format.
149
- * @returns {Promise<object|null>} An object map of { [postId]: postData } or null.
150
- */
151
- async function loadDailySocialPostInsights(config, dependencies, dateString) {
152
- // <<< FIX: Destructure all dependencies here, inside the function >>>
153
- const { db, logger, calculationUtils } = dependencies;
108
+ /** Stage 5: Load daily social post insights */
109
+ async function loadDailySocialPostInsights(config, deps, dateString) {
110
+ const { db, logger, calculationUtils } = deps;
154
111
  const { withRetry } = calculationUtils;
155
- // <<< END FIX >>>
156
112
 
157
- // Use the new config property, or fall back to a default
158
- const socialInsightsCollectionName = config.socialInsightsCollectionName || 'daily_social_insights';
159
- logger.log('INFO', `Loading social post insights for date: ${dateString} from ${socialInsightsCollectionName}`);
160
-
113
+ const collectionName = config.socialInsightsCollectionName || 'daily_social_insights';
114
+ logger.log('INFO', `Loading social post insights for ${dateString} from ${collectionName}`);
115
+
161
116
  try {
162
- const postsCollectionRef = db.collection(socialInsightsCollectionName).doc(dateString).collection('posts');
117
+ const postsCollectionRef = db.collection(collectionName).doc(dateString).collection('posts');
163
118
  const querySnapshot = await withRetry(() => postsCollectionRef.get(), `getSocialPosts(${dateString})`);
119
+ if (querySnapshot.empty) { logger.log('WARN', `No social post insights for ${dateString}`); return null; }
164
120
 
165
- if (querySnapshot.empty) {
166
- logger.log('WARN', `No social post insights found for ${dateString}.`);
167
- return null;
168
- }
169
-
170
121
  const postsMap = {};
171
- querySnapshot.forEach(doc => {
172
- postsMap[doc.id] = doc.data();
173
- });
174
-
175
- logger.log('TRACE', `Successfully loaded ${Object.keys(postsMap).length} social post insights for ${dateString}.`);
122
+ querySnapshot.forEach(doc => { postsMap[doc.id] = doc.data(); });
123
+ logger.log('TRACE', `Loaded ${Object.keys(postsMap).length} social post insights`);
176
124
  return postsMap;
177
-
178
125
  } catch (error) {
179
126
  logger.log('ERROR', `Failed to load social post insights for ${dateString}`, { errorMessage: error.message });
180
127
  return null;
181
128
  }
182
129
  }
183
- // --- END NEW ---
184
130
 
185
-
186
-
187
- /**
188
- * --- NEW ---
189
- * Sub-pipe: pipe.computationSystem.dataLoader.getHistoryPartRefs
190
- * @param {object} config - The computation system configuration object.
191
- * @param {object} dependencies - Contains db, logger, calculationUtils.
192
- * @param {string} dateString - The date in YYYY-MM-DD format.
193
- * @returns {Promise<Firestore.DocumentReference[]>} An array of DocumentReferences.
194
- */
195
- async function getHistoryPartRefs(config, dependencies, dateString) {
196
- const { db, logger, calculationUtils } = dependencies;
131
+ /** Stage 6: Get history part references for a given date */
132
+ async function getHistoryPartRefs(config, deps, dateString) {
133
+ const { db, logger, calculationUtils } = deps;
197
134
  const { withRetry } = calculationUtils;
198
135
 
199
- logger.log('INFO', `Getting history part references for date: ${dateString}`);
136
+ logger.log('INFO', `Getting history part references for ${dateString}`);
200
137
  const allPartRefs = [];
201
- // --- MODIFIED: Use new history collection config keys ---
202
138
  const collectionsToQuery = [config.normalUserHistoryCollection, config.speculatorHistoryCollection];
203
139
 
204
140
  for (const collectionName of collectionsToQuery) {
205
- if (!collectionName) { // Add a check in case config is missing
206
- logger.log('WARN', `History collection name is undefined. Skipping.`);
207
- continue;
208
- }
141
+ if (!collectionName) { logger.log('WARN', `History collection undefined. Skipping.`); continue; }
209
142
  const blockDocsQuery = db.collection(collectionName);
210
- const blockDocRefs = await withRetry(
211
- () => blockDocsQuery.listDocuments(),
212
- `listDocuments(${collectionName})`
213
- );
214
-
215
- if (blockDocRefs.length === 0) {
216
- logger.log('WARN', `No block documents found in collection: ${collectionName}`);
217
- continue;
218
- }
143
+ const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
144
+ if (!blockDocRefs.length) { logger.log('WARN', `No block documents in ${collectionName}`); continue; }
219
145
 
220
- for (const blockDocRef of blockDocRefs) {
146
+ // --- START MODIFICATION ---
147
+ // Run all "listDocuments" calls in parallel instead of a sequential loop
148
+ const partsPromises = blockDocRefs.map(blockDocRef => {
221
149
  const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection).doc(dateString).collection(config.partsSubcollection);
222
- const partDocs = await withRetry(
223
- () => partsCollectionRef.listDocuments(),
224
- `listDocuments(${partsCollectionRef.path})`
225
- );
150
+ // Each call is individually retried
151
+ return withRetry(() => partsCollectionRef.listDocuments(), `listDocuments(${partsCollectionRef.path})`);
152
+ });
153
+
154
+ // Wait for all parallel queries to finish
155
+ const partDocArrays = await Promise.all(partsPromises);
156
+
157
+ // Flatten the arrays of arrays into the final list
158
+ partDocArrays.forEach(partDocs => {
226
159
  allPartRefs.push(...partDocs);
227
- }
160
+ });
161
+ // --- END MODIFICATION ---
228
162
  }
229
163
 
230
- logger.log('INFO', `Found ${allPartRefs.length} history part document references for ${dateString}.`);
164
+ logger.log('INFO', `Found ${allPartRefs.length} history part refs for ${dateString}`);
231
165
  return allPartRefs;
232
166
  }
233
167
 
234
-
235
168
  module.exports = {
236
169
  getPortfolioPartRefs,
237
170
  loadDataByRefs,
238
171
  loadFullDayMap,
239
172
  loadDailyInsights,
240
- loadDailySocialPostInsights,
241
- getHistoryPartRefs, // <-- EXPORT NEW FUNCTION
173
+ loadDailySocialPostInsights,
174
+ getHistoryPartRefs,
242
175
  };
@@ -3,56 +3,37 @@
3
3
  * REFACTORED: Now stateless and receive dependencies where needed.
4
4
  * DYNAMIC: Categorization logic is removed, replaced by manifest.
5
5
  */
6
+ /** --- Computation System Sub-Pipes & Utils (Stateless) --- */
6
7
 
7
8
  const { FieldValue, FieldPath } = require('@google-cloud/firestore');
8
9
 
9
- /**
10
- * Normalizes a calculation name to kebab-case.
11
- * @param {string} name
12
- * @returns {string}
13
- */
10
+ /** Stage 1: Normalize a calculation name to kebab-case */
14
11
  function normalizeName(name) {
15
12
  return name.replace(/_/g, '-');
16
13
  }
17
14
 
18
- /**
19
- * Sub-pipe: pipe.computationSystem.computationUtils.commitBatchInChunks
20
- * @param {object} config - The computation system configuration object.
21
- * @param {object} dependencies - Contains db, logger, calculationUtils.
22
- * @param {Array<object>} writes - Array of { ref: DocumentReference, data: object }.
23
- * @param {string} operationName - Name for logging.
24
- */
25
- async function commitBatchInChunks(config, dependencies, writes, operationName) {
26
- // --- MODIFIED: Get withRetry from dependencies ---
27
- const { db, logger, calculationUtils } = dependencies;
15
+ /** Stage 2: Commit a batch of writes in chunks */
16
+ async function commitBatchInChunks(config, deps, writes, operationName) {
17
+ const { db, logger, calculationUtils } = deps;
28
18
  const { withRetry } = calculationUtils;
29
- // --- END MODIFIED ---
30
-
19
+
31
20
  const batchSizeLimit = config.batchSizeLimit || 450;
32
-
33
- if (writes.length === 0) {
34
- logger.log('WARN', `[${operationName}] No writes to commit.`);
35
- return;
36
- }
21
+ if (!writes.length) { logger.log('WARN', `[${operationName}] No writes to commit.`); return; }
22
+
37
23
  for (let i = 0; i < writes.length; i += batchSizeLimit) {
38
- const batch = db.batch(); // Use db
39
24
  const chunk = writes.slice(i, i + batchSizeLimit);
25
+ const batch = db.batch();
40
26
  chunk.forEach(write => batch.set(write.ref, write.data, { merge: true }));
41
27
 
42
28
  const chunkNum = Math.floor(i / batchSizeLimit) + 1;
43
29
  const totalChunks = Math.ceil(writes.length / batchSizeLimit);
44
- await withRetry(
45
- () => batch.commit(),
46
- `${operationName} (Chunk ${chunkNum}/${totalChunks})`
47
- );
30
+ await withRetry(() => batch.commit(), `${operationName} (Chunk ${chunkNum}/${totalChunks})`);
31
+
48
32
  logger.log('INFO', `[${operationName}] Committed chunk ${chunkNum}/${totalChunks} (${chunk.length} ops).`);
49
33
  }
50
34
  }
51
35
 
52
- /**
53
- * Sub-pipe: pipe.computationSystem.computationUtils.getExpectedDateStrings
54
- * (Stateless)
55
- */
36
+ /** Stage 3: Generate an array of expected date strings between two dates */
56
37
  function getExpectedDateStrings(startDate, endDate) {
57
38
  const dateStrings = [];
58
39
  if (startDate <= endDate) {
@@ -65,86 +46,62 @@ function getExpectedDateStrings(startDate, endDate) {
65
46
  return dateStrings;
66
47
  }
67
48
 
68
- /**
69
- * Internal helper: Finds the earliest date document in a collection.
70
- */
71
- async function getFirstDateFromCollection(config, dependencies, collectionName) {
72
- // --- MODIFIED: Get withRetry from dependencies ---
73
- const { db, logger, calculationUtils } = dependencies;
49
+ /** Stage 4: Get the earliest date in a collection */
50
+ async function getFirstDateFromCollection(config, deps, collectionName) {
51
+ const { db, logger, calculationUtils } = deps;
74
52
  const { withRetry } = calculationUtils;
75
- // --- END MODIFIED ---
53
+
76
54
  let earliestDate = null;
77
55
  try {
78
- const blockDocRefs = await withRetry(
79
- () => db.collection(collectionName).listDocuments(), // Use db
80
- `GetBlocks(${collectionName})`
81
- );
82
-
83
- if (blockDocRefs.length === 0) {
84
- logger.log('WARN', `No block documents found in collection: ${collectionName}`);
85
- return null;
86
- }
87
-
88
- for (const blockDocRef of blockDocRefs) {
89
- const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection)
90
- .where(FieldPath.documentId(), '>=', '2000-01-01')
91
- .orderBy(FieldPath.documentId(), 'asc')
92
- .limit(1);
93
-
94
- const snapshotSnap = await withRetry(
95
- () => snapshotQuery.get(),
96
- `GetEarliestSnapshot(${blockDocRef.path})`
97
- );
98
-
99
- if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
100
- const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
101
- if (!earliestDate || foundDate < earliestDate) {
102
- earliestDate = foundDate;
103
- }
104
- }
105
- }
56
+ const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
57
+ if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
58
+
59
+ for (const blockDocRef of blockDocRefs) {
60
+ const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection)
61
+ .where(FieldPath.documentId(), '>=', '2000-01-01')
62
+ .orderBy(FieldPath.documentId(), 'asc')
63
+ .limit(1);
106
64
 
65
+ const snapshotSnap = await withRetry(() => snapshotQuery.get(), `GetEarliestSnapshot(${blockDocRef.path})`);
66
+ if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
67
+ const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
68
+ if (!earliestDate || foundDate < earliestDate) earliestDate = foundDate;
69
+ }
70
+ }
107
71
  } catch (e) {
108
72
  logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
109
73
  }
74
+
110
75
  return earliestDate;
111
76
  }
112
77
 
113
- /**
114
- * Sub-pipe: pipe.computationSystem.computationUtils.getFirstDateFromSourceData
115
- * @param {object} config - The computation system configuration object.
116
- * @param {object} dependencies - Contains db, logger, calculationUtils.
117
- * @returns {Promise<Date>} The earliest date found or a default fallback date.
118
- */
119
- async function getFirstDateFromSourceData(config, dependencies) {
120
- const { logger } = dependencies;
121
- logger.log('INFO', 'Querying for the earliest date from source portfolio data...');
122
-
123
- // Pass dependencies to sub-pipe
124
- const investorDate = await getFirstDateFromCollection(config, dependencies, config.normalUserPortfolioCollection);
125
- const speculatorDate = await getFirstDateFromCollection(config, dependencies, config.speculatorPortfolioCollection);
126
-
127
- let earliestDate;
128
- if (investorDate && speculatorDate) {
129
- earliestDate = investorDate < speculatorDate ? investorDate : speculatorDate;
130
- } else {
131
- earliestDate = investorDate || speculatorDate;
132
- }
133
-
134
- if (earliestDate) {
135
- logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
136
- return earliestDate;
137
- } else {
138
- const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
139
- logger.log('WARN', `No source data found. Defaulting first date to: ${fallbackDate.toISOString().slice(0, 10)}`);
140
- return fallbackDate;
141
- }
78
+ /** Stage 5: Determine the earliest date from source data across both user types */
79
+ async function getFirstDateFromSourceData(config, deps) {
80
+ const { logger } = deps;
81
+ logger.log('INFO', 'Querying for earliest date from source portfolio data...');
82
+
83
+ const investorDate = await getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection);
84
+ const speculatorDate = await getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection);
85
+
86
+ let earliestDate;
87
+ if (investorDate && speculatorDate) earliestDate = investorDate < speculatorDate ? investorDate : speculatorDate;
88
+ else earliestDate = investorDate || speculatorDate;
89
+
90
+ if (earliestDate) {
91
+ logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
92
+ return earliestDate;
93
+ } else {
94
+ const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
95
+ logger.log('WARN', `No source data found. Defaulting first date to: ${fallbackDate.toISOString().slice(0, 10)}`);
96
+ return fallbackDate;
97
+ }
142
98
  }
143
99
 
144
100
  module.exports = {
145
- FieldValue, FieldPath,
101
+ FieldValue,
102
+ FieldPath,
146
103
  normalizeName,
147
104
  commitBatchInChunks,
148
- getExpectedDateStrings,
105
+ getExpectedDateStrings,
149
106
  getFirstDateFromSourceData,
150
- };
107
+ };