Compare commits

...

66 Commits

Author SHA1 Message Date
7af934ea19 Integrate DevExpress Reporting for envelope history
Added DevExpress.Reporting.Core package reference. Refactored rptEnvelopeHistory to inherit from XtraReport and build its layout programmatically using DevExpress bands, tables, and styling. The report now uses A4 paper size, custom margins, and DevExpress UI components for rendering.
2026-03-09 21:27:06 +01:00
b65367fb6d Refactor FinalizeDocumentJob dependencies and method call
Removed unused constructor parameters from FinalizeDocumentJob for cleaner dependency injection. Updated actionService.FinalizeEnvelope to accept a cancellation token. Added missing using directive for Microsoft.Extensions.Options.
2026-03-09 21:26:52 +01:00
25c31108cb Add EnvelopeReport repository dependency to ReportCreator
Updated ReportCreator constructor to require an IRepository<EnvelopeReport> parameter, enabling direct access to envelope report data within the class. This change prepares the class for future data operations involving envelope reports.
2026-03-09 21:26:19 +01:00
4083833b19 Add EnvelopeReports DbSet to EGDbContextBase
Added EnvelopeReports DbSet property to EGDbContextBase, enabling Entity Framework to manage and query EnvelopeReport entities.
2026-03-09 21:25:30 +01:00
f53bc65acf Add EnvelopeReport entity mapped to VWSIG_ENVELOPE_REPORT
Introduced the EnvelopeReport class in the Domain.Entities namespace, mapped to the "VWSIG_ENVELOPE_REPORT" table using EF Core data annotations. The entity includes properties for envelope ID, head UUID, title, message, status, timestamp, and user, with appropriate column mappings and validation attributes.
2026-03-09 21:25:15 +01:00
070d9be00c Remove obsolete State class and DI registration
Removed the deprecated State class and its properties from the codebase. Also removed State from the dependency injection configuration, as it is no longer used.
2026-03-09 16:52:52 +01:00
6f7b04a26e Refactor: remove strict checks and logging in finalization
Removed exception throwing and logging for failed report creation, email sending, and envelope finalization in FinalizeDocumentJob. Now, these methods are called without checking their return values. Also improved exception message for file export to include envelope Id and added null-forgiving operator to _config.ExportPath.
2026-03-09 16:31:31 +01:00
473358e2b9 Make ExportPath non-nullable in ConfigDto
Changed ExportPath from a nullable string to a non-nullable string in ConfigDto and initialized it with the null-forgiving operator. This ensures ExportPath always has a value and cannot be null.
2026-03-09 16:30:39 +01:00
8f3aa69cbf Refactor document save to use async repository update
Replaced manual SQL and file read with async repository update for saving final document bytes. Removed obsolete helper methods and cleaned up unused imports for improved maintainability and testability.
2026-03-09 16:18:20 +01:00
eededeb1f1 Remove SendFinalEmailWithAttachment and related logic
Removed the SendFinalEmailWithAttachment method and all references to it from SendFinalEmailToCreator and SendFinalEmailToReceivers. The logic for determining email attachments based on FinalEmailType is no longer used. Other functionality in these methods remains unchanged.
2026-03-09 15:41:56 +01:00
737774f077 Remove PDF annotation and envelope data methods
Removed BurnAnnotationsToPdf, GetEnvelopeData, and GetAnnotationData from FinalizeDocumentJob. These methods handled document reading, annotation retrieval, and PDF annotation burning. This change reflects a refactor or shift in document processing responsibilities.
2026-03-09 15:34:58 +01:00
69499273cc Refactor FinalizeDocumentJob to async and use docStatusRepo
Refactored FinalizeDocumentJob to make the Finalize method asynchronous and fetch document annotations from docStatusRepo instead of using GetEnvelopeData. Updated constructor to inject IRepository<Domain.Entities.DocumentStatus>. Improved logging and removed obsolete envelopeData checks.
2026-03-09 15:32:32 +01:00
ead33ab2e7 Refactor Envelope properties; add DefaultDocument property
Refactored property declarations in the Envelope class for improved readability by removing unnecessary line breaks and nullable preprocessor directives. Added a [NotMapped] DefaultDocument property to return the first document in the Documents list, or null if none exist.
2026-03-09 15:09:51 +01:00
d1e2840617 Clean up debug logging in FinalizeDocumentJob
Removed numerous debug-level log statements to reduce log verbosity and focus on warnings and information logs. Updated some log messages to use interpolated strings for clarity. Refactored GetAnnotationData to use C# collection expressions for improved code conciseness.
2026-03-09 11:35:49 +01:00
a39ef6a0e2 Improve error handling in FinalizeDocumentJob
Added RethrowOnError property to control exception rethrowing during envelope finalization. Exceptions are now logged as errors and, if RethrowOnError is true, rethrown to enable stricter error handling and halt execution on failure.
2026-03-09 11:30:43 +01:00
7d620988d8 Refactor envelope finalization into a private method
Extracted envelope finalization logic from the foreach loop into a new private Finalize(Envelope envelope) method. This improves code readability and maintainability by encapsulating all steps of the finalization process without changing functionality.
2026-03-09 11:25:36 +01:00
cc4a7d8c20 Refactor ActionService: DI, CancellationToken support
Refactored ActionService to use constructor injection for IRepository<History>. Updated all public methods to accept optional CancellationToken parameters for improved cancellation support. Added necessary using directives. Class remains a placeholder with [Obsolete] attributes and NotImplementedException.
2026-03-09 10:18:53 +01:00
0b8068f926 Add summary comment to ActionService for migration context
Added an XML summary comment to the ActionService class to indicate it is being migrated from EnvelopeGenerator.CommonServices.Services.ActionService. This provides clarity on the class's origin and intended purpose; no functional changes were made.
2026-03-09 10:12:32 +01:00
9fd7a68798 Refactor envelope processing for per-item error handling
Move per-envelope logic in FinalizeDocumentJob into its own try-catch block within the foreach loop. This ensures that exceptions in processing one envelope do not halt the processing of others, improving robustness and fault tolerance. Removed the outer try-catch-finally block and updated logging to reflect per-envelope and overall job status.
2026-03-09 09:42:55 +01:00
d6e2690bb8 Refactor envelope fetch to use EF LINQ instead of SQL
Replaced raw SQL and DataTable usage with Entity Framework LINQ queries for retrieving completed envelopes. The process now works directly with envelope entities, improving code readability, maintainability, and leveraging EF's querying capabilities. Logging and error handling have been updated to use envelope properties directly.
2026-03-09 09:38:08 +01:00
f04385a03c Remove EnvelopeModel class and its GetById method
Deleted the EnvelopeModel class, which included the GetById method for retrieving Envelope entities from the database. Also removed related using directives and namespace declarations. This cleanup eliminates unused or redundant code.
2026-03-06 14:53:14 +01:00
c88e7b2b9e Refactor FinalizeDocumentJob to use async repository access
Replaced EnvelopeModel with IRepository<Envelope> in FinalizeDocumentJob, switching to dependency-injected, repository-based, and asynchronous data access using Entity Framework. Updated envelope retrieval to use SingleOrDefaultAsync, improving maintainability and scalability. Added necessary using directives to support these changes.
2026-03-06 14:53:00 +01:00
0ee7ec82d6 Mark TempFiles class and members as obsolete
The TempFiles class, including its TempPath property, constructor,
and methods (Create, CleanUpFiles, CleanUp), is now marked with
the [Obsolete("Use memory cache instead of temp files.")] attribute.
This deprecates the use of temp files in favor of a memory cache
approach for future development.
2026-03-06 14:48:19 +01:00
a6d6dc8c4d Add expiration, reminder, and comment fields to EnvelopeDto
Expanded EnvelopeDto with new properties for expiration dates, reminder scheduling, notification options, and an optional comment field. These changes provide finer control over envelope lifecycle and metadata.
2026-03-06 14:43:59 +01:00
ab038df8b9 Update nullability for Title and Comment properties
Removed nullable annotation from Title, making it non-nullable.
Changed Comment to be nullable with conditional compilation
support for nullable reference types.
2026-03-06 14:43:27 +01:00
302249451b Remove ConstantsTests and add Domain folder to test project
Removed the ConstantsTests.cs file, which tested the Normalize
method for EnvelopeSigningType. Updated EnvelopeGenerator.Tests.csproj
to include a Domain folder for future test organization.
2026-03-06 14:16:22 +01:00
a4082fca45 Remove ConfigModel and DbConfig classes
Deleted ConfigModel.cs and DbConfig.cs, removing both the configuration loading logic and the DbConfig data structure from the codebase. This eliminates database-driven configuration management functionality.
2026-03-06 13:58:29 +01:00
f3ae8a9c49 Refactor config loading to async MediatR query in job
Switched FinalizeDocumentJob to use MediatR for async config retrieval, replacing direct model access. Updated _config type to ConfigDto?, injected IMediator, and removed obsolete DbConfig references. Cleaned up ExecuteAsync method for improved clarity and decoupling.
2026-03-06 13:58:16 +01:00
d6058c41d0 Remove DocumentPathOrigin from DbConfig and usages
DocumentPathOrigin property and all related code references have
been removed from DbConfig, ConfigModel, and FinalizeDocumentJob.
DocumentPath is now used exclusively for document path handling,
simplifying configuration and reducing redundancy.
2026-03-06 13:51:20 +01:00
79d093c492 Add new properties to ConfigDto for TFA and metadata
Expanded ConfigDto with properties for document path, timestamps, legacy GUID, and default TFA settings. Added XML documentation for each new property.
2026-03-06 13:22:44 +01:00
56c65b6fbb Add new properties to Config entity with mappings
Added DocumentPath, AddedWhen, ChangedWhen, Guid, DefTfaEnabled, and DefTfaWithPhone properties to the Config entity, each mapped to corresponding database columns with appropriate data types and attributes. Also included a conditional using directive for System under NETFRAMEWORK.
2026-03-06 13:22:22 +01:00
2af18842c4 Merge branch 'master' into feat/service-host 2026-03-06 13:06:38 +01:00
7c7674c822 Show "Confirmed by" or "Signed by" label conditionally
The signature label now displays "Confirmed by" if READ_AND_CONFIRM is true, otherwise "Signed by". This uses the appropriate localized string when available, defaulting to "Signed by" if not. Previously, only "Signed by" was shown.
2026-03-06 12:55:30 +01:00
65f606f573 Update finalize dialog text based on READ_AND_CONFIRM flag
The confirmation dialog when finalizing a document now displays
context-appropriate text depending on the READ_AND_CONFIRM flag.
If true, it shows localized.confirmAgree; otherwise, it shows
localized.sigAgree, improving clarity for different workflows.
2026-03-06 12:55:19 +01:00
41e0d4691b Add in-memory caching to ReadDefaultConfigQueryHandler
Injected IMemoryCache into ReadDefaultConfigQueryHandler and updated the Handle method to cache the default configuration for 30 minutes. This reduces database queries and improves performance for frequently accessed configuration data.
2026-03-06 11:54:50 +01:00
64e0a4f749 Centralize cache key definitions in CacheKey class
Refactored cache key usage by introducing a new static CacheKey class in the Application.Common namespace. Replaced the private DefaultConfigCacheId in ConfigService with CacheKey.DefaultConfig. Updated using statements accordingly. This change improves maintainability by centralizing cache key management and sets the stage for future cache key consolidation.
2026-03-06 11:41:47 +01:00
4cf54d36b9 Add EnforceSingleResult to config query and improve errors
Added EnforceSingleResult to ReadDefaultConfigQuery to allow strict single-result enforcement. Updated handler logic to use SingleOrDefaultAsync when requested. Replaced InvalidOperationException with NotFoundException and improved error messaging when no configuration is found.
2026-03-06 11:35:49 +01:00
020cecabf3 Add query/handler to fetch default config via MediatR
Introduced ReadDefaultConfigQuery and its handler to retrieve the application's default configuration using MediatR. The handler fetches the first Config entity from the repository, maps it to ConfigDto, and throws an exception if no configuration is found.
2026-03-06 11:08:03 +01:00
40c899e47e Merge branch 'master' into feat/service-host 2026-03-06 10:29:27 +01:00
0341505f8d Merge branch 'origin/bugfix/history-inconsistency' 2026-03-06 10:15:22 +01:00
d4eee1718e Bump version to 3.12.0 in project file
Updated <Version>, <AssemblyVersion>, and <FileVersion> fields in EnvelopeGenerator.Web.csproj from 3.11.0 to 3.12.0. No other changes were made.
2026-03-06 10:13:45 +01:00
9b042d8f45 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2026-03-06 09:54:26 +01:00
OlgunR
ad0c847172 Add SigningProcessTitle resource; rename rejection key
Added localized SigningProcessTitle entry to de-DE, en-US, and fr-FR resource files. Renamed RejectionInfoConfirmation to RejectionInfo1Confirmation. Removed duplicate SigningProcessTitle entries. No changes to localized values.
2026-03-06 09:53:18 +01:00
f6d57b1e38 Update progress bar to show Confirmations or Signatures
Progress bar label now displays "Confirmations" if isReadAndConfirm is true, otherwise it shows "Signatures". This improves clarity for users based on the envelope's required action.
2026-03-06 09:52:12 +01:00
b64d2b7478 Refactor: cache IsReadAndConfirm() result in variable
Store envelope.IsReadAndConfirm() in isReadAndConfirm variable to avoid redundant calls and improve code readability when setting ViewData["Title"]. No change to logic or behavior.
2026-03-06 09:50:33 +01:00
OlgunR
f8c7f60cf9 Add resource strings for document confirmation workflows
Added new localized strings for document confirmation processes in de-DE, en-US, and fr-FR resource files, including confirmation messages, UI labels, and process titles. Fixed a typo in the German resource and ensured consistency for the "SigningProcessTitle" key across languages.
2026-03-06 09:50:21 +01:00
44edef8ba1 Update envelope view title logic for confirm state
Refined the logic for setting ViewData["Title"] in ShowEnvelope.cshtml. Now, if the envelope requires read and confirm, the title displays "Confirm Document" instead of just "Sign Document" or "View Document", providing clearer context for users.
2026-03-06 09:43:16 +01:00
647c5d2353 Add localization extensions for confirmation resources
Added three new extension methods to the Extensions class in Resource.cs: ConfirmDoc, ConfirmAgree, and ConfirmationProcessTitle. Each method retrieves the localized value for its respective resource key and includes XML documentation, consistent with existing localization methods.
2026-03-06 09:31:19 +01:00
4ce1d2a370 Add RejectionInfo1ForConfirmation extension method
Introduced a new extension method, RejectionInfo1ForConfirmation, to the Extensions class in Resource.cs. This method retrieves the localized string for "RejectionInfo1ForConfirmation" from an IStringLocalizer instance, providing functionality similar to the existing RejectionInfo1 method.
2026-03-06 09:28:12 +01:00
bcc53bf9f1 Update rejection header for read and confirm envelopes
Add logic to display a specific rejection message when an envelope is rejected as part of a "read and confirm" process. The header now distinguishes between external, confirmation-related, and default rejection scenarios for improved user feedback.
2026-03-06 09:27:41 +01:00
f1e38e3bd3 Reverse IsReadAndConfirm logic for envelope status texts
Swapped the display logic for localized titles and messages based on the IsReadAndConfirm flag. Now, "confirmed" texts are shown when IsReadAndConfirm is true, and "signed" texts when false. This update ensures correct status messaging throughout the envelope confirmation flow.
2026-03-06 09:17:17 +01:00
e095860b17 Update EnvelopeSigned page to handle confirm vs sign
Add conditional logic to EnvelopeSigned.cshtml to display different headings and confirmation messages based on whether the document was signed or confirmed, using the IsReadAndConfirm flag. This improves user feedback by distinguishing between signing and confirming actions.
2026-03-06 01:25:07 +01:00
9cfc74aa88 Refactor title logic and fix localizer syntax in view
Refactored EnvelopeSigned.cshtml to use a local variable for IsReadAndConfirm when setting the page title, improving readability. Also updated Razor syntax for localizer calls in <h1> and <p> elements to ensure correct evaluation and formatting.
2026-03-06 01:23:06 +01:00
7cd6ca3a5f Update EnvelopeSigned view model and title logic
Set model to EnvelopeReceiverDto and import required types. Update ViewData["Title"] to use DocSigned or DocConfirmed based on Envelope.IsReadAndConfirm() result.
2026-03-06 01:20:47 +01:00
9b660cb25a Pass model to EnvelopeSigned view on already signed check
Previously, the "EnvelopeSigned" view was returned without a model when an envelope was already signed. Now, the er model object is passed to the view to provide additional context or data.
2026-03-06 01:20:25 +01:00
3d43d1896d Add DocConfirmed extension method to Resource.cs
Added a new DocConfirmed extension method to the Extensions class in Resource.cs. This method enables retrieval of the "DocConfirmed" localized string via IStringLocalizer, similar to the existing DocSigned method.
2026-03-06 01:20:12 +01:00
afa3694cd7 Add MapAddedWhen to History mapping in MappingProfile
Imported necessary namespaces and updated the CreateHistoryCommand-to-History mapping to include the MapAddedWhen extension method, likely to handle automatic setting of creation timestamps or similar metadata.
2026-02-27 11:34:17 +01:00
48f4ea0c50 Use UTC for AddedWhen timestamp in CreateHistoryCommand
Changed AddedWhen to use DateTime.UtcNow instead of DateTime.Now
to ensure timestamps are stored in UTC, improving consistency
across different time zones and deployment environments.
2026-02-27 11:33:53 +01:00
74c4ddda83 Remove ModifyDocStatusCommandBase and its properties
Deleted the entire ModifyDocStatusCommandBase.cs file, including the record definition, its properties (EnvelopeId, ReceiverId, Value), and the To<TDest>() mapping method. No code or class definitions remain in this file.
2026-02-27 11:32:45 +01:00
f9b1e583df Remove SaveDocStatusCommand and always create doc status
Removed SaveDocStatusCommand and its handler, eliminating logic for updating existing document statuses. DocStatusHandler now always sends CreateDocStatusCommand with simplified parameters when handling DocSignedNotification. This change ensures a new document status is always created instead of updating existing ones.
2026-02-27 11:31:41 +01:00
7c8e0d8481 Merge branch 'refactor/api-unification' into origin/bugfix/history-inconsistency 2026-02-27 11:21:10 +01:00
8258d9f43f Refactor UpdateDocStatusCommand to use generic base class
Refactored UpdateDocStatusCommand to inherit from a generic UpdateCommand base class with a new DocStatusUpdateDto record. Added explicit EnvelopeId, ReceiverId, and Value properties. Removed ChangedWhen property and implemented BuildQueryExpression for querying. Updated using directives and improved XML documentation.
2026-02-19 14:32:45 +01:00
01f3335238 Refactor ModifyDocStatusCommandBase properties
Replaced computed EnvelopeId and ReceiverId with virtual settable properties for greater flexibility. Removed Status, Receiver, and StatusChangedWhen properties. Made Value property virtual and cleaned up related code and comments.
2026-02-19 14:32:35 +01:00
1d0c758e00 Refactor CreateDocStatusCommand structure and properties
Refactored CreateDocStatusCommand to remove inheritance from ModifyDocStatusCommandBase and define its own properties. Added EnvelopeId, ReceiverId, and Value properties. Removed the AddedWhen property. The class now directly implements IRequest<DocumentStatus>.
2026-02-18 12:48:44 +01:00
711f7d12e8 Update MappingProfile for improved DocumentStatus mapping
Enhanced mapping logic in MappingProfile:
- Set Status to Created or Signed in CreateDocStatusCommand mapping based on Value property.
- Automatically set StatusChangedWhen to current UTC time in UpdateDocStatusCommand mapping.
2026-02-18 12:48:11 +01:00
43d89699a9 Add StatusChangedWhen to DocumentStatus entity
Added a nullable DateTime property, StatusChangedWhen, to the DocumentStatus entity. This property is required and mapped to the "STATUS_CHANGED_WHEN" column, enabling tracking of when the document status was last updated.
2026-02-18 12:47:23 +01:00
43 changed files with 663 additions and 580 deletions

