Backend/e-suite.Service.WorkflowProcessor/WorkflowProcessor.cs

233 lines
9.1 KiB
C#

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<bool> 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<bool> 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<ActivityTask> 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<ICollection<ActivityTask>> PlanTaskExecution(AuditUserDetails auditUserDetails, Activity activityInstance, WorkflowVersion workflowVersion, CancellationToken cancellationToken)
{
var activityOrdinalCounter = 1;
//Create task instances for the activity
var activityTasks = new List<ActivityTask>();
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<ICollection<ActivityAssignment>> GenerateAssignmentForTask(ActivityTask activityTask, IAssignees task, AuditUserDetails auditUserDetails, CancellationToken cancellationToken)
{
// Find the IAssignees<T> 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<T>.");
// Get the Assignees property
var assigneesProperty = typedInterface.GetProperty("Assignees");
var assignees = (IEnumerable)assigneesProperty.GetValue(task);
var results = new List<ActivityAssignment>();
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;
}
}