diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/DependencyInjection.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/DependencyInjection.cs index 2be4cd2..37baa5c 100644 --- a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/DependencyInjection.cs +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/DependencyInjection.cs @@ -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() ?? new DatabaseOptions(); if (string.IsNullOrWhiteSpace(connectionString)) { @@ -20,9 +22,14 @@ public static class DependencyInjection services.AddDbContext(options => { - options.UseSqlServer(connectionString); + options.UseSqlServer(connectionString, sqlOptions => + { + sqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds); + }); }); + services.Configure(configuration.GetSection(DatabaseOptions.SectionName)); + services.AddScoped(); services.AddScoped(); diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Options/DatabaseOptions.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Options/DatabaseOptions.cs new file mode 100644 index 0000000..7c65959 --- /dev/null +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Options/DatabaseOptions.cs @@ -0,0 +1,22 @@ +namespace DigitalData.TaskFlow.Infrastructure.Options; + +/// +/// Holds infrastructure database options. +/// +public sealed class DatabaseOptions +{ + /// + /// Configuration section name. + /// + public const string SectionName = "Database"; + + /// + /// Gets or sets command timeout in seconds for SQL operations. + /// + public int CommandTimeoutSeconds { get; set; } = 30; + + /// + /// Gets or sets object type passed to workflow listing procedure. + /// + public string WorkflowListingObjectType { get; set; } = "Ferdinand"; +} diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/DocumentRepository.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/DocumentRepository.cs index 234353e..796a743 100644 --- a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/DocumentRepository.cs +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/DocumentRepository.cs @@ -4,7 +4,7 @@ using DigitalData.TaskFlow.Domain.Documents; namespace DigitalData.TaskFlow.Infrastructure.Repositories; /// -/// Temporary document repository implementation used as Infrastructure phase baseline. +/// Document repository implementation for binary document retrieval. /// public sealed class DocumentRepository : IDocumentRepository { @@ -13,10 +13,13 @@ public sealed class DocumentRepository : IDocumentRepository /// /// Document identifier. /// Cancellation token. - /// Binary document payload. - public Task GetAsync(string id, CancellationToken cancellationToken) + /// Binary document payload when found; otherwise . + public Task 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."); } } diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/WorkflowRepository.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/WorkflowRepository.cs index e550d7c..1ba4ea2 100644 --- a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/WorkflowRepository.cs +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/WorkflowRepository.cs @@ -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; /// /// Workflow repository implementation for reading workflow listing data and applying close operations. /// -public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowRepository +public sealed class WorkflowRepository(TaskFlowDbContext dbContext, IOptions databaseOptions) : IWorkflowRepository { /// /// Retrieves grouped workflow data for a user. @@ -20,7 +23,9 @@ public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowR public async Task 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 EvaluateCloseOutcomeAsync( + int wisagId, + int processId, + CancellationToken cancellationToken) + { + var activeForUser = await dbContext.Database + .SqlQuery($"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($"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($"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 TryGetWorkflowMetaCodeAsync(int wisagId, string objectType, CancellationToken cancellationToken) + { + const string resultType = "json"; + + var jsonPayload = await dbContext.Database + .SqlQuery($"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; + } } }