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; /// /// /// public class CreateEnvelopeCommandHandler : IRequestHandler { private readonly IServiceProvider _provider; private IEnvelopeExecutor Executor => _provider.GetRequiredService(); private IRepository Repository => _provider.GetRequiredService>(); private IRepository DocumentRepository => _provider.GetRequiredService>(); private IRepository EnvelopeReceiverRepository => _provider.GetRequiredService>(); private IRepository ReceiverRepository => _provider.GetRequiredService>(); private IRepository DocReceiverElementRepository => _provider.GetRequiredService>(); private IMediator Mediator => _provider.GetRequiredService(); private readonly IMapper _mapper; /// /// /// /// /// public CreateEnvelopeCommandHandler(IServiceProvider provider, IMapper mapper) { _provider = provider; _mapper = mapper; } /// /// /// /// /// /// public async Task 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 Mediator.Send(new CreateHistoryCommand { EnvelopeId = envelope.Id, UserReference = await ResolveSenderReferenceAsync(envelope, cancel), Status = EnvelopeStatus.EnvelopeCreated }, 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(envelope); } private async Task ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel) { if (!string.IsNullOrWhiteSpace(envelope.User?.Email)) return envelope.User.Email; var senderEmail = await Repository.Query .Where(e => e.Id == envelope.Id) .Select(e => e.User.Email) .FirstOrDefaultAsync(cancel); if (string.IsNullOrWhiteSpace(senderEmail)) throw new BadRequestException("Envelope sender email is missing."); return senderEmail; } 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 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() }) .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 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 = await ResolveSenderReferenceAsync(envelope, cancel); await Mediator.Send(new CreateHistoryCommand { EnvelopeId = envelope.Id, UserReference = userReference, Status = EnvelopeStatus.EnvelopeQueued }, cancel); envelope.Status = EnvelopeStatus.EnvelopeQueued; } }