From 4dba965c758c47896d01b2ebd9db7d7b65421d8f Mon Sep 17 00:00:00 2001 From: TekH Date: Sat, 26 Sep 2026 13:11:44 +0200 Subject: [PATCH] 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 --- .../Services/EnvelopeService.cs | 11 +- .../Pages/EnvelopeSenderEditorPage.razor | 102 +++++++----------- .../Controllers/EnvelopeController.cs | 8 -- .../EnvelopeReceiverPageDataService.cs | 2 +- 4 files changed, 46 insertions(+), 77 deletions(-) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs index fda994ee..ca83a036 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs @@ -96,8 +96,8 @@ public class EnvelopeService(IHttpClientFactory clientFactory) response.StatusCode); } - public async Task SaveDraftAsync( - SaveEnvelopeDraftCommand request, + public async Task 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(_jsonOptions, cancel); + return await response.Content.ReadFromJsonAsync(_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); } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor index 04915d66..347a307b 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor @@ -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 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()) + _signatureFields = (document?.Elements ?? Enumerable.Empty()) .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 }; } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs index ebf4c068..e9ddc0eb 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs @@ -95,7 +95,6 @@ public class EnvelopeController : ControllerBase /// /// /// - [NonAction] [Authorize(AuthenticationSchemes = AuthScheme.Sender)] [HttpPost] public async Task CreateAsync([FromBody] CreateEnvelopeCommand command) @@ -124,11 +123,4 @@ public class EnvelopeController : ControllerBase return Ok(result); } - [Authorize(AuthenticationSchemes = AuthScheme.Sender)] - [HttpPost("draft")] - public async Task SaveDraftAsync([FromBody] SaveEnvelopeDraftCommand command) - { - var result = await _mediator.Send(command.WithAuth(User.GetId())); - return Ok(result); - } } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs index 1dabc011..c16f07dc 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs @@ -52,7 +52,7 @@ public class EnvelopeReceiverPageDataService( /// /// Loads the full document DTO (including receiver elements) for the authenticated sender. /// - public async Task GetDocumentDetailsOfSenderAsync(ClaimsPrincipal user, int envelopeId, CancellationToken cancellationToken = default) + public async Task GetDocumentDetailsOfSenderAsync(ClaimsPrincipal user, int envelopeId, CancellationToken cancellationToken = default) { return await mediator.Send(new ReadDocumentQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken); }