Compare commits

..

2 Commits

Author SHA1 Message Date
380a5f4c0c Add signature submission feature and pipeline updates
Introduced a new feature for submitting signatures, including
backend and frontend changes. Added `SignatureSubmitDto` and
`SubmitAsync` in `SignatureService` to handle signature data
submission. Implemented a new `Submit` endpoint in
`SignatureController` to validate and process signature payloads.

Updated pipeline behaviors to include `SaveSignatureBehavior`
and adjusted the execution order. Enhanced query logic in
`EnvelopeReceiverResolutionBehavior` and `SaveSignatureBehavior`
to align with the updated data model.

Refactored `EGDbContext` to improve trigger handling and logging.
Updated `ReceiverSignedPage.razor` to integrate `SignatureService`
and handle submission errors gracefully. Adjusted application
URLs in `launchSettings.json` and added new database triggers
in `appsettings.json`.
2026-09-28 12:21:44 +02:00
0f53d0509e remove depr API 2026-09-28 09:51:51 +02:00
23 changed files with 167 additions and 466 deletions

View File

@@ -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;
}
/// <summary>
@@ -62,4 +62,4 @@ public sealed record Signature
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
/// </summary>
public required string Place { get; init; }
}
}

View File

@@ -1,21 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
{
public required Envelope Envelope { get; init; }
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
public required string Reason { get; init; }
public required string SenderReference { get; init; }
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
}

View File

@@ -1,42 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
{
public SendDeletedMailHandler(
IRepository<EmailTemplate> tempRepo,
IRepository<EmailOut> emailOutRepo,
IOptions<MailParams> mailParamsOptions,
IOptions<DispatcherParams> dispatcherParamsOptions)
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
{
}
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
{
emailOut.ReferenceString = notification.EmailAddress;
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
}
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
{
var senderName = notification.Envelope.User?.Email;
if (string.IsNullOrWhiteSpace(senderName))
senderName = notification.SenderReference;
return new Dictionary<string, string>
{
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
["[NAME_SENDER]"] = senderName ?? string.Empty,
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
["[REASON]"] = notification.Reason
};
}
}

View File

@@ -65,18 +65,21 @@ public static class DependencyInjection
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
// 1. AnnotationBehavior - Saves annotations (executes second)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
// 2. AnnotationBehavior - Saves annotations for legacy web flow
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
// 2. DocStatusBehavior - Creates document status (executes third)
// 3. DocStatusBehavior - Creates document status
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
// 3. HistoryBehavior - Records history (executes fourth)
// 4. HistoryBehavior - Records history
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
});
return services;
}
}
}

View File

@@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
// If EnvelopeReceiver is not provided, query it from database
if (request.EnvelopeReceiver is null)
{
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
var er = await _erRepo.Query
.Where(request, notnull: true)
.Include(x => x.Envelope)
.Include(x => x.Receiver)
.SingleOrDefaultAsync(cancellationToken)
?? throw new NotFoundException("EnvelopeReceiver not found");
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));

View File

@@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
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)

View File

@@ -1,25 +0,0 @@
using System.ComponentModel.DataAnnotations;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
{
[Required]
public int EnvelopeId { get; init; }
[Required]
[MinLength(1)]
public string Reason { get; init; } = string.Empty;
internal int UserId { get; private set; }
public DeleteEnvelopeCommand WithAuth(int userId)
{
UserId = userId;
return this;
}
}

View File

@@ -1,83 +0,0 @@
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
{
private readonly IRepository<Envelope> _envelopeRepository;
private readonly IMediator _mediator;
public DeleteEnvelopeCommandHandler(
IRepository<Envelope> envelopeRepository,
IMediator mediator)
{
_envelopeRepository = envelopeRepository;
_mediator = mediator;
}
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
{
var reason = request.Reason?.Trim();
if (string.IsNullOrWhiteSpace(reason))
throw new BadRequestException("Please provide a reason.");
var envelope = await _envelopeRepository.Query
.Include(e => e.User)
.Include(e => e.EnvelopeReceivers)
.ThenInclude(er => er.Receiver)
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
if (envelope.UserId != request.UserId)
throw new ForbiddenException();
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
? EnvelopeStatus.EnvelopeWithdrawn
: EnvelopeStatus.EnvelopeDeleted;
envelope.Status = targetStatus;
envelope.Comment = reason;
envelope.ChangedWhen = DateTime.Now;
await _envelopeRepository.UpdateAsync(envelope, e => e.Id == envelope.Id, cancel);
var userReference = envelope.User?.Email;
if (string.IsNullOrWhiteSpace(userReference))
throw new BadRequestException("Envelope sender email is missing.");
await _mediator.Send(new CreateHistoryCommand
{
EnvelopeId = envelope.Id,
UserReference = userReference,
Status = targetStatus,
Comment = reason
}, cancel);
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
{
if (envelopeReceiver.Receiver is null)
continue;
await _mediator.Publish(new EnvelopeDeletedNotification
{
Envelope = envelope,
EnvelopeReceiver = envelopeReceiver,
Reason = reason,
SenderReference = userReference
}, cancel);
}
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
}
}

