Compare commits

...

38 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
25 changed files with 439 additions and 420 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

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

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

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

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