diff --git a/EnvelopeGenerator.Application/Common/MappingProfile.cs b/EnvelopeGenerator.Application/Common/MappingProfile.cs index d87ce12a..c2b5c2d5 100644 --- a/EnvelopeGenerator.Application/Common/MappingProfile.cs +++ b/EnvelopeGenerator.Application/Common/MappingProfile.cs @@ -32,6 +32,7 @@ public class MappingProfile : Profile .ForMember(dest => dest.EnvelopeTypeTitle, opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null)); CreateMap(); + CreateMap(); CreateMap().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); CreateMap().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); CreateMap(); @@ -68,4 +69,4 @@ public class MappingProfile : Profile ? new SmsResponse() { Ok = true } : new SmsResponse() { Ok = false, Errors = gtxRes }); } -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.Application/Documents/Queries/ReadDocumentDetailsQuery.cs b/EnvelopeGenerator.Application/Documents/Queries/ReadDocumentDetailsQuery.cs new file mode 100644 index 00000000..cc00019c --- /dev/null +++ b/EnvelopeGenerator.Application/Documents/Queries/ReadDocumentDetailsQuery.cs @@ -0,0 +1,44 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using DigitalData.Core.Abstraction.Application.Repository; +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace EnvelopeGenerator.Application.Documents.Queries; + +/// +/// Represents a query to read document metadata/details without binary payload. +/// +/// The identifier of the envelope associated with the document. +/// The unique identifier of the sending user. Optional. +public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest; + +/// +/// Handles queries for reading document details without loading BYTE_DATA. +/// +public class ReadDocumentDetailsQueryHandler : IRequestHandler +{ + private readonly IRepository _repo; + private readonly IMapper _mapper; + + public ReadDocumentDetailsQueryHandler(IRepository repo, IMapper mapper) + { + _repo = repo; + _mapper = mapper; + } + + public async Task Handle(ReadDocumentDetailsQuery query, CancellationToken cancel) + { + var docQuery = _repo.Query.AsNoTracking(); + + if (query.UserId is int userId) + docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId); + + return await docQuery + .Where(d => d.EnvelopeId == query.EnvelopeId) + .ProjectTo(_mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancel); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs index d6460a04..e909ebe9 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs @@ -36,24 +36,41 @@ public class DocumentService(IHttpClientFactory httpClientFactory) /// /// Fetches sender document details (including signature elements) by envelope id. - /// Uses GET /api/Document?EnvelopeId={envelopeId}. + /// Uses GET /api/Document/details?EnvelopeId={envelopeId}. /// public async Task GetSenderDocumentDetailsByEnvelopeIdAsync(int envelopeId, CancellationToken cancel = default) { using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); - var response = await http.GetAsync($"/api/Document?EnvelopeId={envelopeId}", cancel); + var detailsResponse = await http.GetAsync($"/api/Document/details?EnvelopeId={envelopeId}", cancel); - if (!response.IsSuccessStatusCode) + if (!detailsResponse.IsSuccessStatusCode) { - var statusCode = (int)response.StatusCode; - var reasonPhrase = response.ReasonPhrase ?? "Unknown error"; + var statusCode = (int)detailsResponse.StatusCode; + var reasonPhrase = detailsResponse.ReasonPhrase ?? "Unknown error"; throw new HttpRequestException( $"Failed to load sender document details. Status: {statusCode} ({reasonPhrase})", null, - response.StatusCode); + detailsResponse.StatusCode); } - return await response.Content.ReadFromJsonAsync(_jsonOptions, cancel); + var details = await detailsResponse.Content.ReadFromJsonAsync(_jsonOptions, cancel) + ?? throw new HttpRequestException("Failed to deserialize sender document details response."); + + var bytesResponse = await http.GetAsync($"/api/Document?EnvelopeId={envelopeId}", cancel); + + if (!bytesResponse.IsSuccessStatusCode) + { + var statusCode = (int)bytesResponse.StatusCode; + var reasonPhrase = bytesResponse.ReasonPhrase ?? "Unknown error"; + throw new HttpRequestException( + $"Failed to load sender document bytes. Status: {statusCode} ({reasonPhrase})", + null, + bytesResponse.StatusCode); + } + + var byteData = await bytesResponse.Content.ReadAsByteArrayAsync(cancel); + + return new DocumentDto(byteData, details); } /// diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DocumentController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DocumentController.cs index 188228d9..1dff6913 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DocumentController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DocumentController.cs @@ -31,17 +31,30 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe /// Cancellation token. [HttpGet] [Authorize(AuthenticationSchemes = AuthScheme.Sender)] - public async Task GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null) + public async Task GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery query) { - if (query is null) - return BadRequest("Missing document query."); - var senderDoc = await mediator.Send(query, cancel); return senderDoc?.ByteData is byte[] senderDocByte ? File(senderDocByte, "application/octet-stream") : NotFound("Document is empty."); } + /// + /// Returns sender document metadata and element details (without binary payload) for editor loading. + /// + /// Document query with EnvelopeId. + /// Cancellation token. + [HttpGet("details")] + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + public async Task GetDocumentDetails(CancellationToken cancel, [FromQuery] ReadDocumentDetailsQuery query) + { + var senderDoc = await mediator.Send(query with { UserId = User.GetId() }, cancel); + if (senderDoc is null) + return NotFound("Document is empty."); + + return Ok(senderDoc); + } + /// /// Gets the document for the specified envelope key. /// diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs index c16f07dc..ef7d63f9 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs @@ -57,6 +57,14 @@ public class EnvelopeReceiverPageDataService( return await mediator.Send(new ReadDocumentQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken); } + /// + /// Loads document metadata/elements for the authenticated sender without binary payload. + /// + public async Task GetDocumentMetadataOfSenderAsync(ClaimsPrincipal user, int envelopeId, CancellationToken cancellationToken = default) + { + return await mediator.Send(new ReadDocumentDetailsQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken); + } + /// /// Loads the current receiver's signature placeholders. ///