View File

@@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext
#region Trigger
// Configure entities to handle database triggers
void AddTrigger<T>() where T : class => _triggers
.Where(t => t.Key == typeof(T).Name)
.SelectMany(t => t.Value)
.ForEach(tName =>
void AddTrigger<T>() 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<T>().ToTable(tb => tb.HasTrigger(tName));
modelBuilder.Entity<T>().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<Config>();
@@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext
#if NETFRAMEWORK
}
#endif
#endif

View File

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

View File

@@ -12,8 +12,6 @@ namespace EnvelopeGenerator.Server.Client.Services;
public class EnvelopeService(IHttpClientFactory clientFactory)
{
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
private sealed record DeleteEnvelopeRequest(string Reason);
private sealed record ErrorResponse(string? Message);
/// <summary>
/// Fetches envelopes from the API with optional filters.
@@ -63,35 +61,4 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
return await response.Content.ReadFromJsonAsync<IEnumerable<EnvelopeDto>>(_jsonOptions, cancel);
}
public async Task DeleteAsync(int envelopeId, string reason, CancellationToken cancel = default)
{
var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server");
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/Envelope/{envelopeId}")
{
Content = JsonContent.Create(new DeleteEnvelopeRequest(reason), options: _jsonOptions)
};
var response = await httpClient.SendAsync(request, cancel);
if (response.IsSuccessStatusCode)
return;
string? message = null;
try
{
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>(_jsonOptions, cancel);
message = error?.Message;
}
catch
{
message = null;
}
var statusCode = (int)response.StatusCode;
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
throw new HttpRequestException(
$"Failed to delete envelope. Status: {statusCode} ({reasonPhrase}){(string.IsNullOrWhiteSpace(message) ? string.Empty : $" - {message}")}",
null,
response.StatusCode);
}
}

View File

@@ -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<List<SignatureDto>>(_jsonOptions, cancel);
return result ?? [];
}
public async Task SubmitAsync(string envelopeKey, IEnumerable<SignatureSubmitDto> 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}");
}
}
}

View File

