From 9435561e78440b26e1b2254232cf8ae6427c80b7 Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 5 Oct 2026 15:55:19 +0200 Subject: [PATCH] Add recent receiver suggestions query and DTO Introduce `RecentReceiverSuggestionDto` to represent recent receiver suggestions with `FullName` and `EmailAddress`. Add `ReadRecentReceiverSuggestionsQuery` to retrieve recent receiver suggestions scoped by sender username and limited to 3-5 results. Implement `ReadRecentReceiverSuggestionsQueryHandler` to process the query using Entity Framework Core and AutoMapper. Update `MappingProfile` to map `EnvelopeReceiver` to `RecentReceiverSuggestionDto`. --- .../Receiver/RecentReceiverSuggestionDto.cs | 8 +++ .../EnvelopeReceivers/MappingProfile.cs | 4 ++ .../ReadRecentReceiverSuggestionsQuery.cs | 72 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 EnvelopeGenerator.Application/Common/Dto/Receiver/RecentReceiverSuggestionDto.cs create mode 100644 EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadRecentReceiverSuggestionsQuery.cs diff --git a/EnvelopeGenerator.Application/Common/Dto/Receiver/RecentReceiverSuggestionDto.cs b/EnvelopeGenerator.Application/Common/Dto/Receiver/RecentReceiverSuggestionDto.cs new file mode 100644 index 00000000..1928f56e --- /dev/null +++ b/EnvelopeGenerator.Application/Common/Dto/Receiver/RecentReceiverSuggestionDto.cs @@ -0,0 +1,8 @@ +namespace EnvelopeGenerator.Application.Common.Dto.Receiver; + +public class RecentReceiverSuggestionDto +{ + public required string FullName { get; set; } + + public required string EmailAddress { get; set; } +} diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/MappingProfile.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/MappingProfile.cs index 54b517b9..819938ea 100644 --- a/EnvelopeGenerator.Application/EnvelopeReceivers/MappingProfile.cs +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/MappingProfile.cs @@ -16,5 +16,9 @@ public class MappingProfile : Profile public MappingProfile() { CreateMap(); + + CreateMap() + .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.Receiver.EmailAddress)); } } diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadRecentReceiverSuggestionsQuery.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadRecentReceiverSuggestionsQuery.cs new file mode 100644 index 00000000..c9017cd6 --- /dev/null +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadRecentReceiverSuggestionsQuery.cs @@ -0,0 +1,72 @@ +using AutoMapper; +using DigitalData.Core.Abstraction.Application.Repository; +using EnvelopeGenerator.Application.Common.Dto.Receiver; +using EnvelopeGenerator.Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries; + +/// +/// Reads recent receiver suggestions for envelope creation. +/// +/// +/// Sender scope is assigned via chain method to avoid exposing username mutation from API query binding. +/// +public record ReadRecentReceiverSuggestionsQuery : IRequest> +{ + /// + /// Sender username used for scoping suggestion results. + /// + internal string? Username { get; private init; } + + /// + /// Desired number of results. Effective range is clamped to 3..5. + /// + public int Take { get; init; } = 5; + + /// + /// Returns a new query instance scoped to the specified sender username. + /// + /// Sender username from authenticated claims. + /// Scoped query instance. + public ReadRecentReceiverSuggestionsQuery ForSender(string username) + { + return this with { Username = username }; + } +} + +/// +/// Handles . +/// +public class ReadRecentReceiverSuggestionsQueryHandler(IRepository repository, IMapper mapper) + : IRequestHandler> +{ + /// + /// Retrieves distinct recent receiver suggestions grouped by receiver email. + /// + /// Suggestion query. + /// Cancellation token. + /// Ordered list of recent receiver suggestions. + public async Task> Handle(ReadRecentReceiverSuggestionsQuery request, CancellationToken cancellationToken) + { + var take = Math.Clamp(request.Take, 3, 5); + + var query = repository.Query + .AsNoTracking() + .Include(er => er.Receiver) + .Where(er => er.Receiver != null); + + if (request.Username is not null) + query = query.Where(er => er.Envelope != null && er.Envelope.User.Username == request.Username); + + var rows = await query + .GroupBy(er => er.Receiver!.EmailAddress.ToUpperInvariant()) + .Select(g => g.OrderByDescending(er => er.ChangedWhen ?? er.AddedWhen).First()) + .OrderByDescending(er => er.ChangedWhen ?? er.AddedWhen) + .Take(take) + .ToListAsync(cancellationToken); + + return mapper.Map>(rows); + } +}