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"); }); } }