refactor(application): update workflow queries and exception handling
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an API-facing exception that carries HTTP and domain meta information.
|
||||
/// </summary>
|
||||
public class ApiException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="statusCode">HTTP status code to return.</param>
|
||||
/// <param name="metaCode">Domain meta code to return.</param>
|
||||
/// <param name="message">Response message.</param>
|
||||
public ApiException(int statusCode, MetaCode metaCode, string message)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
MetaCode = metaCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HTTP status code.
|
||||
/// </summary>
|
||||
public int StatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the domain meta code.
|
||||
/// </summary>
|
||||
public MetaCode MetaCode { get; }
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a 409 API exception.
|
||||
/// </summary>
|
||||
public sealed class ConflictApiException : ApiException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConflictApiException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="metaCode">Domain meta code to return.</param>
|
||||
/// <param name="message">Response message.</param>
|
||||
public ConflictApiException(MetaCode metaCode, string message)
|
||||
: base(409, metaCode, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a 403 API exception.
|
||||
/// </summary>
|
||||
public sealed class ForbiddenApiException : ApiException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ForbiddenApiException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">Response message.</param>
|
||||
public ForbiddenApiException(string message)
|
||||
: base(403, MetaCode.NoAccess, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a 400 API exception.
|
||||
/// </summary>
|
||||
public sealed class InvalidRequestApiException : ApiException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvalidRequestApiException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">Response message.</param>
|
||||
public InvalidRequestApiException(string message)
|
||||
: base(400, MetaCode.InvalidRequest, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
|
||||
public sealed class InvalidWorkflowPayloadException(string message) : Exception(message)
|
||||
{
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a 404 API exception.
|
||||
/// </summary>
|
||||
public sealed class NotFoundApiException : ApiException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotFoundApiException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">Response message.</param>
|
||||
public NotFoundApiException(string message)
|
||||
: base(404, MetaCode.NotFound, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using DigitalData.TaskFlow.Domain.Common;
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Creates standard API envelope instances with centralized meta code/message handling.
|
||||
/// </summary>
|
||||
public static class ApiEnvelopeFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a successful or business-result response envelope.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Payload type.</typeparam>
|
||||
/// <param name="data">Payload data.</param>
|
||||
/// <param name="code">Business result code.</param>
|
||||
/// <param name="message">Optional custom message. If not supplied, a default message is resolved from <paramref name="code"/>.</param>
|
||||
/// <returns>Response envelope containing meta and payload.</returns>
|
||||
public static ApiEnvelope<T> Create<T>(T data, MetaCode code = MetaCode.Ok, string? message = null)
|
||||
{
|
||||
return new ApiEnvelope<T>
|
||||
{
|
||||
Meta =
|
||||
[
|
||||
new MetaEntry
|
||||
{
|
||||
Code = (int)code,
|
||||
Message = message ?? ResolveDefaultMessage(code)
|
||||
}
|
||||
],
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveDefaultMessage(MetaCode code)
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
MetaCode.Ok => "OK",
|
||||
MetaCode.NoOpenWorkflows => "Keine offenen Workflows",
|
||||
MetaCode.NoAccess => "Kein Zugriff",
|
||||
MetaCode.InvalidRequest => "Ungultiger Request",
|
||||
MetaCode.InternalError => "Interner Fehler",
|
||||
MetaCode.NotFound => "Vorgang nicht gefunden",
|
||||
MetaCode.AlreadyClosed => "Vorgang bereits abgeschlossen",
|
||||
_ => "Interner Fehler"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
using DigitalData.TaskFlow.Domain.Documents;
|
||||
using DigitalData.TaskFlow.Domain.ProcedureResults;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Defines document-specific data access operations.
|
||||
/// </summary>
|
||||
public interface IDocumentRepository : IRepository
|
||||
public interface IDocumentRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets document binary content for the given document identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">Document identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Binary document payload when found; otherwise <see langword="null"/>.</returns>
|
||||
Task<DocumentBinaryResult?> GetAsync(string id, CancellationToken cancellationToken);
|
||||
Task<Document?> GetAsync(int userId, long processId, long docId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using DigitalData.TaskFlow.Domain.Closing;
|
||||
using DigitalData.TaskFlow.Domain.Workflows;
|
||||
using DigitalData.TaskFlow.Domain.ProcedureResults;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||
|
||||
@@ -14,21 +13,5 @@ public interface IWorkflowRepository : IRepository
|
||||
/// <param name="wisagId">WISAG user identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Workflow data grouped by source.</returns>
|
||||
Task<WorkflowSourcesResponse> GetAsync(int wisagId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a closing decision to a workflow process.
|
||||
/// </summary>
|
||||
/// <param name="wisagId">WISAG user identifier.</param>
|
||||
/// <param name="source">Source system name.</param>
|
||||
/// <param name="processId">Process identifier.</param>
|
||||
/// <param name="decision">Closing decision payload.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Closing operation result.</returns>
|
||||
Task<CloseWorkflowResult> CloseAsync(
|
||||
int wisagId,
|
||||
string source,
|
||||
int processId,
|
||||
CloseWorkflowDecision decision,
|
||||
CancellationToken cancellationToken);
|
||||
Task<Workflow?> GetByWisagIdAsync(int wisagId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.TaskFlow.Application.Features.Workflows.Commands.CloseWorkflow;
|
||||
using DigitalData.TaskFlow.Domain.Closing;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Common.Mappings;
|
||||
|
||||
/// <summary>
|
||||
/// Defines AutoMapper configuration for application-level mappings.
|
||||
/// </summary>
|
||||
public sealed class ApplicationMappingProfile : Profile
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes mapping configuration.
|
||||
/// </summary>
|
||||
public ApplicationMappingProfile()
|
||||
{
|
||||
CreateMap<CloseWorkflowCommand, CloseWorkflowDecision>();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using DigitalData.TaskFlow.Application.Options;
|
||||
using FluentValidation;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -10,29 +9,30 @@ namespace DigitalData.TaskFlow.Application;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public const string LuckyPennySoftLicenseKeySection = "LuckyPennySoftLicenseKey";
|
||||
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var mediatROptions = configuration
|
||||
.GetSection(MediatROptions.SectionName)
|
||||
.Get<MediatROptions>();
|
||||
var lpSoftLicenseKey = configuration[LuckyPennySoftLicenseKeySection];
|
||||
var hasLpSoftLicenseKey = !string.IsNullOrWhiteSpace(lpSoftLicenseKey);
|
||||
|
||||
services
|
||||
.AddOptions<MediatROptions>()
|
||||
.Bind(configuration.GetSection(MediatROptions.SectionName));
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
services.AddMediatR(config =>
|
||||
{
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
|
||||
config.RegisterServicesFromAssembly(assembly);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(mediatROptions?.LuckyPennySoftLicenseKey))
|
||||
{
|
||||
config.LicenseKey = mediatROptions.LuckyPennySoftLicenseKey;
|
||||
}
|
||||
if (hasLpSoftLicenseKey)
|
||||
config.LicenseKey = lpSoftLicenseKey;
|
||||
});
|
||||
|
||||
services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly);
|
||||
services.AddValidatorsFromAssembly(assembly);
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
services.AddAutoMapper(_ => { }, Assembly.GetExecutingAssembly());
|
||||
services.AddAutoMapper(config =>
|
||||
{
|
||||
if (hasLpSoftLicenseKey)
|
||||
config.LicenseKey = lpSoftLicenseKey;
|
||||
}, assembly);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
using DigitalData.TaskFlow.Application.Common.Factories;
|
||||
using DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||
using DigitalData.TaskFlow.Domain.Common;
|
||||
using DigitalData.TaskFlow.Domain.Documents;
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Features.Documents.Queries.GetDocument;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to retrieve binary document content by document identifier.
|
||||
/// </summary>
|
||||
/// <param name="Id">Document identifier.</param>
|
||||
public sealed record GetDocumentQuery(string Id) : IRequest<ApiEnvelope<DocumentBinaryResult>>;
|
||||
|
||||
/// <summary>
|
||||
/// Validates <see cref="GetDocumentQuery"/> requests.
|
||||
/// </summary>
|
||||
public sealed class GetDocumentQueryValidator : AbstractValidator<GetDocumentQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes validation rules for <see cref="GetDocumentQuery"/>.
|
||||
/// </summary>
|
||||
public GetDocumentQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="GetDocumentQuery"/> requests.
|
||||
/// </summary>
|
||||
public sealed class GetDocumentQueryHandler(IDocumentRepository documentRepository)
|
||||
: IRequestHandler<GetDocumentQuery, ApiEnvelope<DocumentBinaryResult>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the query and returns binary document data in the standard API envelope.
|
||||
/// </summary>
|
||||
/// <param name="request">Query request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Document response envelope.</returns>
|
||||
public async Task<ApiEnvelope<DocumentBinaryResult>> Handle(GetDocumentQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var document = await documentRepository.GetAsync(request.Id, cancellationToken);
|
||||
|
||||
if (document is null)
|
||||
{
|
||||
throw new NotFoundApiException("Dokument nicht gefunden");
|
||||
}
|
||||
|
||||
return ApiEnvelopeFactory.Create(document);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Features.Documents.Queries;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="UserId"></param>
|
||||
/// <param name="ProcessId"></param>
|
||||
/// <param name="Id"></param>
|
||||
public sealed record GetDocumentQuery(int UserId, long ProcessId, long Id) : IRequest<JsonElement>;
|
||||
|
||||
public sealed class GetDocumentQueryHandler(IDocumentRepository Repo) : IRequestHandler<GetDocumentQuery, JsonElement>
|
||||
{
|
||||
public async Task<JsonElement> Handle(GetDocumentQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var document = await Repo.GetAsync(userId: request.UserId, processId: request.ProcessId, docId: request.Id, cancellationToken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(document?.Result))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(document.Result);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.ValueKind == JsonValueKind.Object &&
|
||||
root.TryGetProperty("response", out var response))
|
||||
{
|
||||
return response.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidWorkflowPayloadException(
|
||||
"Stored procedure returned an invalid payload. Expected a JSON object with a 'response' property.");
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
using DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||
using DigitalData.TaskFlow.Application.Common.Factories;
|
||||
using AutoMapper;
|
||||
using DigitalData.TaskFlow.Domain.Closing;
|
||||
using DigitalData.TaskFlow.Domain.Common;
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Features.Workflows.Commands.CloseWorkflow;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to close a workflow process using a selected closing method.
|
||||
/// </summary>
|
||||
/// <param name="WisagId">WISAG user identifier.</param>
|
||||
/// <param name="Source">Source system name.</param>
|
||||
/// <param name="ProcessId">Process identifier.</param>
|
||||
/// <param name="ClosingMethodId">Selected closing method identifier.</param>
|
||||
/// <param name="Comment">Optional user comment.</param>
|
||||
/// <param name="RejectionReasonId">Optional rejection reason identifier.</param>
|
||||
public sealed record CloseWorkflowCommand(
|
||||
int WisagId,
|
||||
string Source,
|
||||
int ProcessId,
|
||||
int ClosingMethodId,
|
||||
string? Comment,
|
||||
int? RejectionReasonId)
|
||||
: IRequest<ApiEnvelope<CloseWorkflowResult>>;
|
||||
|
||||
/// <summary>
|
||||
/// Validates <see cref="CloseWorkflowCommand"/> requests.
|
||||
/// </summary>
|
||||
public sealed class CloseWorkflowCommandValidator : AbstractValidator<CloseWorkflowCommand>
|
||||
{
|
||||
private static readonly string[] AllowedSources = ["Ferdinand", "Contracts"];
|
||||
private const int ApproveMethodId = 2;
|
||||
private const int RejectMethodId = 3;
|
||||
private const int BackToPerMethodId = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes validation rules for <see cref="CloseWorkflowCommand"/>.
|
||||
/// </summary>
|
||||
public CloseWorkflowCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.WisagId)
|
||||
.GreaterThan(0);
|
||||
|
||||
RuleFor(x => x.ProcessId)
|
||||
.GreaterThan(0);
|
||||
|
||||
RuleFor(x => x.ClosingMethodId)
|
||||
.GreaterThan(0);
|
||||
|
||||
RuleFor(x => x.Source)
|
||||
.NotEmpty()
|
||||
.Must(source => AllowedSources.Contains(source))
|
||||
.WithMessage("Source must be one of the supported source systems.");
|
||||
|
||||
RuleFor(x => x.ClosingMethodId)
|
||||
.Must(methodId => methodId is ApproveMethodId or RejectMethodId or BackToPerMethodId)
|
||||
.WithMessage("Unbekannte closing_method_id");
|
||||
|
||||
RuleFor(x => x.Comment)
|
||||
.NotEmpty()
|
||||
.When(x => x.ClosingMethodId is RejectMethodId or BackToPerMethodId)
|
||||
.WithMessage("Kommentar ist fur diese closing_method Pflicht");
|
||||
|
||||
RuleFor(x => x.RejectionReasonId)
|
||||
.NotNull()
|
||||
.When(x => x.ClosingMethodId == RejectMethodId)
|
||||
.WithMessage("rejection_reason_id ist fur diese closing_method Pflicht");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="CloseWorkflowCommand"/> requests.
|
||||
/// </summary>
|
||||
public sealed class CloseWorkflowCommandHandler(IWorkflowRepository workflowRepository, IMapper mapper)
|
||||
: IRequestHandler<CloseWorkflowCommand, ApiEnvelope<CloseWorkflowResult>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the closing request and returns the operation result in the standard API envelope.
|
||||
/// </summary>
|
||||
/// <param name="request">Command request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Closing result envelope.</returns>
|
||||
public async Task<ApiEnvelope<CloseWorkflowResult>> Handle(CloseWorkflowCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.Equals(request.Source, "Contracts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidRequestApiException("Quelle 'Contracts' wird fur Closing aktuell nicht unterstutzt");
|
||||
}
|
||||
|
||||
var decision = mapper.Map<CloseWorkflowDecision>(request);
|
||||
|
||||
var result = await workflowRepository.CloseAsync(
|
||||
request.WisagId,
|
||||
request.Source,
|
||||
request.ProcessId,
|
||||
decision,
|
||||
cancellationToken);
|
||||
|
||||
if (result.Outcome == CloseWorkflowOutcome.NoAccess)
|
||||
{
|
||||
throw new ForbiddenApiException("Kein Zugriff");
|
||||
}
|
||||
|
||||
if (result.Outcome == CloseWorkflowOutcome.NotImplemented)
|
||||
{
|
||||
throw new InvalidOperationException("Closing integration is not finalized yet.");
|
||||
}
|
||||
|
||||
if (result.ProcessId <= 0)
|
||||
{
|
||||
throw new NotFoundApiException("Vorgang nicht gefunden");
|
||||
}
|
||||
|
||||
if (!result.IsClosed)
|
||||
{
|
||||
throw new ConflictApiException(MetaCode.AlreadyClosed, "Vorgang bereits abgeschlossen");
|
||||
}
|
||||
|
||||
return ApiEnvelopeFactory.Create(result);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using DigitalData.TaskFlow.Application.Common.Factories;
|
||||
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||
using DigitalData.TaskFlow.Domain.Common;
|
||||
using DigitalData.TaskFlow.Domain.Enums;
|
||||
using DigitalData.TaskFlow.Domain.Workflows;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Features.Workflows.Queries.GetWorkflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to fetch grouped workflow data for a WISAG user.
|
||||
/// </summary>
|
||||
/// <param name="WisagId">WISAG user identifier.</param>
|
||||
public sealed record GetWorkflowsQuery(int WisagId) : IRequest<ApiEnvelope<WorkflowSourcesResponse>>;
|
||||
|
||||
/// <summary>
|
||||
/// Validates <see cref="GetWorkflowsQuery"/> requests.
|
||||
/// </summary>
|
||||
public sealed class GetWorkflowsQueryValidator : AbstractValidator<GetWorkflowsQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes validation rules for <see cref="GetWorkflowsQuery"/>.
|
||||
/// </summary>
|
||||
public GetWorkflowsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.WisagId)
|
||||
.GreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="GetWorkflowsQuery"/> requests.
|
||||
/// </summary>
|
||||
public sealed class GetWorkflowsQueryHandler(IWorkflowRepository workflowRepository)
|
||||
: IRequestHandler<GetWorkflowsQuery, ApiEnvelope<WorkflowSourcesResponse>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the query and returns a workflow payload wrapped in the standard API envelope.
|
||||
/// </summary>
|
||||
/// <param name="request">Query request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Workflow response envelope.</returns>
|
||||
public async Task<ApiEnvelope<WorkflowSourcesResponse>> Handle(GetWorkflowsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var data = await workflowRepository.GetAsync(request.WisagId, cancellationToken);
|
||||
|
||||
if (data.Sources.Count == 0)
|
||||
{
|
||||
return ApiEnvelopeFactory.Create(data, MetaCode.NoAccess);
|
||||
}
|
||||
|
||||
var hasListingData = data.Sources.Any(source => source switch
|
||||
{
|
||||
FerdinandSourceBlock ferdinand => ferdinand.Listing.Count > 0,
|
||||
ContractsSourceBlock contracts => contracts.Listing.Count > 0,
|
||||
_ => false
|
||||
});
|
||||
|
||||
if (!hasListingData)
|
||||
{
|
||||
return ApiEnvelopeFactory.Create(data, MetaCode.NoOpenWorkflows);
|
||||
}
|
||||
|
||||
return ApiEnvelopeFactory.Create(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using DigitalData.TaskFlow.Application.Common.Exceptions;
|
||||
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DigitalData.TaskFlow.Application.Features.Workflows.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to fetch grouped workflow data for a WISAG user.
|
||||
/// </summary>
|
||||
/// <param name="WisagId">WISAG user identifier.</param>
|
||||
public sealed record GetWorkflowsQuery(int WisagId) : IRequest<JsonElement>;
|
||||
|
||||
public sealed class GetWorkflowsQueryHandler(IWorkflowRepository Repo) : IRequestHandler<GetWorkflowsQuery, JsonElement>
|
||||
{
|
||||
public async Task<JsonElement> Handle(GetWorkflowsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var workflow = await Repo.GetByWisagIdAsync(request.WisagId, cancellationToken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(workflow?.Result))
|
||||
{
|
||||
using var document = JsonDocument.Parse(workflow.Result);
|
||||
var root = document.RootElement;
|
||||
|
||||
if (root.ValueKind == JsonValueKind.Object &&
|
||||
root.TryGetProperty("response", out var response))
|
||||
{
|
||||
return response.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidWorkflowPayloadException(
|
||||
"Stored procedure returned an invalid payload. Expected a JSON object with a 'response' property.");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace DigitalData.TaskFlow.Application.Options;
|
||||
|
||||
public sealed class MediatROptions
|
||||
{
|
||||
public const string SectionName = "MediatR";
|
||||
|
||||
public string? LuckyPennySoftLicenseKey { get; init; }
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
# DigitalData.TaskFlow.Application
|
||||
|
||||
Dieses Projekt enthaelt die Use-Case-Schicht der Anwendung.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Application-Schicht orchestriert fachliche Anwendungsfaelle. Sie nimmt Requests entgegen, validiert diese, ruft Domain-nahe Abstraktionen auf und liefert strukturierte Ergebnisse zurueck.
|
||||
|
||||
## Aktueller Inhalt
|
||||
|
||||
- `DependencyInjection.cs`
|
||||
- Registriert zentrale Application-Dienste:
|
||||
- MediatR
|
||||
- FluentValidation
|
||||
- AutoMapper
|
||||
- Bindet `MediatR`-Optionen inklusive Lizenzschluessel.
|
||||
- `Options/`
|
||||
- `MediatROptions`: Konfiguration fuer MediatR (z. B. `LuckyPennySoftLicenseKey`).
|
||||
|
||||
## Geplante Inhalte (naechste Phasen)
|
||||
|
||||
- `Features/`
|
||||
- Queries/Commands fuer Workflows, Closing und Documents.
|
||||
- `Validators/`
|
||||
- FluentValidation-Regeln pro Request.
|
||||
- `Common/Behaviors/`
|
||||
- Pipeline-Behavior (z. B. zentrale Request-Validierung).
|
||||
- `Common/Interfaces/`
|
||||
- Repository-Abstraktionen fuer Datenzugriff.
|
||||
- `Mappings/`
|
||||
- AutoMapper-Profile zwischen Infrastrukturmodellen und Domainmodellen.
|
||||
|
||||
## Architektur-Regeln
|
||||
|
||||
- Keine direkten SQL- oder EF-Core-Aufrufe.
|
||||
- Keine HTTP-spezifischen Controller-Abhaengigkeiten.
|
||||
- Kommunikation nach aussen nur ueber Interfaces.
|
||||
|
||||
## Rolle im Gesamtsystem
|
||||
|
||||
Diese Schicht ist der zentrale Ausfuehrungspunkt fuer Use-Cases und verbindet die API-Schicht mit Domain und Infrastructure, ohne Technologie-Details nach oben durchzureichen.
|
||||
Reference in New Issue
Block a user