View File

@@ -0,0 +1,13 @@
namespace EnvelopeGenerator.Application.Common;
// TODO: merge other cache keys here as well, e.g. for templates, etc.
/// <summary>
///
/// </summary>
public static class CacheKey
{
/// <summary>
///
/// </summary>
public static readonly Guid DefaultConfig = Guid.NewGuid();
}

View File

@@ -8,6 +8,11 @@ namespace EnvelopeGenerator.Application.Common.Dto;
[ApiExplorerSettings(IgnoreApi = true)]
public class ConfigDto
{
/// <summary>
/// Gets or sets the default document path.
/// </summary>
public string? DocumentPath { get; set; }
/// <summary>
/// Gets or sets the sending profile identifier.
/// </summary>
@@ -26,5 +31,30 @@ public class ConfigDto
/// <summary>
/// Gets or sets the path where exports will be saved.
/// </summary>
public string? ExportPath { get; set; }
public string ExportPath { get; set; } = null!;
/// <summary>
/// Gets or sets the creation timestamp.
/// </summary>
public DateTime AddedWhen { get; set; }
/// <summary>
/// Gets or sets the last update timestamp.
/// </summary>
public DateTime? ChangedWhen { get; set; }
/// <summary>
/// Gets or sets the legacy tinyint GUID field.
/// </summary>
public byte Guid { get; set; }
/// <summary>
/// Gets or sets whether default TFA is enabled.
/// </summary>
public bool DefTfaEnabled { get; set; }
/// <summary>
/// Gets or sets whether default TFA uses phone.
/// </summary>
public bool DefTfaWithPhone { get; set; }
}

