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