- extend CreateEnvelopeCommand to support draft updates and send intent - merge draft save logic into CreateEnvelopeCommandHandler and remove duplicate handler - add DocReceiverElement writer abstraction and infrastructure registration for SQL-based field upserts
356 lines
14 KiB
C#
356 lines
14 KiB
C#
using AutoMapper;
|
|
using MediatR;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using DigitalData.Core.Abstraction.Application.Repository;
|
|
using DigitalData.Core.Exceptions;
|
|
using EnvelopeGenerator.Domain.Entities;
|
|
using EnvelopeGenerator.Application.Common.Dto;
|
|
using EnvelopeGenerator.Application.Common.Interfaces;
|
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
|
using EnvelopeGenerator.Application.Histories.Commands;
|
|
using EnvelopeGenerator.Domain.Constants;
|
|
using EnvelopeGenerator.Domain;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeCommand, EnvelopeDto?>
|
|
{
|
|
private readonly IServiceProvider _provider;
|
|
|
|
private IEnvelopeExecutor Executor => _provider.GetRequiredService<IEnvelopeExecutor>();
|
|
|
|
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
|
|
|
private IRepository<Document> DocumentRepository => _provider.GetRequiredService<IRepository<Document>>();
|
|
|
|
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => _provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
|
|
|
private IRepository<Receiver> ReceiverRepository => _provider.GetRequiredService<IRepository<Receiver>>();
|
|
|
|
private IRepository<DocReceiverElement> DocReceiverElementRepository => _provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
|
|
|
private IDocReceiverElementWriter DocReceiverElementWriter => _provider.GetRequiredService<IDocReceiverElementWriter>();
|
|
|
|
private IMediator Mediator => _provider.GetRequiredService<IMediator>();
|
|
|
|
private readonly IMapper _mapper;
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="provider"></param>
|
|
/// <param name="mapper"></param>
|
|
public CreateEnvelopeCommandHandler(IServiceProvider provider, IMapper mapper)
|
|
{
|
|
_provider = provider;
|
|
_mapper = mapper;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="request"></param>
|
|
/// <param name="cancel"></param>
|
|
/// <returns></returns>
|
|
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
|
{
|
|
var now = DateTime.Now;
|
|
var title = request.Title?.Trim() ?? string.Empty;
|
|
var message = request.Message?.Trim() ?? string.Empty;
|
|
|
|
Envelope envelope;
|
|
if (request.EnvelopeId is int envelopeId)
|
|
{
|
|
envelope = await Repository.Query
|
|
.Include(e => e.User)
|
|
.FirstOrDefaultAsync(e => e.Id == envelopeId, cancel)
|
|
?? throw new BadRequestException($"Envelope {envelopeId} not found.");
|
|
|
|
if (envelope.UserId != request.UserId)
|
|
throw new ForbiddenException();
|
|
|
|
await Repository.UpdateAsync(e =>
|
|
{
|
|
e.Title = title;
|
|
e.Message = message;
|
|
e.TfaEnabled = request.TFAEnabled;
|
|
e.Status = EnvelopeStatus.EnvelopeSaved;
|
|
e.ChangedWhen = now;
|
|
}, e => e.Id == envelopeId, cancel);
|
|
|
|
envelope.Title = title;
|
|
envelope.Message = message;
|
|
envelope.TfaEnabled = request.TFAEnabled;
|
|
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
|
envelope.ChangedWhen = now;
|
|
}
|
|
else
|
|
{
|
|
envelope = request.UseSQLExecutor
|
|
? await Executor.CreateEnvelopeAsync(request.UserId, title, message, request.TFAEnabled, cancel)
|
|
: await Repository.CreateAsync(new Envelope
|
|
{
|
|
UserId = request.UserId,
|
|
Status = EnvelopeStatus.EnvelopeCreated,
|
|
Uuid = Guid.NewGuid().ToString(),
|
|
Title = title,
|
|
Message = message,
|
|
AddedWhen = now,
|
|
ChangedWhen = now,
|
|
Comment = string.Empty,
|
|
Language = "de-DE",
|
|
TfaEnabled = request.TFAEnabled,
|
|
UseAccessCode = false,
|
|
SendReminderEmails = false
|
|
}, cancel);
|
|
}
|
|
|
|
await UpsertDocumentAsync(envelope.Id, request.Document, now, cancel);
|
|
await UpsertReceiversAndFieldsAsync(envelope.Id, request.Receivers, now, cancel);
|
|
|
|
if (request.Send)
|
|
await SendEnvelopeAsync(envelope, cancel);
|
|
|
|
envelope = await Repository.Query
|
|
.Include(e => e.User)
|
|
.Include(e => e.EnvelopeReceivers)
|
|
.ThenInclude(er => er.Receiver)
|
|
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
|
|
|
return _mapper.Map<EnvelopeDto>(envelope);
|
|
}
|
|
|
|
private async Task UpsertDocumentAsync(int envelopeId, CreateEnvelopeDocumentDto? document, DateTime now, CancellationToken cancel)
|
|
{
|
|
if (document is null || string.IsNullOrWhiteSpace(document.DataAsBase64))
|
|
return;
|
|
|
|
byte[] bytes;
|
|
try
|
|
{
|
|
bytes = Convert.FromBase64String(document.DataAsBase64);
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
throw new BadRequestException("Document payload is not valid base64.");
|
|
}
|
|
|
|
var existingDocumentId = await DocumentRepository.Query
|
|
.Where(d => d.EnvelopeId == envelopeId)
|
|
.Select(d => (int?)d.Id)
|
|
.FirstOrDefaultAsync(cancel);
|
|
|
|
if (existingDocumentId is null)
|
|
{
|
|
await DocumentRepository.CreateAsync(new Document
|
|
{
|
|
EnvelopeId = envelopeId,
|
|
AddedWhen = now,
|
|
ByteData = bytes,
|
|
Filename = string.Empty,
|
|
Filepath = string.Empty,
|
|
FileNameOriginal = string.Empty
|
|
}, cancel);
|
|
|
|
return;
|
|
}
|
|
|
|
await DocumentRepository.UpdateAsync(d =>
|
|
{
|
|
d.ByteData = bytes;
|
|
}, d => d.Id == existingDocumentId.Value, cancel);
|
|
}
|
|
|
|
private async Task UpsertReceiversAndFieldsAsync(int envelopeId, IReadOnlyCollection<CreateEnvelopeReceiverDto> receivers, DateTime now, CancellationToken cancel)
|
|
{
|
|
var filteredReceivers = receivers
|
|
.Where(r => !string.IsNullOrWhiteSpace(r.EmailAddress))
|
|
.Select(r => r with
|
|
{
|
|
EmailAddress = r.EmailAddress.Trim().ToLowerInvariant(),
|
|
Name = r.Name?.Trim(),
|
|
PhoneNumber = string.IsNullOrWhiteSpace(r.PhoneNumber) ? null : r.PhoneNumber.Trim(),
|
|
Fields = r.Fields ?? new List<CreateEnvelopeFieldDto>()
|
|
})
|
|
.GroupBy(r => r.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
|
.Select(g => g.First())
|
|
.ToList();
|
|
|
|
if (filteredReceivers.Count == 0)
|
|
return;
|
|
|
|
var document = await DocumentRepository.Query
|
|
.FirstOrDefaultAsync(d => d.EnvelopeId == envelopeId, cancel);
|
|
|
|
if (document is null)
|
|
return;
|
|
|
|
var currentLinks = await EnvelopeReceiverRepository.Query
|
|
.Include(er => er.Receiver)
|
|
.Where(er => er.EnvelopeId == envelopeId)
|
|
.ToListAsync(cancel);
|
|
|
|
var linksByEmail = currentLinks
|
|
.Where(er => !string.IsNullOrWhiteSpace(er.Receiver?.EmailAddress))
|
|
.GroupBy(er => er.Receiver!.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(g => g.Key.ToLowerInvariant(), g => g.First(), StringComparer.OrdinalIgnoreCase);
|
|
|
|
var sequence = currentLinks.Count > 0 ? currentLinks.Max(er => er.Sequence) : 0;
|
|
|
|
foreach (var receiverDraft in filteredReceivers)
|
|
{
|
|
var linkResult = await GetOrCreateEnvelopeReceiverAsync(envelopeId, receiverDraft, linksByEmail, now, sequence, cancel);
|
|
var link = linkResult.Link;
|
|
sequence = linkResult.NextSequence;
|
|
|
|
await DocReceiverElementWriter.DeleteByDocumentAndReceiverAsync(document.Id, link.ReceiverId, cancel);
|
|
|
|
foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0))
|
|
{
|
|
await DocReceiverElementWriter.InsertAsync(new DocReceiverElement
|
|
{
|
|
DocumentId = document.Id,
|
|
ReceiverId = link.ReceiverId,
|
|
ElementType = 1,
|
|
X = field.X,
|
|
Y = field.Y,
|
|
Width = 1.77,
|
|
Height = 1.96,
|
|
Page = field.Page,
|
|
Required = true,
|
|
ReadOnly = false,
|
|
AnnotationIndex = 0,
|
|
AddedWhen = now,
|
|
FullName = link.Name,
|
|
Position = string.Empty,
|
|
Place = string.Empty,
|
|
Tooltip = string.Empty
|
|
}, cancel);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task<(EnvelopeReceiver Link, int NextSequence)> GetOrCreateEnvelopeReceiverAsync(
|
|
int envelopeId,
|
|
CreateEnvelopeReceiverDto receiverDraft,
|
|
IDictionary<string, EnvelopeReceiver> linksByEmail,
|
|
DateTime now,
|
|
int sequence,
|
|
CancellationToken cancel)
|
|
{
|
|
if (linksByEmail.TryGetValue(receiverDraft.EmailAddress, out var existingLink))
|
|
{
|
|
var nextName = string.IsNullOrWhiteSpace(receiverDraft.Name) ? existingLink.Name : receiverDraft.Name!;
|
|
var nextPhoneNumber = receiverDraft.PhoneNumber ?? existingLink.PhoneNumber;
|
|
|
|
await EnvelopeReceiverRepository.UpdateAsync(er =>
|
|
{
|
|
er.Name = nextName;
|
|
er.PhoneNumber = nextPhoneNumber;
|
|
er.ChangedWhen = now;
|
|
}, er => er.EnvelopeId == existingLink.EnvelopeId && er.ReceiverId == existingLink.ReceiverId, cancel);
|
|
|
|
existingLink.Name = nextName;
|
|
existingLink.PhoneNumber = nextPhoneNumber;
|
|
return (existingLink, sequence);
|
|
}
|
|
|
|
var receiver = await ReceiverRepository.Query
|
|
.FirstOrDefaultAsync(r => r.EmailAddress != null && r.EmailAddress.ToLower() == receiverDraft.EmailAddress, cancel);
|
|
|
|
if (receiver is null)
|
|
{
|
|
receiver = await ReceiverRepository.CreateAsync(new Receiver
|
|
{
|
|
EmailAddress = receiverDraft.EmailAddress,
|
|
Signature = receiverDraft.EmailAddress.ToUpperInvariant().GetChecksum(),
|
|
AddedWhen = now,
|
|
TotpSecretkey = string.Empty,
|
|
TfaRegDeadline = null
|
|
}, cancel);
|
|
}
|
|
|
|
sequence++;
|
|
var link = await EnvelopeReceiverRepository.CreateAsync(new EnvelopeReceiver
|
|
{
|
|
EnvelopeId = envelopeId,
|
|
ReceiverId = receiver.Id,
|
|
Sequence = sequence,
|
|
Name = receiverDraft.Name ?? receiverDraft.EmailAddress,
|
|
JobTitle = string.Empty,
|
|
CompanyName = string.Empty,
|
|
PrivateMessage = string.Empty,
|
|
AccessCode = string.Empty,
|
|
AddedWhen = now,
|
|
ChangedWhen = now,
|
|
PhoneNumber = receiverDraft.PhoneNumber ?? string.Empty
|
|
}, cancel);
|
|
|
|
link.Receiver = receiver;
|
|
linksByEmail[receiverDraft.EmailAddress] = link;
|
|
return (link, sequence);
|
|
}
|
|
|
|
private async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancel)
|
|
{
|
|
var hasDocument = await DocumentRepository.Query
|
|
.AnyAsync(d => d.EnvelopeId == envelope.Id && d.ByteData != null && d.ByteData.Length > 0, cancel);
|
|
|
|
if (!hasDocument)
|
|
throw new BadRequestException("Cannot send envelope without a document.");
|
|
|
|
var links = await EnvelopeReceiverRepository.Query
|
|
.Include(er => er.Receiver)
|
|
.Where(er => er.EnvelopeId == envelope.Id)
|
|
.ToListAsync(cancel);
|
|
|
|
if (links.Count == 0)
|
|
throw new BadRequestException("Cannot send envelope without receivers.");
|
|
|
|
var receiverIds = links.Select(l => l.ReceiverId).Distinct().ToList();
|
|
var documentIds = await DocumentRepository.Query
|
|
.Where(d => d.EnvelopeId == envelope.Id)
|
|
.Select(d => d.Id)
|
|
.ToListAsync(cancel);
|
|
|
|
var receiverFieldCounts = await DocReceiverElementRepository.Query
|
|
.Where(e => documentIds.Contains(e.DocumentId))
|
|
.Where(e => receiverIds.Contains(e.ReceiverId))
|
|
.GroupBy(e => e.ReceiverId)
|
|
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
|
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
|
|
|
var receiversWithoutFields = links
|
|
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
|
.Select(link => link.Name)
|
|
.ToList();
|
|
|
|
if (receiversWithoutFields.Count > 0)
|
|
throw new BadRequestException("Cannot send envelope because some receivers have no signature fields.");
|
|
|
|
var changedWhen = DateTime.Now;
|
|
await Repository.UpdateAsync(e =>
|
|
{
|
|
e.Status = EnvelopeStatus.EnvelopeQueued;
|
|
e.ChangedWhen = changedWhen;
|
|
}, e => e.Id == envelope.Id, cancel);
|
|
|
|
var userReference = envelope.User?.Email;
|
|
if (string.IsNullOrWhiteSpace(userReference))
|
|
throw new BadRequestException("Envelope sender email is missing.");
|
|
|
|
await Mediator.Send(new CreateHistoryCommand
|
|
{
|
|
EnvelopeId = envelope.Id,
|
|
UserReference = userReference,
|
|
Status = EnvelopeStatus.EnvelopeQueued
|
|
}, cancel);
|
|
|
|
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
|
}
|
|
}
|