Refactored Case Status automation and shared Case functionality:

* Extracted Case Notification and HTTP Web Hook functionality into a shared Jube.Case library for use across Jube.App and Jube.Engine.
* Added the new Jube.Case library to the Jube.App Docker Compose configuration.
* Cleaned up classes moved to Jube.Case, converting to static classes or methods where appropriate.
* Implemented Case Status automation within the asynchronous background Case Creation process, including upward status classification.
This commit is contained in:
richard-churchman
2026-02-09 09:38:28 +02:00
parent 9fe505127a
commit 76ab3ac1c6
14 changed files with 114 additions and 76 deletions
@@ -16,6 +16,7 @@ namespace Jube.App.Controllers.Helper
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Case;
using Code;
using Data.Context;
using Data.Repository;
@@ -115,10 +116,9 @@ namespace Jube.App.Controllers.Helper
return Ok(model);
}
var sendHttpEndpoint = new SendHttpEndpoint();
if (caseWorkflowMacro.HttpEndpointTypeId != null)
{
await sendHttpEndpoint.SendAsync(caseWorkflowMacro.HttpEndpoint,
await SendHttpEndpoint.SendAsync(caseWorkflowMacro.HttpEndpoint,
caseWorkflowMacro.HttpEndpointTypeId.Value
, values);
}
@@ -21,6 +21,7 @@ namespace Jube.App.Controllers.Repository
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Case;
using Code;
using Data.Context;
using Data.Poco;
@@ -412,10 +413,9 @@ namespace Jube.App.Controllers.Repository
if (caseWorkflowStatus.EnableHttpEndpoint == 1)
{
var sendHttpEndpoint = new SendHttpEndpoint();
if (caseWorkflowStatus.HttpEndpointTypeId != null)
{
await sendHttpEndpoint.SendAsync(caseWorkflowStatus.HttpEndpoint,
await SendHttpEndpoint.SendAsync(caseWorkflowStatus.HttpEndpoint,
caseWorkflowStatus.HttpEndpointTypeId.Value
, values);
}
@@ -18,6 +18,7 @@ namespace Jube.App.Controllers.Repository
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Case;
using Code;
using Data.Context;
using Data.Poco;
@@ -141,10 +142,9 @@ namespace Jube.App.Controllers.Repository
return Ok(await repository.InsertAsync(mapper.Map<CaseNote>(model), token));
}
var sendHttpEndpoint = new SendHttpEndpoint();
if (caseWorkflowAction.HttpEndpointTypeId != null)
{
await sendHttpEndpoint.SendAsync(caseWorkflowAction.HttpEndpoint,
await SendHttpEndpoint.SendAsync(caseWorkflowAction.HttpEndpoint,
caseWorkflowAction.HttpEndpointTypeId.Value
, values);
}
@@ -19,6 +19,7 @@ namespace Jube.App.Controllers.Repository
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Case;
using Code;
using Data.Context;
using Data.Poco;
@@ -175,10 +176,9 @@ namespace Jube.App.Controllers.Repository
return Ok(entry);
}
var sendHttpEndpoint = new SendHttpEndpoint();
if (caseWorkflowForm.HttpEndpointTypeId != null)
{
await sendHttpEndpoint.SendAsync(caseWorkflowForm.HttpEndpoint,
await SendHttpEndpoint.SendAsync(caseWorkflowForm.HttpEndpoint,
caseWorkflowForm.HttpEndpointTypeId.Value
, values);
}
+1
View File
@@ -19,6 +19,7 @@ COPY ["Accord.Genetic/Accord.Genetic.csproj", "Accord.Genetic/"]
COPY ["Accord.MachineLearning/Accord.MachineLearning.csproj", "Accord.MachineLearning/"]
COPY ["Accord.Neuro/Accord.Neuro.csproj", "Accord.Neuro/"]
COPY ["Jube.Parser/Jube.Parser.csproj", "Jube.Parser/"]
COPY ["Jube.Case/Jube.Case.csproj", "Jube.Case/"]
COPY ["Jube.Migrations/Jube.Migrations.csproj", "Jube.Migrations/"]
COPY ["Jube.Service/Jube.Service.csproj", "Jube.Service/"]
COPY ["Jube.Validations/Jube.Validations.csproj", "Jube.Validations/"]
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Jube.DynamicEnvironment\Jube.DynamicEnvironment.csproj"/>
</ItemGroup>
<ItemGroup>
<Reference Include="log4net">
<HintPath>..\..\..\.nuget\packages\log4net\2.0.14\lib\netstandard2.0\log4net.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
@@ -11,37 +11,23 @@
* see <https://www.gnu.org/licenses/>.
*/
namespace Jube.App.Code
namespace Jube.Case
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using DynamicEnvironment;
using log4net;
public class Notification
public class Notification(ILog log, DynamicEnvironment dynamicEnvironment)
{
private readonly DynamicEnvironment dynamicEnvironment;
private readonly ILog log;
public Notification(ILog log, DynamicEnvironment dynamicEnvironment)
{
this.dynamicEnvironment = dynamicEnvironment;
this.log = log;
}
public async Task SendAsync(int notificationType, string notificationDestination, string notificationSubject,
string notificationBody, Dictionary<string, string> values, CancellationToken token = default
)
{
var notificationTokenization = new Tokenisation();
var replacedNotificationDestination = notificationDestination;
if (!String.IsNullOrEmpty(replacedNotificationDestination))
{
var notificationDestinationTokens =
notificationTokenization.ReturnTokens(replacedNotificationDestination);
Tokenisation.ReturnTokens(replacedNotificationDestination);
foreach (var notificationToken in notificationDestinationTokens)
{
if (!values.TryGetValue(notificationToken, out var value))
@@ -59,7 +45,7 @@ namespace Jube.App.Code
var replacedNotificationSubject = notificationSubject;
if (!String.IsNullOrEmpty(replacedNotificationSubject))
{
var notificationSubjectTokens = notificationTokenization.ReturnTokens(replacedNotificationSubject);
var notificationSubjectTokens = Tokenisation.ReturnTokens(replacedNotificationSubject);
foreach (var notificationToken in notificationSubjectTokens)
{
if (!values.TryGetValue(notificationToken, out var value))
@@ -76,7 +62,8 @@ namespace Jube.App.Code
var replacedNotificationBody = notificationBody;
if (!String.IsNullOrEmpty(replacedNotificationBody))
{
var notificationBodyTokens = notificationTokenization.ReturnTokens(replacedNotificationBody);
var notificationBodyTokens = Tokenisation.ReturnTokens(replacedNotificationBody);
foreach (var notificationToken in notificationBodyTokens)
{
if (!values.TryGetValue(notificationToken, out var value))
@@ -11,26 +11,21 @@
* see <https://www.gnu.org/licenses/>.
*/
namespace Jube.App.Code
namespace Jube.Case
{
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
public class SendHttpEndpoint
public static class SendHttpEndpoint
{
public async Task SendAsync(string httpEndpoint, byte httpEndpointTypeId, Dictionary<string, string> values)
public static async Task SendAsync(string httpEndpoint, byte httpEndpointTypeId, Dictionary<string, string> values)
{
if (String.IsNullOrEmpty(httpEndpoint))
{
return;
}
var tokenization = new Tokenisation();
var urlTokens = tokenization.ReturnTokens(httpEndpoint);
var urlTokens = Tokenisation.ReturnTokens(httpEndpoint);
var replacedUrl = httpEndpoint;
foreach (var token in urlTokens)
@@ -11,26 +11,16 @@
* see <https://www.gnu.org/licenses/>.
*/
namespace Jube.App.Code
namespace Jube.Case
{
using System;
using System.Net;
using System.Net.Mail;
using System.Web;
using DynamicEnvironment;
using log4net;
public class SendMail
public class SendMail(DynamicEnvironment dynamicEnvironment, ILog log)
{
private readonly DynamicEnvironment dynamicEnvironment;
private readonly ILog log;
public SendMail(DynamicEnvironment dynamicEnvironment, ILog log)
{
this.dynamicEnvironment = dynamicEnvironment;
this.log = log;
}
public void Send(string toEmail, string subject, string body)
{
try
@@ -11,27 +11,15 @@
* see <https://www.gnu.org/licenses/>.
*/
namespace Jube.App.Code
namespace Jube.Case
{
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using DynamicEnvironment;
using log4net;
public class SendSms
public class SendSms(DynamicEnvironment dynamicEnvironment, ILog log)
{
private static readonly HttpClient HttpClient = new HttpClient();
private readonly DynamicEnvironment dynamicEnvironment;
private readonly ILog log;
public SendSms(DynamicEnvironment dynamicEnvironment, ILog log)
{
this.dynamicEnvironment = dynamicEnvironment;
this.log = log;
}
public async Task SendAsync(string notificationDestination, string notificationBody, CancellationToken token = default)
{
@@ -11,14 +11,13 @@
* see <https://www.gnu.org/licenses/>.
*/
namespace Jube.App.Code
namespace Jube.Case
{
using System.Collections.Generic;
using System.Text.RegularExpressions;
public class Tokenisation
public static class Tokenisation
{
public List<string> ReturnTokens(string message)
public static List<string> ReturnTokens(string message)
{
var matches = Regex.Matches(message, @"\[@(.*?)\]");
var returnList = new List<string>();
@@ -1,6 +1,7 @@
namespace Jube.Engine.BackgroundTasks.TaskStarters.Case
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Data.Context;
@@ -9,7 +10,9 @@ namespace Jube.Engine.BackgroundTasks.TaskStarters.Case
using Data.Repository;
using DynamicEnvironment;
using EntityAnalysisModelInvoke.Models.CaseManagement;
using Jube.Case;
using log4net;
using Newtonsoft.Json.Linq;
public static class CaseProcessing
{
@@ -77,20 +80,27 @@ namespace Jube.Engine.BackgroundTasks.TaskStarters.Case
var existing = await query.ExecuteAsync(model.CaseWorkflowGuid, model.CaseKey, model.CaseKeyValue, token).ConfigureAwait(false);
var repositoryCasesWorkflowsStatus =
new CaseWorkflowStatusRepository(dbContext, createCase.TenantRegistryId);
CaseWorkflowStatus finalCasesWorkflowsStatus = null;
if (existing == null)
{
finalCasesWorkflowsStatus =
await repositoryCasesWorkflowsStatus.GetByGuidAsync(model.CaseWorkflowStatusGuid, token).ConfigureAwait(false);
await repositoryCase.InsertAsync(model, token).ConfigureAwait(false);
}
else
{
var repositoryCasesWorkflowsStatus =
new CaseWorkflowStatusRepository(dbContext, createCase.TenantRegistryId);
var recordCasesWorkflowsStatus =
var existingCasesWorkflowsStatus =
await repositoryCasesWorkflowsStatus.GetByGuidAsync(model.CaseWorkflowStatusGuid, token).ConfigureAwait(false);
if (recordCasesWorkflowsStatus.Priority < existing.Priority)
if (existingCasesWorkflowsStatus.Priority < existing.Priority)
{
finalCasesWorkflowsStatus =
await repositoryCasesWorkflowsStatus.GetByGuidAsync(model.CaseWorkflowStatusGuid, token).ConfigureAwait(false);
model.Id = existing.CaseId;
model.Locked = 0;
model.CaseWorkflowStatusGuid = createCase.CaseWorkflowStatusGuid;
@@ -98,6 +108,48 @@ namespace Jube.Engine.BackgroundTasks.TaskStarters.Case
}
}
var caseBytes = 0;
if (createCase.Json != null)
{
if (finalCasesWorkflowsStatus != null)
{
caseBytes = createCase.Json.Length;
var jObject = JObject.Parse(model.Json);
var values = new Dictionary<string, string>();
foreach (var (key, value) in jObject)
{
if (value != null)
{
values.Add(key, value.ToString());
}
}
if (finalCasesWorkflowsStatus.EnableNotification == 1 ||
finalCasesWorkflowsStatus.EnableHttpEndpoint == 1)
{
if (finalCasesWorkflowsStatus.EnableNotification == 1)
{
var notification = new Notification(log, dynamicEnvironment);
await notification.SendAsync(finalCasesWorkflowsStatus.NotificationTypeId ?? 1,
finalCasesWorkflowsStatus.NotificationDestination,
finalCasesWorkflowsStatus.NotificationSubject,
finalCasesWorkflowsStatus.NotificationBody, values, token);
}
if (finalCasesWorkflowsStatus.EnableHttpEndpoint == 1)
{
if (finalCasesWorkflowsStatus.HttpEndpointTypeId != null)
{
await SendHttpEndpoint.SendAsync(finalCasesWorkflowsStatus.HttpEndpoint,
finalCasesWorkflowsStatus.HttpEndpointTypeId.Value
, values);
}
}
}
}
}
if (log.IsInfoEnabled)
{
log.Info(
@@ -105,7 +157,7 @@ namespace Jube.Engine.BackgroundTasks.TaskStarters.Case
$"Case Workflow ID of {createCase.EntityAnalysisModelInstanceEntryGuid}, " +
$"Case Workflow Status ID of {createCase.CaseWorkflowStatusGuid}, " +
$"Case Key of {createCase.CaseKeyValue}, " +
$"Case JSON Bytes {createCase.Json.Length}");
$"Case JSON Bytes {caseBytes}");
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
+4 -3
View File
@@ -13,9 +13,9 @@
<PackageReference Include="DnsClient" Version="1.2.0"/>
<PackageReference Include="Fastenshtein" Version="1.0.0.5"/>
<PackageReference Include="log4net" Version="2.0.14"/>
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.10.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.10.0" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.10.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.10.0"/>
<PackageReference Include="Microsoft.VisualBasic" Version="10.3.0"/>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
@@ -51,6 +51,7 @@
<ProjectReference Include="..\Accord.Neuro\Accord.Neuro.csproj"/>
<ProjectReference Include="..\Accord.Statistics\Accord.Statistics.csproj"/>
<ProjectReference Include="..\Jube.Cache\Jube.Cache.csproj"/>
<ProjectReference Include="..\Jube.Case\Jube.Case.csproj"/>
<ProjectReference Include="..\Jube.Data\Jube.Data.csproj"/>
<ProjectReference Include="..\Jube.DynamicEnvironment\Jube.DynamicEnvironment.csproj"/>
<ProjectReference Include="..\Jube.Dictionary\Jube.Dictionary.csproj"/>
+6
View File
@@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jube.HealthCheck", "Jube.He
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jube.Sandbox", "Jube.Sandbox\Jube.Sandbox.csproj", "{28E3B7D8-799C-4367-BA9C-1AECF86939D4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jube.Case", "Jube.Case\Jube.Case.csproj", "{0F52BDA7-5117-48E9-8DB8-859221524B50}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -165,6 +167,10 @@ Global
{28E3B7D8-799C-4367-BA9C-1AECF86939D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28E3B7D8-799C-4367-BA9C-1AECF86939D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28E3B7D8-799C-4367-BA9C-1AECF86939D4}.Release|Any CPU.Build.0 = Release|Any CPU
{0F52BDA7-5117-48E9-8DB8-859221524B50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F52BDA7-5117-48E9-8DB8-859221524B50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F52BDA7-5117-48E9-8DB8-859221524B50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F52BDA7-5117-48E9-8DB8-859221524B50}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection