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:
2026-09-26 13:11:44 +02:00
parent 4d2538cc2c
commit 4dba965c75
4 changed files with 46 additions and 77 deletions

View File

@@ -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);
}

View File

@@ -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
};
}

View File

@@ -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);
}
}

View File

@@ -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);
}