refactor(envelope): consolidate draft flow into upsert command
- 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
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces;
|
||||
|
||||
public interface IDocReceiverElementWriter
|
||||
{
|
||||
Task DeleteByDocumentAndReceiverAsync(int documentId, int receiverId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task InsertAsync(DocReceiverElement element, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using MediatR;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@@ -13,23 +12,29 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
/// </summary>
|
||||
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||
{
|
||||
public int? EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Title { get; set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Message { get; set; }
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
||||
/// </summary>
|
||||
public bool TFAEnabled { get; set; } = false;
|
||||
|
||||
public bool Send { get; init; }
|
||||
|
||||
public CreateEnvelopeDocumentDto? Document { get; init; }
|
||||
|
||||
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// ID des Absenders
|
||||
/// </summary>
|
||||
@@ -55,4 +60,14 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
public bool UseSQLExecutor { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
public record CreateEnvelopeDocumentDto(string DataAsBase64);
|
||||
|
||||
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
|
||||
|
||||
public record CreateEnvelopeReceiverDto(
|
||||
string EmailAddress,
|
||||
string? Name,
|
||||
string? PhoneNumber,
|
||||
List<CreateEnvelopeFieldDto> Fields);
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
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;
|
||||
|
||||
@@ -19,6 +25,18 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
||||
|
||||
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>
|
||||
@@ -40,10 +58,298 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
||||
/// <returns></returns>
|
||||
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
||||
{
|
||||
var envelope = request.UseSQLExecutor
|
||||
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel)
|
||||
: await Repository.CreateAsync(request, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
|
||||
public record SaveEnvelopeDraftResultDto(int EnvelopeId, EnvelopeStatus Status);
|
||||
|
||||
public record SaveEnvelopeDraftCommand : IRequest<SaveEnvelopeDraftResultDto>
|
||||
{
|
||||
public int? EnvelopeId { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
public string? Message { get; init; }
|
||||
|
||||
public bool TFAEnabled { get; init; }
|
||||
|
||||
public SaveEnvelopeDraftDocumentDto? Document { get; init; }
|
||||
|
||||
public List<SaveEnvelopeDraftReceiverDto> Receivers { get; init; } = new();
|
||||
|
||||
internal int UserId { get; private set; }
|
||||
|
||||
public SaveEnvelopeDraftCommand WithAuth(int userId)
|
||||
{
|
||||
UserId = userId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public record SaveEnvelopeDraftDocumentDto(string DataAsBase64);
|
||||
|
||||
public record SaveEnvelopeDraftFieldDto(double X, double Y, int Page);
|
||||
|
||||
public record SaveEnvelopeDraftReceiverDto(
|
||||
string EmailAddress,
|
||||
string? Name,
|
||||
string? PhoneNumber,
|
||||
List<SaveEnvelopeDraftFieldDto> Fields);
|
||||
@@ -1,268 +0,0 @@
|
||||
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<SaveEnvelopeDraftCommand, SaveEnvelopeDraftResultDto>
|
||||
{
|
||||
private readonly IRepository<Envelope> _envelopeRepository;
|
||||
private readonly IRepository<Document> _documentRepository;
|
||||
private readonly IRepository<EnvelopeReceiver> _envelopeReceiverRepository;
|
||||
private readonly IRepository<Receiver> _receiverRepository;
|
||||
private readonly IRepository<DocReceiverElement> _docReceiverElementRepository;
|
||||
|
||||
public SaveEnvelopeDraftCommandHandler(
|
||||
IRepository<Envelope> envelopeRepository,
|
||||
IRepository<Document> documentRepository,
|
||||
IRepository<EnvelopeReceiver> envelopeReceiverRepository,
|
||||
IRepository<Receiver> receiverRepository,
|
||||
IRepository<DocReceiverElement> docReceiverElementRepository)
|
||||
{
|
||||
_envelopeRepository = envelopeRepository;
|
||||
_documentRepository = documentRepository;
|
||||
_envelopeReceiverRepository = envelopeReceiverRepository;
|
||||
_receiverRepository = receiverRepository;
|
||||
_docReceiverElementRepository = docReceiverElementRepository;
|
||||
}
|
||||
|
||||
public async Task<SaveEnvelopeDraftResultDto> 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<SaveEnvelopeDraftReceiverDto> 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<SaveEnvelopeDraftFieldDto>()
|
||||
})
|
||||
.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<string, EnvelopeReceiver> linksByEmail,
|
||||
IDictionary<int, EnvelopeReceiver> 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);
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,11 @@ using DigitalData.UserManager.Domain.Entities;
|
||||
using CommandDotNet.Execution;
|
||||
using EnvelopeGenerator.Infrastructure.Repositories;
|
||||
using EnvelopeGenerator.Infrastructure.Executor;
|
||||
using EnvelopeGenerator.Infrastructure.Writers;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
#elif NETFRAMEWORK
|
||||
using System;
|
||||
@@ -56,6 +58,7 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
||||
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
||||
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
||||
services.TryAddScoped<IDocReceiverElementWriter, DocReceiverElementWriter>();
|
||||
|
||||
// User Preferences — distributed cache (dbo.TBDD_CACHE)
|
||||
services.TryAddScoped<IUserPreferencesService, UserPreferencesService>();
|
||||
@@ -214,4 +217,4 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#if NET
|
||||
using EnvelopeGenerator.Application.Common.Interfaces;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Writers;
|
||||
|
||||
public class DocReceiverElementWriter(EGDbContext dbContext) : IDocReceiverElementWriter
|
||||
{
|
||||
public Task DeleteByDocumentAndReceiverAsync(int documentId, int receiverId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return dbContext.Database.ExecuteSqlInterpolatedAsync(
|
||||
$"DELETE FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE [DOCUMENT_ID] = {documentId} AND [RECEIVER_ID] = {receiverId}",
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task InsertAsync(DocReceiverElement element, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return dbContext.Database.ExecuteSqlInterpolatedAsync($@"
|
||||
INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
||||
([DOCUMENT_ID]
|
||||
,[RECEIVER_ID]
|
||||
,[ELEMENT_TYPE]
|
||||
,[POSITION_X]
|
||||
,[POSITION_Y]
|
||||
,[WIDTH]
|
||||
,[HEIGHT]
|
||||
,[REQUIRED]
|
||||
,[READ_ONLY]
|
||||
,[PAGE]
|
||||
,[ADDED_WHEN]
|
||||
,[FULL_NAME]
|
||||
,[POSITION]
|
||||
,[PLACE]
|
||||
,[TOOLTIP]
|
||||
,[ANNOTATION_INDEX])
|
||||
VALUES ({element.DocumentId}
|
||||
,{element.ReceiverId}
|
||||
,{element.ElementType}
|
||||
,{element.X}
|
||||
,{element.Y}
|
||||
,{element.Width}
|
||||
,{element.Height}
|
||||
,{element.Required}
|
||||
,{element.ReadOnly}
|
||||
,{element.Page}
|
||||
,{element.AddedWhen}
|
||||
,{element.FullName}
|
||||
,{element.Position}
|
||||
,{element.Place}
|
||||
,{element.Tooltip}
|
||||
,{element.AnnotationIndex})",
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user