View File

@@ -44,6 +44,16 @@ public record EnvelopeDto : IEnvelope
[TemplatePlaceholder("[MESSAGE]")]
public string Message { get; set; } = string.Empty;
/// <summary>
///
/// </summary>
public DateTime? ExpiresWhen { get; set; }
/// <summary>
///
/// </summary>
public DateTime? ExpiresWarningWhen { get; set; }
/// <summary>
///
/// </summary>
@@ -60,6 +70,11 @@ public record EnvelopeDto : IEnvelope
[TemplatePlaceholder("[DOCUMENT_TITLE]")]
public string Title { get; set; } = string.Empty;
/// <summary>
/// Default value is string.Empty
/// </summary>
public string? Comment { get; set; }
/// <summary>
///
/// </summary>
@@ -70,6 +85,21 @@ public record EnvelopeDto : IEnvelope
/// </summary>
public string Language { get; set; } = "de-DE";
/// <summary>
///
/// </summary>
public bool SendReminderEmails { get; set; }
/// <summary>
///
/// </summary>
public int? FirstReminderDays { get; set; }
/// <summary>
///
/// </summary>
public int? ReminderIntervalDays { get; set; }
/// <summary>
///
/// </summary>
@@ -90,6 +120,26 @@ public record EnvelopeDto : IEnvelope
/// </summary>
public bool UseAccessCode { get; set; } = true;
/// <summary>
///
/// </summary>
public int? FinalEmailToCreator { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToReceivers { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWhenDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWarningWhenDays { get; set; }
/// <summary>
///
/// </summary>

View File

@@ -29,15 +29,12 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
{
await _sender.Send(new SaveDocStatusCommand()
{
Envelope = new() { Id = notification.EnvelopeId },
Receiver = new() { Id = notification.ReceiverId},
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
EnvelopeId = notification.EnvelopeId,
ReceiverId = notification.ReceiverId,
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson
}, cancel);
}
}, cancel);
}

View File

@@ -0,0 +1,69 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
namespace EnvelopeGenerator.Application.Configuration.Queries;
/// <summary>
///
/// </summary>
public record ReadDefaultConfigQuery : IRequest<ConfigDto>
{
/// <summary>
///
/// </summary>
public bool EnforceSingleResult { get; init; }
}
/// <summary>
///
/// </summary>
public class ReadDefaultConfigQueryHandler : IRequestHandler<ReadDefaultConfigQuery, ConfigDto>
{
private readonly IRepository<Config> _repo;
private readonly IMapper _mapper;
private readonly IMemoryCache _cache;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
/// <param name="mapper"></param>
/// <param name="cache"></param>
public ReadDefaultConfigQueryHandler(IRepository<Config> repo, IMapper mapper, IMemoryCache cache)
{
_repo = repo;
_mapper = mapper;
_cache = cache;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<ConfigDto> Handle(ReadDefaultConfigQuery request, CancellationToken cancel)
{
var config = await _cache.GetOrCreateAsync(CacheKey.DefaultConfig, entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
return request.EnforceSingleResult
? _repo.Query.SingleOrDefaultAsync(cancel)
: _repo.Query.FirstOrDefaultAsync(cancel)
?? throw new NotFoundException("Default configuration could not be found. Ensure at least one configuration record exists in the database.");
});
return _mapper.Map<ConfigDto>(config);
}
}

View File

@@ -8,12 +8,22 @@ namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary>
///
/// </summary>
public record CreateDocStatusCommand : ModifyDocStatusCommandBase, IRequest<DocumentStatus>
public record CreateDocStatusCommand : IRequest<DocumentStatus>
{
/// <summary>
/// Gets timestamp when this record was added. Returns the StatusChangedWhen value.
///
/// </summary>
public DateTime AddedWhen => StatusChangedWhen;
public int EnvelopeId { get; set; }
/// <summary>
///
/// </summary>
public int ReceiverId { get; set; }
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
}
/// <summary>

View File

@@ -1,54 +0,0 @@
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary>
///
/// </summary>
public record ModifyDocStatusCommandBase : EnvelopeReceiverQueryBase
{
/// <summary>
///
/// </summary>
public int? EnvelopeId => Envelope.Id;
/// <summary>
///
/// </summary>
public int? ReceiverId => Receiver.Id;
/// <summary>
///
/// </summary>
public override ReceiverQueryBase Receiver { get => base.Receiver; set => base.Receiver = value; }
/// <summary>
/// Gets the current status code.
/// </summary>
public DocumentStatus Status => Value is null ? DocumentStatus.Created : DocumentStatus.Signed;
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
/// <summary>
/// Gets timestamp when this record was added.
/// </summary>
public DateTime StatusChangedWhen { get; } = DateTime.Now;
/// <summary>
/// Maps the current command to a new instance of the specified type.
/// </summary>
/// <typeparam name="TDest"></typeparam>
/// <returns></returns>
public TDest To<TDest>() where TDest : ModifyDocStatusCommandBase, new()
=> new()
{
Key = Key,
Envelope = Envelope,
Receiver = Receiver,
Value = Value
};
}

View File

@@ -1,77 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using AutoMapper;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary>
/// Represents a command to save the status of a document, either by creating a new status or updating an existing one based on the provided envelope and receiver identifiers.
/// It returns the identifier of the saved document status.
/// </summary>
public record SaveDocStatusCommand : ModifyDocStatusCommandBase, IRequest<DocumentStatusDto?>;
/// <summary>
///
/// </summary>
public class SaveDocStatusCommandHandler : IRequestHandler<SaveDocStatusCommand, DocumentStatusDto?>
{
private readonly IMapper _mapper;
private readonly IRepository<DocumentStatus> _repo;
private readonly IRepository<Envelope> _envRepo;
private readonly IRepository<Receiver> _rcvRepo;
/// <summary>
///
/// </summary>
/// <param name="mapper"></param>
/// <param name="repo"></param>
/// <param name="rcvRepo"></param>
/// <param name="envRepo"></param>
public SaveDocStatusCommandHandler(IMapper mapper, IRepository<DocumentStatus> repo, IRepository<Receiver> rcvRepo, IRepository<Envelope> envRepo)
{
_mapper = mapper;
_repo = repo;
_rcvRepo = rcvRepo;
_envRepo = envRepo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<DocumentStatusDto?> Handle(SaveDocStatusCommand request, CancellationToken cancel)
{
// ceck if exists
bool isExists = await _repo.ReadOnly().Where(request).AnyAsync(cancel);
var env = await _envRepo.ReadOnly().Where(request.Envelope).FirstAsync(cancel);
var rcv = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
request.Envelope.Id = env.Id;
request.Receiver.Id = rcv.Id;
if (isExists)
{
var uReq = request.To<UpdateDocStatusCommand>();
await _repo.UpdateAsync(uReq, q => q.Where(request), cancel);
}
else
{
var cReq = request.To<CreateDocStatusCommand>();
await _repo.CreateAsync(cReq, cancel);
}
var docStatus = await _repo.ReadOnly().Where(request).SingleOrDefaultAsync(cancel);
return _mapper.Map<DocumentStatusDto>(docStatus);
}
}

View File

@@ -1,14 +1,41 @@
using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Application.Common.Commands;
using EnvelopeGenerator.Domain.Entities;
using System.Linq.Expressions;
namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary>
///
/// </summary>
public record UpdateDocStatusCommand : ModifyDocStatusCommandBase
/// <param name="Value"></param>
public record DocStatusUpdateDto(string? Value);
/// <summary>
///
/// </summary>
public record UpdateDocStatusCommand : UpdateCommand<DocStatusUpdateDto, DocumentStatus>
{
/// <summary>
/// Gets timestamp when this record was added. Returns the StatusChangedWhen value.
///
/// </summary>
public DateTime? ChangedWhen => StatusChangedWhen;
public int EnvelopeId { get; set; }
/// <summary>
///
/// </summary>
public int ReceiverId { get; set; }
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
/// <summary>
///
/// </summary>
/// <returns></returns>
public override Expression<Func<DocumentStatus, bool>> BuildQueryExpression()
{
return ds => ds.EnvelopeId == EnvelopeId && ds.ReceiverId == ReceiverId;
}
}

View File

@@ -18,11 +18,16 @@ public class MappingProfile : Profile
CreateMap<CreateDocStatusCommand, DocumentStatus>()
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
.ForMember(dest => dest.Receiver, opt => opt.Ignore())
.ForMember(dest => dest.Status, opt => opt.MapFrom(
src => src.Value == null
? Domain.Constants.DocumentStatus.Created
: Domain.Constants.DocumentStatus.Signed))
.MapAddedWhen();
CreateMap<UpdateDocStatusCommand, DocumentStatus>()
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
.ForMember(dest => dest.Receiver, opt => opt.Ignore())
.ForMember(dest => dest.StatusChangedWhen, opt => opt.MapFrom(src => DateTime.UtcNow))
.MapChangedWhen();
}
}

