import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import axios from 'axios'; import { EntityServiceImpl } from 'src/module/meta/service/entity-service-impl.service'; import { UserData } from 'src/module/user/entity/user.entity'; import { In, Repository } from 'typeorm'; import { StageMovementData } from '../entity/stage-movement-data.entity'; import { ACTIVITY_CATEGORIES } from '../repository/activity-log.repository'; import { StageMovementRepository } from '../repository/stage-movement.repository'; import { TaskRepository } from '../repository/task.repository'; import { ActionDataService } from './action-data.service'; import { ActivityLogService } from './activity-log.service'; import { EntityModificationService } from './entity-modification.service'; import { TaskService } from './task.service'; import { ActionCategory } from '../entity/action-category.entity'; import { StageGroup } from '../entity/stage-group.entity'; import { ActionDataEntity } from '../entity/action-data.entity'; import { TaskDataEntity } from '../entity/task-data.entity'; import { IMicroserviceClients } from 'src/module/microservice-client/service/microservice-clients'; import { firstValueFrom } from 'rxjs'; @Injectable() export class WorkflowMetaService extends EntityServiceImpl { constructor( @InjectRepository(StageMovementData) private readonly stageMovementRepo: Repository, private readonly stageMovementRepository: StageMovementRepository, private readonly taskRepository: TaskRepository, private readonly actionDataService: ActionDataService, @Inject('TaskService') private readonly taskService: TaskService, @Inject('ActivityLogService') private readonly activityLogService: ActivityLogService, @Inject('EntityModificationService') private readonly modificationService: EntityModificationService, private readonly configService: ConfigService, @InjectRepository(ActionCategory) private readonly actionCategoryRepo: Repository, @InjectRepository(StageGroup) private readonly stageGroupRepo: Repository, @InjectRepository(ActionDataEntity) private readonly actionDataEntityRepository: Repository, @InjectRepository(TaskDataEntity) private readonly taskDataEntityRepository: Repository, @Inject('MICROSERVICE_CLIENT_FACTORY') private readonly factory: IMicroserviceClients, ) { super(); } // get first stage from stage table async getFirstStage( loggedInUser: UserData, mapped_entity_type: string, mapped_entity_id: number, ): Promise { // step1 find in workflow_level_mapping if not found with current level_id and level_type search in for ORG // step2 find in stage_group => workflow_id // step3 find in stage => stage_group_id // step4 get first stage from stage table return this.stageMovementRepository.getFirstStage({ loggedInUser, mapped_entity_type, mapped_entity_id, }); } /** * Get the current active stage for a given mapped entity */ async getCurrentStage( mapped_entity_type: string, mapped_entity_id: number, loggedInUser: UserData, ): Promise { // Try to find the current stage movement entry const currentStage = await this.stageMovementRepo.findOne({ where: { mapped_entity_type, mapped_entity_id, is_current: 'Y', }, }); if (!currentStage) { // Fetch the latest stage movement for the given entity const latestMovement = await this.stageMovementRepo.findOne({ where: { mapped_entity_type, mapped_entity_id, }, order: { id: 'DESC' }, // Get the most recent movement }); if (!latestMovement) return null; let stageGroup = await this.stageGroupRepo.findOne({ where: { id: latestMovement?.stage_group_id }, }); return { ...latestMovement, stage_group_name: stageGroup?.name, }; } // get group name for the current stage const stageGroup = await this.stageGroupRepo.findOne({ where: { id: currentStage?.stage_group_id }, }); // Return the found or newly created current stage movement return { ...currentStage, stage_group_name: stageGroup?.name, }; } /** * Get next stage ID based on current stage * (Stub logic – replace with real stage transition resolution) */ async getNextStage(currentStage: any): Promise { // get all stage for that stage group const nextStage = await this.stageMovementRepository.getNextStageOrFirstOfNextGroup( currentStage?.stage_group_id, currentStage?.stage_id, ); if (!nextStage) { return { hasNextStage: false, nextStage: null }; } const stageGroup = await this.stageGroupRepo.findOne({ where: { id: nextStage?.stage_group_id, }, }); return { hasNextStage: !!nextStage, nextStage: { ...nextStage, stage_group_name: stageGroup?.name }, }; } /** * Move mapped entity to the next stage in the workflow */ async moveToNextStage( mapped_entity_type: string, mapped_entity_id: number, loggedInUser: UserData, reason_code?: string, remark?: string, ): Promise { const now = new Date(); let currentStage = await this.getCurrentStage( mapped_entity_type, mapped_entity_id, loggedInUser, ); if (currentStage) { await this.taskService.createSystemNote( { reason_code: reason_code || '', remark: remark || '', mapped_entity_id, stage_id: currentStage.stage_id, action_id: currentStage.action_id, stage_group_id: currentStage.stage_group_id, }, loggedInUser, ); } // Case 1: First stage – initialize if (!currentStage) { const getAllResult = await this.getFirstStage( loggedInUser, mapped_entity_type, mapped_entity_id, ); const { mappingUsed, stageGroup, firstStage } = getAllResult; currentStage = await this.stageMovementRepo.save({ entity_type: 'WFSA', mapped_entity_type, mapped_entity_id, name: firstStage.name, status: firstStage.status, current_user_id: loggedInUser.id, stage_action_mapping_id: firstStage.id, organization_id: loggedInUser.organization_id, enterprise_id: loggedInUser.enterprise_id, stage_group_id: stageGroup.id, stage_id: firstStage.id, start_date: new Date(), is_current: 'Y', }); await this.populateActionService( currentStage.stage_id, loggedInUser, mapped_entity_id, mapped_entity_type, ); return `Initialized workflow with first stage (Stage ID: ${firstStage.id}).`; } // Case 2: Has next stage – move forward const stageData = await this.getNextStage(currentStage); if (stageData.hasNextStage) { const { nextStage } = stageData; // ✅ NEW: Log stage group completion if group changes if (nextStage.stage_group_id !== currentStage.stage_group_id) { try { // stage action category SGCP // if current stage actions contain any action with action_category as SGCP then set the status of that action to completed const stageActions = await this.stageMovementRepository.getAllActionByStageId( currentStage.stage_id, ); for (const action of stageActions) { const actionCategoryRepo = this.reflectionHelper.getRepoService('ActionCategory'); const actionCategory = await actionCategoryRepo.findOne({ where: { id: Number(action.action_category), }, }); if (actionCategory?.code == 'SGCP') { await this.taskRepository.updateTaskStatus( loggedInUser, mapped_entity_type, mapped_entity_id, currentStage.stage_id, action.id, ); } } await this.activityLogService.logActivity( { mapped_entity_id, mapped_entity_type, title: 'Stage Group Completed', description: `Stage Group changed from ${currentStage.stage_group_name} to ${nextStage.stage_group_name}.`, action: 'completed', category: ACTIVITY_CATEGORIES.STAGEGROUP, appcode: loggedInUser.appcode, }, loggedInUser, ); } catch (error) { console.error( 'Failed to log stage group completion:', error?.message || error, ); } } // Close current stage currentStage.end_date = now; currentStage.is_current = 'N'; await this.stageMovementRepo.save(currentStage); // Insert next stage await this.stageMovementRepo.save({ entity_type: 'WFSA', mapped_entity_type, mapped_entity_id, name: nextStage.name, status: nextStage.status, current_user_id: loggedInUser.id, stage_action_mapping_id: nextStage.id, organization_id: loggedInUser.organization_id, enterprise_id: loggedInUser.enterprise_id, start_date: now, stage_group_id: nextStage?.stage_group_id, stage_id: nextStage.id, is_current: 'Y', }); await this.populateActionService( stageData.nextStage.id, loggedInUser, mapped_entity_id, mapped_entity_type, ); // Stage completion log try { await this.activityLogService.logActivity( { mapped_entity_id: mapped_entity_id, mapped_entity_type: mapped_entity_type, title: 'Stage Completed', description: `Stage changed from ${currentStage.name} to ${nextStage.name}.`, action: 'status', category: ACTIVITY_CATEGORIES.STAGE, appcode: loggedInUser.appcode, }, loggedInUser, ); } catch (error) { console.error( 'Failed to log activity for stage completion:', error?.message || error, ); } return `Moved to next stage (Stage ID: ${nextStage.id}).`; } // Case 3: No next stage – mark workflow as done currentStage.end_date = now; currentStage.is_current = 'N'; await this.stageMovementRepo.save(currentStage); return 'Workflow completed. No next stage available.'; } async populateActionService( stage_id: number, loggedInUser: UserData, mapped_entity_id: number, mapped_entity_type: string, ): Promise { const actions = await this.stageMovementRepository.getAllActionByStageId(stage_id); // Populate the action service with the retrieved actions if (!actions || actions.length === 0) { return 'No actions found for this stage.'; } // check whether first action's action_category is owner_assignment and assignment_type is AUTO_ASSIGN const firstAction = actions[0]; const actionCategory = await this.actionCategoryRepo.findOne({ where: { id: Number(firstAction.action_category), }, }); const listMasterItemsRepo = this.reflectionHelper.getRepoService('ListMasterItems'); const assignmentType = await listMasterItemsRepo.findOne({ where: { id: Number(firstAction.assignment_type), }, }); // save in action data await this.actionDataService.saveActionData( actions, loggedInUser, mapped_entity_id, mapped_entity_type, ); // save in task data await this.taskService.saveActionData( actions, loggedInUser, mapped_entity_id, mapped_entity_type, ); if ( actionCategory?.code == 'OWAS' && assignmentType?.value == 'round_robin' ) { console.log('Auto-assigning owner based on round-robin assignment type'); await this.assignLead( loggedInUser, mapped_entity_id, mapped_entity_type, stage_id, actions, ); } } async updateLeadOwner(entityData, loggedInUser) { const { lead_id, lead_owner, stage_id, entity_type } = entityData; const updatedData = { id: lead_id, lead_owner: lead_owner, entity_type: entity_type, }; entityData = updatedData; this.modificationService.logModification( { entity_type: 'ENMD', mapped_entity_type: entity_type, mapped_entity_id: lead_id, attribute_key: 'lead_owner', new_value: lead_owner, old_value: entityData.lead_owner, remarks: entityData.remarks, reason_code: entityData.reason_code, stage_id: stage_id, action_id: entityData.action_id, }, loggedInUser, ); const leadData: any = await this.getEntityData( 'LEAD', lead_id, loggedInUser, ); const listMasterItemsRepo = this.reflectionHelper.getRepoService('ListMasterItems'); const unassignedListMasterItemData = await listMasterItemsRepo.find({ where: { listtype: 'LEST', enterprise_id: loggedInUser.enterprise_id, value: In(['unassigned', 'active']), }, }); // Find the IDs explicitly const unassignedId = unassignedListMasterItemData.find( (item) => item.name.toLowerCase() === 'unassigned', )?.id; const activeId = unassignedListMasterItemData.find( (item) => item.name.toLowerCase() === 'active', )?.id; // Use unassignedId for comparison if (leadData?.lead_status === unassignedId) { leadData.lead_status = activeId; } const result = await super.updateEntity( { ...entityData, status: leadData?.lead_status, lead_status: leadData?.lead_status, }, loggedInUser, ); await this.actionDataEntityRepository .createQueryBuilder() .update() .set({ user_id: lead_owner }) .where('mapped_entity_id = :leadId', { leadId: lead_id }) .andWhere('mapped_entity_type = :entityType', { entityType: entity_type }) .andWhere('stage_id = :stageId', { stageId: stage_id }) .andWhere("(is_current = 'Y' OR is_current IS NULL)") .execute(); const leadMeetingRepo = this.reflectionHelper.getRepoService('LeadScheduleMeet'); await leadMeetingRepo .createQueryBuilder() .update() .set({ user_id: lead_owner }) .where('stage_id = :stageId', { stageId: stage_id }) .andWhere('mapped_entity_id = :leadId', { leadId: lead_id }) .andWhere('mapped_entity_type = :entityType', { entityType: 'LEAD' }) .andWhere("(status = 'scheduled' OR status = 'rescheduled')") .execute(); const taskRows = await this.taskDataEntityRepository.find({ where: { mapped_entity_id: lead_id, mapped_entity_type: entity_type, stage_id, }, }); for (const task of taskRows) { const statusRows = await listMasterItemsRepo.findOne({ where: { id: task.status, }, }); const statusName = statusRows?.value?.toLowerCase() || ''; if ( ['todo', 'in_progress'].includes(statusName) || statusName === 'todo' ) { await this.taskDataEntityRepository.update(task.id, { user_id: lead_owner, task_owner: lead_owner, }); } } let leadOwnerName; try { const baseUrl = this.configService.get('REDIRECT_BE_URL'); // Prepare query params const queryParams = new URLSearchParams({ loggedInUser: JSON.stringify(loggedInUser), }).toString(); const url = `${baseUrl}/entity/public/getById/${entityData?.lead_owner}?entity_type=USR&${queryParams}`; const response = await axios.get(url); leadOwnerName = response.data; } catch (error) { console.error('Internal Entity API call failed:', error.message); } try { const logData = { mapped_entity_id: lead_id, mapped_entity_type: 'LEAD', title: 'Owner Assigned', description: leadData?.lead_owner ? `${leadOwnerName?.name} reassigned as Lead owner.` : `${leadOwnerName?.name} assigned as Lead owner.`, action: 'assign', category: ACTIVITY_CATEGORIES.ASSIGN, appcode: loggedInUser.appcode, }; await this.activityLogService.logActivity(logData, loggedInUser); } catch (error) { console.error( 'Failed to log activity for meeting:', error?.message || error, ); // Logging should not block main flow } return result; } async assignLead( loggedInUser: UserData, mapped_entity_id: number, mapped_entity_type: string, stage_id: number, actions: any[], ): Promise { const { organization_id, level_id, level_type } = loggedInUser; let owners = [] as any; const client = this.factory.getClient('SSO'); if (!client) { return null; } owners = await firstValueFrom( client.send('user.leadOwnerDropdown', { organization_id, level_id, level_type, }), ); if (!owners?.length) console.log('No eligible owners found for lead assignment.'); const userIds = owners?.map((o) => Number(o.id)); // normalize to numbers // 2) Find the last assigned *eligible* owner (use IN (...)) const placeholders = userIds.map(() => '?').join(','); const leadRepo = this.reflectionHelper.getRepoService('CRMLead'); const lastRow = await leadRepo .createQueryBuilder('cl') .select('cl.lead_owner', 'lead_owner') .where('cl.organization_id = :orgId', { orgId: organization_id }) .andWhere('cl.level_id = :levelId', { levelId: Number(level_id) }) .andWhere('cl.level_type = :levelType', { levelType: level_type }) .andWhere('cl.lead_owner IN (:...owners)', { owners: userIds }) .orderBy('cl.created_date', 'DESC') .limit(1) .getRawOne(); const lastAssigned = lastRow ? Number(lastRow.lead_owner) : null; // 3) Compute next user in round-robin const lastIdx = lastAssigned != null ? userIds.indexOf(lastAssigned) : -1; const nextIdx = (lastIdx + 1) % userIds.length; const nextUser = userIds[nextIdx]; //4) Update lead owner in the lead table no need because we are updating lead owner in updateLeadOwner method console.log(`Assigning lead to user ID: ${nextUser}`); const firstAction = actions[0]; // update lead owner status await this.updateLeadOwner( { lead_id: mapped_entity_id, lead_owner: nextUser, stage_id: stage_id, entity_type: mapped_entity_type, action_id: firstAction.id, }, loggedInUser, ); // move task await this.taskService.moveTask(loggedInUser, { mapped_entity_type, mapped_entity_id, stage_id, action_id: firstAction.id, }); // if it has only one action then move next stage if (actions.length == 1) { console.log( 'Only one action present and it is owner assignment. Moving to next stage.', ); await this.moveToNextStage( mapped_entity_type, mapped_entity_id, loggedInUser, ); } return nextUser; } }