using e_suite.API.Common.repository; using e_suite.Database.Audit; using e_suite.Database.Core.Tables.Activity; using e_suite.Database.Core.Tables.Domain; using e_suite.Messaging.Common; using e_suite.Workflow.Core; using e_suite.Workflow.Core.Extensions; using e_suite.Workflow.Core.Interfaces; using eSuite.Core.Clock; using eSuite.Core.Enums; using eSuite.Core.Miscellaneous; using Microsoft.Extensions.Logging; using System.Collections; namespace e_suite.Service.WorkflowProcessor; public class WorkflowProcessor : IWorkflowProcessor { private readonly ILogger _logger; private readonly IClock _clock; private readonly IWorkflowTemplateRepository _workflowTemplateRepository; private readonly IWorkflowConverter _workflowConverter; private readonly IActivityRepository _activityRepository; private readonly IActivityMessageSender _activityMessageSender; public WorkflowProcessor(ILogger logger, IClock clock, IWorkflowTemplateRepository workflowTemplateRepository, IActivityRepository activityRepository, IWorkflowConverter workflowConverter, IActivityMessageSender activityMessageSender) { _logger = logger; _clock = clock; _workflowTemplateRepository = workflowTemplateRepository; _activityRepository = activityRepository; _workflowConverter = workflowConverter; _activityMessageSender = activityMessageSender; } public async Task ProgressActivity( AuditUserDetails auditUserDetails, GeneralIdRef activityId, CancellationToken cancellationToken ) { _logger.LogInformation("{DateTime}: Progressing Activity {messageId}", _clock.GetNow, activityId); var activityInstance = await _activityRepository.GetActivityInstanceAsync(activityId, cancellationToken); if (activityInstance == null) { _logger.LogInformation("{DateTime}: Unable to find activity {messageId}", _clock.GetNow, activityId); throw new InvalidDataException("activityInstance not found"); } if (activityInstance.ActivityState == ActivityState.Cancelled) { _logger.LogInformation("{DateTime}: Activity {messageId} is cancelled, skipping", _clock.GetNow, activityId); return; } if (activityInstance.ActivityState == ActivityState.Completed) { _logger.LogInformation("{DateTime}: Activity {messageId} is already completed, skipping", _clock.GetNow, activityId); return; } WorkflowVersion workflowVersion = _workflowConverter.DeserialiseFromDatabase(activityInstance.WorkflowVersion); var hasCompletableTask = false; await _activityRepository.TransactionAsync(async () => { switch (activityInstance.ActivityState) { case ActivityState.Pending: { hasCompletableTask = await StartActivity(auditUserDetails, cancellationToken, activityInstance, workflowVersion); break; } case ActivityState.Active: hasCompletableTask = await ProgressRunningActivity(auditUserDetails, cancellationToken, activityInstance, workflowVersion); if (activityInstance.Tasks.All(x => x.ActivityState == ActivityState.Completed)) { activityInstance.ActivityState = ActivityState.Completed; await _activityRepository.UpdateActivityInstanceAsync(auditUserDetails, activityInstance, cancellationToken); } break; } }); if (hasCompletableTask) { _activityMessageSender.ProgressActivity(activityId); } } private async Task ProgressRunningActivity(AuditUserDetails auditUserDetails, CancellationToken cancellationToken, Activity activityInstance, WorkflowVersion workflowVersion) { bool hasCompletableTask = false; foreach (var task in activityInstance.Tasks) { var taskDefinition = workflowVersion.FindTask(task.TaskGuid)!; if (await taskDefinition.ProgressTask(task, activityInstance.Tasks, workflowVersion, _clock)) { hasCompletableTask = true; } } await _activityRepository.UpdateActivityTasksAsync(auditUserDetails, activityInstance.Tasks, cancellationToken); return hasCompletableTask; } private async Task StartActivity( AuditUserDetails auditUserDetails, CancellationToken cancellationToken, Activity? activityInstance, WorkflowVersion workflowVersion ) { bool hasCompletableTask = false; activityInstance.ActivityState = ActivityState.Active; await _activityRepository.UpdateActivityInstanceAsync(auditUserDetails, activityInstance, cancellationToken); var tasks = await PlanTaskExecution(auditUserDetails, activityInstance, workflowVersion, cancellationToken); await StartInitialTasks(auditUserDetails, tasks, workflowVersion, cancellationToken); if (tasks.Any(task => task.ActivityState == ActivityState.ReadyToComplete)) { hasCompletableTask = true; } return hasCompletableTask; } private async Task StartInitialTasks(AuditUserDetails auditUserDetails, ICollection tasks, WorkflowVersion workflowVersion, CancellationToken cancellationToken) { var startTasks = workflowVersion.GetStartTasks().ToList(); var startTaskGuids = startTasks.Select(t => t.Guid).ToHashSet(); foreach (var task in tasks) { if (startTaskGuids.Contains(task.TaskGuid)) { var taskDefinition = workflowVersion.FindTask(task.TaskGuid)!; await taskDefinition.StartTask(task, _clock); } } await _activityRepository.UpdateActivityTasksAsync(auditUserDetails, tasks, cancellationToken); } private async Task> PlanTaskExecution(AuditUserDetails auditUserDetails, Activity activityInstance, WorkflowVersion workflowVersion, CancellationToken cancellationToken) { var activityOrdinalCounter = 1; //Create task instances for the activity var activityTasks = new List(); foreach (var task in workflowVersion.Tasks) { var activityTask = new ActivityTask { Guid = Guid.NewGuid(), Activity = activityInstance, ActivityState = ActivityState.Pending, TaskGuid = task.Guid, TaskType = task.GetType().FullName!, TaskName = task.Name, ActivityOrdinal = activityOrdinalCounter++, }; if (task is IAssignees assignableTask) activityTask.Assignments = await GenerateAssignmentForTask(activityTask, assignableTask, auditUserDetails, cancellationToken ); activityTasks.Add(activityTask); } await _activityRepository.AddActivityTasksAsync(auditUserDetails, activityTasks, cancellationToken); return activityTasks; } private async Task> GenerateAssignmentForTask(ActivityTask activityTask, IAssignees task, AuditUserDetails auditUserDetails, CancellationToken cancellationToken) { // Find the IAssignees interface implemented by this instance var typedInterface = task.GetType() .GetInterfaces() .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAssignees<,>)); if (typedInterface == null) throw new InvalidOperationException("Task does not implement IAssignees."); // Get the Assignees property var assigneesProperty = typedInterface.GetProperty("Assignees"); var assignees = (IEnumerable)assigneesProperty.GetValue(task); var results = new List(); foreach (var assignee in assignees) { var typedAssignee = (ITaskAssignee)assignee; var activityAssignment = new ActivityAssignment { Guid = Guid.NewGuid(), Raci = typedAssignee.Raci, Role = typedAssignee.Role!, User = typedAssignee.User!, }; if (typedAssignee is IBypassable bypassableAssignee) { activityAssignment.Bypassable = bypassableAssignee.Bypassable; } if (typedAssignee is IApprovalTaskAssignee approvalAssignee) { activityAssignment.AllowNoVerdict = approvalAssignee.AllowNoVerdict; } results.Add(activityAssignment); } return results; } }