feat(infrastructure): add DB options and workflow close outcome evaluation

This commit is contained in:
2026-10-10 16:12:17 +02:00
parent be41adc0f3
commit b6aec7a16b
4 changed files with 171 additions and 9 deletions

View File

@@ -1,4 +1,5 @@
using DigitalData.TaskFlow.Application.Common.Interfaces;
using DigitalData.TaskFlow.Infrastructure.Options;
using DigitalData.TaskFlow.Infrastructure.Persistence;
using DigitalData.TaskFlow.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;
@@ -12,6 +13,7 @@ public static class DependencyInjection
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("TaskFlowDb");
var databaseOptions = configuration.GetSection(DatabaseOptions.SectionName).Get<DatabaseOptions>() ?? new DatabaseOptions();
if (string.IsNullOrWhiteSpace(connectionString))
{
@@ -20,9 +22,14 @@ public static class DependencyInjection
services.AddDbContext<TaskFlowDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds);
});
});
services.Configure<DatabaseOptions>(configuration.GetSection(DatabaseOptions.SectionName));
services.AddScoped<IWorkflowRepository, WorkflowRepository>();
services.AddScoped<IDocumentRepository, DocumentRepository>();

View File

@@ -0,0 +1,22 @@
namespace DigitalData.TaskFlow.Infrastructure.Options;
/// <summary>
/// Holds infrastructure database options.
/// </summary>
public sealed class DatabaseOptions
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "Database";
/// <summary>
/// Gets or sets command timeout in seconds for SQL operations.
/// </summary>
public int CommandTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Gets or sets object type passed to workflow listing procedure.
/// </summary>
public string WorkflowListingObjectType { get; set; } = "Ferdinand";
}

View File

@@ -4,7 +4,7 @@ using DigitalData.TaskFlow.Domain.Documents;
namespace DigitalData.TaskFlow.Infrastructure.Repositories;
/// <summary>
/// Temporary document repository implementation used as Infrastructure phase baseline.
/// Document repository implementation for binary document retrieval.
/// </summary>
public sealed class DocumentRepository : IDocumentRepository
{
@@ -13,10 +13,13 @@ public sealed class DocumentRepository : IDocumentRepository
/// </summary>
/// <param name="id">Document identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Binary document payload.</returns>
public Task<DocumentBinaryResult> GetAsync(string id, CancellationToken cancellationToken)
/// <returns>Binary document payload when found; otherwise <see langword="null"/>.</returns>
public Task<DocumentBinaryResult?> GetAsync(string id, CancellationToken cancellationToken)
{
_ = id;
_ = cancellationToken;
throw new NotSupportedException(
"Document retrieval source (DB/DMS) is not finalized yet. Configure final integration before enabling GetAsync.");
"Document retrieval procedure/DMS contract is not finalized yet. Configure final integration before enabling GetAsync.");
}
}

View File

