import { Inject, Injectable, Logger } from '@nestjs/common'; import { FilterEvaluatorService } from 'src/module/filter/service/filter-evaluator.service'; import { DataSource, Repository } from 'typeorm'; import { WorkflowAutomationEngineService } from './workflow-automation-engine.service'; import { WorkflowAutomation } from '../entity/workflow-automation.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { EntityMaster } from 'src/module/meta/entity/entity-master.entity'; import { ReflectionHelper } from 'src/utils/service/reflection-helper.service'; @Injectable() export class ScheduleHandlerService { private readonly logger = new Logger(ScheduleHandlerService.name); constructor( private readonly dataSource: DataSource, private readonly filterEvaluator: FilterEvaluatorService, private readonly workflowAutomationEngineService: WorkflowAutomationEngineService, @InjectRepository(WorkflowAutomation) private readonly workflowAutomation: Repository, @InjectRepository(EntityMaster) private readonly entityMasterRepository: Repository, @Inject() readonly reflectionHelper: ReflectionHelper, ) {} async scheduleQueryBuilder(workflow_id: number, jobData: any) { // 1️⃣ Fetch workflow automation config const workflow = await this.workflowAutomation.findOne({ where: { id: workflow_id, organization_id: jobData.organization_id, }, }); if (!workflow) throw new Error(`Workflow with ID ${workflow_id} not found`); const scheduleJson = typeof workflow.schedule === 'string' ? JSON.parse(workflow.schedule) : workflow.schedule; const entityType = workflow.applicable_entity_type; // 2️⃣ Get table name from frm_entity_master const entity = await this.entityMasterRepository.findOne({ where: { mapped_entity_type: entityType, organization_id: jobData.organization_id, }, select: ['data_source'], }); if (!entity?.data_source) throw new Error(`Entity ${entityType} not found`); const tableName = entity.data_source; const listMasterItemsRepo = this.reflectionHelper.getRepoService('ListMasterItems'); // 3️⃣ Resolve executionDate (list master item name) const executionDateItem = await listMasterItemsRepo.findOne({ where: { id: scheduleJson.executionDate, }, select: ['name'], }); const executionDateName = executionDateItem?.name?.toLowerCase() || ''; const executionDataOrTime = scheduleJson.executionDateOrTime; const executionUnits = Number(scheduleJson.executionDateUnits || 0); const fixedTime = scheduleJson.fixedTime; // e.g. "16:00" // Build time window logic let comparisonDateCondition = ''; if (executionDateName.includes('after')) { comparisonDateCondition = ` ${executionDataOrTime} >= CURRENT_DATE + INTERVAL '${executionUnits} days' AND ${executionDataOrTime} < CURRENT_DATE + INTERVAL '${executionUnits + 1} days' `; } else if (executionDateName.includes('before')) { comparisonDateCondition = ` ${executionDataOrTime} >= CURRENT_DATE - INTERVAL '${executionUnits + 1} days' AND ${executionDataOrTime} < CURRENT_DATE - INTERVAL '${executionUnits} days' `; } else if (executionDateName.includes('same')) { comparisonDateCondition = ` ${executionDataOrTime} >= CURRENT_DATE AND ${executionDataOrTime} < CURRENT_DATE + INTERVAL '1 days' `; } const qb = this.dataSource .createQueryBuilder() .from(tableName, tableName) .select('*') .where(comparisonDateCondition) .andWhere(`${tableName}.organization_id = :orgId`, { orgId: jobData.organization_id, }); this.logger.debug(`Executing Scheduler Query: ${qb.getSql()}`); const results = await qb.getRawMany(); this.logger.log( `Found ${results.length} scheduled records for ${tableName}`, ); return { results, workflow, tableName }; } // ⚙️ Step 2: Handle scheduled automation (main entry point) async handleScheduledWorkflow(workflow_id: number, jobData: any) { const { results, workflow, tableName } = await this.scheduleQueryBuilder( workflow_id, jobData, ); if (!results?.length) { this.logger.warn( `No records found for scheduled workflow ${workflow_id}`, ); return; } this.logger.log( `Processing ${results.length} records for workflow ${workflow_id}`, ); // Parse the workflow event JSON again to get filter/action info const filter = workflow.criteria_filter_code; const mappedEntityType = workflow.mapped_entity_type; const applicableEntityType = workflow.applicable_entity_type; const matchedEntities: any[] = []; // 🔁 Step 2A: Evaluate criteria for each record for (const record of results) { let entityIdToUse; if (mappedEntityType == applicableEntityType) { entityIdToUse = record.id; } else { entityIdToUse = record.parent_id; } // const entityIdToUse = record.id; // assuming 'id' column in the table const executionUser = { organization_id: jobData.organization_id, enterprise_id: jobData.enterprise_id, level_id: record.level_id, level_type: record.level_type, appcode: jobData.appcode, id: jobData.createdBy, }; if (!record.level_id || !record.level_type) { this.logger.warn( `Skipping entity ${record.id}: missing school context`, ); continue; } this.logger.debug( `Evaluating filter for entity ${record.id} at ${record.level_type}:${record.level_id}`, ); const criteriaMatched = await this.filterEvaluator.evaluateCriteria( mappedEntityType, filter, entityIdToUse, executionUser, ); this.logger.debug( `⚖️ Criteria check for entity ${entityIdToUse} -> ${criteriaMatched}`, ); if (criteriaMatched) { matchedEntities.push(record); } } // 🧮 Step 3: Execute workflow actions for matched entities if (!matchedEntities.length) { this.logger.log(`No records passed criteria for workflow ${workflow_id}`); return; } this.logger.log( `✅ ${matchedEntities.length} entities passed criteria, executing actions...`, ); for (const entity of matchedEntities) { const executionUser = { organization_id: jobData.organization_id, enterprise_id: jobData.enterprise_id, level_id: entity.level_id, level_type: entity.level_type, appcode: jobData.appcode, id: jobData.createdBy, }; await this.workflowAutomationEngineService.executeActions( workflow_id, entity, { ...jobData, user: executionUser, }, ); } } }