diff --git a/EnvelopeGenerator.Application/Common/Dto/Signature.cs b/EnvelopeGenerator.Application/Common/Dto/Signature.cs
index 9a934934..71af71eb 100644
--- a/EnvelopeGenerator.Application/Common/Dto/Signature.cs
+++ b/EnvelopeGenerator.Application/Common/Dto/Signature.cs
@@ -49,7 +49,7 @@ public sealed record Signature
public string? Position
{
get => _position;
- init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
+ init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
}
///
@@ -62,4 +62,4 @@ public sealed record Signature
/// Example: "Berlin" ? rendered as "Berlin, 26.01.2025"
///
public required string Place { get; init; }
-}
\ No newline at end of file
+}
diff --git a/EnvelopeGenerator.Application/DependencyInjection.cs b/EnvelopeGenerator.Application/DependencyInjection.cs
index 23715f65..bf41d504 100644
--- a/EnvelopeGenerator.Application/DependencyInjection.cs
+++ b/EnvelopeGenerator.Application/DependencyInjection.cs
@@ -65,18 +65,21 @@ public static class DependencyInjection
cfg.AddBehavior, EnvelopeReceiverResolutionBehavior>();
// 1. AnnotationBehavior - Saves annotations (executes second)
+ cfg.AddBehavior, SaveSignatureBehavior>();
+
+ // 2. AnnotationBehavior - Saves annotations for legacy web flow
cfg.AddBehavior, AnnotationBehavior>();
- // 2. DocStatusBehavior - Creates document status (executes third)
+ // 3. DocStatusBehavior - Creates document status
cfg.AddBehavior, DocStatusBehavior>();
- // 3. HistoryBehavior - Records history (executes fourth)
+ // 4. HistoryBehavior - Records history
cfg.AddBehavior, HistoryBehavior>();
- // 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
+ // 5. SendSignedMailBehavior - Sends notification email (executes LAST)
cfg.AddBehavior, SendSignedMailBehavior>();
});
return services;
}
-}
\ No newline at end of file
+}
diff --git a/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/EnvelopeReceiverResolutionBehavior.cs b/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/EnvelopeReceiverResolutionBehavior.cs
index 8eeb1669..5a1946d4 100644
--- a/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/EnvelopeReceiverResolutionBehavior.cs
+++ b/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/EnvelopeReceiverResolutionBehavior.cs
@@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior x.Envelope)
+ .Include(x => x.Receiver)
+ .SingleOrDefaultAsync(cancellationToken)
?? throw new NotFoundException("EnvelopeReceiver not found");
request.SetEnvelopeReceiver(_mapper.Map(er));
diff --git a/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/SaveSignatureBehavior.cs b/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/SaveSignatureBehavior.cs
index e4aecec0..f1207ae3 100644
--- a/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/SaveSignatureBehavior.cs
+++ b/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/SaveSignatureBehavior.cs
@@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior
throw new BadRequestException($"Signatures are required for saving signature behavior.");
var elements = await _elementRepo
- .Where(e => e.Document.EnvelopeId == request.Envelope.Id)
- .Where(e => e.ReceiverId == request.Receiver.Id)
+ .Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
+ .Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
.ToListAsync(cancel);
foreach (var element in elements)
diff --git a/EnvelopeGenerator.Infrastructure/EGDbContext.cs b/EnvelopeGenerator.Infrastructure/EGDbContext.cs
index 2b6bdee0..b66f856f 100644
--- a/EnvelopeGenerator.Infrastructure/EGDbContext.cs
+++ b/EnvelopeGenerator.Infrastructure/EGDbContext.cs
@@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext
#region Trigger
// Configure entities to handle database triggers
- void AddTrigger() where T : class => _triggers
- .Where(t => t.Key == typeof(T).Name)
- .SelectMany(t => t.Value)
- .ForEach(tName =>
+ void AddTrigger() where T : class
+ {
+ var entityName = typeof(T).Name;
+
+ var configuredTriggerNames = _triggers
+ .Where(t => t.Key == entityName)
+ .SelectMany(t => t.Value)
+ .Distinct()
+ .ToList();
+
+ foreach (var triggerName in configuredTriggerNames)
{
#if NET
- modelBuilder.Entity().ToTable(tb => tb.HasTrigger(tName));
+ modelBuilder.Entity().ToTable(tb => tb.HasTrigger(triggerName));
#endif
- _logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", tName, typeof(T).Name);
- });
+ _logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", triggerName, entityName);
+ }
+ }
// TODO: call add trigger methods with attributes and reflection
AddTrigger();
@@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext
#if NETFRAMEWORK
}
-#endif
\ No newline at end of file
+#endif
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureSubmitDto.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureSubmitDto.cs
new file mode 100644
index 00000000..78d32317
--- /dev/null
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureSubmitDto.cs
@@ -0,0 +1,10 @@
+namespace EnvelopeGenerator.Server.Client.Models;
+
+public sealed record SignatureSubmitDto
+{
+ public required int Id { get; init; }
+ public required string DataUrl { get; init; }
+ public required string FullName { get; init; }
+ public string? Position { get; init; }
+ public required string Place { get; init; }
+}
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs
index a8f76d22..7b64dffb 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs
@@ -1,6 +1,7 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
+using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Server.Client.Models;
namespace EnvelopeGenerator.Server.Client.Services;
@@ -21,4 +22,32 @@ public class SignatureService(IHttpClientFactory httpClientFactory)
var result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel);
return result ?? [];
}
+
+ public async Task SubmitAsync(string envelopeKey, IEnumerable signatures, CancellationToken cancel = default)
+ {
+ using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
+
+ var payload = signatures
+ .Select(signature => new Signature
+ {
+ Id = signature.Id,
+ DataUrl = signature.DataUrl,
+ FullName = signature.FullName,
+ Position = signature.Position,
+ Place = signature.Place
+ })
+ .ToList();
+
+ var response = await http.PostAsJsonAsync(
+ $"/api/Signature/submit/{Uri.EscapeDataString(envelopeKey)}",
+ payload,
+ cancel);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var body = await response.Content.ReadAsStringAsync(cancel);
+ throw new HttpRequestException(
+ $"Failed to submit signature for envelope {envelopeKey}: {(int)response.StatusCode} {response.StatusCode} - {body}");
+ }
+ }
}
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor
index b1b5224f..25ebac73 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor
@@ -14,6 +14,7 @@
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
+@inject SignatureService SignatureService
@inject AppVersionService AppVersion
@inject IMemoryCache MemoryCache
@inject ILogger Logger
@@ -125,6 +126,12 @@
+ @if (!string.IsNullOrWhiteSpace(_submitErrorMessage))
+ {
+
+ @_submitErrorMessage
+
+ }
@@ -164,17 +171,48 @@
// ----- Submit / logout state -----
bool _isLoggingOut = false;
bool _submitConfirmVisible = false;
+ string? _submitErrorMessage;
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
async Task SubmitAndLogoutAsync()
{
if (_isLoggingOut) return;
+
+ if (string.IsNullOrWhiteSpace(EnvelopeKey) || _sig is null)
+ return;
+
_isLoggingOut = true;
_submitConfirmVisible = false;
+ _submitErrorMessage = null;
await InvokeAsync(StateHasChanged);
- await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey!);
- Navigation.NavigateTo("/envelope/signed-result", forceLoad: true);
+
+ try
+ {
+ if (_signatures.Count == 0)
+ throw new InvalidOperationException("No signature fields found for receiver.");
+
+ var payload = _signatures.Select(field => new SignatureSubmitDto
+ {
+ Id = field.Id,
+ DataUrl = _sig.DataUrl,
+ FullName = _sig.FullName,
+ Position = string.IsNullOrWhiteSpace(_sig.Position) ? null : _sig.Position,
+ Place = _sig.Place
+ }).ToList();
+
+ await SignatureService.SubmitAsync(EnvelopeKey, payload);
+
+ await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
+ Navigation.NavigateTo("/envelope/signed-result", forceLoad: true);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Failed to finalize signature for {EnvelopeKey}", EnvelopeKey);
+ _submitErrorMessage = "Die Unterschrift konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.";
+ _isLoggingOut = false;
+ await InvokeAsync(StateHasChanged);
+ }
}
protected override async Task OnInitializedAsync()
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs
index 2c951983..9e9a33cd 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs
@@ -1,7 +1,10 @@
using EnvelopeGenerator.Server.Extensions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
+using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Application.Documents.Queries;
+using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
+using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
@@ -18,13 +21,15 @@ namespace EnvelopeGenerator.Server.Controllers;
public class SignatureController : ControllerBase
{
private readonly IMediator _mediator;
+ private readonly ILogger _logger;
///
/// Initializes a new instance of .
///
- public SignatureController(IMediator mediator)
+ public SignatureController(IMediator mediator, ILogger logger)
{
_mediator = mediator;
+ _logger = logger;
}
//TODO: update to use signature query
@@ -54,4 +59,46 @@ public class SignatureController : ControllerBase
else
return Ok(rcvSignatures);
}
+
+ ///
+ /// Finalizes the receiver signing flow and persists signature data to TBSIG_DOCUMENT_RECEIVER_ELEMENT.
+ /// Also triggers status/history/mail side effects through SigningCommand pipeline.
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpPost("submit/{envelopeKey}")]
+ public async Task Submit(
+ [FromRoute] string envelopeKey,
+ [FromBody] IEnumerable? signatures,
+ CancellationToken cancel)
+ {
+ if (signatures is null)
+ return BadRequest("Signature payload is required.");
+
+ var uuid = User.EnvelopeUuid();
+ var receiverSignature = User.ReceiverSignature();
+
+ if (await _mediator.IsSignedAsync(uuid, receiverSignature, cancel))
+ return Problem(statusCode: StatusCodes.Status409Conflict);
+
+ if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
+ return Problem(statusCode: StatusCodes.Status423Locked);
+
+ try
+ {
+ await _mediator.Send(new SigningCommand
+ {
+ Envelope = new() { Uuid = uuid },
+ Receiver = new() { Signature = receiverSignature },
+ ReceiverAppType = ReceiverAppType.ReceiverUI,
+ Signatures = signatures.ToList()
+ }, cancel);
+
+ return Ok();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to submit signatures for envelope {EnvelopeKey}", envelopeKey);
+ throw;
+ }
+ }
}
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json
index 39f160ad..64b21d1a 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json
@@ -13,7 +13,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
- "applicationUrl": "https://localhost:8088;http://localhost:5131",
+ "applicationUrl": "https://localhost:8089;http://localhost:5132",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
index 8e1098f6..060188ee 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
@@ -185,9 +185,10 @@
"TimeLimit": "00:30:00"
},
"DbTriggerParams": {
- "Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
- "History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
"DocReceiverElement": [ "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS", "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD" ],
+ "Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
+ "EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
+ "History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
"Receiver": [],