using AutoMapper; using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Exceptions; using DigitalData.EmailProfilerDispatcher.Abstraction.Entities; using DigitalData.UserManager.Domain.Entities; using EnvelopeGenerator.Application.Common.Configurations; using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Interfaces; using EnvelopeGenerator.Application.Common.Interfaces.Services; using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Histories.Commands; using EnvelopeGenerator.Domain; using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Interfaces; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace EnvelopeGenerator.Application.Envelopes.Commands; /// /// /// /// /// /// /// /// public class CreateEnvelopeCommandHandler(IServiceProvider Provider, IMapper Mapper, IEnvelopeNotificationService EnvNtfService, IRequestContext ReqCntxt, ILogger Logger) : IRequestHandler { private IEnvelopeExecutor Executor => Provider.GetRequiredService(); private IRepository Repository => Provider.GetRequiredService>(); private IRepository DocumentRepository => Provider.GetRequiredService>(); private IRepository EnvelopeReceiverRepository => Provider.GetRequiredService>(); private IRepository EnvelopeTypeRepository => Provider.GetRequiredService>(); private IRepository ReceiverRepository => Provider.GetRequiredService>(); private IRepository DocReceiverElementRepository => Provider.GetRequiredService>(); private IRepository EmailTemplateRepository => Provider.GetRequiredService>(); private IRepository EmailOutRepository => Provider.GetRequiredService>(); private MailParams MailParams => Provider.GetRequiredService>().Value; private DispatcherParams DispatcherParams => Provider.GetRequiredService>().Value; private SignatureFieldParams SignatureFieldParams => Provider.GetRequiredService>().CurrentValue; [Obsolete("Use IConfigService instead.")] private IConfigService ConfigService => Provider.GetRequiredService(); private IMediator Mediator => Provider.GetRequiredService(); /// /// /// /// /// /// 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; var settings = await ResolveEnvelopeSettingsAsync(request, existing: null, cancel); 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(); settings = await ResolveEnvelopeSettingsAsync(request, envelope, cancel); await Repository.UpdateAsync(e => { e.Title = title; e.Message = message; e.TfaEnabled = request.TFAEnabled; e.EnvelopeTypeId = settings.EnvelopeTypeId; e.Language = settings.Language; e.UseAccessCode = settings.UseAccessCode; e.CertificationType = settings.CertificationType; e.FinalEmailToCreator = settings.FinalEmailToCreator; e.FinalEmailToReceivers = settings.FinalEmailToReceivers; e.SendReminderEmails = settings.SendReminderEmails; e.FirstReminderDays = settings.FirstReminderDays; e.ReminderIntervalDays = settings.ReminderIntervalDays; e.ExpiresWhenDays = settings.ExpiresWhenDays; e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays; e.ContractType = settings.ContractType; e.ExpiresWhen = settings.ExpiresWhen; e.ExpiresWarningWhen = settings.ExpiresWarningWhen; e.Status = EnvelopeStatus.EnvelopeSaved; e.ChangedWhen = now; }, e => e.Id == envelopeId, cancel); envelope.Title = title; envelope.Message = message; envelope.TfaEnabled = request.TFAEnabled; envelope.EnvelopeTypeId = settings.EnvelopeTypeId; envelope.Language = settings.Language; envelope.UseAccessCode = settings.UseAccessCode; envelope.CertificationType = settings.CertificationType; envelope.FinalEmailToCreator = settings.FinalEmailToCreator; envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers; envelope.SendReminderEmails = settings.SendReminderEmails; envelope.FirstReminderDays = settings.FirstReminderDays; envelope.ReminderIntervalDays = settings.ReminderIntervalDays; envelope.ExpiresWhenDays = settings.ExpiresWhenDays; envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays; envelope.ContractType = settings.ContractType; envelope.ExpiresWhen = settings.ExpiresWhen; envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen; 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, EnvelopeTypeId = settings.EnvelopeTypeId, Language = settings.Language, TfaEnabled = request.TFAEnabled, UseAccessCode = settings.UseAccessCode, CertificationType = settings.CertificationType, FinalEmailToCreator = settings.FinalEmailToCreator, FinalEmailToReceivers = settings.FinalEmailToReceivers, SendReminderEmails = settings.SendReminderEmails, FirstReminderDays = settings.FirstReminderDays, ReminderIntervalDays = settings.ReminderIntervalDays, ExpiresWhenDays = settings.ExpiresWhenDays, ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays, ExpiresWhen = settings.ExpiresWhen, ExpiresWarningWhen = settings.ExpiresWarningWhen, ContractType = settings.ContractType }, cancel); if (request.UseSQLExecutor) { await Repository.UpdateAsync(e => { e.Title = title; e.Message = message; e.TfaEnabled = request.TFAEnabled; e.EnvelopeTypeId = settings.EnvelopeTypeId; e.Language = settings.Language; e.UseAccessCode = settings.UseAccessCode; e.CertificationType = settings.CertificationType; e.FinalEmailToCreator = settings.FinalEmailToCreator; e.FinalEmailToReceivers = settings.FinalEmailToReceivers; e.SendReminderEmails = settings.SendReminderEmails; e.FirstReminderDays = settings.FirstReminderDays; e.ReminderIntervalDays = settings.ReminderIntervalDays; e.ExpiresWhenDays = settings.ExpiresWhenDays; e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays; e.ExpiresWhen = settings.ExpiresWhen; e.ExpiresWarningWhen = settings.ExpiresWarningWhen; e.ContractType = settings.ContractType; e.Status = EnvelopeStatus.EnvelopeSaved; e.ChangedWhen = now; }, e => e.Id == envelope.Id, cancel); envelope.Title = title; envelope.Message = message; envelope.TfaEnabled = request.TFAEnabled; envelope.EnvelopeTypeId = settings.EnvelopeTypeId; envelope.Language = settings.Language; envelope.UseAccessCode = settings.UseAccessCode; envelope.CertificationType = settings.CertificationType; envelope.FinalEmailToCreator = settings.FinalEmailToCreator; envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers; envelope.SendReminderEmails = settings.SendReminderEmails; envelope.FirstReminderDays = settings.FirstReminderDays; envelope.ReminderIntervalDays = settings.ReminderIntervalDays; envelope.ExpiresWhenDays = settings.ExpiresWhenDays; envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays; envelope.ExpiresWhen = settings.ExpiresWhen; envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen; envelope.ContractType = settings.ContractType; envelope.Status = EnvelopeStatus.EnvelopeSaved; envelope.ChangedWhen = now; } 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 ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel) { EnvelopeType? selectedType = null; if (request.EnvelopeTypeId is int envelopeTypeId && envelopeTypeId > 0) { selectedType = await EnvelopeTypeRepository.Query .FirstOrDefaultAsync(t => t.Id == envelopeTypeId, cancel) ?? throw new BadRequestException($"Envelope type {envelopeTypeId} not found."); } var expiresWhenDays = Math.Max(0, request.ExpiresWhenDays ?? selectedType?.ExpiresWhenDays ?? existing?.ExpiresWhenDays ?? 0); var expiresWarningWhenDays = Math.Max(0, request.ExpiresWarningWhenDays ?? selectedType?.ExpiresWarningWhenDays ?? existing?.ExpiresWarningWhenDays ?? 0); var useAccessCode = request.UseAccessCode ?? selectedType?.UseAccessCode ?? existing?.UseAccessCode ?? false; if (request.TFAEnabled) useAccessCode = true; return new EnvelopeSettings( EnvelopeTypeId: request.EnvelopeTypeId ?? existing?.EnvelopeTypeId, Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language), UseAccessCode: useAccessCode, CertificationType: request.CertificationType ?? selectedType?.CertificationType ?? existing?.CertificationType ?? CertificationType.AdvancedElectronicSignature, FinalEmailToCreator: request.FinalEmailToCreator ?? (int?)selectedType?.FinalEmailToCreator ?? existing?.FinalEmailToCreator ?? (int)FinalEmailType.No, FinalEmailToReceivers: request.FinalEmailToReceivers ?? (int?)selectedType?.FinalEmailToReceivers ?? existing?.FinalEmailToReceivers ?? (int)FinalEmailType.No, SendReminderEmails: request.SendReminderEmails ?? selectedType?.SendReminderEmails ?? existing?.SendReminderEmails ?? false, FirstReminderDays: Math.Max(0, request.FirstReminderDays ?? selectedType?.FirstReminderDays ?? existing?.FirstReminderDays ?? 0), ReminderIntervalDays: Math.Max(0, request.ReminderIntervalDays ?? selectedType?.ReminderIntervalDays ?? existing?.ReminderIntervalDays ?? 0), ExpiresWhenDays: expiresWhenDays, ExpiresWarningWhenDays: expiresWarningWhenDays, ExpiresWhen: expiresWhenDays > 0 ? DateTime.Now.AddDays(expiresWhenDays) : null, ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null, ContractType: existing?.ContractType ?? selectedType?.ContractType ?? ContractType.Contract); } private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2) { if (!string.IsNullOrWhiteSpace(preferred)) return preferred.Trim(); if (!string.IsNullOrWhiteSpace(fallback1)) return fallback1.Trim(); if (!string.IsNullOrWhiteSpace(fallback2)) return fallback2.Trim(); return "de-DE"; } private sealed record EnvelopeSettings( int? EnvelopeTypeId, string Language, bool UseAccessCode, CertificationType CertificationType, int FinalEmailToCreator, int FinalEmailToReceivers, bool SendReminderEmails, int FirstReminderDays, int ReminderIntervalDays, int ExpiresWhenDays, int ExpiresWarningWhenDays, DateTime? ExpiresWhen, DateTime? ExpiresWarningWhen, ContractType ContractType); 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 = SignatureFieldParams.Width, Height = SignatureFieldParams.Height, 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 envRcvs = await EnvelopeReceiverRepository.Query .Include(er => er.Receiver) .Where(er => er.EnvelopeId == envelope.Id) .ToListAsync(cancel); if (envRcvs.Count == 0) throw new BadRequestException("Cannot send envelope without receivers."); var receiverIds = envRcvs.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 = envRcvs .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); await SendInvitationEmailsAsync(envelope, envRcvs, cancel); envelope.Status = EnvelopeStatus.EnvelopeQueued; } private async Task SendInvitationEmailsAsync( Envelope envelope, IReadOnlyCollection envRcvs, CancellationToken cancel) { foreach (var er in envRcvs) { var advancedSignature = envelope.CertificationType == CertificationType.AdvancedElectronicSignature; var origin = new Uri(ReqCntxt.Origin ?? throw new InvalidOperationException($"The request origin is missing in {nameof(CreateEnvelopeCommandHandler)}.")); try { await EnvNtfService.Notify(er.Receiver.EmailAddress, new DocumentReceivedNotification { AdvancedSignature = advancedSignature, DocumentTitle = envelope.Title ?? string.Empty, NameReceiver = er.Name ?? string.Empty, EnvelopeUuid = envelope.Uuid, IsReadAndConfirm = envelope.IsReadAndConfirm(), Message = envelope.Message ?? string.Empty, NameSender = $"{envelope.User.Prename} {envelope.User.Name}", Origin = origin, ReceiverSignature = er.Receiver.Signature, }, cancel); } catch(Exception ex) { Logger.LogError(ex, "Failed to send invitation email for receiver {emailAddress} in envelope {envelopeId}.", er.Receiver.EmailAddress, envelope.Id); } } } private static string ReplacePlaceHolders(string text, Dictionary placeHolders) { foreach (var placeHolder in placeHolders) text = text.Replace(placeHolder.Key, placeHolder.Value); return text; } }