feat(sender): add envelope draft save command and endpoint
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
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);
|
||||
@@ -0,0 +1,268 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
@@ -94,4 +95,51 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
public async Task<SaveEnvelopeDraftResultDto?> SaveDraftAsync(
|
||||
SaveEnvelopeDraftCommand request,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var payload = new
|
||||
{
|
||||
EnvelopeId = request.EnvelopeId,
|
||||
Title = request.Title,
|
||||
Message = request.Message,
|
||||
TFAEnabled = request.TFAEnabled,
|
||||
Document = request.Document is null
|
||||
? null
|
||||
: new { DataAsBase64 = request.Document.DataAsBase64 },
|
||||
Receivers = request.Receivers.Select(r => new
|
||||
{
|
||||
EmailAddress = r.EmailAddress,
|
||||
Name = r.Name,
|
||||
PhoneNumber = r.PhoneNumber,
|
||||
Fields = r.Fields.Select(f => new { X = f.X, Y = f.Y, Page = f.Page }).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var response = await httpClient.PostAsJsonAsync("/api/Envelope/draft", payload, _jsonOptions, cancel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.Content.ReadFromJsonAsync<SaveEnvelopeDraftResultDto>(_jsonOptions, cancel);
|
||||
|
||||
string? message = null;
|
||||
try
|
||||
{
|
||||
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>(_jsonOptions, cancel);
|
||||
message = error?.Message;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message = null;
|
||||
}
|
||||
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||
throw new HttpRequestException(
|
||||
$"Failed to save envelope draft. Status: {statusCode} ({reasonPhrase}){(string.IsNullOrWhiteSpace(message) ? string.Empty : $" - {message}")}",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,4 +123,12 @@ public class EnvelopeController : ControllerBase
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpPost("draft")]
|
||||
public async Task<IActionResult> SaveDraftAsync([FromBody] SaveEnvelopeDraftCommand command)
|
||||
{
|
||||
var result = await _mediator.Send(command.WithAuth(User.GetId()));
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user