@@ -218,6 +218,182 @@ export class QueueService {
218218 return finalJob ;
219219 }
220220
221+ /**
222+ * From a follow-up job (e.g. scope1+2 or scope3), find the original
223+ * EXTRACT_EMISSIONS job for the same process/thread and enqueue a new
224+ * extract-emissions job with runOnly set to the requested scopes.
225+ */
226+ public async rerunExtractEmissionsFromFollowup (
227+ followupQueueName : string ,
228+ followupJobId : string ,
229+ scopes : string [ ] ,
230+ ) : Promise < DataJob > {
231+ console . info ( '[QueueService] rerunExtractEmissionsFromFollowup: Starting' , {
232+ followupQueueName,
233+ followupJobId,
234+ scopes,
235+ } ) ;
236+
237+ const followupJob = await this . getFollowupJob ( followupQueueName , followupJobId ) ;
238+ const threadId = this . getThreadIdFromJob ( followupJob ) ;
239+
240+ const extractEmissionsJob = await this . getLatestExtractEmissionsJobForThread ( threadId ) ;
241+ const fiscalYear = await this . getLatestFiscalYearForThread ( threadId ) ;
242+
243+ const companyName = this . getCompanyNameFromJobs (
244+ extractEmissionsJob ,
245+ followupJob ,
246+ threadId
247+ ) ;
248+
249+ const mergedData = this . buildExtractRerunData (
250+ followupJob ,
251+ extractEmissionsJob ,
252+ fiscalYear ,
253+ scopes
254+ ) ;
255+
256+ const newJob = await this . enqueueExtractRerun ( companyName , mergedData ) ;
257+
258+ console . info ( '[QueueService] rerunExtractEmissionsFromFollowup: New job created' , {
259+ newJobId : newJob . id ,
260+ scopes,
261+ } ) ;
262+
263+ return this . getJobData ( QUEUE_NAMES . EXTRACT_EMISSIONS , newJob . id ! ) ;
264+ }
265+
266+ private async getFollowupJob (
267+ followupQueueName : string ,
268+ followupJobId : string
269+ ) : Promise < DataJob > {
270+ return this . getJobData ( followupQueueName , followupJobId ) ;
271+ }
272+
273+ private getThreadIdFromJob ( job : DataJob ) : string {
274+ const followupData : any = job . data ?? { } ;
275+
276+ const threadId =
277+ followupData . threadId ??
278+ job . threadId ??
279+ job . processId ;
280+
281+ if ( ! threadId ) {
282+ console . error ( '[QueueService] getThreadIdFromJob: Missing threadId' , {
283+ jobId : job . id ,
284+ } ) ;
285+ throw new Error ( 'Cannot locate process/thread for this job (no threadId).' ) ;
286+ }
287+
288+ return threadId ;
289+ }
290+
291+ private async getLatestExtractEmissionsJobForThread ( threadId : string ) : Promise < DataJob > {
292+ const extractJobs = await this . getDataJobs (
293+ [ QUEUE_NAMES . EXTRACT_EMISSIONS ] ,
294+ undefined ,
295+ threadId
296+ ) ;
297+
298+ if ( ! extractJobs . length ) {
299+ console . error ( '[QueueService] getLatestExtractEmissionsJobForThread: No EXTRACT_EMISSIONS job found' , {
300+ threadId,
301+ } ) ;
302+ throw new Error ( 'No EXTRACT_EMISSIONS job found for this process.' ) ;
303+ }
304+
305+ return extractJobs . sort (
306+ ( firstJob , secondJob ) => ( secondJob . timestamp ?? 0 ) - ( firstJob . timestamp ?? 0 )
307+ ) [ 0 ] ;
308+ }
309+
310+ private getCompanyNameFromJobs (
311+ extractEmissionsJob : DataJob ,
312+ followupJob : DataJob ,
313+ threadId : string
314+ ) : string {
315+ const extractData : any = extractEmissionsJob . data ?? { } ;
316+ const followupData : any = followupJob . data ?? { } ;
317+
318+ return (
319+ extractData . companyName ??
320+ followupData . companyName ??
321+ threadId
322+ ) ;
323+ }
324+
325+ private buildExtractRerunData (
326+ followupJob : DataJob ,
327+ extractEmissionsJob : DataJob ,
328+ fiscalYear : any | undefined ,
329+ scopes : string [ ] ,
330+ ) : any {
331+ const extractData : any = extractEmissionsJob . data ?? { } ;
332+ const followupData : any = followupJob . data ?? { } ;
333+
334+ return {
335+ ...extractData ,
336+ ...( followupData . wikidata ? { wikidata : followupData . wikidata } : { } ) ,
337+ ...( fiscalYear ? { fiscalYear } : { } ) ,
338+ runOnly : scopes ,
339+ } ;
340+ }
341+
342+ private async enqueueExtractRerun (
343+ companyName : string ,
344+ jobData : any ,
345+ ) : Promise < Job > {
346+ const extractQueue = await this . getQueue ( QUEUE_NAMES . EXTRACT_EMISSIONS ) ;
347+ return extractQueue . add ( 'rerun emissions ' + companyName , jobData ) ;
348+ }
349+
350+ private async getLatestFiscalYearForThread ( threadId : string ) : Promise < any | undefined > {
351+ // For FOLLOW_UP_FISCAL_YEAR jobs, the fiscal year lives in the *return value* JSON, e.g.:
352+ // { "value": { "fiscalYear": { startMonth, endMonth } }, ... }.
353+ try {
354+ const fiscalJobs = await this . getDataJobs (
355+ [ QUEUE_NAMES . FOLLOW_UP_FISCAL_YEAR ] ,
356+ undefined ,
357+ threadId
358+ ) ;
359+
360+ if ( fiscalJobs . length === 0 ) {
361+ return undefined ;
362+ }
363+
364+ const latestFiscal = fiscalJobs . sort (
365+ ( firstJob , secondJob ) => ( secondJob . timestamp ?? 0 ) - ( firstJob . timestamp ?? 0 )
366+ ) [ 0 ] ;
367+
368+ const returnValue = latestFiscal . returnvalue ;
369+ if ( typeof returnValue === 'string' ) {
370+ try {
371+ const parsed = JSON . parse ( returnValue ) ;
372+ return parsed . fiscalYear ?? parsed . value ?. fiscalYear ?? undefined ;
373+ } catch ( parseErr ) {
374+ console . warn ( '[QueueService] getLatestFiscalYearForThread: Failed to parse fiscalYear returnvalue' , {
375+ threadId,
376+ error : parseErr ,
377+ } ) ;
378+ return undefined ;
379+ }
380+ }
381+
382+ if ( returnValue && typeof returnValue === 'object' ) {
383+ const parsed : any = returnValue ;
384+ return parsed . fiscalYear ?? parsed . value ?. fiscalYear ?? undefined ;
385+ }
386+
387+ return undefined ;
388+ } catch ( err ) {
389+ console . warn ( '[QueueService] getLatestFiscalYearForThread: Failed to fetch FOLLOW_UP_FISCAL_YEAR jobs' , {
390+ threadId,
391+ error : err ,
392+ } ) ;
393+ return undefined ;
394+ }
395+ }
396+
221397 /**
222398 * Re-run all jobs that match a given worker name (e.g. a value in data.runOnly[])
223399 * across one or more queues.
0 commit comments