using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Exceptions; using EnvelopeGenerator.Domain; using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; namespace EnvelopeGenerator.Application.Envelopes.Commands; public class SaveEnvelopeDraftCommandHandler : IRequestHandler { private readonly IRepository _envelopeRepository; private readonly IRepository _documentRepository; private readonly IRepository _envelopeReceiverRepository; private readonly IRepository _receiverRepository; private readonly IRepository _docReceiverElementRepository; public SaveEnvelopeDraftCommandHandler( IRepository envelopeRepository, IRepository documentRepository, IRepository envelopeReceiverRepository, IRepository receiverRepository, IRepository docReceiverElementRepository) { _envelopeRepository = envelopeRepository; _documentRepository = documentRepository; _envelopeReceiverRepository = envelopeReceiverRepository; _receiverRepository = receiverRepository; _docReceiverElementRepository = docReceiverElementRepository; } public async Task Handle(SaveEnvelopeDraftCommand request, CancellationToken cancel) { var now = DateTime.Now; var title = request.Title?.Trim() ?? string.Empty; var message = request.Message?.Trim() ?? string.Empty; int targetEnvelopeId; if (request.EnvelopeId is int envelopeId) { var envelopeInfo = await _envelopeRepository.Query .Where(e => e.Id == envelopeId) .Select(e => new { e.Id, e.UserId }) .FirstOrDefaultAsync(cancel) ?? throw new NotFoundException($"Envelope {envelopeId} not found."); if (envelopeInfo.UserId != request.UserId) throw new ForbiddenException(); await _envelopeRepository.UpdateAsync(e => { e.Title = title; e.Message = message; e.TfaEnabled = request.TFAEnabled; e.Status = EnvelopeStatus.EnvelopeSaved; e.ChangedWhen = now; }, e => e.Id == envelopeId, cancel); targetEnvelopeId = envelopeInfo.Id; } else { var envelope = new Envelope { UserId = request.UserId, Status = EnvelopeStatus.EnvelopeSaved, 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 }; envelope = await _envelopeRepository.CreateAsync(envelope, cancel); targetEnvelopeId = envelope.Id; } await UpsertDocumentAsync(targetEnvelopeId, request.Document, now, cancel); await UpsertReceiversAndFieldsAsync(targetEnvelopeId, request.Receivers, now, cancel); return new SaveEnvelopeDraftResultDto(targetEnvelopeId, EnvelopeStatus.EnvelopeSaved); } private async Task UpsertDocumentAsync(int envelopeId, SaveEnvelopeDraftDocumentDto? 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 linksByReceiverId = currentLinks.ToDictionary(er => er.ReceiverId, er => er); 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, linksByReceiverId, 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, SaveEnvelopeDraftReceiverDto receiverDraft, IDictionary linksByEmail, IDictionary linksByReceiverId, 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; existingLink.Name = nextName; existingLink.PhoneNumber = nextPhoneNumber; existingLink.ChangedWhen = now; await _envelopeReceiverRepository.UpdateAsync(er => { er.Name = nextName; er.PhoneNumber = nextPhoneNumber; er.ChangedWhen = now; }, er => er.EnvelopeId == existingLink.EnvelopeId && er.ReceiverId == existingLink.ReceiverId, cancel); 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 = 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 }; link = await _envelopeReceiverRepository.CreateAsync(link, cancel); link.Receiver = receiver; linksByReceiverId[link.ReceiverId] = link; linksByEmail[receiverDraft.EmailAddress] = link; return (link, sequence); } }