From d0606f3605450334dde8b54d4dc7b147f03c9db4 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 21 Jul 2026 14:39:03 +0200 Subject: [PATCH] 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 --- .../AddStamp/AddStampCommand.cs | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 DocumentOperator.Application/AddStamp/AddStampCommand.cs diff --git a/DocumentOperator.Application/AddStamp/AddStampCommand.cs b/DocumentOperator.Application/AddStamp/AddStampCommand.cs new file mode 100644 index 0000000..9352298 --- /dev/null +++ b/DocumentOperator.Application/AddStamp/AddStampCommand.cs @@ -0,0 +1,125 @@ +using FluentValidation; +using MediatR; + +namespace DocumentOperator.Application.AddStamp; + +/// +/// Command to add a stamp (text, image, or predefined) to PDF pages. +/// Combines Command, Handler, and Validator in a single file (Vertical Slice pattern). +/// +public record AddStampCommand : IRequest +{ + 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; } +} + +/// +/// Handler for AddStampCommand. +/// +public class AddStampHandler(Common.Interfaces.IPdfProcessor pdfProcessor) : IRequestHandler +{ + public async Task 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 + ); + } +} + +/// +/// Validator for AddStampCommand. +/// +public class AddStampValidator : AbstractValidator +{ + 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"); + }); + } +}