diff --git a/EnvelopeGenerator.API/Controllers/EnvelopeController.cs b/EnvelopeGenerator.API/Controllers/EnvelopeController.cs index c1bb54d2..11ca0b17 100644 --- a/EnvelopeGenerator.API/Controllers/EnvelopeController.cs +++ b/EnvelopeGenerator.API/Controllers/EnvelopeController.cs @@ -27,6 +27,8 @@ namespace EnvelopeGenerator.API.Controllers; [Authorize] public class EnvelopeController : ControllerBase { + public record DeleteEnvelopeRequest(string Reason); + private readonly ILogger _logger; private readonly IMediator _mediator; @@ -108,4 +110,17 @@ public class EnvelopeController : ControllerBase else return Ok(res); } -} \ No newline at end of file + + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + [HttpDelete("{id:int}")] + public async Task 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); + } +} diff --git a/EnvelopeGenerator.API/Middleware/ExceptionHandlingMiddleware.cs b/EnvelopeGenerator.API/Middleware/ExceptionHandlingMiddleware.cs index 08d6c392..9fa448dc 100644 --- a/EnvelopeGenerator.API/Middleware/ExceptionHandlingMiddleware.cs +++ b/EnvelopeGenerator.API/Middleware/ExceptionHandlingMiddleware.cs @@ -69,6 +69,11 @@ 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; diff --git a/EnvelopeGenerator.Application/Common/Notifications/EnvelopeDeleted/EnvelopeDeletedNotification.cs b/EnvelopeGenerator.Application/Common/Notifications/EnvelopeDeleted/EnvelopeDeletedNotification.cs new file mode 100644 index 00000000..eb35b1e2 --- /dev/null +++ b/EnvelopeGenerator.Application/Common/Notifications/EnvelopeDeleted/EnvelopeDeletedNotification.cs @@ -0,0 +1,21 @@ +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."); +} diff --git a/EnvelopeGenerator.Application/Common/Notifications/EnvelopeDeleted/Handlers/SendDeletedMailHandler.cs b/EnvelopeGenerator.Application/Common/Notifications/EnvelopeDeleted/Handlers/SendDeletedMailHandler.cs new file mode 100644 index 00000000..c842bfef --- /dev/null +++ b/EnvelopeGenerator.Application/Common/Notifications/EnvelopeDeleted/Handlers/SendDeletedMailHandler.cs @@ -0,0 +1,42 @@ +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 +{ + public SendDeletedMailHandler( + IRepository tempRepo, + IRepository emailOutRepo, + IOptions mailParamsOptions, + IOptions 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 CreatePlaceHolders(EnvelopeDeletedNotification notification) + { + var senderName = notification.Envelope.User?.Email; + if (string.IsNullOrWhiteSpace(senderName)) + senderName = notification.SenderReference; + + return new Dictionary + { + ["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty, + ["[NAME_SENDER]"] = senderName ?? string.Empty, + ["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty, + ["[REASON]"] = notification.Reason + }; + } +} diff --git a/EnvelopeGenerator.Application/Envelopes/Commands/DeleteEnvelopeCommand.cs b/EnvelopeGenerator.Application/Envelopes/Commands/DeleteEnvelopeCommand.cs new file mode 100644 index 00000000..e0e0e436 --- /dev/null +++ b/EnvelopeGenerator.Application/Envelopes/Commands/DeleteEnvelopeCommand.cs @@ -0,0 +1,25 @@ +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 +{ + [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; + } +} diff --git a/EnvelopeGenerator.Application/Envelopes/Commands/DeleteEnvelopeCommandHandler.cs b/EnvelopeGenerator.Application/Envelopes/Commands/DeleteEnvelopeCommandHandler.cs new file mode 100644 index 00000000..eee92f48 --- /dev/null +++ b/EnvelopeGenerator.Application/Envelopes/Commands/DeleteEnvelopeCommandHandler.cs @@ -0,0 +1,83 @@ +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 +{ + private readonly IRepository _envelopeRepository; + private readonly IMediator _mediator; + + public DeleteEnvelopeCommandHandler( + IRepository envelopeRepository, + IMediator mediator) + { + _envelopeRepository = envelopeRepository; + _mediator = mediator; + } + + public async Task 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()) + { + 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); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs index 56e1aca2..b6a600af 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs @@ -27,6 +27,8 @@ namespace EnvelopeGenerator.Server.Controllers; [Authorize] public class EnvelopeController : ControllerBase { + public record DeleteEnvelopeRequest(string Reason); + private readonly ILogger _logger; private readonly IMediator _mediator; @@ -108,4 +110,17 @@ public class EnvelopeController : ControllerBase else return Ok(res); } -} \ No newline at end of file + + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + [HttpDelete("{id:int}")] + public async Task 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); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Middleware/ExceptionHandlingMiddleware.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Middleware/ExceptionHandlingMiddleware.cs index 8f4b252c..e14ecb68 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Middleware/ExceptionHandlingMiddleware.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Middleware/ExceptionHandlingMiddleware.cs @@ -69,6 +69,11 @@ 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; diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json index 9dadd643..6c2c3269 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json @@ -186,7 +186,7 @@ }, "DbTriggerParams": { "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": [],