bulltrackers-module 1.0.21 → 1.0.23

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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @fileoverview Factory function to create the Computation System handler.
3
+ */
4
+ const { Firestore } = require('@google-cloud/firestore');
5
+ const { logger: defaultLogger } = require("sharedsetup")(__filename);
6
+ const { runComputationOrchestrator } = require('./helpers/orchestration_helpers');
7
+
8
+ /**
9
+ * Creates the Computation System Cloud Function handler (HTTP triggered).
10
+ * @param {object} config - The Computation System configuration object.
11
+ * @param {object} dependencies - Shared dependencies { firestore, logger }.
12
+ * @returns {Function} The async Cloud Function handler for HTTP requests.
13
+ */
14
+ function createComputationSystemHandler(config, dependencies) {
15
+ // Use dependencies passed from index.js
16
+ const firestore = dependencies.firestore || new Firestore();
17
+ const logger = dependencies.logger || defaultLogger;
18
+
19
+ return async (req, res) => {
20
+ const functionName = 'ComputationSystem';
21
+ logger.log('INFO', `[${functionName}] Starting...`);
22
+ try {
23
+ // Pass the config down to the orchestrator
24
+ const summary = await runComputationOrchestrator(firestore, logger, config);
25
+
26
+ logger.log('SUCCESS', `[${functionName}] Done.`);
27
+ if (!res.headersSent) {
28
+ res.status(200).send({ message: 'Computation complete', summary });
29
+ }
30
+ } catch (error) {
31
+ logger.log('ERROR', `[${functionName}] Failed.`, { errorMessage: error.message, errorStack: error.stack });
32
+ if (!res.headersSent) {
33
+ res.status(500).send({ message: 'Computation Error', error: error.message });
34
+ }
35
+ }
36
+ };
37
+ }
38
+
39
+ module.exports = {
40
+ createComputationSystemHandler,
41
+ };
@@ -1,21 +1,21 @@
1
1
  /**
2
2
  * @fileoverview Core orchestration logic for the Computation System.
3
- * Contains both the high-level pass orchestrator and the low-level
4
- * function for processing a single date.
5
3
  */
6
4
 
7
5
  const { FieldPath } = require('@google-cloud/firestore');
8
6
  const { getPortfolioPartRefs, loadFullDayMap, loadDataByRefs } = require('../utils/data_loader.js');
9
7
  const {
8
+ // Note: calculation categories are now sourced from utils.js which gets them from the unified package
10
9
  historicalCalculations, dailyCalculations, HISTORICAL_CALC_NAMES,
11
- withRetry, getExpectedDateStrings, processJobsInParallel, getFirstDateFromSourceData,
12
- commitBatchInChunks, unifiedUtils
10
+ withRetry, // Ensure withRetry is correctly imported if needed here, or rely on utils.js export
11
+ getExpectedDateStrings, processJobsInParallel, getFirstDateFromSourceData,
12
+ commitBatchInChunks, unifiedUtils // unifiedUtils comes from the calculations package via utils.js
13
13
  } = require('../utils/utils.js');
14
14
 
15
15
 
16
- // --- CORE CALCULATION HELPERS (Moved from original utils.js) ---
16
+ // --- CORE CALCULATION HELPERS ---
17
17
 
18
- /** Initializes calculator instances from the provided calculation classes. */
18
+ /** Initializes calculator instances. */
19
19
  function initializeCalculators(calculationsToRun, sourcePackage) {
20
20
  const state = {};
21
21
  for (const { category, calcName } of calculationsToRun) {
@@ -24,54 +24,61 @@ function initializeCalculators(calculationsToRun, sourcePackage) {
24
24
  try {
25
25
  state[`${category}/${calcName}`] = new CalculationClass();
26
26
  } catch (e) {
27
- // Use logger, but it's not passed here, so console.warn
28
27
  console.warn(`[Orchestrator] Init failed for ${category}/${calcName}`, { errorMessage: e.message });
29
- state[`${category}/${calcName}`] = null;
28
+ state[`${category}/${calcName}`] = null; // Mark as failed
30
29
  }
31
30
  } else {
32
31
  console.warn(`[Orchestrator] Calculation class not found: ${category}/${calcName}`);
32
+ state[`${category}/${calcName}`] = null; // Mark as missing
33
33
  }
34
34
  }
35
35
  return state;
36
36
  }
37
37
 