@@ -521,54 +521,6 @@
</BodyContentTemplate>
</DxPopup>
<DxPopup @bind-Visible="@_deletePopupVisible"
HeaderText="Umschlag löschen"
Width="560px"
MaxWidth="95vw"
ShowFooter="true"
CloseOnOutsideClick="false"
ShowCloseButton="true"
CloseOnEscape="true"
ShowHeader="true"
CssClass="sender-delete-popup">
<BodyContentTemplate>
<div style="display:flex;flex-direction:column;gap:0.75rem;">
<p style="margin:0;color:#374151;font-size:0.9rem;line-height:1.45;">
Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.
</p>
@if (_selectedEnvelope is not null)
{
<p style="margin:0;color:#6b7280;font-size:0.825rem;">
Umschlag: <strong>#@_selectedEnvelope.Id - @_selectedEnvelope.Title</strong>
</p>
}
<DxMemo @bind-Text="_deleteReason"
NullText="Begründung eingeben..."
Rows="5"
MaxLength="2000"
CssClass="w-100" />
@if (!string.IsNullOrWhiteSpace(_deleteErrorMessage))
{
<div class="alert alert-danger" style="margin:0;padding:0.5rem 0.75rem;font-size:0.85rem;">
@_deleteErrorMessage
</div>
}
</div>
</BodyContentTemplate>
<FooterContentTemplate>
<div style="display:flex;justify-content:flex-end;gap:0.5rem;width:100%;">
<DxButton Text="Abbrechen"
RenderStyle="ButtonRenderStyle.Secondary"
Click="CloseDeletePopup"
Enabled="@(!_isDeletingEnvelope)" />
<DxButton Text="Löschen"
RenderStyle="ButtonRenderStyle.Danger"
Click="ConfirmDeleteEnvelopeAsync"
Enabled="@(!_isDeletingEnvelope)" />
</div>
</FooterContentTemplate>
</DxPopup>
@code {
private IEnumerable<EnvelopeDto>? _allEnvelopes;
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
@@ -597,11 +549,6 @@
private bool _exportSelectedRowsOnly = false;
private bool _isExporting = false;
private bool _deletePopupVisible = false;
private bool _isDeletingEnvelope = false;
private string _deleteReason = string.Empty;
private string? _deleteErrorMessage;
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
private const string GridKeyActive = "sender.active-envelopes";
private const string GridKeyCompleted = "sender.completed-envelopes";
@@ -734,51 +681,7 @@
void DeleteEnvelope()
{
if (_selectedEnvelope == null) return;
_deleteReason = string.Empty;
_deleteErrorMessage = null;
_deletePopupVisible = true;
}
void CloseDeletePopup()
{
if (_isDeletingEnvelope)
return;
_deletePopupVisible = false;
_deleteErrorMessage = null;
}
async Task ConfirmDeleteEnvelopeAsync()
{
if (_selectedEnvelope is null)
return;
if (string.IsNullOrWhiteSpace(_deleteReason))
{
_deleteErrorMessage = "Bitte geben Sie einen Löschgrund an.";
return;
}
_isDeletingEnvelope = true;
_deleteErrorMessage = null;
await InvokeAsync(StateHasChanged);
try
{
await EnvelopeService.DeleteAsync(_selectedEnvelope.Id, _deleteReason.Trim());
_deletePopupVisible = false;
_selectedEnvelope = null;
await LoadEnvelopesAsync();
}
catch (Exception ex)
{
_deleteErrorMessage = ex.Message;
}
finally
{
_isDeletingEnvelope = false;
await InvokeAsync(StateHasChanged);
}
// TODO: Show delete confirmation dialog
}
async Task LogoutAsync()

View File

@@ -12,6 +12,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<ReceiverSignedPage> Logger
@@ -122,6 +123,12 @@
</p>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(_submitErrorMessage))
{
<div class="alert alert-danger mb-0" role="alert" style="font-size: 0.85rem;">
@_submitErrorMessage
</div>
}
</BodyContentTemplate>
<FooterContentTemplate>
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
@@ -161,17 +168,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("/", 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("/", 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()

View File

@@ -27,8 +27,6 @@ namespace EnvelopeGenerator.Server.Controllers;
[Authorize]
public class EnvelopeController : ControllerBase
{
public record DeleteEnvelopeRequest(string Reason);
private readonly ILogger<EnvelopeController> _logger;
private readonly IMediator _mediator;
@@ -110,17 +108,4 @@ public class EnvelopeController : ControllerBase
else
return Ok(res);
}
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
[HttpDelete("{id:int}")]
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
{
var result = await _mediator.Send(new DeleteEnvelopeCommand
{
EnvelopeId = id,
Reason = request?.Reason ?? string.Empty
}.WithAuth(User.GetId()));
return Ok(result);
}
}
}

View File

@@ -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<SignatureController> _logger;
/// <summary>
/// Initializes a new instance of <see cref="SignatureController"/>.
/// </summary>
public SignatureController(IMediator mediator)
public SignatureController(IMediator mediator, ILogger<SignatureController> logger)
{
_mediator = mediator;
_logger = logger;
}
//TODO: update to use signature query
@@ -54,4 +59,46 @@ public class SignatureController : ControllerBase
else
return Ok(rcvSignatures);
}
/// <summary>
/// Finalizes the receiver signing flow and persists signature data to TBSIG_DOCUMENT_RECEIVER_ELEMENT.
/// Also triggers status/history/mail side effects through SigningCommand pipeline.
/// </summary>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpPost("submit/{envelopeKey}")]
public async Task<IActionResult> Submit(
[FromRoute] string envelopeKey,
[FromBody] IEnumerable<Signature>? 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;
}
}
}

View File

@@ -69,11 +69,6 @@ public class ExceptionHandlingMiddleware
message = notFoundEx.Message;
break;
case ForbiddenException forbiddenEx:
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
message = forbiddenEx.Message;
break;
default:
logger.LogError(exception, "Unhandled exception occurred.");
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

View File

@@ -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"
},

View File

@@ -185,7 +185,9 @@
"TimeLimit": "00:30:00"
},
"DbTriggerParams": {
"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" ],

View File

