Removed `IDocReceiverElementWriter` interface and its implementation, replacing it with repository-based operations (`IRepository<DocReceiverElement>`). Updated `CreateEnvelopeCommandHandler` to use repository methods for deleting and creating `DocReceiverElement` records. Cleaned up dependency injection by removing `IDocReceiverElementWriter` registration. Added new database triggers for `DocReceiverElement` in `appsettings.json` and `appsettings.Database.json`. Introduced `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` to outline the migration of the sender save/edit workflow to Blazor Server. This includes splitting save and send actions, supporting draft persistence, and enabling draft editing from the dashboard. Removed unused namespaces and performed code cleanup. Updated testing strategies to ensure correctness and prevent regressions.
355 lines
13 KiB
C#
355 lines
13 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.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 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 DocReceiverElementRepository.DeleteAsync(query => query
|
|
.Where(e => e.DocumentId == document.Id)
|
|
.Where(e => e.ReceiverId == link.ReceiverId), cancel);
|
|
|
|
foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0))
|
|
{
|
|
await DocReceiverElementRepository.CreateAsync(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;
|
|
}
|
|
}
|