Able to get a milestone task into the ready to complete phase, including storing the outcome

This commit is contained in:
Colin Dawson 2026-03-12 13:09:55 +00:00
parent 8afe7ac5a0
commit e24c8e68fe
9 changed files with 2362 additions and 21 deletions

View File

@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
namespace e_suite.Database.Core.Tables.Activity;
[DisplayName("Activity")]
[DisplayName("ActivityTask")]
[Table("ActivityTasks", Schema = "Activity")]
[Index(nameof(Guid), IsUnique = true)]
[Index(nameof(ActivityId), nameof(ActivityOrdinal), IsUnique = true)]
@ -69,4 +69,6 @@ public class ActivityTask : IGeneralId, ISoftDeletable
public virtual ActivityTask ParentTask { get; set; } = null!;
public ICollection<ActivityTask> Tasks { get; set; } = [];
public List<string> Outcomes { get; set; } = [];
}

View File

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace esuite.Database.SqlServer.Migrations
{
/// <inheritdoc />
public partial class AddedTaskOutcomes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Outcomes",
schema: "Activity",
table: "ActivityTasks",
type: "nvarchar(max)",
nullable: false,
defaultValue: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Outcomes",
schema: "Activity",
table: "ActivityTasks");
}
}
}

View File

@ -183,6 +183,10 @@ namespace esuite.Database.SqlServer.Migrations
b.Property<DateTimeOffset>("LastUpdated")
.HasColumnType("datetimeoffset");
b.PrimitiveCollection<string>("Outcomes")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<long?>("ParentId")
.HasColumnType("bigint");

View File

@ -1,6 +1,7 @@
using e_suite.API.Common.repository;
using e_suite.Database.Audit;
using e_suite.Database.Core.Tables.Activity;
using e_suite.Messaging.Common;
using e_suite.Workflow.Core;
using e_suite.Workflow.Core.Extensions;
using eSuite.Core.Clock;
@ -17,14 +18,16 @@ public class WorkflowProcessor : IWorkflowProcessor
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)
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(
@ -58,32 +61,53 @@ public class WorkflowProcessor : IWorkflowProcessor
var workflowVersion = _workflowConverter.DeserialiseFromDatabase(activityInstance.WorkflowVersion);
bool hasCompletedTask = false;
var hasCompletableTask = false;
await _activityRepository.TransactionAsync(async () =>
{
ICollection<ActivityTask> tasks;
if (activityInstance.ActivityState == ActivityState.Pending)
switch (activityInstance.ActivityState)
{
case ActivityState.Pending:
{
hasCompletableTask = await StartActivity(auditUserDetails, cancellationToken, activityInstance,
workflowVersion);
break;
}
case ActivityState.Active:
throw new NotImplementedException("don't know how to progress a running instance.");
case ActivityState.ReadyToComplete:
throw new NotImplementedException("don't know how to progress a ReadyToComplete instance.");
}
});
if (hasCompletableTask)
{
_activityMessageSender.ProgressActivity(activityId);
}
}
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);
tasks = await PlanTaskExecution(auditUserDetails, activityInstance, workflowVersion, cancellationToken);
var tasks = await PlanTaskExecution(auditUserDetails, activityInstance, workflowVersion, cancellationToken);
await StartInitialTasks(auditUserDetails, tasks, workflowVersion, cancellationToken);
if (tasks.Any(task => task.ActivityState == ActivityState.ReadyToComplete))
{
hasCompletedTask = true;
hasCompletableTask = true;
}
}
});
if (hasCompletedTask)
{
//send workflow progress message here.
}
return hasCompletableTask;
}
private async Task StartInitialTasks(AuditUserDetails auditUserDetails, ICollection<ActivityTask> tasks, WorkflowVersion workflowVersion, CancellationToken cancellationToken)

View File

@ -14,6 +14,7 @@
<ItemGroup>
<ProjectReference Include="..\e-suite.API.Common\e-suite.API.Common\e-suite.API.Common.csproj" />
<ProjectReference Include="..\e-suite.DependencyInjection\e-suite.DependencyInjection.csproj" />
<ProjectReference Include="..\e-suite.Messaging.Common\e-suite.Messaging.Common\e-suite.Messaging.Common.csproj" />
<ProjectReference Include="..\e-suite.Workflow.Core\e-suite.Workflow.Core.csproj" />
</ItemGroup>

View File

@ -0,0 +1,50 @@
using e_suite.Database.Core.Tables.Activity;
using eSuite.Core.Enums;
namespace e_suite.Workflow.Core.Extensions;
public static class ActivityTaskOutcomeExtensions
{
extension(ActivityTask task)
{
public void SetState(ActivityState newState)
{
if (newState == ActivityState.Active)
{
if (task.StartDateTime == null)
throw new InvalidOperationException(
$"Cannot set state to Active because StartDateTime is not set for task {task.Id}."
);
}
if (newState == ActivityState.ReadyToComplete)
{
if (task.Outcomes == null || task.Outcomes.Count == 0)
throw new InvalidOperationException(
$"Cannot set state to ReadyToComplete because no outcomes have been recorded for task {task.Id}."
);
}
if (newState == ActivityState.Completed)
{
if (task.FinishDateTime == null)
throw new InvalidOperationException(
$"Cannot set state to Completed because FinishDateTime is not set for task {task.Id}."
);
}
task.ActivityState = newState;
}
public void AddOutcome<T>(T outcome)
{
if (outcome == null) throw new ArgumentNullException(nameof(outcome));
// Explicit, predictable conversion
var newOutcome = outcome.ToString()!.Trim();
if (!task.Outcomes.Contains(newOutcome))
task.Outcomes.Add(newOutcome);
}
}
}

View File

@ -143,8 +143,8 @@ public static class TaskExtensions
public async Task StartTask(ActivityTask activityTask, IClock clock)
{
activityTask.ActivityState = ActivityState.Active;
activityTask.StartDateTime = clock.GetNow;
activityTask.SetState(ActivityState.Active);
await task.OnStartedAsync(activityTask);
}

View File

@ -1,6 +1,7 @@
using e_suite.Database.Core.Tables.Activity;
using e_suite.Workflow.Core.Attributes;
using e_suite.Workflow.Core.Enums;
using e_suite.Workflow.Core.Extensions;
using e_suite.Workflow.Core.Interfaces;
using eSuite.Core.Enums;
@ -17,7 +18,7 @@ public class MilestoneTask : TaskBase, IOutcome<DefaultOutcome>
public override async Task OnStartedAsync(ActivityTask activityTask)
{
await base.OnStartedAsync(activityTask);
activityTask.ActivityState = ActivityState.ReadyToComplete;
//TODO need to set the outcome of completed here
activityTask.AddOutcome(DefaultOutcome.Complete);
activityTask.SetState(ActivityState.ReadyToComplete);
}
}