@@ -1,109 +0,0 @@
using DigitalData.Core.Exceptions;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Tests.Application;
[TestFixture]
public class DeleteEnvelopeCommandTests : TestBase
{
protected override void ConfigureServices(IServiceCollection services)
{
}
[Test]
public async Task DeleteEnvelope_DraftEnvelope_ShouldSetDeleted_CreateHistory_AndQueueDeletionEmail()
{
CancellationToken cancel = default;
var reason = "Invalid receiver data";
var envelope = this.CreateEnvelope(User.Id);
envelope.Status = EnvelopeStatus.EnvelopeCreated;
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
var createReceiverCmd = this.CreateReceiverCommand();
(var receiver, _) = await Mediator.Send(createReceiverCmd, cancel);
var envReceiver = this.CreateEnvelopeReceiver(envelope.Id, receiver.Id);
await GetRepository<EnvelopeReceiver>().CreateAsync(envReceiver, cancel);
var result = await Mediator.Send(new DeleteEnvelopeCommand
{
EnvelopeId = envelope.Id,
Reason = reason
}.WithAuth(User.Id), cancel);
Assert.That(result.EnvelopeId, Is.EqualTo(envelope.Id));
Assert.That(result.AppliedStatus, Is.EqualTo(EnvelopeStatus.EnvelopeDeleted));
Assert.That(result.IsNoOp, Is.False);
var histories = await GetRepository<History>().Query
.Where(h => h.EnvelopeId == envelope.Id && h.Status == EnvelopeStatus.EnvelopeDeleted)
.ToListAsync(cancel);
Assert.That(histories, Has.Count.EqualTo(1));
Assert.That(histories[0].Comment, Is.EqualTo(reason));
var emails = await GetRepository<EmailOut>().Query
.Where(e => e.WfId == (int)EnvelopeStatus.MessageDeletionSent && e.ReferenceId == receiver.Id)
.ToListAsync(cancel);
Assert.That(emails, Has.Count.EqualTo(1));
Assert.That(emails[0].EmailAddress, Is.EqualTo(receiver.EmailAddress));
}
[Test]
public async Task DeleteEnvelope_SentEnvelope_ShouldSetWithdrawn()
{
CancellationToken cancel = default;
var envelope = this.CreateEnvelope(User.Id);
envelope.Status = EnvelopeStatus.EnvelopeQueued;
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
var result = await Mediator.Send(new DeleteEnvelopeCommand
{
EnvelopeId = envelope.Id,
Reason = "Sender requested cancellation"
}.WithAuth(User.Id), cancel);
Assert.That(result.AppliedStatus, Is.EqualTo(EnvelopeStatus.EnvelopeWithdrawn));
var histories = await GetRepository<History>().Query
.Where(h => h.EnvelopeId == envelope.Id && h.Status == EnvelopeStatus.EnvelopeWithdrawn)
.ToListAsync(cancel);
Assert.That(histories, Has.Count.EqualTo(1));
}
[Test]
public void DeleteEnvelope_MissingReason_ShouldThrowBadRequest()
{
Assert.ThrowsAsync<BadRequestException>(async () =>
await Mediator.Send(new DeleteEnvelopeCommand
{
EnvelopeId = 123,
Reason = " "
}.WithAuth(User.Id)));
}
[Test]
public async Task DeleteEnvelope_WrongOwner_ShouldThrowForbidden()
{
CancellationToken cancel = default;
var envelope = this.CreateEnvelope(User.Id);
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
var otherUserCmd = this.CreateUserCommand();
var otherUser = await GetRepository<DigitalData.UserManager.Domain.Entities.User>().CreateAsync(otherUserCmd, cancel);
Assert.ThrowsAsync<ForbiddenException>(async () =>
await Mediator.Send(new DeleteEnvelopeCommand
{
EnvelopeId = envelope.Id,
Reason = "Not allowed"
}.WithAuth(otherUser.Id), cancel));
}
}

View File

@@ -52,7 +52,7 @@ public class Fake
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
{
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase($"EnvelopeGeneratorTestDb_{Guid.NewGuid():N}"));
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase("EnvelopeGeneratorTestDb"));
opt.AddDbTriggerParams(context.Configuration);
@@ -308,4 +308,4 @@ public static class Extensions
.Select(_ => fake.CreateUserCommand())
.ToList();
#endregion
}
}

View File

@@ -38,10 +38,7 @@ public abstract class TestBase : Faker
// Add seed email templates
foreach (var temp in SeedEmailTemplates)
{
temp.LangCode ??= "de";
await repo.CreateAsync(temp);
}
}
[TearDown]
@@ -143,4 +140,4 @@ public abstract class TestBase : Faker
ChangedWhen = DateTime.Parse("2025-05-12 10:43:44.290")
}
};
}
}

View File

@@ -36,8 +36,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.DependencyInjection", "EnvelopeGenerator.DependencyInjection\EnvelopeGenerator.DependencyInjection.csproj", "{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
@@ -95,10 +93,6 @@ Global
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.Build.0 = Release|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -136,7 +130,6 @@ Global
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{BF1700D5-592E-4FFA-84E8-5480E289A1F0} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{4E6C54DA-576D-0955-2564-9EC890BB8279} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0}