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