Files
EnvelopeGenerator/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs
TekH 4dba965c75 feat(sender): route editor save and send through unified envelope upsert API
- expose POST /api/Envelope for sender-authenticated create/update/send operations

- replace client draft-specific call with EnvelopeService.UpsertAsync(CreateEnvelopeCommand)

- update sender editor save/send flow to persist draft first and send by envelope id

- allow editor hydration without document by making sender document fetch nullable
2026-09-26 13:11:44 +02:00

147 lines
6.3 KiB
C#

using System.Security.Claims;
using System.Text.Json;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Documents.Queries;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Receivers.Queries;
using EnvelopeGenerator.Server.Client.Models;
using EnvelopeGenerator.Server.Client.Models.Constants;
using EnvelopeGenerator.Server.Extensions;
using EnvelopeGenerator.Server.Options;
using MediatR;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto;
namespace EnvelopeGenerator.Server.Services;
/// <summary>
/// Loads receiver page data directly from MediatR and distributed cache.
/// </summary>
public class EnvelopeReceiverPageDataService(
IMediator mediator,
IDistributedCache cache,
IOptions<CacheOptions> cacheOptions,
IMemoryCache memoryCache)
{
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
/// <summary>
/// Loads the PDF document bytes for the authenticated receiver.
/// </summary>
public async Task<byte[]?> GetDocumentOfReceiverAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
{
var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken);
return document.ByteData;
}
/// <summary>
/// Loads the PDF document bytes for the authenticated receiver.
/// </summary>
public async Task<byte[]?> GetDocumentOfSenderAsync(ClaimsPrincipal user, int envelopeId, CancellationToken cancellationToken = default)
{
var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken);
return (document.ByteData is byte[] data && data.Length > 0)
? data
: throw new NotFoundException($"Document for envelope {envelopeId} not found.");
}
/// <summary>
/// Loads the full document DTO (including receiver elements) for the authenticated sender.
/// </summary>
public async Task<DocumentDto?> GetDocumentDetailsOfSenderAsync(ClaimsPrincipal user, int envelopeId, CancellationToken cancellationToken = default)
{
return await mediator.Send(new ReadDocumentQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken);
}
/// <summary>
/// Loads the current receiver's signature placeholders.
/// </summary>
public async Task<IReadOnlyList<SignatureDto>> GetSignaturesAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
{
var receiverId = user.ReceiverId();
var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken);
if (document.Elements is not IEnumerable<DocReceiverElementDto> elements)
return [];
var signatures = elements
.Where(element => element.ReceiverId == receiverId)
.Select(MapSignature)
.ToList();
return signatures.Convert(UnitOfLength.Point);
}
/// <summary>
/// Loads the envelope receiver data for the specified envelope key.
/// </summary>
public async Task<ApplicationEnvelopeReceiverDto?> GetEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancellationToken = default)
{
var result = await mediator.Send(new ReadEnvelopeReceiverQuery { Key = envelopeKey }, cancellationToken);
return result.SingleOrDefault();
}
/// <summary>
/// Loads the cached signature for the authenticated receiver.
/// </summary>
public async Task<SignatureCaptureDto?> GetCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
{
var json = await cache.GetStringAsync(GetSignatureCacheKey(user), cancellationToken);
return json is null ? null : JsonSerializer.Deserialize<SignatureCaptureDto>(json);
}
/// <summary>
/// Saves the cached signature for the authenticated receiver.
/// </summary>
public async Task SaveCachedSignatureAsync(ClaimsPrincipal user, SignatureCaptureDto signature, CancellationToken cancellationToken = default)
{
var json = JsonSerializer.Serialize(signature);
var options = cacheOptions.Value.SignatureCacheExpiration.HasValue
? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value }
: new DistributedCacheEntryOptions();
await cache.SetStringAsync(GetSignatureCacheKey(user), json, options, cancellationToken);
}
/// <summary>
/// Deletes the cached signature for the authenticated receiver.
/// </summary>
public Task DeleteCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
=> cache.RemoveAsync(GetSignatureCacheKey(user), cancellationToken);
private static string GetSignatureCacheKey(ClaimsPrincipal user)
=> $"{SignatureCacheKeyPrefix}{user.ReceiverSignature()}";
private static SignatureDto MapSignature(DocReceiverElementDto element) => new()
{
Id = element.Id,
X = element.X,
Y = element.Y,
Page = element.Page,
SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType
};
private static readonly string ReceiverEmailSearchCacheKey = Guid.NewGuid().ToString();
public async Task<IEnumerable<string>> SearchReceiverEMailsAsync(string emailSearchTerm, CancellationToken cancellationToken = default)
{
return await memoryCache.GetOrCreateAsync(ReceiverEmailSearchCacheKey + emailSearchTerm, async entry =>
{
var query = new ReadReceiverQuery { EmailAddressSearch = emailSearchTerm };
var receivers = await mediator.Send(query, cancellationToken);
if(receivers.Any())
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
else
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
return receivers.Select(r => r.EmailAddress);
}) ?? [];
}
}