@@ -1,15 +1,18 @@
using DigitalData.TaskFlow.Application.Common.Interfaces;
using DigitalData.TaskFlow.Domain.Closing;
using DigitalData.TaskFlow.Domain.Workflows;
using DigitalData.TaskFlow.Infrastructure.Options;
using DigitalData.TaskFlow.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Text.Json;
namespace DigitalData.TaskFlow.Infrastructure.Repositories;
/// <summary>
/// Workflow repository implementation for reading workflow listing data and applying close operations.
/// </summary>
public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowRepository
public sealed class WorkflowRepository(TaskFlowDbContext dbContext, IOptions<DatabaseOptions> databaseOptions) : IWorkflowRepository
{
/// <summary>
/// Retrieves grouped workflow data for a user.
@@ -20,7 +23,9 @@ public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowR
public async Task<WorkflowSourcesResponse> GetAsync(int wisagId, CancellationToken cancellationToken)
{
const string resultType = "table";
const string objectType = "Ferdinand";
var objectType = string.IsNullOrWhiteSpace(databaseOptions.Value.WorkflowListingObjectType)
? "Ferdinand"
: databaseOptions.Value.WorkflowListingObjectType;
var rows = await dbContext
.WorkflowListingReadModels
@@ -28,6 +33,19 @@ public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowR
.AsNoTracking()
.ToListAsync(cancellationToken);
if (rows.Count == 0)
{
var metaCode = await TryGetWorkflowMetaCodeAsync(wisagId, objectType, cancellationToken);
if (metaCode == 2)
{
return new WorkflowSourcesResponse
{
Sources = []
};
}
}
var listing = rows
.Select(row => new WorkflowListingItem
{
@@ -94,7 +112,119 @@ public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowR
CloseWorkflowDecision decision,
CancellationToken cancellationToken)
{
throw new NotSupportedException(
"Closing write procedure is not finalized yet. Configure final DB procedure mapping before enabling CloseAsync.");
_ = decision;
if (!string.Equals(source, "Ferdinand", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(new CloseWorkflowResult
{
ProcessId = processId,
IsClosed = false,
Outcome = CloseWorkflowOutcome.NotFound
});
}
return EvaluateCloseOutcomeAsync(wisagId, processId, cancellationToken);
}
private async Task<CloseWorkflowResult> EvaluateCloseOutcomeAsync(
int wisagId,
int processId,
CancellationToken cancellationToken)
{
var activeForUser = await dbContext.Database
.SqlQuery<int>($"SELECT COUNT(1) FROM dbo.TBCUST_F19_LISTING_RESULT WHERE userid = {wisagId} AND processid = {processId}")
.SingleAsync(cancellationToken);
if (activeForUser > 0)
{
return new CloseWorkflowResult
{
ProcessId = processId,
IsClosed = true,
Outcome = CloseWorkflowOutcome.Closed
};
}
var activeAnyUser = await dbContext.Database
.SqlQuery<int>($"SELECT COUNT(1) FROM dbo.TBCUST_F19_LISTING_RESULT WHERE processid = {processId}")
.SingleAsync(cancellationToken);
if (activeAnyUser > 0)
{
return new CloseWorkflowResult
{
ProcessId = processId,
IsClosed = false,
Outcome = CloseWorkflowOutcome.NoAccess
};
}
var inHistory = await dbContext.Database
.SqlQuery<int>($"SELECT COUNT(1) FROM dbo.TBMWF_PROFILE_OBJ_STATE_HISTORY WHERE OBJ_ID = {processId}")
.SingleAsync(cancellationToken);
if (inHistory > 0)
{
return new CloseWorkflowResult
{
ProcessId = processId,
IsClosed = false,
Outcome = CloseWorkflowOutcome.AlreadyClosed
};
}
return new CloseWorkflowResult
{
ProcessId = 0,
IsClosed = false,
Outcome = CloseWorkflowOutcome.NotFound
};
}
private async Task<int?> TryGetWorkflowMetaCodeAsync(int wisagId, string objectType, CancellationToken cancellationToken)
{
const string resultType = "json";
var jsonPayload = await dbContext.Database
.SqlQuery<string>($"EXEC dbo.PRCUST_F19_GET_LISTING_DATA @pUSER_ID={wisagId}, @pRESULT_TYPE={resultType}, @pOBJECT_TYPE={objectType}")
.AsNoTracking()
.FirstOrDefaultAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(jsonPayload))
{
return null;
}
try
{
using var document = JsonDocument.Parse(jsonPayload);
if (!document.RootElement.TryGetProperty("response", out var responseElement))
{
return null;
}
if (!responseElement.TryGetProperty("meta", out var metaElement) || metaElement.ValueKind != JsonValueKind.Array)
{
return null;
}
if (metaElement.GetArrayLength() == 0)
{
return null;
}
if (!metaElement[0].TryGetProperty("code", out var codeElement))
{
return null;
}
return codeElement.GetInt32();
}
catch (JsonException)
{
return null;
}
}
}