bulltrackers-module 1.0.130 → 1.0.132

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.
@@ -6,9 +6,11 @@
6
6
  * It reads its pass number from the config and executes only those calculations.
7
7
  * This file contains the high-level "manual" of steps. The "how-to" logic
8
8
  * is extracted into 'computation_system_utils.js'.
9
+ * --- MODIFIED: To use getEarliestDataDates and pass the date map to the orchestrator helpers. ---
9
10
  */
10
11
  const { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
11
- const { getExpectedDateStrings, getFirstDateFromSourceData } = require('../utils/utils.js');
12
+ // --- MODIFIED: Import getEarliestDataDates ---
13
+ const { getExpectedDateStrings, getEarliestDataDates } = require('../utils/utils.js');
12
14
 
13
15
  async function runComputationPass(config, dependencies, computationManifest) {
14
16
  const { logger } = dependencies;
@@ -17,7 +19,12 @@ async function runComputationPass(config, dependencies, computationManifest) {
17
19
 
18
20
  const yesterday = new Date(); yesterday.setUTCDate(yesterday.getUTCDate()-1);
19
21
  const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
20
- const firstDate = await getFirstDateFromSourceData(config, dependencies);
22
+
23
+ // --- MODIFIED: Call new date function ---
24
+ const earliestDates = await getEarliestDataDates(config, dependencies);
25
+ const firstDate = earliestDates.absoluteEarliest; // Use the absolute earliest for the loop
26
+ // --- END MODIFICATION ---
27
+
21
28
  const startDateUTC = firstDate ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate())) : new Date(config.earliestComputationDate+'T00:00:00Z');
22
29
  const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
23
30
 
@@ -30,7 +37,10 @@ async function runComputationPass(config, dependencies, computationManifest) {
30
37
  for (const dateStr of allExpectedDates) {
31
38
  const dateToProcess = new Date(dateStr+'T00:00:00Z');
32
39
  try {
33
- const rootData = await checkRootDataAvailability(dateStr, config, dependencies); if (!rootData) continue;
40
+ // --- MODIFIED: Pass earliestDates map to checkRootDataAvailability ---
41
+ const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates); if (!rootData) continue;
42
+ // --- END MODIFICATION ---
43
+
34
44
  const fetchedDeps = await fetchDependenciesForPass(dateStr, calcsInThisPass, computationManifest, config, dependencies);
35
45
  const { standardCalcsToRun, metaCalcsToRun } = filterCalculations(standardCalcs, metaCalcs, rootData.status, fetchedDeps, passToRun, dateStr, logger);
36
46
  if (standardCalcsToRun.length) await runStandardComputationPass(dateToProcess, standardCalcsToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData);
@@ -44,4 +54,4 @@ async function runComputationPass(config, dependencies, computationManifest) {
44
54
  logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
45
55
  }
46
56
 
47
- module.exports = { runComputationPass };
57
+ module.exports = { runComputationPass };
@@ -5,32 +5,95 @@ const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
5
5
  /** Stage 1: Group manifest by pass number */
6
6
  function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
7
7
 
8
- /** Stage 2: Check root data dependencies for a calc */
9
- function checkRootDependencies(calcManifest, rootDataStatus) { if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) return true;
10
- for (const dep of calcManifest.rootDataDependencies) if ((dep==='portfolio'&&!rootDataStatus.hasPortfolio)||(dep==='insights'&&!rootDataStatus.hasInsights)||(dep==='social'&&!rootDataStatus.hasSocial)||(dep==='history'&&!rootDataStatus.hasHistory)) return false;
11
- return true;
8
+ /** * --- MODIFIED: Returns detailed missing dependencies for logging ---
9
+ * Stage 2: Check root data dependencies for a calc
10
+ */
11
+ function checkRootDependencies(calcManifest, rootDataStatus) {
12
+ const missing = [];
13
+ if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) {
14
+ return { canRun: true, missing };
15
+ }
16
+ for (const dep of calcManifest.rootDataDependencies) {
17
+ if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
18
+ else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
19
+ else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
20
+ else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
21
+ }
22
+ return { canRun: missing.length === 0, missing };
12
23
  }
13
24
 
14
- /** Stage 3: Check root data availability for a date */
15
- // --- START FIX ---
16
- // The signature is changed from '{ logger, ...deps }' to 'dependencies'
17
- // to ensure the full object is passed down.
18
- async function checkRootDataAvailability(dateStr, config, dependencies) {
19
- const { logger } = dependencies; // Destructure logger for local use
25
+ /** * --- MODIFIED: Uses earliestDates map to avoid unnecessary queries ---
26
+ * Stage 3: Check root data availability for a date
27
+ */
28
+ async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
29
+ const { logger } = dependencies;
20
30
  logger.log('INFO', `[PassRunner] Checking root data for ${dateStr}...`);
31
+
32
+ const dateToProcess = new Date(dateStr + 'T00:00:00Z');
33
+ let portfolioRefs = [], insightsData = null, socialData = null, historyRefs = [];
34
+ let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
35
+
21
36
  try {
22
- const [portfolioRefs, insightsData, socialData, historyRefs] = await Promise.all([
23
- getPortfolioPartRefs(config, dependencies, dateStr), // Pass full 'dependencies'
24
- loadDailyInsights(config, dependencies, dateStr), // Pass full 'dependencies'
25
- loadDailySocialPostInsights(config, dependencies, dateStr), // Pass full 'dependencies'
26
- getHistoryPartRefs(config, dependencies, dateStr) // Pass full 'dependencies'
27
- ]);
28
- const hasPortfolio = !!(portfolioRefs?.length), hasInsights = !!insightsData, hasSocial = !!socialData, hasHistory = !!(historyRefs?.length);
29
- if (!(hasPortfolio||hasInsights||hasSocial||hasHistory)) { logger.log('WARN', `[PassRunner] No root data for ${dateStr}.`); return null; }
30
- return { portfolioRefs: portfolioRefs||[], insightsData: insightsData||null, socialData: socialData||null, historyRefs: historyRefs||[], status: { hasPortfolio, hasInsights, hasSocial, hasHistory } };
31
- } catch (err) { logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message }); return null; }
37
+ const tasks = [];
38
+
39
+ if (dateToProcess >= earliestDates.portfolio) {
40
+ tasks.push(
41
+ getPortfolioPartRefs(config, dependencies, dateStr).then(res => {
42
+ portfolioRefs = res;
43
+ hasPortfolio = !!(res?.length);
44
+ })
45
+ );
46
+ }
47
+
48
+ if (dateToProcess >= earliestDates.insights) {
49
+ tasks.push(
50
+ loadDailyInsights(config, dependencies, dateStr).then(res => {
51
+ insightsData = res;
52
+ hasInsights = !!res;
53
+ })
54
+ );
55
+ }
56
+
57
+ if (dateToProcess >= earliestDates.social) {
58
+ tasks.push(
59
+ loadDailySocialPostInsights(config, dependencies, dateStr).then(res => {
60
+ socialData = res;
61
+ hasSocial = !!res;
62
+ })
63
+ );
64
+ }
65
+
66
+ if (dateToProcess >= earliestDates.history) {
67
+ tasks.push(
68
+ getHistoryPartRefs(config, dependencies, dateStr).then(res => {
69
+ historyRefs = res;
70
+ hasHistory = !!(res?.length);
71
+ })
72
+ );
73
+ }
74
+
75
+ await Promise.all(tasks);
76
+
77
+ if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) {
78
+ logger.log('WARN', `[PassRunner] No root data for ${dateStr}.`);
79
+ return null;
80
+ }
81
+
82
+ return {
83
+ portfolioRefs,
84
+ insightsData,
85
+ socialData,
86
+ historyRefs,
87
+ status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
88
+ };
89
+
90
+ } catch (err) {
91
+ logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message });
92
+ return null;
93
+ }
32
94
  }
