import { Process, Processor, OnQueueActive, OnQueueCompleted, OnQueueFailed, } from '@nestjs/bull'; import { Logger, Inject } from '@nestjs/common'; import { Job } from 'bull'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { ScheduledWorkflow } from '../entities/scheduled-workflow.entity'; import { WorkflowExecutionLog } from '../entities/workflow-execution-log.entity'; import { WORKFLOW_SCHEDULE_QUEUE, EXECUTE_SCHEDULED_WORKFLOW_JOB, EXECUTION_STATUS_PENDING, EXECUTION_STATUS_RUNNING, EXECUTION_STATUS_COMPLETED, EXECUTION_STATUS_FAILED, EXECUTION_STATUS_PARTIAL, DEFAULT_BATCH_SIZE, } from '../constants/schedule.constants'; import { ScheduleJobData, BatchProcessingResult, } from '../interfaces/schedule-job-data.interface'; import { ScheduleHandlerService } from 'src/module/workflow-automation/service/schedule-handler.service'; import { ReflectionHelper } from 'src/utils/service/reflection-helper.service'; /** * Schedule Processor * Processes scheduled workflow execution jobs from Bull queue */ @Processor(WORKFLOW_SCHEDULE_QUEUE) export class ScheduleProcessor { private readonly logger = new Logger(ScheduleProcessor.name); constructor( @InjectRepository(ScheduledWorkflow) private readonly scheduledWorkflowRepository: Repository, @InjectRepository(WorkflowExecutionLog) private readonly executionLogRepository: Repository, @Inject('ScheduleHandlerService') private readonly scheduleHandlerService: ScheduleHandlerService, private readonly reflectionHelper: ReflectionHelper, private readonly dataSource: DataSource ) {} /** * Main job processor for scheduled workflow execution */ @Process(EXECUTE_SCHEDULED_WORKFLOW_JOB) async handleScheduledWorkflowExecution(job: Job) { const { schedule_id, workflow_id, organization_id, triggeredBy, createdBy } = job.data; this.logger.log( `๐Ÿš€ [handleScheduledWorkflowExecution] Invoked for schedule_id=${schedule_id}, workflowId=${workflow_id}`, ); // Create execution log entry const executionLog = this.executionLogRepository.create({ schedule_id: schedule_id, workflow_id: workflow_id, job_id: job.id.toString(), execution_status: EXECUTION_STATUS_PENDING, triggered_by: triggeredBy, triggered_by_user_id: triggeredBy === 'MANUAL' ? createdBy : null, organization_id: organization_id, enterprise_id: job.data.enterprise_id, created_by: createdBy, entity_type: 'WFEL', }); this.logger.debug(`๐Ÿงพ Creating execution log for jobId=${job.id}`); await this.executionLogRepository.save(executionLog); try { // Update status to running executionLog.execution_status = EXECUTION_STATUS_RUNNING; executionLog.started_at = new Date(); await this.executionLogRepository.save(executionLog); this.logger.log( `๐Ÿƒ Workflow execution started for schedule_id=${schedule_id}`, ); // Get scheduled workflow details const schedule = await this.scheduledWorkflowRepository.findOne({ where: { id: schedule_id }, }); if (!schedule) { throw new Error(`Scheduled workflow not found: ${schedule_id}`); } this.logger.debug( `๐Ÿ“‹ Loaded schedule from DB: ${JSON.stringify(schedule)}`, ); // Execute workflow actions this.logger.log(`โš™๏ธ Executing workflow actions...`); const result = await this.executeWorkflowActions(schedule, job.data); this.logger.debug(`๐Ÿงฎ Execution result: ${JSON.stringify(result)}`); // Update execution log with results const completedAt = new Date(); executionLog.execution_status = result.failedRecords > 0 && result.successfulRecords > 0 ? EXECUTION_STATUS_PARTIAL : result.failedRecords > 0 ? EXECUTION_STATUS_FAILED : EXECUTION_STATUS_COMPLETED; executionLog.completed_at = completedAt; executionLog.duration_ms = completedAt.getTime() - executionLog.started_at.getTime(); executionLog.total_records = result.totalRecords; executionLog.successful_records = result.successfulRecords; executionLog.failed_records = result.failedRecords; executionLog.execution_details = { batchesProcessed: Math.ceil(result.totalRecords / DEFAULT_BATCH_SIZE), errors: result.errors, }; await this.executionLogRepository.save(executionLog); // Update schedule execution count and last execution time schedule.execution_count += 1; schedule.last_execution_at = completedAt; await this.scheduledWorkflowRepository.save(schedule); this.logger.log( `โœ… Workflow execution completed: schedule_id=${schedule_id}, status=${executionLog.execution_status}, processed=${result.totalRecords}`, ); return { success: true, executionLogId: executionLog.id, result, }; } catch (error) { this.logger.error( `๐Ÿ”ฅ Workflow execution failed: schedule_id=${schedule_id}, error=${error.message}`, error.stack, ); // Update execution log with error executionLog.execution_status = EXECUTION_STATUS_FAILED; executionLog.completed_at = new Date(); executionLog.duration_ms = executionLog.started_at ? executionLog.completed_at.getTime() - executionLog.started_at.getTime() : 0; executionLog.error_message = error.message; executionLog.error_stack = error.stack; await this.executionLogRepository.save(executionLog); throw error; } } /** * Execute all actions defined in the workflow */ private async executeWorkflowActions( schedule: ScheduledWorkflow, jobData: ScheduleJobData, ): Promise { const result: BatchProcessingResult = { totalRecords: 0, processedRecords: 0, successfulRecords: 0, failedRecords: 0, errors: [], }; const resultData: any = await this.scheduleHandlerService.handleScheduledWorkflow( jobData.workflow_id, jobData, ); if (!schedule.actions || schedule.actions.length === 0) { this.logger.warn(`No actions defined for schedule: ${schedule.id}`); return result; } // Execute each action sequentially for (const action of schedule.actions) { try { const actionResult = await this.executeAction(action, jobData); result.totalRecords += actionResult.totalRecords; result.processedRecords += actionResult.processedRecords; result.successfulRecords += actionResult.successfulRecords; result.failedRecords += actionResult.failedRecords; result.errors.push(...actionResult.errors); } catch (error) { this.logger.error( `Action execution failed: actionType=${action.actionType}, error=${error.message}`, ); result.failedRecords += 1; result.errors.push({ error: `Action ${action.actionType} failed: ${error.message}`, }); } } return resultData; } /** * Execute a single workflow action */ private async executeAction(action: any, jobData: ScheduleJobData) { this.logger.log(`๐Ÿš€ Executing scheduled action: ${action.actionType}`); try { // Weโ€™ll assume jobData contains workflowId and user info const { workflow_id, loggedInUser } = jobData; // ๐Ÿง  Call your handler that performs filtering and workflow execution const result: any = await this.scheduleHandlerService.handleScheduledWorkflow( workflow_id, loggedInUser, ); this.logger.log( `โœ… Scheduled workflow ${workflow_id} executed successfully`, ); return { totalRecords: result?.length || 0, processedRecords: result?.length || 0, successfulRecords: result?.length || 0, failedRecords: 0, errors: [], }; } catch (error) { this.logger.error( `๐Ÿ”ฅ Error executing scheduled workflow action: ${error.message}`, error.stack, ); return { totalRecords: 0, processedRecords: 0, successfulRecords: 0, failedRecords: 1, errors: [{ error: error.message }], }; } } /** * Execute send email action */ private async executeSendEmailAction( action: any, jobData: ScheduleJobData, ): Promise { const result: BatchProcessingResult = { totalRecords: 0, processedRecords: 0, successfulRecords: 0, failedRecords: 0, errors: [], }; try { // Get target records based on filter criteria const records = await this.getTargetRecords( action.targetEntityType, action.filterCriteria, jobData, ); result.totalRecords = records.length; // Process in batches const batches = this.chunkArray(records, DEFAULT_BATCH_SIZE); for (const batch of batches) { for (const record of batch) { try { // TODO: Integrate with email service // await this.emailService.sendEmail({ // to: record.email, // subject: action.actionConfig.subject, // template: action.actionConfig.template, // data: record, // }); result.processedRecords += 1; result.successfulRecords += 1; } catch (error) { result.processedRecords += 1; result.failedRecords += 1; result.errors.push({ recordId: record.id, error: error.message, }); } } } } catch (error) { throw new Error(`Send email action failed: ${error.message}`); } return result; } /** * Execute update records action */ private async executeUpdateRecordsAction( action: any, jobData: ScheduleJobData, ): Promise { const result: BatchProcessingResult = { totalRecords: 0, processedRecords: 0, successfulRecords: 0, failedRecords: 0, errors: [], }; try { // Get target records const records = await this.getTargetRecords( action.targetEntityType, action.filterCriteria, jobData, ); result.totalRecords = records.length; // Process in batches const batches = this.chunkArray(records, DEFAULT_BATCH_SIZE); for (const batch of batches) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { for (const record of batch) { // Build update query based on action config const updateFields = action.actionConfig.updateFields || {}; const updateValues = Object.keys(updateFields).map((key) => { return `${key} = ?`; }); const values = Object.values(updateFields); if (updateValues.length > 0) { const tableName = this.getTableNameForEntityType( action.targetEntityType, ); await queryRunner.query( `UPDATE ${tableName} SET ${updateValues.join(', ')} WHERE id = ?`, [...values, record.id], ); } result.processedRecords += 1; result.successfulRecords += 1; } await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); result.failedRecords += batch.length; result.errors.push({ error: `Batch update failed: ${error.message}`, }); } finally { await queryRunner.release(); } } } catch (error) { throw new Error(`Update records action failed: ${error.message}`); } return result; } /** * Execute create task action */ // private async executeCreateTaskAction( // action: any, // jobData: ScheduleJobData, // ): Promise { // const result: BatchProcessingResult = { // totalRecords: 0, // processedRecords: 0, // successfulRecords: 0, // failedRecords: 0, // errors: [], // }; // try { // // Get target records // const records = await this.getTargetRecords( // action.targetEntityType, // action.filterCriteria, // jobData, // ); // result.totalRecords = records.length; // // Task repo (cr_wf_task) // const taskRepo = this.reflectionHelper.getRepoService('TaskDataEntity'); // // Process in batches // const batches = this.chunkArray(records, DEFAULT_BATCH_SIZE); // for (const batch of batches) { // for (const record of batch) { // try { // // Build task object // const task = { // name: action.actionConfig.taskName || 'Scheduled Task', // description: action.actionConfig.taskDescription || '', // status: 'PENDING', // mapped_entity_id: record.id, // mapped_entity_type: action.targetEntityType, // organization_id: jobData.organizationId, // enterprise_id: jobData.enterpriseId, // created_by: jobData.createdBy, // created_date: new Date(), // NOW() // entity_type: 'WFTK', // }; // // Insert using TypeORM repository // await taskRepo.insert(task); // result.processedRecords += 1; // result.successfulRecords += 1; // } catch (error) { // result.processedRecords += 1; // result.failedRecords += 1; // result.errors.push({ // recordId: record.id, // error: error.message, // }); // } // } // } // } catch (error) { // throw new Error(`Create task action failed: ${error.message}`); // } // return result; // } /** * Execute send notification action */ // private async executeSendNotificationAction( // action: any, // jobData: ScheduleJobData, // ): Promise { // const result: BatchProcessingResult = { // totalRecords: 0, // processedRecords: 0, // successfulRecords: 0, // failedRecords: 0, // errors: [], // }; // try { // // Get target records // const records = await this.getTargetRecords( // action.targetEntityType, // action.filterCriteria, // jobData, // ); // result.totalRecords = records.length; // // Prepare repo // const notificationRepo = this.reflectionHelper.getRepoService( // 'NotificationData', // cr_notification repo // ); // // Process in batches // const batches = this.chunkArray(records, DEFAULT_BATCH_SIZE); // for (const batch of batches) { // for (const record of batch) { // try { // // Build notification object // const notification = { // user_id: record.user_id || jobData.createdBy, // event_type: action.actionConfig.eventType || 'WORKFLOW_SCHEDULED', // message: // action.actionConfig.message || 'Scheduled workflow executed', // mapped_entity_id: record.id, // mapped_entity_type: action.targetEntityType, // is_read: 0, // organization_id: jobData.organizationId, // created_date: new Date(), // NOW() // entity_type: 'NOTF', // status: 'ACTIVE', // }; // // Insert using TypeORM (replaces raw SQL) // await notificationRepo.insert(notification); // result.processedRecords += 1; // result.successfulRecords += 1; // } catch (error) { // result.processedRecords += 1; // result.failedRecords += 1; // result.errors.push({ // recordId: record.id, // error: error.message, // }); // } // } // } // } catch (error) { // throw new Error(`Send notification action failed: ${error.message}`); // } // return result; // } /** * Get target records based on entity type and filter criteria */ private async getTargetRecords( entityType: string, filterCriteria: any, jobData: ScheduleJobData, ): Promise { // Get repository dynamically based on entityType const repo = this.reflectionHelper.getRepoService(entityType); // Start QueryBuilder const qb = repo.createQueryBuilder('e'); // Mandatory organization filter qb.where('e.organization_id = :orgId', { orgId: jobData.organization_id, }); // Apply additional filters if (filterCriteria && Object.keys(filterCriteria).length > 0) { for (const key of Object.keys(filterCriteria)) { qb.andWhere(`e.${key} = :${key}`, { [key]: filterCriteria[key], }); } } const records = await qb.getMany(); return records; } /** * Get table name for entity type */ private getTableNameForEntityType(entityType: string): string { // Map entity types to table names const entityTypeMap: Record = { USR: 'sso_user', LEAD: 'cr_lead', TASK: 'cr_wf_task', // Add more mappings as needed }; return entityTypeMap[entityType] || 'unknown_table'; } /** * Split array into chunks */ private chunkArray(array: T[], chunkSize: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } /** * Event handler for when job becomes active */ @OnQueueActive() onActive(job: Job) { this.logger.log( `Processing job ${job.id} of type ${job.name} for schedule ${job.data.schedule_id}`, ); } /** * Event handler for when job completes */ @OnQueueCompleted() onCompleted(job: Job, result: any) { this.logger.log( `Job ${job.id} completed for schedule ${job.data.schedule_id} with result: ${JSON.stringify(result)}`, ); } /** * Event handler for when job fails */ @OnQueueFailed() onFailed(job: Job, error: Error) { this.logger.error( `Job ${job.id} failed for schedule ${job.data.schedule_id} with error: ${error.message}`, error.stack, ); } }