feat(application): enforce validation and API error mapping rules

This commit is contained in:
2026-10-10 16:12:05 +02:00
parent f8e877ac06
commit be41adc0f3
6 changed files with 75 additions and 2 deletions

View File

@@ -0,0 +1,18 @@
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)
{
}
}

View File

@@ -0,0 +1,18 @@
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)
{
}
}

View File

@@ -12,6 +12,6 @@ public interface IDocumentRepository : IRepository
/// </summary>
/// <param name="id">Document identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Binary document payload.</returns>
Task<DocumentBinaryResult> GetAsync(string id, CancellationToken cancellationToken);
/// <returns>Binary document payload when found; otherwise <see langword="null"/>.</returns>
Task<DocumentBinaryResult?> GetAsync(string id, CancellationToken cancellationToken);
}

View File

@@ -1,4 +1,5 @@
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;
@@ -45,6 +46,11 @@ public sealed class GetDocumentQueryHandler(IDocumentRepository documentReposito
{
var document = await documentRepository.GetAsync(request.Id, cancellationToken);
if (document is null)
{
throw new NotFoundApiException("Dokument nicht gefunden");
}
return ApiEnvelopeFactory.Create(document);
}
}

View File

@@ -34,6 +34,8 @@ public sealed record CloseWorkflowCommand(
public sealed class CloseWorkflowCommandValidator : AbstractValidator<CloseWorkflowCommand>
{
private static readonly string[] AllowedSources = ["Ferdinand", "Contracts"];
private const int ApproveMethodId = 2;
private const int RejectMethodId = 3;
/// <summary>
/// Initializes validation rules for <see cref="CloseWorkflowCommand"/>.
@@ -53,6 +55,20 @@ public sealed class CloseWorkflowCommandValidator : AbstractValidator<CloseWorkf
.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)
.WithMessage("Unbekannte closing_method_id");
RuleFor(x => x.Comment)
.NotEmpty()
.When(x => x.ClosingMethodId == RejectMethodId)
.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");
}
}
@@ -79,6 +95,16 @@ public sealed class CloseWorkflowCommandHandler(IWorkflowRepository workflowRepo
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");

View File

@@ -45,6 +45,11 @@ public sealed class GetWorkflowsQueryHandler(IWorkflowRepository workflowReposit
{
var data = await workflowRepository.GetAsync(request.WisagId, cancellationToken);
if (data.Sources.Count == 0)
{
return ApiEnvelopeFactory.Create(data, MetaCode.NoAccess);
}
var hasListingData = data.Sources
.SelectMany(source => source switch
{