feat(sender): route editor save and send through unified envelope upsert API
- expose POST /api/Envelope for sender-authenticated create/update/send operations - replace client draft-specific call with EnvelopeService.UpsertAsync(CreateEnvelopeCommand) - update sender editor save/send flow to persist draft first and send by envelope id - allow editor hydration without document by making sender document fetch nullable
This commit is contained in:
@@ -96,8 +96,8 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
public async Task<SaveEnvelopeDraftResultDto?> SaveDraftAsync(
|
||||
SaveEnvelopeDraftCommand request,
|
||||
public async Task<EnvelopeDto?> UpsertAsync(
|
||||
CreateEnvelopeCommand request,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
@@ -107,6 +107,7 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||
Title = request.Title,
|
||||
Message = request.Message,
|
||||
TFAEnabled = request.TFAEnabled,
|
||||
Send = request.Send,
|
||||
Document = request.Document is null
|
||||
? null
|
||||
: new { DataAsBase64 = request.Document.DataAsBase64 },
|
||||
@@ -119,10 +120,10 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var response = await httpClient.PostAsJsonAsync("/api/Envelope/draft", payload, _jsonOptions, cancel);
|
||||
var response = await httpClient.PostAsJsonAsync("/api/Envelope", payload, _jsonOptions, cancel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.Content.ReadFromJsonAsync<SaveEnvelopeDraftResultDto>(_jsonOptions, cancel);
|
||||
return await response.Content.ReadFromJsonAsync<EnvelopeDto>(_jsonOptions, cancel);
|
||||
|
||||
string? message = null;
|
||||
try
|
||||
@@ -138,7 +139,7 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||
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}")}",
|
||||
$"Failed to save envelope. Status: {statusCode} ({reasonPhrase}){(string.IsNullOrWhiteSpace(message) ? string.Empty : $" - {message}")}",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@using DevExpress.Blazor
|
||||
@using EnvelopeGenerator.Application.Common.Dto
|
||||
@using EnvelopeGenerator.Application.Envelopes.Commands
|
||||
@using EnvelopeGenerator.Application.EnvelopeReceivers.Commands
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@using EnvelopeGenerator.Server.Services
|
||||
@using Microsoft.AspNetCore.WebUtilities
|
||||
@@ -16,7 +15,6 @@
|
||||
@inject AppVersionService AppVersion
|
||||
@inject ILogger<EnvelopeSenderEditorPage> Logger
|
||||
@inject EnvelopeReceiverPageDataService ReceiverPageDataService
|
||||
@inject EnvelopeReceiverService EnvelopeReceiverService
|
||||
@inject EnvelopeService EnvelopeService
|
||||
@inject IEnvelopeAuthService EnvelopeAuthService
|
||||
@inject IMemoryCache MemoryCache
|
||||
@@ -598,8 +596,6 @@
|
||||
?? throw new InvalidOperationException($"Envelope {envelopeId} not found.");
|
||||
|
||||
var document = await ReceiverPageDataService.GetDocumentDetailsOfSenderAsync(user, envelopeId);
|
||||
if (document.ByteData is null || document.ByteData.Length == 0)
|
||||
throw new InvalidOperationException($"Document for envelope {envelopeId} is missing.");
|
||||
|
||||
_envelopeTitle = envelope.Title ?? string.Empty;
|
||||
_envelopeMessage = envelope.Message ?? string.Empty;
|
||||
@@ -620,7 +616,7 @@
|
||||
.GroupBy(r => r.Email, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.First().Color, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_signatureFields = (document.Elements ?? Enumerable.Empty<DocReceiverElementDto>())
|
||||
_signatureFields = (document?.Elements ?? Enumerable.Empty<DocReceiverElementDto>())
|
||||
.Select(element =>
|
||||
{
|
||||
receiverById.TryGetValue(element.ReceiverId, out var envelopeReceiver);
|
||||
@@ -644,9 +640,11 @@
|
||||
})
|
||||
.ToList();
|
||||
|
||||
_originalPdfBytes = document.ByteData;
|
||||
_pdfBytes = DrawPlaceholders(_originalPdfBytes, _signatureFields);
|
||||
_pdfLoaded = true;
|
||||
_originalPdfBytes = document?.ByteData;
|
||||
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
||||
_pdfBytes = _pdfLoaded
|
||||
? DrawPlaceholders(_originalPdfBytes!, _signatureFields)
|
||||
: null;
|
||||
_fileName = $"Envelope-{envelopeId}.pdf";
|
||||
_pendingReceiverForPlacement = null;
|
||||
_loadedEnvelopeId = envelopeId;
|
||||
@@ -817,21 +815,21 @@
|
||||
try
|
||||
{
|
||||
var previousEnvelopeId = EnvelopeId;
|
||||
var command = BuildSaveDraftCommand();
|
||||
var command = BuildCreateEnvelopeCommand(send: false);
|
||||
Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId} IsEditMode={IsEditMode}", command.EnvelopeId, IsEditMode);
|
||||
var result = await EnvelopeService.SaveDraftAsync(command);
|
||||
var result = await EnvelopeService.UpsertAsync(command);
|
||||
|
||||
if (result is null)
|
||||
throw new InvalidOperationException("Draft save did not return a result.");
|
||||
|
||||
EnvelopeId = result.EnvelopeId;
|
||||
_loadedEnvelopeId = result.EnvelopeId;
|
||||
_editingEnvelopeId = result.EnvelopeId;
|
||||
EnvelopeId = result.Id;
|
||||
_loadedEnvelopeId = result.Id;
|
||||
_editingEnvelopeId = result.Id;
|
||||
|
||||
if (previousEnvelopeId != result.EnvelopeId)
|
||||
if (previousEnvelopeId != result.Id)
|
||||
{
|
||||
var sid = string.IsNullOrWhiteSpace(Esid) ? Guid.NewGuid().ToString("N") : Esid;
|
||||
NavigationManager.NavigateTo($"/sender/editor?esid={sid}&envelopeId={result.EnvelopeId}", forceLoad: false);
|
||||
NavigationManager.NavigateTo($"/sender/editor?esid={sid}&envelopeId={result.Id}", forceLoad: false);
|
||||
}
|
||||
|
||||
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
||||
@@ -848,7 +846,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Send — POST /api/EnvelopeReceiver ──
|
||||
// ── Send envelope ──
|
||||
async Task SendAsync()
|
||||
{
|
||||
// ── Validation ──
|
||||
@@ -895,61 +893,38 @@
|
||||
|
||||
try
|
||||
{
|
||||
// ── Build request ──
|
||||
// Document: use the ORIGINAL pdf (without placeholder burn-in) as base64
|
||||
var docBase64 = Convert.ToBase64String(_originalPdfBytes);
|
||||
var envelopeId = _editingEnvelopeId
|
||||
?? EnvelopeId
|
||||
?? _loadedEnvelopeId
|
||||
?? GetEnvelopeIdFromCurrentUri();
|
||||
|
||||
// Build receivers list — each receiver gets their own signature fields
|
||||
// Coordinate conversion: PDF points → inches (DB stores inches)
|
||||
var receiversCmd = _receivers.Select(receiver =>
|
||||
if (!envelopeId.HasValue || envelopeId.Value <= 0)
|
||||
{
|
||||
var fields = _signatureFields
|
||||
.Where(f => f.ReceiverName == receiver.FullName)
|
||||
.Select(f => new DocReceiverElementCreateDto(
|
||||
X: f.XPt / 72.0,
|
||||
Y: f.YPt / 72.0,
|
||||
Page: f.Page))
|
||||
.ToList();
|
||||
var saveResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: false))
|
||||
?? throw new InvalidOperationException("Draft save did not return a result.");
|
||||
|
||||
return new ReceiverGetOrCreateCommand
|
||||
{
|
||||
EmailAddress = receiver.Email,
|
||||
Salution = receiver.FullName,
|
||||
PhoneNumber = string.IsNullOrWhiteSpace(receiver.PhoneNumber)
|
||||
? null
|
||||
: receiver.PhoneNumber,
|
||||
DocReceiverElements = fields,
|
||||
};
|
||||
}).ToList();
|
||||
envelopeId = saveResult.Id;
|
||||
EnvelopeId = saveResult.Id;
|
||||
_loadedEnvelopeId = saveResult.Id;
|
||||
_editingEnvelopeId = saveResult.Id;
|
||||
}
|
||||
|
||||
var command = new CreateEnvelopeReceiverCommand
|
||||
{
|
||||
Title = _envelopeTitle.Trim(),
|
||||
Message = string.IsNullOrWhiteSpace(_envelopeMessage)
|
||||
? "Bitte unterzeichnen Sie das beigefügte Dokument."
|
||||
: _envelopeMessage.Trim(),
|
||||
TFAEnabled = false,
|
||||
Document = new DocumentCreateCommand { DataAsBase64 = docBase64 },
|
||||
Receivers = receiversCmd,
|
||||
};
|
||||
var sendResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: true) with { EnvelopeId = envelopeId })
|
||||
?? throw new InvalidOperationException("Send did not return a result.");
|
||||
|
||||
var result = await EnvelopeReceiverService.CreateAsync(command);
|
||||
|
||||
Logger.LogInformation(
|
||||
"[SenderEditor] Envelope created. Id={Id} SentReceivers={Count}",
|
||||
result?.Id, result?.SentReceiver.Count());
|
||||
Logger.LogInformation("[SenderEditor] Envelope sent from draft. Id={Id} Status={Status}", sendResult.Id, sendResult.Status);
|
||||
|
||||
// Success — show popup; GoToDashboard clears cache and navigates
|
||||
_saveErrorMessage = null;
|
||||
_saveSuccessHeader = "Umschlag erstellt";
|
||||
_saveSuccessTitle = "Umschlag wurde erfolgreich erstellt.";
|
||||
_saveSuccessBody = "Der Umschlag wurde gespeichert und die Empfänger wurden benachrichtigt.";
|
||||
_saveSuccessHeader = "Umschlag gesendet";
|
||||
_saveSuccessTitle = "Umschlag wurde erfolgreich gesendet.";
|
||||
_saveSuccessBody = "Der bestehende Entwurf wurde versendet.";
|
||||
_saveNavigateToDashboard = true;
|
||||
_savePopupVisible = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "[SenderEditor] Failed to create envelope");
|
||||
Logger.LogError(ex, "[SenderEditor] Failed to send envelope draft");
|
||||
_saveErrorMessage = ex.Message;
|
||||
_savePopupVisible = true;
|
||||
}
|
||||
@@ -959,7 +934,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
SaveEnvelopeDraftCommand BuildSaveDraftCommand()
|
||||
CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send)
|
||||
{
|
||||
var envelopeId = _editingEnvelopeId
|
||||
?? EnvelopeId
|
||||
@@ -977,26 +952,27 @@
|
||||
{
|
||||
var fields = _signatureFields
|
||||
.Where(f => f.ReceiverName == receiver.FullName)
|
||||
.Select(f => new SaveEnvelopeDraftFieldDto(
|
||||
.Select(f => new CreateEnvelopeFieldDto(
|
||||
X: f.XPt / 72.0,
|
||||
Y: f.YPt / 72.0,
|
||||
Page: f.Page))
|
||||
.ToList();
|
||||
|
||||
return new SaveEnvelopeDraftReceiverDto(
|
||||
return new CreateEnvelopeReceiverDto(
|
||||
EmailAddress: receiver.Email,
|
||||
Name: receiver.FullName,
|
||||
PhoneNumber: string.IsNullOrWhiteSpace(receiver.PhoneNumber) ? null : receiver.PhoneNumber,
|
||||
Fields: fields);
|
||||
}).ToList();
|
||||
|
||||
return new SaveEnvelopeDraftCommand
|
||||
return new CreateEnvelopeCommand
|
||||
{
|
||||
EnvelopeId = envelopeId,
|
||||
Title = _envelopeTitle,
|
||||
Message = _envelopeMessage,
|
||||
TFAEnabled = false,
|
||||
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new SaveEnvelopeDraftDocumentDto(docBase64),
|
||||
Send = send,
|
||||
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64),
|
||||
Receivers = receiverCommands
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ public class EnvelopeController : ControllerBase
|
||||
/// </summary>
|
||||
/// <param name="command"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
||||
@@ -124,11 +123,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class EnvelopeReceiverPageDataService(
|
||||
/// <summary>
|
||||
/// Loads the full document DTO (including receiver elements) for the authenticated sender.
|
||||
/// </summary>
|
||||
public async Task<DocumentDto> GetDocumentDetailsOfSenderAsync(ClaimsPrincipal user, int envelopeId, CancellationToken cancellationToken = default)
|
||||
public async Task<DocumentDto?> GetDocumentDetailsOfSenderAsync(ClaimsPrincipal user, int envelopeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await mediator.Send(new ReadDocumentQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user