38
- /** Streams day's data and calls the process() method on all calculators. */
39
- async function streamAndProcess(firestore, dateStr, todayRefs, state, passName, logger, yesterdayPortfolios = {}) {
38
+ /** Streams day's data, calls process() on calculators, using config for batch size. */
39
+ async function streamAndProcess(firestore, dateStr, todayRefs, state, passName, logger, config, yesterdayPortfolios = {}) {
40
40
  logger.log('INFO', `[${passName}] Streaming ${todayRefs.length} 'today' part docs for ${dateStr}...`);
41
+ // unifiedUtils comes from calculations package via utils.js
41
42
  const { instrumentToTicker, instrumentToSector } = await unifiedUtils.loadInstrumentMappings();
42
43
  const context = { instrumentMappings: instrumentToTicker, sectorMapping: instrumentToSector };
44
+ const batchSize = config.partRefBatchSize || 10; // Use config for chunk size
43
45
 
44
- for (let i = 0; i < todayRefs.length; i += 10) { // Process in chunks of 10 part files
45
- const batchRefs = todayRefs.slice(i, i + 10);
46
- const todayPortfoliosChunk = await loadDataByRefs(firestore, batchRefs);
46
+ for (let i = 0; i < todayRefs.length; i += batchSize) {
47
+ const batchRefs = todayRefs.slice(i, i + batchSize);
48
+ // Pass config to data loader
49
+ const todayPortfoliosChunk = await loadDataByRefs(firestore, batchRefs, config);
47
50
 
48
51
  for (const uid in todayPortfoliosChunk) {
49
52
  const p = todayPortfoliosChunk[uid];
50
- if (!p) continue;
51
-
53
+ if (!p) continue; // Skip if portfolio data is null/undefined for this user
54
+
55
+ // Determine user type based on portfolio structure
52
56
  const userType = p.PublicPositions ? 'speculator' : 'normal';
53
-
57
+
54
58
  for (const key in state) {
55
59
  const calc = state[key];
60
+ // Skip if calculator failed initialization or has no process method
56
61
  if (!calc || typeof calc.process !== 'function') continue;
57
62
 
58
63
  const [category, calcName] = key.split('/');
59
64
  let processArgs;
60
65
 
61
66
  if (HISTORICAL_CALC_NAMES.has(calcName)) {
67
+ // Historical calculations require yesterday's data
62
68
  const pYesterday = yesterdayPortfolios[uid];
63
- if (!pYesterday) continue; // Skip if no yesterday data for historical calc
64
- processArgs = [p, pYesterday, uid];
69
+ if (!pYesterday) continue; // Skip if no yesterday data
70
+ processArgs = [p, pYesterday, uid]; // pToday, pYesterday, userId
65
71
  } else {
66
- // Filter daily calcs by user type
72
+ // Filter daily calculations based on user type and calculation category
67
73
  if ((userType === 'normal' && category === 'speculators') ||
68
74
  (userType === 'speculator' && !['speculators', 'sanity'].includes(category))) {
69
- continue;
75
+ continue; // Skip speculator calcs for normal users, and non-speculator/sanity calcs for speculators
70
76
  }
71
- processArgs = [p, uid, context];
77
+ processArgs = [p, uid, context]; // pToday, userId, context
72
78
  }
73
-
79
+
74
80
  try {
81
+ // Allow process method to be async or sync
75
82
  await Promise.resolve(calc.process(...processArgs));
76
83
  } catch (e) {
77
84
  logger.log('WARN', `Process error in ${key} for user ${uid}`, { err: e.message });
@@ -82,95 +89,128 @@ async function streamAndProcess(firestore, dateStr, todayRefs, state, passName,
82
89
  }
83
90
 
84
91
 
85
- // --- LOW-LEVEL ORCHESTRATOR (Moved from pass_orchestrators.js) ---
92
+ // --- LOW-LEVEL ORCHESTRATOR ---
86
93
 
87
94
  /**
88
- * Runs a set of calculations for a given date and writes the final results.
89
- * @param {Firestore} firestore A Firestore client instance.
90
- * @param {Function} logger A logger instance.
91
- * @param {Date} dateToProcess - The date to run computations for.
92
- * @param {Array} calculationsToRun - List of { category, calcName } to run.
93
- * @param {String} passName - The name of the pass for logging.
94
- * @param {Object} sourcePackage - The object containing calculation classes.
95
- * @returns {Object} A summary of the execution.
95
+ * Runs computations for a single date using the provided config.
96
+ * @param {Firestore} firestore Firestore client.
97
+ * @param {Function} logger Logger instance.
98
+ * @param {Date} dateToProcess The date to process.
99
+ * @param {Array<object>} calculationsToRun List of { category, calcName }.
100
+ * @param {string} passName Name for logging.
101
+ * @param {object} sourcePackage Object containing calculation classes.
102
+ * @param {object} config The computation system configuration object.
103
+ * @returns {Promise<object>} Execution summary.
96
104
  */
97
- async function runUnifiedComputation(firestore, logger, dateToProcess, calculationsToRun, passName, sourcePackage) {
105
+ async function runUnifiedComputation(firestore, logger, dateToProcess, calculationsToRun, passName, sourcePackage, config) { // Added config
98
106
  const dateStr = dateToProcess.toISOString().slice(0, 10);
99
107
  logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
100
108
 
101
109
  try {
102
110
  // --- 1. Data Loading ---
103
- const todayRefs = await getPortfolioPartRefs(firestore, dateStr);
111
+ // Pass config to data loader functions
112
+ const todayRefs = await getPortfolioPartRefs(firestore, dateStr, config);
104
113
  if (todayRefs.length === 0) {
114
+ logger.log('INFO', `[${passName}] No portfolio data found for ${dateStr}. Skipping.`);
105
115
  return { success: true, date: dateStr, message: "No portfolio data for today." };
106
116
  }
107
117
 
108
118
  let yesterdayPortfolios = {};
119
+ // Check if any calculation *in this run* needs yesterday's data
109
120
  const requiresYesterday = calculationsToRun.some(c => sourcePackage[c.category]?.[c.calcName]?.prototype?.process.length === 3);
110
121
 
111
122
  if (requiresYesterday) {
112
123
  const prev = new Date(dateToProcess);
113
124
  prev.setUTCDate(prev.getUTCDate() - 1);
114
125
  const prevStr = prev.toISOString().slice(0, 10);
115
- const yesterdayRefs = await getPortfolioPartRefs(firestore, prevStr);
116
- yesterdayPortfolios = await loadFullDayMap(firestore, yesterdayRefs);
126
+ // Pass config here too
127
+ const yesterdayRefs = await getPortfolioPartRefs(firestore, prevStr, config);
128
+ if (yesterdayRefs.length > 0) {
129
+ yesterdayPortfolios = await loadFullDayMap(firestore, yesterdayRefs, config);
130
+ logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) portfolio map for historical calcs.`);
131
+ } else {
132
+ logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) portfolio data not found. Historical calcs requiring it will be skipped.`);
133
+ }
117
134
  }
118
135
 
119
136
  // --- 2. In-Memory Processing ---
120
137
  const state = initializeCalculators(calculationsToRun, sourcePackage);
121
- await streamAndProcess(firestore, dateStr, todayRefs, state, passName, logger, yesterdayPortfolios);
138
+ // Pass config here
139
+ await streamAndProcess(firestore, dateStr, todayRefs, state, passName, logger, config, yesterdayPortfolios);
122
140
 
123
- // --- 3. REFACTORED: Write results PER-CALCULATION ---
141
+ // --- 3. Write Results Per Calculation ---
124
142
  let successCount = 0;
125
- const resultsSubcollection = firestore.collection('unified_insights').doc(dateStr).collection('results');
143
+ // Use config for collection names
144
+ const resultsCollectionRef = firestore.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
126
145
 
127
146
  for (const key in state) {
128
147
  const calc = state[key];
148
+ // Skip if calculator failed init or has no getResult
129
149
  if (!calc || typeof calc.getResult !== 'function') continue;
130
-
150
+
131
151
  const [category, calcName] = key.split('/');
132
- const pendingWrites = [];
133
- const summaryData = {};
134
152
 
135
153
  try {
154
+ // Allow getResult to be async or sync
136
155
  const result = await Promise.resolve(calc.getResult());
156
+ const pendingWrites = [];
157
+ const summaryData = {}; // Summary for this specific calculation's success
137
158
 
159
+ // Only proceed if result is not null/undefined and has data
138
160
  if (result && Object.keys(result).length > 0) {
139
-
140
161
  if (result.sharded_user_profitability) {
162
+ // Handle sharded profitability - use config for collection name
141
163
  for (const shardId in result.sharded_user_profitability) {
142
- const shardData = result.sharded_user_profitability[shardId];
143
- if (shardData && Object.keys(shardData).length > 0) {
144
- const shardRef = firestore.collection('unified_insights').doc(shardId);
145
- pendingWrites.push({ ref: shardRef, data: { profits: shardData } });
146
- }
164
+ const shardData = result.sharded_user_profitability[shardId];
165
+ if (shardData && Object.keys(shardData).length > 0) {
166
+ // Use config collection name
167
+ const shardRef = firestore.collection(config.shardedProfitabilityCollection).doc(shardId);
168
+ // Use merge:true to update existing shard docs safely
169
+ pendingWrites.push({ ref: shardRef, data: { profits: shardData } });
170
+ }
147
171
  }
172
+ // Mark sharded calculation as processed in summary
173
+ if (!summaryData[category]) summaryData[category] = {};
174
+ summaryData[category][calcName] = true;
175
+
148
176
  } else {
149
- const computationDocRef = resultsSubcollection.doc(category)
150
- .collection('computations')
177
+ // Handle normal results - use config for subcollection names
178
+ const computationDocRef = resultsCollectionRef.doc(category)
179
+ .collection(config.computationsSubcollection)
151
180
  .doc(calcName);
152
181
  pendingWrites.push({ ref: computationDocRef, data: result });
182
+
183
+ // Mark normal calculation as processed in summary
184
+ if (!summaryData[category]) summaryData[category] = {};
185
+ summaryData[category][calcName] = true;
153
186
  }
154
187
 
155
- if (!summaryData[category]) summaryData[category] = {};
156
- summaryData[category][calcName] = true;
157
-
158
- const docRef = firestore.collection('unified_insights').doc(dateStr);
159
- pendingWrites.push({ ref: docRef, data: summaryData });
160
-
161
- await commitBatchInChunks(
162
- firestore,
163
- pendingWrites,
164
- `Commit ${passName} ${dateStr} [${key}]`
165
- );
166
- successCount++;
188
+ // Add summary update to the top-level date document using merge
189
+ if (Object.keys(summaryData).length > 0) {
190
+ const topLevelDocRef = firestore.collection(config.resultsCollection).doc(dateStr);
191
+ pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
192
+ }
193
+
194
+ // Commit writes if any were generated
195
+ if (pendingWrites.length > 0) {
196
+ await commitBatchInChunks(
197
+ firestore,
198
+ pendingWrites,
199
+ `Commit ${passName} ${dateStr} [${key}]`,
200
+ config // Pass config
201
+ );
202
+ successCount++;
203
+ }
204
+ } else {
205
+ logger.log('INFO', `[${passName}] Calculation ${key} produced no results for ${dateStr}. Skipping write.`);
167
206
  }
168
207
  } catch (e) {
169
- logger.log('ERROR', `[${passName}] getResult/Commit failed for ${key}`, { err: e.message });
208
+ logger.log('ERROR', `[${passName}] getResult/Commit failed for ${key} on ${dateStr}`, { err: e.message });
170
209
  }
171
210
  }
172
-
173
- logger.log('SUCCESS', `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
211
+
212
+ const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
213
+ logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
174
214
  return { success: true, date: dateStr, successful: successCount, failed: calculationsToRun.length - successCount };
175
215
 
176
216
  } catch (err) {
@@ -180,21 +220,22 @@ async function runUnifiedComputation(firestore, logger, dateToProcess, calculati
180
220
  }
181
221
 
182
222
 
183
- // --- HIGH-LEVEL ORCHESTRATOR (Moved from ComputationSystem.js) ---
223
+ // --- HIGH-LEVEL ORCHESTRATOR ---
184
224
 
185
225
  /**
186
- * Main entry point for the Unified Computation System logic.
187
- * @param {Firestore} firestore A Firestore client instance.
188
- * @param {Function} logger A logger instance.
189
- * @returns {Promise<Object>} A summary of all passes.
226
+ * Main entry point for the Unified Computation System logic, using config.
227
+ * @param {Firestore} firestore Firestore client.
228
+ * @param {Function} logger Logger instance.
229
+ * @param {object} config The computation system configuration object.
230
+ * @returns {Promise<Object>} Summary of all passes.
190
231
  */
191
- async function runComputationOrchestrator(firestore, logger) {
232
+ async function runComputationOrchestrator(firestore, logger, config) { // Added config
192
233
  const summary = { pass1_results: [], pass2_results: [] };
193
234
  const yesterday = new Date();
194
235
  yesterday.setUTCDate(yesterday.getUTCDate() - 1);
195
236
  const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
196
237
 
197
- // --- Master Calculation Lists ---
238
+ // --- Master Calculation Lists (sourced via utils.js) ---
198
239
  const masterHistoricalList = Object.entries(historicalCalculations).flatMap(([cat, calcs]) =>
199
240
  Object.keys(calcs).map(name => ({ category: cat, calcName: name }))
200
241
  );
@@ -204,63 +245,87 @@ async function runComputationOrchestrator(firestore, logger) {
204
245
  const masterFullList = [...masterHistoricalList, ...masterDailyList];
205
246
 
206
247
  // --- Date & Job Discovery ---
207
- const firstDate = await getFirstDateFromSourceData(firestore);
208
- const startDateUTC = new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()));
209
-
248
+ // Pass config here
249
+ const firstDate = await getFirstDateFromSourceData(firestore, config);
250
+ // Use config for fallback date if needed
251
+ const startDateUTC = firstDate
252
+ ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
253
+ : new Date(config.earliestComputationDate + 'T00:00:00Z'); // Ensure UTC
254
+
210
255
  const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
211
- const insightDocs = await withRetry(() => firestore.collection('unified_insights').get(), "ListAllInsightDocs");
256
+ // Use config for resultsCollection
257
+ const insightDocs = await withRetry(() => firestore.collection(config.resultsCollection).get(), "ListAllInsightDocs");
212
258
  const existingDateIds = new Set(insightDocs.docs.map(d => d.id).filter(id => /^\d{4}-\d{2}-\d{2}$/.test(id)));
213
259
 
214
260
  // --- PASS 1: Find and process entirely missing days (DAILY CALCS ONLY) ---
215
261
  const missingDates = allExpectedDates.filter(dateStr => !existingDateIds.has(dateStr));
216
262
  const pass1Jobs = missingDates.map(date => ({ date, missing: masterDailyList }));
217
-
263
+
264
+ logger.log('INFO', `[Orchestrator] Pass 1: Found ${pass1Jobs.length} missing dates to process (daily calcs only).`);
218
265
  const pass1Results = await processJobsInParallel(
219
266
  pass1Jobs,
220
- (date, missing) => runUnifiedComputation(firestore, logger, date, missing, 'Pass 1 (Daily Only)', dailyCalculations),
221
- 'Pass 1'
267
+ // Pass config to runUnifiedComputation
268
+ (date, missing) => runUnifiedComputation(firestore, logger, date, missing, 'Pass 1 (Daily Only)', dailyCalculations, config),
269
+ 'Pass 1',
270
+ config // Pass config to processJobsInParallel
222
271
  );
272
+ // Process results (handle fulfilled/rejected)
223
273
  summary.pass1_results = pass1Results.map((r, i) => r.status === 'fulfilled' ? r.value : { success: false, date: pass1Jobs[i].date, error: r.reason?.message });
224
274
 
275
+
225
276
  // --- PASS 2: Find and process incomplete days ---
226
277
  const pass2Jobs = [];
227
- const updatedInsightDocs = await withRetry(() => firestore.collection('unified_insights').where(FieldPath.documentId(), 'in', allExpectedDates).get(), "ListAllInsightDocs-Pass2");
278
+ // Use config for resultsCollection
279
+ const updatedInsightDocs = await withRetry(() => firestore.collection(config.resultsCollection).where(FieldPath.documentId(), 'in', allExpectedDates).get(), "ListAllInsightDocs-Pass2");
228
280
 
229
281
  updatedInsightDocs.forEach(doc => {
230
- if (!/^\d{4}-\d{2}-\d{2}$/.test(doc.id)) return;
282
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(doc.id)) return; // Skip non-date docs
231
283
 
232
284
  const data = doc.data();
285
+ // Check which computations are missing based on the summary flags in the top-level doc
233
286
  const missingCalcs = masterFullList.filter(({ category, calcName }) => !data?.[category]?.[calcName]);
234
287
 
235
288
  if (missingCalcs.length > 0) {
236
289
  pass2Jobs.push({ date: doc.id, missing: missingCalcs });
237
290
  }
238
291
  });
239
-
292
+
293
+ logger.log('INFO', `[Orchestrator] Pass 2: Found ${pass2Jobs.length} incomplete dates to process.`);
240
294
  const pass2Results = await processJobsInParallel(
241
295
  pass2Jobs,
242
- (date, missing) => {
296
+ async (date, missing) => { // Make task function async
243
297
  const historicalMissing = missing.filter(c => HISTORICAL_CALC_NAMES.has(c.calcName));
244
298
  const dailyMissing = missing.filter(c => !HISTORICAL_CALC_NAMES.has(c.calcName));
245
- const promises = [];
299
+ const results = []; // Collect results from both runs if applicable
300
+
246
301
  if (historicalMissing.length > 0) {
247
- promises.push(runUnifiedComputation(firestore, logger, date, historicalMissing, 'Pass 2 (Historical)', historicalCalculations));
302
+ logger.log('INFO', `[Pass 2] Running ${historicalMissing.length} historical calcs for ${date.toISOString().slice(0, 10)}.`);
303
+ // Pass config here
304
+ const histResult = await runUnifiedComputation(firestore, logger, date, historicalMissing, 'Pass 2 (Historical)', historicalCalculations, config);
305
+ results.push(histResult);
248
306
  }
249
307
  if (dailyMissing.length > 0) {
250
- promises.push(runUnifiedComputation(firestore, logger, date, dailyMissing, 'Pass 2 (Daily)', dailyCalculations));
308
+ logger.log('INFO', `[Pass 2] Running ${dailyMissing.length} daily calcs for ${date.toISOString().slice(0, 10)}.`);
309
+ // Pass config here
310
+ const dailyResult = await runUnifiedComputation(firestore, logger, date, dailyMissing, 'Pass 2 (Daily)', dailyCalculations, config);
311
+ results.push(dailyResult);
251
312
  }
252
- return Promise.all(promises);
313
+ // Return combined results or a summary
314
+ return results.length === 1 ? results[0] : { date: date.toISOString().slice(0,10), results };
253
315
  },
254
- 'Pass 2'
316
+ 'Pass 2',
317
+ config // Pass config to processJobsInParallel
255
318
  );
319
+ // Process results (handle fulfilled/rejected)
256
320
  summary.pass2_results = pass2Results.map((r, i) => r.status === 'fulfilled' ? r.value : { success: false, date: pass2Jobs[i].date, error: r.reason?.message });
257
321
 
322
+
258
323
  // --- FINISH ---
259
- // The wrapper function will log success and send the response.
324
+ logger.log('INFO', '[Orchestrator] Computation orchestration finished.');
260
325
  return summary;
261
326
  }
262
327
 
263
328
  module.exports = {
264
329
  runComputationOrchestrator,
265
- // We don't need to export runUnifiedComputation as it's only used internally
330
+ // runUnifiedComputation is only used internally now
266
331
  };
@@ -1,9 +1,11 @@
1
1
  /**
2
- * @fileoverview Exports the computation system's main logic.
2
+ * @fileoverview Exports the computation system's main logic and handler creator.
3
3
  */
4
4
 
5
5
  const { runComputationOrchestrator } = require('./helpers/orchestration_helpers.js');
6
+ const { createComputationSystemHandler } = require('./handler_creator.js'); // <-- Import the new factory
6
7
 
7
8
  module.exports = {
8
9
  runComputationOrchestrator,
10
+ createComputationSystemHandler, // <-- Export the factory
9
11
  };
@@ -4,25 +4,26 @@
4
4
  */
5
5
 
6
6
  const { logger } = require("sharedsetup")(__filename);
7
- // Import withRetry from the unified calculations package
8
7
  const { withRetry } = require('aiden-shared-calculations-unified').utils;
9
8
 
10
- const COLLECTIONS_TO_QUERY = ['NormalUserPortfolios', 'SpeculatorUserPortfolios'];
11
- const PART_REF_BATCH_SIZE = 50; // Number of document snapshots to load at once
9
+ // REMOVE Constants like COLLECTIONS_TO_QUERY, PART_REF_BATCH_SIZE
12
10
 
11
+ // Modify getPortfolioPartRefs to accept config
13
12
  /**
14
13
  * Gets a list of all portfolio "part" document references for a given date.
15
- * This function is lightweight and only fetches references, not data.
16
14
  *
17
15
  * @param {Firestore} firestore A Firestore client instance.
18
16
  * @param {string} dateString The date in YYYY-MM-DD format.
17
+ * @param {object} config The computation system configuration object.
19
18
  * @returns {Promise<Firestore.DocumentReference[]>} An array of DocumentReferences.
20
19
  */
21
- async function getPortfolioPartRefs(firestore, dateString) {
20
+ async function getPortfolioPartRefs(firestore, dateString, config) {
22
21
  logger.log('INFO', `Getting portfolio part references for date: ${dateString}`);
23
22
  const allPartRefs = [];
23
+ // Use config for collection names
24
+ const collectionsToQuery = [config.normalUserPortfolioCollection, config.speculatorPortfolioCollection];
24
25
 
25
- for (const collectionName of COLLECTIONS_TO_QUERY) {
26
+ for (const collectionName of collectionsToQuery) {
26
27
  const blockDocsQuery = firestore.collection(collectionName);
27
28
  const blockDocRefs = await withRetry(
28
29
  () => blockDocsQuery.listDocuments(),
@@ -30,12 +31,13 @@ async function getPortfolioPartRefs(firestore, dateString) {
30
31
  );
31
32
 
32
33
  if (blockDocRefs.length === 0) {
33
- logger.log('WARN', `No block documents found in collection: ${collectionName}`);
34
- continue;
35
- }
34
+ logger.log('WARN', `No block documents found in collection: ${collectionName}`);
35
+ continue;
36
+ }
36
37
 
37
38
  for (const blockDocRef of blockDocRefs) {
38
- const partsCollectionRef = blockDocRef.collection('snapshots').doc(dateString).collection('parts');
39
+ // Use config for subcollection names
40
+ const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection).doc(dateString).collection(config.partsSubcollection);
39
41
  const partDocs = await withRetry(
40
42
  () => partsCollectionRef.listDocuments(),
41
43
  `listDocuments(${partsCollectionRef.path})`
@@ -48,30 +50,26 @@ async function getPortfolioPartRefs(firestore, dateString) {
48
50
  return allPartRefs;
49
51
  }
50
52
 
51
-
53
+ // Modify loadDataByRefs to accept config
52
54
  /**
53
55
  * Loads data from an array of DocumentReferences using firestore.getAll().
54
- * Merges all data into a single map keyed by user ID.
55
56
  *
56
57
  * @param {Firestore} firestore A Firestore client instance.
57
58
  * @param {Firestore.DocumentReference[]} refs An array of DocumentReferences to load.
59
+ * @param {object} config The computation system configuration object.
58
60
  * @returns {Promise<object>} A single map of { [userId]: portfolioData }.
59
61
  */
60
- async function loadDataByRefs(firestore, refs) {
61
- if (!refs || refs.length === 0) {
62
- return {};
63
- }
64
-
62
+ async function loadDataByRefs(firestore, refs, config) {
63
+ if (!refs || refs.length === 0) { return {}; }
65
64
  const mergedPortfolios = {};
65
+ const batchSize = config.partRefBatchSize || 50; // Use config
66
66
 
67
- for (let i = 0; i < refs.length; i += PART_REF_BATCH_SIZE) {
68
- const batchRefs = refs.slice(i, i + PART_REF_BATCH_SIZE);
69
-
67
+ for (let i = 0; i < refs.length; i += batchSize) { // Use config batch size
68
+ const batchRefs = refs.slice(i, i + batchSize); // Use config batch size
70
69
  const snapshots = await withRetry(
71
70
  () => firestore.getAll(...batchRefs),
72
- `getAll(batch ${Math.floor(i / PART_REF_BATCH_SIZE)})`
71
+ `getAll(batch ${Math.floor(i / batchSize)})` // Use config batch size
73
72
  );
74
-
75
73
  for (const doc of snapshots) {
76
74
  if (doc.exists) {
77
75
  const data = doc.data();
@@ -86,11 +84,19 @@ async function loadDataByRefs(firestore, refs) {
86
84
  return mergedPortfolios;
87
85
  }
88
86
 
89
- /** Loads all portfolio data for a day by streaming part references. */
90
- async function loadFullDayMap(firestore, partRefs) {
87
+ // Modify loadFullDayMap to accept config
88
+ /**
89
+ * Loads all portfolio data for a day by streaming part references.
90
+ * @param {Firestore} firestore A Firestore client instance.
91
+ * @param {Firestore.DocumentReference[]} partRefs Array of part document references.
92
+ * @param {object} config The computation system configuration object.
93
+ * @returns {Promise<object>} A single map of { [userId]: portfolioData }.
94
+ */
95
+ async function loadFullDayMap(firestore, partRefs, config) {
91
96
  if (partRefs.length === 0) return {};
92
97
  logger.log('TRACE', `Loading full day map from ${partRefs.length} references...`);
93
- const fullMap = await loadDataByRefs(firestore, partRefs);
98
+ // Pass config here
99
+ const fullMap = await loadDataByRefs(firestore, partRefs, config);
94
100
  logger.log('TRACE', `Full day map loaded with ${Object.keys(fullMap).length} users.`);
95
101
  return fullMap;
96
102
  }
@@ -1,20 +1,19 @@
1
1
  /**
2
2
  * @fileoverview Helper functions for the Unified Computation System.
3
- * This module imports all calculations and utilities from the single
4
- * `aiden-shared-calculations-unified` package and categorizes them for the orchestrator.
3
+ * Categorizes calculations and provides utility functions like retry logic,
4
+ * batch committing, date generation, parallel processing, and finding earliest dates.
5
5
  */
6
6
 
7
7
  const { FieldValue, FieldPath } = require('@google-cloud/firestore');
8
8
  const { logger } = require("sharedsetup")(__filename);
9
+ // Import calculations and utils from the unified package
9
10
  const { calculations, utils } = require('aiden-shared-calculations-unified');
11
+ const { withRetry } = utils; // Get withRetry from the package utils
10
12
 
11
- const { withRetry } = utils; // Get withRetry from the package.
12
-
13
- // --- Configuration ---
14
- const BATCH_SIZE_LIMIT = 500;
15
- const MAX_CONCURRENT_DATES = 3;
13
+ // REMOVE Constants like BATCH_SIZE_LIMIT, MAX_CONCURRENT_DATES
16
14
 
17
15
  // --- Calculation Categorization ---
16
+ // This relies on the structure exported by 'aiden-shared-calculations-unified'
18
17
  const HISTORICAL_CALC_NAMES = new Set([
19
18
  'paper-vs-diamond-hands', 'smart-money-flow', 'profitability-migration',
20
19
  'user-profitability-tracker', 'sector-rotation', 'crowd-conviction-score',
@@ -25,6 +24,7 @@ const HISTORICAL_CALC_NAMES = new Set([
25
24
  const historicalCalculations = {};
26
25
  const dailyCalculations = {};
27
26
 
27
+ // Assuming 'calculations' from the package has the same category structure
28
28
  for (const category in calculations) {
29
29
  for (const calcName in calculations[category]) {
30
30
  if (HISTORICAL_CALC_NAMES.has(calcName)) {
@@ -37,22 +37,28 @@ for (const category in calculations) {
37
37
  }
38
38
  }
39
39
 
40
-
41
40
  // --- UTILITY FUNCTIONS ---
42
41
 
43
- /** Commits Firestore batch writes in chunks to respect the 500 operation limit. */
44
- async function commitBatchInChunks(firestore, writes, operationName) {
42
+ /**
43
+ * Commits Firestore batch writes in chunks based on config limit.
44
+ * @param {Firestore} firestore A Firestore client instance.
45
+ * @param {Array<object>} writes Array of { ref: DocumentReference, data: object }.
46
+ * @param {string} operationName Name for logging.
47
+ * @param {object} config The computation system configuration object.
48
+ */
49
+ async function commitBatchInChunks(firestore, writes, operationName, config) {
50
+ const batchSizeLimit = config.batchSizeLimit || 450; // Use config with default
45
51
  if (writes.length === 0) {
46
52
  logger.log('WARN', `[${operationName}] No writes to commit.`);
47
53
  return;
48
54
  }
49
- for (let i = 0; i < writes.length; i += BATCH_SIZE_LIMIT) {
55
+ for (let i = 0; i < writes.length; i += batchSizeLimit) { // Use config limit
50
56
  const batch = firestore.batch();
51
- const chunk = writes.slice(i, i + BATCH_SIZE_LIMIT);
57
+ const chunk = writes.slice(i, i + batchSizeLimit); // Use config limit
52
58
  chunk.forEach(write => batch.set(write.ref, write.data, { merge: true }));
53
-
54
- const chunkNum = (i / BATCH_SIZE_LIMIT) + 1;
55
- const totalChunks = Math.ceil(writes.length / BATCH_SIZE_LIMIT);
59
+
60
+ const chunkNum = Math.floor(i / batchSizeLimit) + 1; // Use config limit
61
+ const totalChunks = Math.ceil(writes.length / batchSizeLimit); // Use config limit
56
62
  await withRetry(
57
63
  () => batch.commit(),
58
64
  `${operationName} (Chunk ${chunkNum}/${totalChunks})`
@@ -67,22 +73,35 @@ async function commitBatchInChunks(firestore, writes, operationName) {
67
73
  function getExpectedDateStrings(startDate, endDate) {
68
74
  const dateStrings = [];
69
75
  if (startDate <= endDate) {
70
- for (let d = new Date(startDate); d <= endDate; d.setUTCDate(d.getUTCDate() + 1)) {
76
+ // Ensure dates are treated as UTC to avoid timezone issues
77
+ const startUTC = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()));
78
+ const endUTC = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()));
79
+ for (let d = startUTC; d <= endUTC; d.setUTCDate(d.getUTCDate() + 1)) {
71
80
  dateStrings.push(new Date(d).toISOString().slice(0, 10));
72
81
  }
73
82
  }
74
83
  return dateStrings;
75
84
  }
76
85
 
77
- /** Processes job objects in parallel batches. */
78
- async function processJobsInParallel(jobs, taskFunction, passName) {
86
+
87
+ /**
88
+ * Processes job objects in parallel batches based on config limit.
89
+ * @param {Array<object>} jobs Array of jobs, each with { date, missing }.
90
+ * @param {Function} taskFunction Async function to process a job (receives Date, missing array).
91
+ * @param {string} passName Name for logging.
92
+ * @param {object} config The computation system configuration object.
93
+ * @returns {Promise<Array<PromiseSettledResult>>} Array of promise settlement results.
94
+ */
95
+ async function processJobsInParallel(jobs, taskFunction, passName, config) {
79
96
  const results = [];
97
+ const maxConcurrentDates = config.maxConcurrentDates || 3; // Use config
80
98
  if (jobs.length > 0) {
81
- logger.log('INFO', `[${passName}] Processing ${jobs.length} jobs with ${MAX_CONCURRENT_DATES} parallel workers.`);
82
- jobs.sort((a, b) => new Date(a.date) - new Date(b.date)); // Ensure chronological processing
83
- for (let i = 0; i < jobs.length; i += MAX_CONCURRENT_DATES) {
84
- const jobBatch = jobs.slice(i, i + MAX_CONCURRENT_DATES);
85
- const promises = jobBatch.map(job => taskFunction(new Date(job.date), job.missing));
99
+ logger.log('INFO', `[${passName}] Processing ${jobs.length} jobs with ${maxConcurrentDates} parallel workers.`); // Use config
100
+ jobs.sort((a, b) => new Date(a.date) - new Date(b.date)); // Ensure chronological
101
+ for (let i = 0; i < jobs.length; i += maxConcurrentDates) { // Use config
102
+ const jobBatch = jobs.slice(i, i + maxConcurrentDates); // Use config
103
+ // taskFunction now receives config implicitly when called via runUnifiedComputation
104
+ const promises = jobBatch.map(job => taskFunction(new Date(job.date + 'T00:00:00Z'), job.missing)); // Ensure date is parsed correctly
86
105
  results.push(...await Promise.allSettled(promises));
87
106
  }
88
107
  } else {
@@ -92,10 +111,13 @@ async function processJobsInParallel(jobs, taskFunction, passName) {
92
111
  }
93
112
 
94
113
  /**
95
- * Finds the earliest date document by searching inside the 'snapshots' subcollection
96
- * of all block documents (e.g., '1M', '2M') within a given collection.
114
+ * Finds the earliest date document in a collection's snapshots.
115
+ * @param {Firestore} firestore A Firestore client instance.
116
+ * @param {string} collectionName The top-level portfolio collection name.
117
+ * @param {object} config The computation system configuration object.
118
+ * @returns {Promise<Date|null>} The earliest date found, or null.
97
119
  */
98
- async function getFirstDateFromCollection(firestore, collectionName) {
120
+ async function getFirstDateFromCollection(firestore, collectionName, config) {
99
121
  let earliestDate = null;
100
122
  try {
101
123
  const blockDocRefs = await withRetry(
@@ -103,29 +125,32 @@ async function getFirstDateFromCollection(firestore, collectionName) {
103
125
  `GetBlocks(${collectionName})`
104
126
  );
105
127
 
106
- if (blockDocRefs.length === 0) {
107
- logger.log('WARN', `No block documents found in collection: ${collectionName}`);
108
- return null;
109
- }
128
+ if (blockDocRefs.length === 0) {
129
+ logger.log('WARN', `No block documents found in collection: ${collectionName}`);
130
+ return null;
131
+ }
132
+
133
+ for (const blockDocRef of blockDocRefs) {
134
+ // Use config subcollection name
135
+ const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection)
136
+ .where(FieldPath.documentId(), '>=', '2000-01-01') // Basic date format check
137
+ .orderBy(FieldPath.documentId(), 'asc')
138
+ .limit(1);
139
+
140
+ const snapshotSnap = await withRetry(
141
+ () => snapshotQuery.get(),
142
+ `GetEarliestSnapshot(${blockDocRef.path})`
143
+ );
144
+
145
+ // Validate the ID format is YYYY-MM-DD before parsing
146
+ if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
147
+ const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z'); // Treat as UTC
148
+ if (!earliestDate || foundDate < earliestDate) {
149
+ earliestDate = foundDate;
150
+ }
151
+ }
152
+ }
110
153
 
111
- for (const blockDocRef of blockDocRefs) {
112
- const snapshotQuery = blockDocRef.collection('snapshots')
113
- .where(FieldPath.documentId(), '>=', '2000-01-01')
114
- .orderBy(FieldPath.documentId(), 'asc')
115
- .limit(1);
116
-
117
- const snapshotSnap = await withRetry(
118
- () => snapshotQuery.get(),
119
- `GetEarliestSnapshot(${blockDocRef.path})`
120
- );
121
-
122
- if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
123
- const foundDate = new Date(snapshotSnap.docs[0].id);
124
- if (!earliestDate || foundDate < earliestDate) {
125
- earliestDate = foundDate;
126
- }
127
- }
128
- }
129
154
  } catch (e) {
130
155
  logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
131
156
  }
@@ -133,32 +158,34 @@ async function getFirstDateFromCollection(firestore, collectionName) {
133
158
  }
134
159
 
135
160
  /**
136
- * Determines the absolute earliest date from source portfolio data collections.
161
+ * Determines the absolute earliest date from source portfolio data collections using config.
162
+ * @param {Firestore} firestore A Firestore client instance.
163
+ * @param {object} config The computation system configuration object.
164
+ * @returns {Promise<Date>} The earliest date found or a default fallback date.
137
165
  */
138
- async function getFirstDateFromSourceData(firestore) {
166
+ async function getFirstDateFromSourceData(firestore, config) {
139
167
  logger.log('INFO', 'Querying for the earliest date from source portfolio data...');
140
-
141
- const investorDate = await getFirstDateFromCollection(firestore, 'NormalUserPortfolios');
142
- const speculatorDate = await getFirstDateFromCollection(firestore, 'SpeculatorUserPortfolios');
143
-
144
- let earliestDate;
145
-
146
- if (investorDate && speculatorDate) {
147
- earliestDate = investorDate < speculatorDate ? investorDate : speculatorDate;
148
- } else {
149
- earliestDate = investorDate || speculatorDate;
150
- }
151
-
152
- if (earliestDate) {
153
- logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
154
- return earliestDate;
155
- }
156
-
157
- // Fallback if no source data is found at all.
158
- const d = new Date();
159
- d.setUTCDate(d.getUTCDate() - 30);
160
- logger.log('WARN', `No source data found. Defaulting first date to: ${d.toISOString().slice(0, 10)}`);
161
- return d;
168
+ // Use config for collection names
169
+ const investorDate = await getFirstDateFromCollection(firestore, config.normalUserPortfolioCollection, config);
170
+ const speculatorDate = await getFirstDateFromCollection(firestore, config.speculatorPortfolioCollection, config);
171
+
172
+ let earliestDate;
173
+ // Determine the earlier of the two dates found
174
+ if (investorDate && speculatorDate) {
175
+ earliestDate = investorDate < speculatorDate ? investorDate : speculatorDate;
176
+ } else {
177
+ earliestDate = investorDate || speculatorDate; // Use whichever one was found
178
+ }
179
+
180
+ if (earliestDate) {
181
+ logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
182
+ return earliestDate;
183
+ } else {
184
+ // Fallback using config.earliestComputationDate or a hardcoded default
185
+ const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
186
+ logger.log('WARN', `No source data found. Defaulting first date to: ${fallbackDate.toISOString().slice(0, 10)}`);
187
+ return fallbackDate;
188
+ }
162
189
  }
163
190
 
164
191
  module.exports = {
@@ -10,7 +10,7 @@ const { dispatchTasksInBatches } = require('./helpers/dispatch_helpers');
10
10
  * @param {object} config - Configuration object loaded from the calling function's context.
11
11
  * @returns {Function} The Cloud Function handler.
12
12
  */
13
- function createDispatcherHandler(pubsubClient, config) {
13
+ function createDispatcherHandler(config, pubsubClient) {
14
14
  return async (message, context) => {
15
15
  try {
16
16
  // 1. Decode Message
@@ -4,14 +4,15 @@
4
4
  */
5
5
 
6
6
  const { FieldPath } = require('@google-cloud/firestore');
7
- const MAX_DATE_RANGE = 100; // Limit queries to a 100-day range
7
+ // REMOVE: const MAX_DATE_RANGE = 100;
8
8
 
9
9
  /**
10
- * Validates request parameters.
10
+ * Validates request parameters using config.
11
11
  * @param {object} query - The request query object.
12
+ * @param {object} config - The Generic API V2 configuration object.
12
13
  * @returns {string|null} An error message if validation fails, otherwise null.
13
14
  */
14
- const validateRequest = (query) => {
15
+ const validateRequest = (query, config) => {
15
16
  if (!query.computations) return "Missing 'computations' parameter.";
16
17
  if (!query.startDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.startDate)) return "Missing or invalid 'startDate'.";
17
18
  if (!query.endDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.endDate)) return "Missing or invalid 'endDate'.";
@@ -20,9 +21,10 @@ const validateRequest = (query) => {
20
21
  const end = new Date(query.endDate);
21
22
  if (end < start) return "'endDate' must be after 'startDate'.";
22
23
 
24
+ const maxDateRange = config.maxDateRange || 100; // Use config value with default
23
25
  const diffTime = Math.abs(end - start);
24
26
  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
25
- if (diffDays > MAX_DATE_RANGE) return `Date range cannot exceed ${MAX_DATE_RANGE} days.`;
27
+ if (diffDays > maxDateRange) return `Date range cannot exceed ${maxDateRange} days.`;
26
28
 
27
29
  return null;
28
30
  };
@@ -38,7 +40,7 @@ const buildCalculationMap = (unifiedCalculations) => {
38
40
  for (const calcName in unifiedCalculations[category]) {
39
41
  calcMap[calcName] = {
40
42
  category: category,
41
- path: `unified_insights/{date}/results/${category}/computations/${calcName}`
43
+ // Path construction moved to fetchUnifiedData where config is available
42
44
  };
43
45
  }
44
46
  }
@@ -55,7 +57,7 @@ const getDateStringsInRange = (startDate, endDate) => {
55
57
  const dates = [];
56
58
  const current = new Date(startDate + 'T00:00:00Z');
57
59
  const end = new Date(endDate + 'T00:00:00Z');
58
-
60
+
59
61
  while (current <= end) {
60
62
  dates.push(current.toISOString().slice(0, 10));
61
63
  current.setUTCDate(current.getUTCDate() + 1);
@@ -64,16 +66,22 @@ const getDateStringsInRange = (startDate, endDate) => {
64
66
  };
65
67
 
66
68
  /**
67
- * Fetches all requested computation data from Firestore for the given date range.
69
+ * Fetches all requested computation data from Firestore for the given date range using config.
70
+ * @param {object} config - The Generic API V2 configuration object.
68
71
  * @param {Firestore} firestore - A Firestore client instance.
69
72
  * @param {Logger} logger - A logger instance.
70
73
  * @param {string[]} calcKeys - Array of computation keys to fetch.
71
74
  * @param {string[]} dateStrings - Array of dates to fetch for.
72
- * @param {Object} calcMap - The pre-built calculation lookup map.
75
+ * @param {Object} calcMap - The pre-built calculation lookup map (only category needed).
73
76
  * @returns {Promise<Object>} A nested object of [date][computationKey] = data.
74
77
  */
75
- const fetchUnifiedData = async (firestore, logger, calcKeys, dateStrings, calcMap) => {
78
+ const fetchUnifiedData = async (config, firestore, logger, calcKeys, dateStrings, calcMap) => {
76
79
  const response = {};
80
+ // Use collection/subcollection names from config
81
+ const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
82
+ const resultsSub = config.resultsSubcollection || 'results';
83
+ const compsSub = config.computationsSubcollection || 'computations';
84
+
77
85
  try {
78
86
  for (const date of dateStrings) {
79
87
  response[date] = {};
@@ -83,14 +91,15 @@ const fetchUnifiedData = async (firestore, logger, calcKeys, dateStrings, calcMa
83
91
  for (const key of calcKeys) {
84
92
  const pathInfo = calcMap[key];
85
93
  if (pathInfo) {
86
- const docRef = firestore.collection('unified_insights').doc(date)
87
- .collection('results').doc(pathInfo.category)
88
- .collection('computations').doc(key);
89
-
94
+ // Construct path using config values
95
+ const docRef = firestore.collection(insightsCollection).doc(date)
96
+ .collection(resultsSub).doc(pathInfo.category)
97
+ .collection(compsSub).doc(key);
98
+
90
99
  docRefs.push(docRef);
91
100
  keyPaths.push(key);
92
101
  } else {
93
- logger.log('WARN', `[${date}] No path found for computation key: ${key}`);
102
+ logger.log('WARN', `[${date}] No path info found for computation key: ${key}`);
94
103
  }
95
104
  }
96
105
 
@@ -114,17 +123,19 @@ const fetchUnifiedData = async (firestore, logger, calcKeys, dateStrings, calcMa
114
123
  };
115
124
 
116
125
  /**
117
- * Creates the main Express request handler for the API.
126
+ * Creates the main Express request handler for the API, injecting config.
127
+ * @param {object} config - The Generic API V2 configuration object.
118
128
  * @param {Firestore} firestore - A Firestore client instance.
119
129
  * @param {Logger} logger - A logger instance.
120
130
  * @param {Object} calcMap - The pre-built calculation lookup map.
121
131
  * @returns {Function} An async Express request handler.
122
132
  */
123
- const createApiHandler = (firestore, logger, calcMap) => {
133
+ const createApiHandler = (config, firestore, logger, calcMap) => {
124
134
  return async (req, res) => {
125
- const validationError = validateRequest(req.query);
135
+ // Pass config to validator
136
+ const validationError = validateRequest(req.query, config);
126
137
  if (validationError) {
127
- logger.log('WARN', 'Bad Request', { error: validationError });
138
+ logger.log('WARN', 'API Bad Request', { error: validationError, query: req.query });
128
139
  return res.status(400).send({ status: 'error', message: validationError });
129
140
  }
130
141
 
@@ -132,7 +143,8 @@ const createApiHandler = (firestore, logger, calcMap) => {
132
143
  const computationKeys = req.query.computations.split(',');
133
144
  const dateStrings = getDateStringsInRange(req.query.startDate, req.query.endDate);
134
145
 
135
- const data = await fetchUnifiedData(firestore, logger, computationKeys, dateStrings, calcMap);
146
+ // Pass config to data fetcher
147
+ const data = await fetchUnifiedData(config, firestore, logger, computationKeys, dateStrings, calcMap);
136
148
 
137
149
  res.status(200).send({
138
150
  status: 'success',
@@ -152,5 +164,6 @@ const createApiHandler = (firestore, logger, calcMap) => {
152
164
 
153
165
  module.exports = {
154
166
  buildCalculationMap,
155
- createApiHandler,
167
+ createApiHandler, // Export the factory for the handler
168
+ // Internal helpers like validateRequest, getDateStringsInRange, fetchUnifiedData are not exported
156
169
  };
@@ -9,24 +9,26 @@ const { buildCalculationMap, createApiHandler } = require('./helpers/api_helpers
9
9
 
10
10
  /**
11
11
  * Creates and configures the Express app for the Generic API.
12
- * @param {Firestore} firestore A Firestore client instance.
13
- * @param {Logger} logger A logger instance.
14
- * @param {Object} unifiedCalculations The calculations manifest.
12
+ * @param {object} config - The Generic API V2 configuration object.
13
+ * @param {object} dependencies - Shared dependencies { firestore, logger }.
14
+ * @param {Object} unifiedCalculations - The calculations manifest.
15
15
  * @returns {express.Application} The configured Express app.
16
16
  */
17
- function createApiApp(firestore, logger, unifiedCalculations) {
17
+ function createApiApp(config, dependencies, unifiedCalculations) {
18
18
  const app = express();
19
-
19
+ const { firestore, logger } = dependencies; // Extract dependencies
20
+
20
21
  // --- Pre-compute Calculation Map ---
22
+ // This doesn't need config directly
21
23
  const calcMap = buildCalculationMap(unifiedCalculations);
22
-
24
+
23
25
  // --- Middleware ---
24
26
  app.use(cors({ origin: true }));
25
27
  app.use(express.json());
26
28
 
27
29
  // --- Main API Endpoint ---
28
- // The handler is created with its dependencies injected
29
- app.get('/', createApiHandler(firestore, logger, calcMap));
30
+ // The handler is created with its dependencies and config injected
31
+ app.get('/', createApiHandler(config, firestore, logger, calcMap));
30
32
 
31
33
  // --- Health Check Endpoint ---
32
34
  app.get('/health', (req, res) => {
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @fileoverview Factory function to create the Task Engine handler.
3
+ */
4
+ const { Firestore } = require('@google-cloud/firestore'); // Needed if db is not passed in
5
+ const { PubSub } = require('@google-cloud/pubsub'); // Needed if pubsub is not passed in
6
+ const { logger: defaultLogger } = require("sharedsetup")(__filename); // Use default logger if none provided
7
+ const { core } = require('../../'); // Access core utilities like managers
8
+ const TaskEngineHelpers = require('./helpers');
9
+ const TaskEngineUtils = require('./utils');
10
+
11
+ /**
12
+ * Creates the Task Engine Cloud Function handler.
13
+ * @param {object} config - The Task Engine configuration object (e.g., cfg.taskEngine).
14
+ * @param {object} [dependencies] - Optional dependencies. If not provided, initializes defaults.
15
+ * @param {Firestore} [dependencies.firestore] - Firestore client instance.
16
+ * @param {PubSub} [dependencies.pubsub] - PubSub client instance.
17
+ * @param {object} [dependencies.logger] - Logger instance.
18
+ * @returns {Function} The async Cloud Function handler for Pub/Sub messages.
19
+ */
20
+ function createTaskEngineHandler(config, dependencies = {}) {
21
+ // Initialize clients using provided dependencies or defaults
22
+ const firestore = dependencies.firestore || new Firestore();
23
+ const pubsub = dependencies.pubsub || new PubSub();
24
+ const logger = dependencies.logger || defaultLogger; // Use provided or default logger
25
+
26
+ // Initialize managers and batch manager within this scope
27
+ const headerManager = new core.utils.IntelligentHeaderManager(firestore, logger, config);
28
+ const proxyManager = new core.utils.IntelligentProxyManager(firestore, logger, config);
29
+ const batchManager = new TaskEngineUtils.FirestoreBatchManager(firestore, headerManager, config); // Pass headerManager
30
+
31
+ const CLIENTS = {
32
+ pubsub,
33
+ nativeFirestore: firestore, // Pass the initialized Firestore instance
34
+ headerManager,
35
+ proxyManager,
36
+ batchManager
37
+ };
38
+
39
+ // Return the actual handler function
40
+ return async (message) => {
41
+ // Basic check for message structure (Cloud Functions Pub/Sub trigger)
42
+ if (!message || !message.data) {
43
+ logger.log('ERROR', '[TaskEngine Module] Received invalid message structure.', { message });
44
+ // Acknowledge Pub/Sub by not throwing, but log error
45
+ return;
46
+ }
47
+
48
+ let task;
49
+ try {
50
+ // Decode the message
51
+ task = JSON.parse(Buffer.from(message.data, 'base64').toString('utf-8'));
52
+ } catch (e) {
53
+ logger.log('ERROR', '[TaskEngine Module] Failed to parse Pub/Sub message data.', { error: e.message, data: message.data });
54
+ // Acknowledge Pub/Sub by not throwing
55
+ return;
56
+ }
57
+
58
+ // Generate a unique ID for logging this specific task execution
59
+ const taskId = `${task.type || 'unknown'}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || 'batch'}-${Date.now()}`;
60
+ logger.log('INFO', `[TaskEngine/${taskId}] Received.`);
61
+
62
+ try {
63
+ // Optional: Random delay
64
+ await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 250));
65
+
66
+ // Determine the correct handler function based on task type
67
+ const handlerFunction = {
68
+ discover: TaskEngineHelpers.handleDiscover,
69
+ verify: TaskEngineHelpers.handleVerify,
70
+ update: TaskEngineHelpers.handleUpdate
71
+ }[task.type];
72
+
73
+ // Execute the handler or log an error if the type is unknown
74
+ if (handlerFunction) {
75
+ await handlerFunction(task, taskId, CLIENTS, config); // Pass CLIENTS and config
76
+ logger.log('SUCCESS', `[TaskEngine/${taskId}] Done.`);
77
+ } else {
78
+ logger.log('ERROR', `[TaskEngine/${taskId}] Unknown task type received: ${task.type}`);
79
+ // Acknowledge Pub/Sub by not throwing
80
+ }
81
+ } catch (error) {
82
+ // Log the error but crucially *do not re-throw* if you want Pub/Sub to ack the message
83
+ // Only re-throw if you want Pub/Sub to attempt redelivery based on its settings.
84
+ // Your existing logic logs but then re-throws - decide if that's the desired behavior.
85
+ logger.log('ERROR', `[TaskEngine/${taskId}] Failed.`, { errorMessage: error.message, errorStack: error.stack });
86
+ // throw error; // Optional: Uncomment if you want Pub/Sub to retry
87
+ } finally {
88
+ // Ensure batches are flushed eventually, though the batch manager handles timed flushes.
89
+ // A final flush here might be redundant if the instance stays alive, but could be useful
90
+ // if the instance might terminate immediately after processing.
91
+ try {
92
+ // Consider if flushing on every single message is desired vs relying on timed flush
93
+ // await batchManager.flushBatches();
94
+ } catch (flushError) {
95
+ logger.log('ERROR', `[TaskEngine/${taskId}] Error during final flush attempt.`, { error: flushError.message });
96
+ }
97
+ }
98
+ };
99
+ }
100
+
101
+ module.exports = {
102
+ createTaskEngineHandler,
103
+ };
@@ -4,8 +4,10 @@
4
4
 
5
5
  const helpers = require('./helpers');
6
6
  const utils = require('./utils');
7
+ const { createTaskEngineHandler } = require('./handler_creator'); // <-- Import the new factory
7
8
 
8
9
  module.exports = {
9
10
  helpers,
10
11
  utils,
12
+ createTaskEngineHandler, // <-- Export the factory
11
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [