Compare commits

...

7 Commits

Author SHA1 Message Date
911c812b19 refactor(Annotation): change Id column type from int to bigint in Annotation entity 2025-10-21 11:45:23 +02:00
8ae0f79365 refactor(AnnotationDto): split AnnotationDto into AnnotationCreateDto and AnnotationDto
- Introduced AnnotationCreateDto for creation-specific properties
- AnnotationDto now inherits from AnnotationCreateDto and includes Id, AddedWhen, ChangedWhen, and ChangedWho
- Added Type property to AnnotationCreateDto
 - remove CreateAnnotationCommand
2025-10-21 11:42:01 +02:00
0ca54fe1fe feat(DocSignedNotification): replace Annotations with PsPdfKitAnnotation in DocSignedNotification
- Introduced new record `PsPdfKitAnnotation` to encapsulate both Instant and Structured annotation data
- Updated `DocSignedNotification` to use `PsPdfKitAnnotation` instead of `ExpandoObject Annotations`
- Modified extension methods to accept and map `PsPdfKitAnnotation`
- Added reference to `EnvelopeGenerator.Application.Annotations.Commands` for `CreateAnnotationCommand`
2025-10-21 10:11:36 +02:00
a1d6b5347f refactor(annotation): simplify mapSignature function to return a flat array
Reworked mapSignature to return a single flattened array combining formFields,
frames, and signatures instead of a nested object. This simplifies downstream
processing and improves readability.
2025-10-21 09:48:06 +02:00
6cc631111c refactor(annotation): simplify field mapping and return structured objects
Refactored the mapSignature function to:
- Return cleaner structured objects for formFields, frames, and signatures
- Include `type` and `value` properties in returned objects
- Remove direct mutation of field and annotation objects
- Improve readability and maintainability of data mapping logic
2025-10-21 09:38:47 +02:00
9d6074874f fix(annotation): correctly assign elementId for signature annotations
Previously, signature annotations did not include elementId mapping logic, which caused issues when linking annotations to their corresponding elements. This update adds logic to extract elementId from the nearest signature annotation (similar to frame annotations) to ensure proper association.
2025-10-21 09:25:01 +02:00
26bdb0806d feat(MappingProfile): add AutoMapper profile for CreateAnnotationCommand to Annotation 2025-10-21 09:09:56 +02:00
11 changed files with 86 additions and 197 deletions

View File

@@ -1,136 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Dynamic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Annotations.Commands;
/// <summary>
///
/// </summary>
public record CreateAnnotationCommand : EnvelopeReceiverQueryBase, IRequest<IEnumerable<Signature>>
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = null
};
/// <summary>
///
/// </summary>
[JsonIgnore]
public string PSPDFKitInstantJSON
{
set => PSPDFKitInstant = JsonSerializer.Deserialize<dynamic>(value, SerializerOptions) ?? new ExpandoObject();
}
/// <summary>
///
/// </summary>
[JsonIgnore]
public ExpandoObject PSPDFKitInstant { get; set; } = null!;
}
/// <summary>
///
/// </summary>
public static class CreateAnnotationCommandExtensions
{
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="envelopeKey"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static Task<IEnumerable<Signature>> CreateAnnotation(this ISender sender, string envelopeKey, CancellationToken cancel = default)
=> sender.Send(new CreateAnnotationCommand() { Key = envelopeKey }, cancel);
}
/// <summary>
///
/// </summary>
public class CreateAnnotationCommandHandler : IRequestHandler<CreateAnnotationCommand, IEnumerable<Signature>>
{
/// <summary>
///
/// </summary>
private readonly IRepository<Signature> _signRepo;
/// <summary>
///
/// </summary>
private readonly IRepository<Annotation> _annotRepo;
/// <summary>
///
/// </summary>
/// <param name="signRepo"></param>
/// <param name="annotRepo"></param>
public CreateAnnotationCommandHandler(IRepository<Signature> signRepo, IRepository<Annotation> annotRepo)
{
_signRepo = signRepo;
_annotRepo = annotRepo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<IEnumerable<Signature>> Handle(CreateAnnotationCommand request, CancellationToken cancel)
{
var query = _signRepo.Query;
#region Envelope Query
if (request.Envelope.Id is int envelopeId)
query = query.Where(annot => annot.Document.EnvelopeId == envelopeId);
if (request.Envelope.Uuid is string envelopeUuid)
query = query.Where(annot => annot.Document.Envelope!.Uuid == envelopeUuid);
#endregion
// Receiver Query
query = query.Where(request.Receiver);
var elements = await query.ToListAsync(cancel);
foreach (var element in elements)
{
var annots = ParsePSPDFKitInstant(request.PSPDFKitInstant, element.Id)
.Select(annot => new Annotation()
{
ElementId = element.Id,
Name = annot.Key,
Value = annot.Value,
AddedWhen = DateTime.UtcNow
});
element.Annotations = await _annotRepo.CreateAsync(annots, cancel);
}
return elements;
}
/// <summary>
///
/// </summary>
/// <param name="instant"></param>
/// <param name="elementId"></param>
/// <returns></returns>
public static Dictionary<string, string> ParsePSPDFKitInstant(ExpandoObject instant, int elementId)
{
Dictionary<string, string> annots = new();
// parse json here
return annots;
}
}

View File

@@ -3,13 +3,8 @@
/// <summary>
///
/// </summary>
public record AnnotationDto
public record AnnotationCreateDto
{
/// <summary>
///
/// </summary>
public long Id { get; init; }
/// <summary>
///
/// </summary>
@@ -25,6 +20,22 @@ public record AnnotationDto
/// </summary>
public string Value { get; init; } = null!;
/// <summary>
///
/// </summary>
public string Type { get; init; } = null!;
}
/// <summary>
///
/// </summary>
public record AnnotationDto : AnnotationCreateDto
{
/// <summary>
///
/// </summary>
public long Id { get; init; }
/// <summary>
///
/// </summary>

View File

@@ -51,6 +51,8 @@ public class MappingProfile : Profile
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
CreateMap<AnnotationCreateDto, Annotation>()
.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.UtcNow));
// Messaging mappings
// for GTX messaging

View File

@@ -1,12 +1,19 @@
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Newtonsoft.Json;
using System.Dynamic;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
/// <summary>
///
/// </summary>
/// <param name="Instant"></param>
/// <param name="Structured"></param>
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
/// <summary>
///
/// </summary>
@@ -16,7 +23,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
/// <summary>
///
/// </summary>
public required ExpandoObject Annotations { get; init; }
public PsPdfKitAnnotation PsPdfKitAnnotation { get; init; } = null!;
/// <summary>
///
@@ -40,17 +47,17 @@ public static class DocSignedNotificationExtensions
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
/// </summary>
/// <param name="dto">The DTO to convert.</param>
/// <param name="annotations"></param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, ExpandoObject annotations)
=> new(dto) { Annotations = annotations };
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
/// <summary>
/// Asynchronously converts a <see cref="Task{EnvelopeReceiverDto}"/> to a <see cref="DocSignedNotification"/>.
///
/// </summary>
/// <param name="dtoTask">The task that returns the DTO to convert.</param>
/// <param name="annotations"></param>
/// <returns>A task that represents the asynchronous conversion operation.</returns>
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, ExpandoObject annotations)
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { Annotations = annotations } : null;
/// <param name="dtoTask"></param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns></returns>
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation psPdfKitAnnotation)
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
}

View File

@@ -1,4 +1,5 @@
using EnvelopeGenerator.Application.Annotations.Commands;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
@@ -11,15 +12,15 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
/// <summary>
///
/// </summary>
private readonly ISender _sender;
private readonly IRepository<Annotation> _repo;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public AnnotationHandler(ISender sender)
/// <param name="repository"></param>
public AnnotationHandler(IRepository<Annotation> repository)
{
_sender = sender;
_repo = repository;
}
/// <summary>
@@ -28,11 +29,6 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateAnnotationCommand()
{
Envelope = new() { Id = notification.EnvelopeId },
Receiver = new() { Id = notification.ReceiverId },
PSPDFKitInstant = notification.Annotations
}, cancel);
}
public Task Handle(DocSignedNotification notification, CancellationToken cancel)
=> _repo.CreateAsync(notification.PsPdfKitAnnotation.Structured, cancel);
}

View File

@@ -33,7 +33,7 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{
Envelope = new() { Id = notification.EnvelopeId },
Receiver = new() { Id = notification.ReceiverId},
Value = JsonSerializer.Serialize(notification.Annotations, Format.Json.ForAnnotations)
Value = JsonSerializer.Serialize(notification.PsPdfKitAnnotation.Instant, Format.Json.ForAnnotations)
}, cancel);
}
}

View File

@@ -2,7 +2,6 @@
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Newtonsoft.Json;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;

View File

@@ -16,7 +16,7 @@ public class Annotation
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("GUID", TypeName = "int")]
[Column("GUID", TypeName = "bigint")]
public long Id { get; set; }
[Required]

View File

@@ -44,7 +44,7 @@ public class AnnotationController : ControllerBase
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost]
public async Task<IActionResult> CreateOrUpdate([FromBody] ExpandoObject annotations, CancellationToken cancel = default)
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation psPdfKitAnnotation, CancellationToken cancel = default)
{
// get claims
var signature = User.GetAuthReceiverSignature();
@@ -62,7 +62,7 @@ public class AnnotationController : ControllerBase
var docSignedNotification = await _mediator
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
.ToDocSignedNotification(annotations)
.ToDocSignedNotification(psPdfKitAnnotation)
?? throw new NotFoundException("Envelope receiver is not found.");
await _mediator.Publish(docSignedNotification, cancel);

View File

@@ -1,5 +1,4 @@
using EnvelopeGenerator.Application.Annotations.Commands;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -28,11 +27,4 @@ public class TestAnnotationController : ControllerBase
await _mediator.Publish(new RemoveSignatureNotification(uuid));
return Ok();
}
[HttpPost("{envelopeKey}")]
public async Task<IActionResult> Create([FromRoute] string envelopeKey, CancellationToken cancel)
{
var annot = await _mediator.CreateAnnotation(envelopeKey, cancel);
return Ok(annot);
}
}

View File

@@ -317,34 +317,52 @@ function fixBase64(escapedBase64) {
}
function mapSignature(iJSON) {
return {
formFields: iJSON.formFieldValues.filter(field => !field.name.includes("label")).map((field) => {
return [
// formFields
...iJSON.formFieldValues.filter(field => !field.name.includes("label")).map((field) => {
const nameParts = field.name.split('#');
field.elementId = Number(nameParts[2]);
field.name = nameParts[3];
return field;
return {
elementId: Number(nameParts[2]),
name: nameParts[3],
value: field.value,
type: field.type
};
}),
frames: iJSON.annotations.filter(annot => annot.description === 'FRAME').map((annot) => {
// frames
...iJSON.annotations.filter(annot => annot.description === 'FRAME').map((annot) => {
const preElement = findNearest(annot, e => e.bbox[0], e => e.bbox[1], ...iJSON.annotations.filter(field => field.id.includes("signature")));
const idPartsOfPre = preElement.id.split('#');
annot.elementId = Number(idPartsOfPre[2]);
annot.name = 'frame';
annot.value = fixBase64(iJSON.attachments[annot.imageAttachmentId]?.binary);
return annot;
return {
elementId: Number(idPartsOfPre[2]),
name: 'frame',
value: fixBase64(iJSON.attachments[annot.imageAttachmentId]?.binary),
type: annot.type
};
}),
signatures: iJSON.annotations.filter(annot => annot.isSignature).map(annot => {
// signatures
...iJSON.annotations.filter(annot => annot.isSignature).map(annot => {
const preElement = findNearest(annot, e => e.bbox[0], e => e.bbox[1], ...iJSON.annotations.filter(field => field.id.includes("signature")));
const idPartsOfPre = preElement.id.split('#');
let value;
if (annot.imageAttachmentId)
annot.value = iJSON.attachments[annot.imageAttachmentId]?.binary;
value = iJSON.attachments[annot.imageAttachmentId]?.binary;
else if (annot.lines && annot.strokeColor)
annot.value = JSON.stringify({
value = JSON.stringify({
lines: annot.lines,
strokeColor: annot.strokeColor
});
else
throw new Error("Signature mapping failed: The data structure from the third-party library is incompatible or missing required fields.");
annot.name = 'signature';
return annot;
return {
elementId: Number(idPartsOfPre[2]),
name: 'signature',
value,
type: annot.type
};
})
};
];
}