feat(application): Add AddAnnotationCommand with handler and validator

- Create AddAnnotationCommand (Command/Handler/Validator merged in single file)
- Use primary constructors for handler (IPdfProcessor dependency)
- FluentValidation rules: stream required, pageNumber > 0, content for FreeText/StickyNote
- Validate textMarkupStyle required for TextMarkup annotations
- Validate color format (6-digit hex) and rectangle coordinates (X2>X1, Y2>Y1)
This commit is contained in:
2026-07-21 12:28:30 +02:00
parent a23c78ec3a
commit 22ac2889af

View File

@@ -0,0 +1,76 @@
using DocumentOperator.Application.Common.Interfaces;
using DocumentOperator.Domain.Models.ValueObjects;
using FluentValidation;
using MediatR;
namespace DocumentOperator.Application.AddAnnotation;
/// <summary>
/// Command to add an annotation to a PDF document.
/// </summary>
public record AddAnnotationCommand : IRequest<byte[]>
{
public required Stream PdfStream { get; init; }
public required AnnotationType AnnotationType { get; init; }
public required int PageNumber { get; init; }
public required (double X1, double Y1, double X2, double Y2) Rectangle { get; init; }
public string? Content { get; init; }
public string? Author { get; init; }
public string? Color { get; init; }
public TextMarkupStyle? TextMarkupStyle { get; init; }
}
/// <summary>
/// Handler for AddAnnotationCommand.
/// </summary>
public class AddAnnotationHandler(IPdfProcessor pdfProcessor) : IRequestHandler<AddAnnotationCommand, byte[]>
{
public async Task<byte[]> Handle(AddAnnotationCommand request, CancellationToken cancellationToken)
{
return await pdfProcessor.AddAnnotationAsync(
request.PdfStream,
request.AnnotationType,
request.PageNumber,
request.Rectangle,
request.Content,
request.Author,
request.Color,
request.TextMarkupStyle);
}
}
/// <summary>
/// Validator for AddAnnotationCommand.
/// </summary>
public class AddAnnotationValidator : AbstractValidator<AddAnnotationCommand>
{
public AddAnnotationValidator()
{
RuleFor(x => x.PdfStream)
.NotNull()
.WithMessage("PDF stream is required");
RuleFor(x => x.PageNumber)
.GreaterThan(0)
.WithMessage("Page number must be greater than 0");
RuleFor(x => x.Content)
.NotEmpty()
.When(x => x.AnnotationType == AnnotationType.FreeText || x.AnnotationType == AnnotationType.StickyNote)
.WithMessage("Content is required for FreeText and StickyNote annotations");
RuleFor(x => x.TextMarkupStyle)
.NotNull()
.When(x => x.AnnotationType == AnnotationType.TextMarkup)
.WithMessage("TextMarkupStyle is required for TextMarkup annotations");
RuleFor(x => x.Color)
.Matches("^[0-9A-Fa-f]{6}$")
.When(x => !string.IsNullOrWhiteSpace(x.Color))
.WithMessage("Color must be a 6-digit hex value (e.g., 'FF0000' for red)");
RuleFor(x => x.Rectangle)
.Must(r => r.X2 > r.X1 && r.Y2 > r.Y1)
.WithMessage("Rectangle coordinates must define a valid area (X2 > X1 and Y2 > Y1)");
}
}