View File

@@ -34,7 +34,7 @@ public record CreateHistoryCommand : EnvelopeReceiverQueryBase, IRequest<History
/// <summary>
///
/// </summary>
public DateTime AddedWhen { get; } = DateTime.Now;
public DateTime AddedWhen { get; } = DateTime.UtcNow;
/// <summary>
///

View File

@@ -1,4 +1,5 @@
using AutoMapper;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain.Entities;
@@ -17,6 +18,7 @@ public class MappingProfile: Profile
CreateMap<CreateHistoryCommand, History>()
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
.ForMember(dest => dest.Sender, opt => opt.Ignore())
.ForMember(dest => dest.Receiver, opt => opt.Ignore());
.ForMember(dest => dest.Receiver, opt => opt.Ignore())
.MapAddedWhen();
}
}

View File

@@ -190,6 +190,13 @@ public static class Extensions
/// <returns></returns>
public static string SignDoc(this IStringLocalizer localizer) => localizer[nameof(SignDoc)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ConfirmDoc(this IStringLocalizer localizer) => localizer[nameof(ConfirmDoc)].Value;
/// <summary>
///
/// </summary>
@@ -204,6 +211,13 @@ public static class Extensions
/// <returns></returns>
public static string DocSigned(this IStringLocalizer localizer) => localizer[nameof(DocSigned)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DocConfirmed(this IStringLocalizer localizer) => localizer[nameof(DocConfirmed)].Value;
/// <summary>
///
/// </summary>
@@ -239,6 +253,13 @@ public static class Extensions
/// <returns></returns>
public static string SigAgree(this IStringLocalizer localizer) => localizer[nameof(SigAgree)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ConfirmAgree(this IStringLocalizer localizer) => localizer[nameof(ConfirmAgree)].Value;
/// <summary>
///
/// </summary>
@@ -267,6 +288,13 @@ public static class Extensions
/// <returns></returns>
public static string RejectionInfo1(this IStringLocalizer localizer) => localizer[nameof(RejectionInfo1)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string RejectionInfo1ForConfirmation(this IStringLocalizer localizer) => localizer[nameof(RejectionInfo1ForConfirmation)].Value;
/// <summary>
///
/// </summary>
@@ -295,6 +323,13 @@ public static class Extensions
/// <returns></returns>
public static string SigningProcessTitle(this IStringLocalizer localizer) => localizer[nameof(SigningProcessTitle)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ConfirmationProcessTitle(this IStringLocalizer localizer) => localizer[nameof(ConfirmationProcessTitle)].Value;
/// <summary>
///
/// </summary>

View File

@@ -250,7 +250,7 @@
<value>Sie können bei Bedarf mit {0}, &lt;a href="mailto:{1}?subject={2}&amp;body=Sehr geehrte(r)%20{0},%0A%0A%0A"&gt;{1}&lt;/a&gt; Kontakt aufnehmen.</value>
</data>
<data name="RejectionInfo2_ext" xml:space="preserve">
<value>Das Vorgang wurde von einer der beteiligten Parteien abgelehnt. Sie können bei Bedarf mit {0}, &lt;a href="mailto:{1}?subject={2}&amp;body=Sehr geehrte(r)%20{0},%0A%0A%0A"&gt;{1}&lt;/a&gt; Kontakt aufnehmen.</value>
<value>Der Vorgang wurde von einer der beteiligten Parteien abgelehnt. Sie können bei Bedarf mit {0}, &lt;a href="mailto:{1}?subject={2}&amp;body=Sehr geehrte(r)%20{0},%0A%0A%0A"&gt;{1}&lt;/a&gt; Kontakt aufnehmen.</value>
</data>
<data name="RejectionReasonQ" xml:space="preserve">
<value>Bitte geben Sie einen Grund an:</value>
@@ -447,4 +447,34 @@
<data name="DocumentReset" xml:space="preserve">
<value>Dokument wurde zurückgesetzt.</value>
</data>
<data name="DocumentSuccessfullyConfirmed" xml:space="preserve">
<value>Dokument erfolgreich gelesen und bestätigt!</value>
</data>
<data name="DocumentConfirmedConfirmationMessage" xml:space="preserve">
<value>Sie haben das Dokument gelesen und bestätigt. Im Anschluss erhalten Sie eine schriftliche Bestätigung.</value>
</data>
<data name="Confirm" xml:space="preserve">
<value>Bestätigen</value>
</data>
<data name="RejectionInfo1Confirmation" xml:space="preserve">
<value>Dieser Bestätigungsvorgang wurde abgelehnt!</value>
</data>
<data name="ConfirmDoc" xml:space="preserve">
<value>Dokument bestätigen</value>
</data>
<data name="DocConfirmed" xml:space="preserve">
<value>Dokument bestätigt</value>
</data>
<data name="ConfirmAgree" xml:space="preserve">
<value>Durch Klick auf Abschließen bestätige ich, das Dokument gelesen und zur Kenntnis genommen zu haben.</value>
</data>
<data name="ConfirmedBy" xml:space="preserve">
<value>Bestätigt von</value>
</data>
<data name="ConfirmationProcessTitle" xml:space="preserve">
<value>Titel des Lesebetätigungs-Vorgangs</value>
</data>
<data name="Confirmations" xml:space="preserve">
<value>Bestätigungen</value>
</data>
</root>

View File

@@ -447,4 +447,34 @@
<data name="DocumentReset" xml:space="preserve">
<value>Document has been reset.</value>
</data>
<data name="DocumentSuccessfullyConfirmed" xml:space="preserve">
<value>Document successfully red and confirmed!</value>
</data>
<data name="DocumentConfirmedConfirmationMessage" xml:space="preserve">
<value>You have read and confirmed the document. You will receive a written confirmation afterwards.</value>
</data>
<data name="Confirm" xml:space="preserve">
<value>Confirm</value>
</data>
<data name="RejectionInfo1Confirmation" xml:space="preserve">
<value>This confirmation process has been rejected!</value>
</data>
<data name="ConfirmDoc" xml:space="preserve">
<value>Confirm Document</value>
</data>
<data name="DocConfirmed" xml:space="preserve">
<value>Document confirmed</value>
</data>
<data name="ConfirmAgree" xml:space="preserve">
<value>By clicking on “Complete”, I confirm that I have read and taken note of the document.</value>
</data>
<data name="ConfirmedBy" xml:space="preserve">
<value>Confirmed by</value>
</data>
<data name="ConfirmationProcessTitle" xml:space="preserve">
<value>Title of the read confirmation process</value>
</data>
<data name="Confirmations" xml:space="preserve">
<value>Confirmations</value>
</data>
</root>

View File

@@ -447,4 +447,34 @@
<data name="DocumentReset" xml:space="preserve">
<value>Le document a été réinitialisé.</value>
</data>
<data name="DocumentSuccessfullyConfirmed" xml:space="preserve">
<value>Document lu et confirmé avec succès !</value>
</data>
<data name="DocumentConfirmedConfirmationMessage" xml:space="preserve">
<value>Vous avez lu et confirmé le document. Vous recevrez une confirmation écrite par la suite.</value>
</data>
<data name="Confirm" xml:space="preserve">
<value>Confirmer</value>
</data>
<data name="RejectionInfo1Confirmation" xml:space="preserve">
<value>Cette procédure de confirmation a été rejetée !</value>
</data>
<data name="ConfirmDoc" xml:space="preserve">
<value>Confirmer le document</value>
</data>
<data name="DocConfirmed" xml:space="preserve">
<value>Document confirmé</value>
</data>
<data name="ConfirmAgree" xml:space="preserve">
<value>En cliquant sur « Terminer », je confirme avoir lu et pris connaissance du document.</value>
</data>
<data name="ConfirmedBy" xml:space="preserve">
<value>Confirmé par</value>
</data>
<data name="ConfirmationProcessTitle" xml:space="preserve">
<value>Titre de la procédure de confirmation de lecture</value>
</data>
<data name="Confirmations" xml:space="preserve">
<value>Confirmations</value>
</data>
</root>

View File

@@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common;
namespace EnvelopeGenerator.Application.Services;
@@ -16,8 +17,6 @@ namespace EnvelopeGenerator.Application.Services;
[Obsolete("Use MediatR")]
public class ConfigService : ReadService<IConfigRepository, ConfigDto, Config, int>, IConfigService
{
private static readonly Guid DefaultConfigCacheId = Guid.NewGuid();
private readonly IMemoryCache _cache;
private readonly ILogger<ConfigService> _logger;
@@ -62,7 +61,7 @@ public class ConfigService : ReadService<IConfigRepository, ConfigDto, Config, i
/// </exception>
public async Task<ConfigDto> ReadDefaultAsync()
{
var config = await _cache.GetOrCreateAsync(DefaultConfigCacheId, _ => ReadFirstAsync().ThenAsync(
var config = await _cache.GetOrCreateAsync(CacheKey.DefaultConfig, _ => ReadFirstAsync().ThenAsync(
Success: config => config,
Fail: (mssg, ntc) =>
{

View File

@@ -1,11 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
#if NETFRAMEWORK
using System;
#endif
namespace EnvelopeGenerator.Domain.Entities
{
[Table("TBSIG_CONFIG", Schema = "dbo")]
public class Config
{
[Column("DOCUMENT_PATH", TypeName = "nvarchar(256)")]
public string DocumentPath { get; set; }
[Column("SENDING_PROFILE", TypeName = "int")]
[Required]
public int SendingProfile { get; set; }
@@ -19,5 +25,24 @@ namespace EnvelopeGenerator.Domain.Entities
[Column("EXPORT_PATH", TypeName = "nvarchar(256)")]
public string ExportPath { get; set; }
[Column("ADDED_WHEN", TypeName = "datetime")]
[Required]
public DateTime AddedWhen { get; set; }
[Column("CHANGED_WHEN", TypeName = "datetime")]
public DateTime? ChangedWhen { get; set; }
[Column("GUID", TypeName = "tinyint")]
[Required]
public byte Guid { get; set; }
[Column("DEF_TFA_ENABLED", TypeName = "bit")]
[Required]
public bool DefTfaEnabled { get; set; }
[Column("DEF_TFA_WITH_PHONE", TypeName = "bit")]
[Required]
public bool DefTfaWithPhone { get; set; }
}
}

View File

@@ -35,6 +35,10 @@ namespace EnvelopeGenerator.Domain.Entities
[Column("STATUS")]
public Constants.DocumentStatus Status { get; set; }
[Required]
[Column("STATUS_CHANGED_WHEN", TypeName = "datetime")]
public DateTime? StatusChangedWhen { get; set; }
[Required]
[Column("ADDED_WHEN", TypeName = "datetime")]
public DateTime AddedWhen { get; set; }

View File

@@ -76,11 +76,14 @@ namespace EnvelopeGenerator.Domain.Entities
#if nullable
?
#endif
Title
{ get; set; }
Title { get; set; }
[Column("COMMENT", TypeName = "nvarchar(128)")]
public string Comment { get; set; }
public string
#if nullable
?
#endif
Comment { get; set; }
[Column("CONTRACT_TYPE")]
public int? ContractType { get; set; }
@@ -151,8 +154,7 @@ namespace EnvelopeGenerator.Domain.Entities
#if nullable
?
#endif
Type
{ get; set; }
Type { get; set; }
#if NETFRAMEWORK
[NotMapped]
@@ -166,22 +168,26 @@ namespace EnvelopeGenerator.Domain.Entities
#if nullable
?
#endif
Documents
{ get; set; }
Documents { get; set; }
[NotMapped]
public Document
#if nullable
?
#endif
DefaultDocument => Documents?.FirstOrDefault();
public List<History>
#if nullable
?
#endif
Histories
{ get; set; }
Histories { get; set; }
public List<EnvelopeReceiver>
#if nullable
?
#endif
EnvelopeReceivers
{ get; set; }
EnvelopeReceivers { get; set; }
//#if NETFRAMEWORK
/// <summary>

View File

@@ -0,0 +1,36 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EnvelopeGenerator.Domain.Entities
{
[Table("VWSIG_ENVELOPE_REPORT", Schema = "dbo")]
public class EnvelopeReport
{
[Column("ENVELOPE_ID", TypeName = "int")]
[Required]
public int EnvelopeId { get; set; }
[Column("HEAD_UUID", TypeName = "nvarchar(36)")]
[Required]
public string HeadUuid { get; set; }
[Column("HEAD_TITLE", TypeName = "nvarchar(128)")]
public string HeadTitle { get; set; }
[Column("HEAD_MESSAGE", TypeName = "nvarchar(max)")]
[Required]
public string HeadMessage { get; set; }
[Column("POS_STATUS", TypeName = "int")]
[Required]
public int PosStatus { get; set; }
[Column("POS_WHEN", TypeName = "datetime")]
public DateTime? PosWhen { get; set; }
[Column("POS_WHO", TypeName = "nvarchar(128)")]
[Required]
public string PosWho { get; set; }
}
}

View File

@@ -79,6 +79,8 @@ public abstract class EGDbContextBase : DbContext
public DbSet<ClientUser> ClientUsers { get; set; }
public DbSet<EnvelopeReport> EnvelopeReports { get; set; }
private readonly DbTriggerParams _triggers;
private readonly ILogger

View File

@@ -17,6 +17,7 @@
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="DevExpress.Reporting.Core" Version="24.2.*" />
</ItemGroup>
<ItemGroup>

View File

@@ -18,7 +18,6 @@ public static class DependencyInjection
services.AddScoped<PDFBurner>();
services.AddScoped<PDFMerger>();
services.AddScoped<ReportModel>();
services.AddSingleton<State>();
services.AddScoped<MSSQLServer>();
//TODO: Check lifetime of services. They might be singleton or scoped.

View File

@@ -1,30 +1,34 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.ServiceHost.Jobs;
/// <summary>
/// migrate from EnvelopeGenerator.CommonServices.Services.ActionService
/// </summary>
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
public class ActionService
public class ActionService(IRepository<History> histRepo)
{
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
public bool CreateReport(Envelope envelope)
public bool CreateReport(Envelope envelope, CancellationToken cancel = default)
{
throw new NotImplementedException();
}
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
public bool FinalizeEnvelope(Envelope envelope)
public bool FinalizeEnvelope(Envelope envelope, CancellationToken cancel = default)
{
throw new NotImplementedException();
}
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
public bool CompleteEnvelope(Envelope envelope)
public bool CompleteEnvelope(Envelope envelope, CancellationToken cancel = default)
{
throw new NotImplementedException();
}
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
public bool CompleteEnvelope(Envelope envelope, Receiver receiver)
public bool CompleteEnvelope(Envelope envelope, Receiver receiver, CancellationToken cancel = default)
{
throw new NotImplementedException();
}

View File

@@ -1,34 +0,0 @@
using DigitalData.Modules.Database;
using EnvelopeGenerator.ServiceHost.Extensions;
namespace EnvelopeGenerator.ServiceHost.Jobs;
public class ConfigModel(MSSQLServer Database, ILogger Logger)
{
public DbConfig LoadConfiguration()
{
try
{
const string sql = "SELECT TOP 1 * FROM TBSIG_CONFIG";
var table = Database.GetDatatable(sql);
var row = table.Rows[0];
return new DbConfig
{
DocumentPath = row.ItemEx("DOCUMENT_PATH", string.Empty),
DocumentPathOrigin = row.ItemEx("DOCUMENT_PATH", string.Empty),
ExportPath = row.ItemEx("EXPORT_PATH", string.Empty),
SendingProfile = row.ItemEx("SENDING_PROFILE", 0),
SignatureHost = row.ItemEx("SIGNATURE_HOST", string.Empty),
ExternalProgramName = row.ItemEx("EXTERNAL_PROGRAM_NAME", string.Empty),
Default_Tfa_Enabled = row.ItemEx("DEF_TFA_ENABLED", false),
Default_Tfa_WithPhone = row.ItemEx("DEF_TFA_WITH_PHONE", false)
};
}
catch (Exception ex)
{
Logger.LogError(ex);
return new DbConfig();
}
}
}

View File

@@ -1,13 +0,0 @@
namespace EnvelopeGenerator.ServiceHost.Jobs;
public class DbConfig
{
public string ExternalProgramName { get; set; } = "signFLOW";
public string DocumentPathOrigin { get; set; } = string.Empty;
public string DocumentPath { get; set; } = string.Empty;
public string ExportPath { get; set; } = string.Empty;
public int SendingProfile { get; set; }
public string SignatureHost { get; set; } = string.Empty;
public bool Default_Tfa_Enabled { get; set; }
public bool Default_Tfa_WithPhone { get; set; }
}

View File

@@ -1,38 +0,0 @@
using DigitalData.Modules.Database;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.ServiceHost.Extensions;
namespace EnvelopeGenerator.ServiceHost.Jobs;
public class EnvelopeModel(MSSQLServer Database, ILogger Logger)
{
public Envelope? GetById(int envelopeId)
{
try
{
var sql = $"SELECT * FROM [dbo].[TBSIG_ENVELOPE] WHERE GUID = {envelopeId}";
var table = Database.GetDatatable(sql);
var row = table.Rows.Cast<System.Data.DataRow>().SingleOrDefault();
if (row is null)
{
return null;
}
return new Envelope
{
Id = row.ItemEx("GUID", 0),
Uuid = row.ItemEx("ENVELOPE_UUID", string.Empty),
FinalEmailToCreator = row.ItemEx("FINAL_EMAIL_TO_CREATOR", 0),
FinalEmailToReceivers = row.ItemEx("FINAL_EMAIL_TO_RECEIVERS", 0),
UserId = row.ItemEx("USER_ID", 0),
User = null!,
EnvelopeReceivers = new List<EnvelopeReceiver>()
};
}
catch (Exception ex)
{
Logger.LogError(ex);
return null;
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Data;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.ServiceHost.Exceptions;
@@ -7,7 +8,7 @@ using EnvelopeGenerator.ServiceHost.Extensions;
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
[Obsolete("Instead of ReportModel create and use EnvelopeReport mediator queries")]
public class ReportCreator(ReportModel ReportModel, ILogger<ReportCreator> Logger)
public class ReportCreator(ReportModel ReportModel, ILogger<ReportCreator> Logger, IRepository<EnvelopeReport> reportRepo)
{
[Obsolete("Solve the spaghetti...")]
private Envelope? _envelope;

View File

@@ -1,9 +1,94 @@
using DevExpress.XtraReports.UI;
using DevExpress.Drawing;
using System.Drawing;
using System.IO;
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
public class rptEnvelopeHistory
public class rptEnvelopeHistory : XtraReport
{
public rptEnvelopeHistory()
{
BuildLayout();
}
private void BuildLayout()
{
Bands.AddRange(
[
new TopMarginBand(),
new BottomMarginBand(),
new ReportHeaderBand { HeightF = 40 },
new PageHeaderBand { HeightF = 24 },
new DetailBand { HeightF = 22 }
]);
var reportHeader = (ReportHeaderBand)Bands[nameof(ReportHeaderBand)]!;
var pageHeader = (PageHeaderBand)Bands[nameof(PageHeaderBand)]!;
var detail = (DetailBand)Bands[nameof(DetailBand)]!;
var title = new XRLabel
{
BoundsF = new RectangleF(0, 0, 750, 28),
Font = new DXFont("Segoe UI", 14, DXFontStyle.Bold),
Text = "Envelope History"
};
reportHeader.Controls.Add(title);
var headerTable = CreateTable(isHeader: true);
pageHeader.Controls.Add(headerTable);
var detailTable = CreateTable(isHeader: false);
detail.Controls.Add(detailTable);
Margins = new DXMargins(40, 40, 40, 40);
PaperKind = (DevExpress.Drawing.Printing.DXPaperKind)System.Drawing.Printing.PaperKind.A4;
}
private static XRTable CreateTable(bool isHeader)
{
var table = new XRTable
{
BoundsF = new RectangleF(0, 0, 750, 22),
Borders = DevExpress.XtraPrinting.BorderSide.All
};
var row = new XRTableRow();
if (isHeader)
{
row.Font = new DXFont("Segoe UI", 9, DXFontStyle.Bold);
row.BackColor = Color.Gainsboro;
row.Cells.Add(CreateCell("Date"));
row.Cells.Add(CreateCell("Status"));
row.Cells.Add(CreateCell("User"));
row.Cells.Add(CreateCell("Title"));
row.Cells.Add(CreateCell("Subject"));
}
else
{
row.Font = new DXFont("Segoe UI", 9, DXFontStyle.Regular);
row.Cells.Add(CreateCell("[ItemDate]"));
row.Cells.Add(CreateCell("[ItemStatusTranslated]"));
row.Cells.Add(CreateCell("[ItemUserReference]"));
row.Cells.Add(CreateCell("[EnvelopeTitle]"));
row.Cells.Add(CreateCell("[EnvelopeSubject]"));
}
table.Rows.Add(row);
return table;
}
private static XRTableCell CreateCell(string textOrExpression)
{
return new XRTableCell
{
Text = textOrExpression,
Padding = new DevExpress.XtraPrinting.PaddingInfo(4, 4, 2, 2),
CanGrow = false
};
}
public object? DataSource { get; set; }
public string? DataMember { get; set; }

View File

@@ -1,22 +1,29 @@
using System.Data;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Modules.Database;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Configuration.Queries;
using EnvelopeGenerator.Application.Envelopes.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.ServiceHost.Exceptions;
using EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
using GdPicture14;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using EnvelopeGenerator.ServiceHost.Extensions;
using EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
using GdPicture.Internal.MSOfficeBinary.translator.Spreadsheet.XlsFileFormat.Records;
using GdPicture14;
using MediatR;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Data;
namespace EnvelopeGenerator.ServiceHost.Jobs;
[Obsolete("ActionService is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
public class FinalizeDocumentJob(IOptions<WorkerOptions> options, IConfiguration config, ILogger<FinalizeDocumentJob> logger, TempFiles tempFiles, ActionService actionService, PDFBurner pdfBurner, PDFMerger pdfMerger, ReportCreator reportCreator, ConfigModel _configModel, EnvelopeModel _envelopeModel, ReportModel _reportModel, MSSQLServer _database, GdViewer? _gdViewer, LicenseManager licenseManager)
public class FinalizeDocumentJob(IOptions<WorkerOptions> options, ILogger<FinalizeDocumentJob> logger, TempFiles tempFiles, ActionService actionService, PDFBurner pdfBurner, PDFMerger pdfMerger, ReportCreator reportCreator, GdViewer? _gdViewer, LicenseManager licenseManager, IMediator mediator, IRepository<Envelope> envRepo, IRepository<Domain.Entities.DocumentStatus> docStatusRepo)
{
private readonly WorkerOptions _options = options.Value;
private DbConfig? _config;
private ConfigDto? _config;
private const int CompleteWaitTime = 1;
private string _parentFolderUid = string.Empty;
@@ -30,182 +37,78 @@ public class FinalizeDocumentJob(IOptions<WorkerOptions> options, IConfiguration
public byte[]? DocAsByte { get; set; }
}
public Task ExecuteAsync(CancellationToken cancellationToken = default)
public bool RethrowOnError { get; set; } = true;
public async Task ExecuteAsync(CancellationToken cancel = default)
{
var gdPictureKey = _options.GdPictureLicenseKey;
tempFiles.Create();
var jobId = typeof(FinalizeDocumentJob).FullName;
logger.LogDebug("Starting job {jobId}", jobId);
try
_config = await mediator.Send(new ReadDefaultConfigQuery(), cancel);
var envelopes = await envRepo
.Where(e => e.Status == EnvelopeStatus.EnvelopeCompletelySigned
&& e.ChangedWhen.HasValue
&& EF.Functions.DateDiffMinute(e.ChangedWhen.Value, DateTime.Now) >= CompleteWaitTime)
.OrderBy(e => e.Id)
.ToListAsync(cancel);
if (envelopes.Count > 0)
logger.LogInformation("Found [{count}] completed envelopes.", envelopes.Count);
foreach (var envelope in envelopes)
{
logger.LogDebug("Loading Configuration..");
_config = _configModel?.LoadConfiguration() ?? throw new InvalidOperationException("Configuration could not be loaded because there is no record");
logger.LogDebug("DocumentPath: [{documentPath}]", _config.DocumentPath);
logger.LogDebug("ExportPath: [{exportPath}]", _config.ExportPath);
var completeStatus = EnvelopeStatus.EnvelopeCompletelySigned;
var sql = $"SELECT * FROM TBSIG_ENVELOPE WHERE STATUS = {completeStatus} AND DATEDIFF(minute, CHANGED_WHEN, GETDATE()) >= {CompleteWaitTime} ORDER BY GUID";
var table = _database.GetDatatable(sql);
var envelopeIds = table.Rows.Cast<DataRow>()
.Select(r => r.Field<int>("GUID"))
.ToList();
if (envelopeIds.Count > 0)
try
{
logger.LogInformation("Found [{count}] completed envelopes.", envelopeIds.Count);
await Finalize(envelope, cancel);
}
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception while working envelope [{id}]", envelope.Id);
if(RethrowOnError)
throw;
}
var total = envelopeIds.Count;
var current = 1;
foreach (var id in envelopeIds)
{
logger.LogInformation("Finalizing Envelope [{id}] ({current}/{total})", id, current, total);
try
{
var envelope = _envelopeModel?.GetById(id);
if (envelope is null)
{
logger.LogWarning("Envelope could not be loaded for Id [{id}]!", id);
throw new ArgumentNullException(nameof(EnvelopeData));
}
logger.LogDebug("Loading Envelope Data..");
var envelopeData = GetEnvelopeData(id);
if (envelopeData is null)
{
logger.LogWarning("EnvelopeData could not be loaded for Id [{id}]!", id);
throw new ArgumentNullException(nameof(EnvelopeData));
}
logger.LogDebug("Burning Annotations to pdf ...");
var burnedDocument = BurnAnnotationsToPdf(envelopeData);
if (burnedDocument is null)
{
logger.LogWarning("Document could not be finalized!");
throw new ApplicationException("Document could not be finalized");
}
if (actionService?.CreateReport(envelope) == false)
{
logger.LogWarning("Document Report could not be created!");
throw new ApplicationException("Document Report could not be created");
}
logger.LogDebug("Creating report..");
var report = reportCreator!.CreateReport(envelope);
logger.LogDebug("Report created!");
logger.LogDebug("Merging documents ...");
var mergedDocument = pdfMerger!.MergeDocuments(burnedDocument, report);
logger.LogDebug("Documents merged!");
var outputDirectoryPath = Path.Combine(_config.ExportPath, _parentFolderUid);
logger.LogDebug("oOutputDirectoryPath is {outputDirectoryPath}", outputDirectoryPath);
if (!Directory.Exists(outputDirectoryPath))
{
logger.LogDebug("Directory not existing. Creating ... ");
Directory.CreateDirectory(outputDirectoryPath);
}
var outputFilePath = Path.Combine(outputDirectoryPath, $"{envelope.Uuid}.pdf");
logger.LogDebug("Writing finalized Pdf to disk..");
logger.LogInformation("Output path is [{outputFilePath}]", outputFilePath);
try
{
File.WriteAllBytes(outputFilePath, mergedDocument);
}
catch (Exception ex)
{
logger.LogWarning("Could not export final document to disk!");
throw new ExportDocumentException("Could not export final document to disk!", ex);
}
logger.LogDebug("Writing EB-bytes to database...");
UpdateFileDb(outputFilePath, envelope.Id);
if (!SendFinalEmails(envelope))
{
throw new ApplicationException("Final emails could not be sent!");
}
logger.LogInformation("Report-mails successfully sent!");
logger.LogDebug("Setting envelope status..");
if (actionService?.FinalizeEnvelope(envelope) == false)
{
logger.LogWarning("Envelope could not be finalized!");
throw new ApplicationException("Envelope could not be finalized");
}
}
catch (Exception ex)
{
logger.LogError(ex);
logger.LogWarning(ex, "Unhandled exception while working envelope [{id}]", id);
}
current += 1;
logger.LogInformation("Envelope [{id}] finalized!", id);
}
logger.LogDebug("Completed job {jobId} successfully!", jobId);
}
catch (MergeDocumentException ex)
{
logger.LogWarning("Certificate Document job failed at step: Merging documents!");
logger.LogError(ex);
}
catch (ExportDocumentException ex)
{
logger.LogWarning("Certificate Document job failed at step: Exporting document!");
logger.LogError(ex);
}
catch (Exception ex)
{
logger.LogWarning("Certificate Document job failed!");
logger.LogError(ex);
}
finally
{
logger.LogDebug("Job execution for [{jobId}] ended", jobId);
}
return Task.FromResult(true);
}
private void UpdateFileDb(string filePath, long envelopeId)
{
try
{
var imageData = ReadFile(filePath);
if (imageData is null)
{
return;
}
var query = $"UPDATE TBSIG_ENVELOPE SET DOC_RESULT = @ImageData WHERE GUID = {envelopeId}";
using var command = new SqlCommand(query, _database!.GetConnection);
command.Parameters.Add(new SqlParameter("@ImageData", imageData));
command.ExecuteNonQuery();
}
catch (Exception ex)
{
logger?.LogError(ex);
logger.LogInformation("Envelope [{id}] finalized!", envelope.Id);
}
}
private static byte[]? ReadFile(string path)
private async Task Finalize(Envelope envelope, CancellationToken cancel)
{
var fileInfo = new FileInfo(path);
var numBytes = fileInfo.Length;
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream);
return reader.ReadBytes((int)numBytes);
var annotations = await docStatusRepo.Where(s => s.EnvelopeId == envelope.Id).Select(s => s.Value).ToListAsync(cancel);
var burnedDocument = pdfBurner!.BurnAnnotsToPDF(envelope.DefaultDocument.ByteData!, annotations, envelope.Id)
?? throw new ApplicationException("Document could not be finalized");
actionService.CreateReport(envelope, cancel);
var report = reportCreator!.CreateReport(envelope);
var mergedDocument = pdfMerger!.MergeDocuments(burnedDocument, report);
var outputDirectoryPath = Path.Combine(_config!.ExportPath, _parentFolderUid);
if (!Directory.Exists(outputDirectoryPath))
Directory.CreateDirectory(outputDirectoryPath);
var outputFilePath = Path.Combine(outputDirectoryPath, $"{envelope.Uuid}.pdf");
try
{
File.WriteAllBytes(outputFilePath, mergedDocument);
}
catch (Exception ex)
{
throw new ExportDocumentException("Could not export final document to disk. Envelope Id is " + envelope.Id, ex);
}
var outputFile = await File.ReadAllBytesAsync(outputFilePath, cancel);
await envRepo.UpdateAsync(e => e.DocResult = outputFile, e => e.Id == envelope.Id, cancel);
SendFinalEmails(envelope);
actionService?.FinalizeEnvelope(envelope, cancel);
}
private bool SendFinalEmails(Envelope envelope)
@@ -214,23 +117,17 @@ public class FinalizeDocumentJob(IOptions<WorkerOptions> options, IConfiguration
var mailToReceivers = (FinalEmailType)(envelope.FinalEmailToReceivers ?? 0);
if (mailToCreator != FinalEmailType.No)
{
logger?.LogDebug("Sending email to creator ...");
SendFinalEmailToCreator(envelope, mailToCreator);
}
else
{
logger?.LogWarning("No SendFinalEmailToCreator - oMailToCreator [{0}] <> [{1}] ", mailToCreator, FinalEmailType.No);
}
logger?.LogWarning("No SendFinalEmailToCreator - oMailToCreator [{mailToCreator}] <> [{noFinalEmailType}] ", mailToCreator, FinalEmailType.No);
if (mailToReceivers != FinalEmailType.No)
{
logger?.LogDebug("Sending emails to receivers..");
SendFinalEmailToReceivers(envelope, mailToReceivers);
}
else
{
logger?.LogWarning("No SendFinalEmailToReceivers - oMailToCreator [{0}] <> [{1}] ", mailToReceivers, FinalEmailType.No);
logger?.LogWarning("No SendFinalEmailToReceivers - oMailToCreator [{mailToReceivers}] <> [{noFinalEmailType}] ", mailToReceivers, FinalEmailType.No);
}
return true;
@@ -238,12 +135,9 @@ public class FinalizeDocumentJob(IOptions<WorkerOptions> options, IConfiguration
private bool SendFinalEmailToCreator(Envelope envelope, FinalEmailType mailToCreator)
{
var includeAttachment = SendFinalEmailWithAttachment(mailToCreator);
logger?.LogDebug("Attachment included: [{0}]", includeAttachment);
if (actionService?.CompleteEnvelope(envelope) == false)
{
logger?.LogError(new Exception("CompleteEnvelope failed"), "Envelope could not be completed for receiver [{0}]", envelope.User?.Email);
logger?.LogError(new Exception("CompleteEnvelope failed"), "Envelope could not be completed for receiver [{email}]", envelope.User?.Email);
return false;
}
@@ -252,109 +146,15 @@ public class FinalizeDocumentJob(IOptions<WorkerOptions> options, IConfiguration
private bool SendFinalEmailToReceivers(Envelope envelope, FinalEmailType mailToReceivers)
{
var includeAttachment = SendFinalEmailWithAttachment(mailToReceivers);
logger?.LogDebug("Attachment included: [{0}]", includeAttachment);
foreach (var receiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
{
if (actionService?.CompleteEnvelope(envelope, receiver.Receiver) == false)
{
logger?.LogError(new Exception("CompleteEnvelope failed"), "Envelope could not be completed for receiver [{0}]", receiver.Receiver?.EmailAddress);
logger?.LogError(new Exception("CompleteEnvelope failed"), "Envelope could not be completed for receiver [{email}]", receiver.Receiver?.EmailAddress);
return false;
}
}
return true;
}
private static bool SendFinalEmailWithAttachment(FinalEmailType type)
{
return type == FinalEmailType.YesWithAttachment;
}
private byte[] BurnAnnotationsToPdf(EnvelopeData envelopeData)
{
var envelopeId = envelopeData.EnvelopeId;
logger?.LogInformation("Burning [{0}] signatures", envelopeData.AnnotationData.Count);
var annotations = envelopeData.AnnotationData;
var inputPath = string.Empty;
if (envelopeData.DocAsByte is null)
{
inputPath = envelopeData.DocumentPath;
logger?.LogInformation("Input path: [{0}]", inputPath);
}
else
{
logger?.LogDebug("we got bytes..");
inputPath = _config!.DocumentPathOrigin;
logger?.LogDebug("oInputPath: {0}", _config.DocumentPathOrigin);
}
if (envelopeData.DocAsByte is null)
{
var directorySource = Path.GetDirectoryName(inputPath) ?? string.Empty;
var split = directorySource.Split('\\');
_parentFolderUid = split[^1];
}
else
{
_parentFolderUid = envelopeData.EnvelopeUuid;
}
logger?.LogInformation("ParentFolderUID: [{0}]", _parentFolderUid);
byte[] inputDocumentBuffer;
if (envelopeData.DocAsByte is not null)
{
inputDocumentBuffer = envelopeData.DocAsByte;
}
else
{
try
{
inputDocumentBuffer = File.ReadAllBytes(inputPath);
}
catch (Exception ex)
{
throw new BurnAnnotationException("Source document could not be read from disk!", ex);
}
}
return pdfBurner!.BurnAnnotsToPDF(inputDocumentBuffer, annotations, envelopeId);
}
private EnvelopeData? GetEnvelopeData(int envelopeId)
{
var sql = $"SELECT T.GUID, T.ENVELOPE_UUID, T.ENVELOPE_TYPE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T\n JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID\n WHERE T.GUID = {envelopeId}";
var table = _database!.GetDatatable(sql);
var row = table.Rows.Cast<DataRow>().SingleOrDefault();
if (row is null)
{
return null;
}
var annotationData = GetAnnotationData(envelopeId);
var data = new EnvelopeData
{
EnvelopeId = envelopeId,
DocumentPath = row.ItemEx("FILEPATH", string.Empty),
AnnotationData = annotationData,
DocAsByte = row.Field<byte[]?>("BYTE_DATA"),
EnvelopeUuid = row.ItemEx("ENVELOPE_UUID", string.Empty)
};
logger?.LogDebug("Document path: [{0}]", data.DocumentPath);
return data;
}
private List<string> GetAnnotationData(int envelopeId)
{
var sql = $"SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = {envelopeId}";
var table = _database!.GetDatatable(sql);
return table.Rows.Cast<DataRow>()
.Select(r => r.ItemEx("VALUE", string.Empty))
.ToList();
}
}
}

View File

@@ -1,14 +0,0 @@
using DigitalData.Modules.Database;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.ServiceHost.Jobs;
[Obsolete("Use DbContext")]
public class State
{
public int UserId { get; set; }
public FormUser? User { get; set; }
public Config? Config { get; set; }
public DbConfig? DbConfig { get; set; }
public MSSQLServer? Database { get; set; }
}

View File

@@ -2,12 +2,17 @@ using EnvelopeGenerator.ServiceHost.Extensions;
namespace EnvelopeGenerator.ServiceHost.Jobs;
[Obsolete("Use memory cache instead of temp files.")]
public class TempFiles
{
[Obsolete("Use memory cache instead of temp files.")]
public string TempPath { get; }
private readonly ILogger<TempFiles> _logger;
[Obsolete("Use memory cache instead of temp files.")]
public TempFiles(ILogger<TempFiles> logger)
{
_logger = logger;
@@ -15,6 +20,8 @@ public class TempFiles
TempPath = Path.Combine(tempDirectoryPath, "EnvelopeGenerator");
}
[Obsolete("Use memory cache instead of temp files.")]
public bool Create()
{
try
@@ -37,6 +44,8 @@ public class TempFiles
}
}
[Obsolete("Use memory cache instead of temp files.")]
private bool CleanUpFiles()
{
try
@@ -56,6 +65,8 @@ public class TempFiles
}
}
[Obsolete("Use memory cache instead of temp files.")]
public bool CleanUp()
{
try

View File

@@ -1,17 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
using NUnit.Framework;
namespace EnvelopeGenerator.Tests.Domain;
public class ConstantsTests
{
[TestCase(EnvelopeSigningType.ReadAndSign, EnvelopeSigningType.ReadAndSign)]
[TestCase(EnvelopeSigningType.WetSignature, EnvelopeSigningType.WetSignature)]
[TestCase((EnvelopeSigningType)5, EnvelopeSigningType.WetSignature)]
public void Normalize_ReturnsExpectedValue(EnvelopeSigningType input, EnvelopeSigningType expected)
{
var normalized = input.Normalize();
Assert.That(normalized, Is.EqualTo(expected));
}
}

View File

@@ -53,4 +53,8 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Domain\" />
</ItemGroup>
</Project>

View File

@@ -91,7 +91,7 @@ public class EnvelopeController : ViewControllerBase
if (await _historyService.IsSigned(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress))
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return View("EnvelopeSigned");
return View("EnvelopeSigned", er);
}
#endregion

View File

@@ -12,9 +12,9 @@
<PackageTags>digital data envelope generator web</PackageTags>
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Version>3.11.0</Version> <!-- NuGet package version -->
<AssemblyVersion>3.11.0.0</AssemblyVersion> <!-- Assembly version for API compatibility -->
<FileVersion>3.11.0.0</FileVersion> <!-- Windows file version -->
<Version>3.12.0</Version> <!-- NuGet package version -->
<AssemblyVersion>3.12.0.0</AssemblyVersion> <!-- Assembly version for API compatibility -->
<FileVersion>3.12.0.0</FileVersion> <!-- Windows file version -->
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup>

View File

@@ -9,12 +9,14 @@
@using EnvelopeGenerator.Web.Extensions
@using Newtonsoft.Json
@using Newtonsoft.Json.Serialization
@using EnvelopeGenerator.Domain.Interfaces;
@model EnvelopeReceiverDto;
@{
var envelope = Model.Envelope;
var document = Model.Envelope?.Documents?.FirstOrDefault();
var sender = Model.Envelope?.User;
var isExt = ViewBag.IsExt ?? false;
bool IsReadAndConfirm = Model!.Envelope!.IsReadAndConfirm();
}
<div class="page container p-5">
<header class="text-center">
@@ -54,7 +56,11 @@
c-5.791,5.79-15.176,5.79-20.969,0l-30.32-30.322l-11.676,11.676l30.32,30.32c5.79,5.79,5.79,15.178,0,20.969L299.11,404.045z"/>
</svg>
</div>
<h1>@(isExt ? _localizer.RejectionInfo1Ext() : _localizer.RejectionInfo1())</h1>
<h1>@(isExt
? _localizer.RejectionInfo1Ext()
: IsReadAndConfirm
? _localizer.RejectionInfo1ForConfirmation()
: _localizer.RejectionInfo1())</h1>
</header>
<section class="text-center">
<div class="card-body p-0 m-0 ms-4">

View File

@@ -1,5 +1,11 @@
@{
ViewData["Title"] = _localizer.DocSigned();
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
@using EnvelopeGenerator.Domain.Interfaces;
@model EnvelopeReceiverDto;
bool IsReadAndConfirm = Model!.Envelope!.IsReadAndConfirm();
ViewData["Title"] = IsReadAndConfirm
? _localizer.DocConfirmed()
: _localizer.DocSigned();
}
<div class="page container p-5">
<header class="text-center">
@@ -9,9 +15,13 @@
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z" />
</svg>
</div>
<h1>@_localizer["DocumentSuccessfullySigned"]</h1>
<h1>@(IsReadAndConfirm
? _localizer["DocumentSuccessfullyConfirmed"]
: _localizer["DocumentSuccessfullySigned"])</h1>
</header>
<section class="text-center">
<p>@_localizer["DocumentSignedConfirmationMessage"]</p>
<p>@(IsReadAndConfirm
? _localizer["DocumentConfirmedConfirmationMessage"]
: _localizer["DocumentSignedConfirmationMessage"])</p>
</section>
</div>

View File

@@ -9,6 +9,7 @@
@using EnvelopeGenerator.Web.Extensions
@using Newtonsoft.Json
@using Newtonsoft.Json.Serialization
@using EnvelopeGenerator.Domain.Interfaces
@model EnvelopeReceiverDto;
@{
var userCulture = ViewData["UserCulture"] as Culture;
@@ -24,7 +25,13 @@
if (ViewData["IsReadOnly"] is bool isReadOnly_bool)
isReadOnly = isReadOnly_bool;
ViewData["Title"] = isReadOnly ? _localizer.ViewDoc() : _localizer.SignDoc();
var isReadAndConfirm = envelope.IsReadAndConfirm();
ViewData["Title"] = isReadOnly
? _localizer.ViewDoc()
: isReadAndConfirm
? _localizer.ConfirmDoc()
: _localizer.SignDoc();
}
<div class="envelope-view">
@if (!isReadOnly)
@@ -62,7 +69,9 @@
<div class="progress-container">
<div id="signed-count-bar" class="progress"></div>
<span class="progress-text">
<span id="signed-count">0</span>/<span id="signature-count">@signatureCount</span> @_localizer["Signatures"]
<span id="signed-count">0</span>/<span id="signature-count">@signatureCount</span> @(isReadAndConfirm
? _localizer["Confirmations"]
: _localizer["Signatures"])
</span>
</div>
}

View File

@@ -275,7 +275,7 @@ async function createAnnotationFrameBlob(receiverName, receiverSignature, timest
ctx.fillStyle = 'black'
ctx.font = `${fontSize * scale}px sans-serif`
ctx.fillText(localized.signedBy ?? 'Signed by', 15 * scale, 10 * scale)
ctx.fillText((READ_AND_CONFIRM ? localized.confirmedBy : localized.signedBy) ?? 'Signed by', 15 * scale, 10 * scale)
ctx.fillText(receiverName, 15 * scale, 60 * scale)
ctx.fillText(signatureString, 15 * scale, 70 * scale)

View File

@@ -255,7 +255,7 @@ class App {
return Swal.fire({
title: localized.confirmation,
html: `<div class="text-start fs-6 p-0 m-0">${localized.sigAgree}</div>`,
html: `<div class="text-start fs-6 p-0 m-0">${READ_AND_CONFIRM ? localized.confirmAgree : localized.sigAgree}</div>`,
icon: "question",
showCancelButton: true,
confirmButtonColor: "#3085d6",

View File

@@ -1,3 +1,3 @@
const formatLocalized=(n,...t)=>typeof n=="string"?n.replace(/\{(\d+)\}/g,(n,i)=>t[i]??""):"";class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){if(this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale),addToolbarItems(this.pdfKit,this.handleClick.bind(this)),this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this)),this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this)),this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this)),this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()}),!READ_AND_CONFIRM)try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n)}catch(n){console.error("Error loading annotations:",n)}READ_AND_CONFIRM||[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const r=t.boundingBox.left-20,u=t.boundingBox.top-20,n=150,i=75,f=new Date,e=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,f,n,i),o=await fetch(e),s=await o.blob(),h=await this.pdfKit.createAttachment(s),c=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:r,top:u,width:n,height:i}),t.pageIndex,h,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(c)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:localized.success,text:localized.documentReset,icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(`
Request failed: ${t}
`)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify(localized.copyLinkSuccess,{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify(localized.copyLinkFailure??localized.unexpectedErrorTitle,{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){let n=undefined;if(READ_AND_CONFIRM){const n=JSON.parse(sessionStorage.getItem("pspdf_all_pages_rendered")||"false")===!0;if(!n){const n=JSON.parse(sessionStorage.getItem("pspdf_unviewed_pages")||"[]"),t=n.length?formatLocalized(localized.viewRemainingPages,n.join(", ")):localized.viewAllPages;return await Swal.fire({title:localized.warning,text:t,icon:"warning"}),!1}}else{const i=await this.pdfKit.exportInstantJSON(),r=i.formFieldValues,u=r.filter(n=>isFieldRequired(n)),f=u.some(n=>n.value===undefined||n.value===null||n.value==="");if(f)return Swal.fire({title:localized.warning,text:localized.locationFieldsRequired,icon:"warning"}),!1;const e=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),o=r.filter(n=>isCityField(n));for(var t of o)if(!IS_MOBILE_DEVICE&&!e.test(t.value))return Swal.fire({title:localized.warning,text:formatLocalized(localized.cityFormatInvalid,t.value),icon:"warning"}),!1;const s=await this.validateAnnotations(this.signatureCount);if(s===!1)return Swal.fire({title:localized.warning,text:localized.missingSignatures,icon:"warning"}),!1;n={instant:i,structured:mapSignature(i)}}return Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}try{const t=READ_AND_CONFIRM?await signEnvelope():await signEnvelope(n);if(t.ok)return!0;if(t.status===409)return Swal.fire({title:localized.warning,text:localized.envelopeUnavailable,icon:"warning"}),!1;if(t.status===423)Swal.fire({title:localized.info??localized.warning,text:localized.envelopeRejectedRedirect,icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=Swal.fire({title:localized.resetConfirmTitle,text:localized.resetConfirmText,icon:"question",showCancelButton:!0});if(n.isConfirmed){const n=await deleteAnnotations(this.pdfKit)}return n}fakeElementId=0;}
`)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify(localized.copyLinkSuccess,{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify(localized.copyLinkFailure??localized.unexpectedErrorTitle,{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){let n=undefined;if(READ_AND_CONFIRM){const n=JSON.parse(sessionStorage.getItem("pspdf_all_pages_rendered")||"false")===!0;if(!n){const n=JSON.parse(sessionStorage.getItem("pspdf_unviewed_pages")||"[]"),t=n.length?formatLocalized(localized.viewRemainingPages,n.join(", ")):localized.viewAllPages;return await Swal.fire({title:localized.warning,text:t,icon:"warning"}),!1}}else{const i=await this.pdfKit.exportInstantJSON(),r=i.formFieldValues,u=r.filter(n=>isFieldRequired(n)),f=u.some(n=>n.value===undefined||n.value===null||n.value==="");if(f)return Swal.fire({title:localized.warning,text:localized.locationFieldsRequired,icon:"warning"}),!1;const e=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),o=r.filter(n=>isCityField(n));for(var t of o)if(!IS_MOBILE_DEVICE&&!e.test(t.value))return Swal.fire({title:localized.warning,text:formatLocalized(localized.cityFormatInvalid,t.value),icon:"warning"}),!1;const s=await this.validateAnnotations(this.signatureCount);if(s===!1)return Swal.fire({title:localized.warning,text:localized.missingSignatures,icon:"warning"}),!1;n={instant:i,structured:mapSignature(i)}}return Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${READ_AND_CONFIRM?localized.confirmAgree:localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}try{const t=READ_AND_CONFIRM?await signEnvelope():await signEnvelope(n);if(t.ok)return!0;if(t.status===409)return Swal.fire({title:localized.warning,text:localized.envelopeUnavailable,icon:"warning"}),!1;if(t.status===423)Swal.fire({title:localized.info??localized.warning,text:localized.envelopeRejectedRedirect,icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=Swal.fire({title:localized.resetConfirmTitle,text:localized.resetConfirmText,icon:"question",showCancelButton:!0});if(n.isConfirmed){const n=await deleteAnnotations(this.pdfKit)}return n}fakeElementId=0;}