feat(stamp): Add Application layer (AddStampCommand with handler/validator)
- Vertical slice: Command + Handler + Validator in single file - Validation rules enforce StampType-specific required fields - Text stamps: Text + FontName + FontSize required - Image stamps: ImageBytes required - Predefined stamps: PredefinedType required - Supports Origin, Rotation, Opacity, Placement, Size parameters
This commit is contained in:
125
DocumentOperator.Application/AddStamp/AddStampCommand.cs
Normal file
125
DocumentOperator.Application/AddStamp/AddStampCommand.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.AddStamp;
|
||||
|
||||
/// <summary>
|
||||
/// Command to add a stamp (text, image, or predefined) to PDF pages.
|
||||
/// Combines Command, Handler, and Validator in a single file (Vertical Slice pattern).
|
||||
/// </summary>
|
||||
public record AddStampCommand : IRequest<byte[]>
|
||||
{
|
||||
public required Stream PdfStream { get; init; }
|
||||
public required Domain.Models.ValueObjects.StampType StampType { get; init; }
|
||||
public int[]? PageNumbers { get; init; } // null = all pages
|
||||
public required (double X, double Y) Position { get; init; }
|
||||
public (double Width, double Height)? Size { get; init; }
|
||||
public Domain.Models.ValueObjects.AnnotationOrigin Origin { get; init; } = Domain.Models.ValueObjects.AnnotationOrigin.BottomLeft;
|
||||
public string? Text { get; init; }
|
||||
public string? FontName { get; init; }
|
||||
public double? FontSize { get; init; }
|
||||
public string? Color { get; init; }
|
||||
public double? Opacity { get; init; }
|
||||
public double? Rotation { get; init; }
|
||||
public Domain.Models.ValueObjects.StampPlacement Placement { get; init; } = Domain.Models.ValueObjects.StampPlacement.Foreground;
|
||||
public byte[]? ImageBytes { get; init; }
|
||||
public Domain.Models.ValueObjects.PredefinedStampType? PredefinedType { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for AddStampCommand.
|
||||
/// </summary>
|
||||
public class AddStampHandler(Common.Interfaces.IPdfProcessor pdfProcessor) : IRequestHandler<AddStampCommand, byte[]>
|
||||
{
|
||||
public async Task<byte[]> Handle(AddStampCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
return await pdfProcessor.AddStampAsync(
|
||||
command.PdfStream,
|
||||
command.StampType,
|
||||
command.PageNumbers,
|
||||
command.Position,
|
||||
command.Size,
|
||||
command.Origin,
|
||||
command.Text,
|
||||
command.FontName,
|
||||
command.FontSize,
|
||||
command.Color,
|
||||
command.Opacity,
|
||||
command.Rotation,
|
||||
command.Placement,
|
||||
command.ImageBytes,
|
||||
command.PredefinedType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validator for AddStampCommand.
|
||||
/// </summary>
|
||||
public class AddStampValidator : AbstractValidator<AddStampCommand>
|
||||
{
|
||||
public AddStampValidator()
|
||||
{
|
||||
RuleFor(x => x.PdfStream)
|
||||
.NotNull().WithMessage("PDF stream is required");
|
||||
|
||||
RuleFor(x => x.Position.X)
|
||||
.GreaterThanOrEqualTo(0).WithMessage("Position X must be >= 0");
|
||||
|
||||
RuleFor(x => x.Position.Y)
|
||||
.GreaterThanOrEqualTo(0).WithMessage("Position Y must be >= 0");
|
||||
|
||||
// Text stamp validation
|
||||
When(x => x.StampType == Domain.Models.ValueObjects.StampType.Text, () =>
|
||||
{
|
||||
RuleFor(x => x.Text)
|
||||
.NotEmpty().WithMessage("Text is required for Text stamp type");
|
||||
});
|
||||
|
||||
// Image stamp validation
|
||||
When(x => x.StampType == Domain.Models.ValueObjects.StampType.Image, () =>
|
||||
{
|
||||
RuleFor(x => x.ImageBytes)
|
||||
.NotNull().WithMessage("Image bytes are required for Image stamp type")
|
||||
.Must(bytes => bytes != null && bytes.Length > 0).WithMessage("Image bytes cannot be empty");
|
||||
});
|
||||
|
||||
// Predefined stamp validation
|
||||
When(x => x.StampType == Domain.Models.ValueObjects.StampType.Predefined, () =>
|
||||
{
|
||||
RuleFor(x => x.PredefinedType)
|
||||
.NotNull().WithMessage("Predefined type is required for Predefined stamp type");
|
||||
});
|
||||
|
||||
// Optional parameter validations
|
||||
When(x => x.Opacity.HasValue, () =>
|
||||
{
|
||||
RuleFor(x => x.Opacity!.Value)
|
||||
.InclusiveBetween(0.0, 1.0).WithMessage("Opacity must be between 0.0 and 1.0");
|
||||
});
|
||||
|
||||
When(x => x.Rotation.HasValue, () =>
|
||||
{
|
||||
RuleFor(x => x.Rotation!.Value)
|
||||
.InclusiveBetween(0.0, 360.0).WithMessage("Rotation must be between 0 and 360 degrees");
|
||||
});
|
||||
|
||||
When(x => x.FontSize.HasValue, () =>
|
||||
{
|
||||
RuleFor(x => x.FontSize!.Value)
|
||||
.GreaterThan(0).WithMessage("Font size must be positive");
|
||||
});
|
||||
|
||||
When(x => !string.IsNullOrWhiteSpace(x.Color), () =>
|
||||
{
|
||||
RuleFor(x => x.Color!)
|
||||
.Matches(@"^[0-9A-Fa-f]{6}$").WithMessage("Color must be 6-digit hex (e.g., 'FF0000')");
|
||||
});
|
||||
|
||||
When(x => x.PageNumbers != null, () =>
|
||||
{
|
||||
RuleFor(x => x.PageNumbers!)
|
||||
.Must(pages => pages.All(p => p > 0)).WithMessage("All page numbers must be >= 1");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user