From e5347b063da778d417ee9cee1e128ae146173126 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 9 Jun 2026 18:33:57 +0200 Subject: [PATCH] Add pipeline behaviors for signing process Introduced four new pipeline behaviors (`AnnotationBehavior`, `DocStatusBehavior`, `HistoryBehavior`, and `SendSignedMailBehavior`) to modularize the signing process. These behaviors handle annotation persistence, document status creation, history recording, and signed mail notifications, respectively. - `AnnotationBehavior`: Saves annotations using `IRepository`. - `DocStatusBehavior`: Creates document status via `ISender`. - `HistoryBehavior`: Records signing history and validates receiver information. - `SendSignedMailBehavior`: Sends signed mail notifications using email templates and dynamic placeholder replacement. Added error handling for missing data in `HistoryBehavior` and `SendSignedMailBehavior`. Updated the signing pipeline to execute these behaviors sequentially, ensuring a structured and extensible workflow. --- .../Signature/Behaviors/AnnotationBehavior.cs | 40 ++++++ .../Signature/Behaviors/DocStatusBehavior.cs | 49 +++++++ .../Signature/Behaviors/HistoryBehavior.cs | 47 +++++++ .../Behaviors/SendSignedMailBehavior.cs | 122 ++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 EnvelopeGenerator.Application/Signature/Behaviors/AnnotationBehavior.cs create mode 100644 EnvelopeGenerator.Application/Signature/Behaviors/DocStatusBehavior.cs create mode 100644 EnvelopeGenerator.Application/Signature/Behaviors/HistoryBehavior.cs create mode 100644 EnvelopeGenerator.Application/Signature/Behaviors/SendSignedMailBehavior.cs diff --git a/EnvelopeGenerator.Application/Signature/Behaviors/AnnotationBehavior.cs b/EnvelopeGenerator.Application/Signature/Behaviors/AnnotationBehavior.cs new file mode 100644 index 00000000..00d08747 --- /dev/null +++ b/EnvelopeGenerator.Application/Signature/Behaviors/AnnotationBehavior.cs @@ -0,0 +1,40 @@ +using DigitalData.Core.Abstraction.Application.Repository; +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Signature.Commands; +using EnvelopeGenerator.Domain.Entities; +using MediatR; + +namespace EnvelopeGenerator.Application.Signature.Behaviors; + +/// +/// Pipeline behavior that saves annotations. +/// Executes first in the signing process. +/// +public class AnnotationBehavior : IPipelineBehavior +{ + private readonly IRepository _repo; + + /// + /// + /// + /// + public AnnotationBehavior(IRepository repository) + { + _repo = repository; + } + + /// + /// + /// + /// + /// + /// + /// + public async Task Handle(SignCommand request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot) + await _repo.CreateAsync(annot.Structured, cancellationToken); + + return await next(); + } +} diff --git a/EnvelopeGenerator.Application/Signature/Behaviors/DocStatusBehavior.cs b/EnvelopeGenerator.Application/Signature/Behaviors/DocStatusBehavior.cs new file mode 100644 index 00000000..666c4231 --- /dev/null +++ b/EnvelopeGenerator.Application/Signature/Behaviors/DocStatusBehavior.cs @@ -0,0 +1,49 @@ +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.DocStatus.Commands; +using EnvelopeGenerator.Application.Signature.Commands; +using EnvelopeGenerator.Domain.Constants; +using MediatR; +using System.Text.Json; + +namespace EnvelopeGenerator.Application.Signature.Behaviors; + +/// +/// Pipeline behavior that creates document status. +/// Executes second in the signing process. +/// +public class DocStatusBehavior : IPipelineBehavior +{ + private const string BlankAnnotationJson = "{}"; + + private readonly ISender _sender; + + /// + /// + /// + /// + public DocStatusBehavior(ISender sender) + { + _sender = sender; + } + + /// + /// + /// + /// + /// + /// + /// + public async Task Handle(SignCommand request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + await _sender.Send(new CreateDocStatusCommand() + { + EnvelopeId = request.EnvelopeReceiver.EnvelopeId, + ReceiverId = request.EnvelopeReceiver.ReceiverId, + Value = request.PsPdfKitAnnotation is PsPdfKitAnnotation annot + ? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations) + : BlankAnnotationJson + }, cancellationToken); + + return await next(); + } +} diff --git a/EnvelopeGenerator.Application/Signature/Behaviors/HistoryBehavior.cs b/EnvelopeGenerator.Application/Signature/Behaviors/HistoryBehavior.cs new file mode 100644 index 00000000..0ba0fc14 --- /dev/null +++ b/EnvelopeGenerator.Application/Signature/Behaviors/HistoryBehavior.cs @@ -0,0 +1,47 @@ +using EnvelopeGenerator.Application.Common.Extensions; +using EnvelopeGenerator.Application.Histories.Commands; +using EnvelopeGenerator.Application.Signature.Commands; +using EnvelopeGenerator.Domain.Constants; +using MediatR; + +namespace EnvelopeGenerator.Application.Signature.Behaviors; + +/// +/// Pipeline behavior that records history. +/// Executes third in the signing process. +/// +public class HistoryBehavior : IPipelineBehavior +{ + private readonly ISender _sender; + + /// + /// + /// + /// + public HistoryBehavior(ISender sender) + { + _sender = sender; + } + + /// + /// + /// + /// + /// + /// + /// + public async Task Handle(SignCommand request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + if (request.EnvelopeReceiver.Receiver is null) + throw new InvalidOperationException($"Receiver information is missing in the notification. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}"); + + await _sender.Send(new CreateHistoryCommand() + { + EnvelopeId = request.EnvelopeReceiver.EnvelopeId, + UserReference = request.EnvelopeReceiver.Receiver.EmailAddress, + Status = EnvelopeStatus.DocumentSigned, + }, cancellationToken); + + return await next(); + } +} diff --git a/EnvelopeGenerator.Application/Signature/Behaviors/SendSignedMailBehavior.cs b/EnvelopeGenerator.Application/Signature/Behaviors/SendSignedMailBehavior.cs new file mode 100644 index 00000000..7c2ec2c3 --- /dev/null +++ b/EnvelopeGenerator.Application/Signature/Behaviors/SendSignedMailBehavior.cs @@ -0,0 +1,122 @@ +using DigitalData.Core.Abstraction.Application.Repository; +using DigitalData.EmailProfilerDispatcher.Abstraction.Entities; +using EnvelopeGenerator.Application.Common.Configurations; +using EnvelopeGenerator.Application.Common.Extensions; +using EnvelopeGenerator.Application.Signature.Commands; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Domain.Entities; +using EnvelopeGenerator.Domain.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace EnvelopeGenerator.Application.Signature.Behaviors; + +/// +/// Pipeline behavior that sends signed mail notification. +/// Executes LAST in the signing process - only if all previous behaviors succeed. +/// +public class SendSignedMailBehavior : IPipelineBehavior +{ + private readonly IRepository _tempRepo; + private readonly IRepository _emailOutRepo; + private readonly MailParams _mailParams; + private readonly DispatcherParams _dispatcherParams; + + /// + /// + /// + /// + /// + /// + /// + public SendSignedMailBehavior( + IRepository tempRepo, + IRepository emailOutRepo, + IOptions mailParamsOptions, + IOptions dispatcherParamsOptions) + { + _tempRepo = tempRepo; + _emailOutRepo = emailOutRepo; + _mailParams = mailParamsOptions.Value; + _dispatcherParams = dispatcherParamsOptions.Value; + } + + /// + /// + /// + /// + /// + /// + /// + public async Task Handle(SignCommand request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var placeHolders = CreatePlaceHolders(request); + + var temp = await _tempRepo + .Where(x => x.Name == EmailTemplateType.DocumentSigned.ToString()) + .SingleOrDefaultAsync(cancellationToken) + ?? throw new InvalidOperationException($"Email template not found. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}"); + + temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, _mailParams.Placeholders); + temp.Body = ReplacePlaceHolders(temp.Body, placeHolders, _mailParams.Placeholders); + + var emailOut = new EmailOut + { + EmailAddress = request.EmailAddress, + EmailBody = temp.Body, + EmailSubj = temp.Subject, + AddedWhen = DateTime.Now, + AddedWho = _dispatcherParams.AddedWho, + SendingProfile = _dispatcherParams.SendingProfile, + ReminderTypeId = _dispatcherParams.ReminderTypeId, + EmailAttmt1 = _dispatcherParams.EmailAttmt1, + WfId = (int)EnvelopeStatus.MessageConfirmationSent, + ReferenceString = request.EmailAddress, + ReferenceId = request.EnvelopeReceiver.ReceiverId + }; + + await _emailOutRepo.CreateAsync(emailOut, cancellationToken); + + return await next(); + } + + private Dictionary CreatePlaceHolders(SignCommand request) + { + var placeHolders = new Dictionary() + { + { "[NAME_RECEIVER]", request.EnvelopeReceiver.Name ?? string.Empty }, + { "[DOCUMENT_TITLE]", request.EnvelopeReceiver.Envelope?.Title ?? string.Empty }, + }; + + if (request.EnvelopeReceiver.Envelope.IsReadAndConfirm()) + { + placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen"; + placeHolders["[DOCUMENT_PROCESS]"] = string.Empty; + placeHolders["[FINAL_STATUS]"] = "Lesebestätigung"; + placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt"; + placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!"; + placeHolders["[RECEIVER_ACTION]"] = "bestätigt"; + } + else + { + placeHolders["[SIGNATURE_TYPE]"] = "Signieren"; + placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben"; + placeHolders["[FINAL_STATUS]"] = "Signatur"; + placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet"; + placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen."; + placeHolders["[RECEIVER_ACTION]"] = "unterschrieben"; + } + + return placeHolders; + } + + private static string ReplacePlaceHolders(string text, params Dictionary[] placeHoldersList) + { + foreach (var placeHolders in placeHoldersList) + foreach (var ph in placeHolders) + text = text.Replace(ph.Key, ph.Value); + return text; + } +}