33
- // --- END FIX ---
95
+
96
+
34
97
 
35
98
  /** Stage 4: Fetch computed dependencies from Firestore */
36
99
  async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
@@ -50,14 +113,47 @@ async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, conf
50
113
  return fetched;
51
114
  }
52
115
 
53
- /** Stage 5: Filter calculations based on available root data and dependencies */
116
+ /** * --- MODIFIED: Added detailed logging ---
117
+ * Stage 5: Filter calculations based on available root data and dependencies
118
+ */
54
119
  function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, fetchedDeps, passToRun, dateStr, logger) {
55
120
  const skipped = new Set();
56
- const standardCalcsToRun = standardCalcs.filter(c => checkRootDependencies(c, rootDataStatus) || (logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} missing root data`), skipped.add(c.name), false));
57
- const metaCalcsToRun = metaCalcs.filter(c => checkRootDependencies(c, rootDataStatus) && (c.dependencies||[]).every(d=>fetchedDeps[normalizeName(d)]) || (logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} missing dep`), skipped.add(c.name), false));
121
+
122
+ // Filter Standard Calcs
123
+ const standardCalcsToRun = standardCalcs.filter(c => {
124
+ const { canRun, missing } = checkRootDependencies(c, rootDataStatus);
125
+ if (canRun) return true;
126
+
127
+ logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Missing root data: [${missing.join(', ')}]`);
128
+ skipped.add(c.name);
129
+ return false;
130
+ });
131
+
132
+ // Filter Meta Calcs
133
+ const metaCalcsToRun = metaCalcs.filter(c => {
134
+ // 1. Check root data
135
+ const { canRun, missing: missingRoot } = checkRootDependencies(c, rootDataStatus);
136
+ if (!canRun) {
137
+ logger.log('INFO', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing root data: [${missingRoot.join(', ')}]`);
138
+ skipped.add(c.name);
139
+ return false;
140
+ }
141
+
142
+ // 2. Check computed dependencies
143
+ const missingDeps = (c.dependencies || []).map(normalizeName).filter(d => !fetchedDeps[d]);
144
+ if (missingDeps.length > 0) {
145
+ logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
146
+ skipped.add(c.name);
147
+ return false;
148
+ }
149
+
150
+ return true; // All checks passed
151
+ });
152
+
58
153
  return { standardCalcsToRun, metaCalcsToRun };
