import { BadRequestException, Inject, Injectable, Logger, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { WorkflowAutomation } from '../entity/workflow-automation.entity'; import { WorkflowAutomationActionEntity } from '../entity/workflow-automation-action.entity'; import { EntityServiceImpl } from 'src/module/meta/service/entity-service-impl.service'; import { UserData } from 'src/module/user/entity/user.entity'; import { SavedFilterService } from 'src/module/filter/service/saved-filter.service'; import { ENTITYTYPE_SAVEDFILTERMASTER } from 'src/constant/global.constant'; import { ScheduledWorkflow } from 'src/module/workflow-schedule/entities/scheduled-workflow.entity'; import { WorkflowScheduleService } from 'src/module/workflow-schedule/service/workflow-schedule.service'; import * as moment from 'moment'; import { Update } from 'aws-sdk/clients/dynamodb'; import { UpdateScheduleDto } from 'src/module/workflow-schedule/dto/update-schedule.dto'; @Injectable() export class WorkflowAutomationService extends EntityServiceImpl { private readonly logger = new Logger(WorkflowAutomationService.name); constructor( @InjectRepository(WorkflowAutomation) private readonly wfRepo: Repository, @InjectRepository(WorkflowAutomationActionEntity) private readonly actionRepo: Repository, @Inject('SavedFilterService') private readonly savedFilterService: SavedFilterService, private readonly dataSource: DataSource, @InjectRepository(ScheduledWorkflow) private readonly scheduledWorkflowRepository: Repository, private readonly workflowScheduleService: WorkflowScheduleService, ) { super(); } async createRule( data: Partial, ): Promise { const rule = this.wfRepo.create(data); return this.wfRepo.save(rule); } async updateRule( id: number, data: Partial, ): Promise { await this.wfRepo.update(id, data); return this.wfRepo.findOneBy({ id }); } async deleteRule(id: number): Promise { await this.wfRepo.delete(id); } async getRule(id: number): Promise { return this.wfRepo.findOneBy({ id }); } async getActiveRules( entityType: string, event: string, loggedInUser: UserData, ): Promise { return this.wfRepo.find({ where: { applicable_entity_type: entityType, trigger_event: event, enterprise_id: loggedInUser.enterprise_id, }, }); } async getActionsForRule( workflow_automation_id: number, ): Promise { return this.actionRepo.find({ where: { workflow_automation_id, status: 'ACTIVE', }, }); } async updateEntity(entityData: any, loggedInUser: UserData) { this.logger.debug( `Updating WorkflowAutomation with data: ${JSON.stringify(entityData)}`, ); const { event, filter, action, ...workflowData } = entityData; // 1. Update core workflow columns first this.logger.log(`Updating core workflow fields for workflow...`); let workflow = await super.updateEntity(workflowData, loggedInUser); this.logger.debug(`Workflow updated (id=${workflow.id})`); // 2. EVENT FILTER if (event?.triggerType) { this.logger.log(`Processing EVENT filter for workflow ${workflow.id}`); if (event.triggerType === 'on_event') { const eventFilterMaster = { entity_type: 'SFM', name: `Event_Filter_${workflow.id}`, filterDetails: event?.eventFilterJson ?? [], filter_scope: 'RULE', organization_id: loggedInUser.organization_id, enterprise_id: loggedInUser.enterprise_id, }; if (workflow.condition_filter_code) { this.logger.debug( `Existing event filter found: ${workflow.condition_filter_code}`, ); const existing = await this.savedFilterService.getEntityDataByCode( ENTITYTYPE_SAVEDFILTERMASTER, workflow.condition_filter_code, loggedInUser, ); if (existing) { eventFilterMaster['id'] = existing.id; const updatedEvent = await this.savedFilterService.updateEntity( eventFilterMaster as any, loggedInUser, ); workflow.condition_filter_code = updatedEvent.code; this.logger.log( `Updated existing event filter: ${updatedEvent.code}`, ); } } else { const savedEvent = await this.savedFilterService.createEntity( eventFilterMaster, loggedInUser, ); workflow.condition_filter_code = savedEvent.code; this.logger.log(`Created new event filter: ${savedEvent.code}`); } workflow.trigger_event = event?.event ?? null; } if (event.triggerType === 'on_schedule') { // just store JSON directly into schedule column workflow.schedule = event?.scheduleJson ?? null; workflow.trigger_type = 'on_schedule'; this.logger.log( `Stored schedule JSON for workflow ${workflow.id}: ${JSON.stringify( workflow.schedule, )}`, ); await this.registerScheduleAutomation(workflow, loggedInUser); } workflow.trigger_type = event.triggerType; } // 3. CRITERIA FILTER if (filter) { this.logger.log(`Processing CRITERIA filter for workflow ${workflow.id}`); workflow.criteria_filter_code = filter.filter_code ?? null; } // 4. ACTIONS if (Array.isArray(action) && action.length) { this.logger.log( `Processing ${action.length} actions for workflow ${workflow.id}`, ); const existingActions = await this.dataSource .getRepository(WorkflowAutomationActionEntity) .find({ where: { workflow_automation_id: workflow.id }, }); const existingMap = new Map(existingActions.map((a) => [a.name, a])); // 2. Upsert actions for (const a of action) { if (!a?.name) { this.logger.warn( `Skipping action without unique name: ${JSON.stringify(a)}`, ); continue; } const existing = existingMap.get(a.name); if (existing) { // update existing.payload = a.actionPayload ?? {}; existing.actioncategoryname = a.actionCategoryName ?? existing.actioncategoryname; existing.status = a.status ?? existing.status; existing.action_category_id = a.actionCategory ?? existing.action_category_id; await this.dataSource .getRepository(WorkflowAutomationActionEntity) .save(existing); this.logger.log(`Updated action: ${a.actionCategory}`); } else { // insert const actionEntity = new WorkflowAutomationActionEntity(); actionEntity.entity_type = 'WFAA'; actionEntity.workflow_automation_id = workflow.id; actionEntity.action_category_id = a.actionCategory; actionEntity.actioncategoryname = a.actionCategoryName; actionEntity.payload = a.actionPayload ?? {}; actionEntity.name = a.name; actionEntity.status = a.status; await this.dataSource .getRepository(WorkflowAutomationActionEntity) .save(actionEntity); this.logger.log(`Created new action: ${a.actionCategory}`); } } const incomingNames = action.map((a) => a.name); const toDelete = existingActions.filter( (a) => !incomingNames.includes(a.name), ); if (toDelete.length) { await this.dataSource .getRepository(WorkflowAutomationActionEntity) .remove(toDelete); this.logger.log( `Deleted ${toDelete.length} actions not present in request.`, ); } } // 5. Save workflow again with updated codes workflow = await super.updateEntity(workflow, loggedInUser); this.logger.debug(`Final workflow updated (id=${workflow.id})`); return workflow; } async getEntityData(entity_type: string, id: number, loggedInUser: UserData) { this.logger.log(`Fetching WorkflowAutomation by id=${id}`); // 1. Get workflow automation entity const workflowAutomation = await this.dataSource .getRepository(WorkflowAutomation) .findOne({ where: { id }, }); if (!workflowAutomation) { throw new NotFoundException(`WorkflowAutomation not found with id=${id}`); } // 2. EVENT FILTER let event: any = null; if (workflowAutomation.trigger_type === 'on_event') { if (workflowAutomation.condition_filter_code) { const eventFilterMaster: any = await this.savedFilterService.getEntityDataByCode( ENTITYTYPE_SAVEDFILTERMASTER, workflowAutomation.condition_filter_code, loggedInUser, ); if (eventFilterMaster) { // 🔑 fetch filter details using mapped_filter_code const eventFilterDetails = await this.savedFilterService.getDetailsByCode( eventFilterMaster.code, ); event = { eventFilterJson: eventFilterDetails ?? [], triggerType: workflowAutomation.trigger_type, event: workflowAutomation.trigger_event, }; } } } if (workflowAutomation.trigger_type === 'on_schedule') { event = { triggerType: workflowAutomation.trigger_type, scheduleJson: workflowAutomation.schedule, }; } // 3. CRITERIA FILTER let filter: any = null; filter = { filter_code: workflowAutomation.criteria_filter_code, }; // 4. ACTIONS const actions = await this.dataSource .getRepository(WorkflowAutomationActionEntity) .find({ where: { workflow_automation_id: workflowAutomation.id }, }); const action = actions.map((a) => ({ actionCategory: a.action_category_id, actionCategoryName: a.actioncategoryname, actionPayload: a.payload, name: a.name, status: a.status, })); // 5. Final response const response = { ...workflowAutomation, event, filter, action, }; this.logger.debug(`Fetched WorkflowAutomation (id=${id}) with relations`); return response; } async registerScheduleAutomation(workflow: any, loggedInUser: UserData) { this.logger.log( `Registering schedule automation for workflow ${workflow.id}`, ); const scheduleJson = workflow.schedule; if (!scheduleJson) { this.logger.warn(`No schedule JSON provided for workflow ${workflow.id}`); return; } // Try to find an existing scheduled workflow by workflow_id const existingSchedule = await this.scheduledWorkflowRepository.findOne({ where: { workflow_id: workflow.id }, }); const cronExpression = await this.generateDailyCron(scheduleJson.fixedTime); const payload = { id: Number(existingSchedule?.id ?? null), workflow_id: workflow.id, workflow_name: workflow.name, name: scheduleJson.name ?? `Schedule_${workflow.id}`, description: scheduleJson.description ?? '', cron_expression: cronExpression, timezone: scheduleJson.timezone ?? 'Asia/Kolkata', start_date: scheduleJson.start_date ?? null, end_date: scheduleJson.end_date ?? null, max_executions: scheduleJson.max_executions ?? null, retry_config: scheduleJson.retry_config ?? {}, actions: scheduleJson.actions ?? [], metadata: scheduleJson.metadata ?? {}, is_enabled: scheduleJson.is_enabled ?? true, organization_id: loggedInUser.organization_id, enterprise_id: loggedInUser.enterprise_id, level_id: loggedInUser.level_id, level_type: loggedInUser.level_type, appcode: loggedInUser.appcode, }; if (existingSchedule) { this.logger.log( `Existing schedule found (id=${existingSchedule.id}), updating...`, ); // payload['id'] = existingSchedule.id; return await this.workflowScheduleService.updateSchedule( payload, loggedInUser, ); } else { this.logger.log(`No existing schedule found, creating new one...`); return await this.workflowScheduleService.createSchedule( payload, loggedInUser, ); } } // CRON async generateDailyCron(executionTime: string): Promise { const time = moment(executionTime, 'HH:mm'); if (!time.isValid()) { throw new Error('Invalid executionTime format (expected HH:mm)'); } const minute = time.minute(); const hour = time.hour(); // Ensure cron has 5 fields: minute hour day month weekday const cron = `${minute} ${hour} * * *`; // ✅ Validate against your regex before returning const cronRegex = /^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([01]?\d|2\d|3[01])) (\*|([1-9]|1[0-2])) (\*|([0-6]))$/; if (!cronRegex.test(cron)) { throw new Error( `Generated cron expression "${cron}" does not match expected format`, ); } return cron; } /** * Updates the sequence of multiple workflow automations. */ async updateSequence( body: any[], loggedInUser: any, ): Promise<{ updated: number; results: any[] }> { if (!Array.isArray(body) || body.length === 0) { throw new BadRequestException( 'Invalid input. Expected a non-empty array.', ); } const results: { id: number | string | null; success: boolean; result?: any; error?: string; }[] = []; for (const item of body) { const { id, sequence } = item; // Validate required fields if (!id || sequence === undefined || sequence === null) { results.push({ id: id || null, success: false, error: 'Missing id or sequence', }); continue; } try { // Check if workflow exists const existing = await this.wfRepo.findOne({ where: { id } }); if (!existing) { results.push({ id, success: false, error: `Workflow with id ${id} not found`, }); continue; } // Update sequence + audit fields existing.sequence = Number(sequence); existing.modified_by = loggedInUser?.id || loggedInUser?.user_id || null; existing.modified_date = new Date(); const saved = await this.wfRepo.save(existing); results.push({ id, success: true, result: saved }); } catch (err) { results.push({ id, success: false, error: err.message || 'Unknown error occurred', }); } } return { updated: results.filter((r) => r.success).length, results, }; } }