59
154
  }
60
155
 
156
+
61
157
  /** Stage 6: Initialize calculator instances */
62
158
  function initializeCalculators(calcs, logger) { const state = {}; for (const c of calcs) { const name=normalizeName(c.name), Cl=c.class; if(typeof Cl==='function') try { const inst=new Cl(); inst.manifest=c; state[name]=inst; } catch(e){logger.warn(`Init failed ${name}`,{errorMessage:e.message}); state[name]=null;} else {logger.warn(`Class missing ${name}`); state[name]=null;} } return state; }
63
159
 
@@ -79,20 +175,159 @@ async function streamAndProcess(dateStr, todayRefs, state, passName, config, dep
79
175
 
80
176
  /** Stage 9: Run standard computations */
81
177
  async function runStandardComputationPass(date, calcs, passName, config, deps, rootData) {
82
- const dStr=date.toISOString().slice(0,10), logger=deps.logger;
178
+ const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
83
179
  logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
84
- const fullRoot=await loadHistoricalData(date, calcs, config, deps, rootData);
85
- const state=initializeCalculators(calcs, logger);
180
+ const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
181
+ const state = initializeCalculators(calcs, logger);
86
182
  await streamAndProcess(dStr, fullRoot.portfolioRefs, state, passName, config, deps, fullRoot);
87
- let success=0; for(const name in state){ const calc=state[name]; if(!calc||typeof calc.getResult!=='function') continue; try{ const result=await Promise.resolve(calc.getResult()); if(result&&Object.keys(result).length){ /* Commit logic omitted for brevity */ success++; } } catch(e){logger.log('ERROR',`getResult failed ${name} for ${dStr}`,{err:e.message,stack:e.stack});} }
88
- logger.log(success===calcs.length?'SUCCESS':'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`);
183
+
184
+ // --- START: FULL COMMIT LOGIC ---
185
+ let success = 0;
186
+ const standardWrites = [];
187
+ const shardedWrites = {}; // Format: { [collectionName]: { [docId]: data } }
188
+
189
+ for (const name in state) {
190
+ const calc = state[name];
191
+ if (!calc || typeof calc.getResult !== 'function') continue;
192
+
193
+ try {
194
+ const result = await Promise.resolve(calc.getResult());
195
+ if (result && Object.keys(result).length > 0) {
196
+
197
+ // Separate sharded data from standard data
198
+ const standardResult = {};
199
+ for (const key in result) {
200
+ if (key.startsWith('sharded_')) {
201
+ // This is sharded data, e.g., sharded_user_profitability
202
+ // The value is expected to be: { "collection_name": { "doc1": {...}, "doc2": {...} } }
203
+ const shardedData = result[key];
204
+ for (const collectionName in shardedData) {
205
+ if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
206
+ // Merge doc data (e.g., combining data for "user_profitability_shard_1")
207
+ Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
208
+ }
209
+ } else {
210
+ // This is a standard, single-doc result
211
+ standardResult[key] = result[key];
212
+ }
213
+ }
214
+
215
+ // Add standard result to the batch
216
+ if (Object.keys(standardResult).length > 0) {
217
+ const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
218
+ .collection(config.resultsSubcollection).doc(calc.manifest.category)
219
+ .collection(config.computationsSubcollection).doc(name);
220
+
221
+ standardWrites.push({ ref: docRef, data: standardResult });
222
+ }
223
+ success++; // Mark as success even if only sharded data was produced
224
+ }
225
+ } catch (e) {
226
+ logger.log('ERROR', `getResult failed ${name} for ${dStr}`, { err: e.message, stack: e.stack });
227
+ }
228
+ }
229
+
230
+ // Commit standard (non-sharded) writes in chunks
231
+ if (standardWrites.length > 0) {
232
+ await commitBatchInChunks(config, deps, standardWrites, `${passName} Standard ${dStr}`);
233
+ }
234
+
235
+ // Commit all sharded writes
236
+ for (const collectionName in shardedWrites) {
237
+ const docs = shardedWrites[collectionName];
238
+ const shardedDocWrites = [];
239
+ for (const docId in docs) {
240
+ // This assumes docId is the full path for sharded docs, or just the doc ID
241
+ // Based on user_profitability_tracker, it's just the doc ID.
242
+ const docRef = deps.db.collection(collectionName).doc(docId);
243
+ shardedDocWrites.push({ ref: docRef, data: docs[docId] });
244
+ }
245
+ if (shardedDocWrites.length > 0) {
246
+ await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`);
247
+ }
248
+ }
249
+ // --- END: FULL COMMIT LOGIC ---
250
+
251
+ logger.log(success === calcs.length ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`);
89
252
  }
90
253
 
91
254
  /** Stage 10: Run meta computations */
92
- async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) { const dStr=date.toISOString().slice(0,10), logger=deps.logger;
255
+ async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
256
+ const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
93
257
  logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
94
- let success=0; for(const mCalc of calcs){ const name=normalizeName(mCalc.name), Cl=mCalc.class; if(typeof Cl!=='function'){ logger.log('ERROR',`Invalid class ${name}`); continue; } const inst=new Cl(); try{ const result=await Promise.resolve(inst.process(dStr,{...deps,rootData},config,fetchedDeps)); if(result&&Object.keys(result).length){ /* Commit logic omitted */ success++; } } catch(e){logger.log('ERROR',`Meta-calc failed ${name} for ${dStr}`,{err:e.message,stack:e.stack}); } }
95
- logger.log(success===calcs.length?'SUCCESS':'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`);
258
+
259
+ // --- START: FULL COMMIT LOGIC ---
260
+ let success = 0;
261
+ const standardWrites = [];
262
+ const shardedWrites = {}; // Format: { [collectionName]: { [docId]: data } }
263
+
264
+ for (const mCalc of calcs) {
265
+ const name = normalizeName(mCalc.name), Cl = mCalc.class;
266
+ if (typeof Cl !== 'function') {
267
+ logger.log('ERROR', `Invalid class ${name}`);
268
+ continue;
269
+ }
270
+ const inst = new Cl();
271
+ try {
272
+ // Pass the full dependencies object to process()
273
+ const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData }, config, fetchedDeps));
274
+
275
+ if (result && Object.keys(result).length > 0) {
276
+
277
+ // Separate sharded data from standard data
278
+ const standardResult = {};
279
+ for (const key in result) {
280
+ if (key.startsWith('sharded_')) {
281
+ const shardedData = result[key];
282
+ for (const collectionName in shardedData) {
283
+ if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
284
+ Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
285
+ }
286
+ } else {
287
+ standardResult[key] = result[key];
288
+ }
289
+ }
290
+
291
+ // Add standard result to the batch
292
+ if (Object.keys(standardResult).length > 0) {
293
+ const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
294
+ .collection(config.resultsSubcollection).doc(mCalc.category)
295
+ .collection(config.computationsSubcollection).doc(name);
296
+
297
+ standardWrites.push({ ref: docRef, data: standardResult });
298
+ }
299
+ success++;
300
+ }
301
+ } catch (e) {
302
+ logger.log('ERROR', `Meta-calc failed ${name} for ${dStr}`, { err: e.message, stack: e.stack });
303
+ }
304
+ }
305
+
306
+ // Commit standard (non-sharded) writes in chunks
307
+ if (standardWrites.length > 0) {
308
+ await commitBatchInChunks(config, deps, standardWrites, `${passName} Meta ${dStr}`);
309
+ }
310
+
311
+ // Commit all sharded writes
312
+ for (const collectionName in shardedWrites) {
313
+ const docs = shardedWrites[collectionName];
314
+ const shardedDocWrites = [];
315
+ for (const docId in docs) {
316
+ // Special case for stateful meta-calcs that write to a specific path
317
+ const docRef = docId.includes('/')
318
+ ? deps.db.doc(docId) // docId is a full path
319
+ : deps.db.collection(collectionName).doc(docId); // docId is just an ID
320
+
321
+ shardedDocWrites.push({ ref: docRef, data: docs[docId] });
322
+ }
323
+ if (shardedDocWrites.length > 0) {
324
+ await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`);
325
+ }
326
+ }
327
+ // --- END: FULL COMMIT LOGIC ---
328
+
329
+ logger.log(success === calcs.length ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`);
96
330
  }
97
331
 
332
+
98
333
  module.exports = { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass };
@@ -2,8 +2,10 @@
2
2
  * @fileoverview Computation system sub-pipes and utils.
3
3
  * REFACTORED: Now stateless and receive dependencies where needed.
4
4
  * DYNAMIC: Categorization logic is removed, replaced by manifest.
5
+ * --- MODIFIED: getFirstDateFromSourceData is now getEarliestDataDates
6
+ * and queries all data sources to build an availability map. ---
5
7
  */
6
- /** --- Computation System Sub-Pipes & Utils (Stateless) --- */
8
+ /** --- Computation System Sub-Pipes & Utils (Stateless, Dependency-Injection) --- */
7
9
 
8
10
  const { FieldValue, FieldPath } = require('@google-cloud/firestore');
9
11
 
@@ -46,13 +48,46 @@ function getExpectedDateStrings(startDate, endDate) {
46
48
  return dateStrings;
47
49
  }
48
50
 
49
- /** Stage 4: Get the earliest date in a collection */
51
+ /**
52
+ * --- NEW HELPER ---
53
+ * Stage 4: Get the earliest date in a *flat* collection where doc IDs are dates.
54
+ */
55
+ async function getFirstDateFromSimpleCollection(config, deps, collectionName) {
56
+ const { db, logger, calculationUtils } = deps;
57
+ const { withRetry } = calculationUtils;
58
+
59
+ try {
60
+ if (!collectionName) {
61
+ logger.log('WARN', `[Core Utils] Collection name not provided for simple date query.`);
62
+ return null;
63
+ }
64
+ const query = db.collection(collectionName)
65
+ .where(FieldPath.documentId(), '>=', '2000-01-01')
66
+ .orderBy(FieldPath.documentId(), 'asc')
67
+ .limit(1);
68
+
69
+ const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
70
+
71
+ if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) {
72
+ return new Date(snapshot.docs[0].id + 'T00:00:00Z');
73
+ }
74
+ } catch (e) {
75
+ logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /** Stage 4: Get the earliest date in a sharded collection */
50
81
  async function getFirstDateFromCollection(config, deps, collectionName) {
51
82
  const { db, logger, calculationUtils } = deps;
52
83
  const { withRetry } = calculationUtils;
53
84
 
54
85
  let earliestDate = null;
55
86
  try {
87
+ if (!collectionName) {
88
+ logger.log('WARN', `[Core Utils] Collection name not provided for sharded date query.`);
89
+ return null;
90
+ }
56
91
  const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
57
92
  if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
58
93
 
@@ -75,26 +110,69 @@ async function getFirstDateFromCollection(config, deps, collectionName) {
75
110
  return earliestDate;
76
111
  }
77
112
 
78
- /** Stage 5: Determine the earliest date from source data across both user types */
79
- async function getFirstDateFromSourceData(config, deps) {
113
+ /** * --- MODIFIED FUNCTION ---
114
+ * Stage 5: Determine the earliest date from *all* source data.
115
+ */
116
+ async function getEarliestDataDates(config, deps) {
80
117
  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
- }
118
+ logger.log('INFO', 'Querying for earliest date from ALL source data collections...');
119
+
120
+ // These return null on error or if empty
121
+ const [
122
+ investorDate,
123
+ speculatorDate,
124
+ investorHistoryDate,
125
+ speculatorHistoryDate,
126
+ insightsDate,
127
+ socialDate
128
+ ] = await Promise.all([
129
+ getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection),
130
+ getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection),
131
+ getFirstDateFromCollection(config, deps, config.normalUserHistoryCollection),
132
+ getFirstDateFromCollection(config, deps, config.speculatorHistoryCollection),
133
+ getFirstDateFromSimpleCollection(config, deps, config.insightsCollectionName),
134
+ getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName)
135
+ ]);
136
+
137
+ // Helper to find the minimum (earliest) of a set of dates
138
+ const getMinDate = (...dates) => {
139
+ const validDates = dates.filter(Boolean); // Filter out nulls
140
+ if (validDates.length === 0) return null;
141
+ return new Date(Math.min(...validDates));
142
+ };
143
+
144
+ const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
145
+ const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
146
+ const earliestInsightsDate = getMinDate(insightsDate); // Already a single date
147
+ const earliestSocialDate = getMinDate(socialDate); // Already a single date
148
+
149
+ const absoluteEarliest = getMinDate(
150
+ earliestPortfolioDate,
151
+ earliestHistoryDate,
152
+ earliestInsightsDate,
153
+ earliestSocialDate
154
+ );
155
+
156
+ // Fallback date
157
+ const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
158
+
159
+ const result = {
160
+ portfolio: earliestPortfolioDate || new Date('2999-12-31'), // Use a 'far future' date if null
161
+ history: earliestHistoryDate || new Date('2999-12-31'),
162
+ insights: earliestInsightsDate || new Date('2999-12-31'),
163
+ social: earliestSocialDate || new Date('2999-12-31'),
164
+ absoluteEarliest: absoluteEarliest || fallbackDate // Use fallback for the main loop
165
+ };
166
+
167
+ logger.log('INFO', 'Earliest data availability map built:', {
168
+ portfolio: result.portfolio.toISOString().slice(0, 10),
169
+ history: result.history.toISOString().slice(0, 10),
170
+ insights: result.insights.toISOString().slice(0, 10),
171
+ social: result.social.toISOString().slice(0, 10),
172
+ absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10)
173
+ });
174
+
175
+ return result;
98
176
  }
99
177
 
100
178
  module.exports = {
@@ -103,5 +181,6 @@ module.exports = {
103
181
  normalizeName,
104
182
  commitBatchInChunks,
105
183
  getExpectedDateStrings,
106
- getFirstDateFromSourceData,
107
- };
184
+ // getFirstDateFromSourceData, // This is replaced
185
+ getEarliestDataDates, // <-- EXPORT NEW FUNCTION
186
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.130",
3
+ "version": "1.0.132",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [