Compare commits

...

52 Commits

Author SHA1 Message Date
172f2e27d7 Bump version to 3.9.0 in EnvelopeGenerator.Web.csproj
Updated the project, assembly, and file versions from 3.8.2 to 3.9.0 in the EnvelopeGenerator.Web.csproj file. This prepares the project for the next release.
2026-01-27 12:07:02 +01:00
d350e2ae48 Add envelope validation to BurnAnnotsToPDF function
Retrieve envelope by ID before burning annotations. Throw an exception if not found, and return the original PDF if the envelope is read-only. This adds validation and early exit logic before processing signatures and annotations.
2026-01-27 11:49:19 +01:00
2779452d72 Add ReadOnly property to Envelope class
Added a ReadOnly property to the Envelope class that returns true when EnvelopeTypeId is 2. The property is marked with [JsonIgnore] and [NotMapped] to exclude it from JSON serialization and database mapping.
2026-01-27 11:49:02 +01:00
5ebc6c6739 Update SQL to include ENVELOPE_TYPE in envelope query
The GetEnvelopeData function's SQL query now selects the ENVELOPE_TYPE column from the TBSIG_ENVELOPE table, allowing retrieval of envelope type information along with other envelope data.
2026-01-26 15:07:58 +01:00
891593755e Remove DLL ref, update configs, add WCF metadata
Removed EnvelopeGenerator.CommonServices DLL reference from BBTests project file, updated project GUIDs to lowercase, added WCFMetadata ItemGroup for connected services, and set BBTests Release config to use Debug in solution file.
2026-01-26 15:02:49 +01:00
b20260674e Improve PDF font handling; simplify report SQL fields
Enhanced PDFBurner to use a static FontProvider for better font support when rendering form field values. In ReportCreator, removed unused HEAD_TITLE and HEAD_SUBJECT fields from the SQL query and related mapping, streamlining report item loading.
2026-01-22 15:57:09 +01:00
7e5ff6bcb2 Refactor APIEnvelopeJob to use primary constructor
Simplifies APIEnvelopeJob by adopting C# primary constructor syntax.
Removes explicit constructors and initializes logger inline,
enabling direct dependency injection and reducing boilerplate.
2026-01-22 12:47:58 +01:00
6eed9b1e31 Update WorkerSettings with SQL Server connection string
Updated the ConnectionString property in both appsettings.json and appsettings.Development.json from an empty value to a specific SQL Server connection string, including server, database, credentials, and security options. This enables the application to connect to the designated database environment.
2026-01-22 10:14:44 +01:00
d4b1a4921c Refactor worker to use config, DI, and Quartz scheduling
- Add WorkerSettings class and update appsettings for config-driven setup
- Integrate Quartz.NET for job scheduling (FinalizeDocumentJob, APIEnvelopeJob)
- Refactor Program.cs for DI of services (TempFileManager, PDFBurner, etc.)
- Implement TempFileManager for temp folder management and cleanup
- Rewrite Worker class for config validation, DB check, and lifecycle logging
- Update csproj to include Quartz and EnvelopeGenerator.Jobs references
- Improve maintainability, error handling, and logging throughout
2026-01-22 09:51:35 +01:00
f078bafdde Refactor Jobs namespace and improve PDF handling
Refactored all EnvelopeGenerator.Jobs files to use the EnvelopeGenerator.Jobs namespace instead of EnvelopeGenerator.CommonServices.Jobs. Updated the .csproj to remove custom content and compile includes for the Jobs folder. Switched FinalizeDocumentJob to use dependency injection for PDFBurner, PDFMerger, and ReportCreator. Improved image annotation logic in PDFBurner for better placement and scaling, and refactored form field value rendering for conditional font styling. Aliased Document as LayoutDocument in ReportCreator to avoid ambiguity. Removed the obsolete Class1.cs file and made minor type safety improvements. These changes modernize the codebase and enhance maintainability.
2026-01-22 09:51:16 +01:00
786a3e128d Add EnvelopeGenerator.WorkerService as background worker
Added a new WorkerService project to the solution, targeting .NET 8.0 and using Microsoft.Extensions.Hosting. Implemented a Worker class that logs periodically, set up hosting in Program.cs, and included configuration files for logging and development. Updated the solution file to include and configure the new project.
2026-01-20 16:46:30 +01:00
ff3a146636 Add job framework for envelope processing and PDF finalization
Introduced new job classes for envelope processing and document finalization, including APIEnvelopeJob and FinalizeDocumentJob, both implementing Quartz IJob. Added supporting utilities for PDF annotation burning (PDFBurner), PDF merging (PDFMerger), and report generation (ReportCreator), along with related data models and exception types. Updated project references and dependencies to support Quartz scheduling, SQL Server access, and PDF manipulation with iText. This establishes a modular, extensible job-processing framework for envelope management and reporting.
2026-01-20 16:28:05 +01:00
40b2cad598 Add EnvelopeGenerator.Jobs project to the solution
Added new EnvelopeGenerator.Jobs project targeting .NET 8.0, with initial Class1.cs placeholder. Updated solution file to include the project with appropriate build configurations and solution folder nesting.
2026-01-20 15:52:41 +01:00
5c675be0ed Add type check to Handle method in AnnotationHandler
Refactored the Handle method to include a type check for PsPdfKitAnnotation before creating an annotation. This prevents errors when the notification does not contain the expected annotation type.
2026-01-20 13:57:52 +01:00
58164be640 Handle missing PsPdfKitAnnotation with blank JSON
Refactored DocStatusHandler to assign a blank JSON object ("{}")
to the Value property when PsPdfKitAnnotation is not present or
not of the expected type, preventing potential runtime errors.
2026-01-20 13:57:43 +01:00
a639377195 Make PsPdfKitAnnotation property and param nullable
Changed PsPdfKitAnnotation in DocSignedNotification to be nullable, removing the need for the null-forgiving operator. Updated ToDocSignedNotification to accept a nullable PsPdfKitAnnotation, ensuring consistency and allowing for cases where the annotation may be absent.
2026-01-20 13:57:29 +01:00
e3d6e87ee5 Allow nullable annotation param; validate for non-readonly
Make psPdfKitAnnotation optional in CreateOrUpdate. Add validation to require an annotation for non read-and-confirm envelopes, returning BadRequest if missing.
2026-01-20 12:11:49 +01:00
2795b91386 Refactor handleFinish to streamline READ_AND_CONFIRM flow and improve validation checks 2026-01-20 11:57:04 +01:00
ca248c3aa6 Support READ_AND_CONFIRM flow in handleFinish
Add logic to require all pages viewed before allowing finish when READ_AND_CONFIRM is enabled. Skip annotation and form field validations in this mode. Show warnings for unviewed pages and handle errors for save/sign actions. Update minified app to match new flow.
2026-01-20 11:44:40 +01:00
383634fca6 Conditionally add btn_refresh event based on READ_AND_CONFIRM
Only attach click handlers to btn_refresh elements when
READ_AND_CONFIRM is false. btn_complete and btn_reject
handlers remain unaffected. This change applies to both
app.js and app.min.js.
2026-01-20 11:11:59 +01:00
75097afa06 Add refresh button to envelope UI when not read-only
A "Refresh" button with a counterclockwise arrow icon is now shown in the envelope UI, but only when the envelope is not in read-only mode. The button uses standard styling classes for consistency.
2026-01-20 11:11:39 +01:00
77975c0644 Conditionally show "reset" button in mobile toolbar
The "reset" button in getMobileWritableItems is now only included if READ_AND_CONFIRM is falsy. This prevents the button from appearing when READ_AND_CONFIRM is true. The same conditional logic was applied to the minified ui.min.js. Code was also refactored for clarity.
2026-01-20 11:11:25 +01:00
5707213edd Conditionally apply PDF background for read-only envelopes
When preparing the PDF for the view, only apply the Background
method if the envelope is not read-only. This ensures that
read-only envelopes are displayed without additional background
elements.
2026-01-20 10:54:31 +01:00
ad54ba9dc4 Conditionally load annotations based on READ_AND_CONFIRM
Annotations are now only loaded if READ_AND_CONFIRM is falsy.
This prevents unnecessary annotation creation in read-and-confirm
scenarios. Changes applied to both app.js and app.min.js.
2026-01-20 10:54:16 +01:00
1f233153cf Restrict page view tracking to READ_AND_CONFIRM mode
Previously, page view tracking and sessionStorage updates ran unconditionally. Now, this logic is only executed when READ_AND_CONFIRM is enabled, ensuring viewed/unviewed page state is only tracked when required. Updated both source and minified files accordingly.
2026-01-20 10:38:37 +01:00
513ec007eb Set ViewData["ReadAndConfirm"] for envelope read-only state
Added "ReadAndConfirm" to ViewData, passing the envelope's ReadOnly property to the "ShowEnvelope" view. This enables the view to adjust its behavior or UI based on whether the envelope is read-only.
2026-01-20 10:30:25 +01:00
1305714da2 Move ReadOnly property from Envelope to EnvelopeDto
The ReadOnly property logic was shifted from the Envelope entity
to the EnvelopeDto record, ensuring that read-only status is
determined at the DTO layer rather than in the data model.
2026-01-20 10:30:07 +01:00
1e90cda393 Add READ_AND_CONFIRM JS constant from ViewData flag
Introduced a READ_AND_CONFIRM JavaScript constant in _Layout.cshtml, which reflects the server-side ViewData["ReadAndConfirm"] boolean value. This enables client-side scripts to easily check if the "ReadAndConfirm" flag is set.
2026-01-20 09:54:45 +01:00
5a5cbcb14d Track viewed PDF pages and persist state in sessionStorage
Added logic to monitor which PDF pages have been viewed by the user. The list of unviewed pages and a flag for all pages viewed are stored in sessionStorage and updated as the user navigates. Changes applied to both ui.js and ui.min.js.
2026-01-19 17:06:43 +01:00
a35f06070a Remove total page count logging after PSPDFKit load
Refactored ui.js and ui.min.js to eliminate retrieval and logging of the total page count after loading the PSPDFKit instance. The code now returns the instance directly after setting up the page change event listener, reducing unnecessary logging and simplifying the load process.
2026-01-19 17:03:05 +01:00
2606066103 Add logging for page changes and total pages in PSPDFKit
Added a .then() handler to loadPSPDFKit to log the active page number on page change events and log the total number of pages after loading. This aids in debugging and tracking user navigation within the document.
2026-01-19 16:57:34 +01:00
7495e062a9 Remove EnvelopeSigningType enum and update envelope logic
Removed EnvelopeSigningType enum and related normalization logic. Added a ReadOnly property to Envelope that uses EnvelopeTypeId to determine read-only status. Envelope type handling now relies on EnvelopeTypeId (int?) instead of the enum.
2026-01-19 16:50:02 +01:00
293044bec3 Handle envelope type with framework-specific properties
Add conditional logic to map "ENVELOPE_TYPE" to either an int? EnvelopeTypeId (for .NET Framework) or a type-safe EnvelopeSigningType SigningType (for .NET), supporting both legacy and modern approaches.
2026-01-19 16:21:58 +01:00
e0ff976d21 Add unit tests for EnvelopeSigningType.Normalize method
Added ConstantsTests to verify Normalize behavior for EnvelopeSigningType, including handling of valid and out-of-range enum values using NUnit test cases. Ensures normalization logic works as expected.
2026-01-19 16:08:39 +01:00
bec45ab1f1 Refactor test namespaces to EnvelopeGenerator.Tests.Application
All test files and utilities now use the EnvelopeGenerator.Tests.Application namespace for improved organization and clarity. No functional changes were made; updates are limited to namespaces and using directives. This makes it explicit that these are application-level tests and related helpers.
2026-01-19 16:02:39 +01:00
fecd054a5c Add EnvelopeSigningType enum and Normalize extension
Introduced EnvelopeSigningType enum with WetSignature and ReadAndSign values in the EnvelopeGenerator.Domain.Constants namespace. Added EnvelopeSigningTypeExtensions with a Normalize method to standardize enum values.
2026-01-19 16:01:13 +01:00
32b488c50f Refactor test dependency resolution in DocSignedNotificationTests
Refactored DocSignedNotificationTests to use typed repository
retrieval and explicit service resolution for PsPdfKitAnnotation.
Added a Services property to TestBase for easier access to
IServiceProvider. These changes improve clarity and robustness
of test dependency management.
2026-01-19 15:56:12 +01:00
9cfdd16970 Refactor test DB config to support SQL Server and fix seeding
Refactored Fake.cs to configure both in-memory and SQL Server
database contexts for testing, using the "Default" connection
string from configuration. Added detailed EF logging and SQL
executor setup. In TestBase.cs, fixed Setup to use the correct
repository instance for seeding email templates.
2026-01-19 15:51:09 +01:00
4da5848253 Refactor test namespaces; update package version
Changed test file namespaces from EnvelopeGenerator.Tests.Application to EnvelopeGenerator.Tests for consistency. Updated DigitalData.Core.Abstraction.Application package from 1.4.0 to 1.6.0 to incorporate latest improvements.
2026-01-19 15:14:59 +01:00
88da210ba2 Update DigitalData.Core.Abstraction.Application to 1.6.0
Updated DigitalData.Core.Abstraction.Application from 1.4.0 to 1.6.0 across all relevant project files and package configs. Also updated DigitalData.Core.Infrastructure from 2.4.5 to 2.6.1 in EnvelopeGenerator.Infrastructure.csproj. No other code changes were made.
2026-01-19 15:13:53 +01:00
fc23ba840e chore(Web): bump to 3.8.2 2025-11-20 16:47:45 +01:00
140d271b28 refactor(privacy-policy): remove 6. Hinweisgebersystem 2025-11-20 16:46:31 +01:00
a3b12a6957 bump to 3.8.1 2025-11-20 14:49:10 +01:00
16bdc7820d update privacy policy (English) 2025-11-20 14:14:25 +01:00
06e32b99ea update privacy policy (German) title and last updated date 2025-11-20 14:06:51 +01:00
c7c78f96a6 refactor(PDFBurner): fix merging errors 2025-11-20 12:34:30 +01:00
5c232e61f2 merge PDFBurner changes 2025-11-20 11:57:00 +01:00
24c9321c0f bump to 3.8.0 2025-11-20 10:34:19 +01:00
c75c2b1dd5 feat(envelope-api.js): append envKey query parameter to all outgoing requests
Added automatic injection of the envKey query parameter into all request URLs within sendRequest.
Updated URL handling to use the URL API, ensuring consistent parameter merging and preventing missing envKey issues.
2025-11-20 10:33:54 +01:00
8445757f34 feat: replace default cookie events with custom EnvelopeCookieManager and introduce custom auth cookie name (env_auth) 2025-11-20 10:32:32 +01:00
b088eb089f feat(EnvelopeCookieManager): add EnvelopeCookieManager to support envelope-specific cookie names
- Introduce EnvelopeCookieManager wrapper around ChunkingCookieManager to generate dynamic cookie names based on envelopeReceiverId or envKey. Ensures request/response cookies are scoped per envelope.
2025-11-20 10:30:49 +01:00
1f745ae79c refactor(pdfburner): simplify form field handling and improve default field naming
- Replaced ImmutableDictionary-based FormFieldIndex with a simpler Dictionary
- Updated form field ordering to: NoName, signature, position, city, date
- Removed manual formFieldIndex counter, now using dictionary lookup by fieldName
- Introduced FieldNames class with NoName constant (guid-based) for unnamed fields
- Defaulted Annotation.fieldName to FieldNames.NoName instead of Nothing
2025-10-09 18:59:18 +02:00
56 changed files with 1621 additions and 442 deletions

View File

@@ -74,6 +74,11 @@ public record EnvelopeDto
/// </summary>
public int? EnvelopeTypeId { get; set; }
/// <summary>
///
/// </summary>
public bool ReadOnly => EnvelopeTypeId == 2;
/// <summary>
///
/// </summary>

View File

@@ -24,7 +24,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
/// <summary>
///
/// </summary>
public PsPdfKitAnnotation PsPdfKitAnnotation { get; init; } = null!;
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
/// <summary>
///
@@ -59,7 +59,7 @@ public static class DocSignedNotificationExtensions
/// <param name="dtoTask"></param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns></returns>
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation psPdfKitAnnotation)
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
/// <summary>

View File

@@ -29,6 +29,9 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task Handle(DocSignedNotification notification, CancellationToken cancel)
=> _repo.CreateAsync(notification.PsPdfKitAnnotation.Structured, cancel);
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
{
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancel);
}
}

View File

@@ -10,6 +10,8 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// </summary>
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{
private const string BlankAnnotationJson = "{}";
private readonly ISender _sender;
/// <summary>
@@ -33,7 +35,9 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{
Envelope = new() { Id = notification.EnvelopeId },
Receiver = new() { Id = notification.ReceiverId},
Value = JsonSerializer.Serialize(notification.PsPdfKitAnnotation.Instant, Format.Json.ForAnnotations)
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson
}, cancel);
}
}

View File

@@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />

View File

@@ -70,8 +70,8 @@
<Reference Include="DigitalData.Controls.DocumentViewer, Version=1.9.8.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Controls.DocumentViewer.1.9.8\lib\net462\DigitalData.Controls.DocumentViewer.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
@@ -109,9 +109,6 @@
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.SqlServer.dll</HintPath>
</Reference>
<Reference Include="EnvelopeGenerator.CommonServices">
<HintPath>..\EnvelopeGenerator.CommonServices\bin\Debug\EnvelopeGenerator.CommonServices.dll</HintPath>
</Reference>
<Reference Include="FirebirdSql.Data.FirebirdClient, Version=7.5.0.0, Culture=neutral, PublicKeyToken=3750abcc3150b00c, processorArchitecture=MSIL">
<HintPath>..\packages\FirebirdSql.Data.FirebirdClient.7.5.0\lib\net452\FirebirdSql.Data.FirebirdClient.dll</HintPath>
</Reference>
@@ -466,8 +463,12 @@
<Project>{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}</Project>
<Name>EnvelopeGenerator.CommonServices</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj">
<Project>{6ea0c51f-c2b1-4462-8198-3de0b32b74f8}</Project>
<Name>EnvelopeGenerator.CommonServices</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj">
<Project>{4F32A98D-E6F0-4A09-BD97-1CF26107E837}</Project>
<Project>{4f32a98d-e6f0-4a09-bd97-1cf26107e837}</Project>
<Name>EnvelopeGenerator.Domain</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj">
@@ -495,6 +496,9 @@
<Content Include="MailLicense.xml" />
<Content Include="README.txt" />
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Connected Services\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
<Import Project="..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets" Condition="Exists('..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

View File

@@ -3,7 +3,7 @@
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
<package id="DigitalData.Controls.DocumentViewer" version="1.9.8" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />

View File

@@ -72,8 +72,8 @@
<Reference Include="DevExpress.XtraEditors.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a" />
<Reference Include="DevExpress.XtraGauges.v21.2.Core, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
<Reference Include="DevExpress.XtraReports.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>

View File

@@ -422,7 +422,7 @@ Namespace Jobs
End Function
Private Function GetEnvelopeData(pEnvelopeId As Integer) As EnvelopeData
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID,T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID, T.ENVELOPE_TYPE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
WHERE T.GUID = {pEnvelopeId}"
Dim oTable As DataTable = Database.GetDatatable(oSql)

View File

@@ -37,6 +37,15 @@ Namespace Jobs.FinalizeDocument
Public Function BurnAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String), envelopeId As Integer) As Byte()
'read the elements of envelope with their annotations
Using scope = Factory.Shared.ScopeFactory.CreateScope()
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
If envelope Is Nothing Then
Throw New BurnAnnotationException($"Envelope with Id {envelopeId} not found.")
ElseIf envelope.ReadOnly Then
Return pSourceBuffer
End If
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _

View File

@@ -2,7 +2,7 @@
<packages>
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />

View File

@@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using EnvelopeGenerator.Domain.Constants;
using Newtonsoft.Json;
#if NETFRAMEWORK
using System;
@@ -107,6 +108,10 @@ public class Envelope
[Column("ENVELOPE_TYPE")]
public int? EnvelopeTypeId { get; set; }
[JsonIgnore]
[NotMapped]
public bool ReadOnly => EnvelopeTypeId == 2;
[Column("CERTIFICATION_TYPE")]
public int? CertificationType { get; set; }

View File

@@ -12,7 +12,7 @@ namespace EnvelopeGenerator.Infrastructure
public EGDbContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) // Önemli!
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.migration.json")
.Build();

View File

@@ -22,8 +22,8 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.5" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
<PackageReference Include="QuestPDF" Version="2025.7.1" />
</ItemGroup>

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Quartz" Version="3.9.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,151 @@
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Quartz;
namespace EnvelopeGenerator.Jobs.APIBackendJobs;
public class APIEnvelopeJob(ILogger<APIEnvelopeJob>? logger = null) : IJob
{
private readonly ILogger<APIEnvelopeJob> _logger = logger ?? NullLogger<APIEnvelopeJob>.Instance;
public async Task Execute(IJobExecutionContext context)
{
var jobId = context.JobDetail.Key.ToString();
_logger.LogDebug("API Envelopes - Starting job {JobId}", jobId);
try
{
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
if (string.IsNullOrWhiteSpace(connectionString))
{
_logger.LogWarning("API Envelopes - Connection string missing");
return;
}
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(context.CancellationToken);
await ProcessInvitationsAsync(connection, context.CancellationToken);
await ProcessWithdrawnAsync(connection, context.CancellationToken);
_logger.LogDebug("API Envelopes - Completed job {JobId} successfully", jobId);
}
catch (System.Exception ex)
{
_logger.LogError(ex, "API Envelopes job failed");
}
finally
{
_logger.LogDebug("API Envelopes execution for {JobId} ended", jobId);
}
}
private async Task ProcessInvitationsAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
{
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE SOURCE = 'API' AND STATUS = 1003 ORDER BY GUID";
var envelopeIds = new List<int>();
await using (var command = new SqlCommand(sql, connection))
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
if (reader[0] is int id)
{
envelopeIds.Add(id);
}
}
}
if (envelopeIds.Count == 0)
{
_logger.LogDebug("SendInvMail - No envelopes found");
return;
}
_logger.LogInformation("SendInvMail - Found {Count} envelopes", envelopeIds.Count);
var total = envelopeIds.Count;
var current = 1;
foreach (var id in envelopeIds)
{
_logger.LogInformation("SendInvMail - Processing Envelope {EnvelopeId} ({Current}/{Total})", id, current, total);
try
{
// Placeholder for invitation email sending logic.
_logger.LogDebug("SendInvMail - Marking envelope {EnvelopeId} as queued", id);
const string updateSql = "UPDATE TBSIG_ENVELOPE SET CURRENT_WORK_APP = @App WHERE GUID = @Id";
await using var updateCommand = new SqlCommand(updateSql, connection);
updateCommand.Parameters.AddWithValue("@App", "signFLOW_API_EnvJob_InvMail");
updateCommand.Parameters.AddWithValue("@Id", id);
await updateCommand.ExecuteNonQueryAsync(cancellationToken);
}
catch (System.Exception ex)
{
_logger.LogWarning(ex, "SendInvMail - Unhandled exception while working envelope {EnvelopeId}", id);
}
current++;
_logger.LogInformation("SendInvMail - Envelope finalized");
}
}
private async Task ProcessWithdrawnAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
{
const string sql = @"SELECT ENV.GUID, REJ.COMMENT AS REJECTION_REASON FROM
(SELECT * FROM TBSIG_ENVELOPE WHERE STATUS = 1009 AND SOURCE = 'API') ENV INNER JOIN
(SELECT MAX(GUID) GUID, ENVELOPE_ID, MAX(ADDED_WHEN) ADDED_WHEN, MAX(ACTION_DATE) ACTION_DATE, COMMENT FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 1009 GROUP BY ENVELOPE_ID, COMMENT ) REJ ON ENV.GUID = REJ.ENVELOPE_ID LEFT JOIN
(SELECT * FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 3004 ) M_Send ON ENV.GUID = M_Send.ENVELOPE_ID
WHERE M_Send.GUID IS NULL";
var withdrawn = new List<(int EnvelopeId, string Reason)>();
await using (var command = new SqlCommand(sql, connection))
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
var id = reader.GetInt32(0);
var reason = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
withdrawn.Add((id, reason));
}
}
if (withdrawn.Count == 0)
{
_logger.LogDebug("WithdrawnEnv - No envelopes found");
return;
}
_logger.LogInformation("WithdrawnEnv - Found {Count} envelopes", withdrawn.Count);
var total = withdrawn.Count;
var current = 1;
foreach (var (envelopeId, reason) in withdrawn)
{
_logger.LogInformation("WithdrawnEnv - Processing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
try
{
// Log withdrawn mail trigger placeholder
const string insertHistory = "INSERT INTO TBSIG_ENVELOPE_HISTORY (ENVELOPE_ID, STATUS, USER_REFERENCE, ADDED_WHEN, ACTION_DATE, COMMENT) VALUES (@EnvelopeId, @Status, @UserReference, GETDATE(), GETDATE(), @Comment)";
await using var insertCommand = new SqlCommand(insertHistory, connection);
insertCommand.Parameters.AddWithValue("@EnvelopeId", envelopeId);
insertCommand.Parameters.AddWithValue("@Status", 3004);
insertCommand.Parameters.AddWithValue("@UserReference", "API");
insertCommand.Parameters.AddWithValue("@Comment", reason ?? string.Empty);
await insertCommand.ExecuteNonQueryAsync(cancellationToken);
}
catch (System.Exception ex)
{
_logger.LogWarning(ex, "WithdrawnEnv - Unhandled exception while working envelope {EnvelopeId}", envelopeId);
}
current++;
_logger.LogInformation("WithdrawnEnv - Envelope finalized");
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Data;
namespace EnvelopeGenerator.Jobs;
public static class DataRowExtensions
{
public static T? GetValueOrDefault<T>(this DataRow row, string columnName, T? defaultValue = default)
{
if (!row.Table.Columns.Contains(columnName))
{
return defaultValue;
}
var value = row[columnName];
if (value == DBNull.Value)
{
return defaultValue;
}
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return defaultValue;
}
}
}

View File

@@ -0,0 +1,28 @@
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public static class FinalizeDocumentExceptions
{
public class MergeDocumentException : ApplicationException
{
public MergeDocumentException(string message) : base(message) { }
public MergeDocumentException(string message, Exception innerException) : base(message, innerException) { }
}
public class BurnAnnotationException : ApplicationException
{
public BurnAnnotationException(string message) : base(message) { }
public BurnAnnotationException(string message, Exception innerException) : base(message, innerException) { }
}
public class CreateReportException : ApplicationException
{
public CreateReportException(string message) : base(message) { }
public CreateReportException(string message, Exception innerException) : base(message, innerException) { }
}
public class ExportDocumentException : ApplicationException
{
public ExportDocumentException(string message) : base(message) { }
public ExportDocumentException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@@ -0,0 +1,229 @@
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Quartz;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class FinalizeDocumentJob : IJob
{
private readonly ILogger<FinalizeDocumentJob> _logger;
private readonly PDFBurner _pdfBurner;
private readonly PDFMerger _pdfMerger;
private readonly ReportCreator _reportCreator;
private record ConfigSettings(string DocumentPath, string DocumentPathOrigin, string ExportPath);
public FinalizeDocumentJob(
ILogger<FinalizeDocumentJob> logger,
PDFBurner pdfBurner,
PDFMerger pdfMerger,
ReportCreator reportCreator)
{
_logger = logger;
_pdfBurner = pdfBurner;
_pdfMerger = pdfMerger;
_reportCreator = reportCreator;
}
public async Task Execute(IJobExecutionContext context)
{
var jobId = context.JobDetail.Key.ToString();
_logger.LogDebug("Starting job {JobId}", jobId);
try
{
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
if (string.IsNullOrWhiteSpace(connectionString))
{
_logger.LogWarning("FinalizeDocument - Connection string missing");
return;
}
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(context.CancellationToken);
var config = await LoadConfigurationAsync(connection, context.CancellationToken);
var envelopes = await LoadCompletedEnvelopesAsync(connection, context.CancellationToken);
if (envelopes.Count == 0)
{
_logger.LogInformation("No completed envelopes found");
return;
}
var total = envelopes.Count;
var current = 1;
foreach (var envelopeId in envelopes)
{
_logger.LogInformation("Finalizing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
try
{
var envelopeData = await GetEnvelopeDataAsync(connection, envelopeId, context.CancellationToken);
if (envelopeData is null)
{
_logger.LogWarning("Envelope data not found for {EnvelopeId}", envelopeId);
continue;
}
var data = envelopeData.Value;
var envelope = new Envelope
{
Id = envelopeId,
Uuid = data.EnvelopeUuid ?? string.Empty,
Title = data.Title ?? string.Empty,
FinalEmailToCreator = (int)FinalEmailType.No,
FinalEmailToReceivers = (int)FinalEmailType.No
};
var burned = _pdfBurner.BurnAnnotsToPDF(data.DocumentBytes, data.AnnotationData, envelopeId);
var report = _reportCreator.CreateReport(connection, envelope);
var merged = _pdfMerger.MergeDocuments(burned, report);
var outputDirectory = Path.Combine(config.ExportPath, data.ParentFolderUid);
Directory.CreateDirectory(outputDirectory);
var outputPath = Path.Combine(outputDirectory, $"{envelope.Uuid}.pdf");
await File.WriteAllBytesAsync(outputPath, merged, context.CancellationToken);
await UpdateDocumentResultAsync(connection, envelopeId, merged, context.CancellationToken);
await ArchiveEnvelopeAsync(connection, envelopeId, context.CancellationToken);
}
catch (MergeDocumentException ex)
{
_logger.LogWarning(ex, "Certificate Document job failed at merging documents");
}
catch (ExportDocumentException ex)
{
_logger.LogWarning(ex, "Certificate Document job failed at exporting document");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while working envelope {EnvelopeId}", envelopeId);
}
current++;
_logger.LogInformation("Envelope {EnvelopeId} finalized", envelopeId);
}
_logger.LogDebug("Completed job {JobId} successfully", jobId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Certificate Document job failed");
}
finally
{
_logger.LogDebug("Job execution for {JobId} ended", jobId);
}
}
private async Task<ConfigSettings> LoadConfigurationAsync(SqlConnection connection, CancellationToken cancellationToken)
{
const string sql = "SELECT TOP 1 DOCUMENT_PATH, EXPORT_PATH FROM TBSIG_CONFIG";
await using var command = new SqlCommand(sql, connection);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (await reader.ReadAsync(cancellationToken))
{
var documentPath = reader.IsDBNull(0) ? string.Empty : reader.GetString(0);
var exportPath = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
return new ConfigSettings(documentPath, documentPath, exportPath);
}
return new ConfigSettings(string.Empty, string.Empty, Path.GetTempPath());
}
private async Task<List<int>> LoadCompletedEnvelopesAsync(SqlConnection connection, CancellationToken cancellationToken)
{
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE STATUS = @Status AND DATEDIFF(minute, CHANGED_WHEN, GETDATE()) >= 1 ORDER BY GUID";
var ids = new List<int>();
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeCompletelySigned);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
ids.Add(reader.GetInt32(0));
}
return ids;
}
private async Task<(int EnvelopeId, string? EnvelopeUuid, string? Title, byte[] DocumentBytes, List<string> AnnotationData, string ParentFolderUid)?> GetEnvelopeDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = @"SELECT T.GUID, T.ENVELOPE_UUID, T.TITLE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
WHERE T.GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await using var reader = await command.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
{
return null;
}
var envelopeUuid = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
var title = reader.IsDBNull(2) ? string.Empty : reader.GetString(2);
var filePath = reader.IsDBNull(3) ? string.Empty : reader.GetString(3);
var bytes = reader.IsDBNull(4) ? Array.Empty<byte>() : (byte[])reader[4];
await reader.CloseAsync();
if (bytes.Length == 0 && !string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
{
bytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
}
var annotations = await GetAnnotationDataAsync(connection, envelopeId, cancellationToken);
var parentFolderUid = !string.IsNullOrWhiteSpace(filePath)
? Path.GetFileName(Path.GetDirectoryName(filePath) ?? string.Empty)
: envelopeUuid;
return (envelopeId, envelopeUuid, title, bytes, annotations, parentFolderUid ?? envelopeUuid ?? envelopeId.ToString());
}
private async Task<List<string>> GetAnnotationDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = "SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = @EnvelopeId";
var result = new List<string>();
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
if (!reader.IsDBNull(0))
{
result.Add(reader.GetString(0));
}
}
return result;
}
private static async Task UpdateDocumentResultAsync(SqlConnection connection, int envelopeId, byte[] bytes, CancellationToken cancellationToken)
{
const string sql = "UPDATE TBSIG_ENVELOPE SET DOC_RESULT = @ImageData WHERE GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@ImageData", bytes);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
private static async Task ArchiveEnvelopeAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = "UPDATE TBSIG_ENVELOPE SET STATUS = @Status, CHANGED_WHEN = GETDATE() WHERE GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeArchived);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,277 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using iText.IO.Image;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Font;
using iText.Layout.Properties;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
using LayoutImage = iText.Layout.Element.Image;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFBurner
{
private static readonly FontProvider FontProvider = CreateFontProvider();
private readonly ILogger<PDFBurner> _logger;
private readonly PDFBurnerParams _pdfBurnerParams;
public PDFBurner() : this(NullLogger<PDFBurner>.Instance, new PDFBurnerParams())
{
}
public PDFBurner(ILogger<PDFBurner> logger, PDFBurnerParams? pdfBurnerParams = null)
{
_logger = logger;
_pdfBurnerParams = pdfBurnerParams ?? new PDFBurnerParams();
}
public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, IList<string> instantJsonList, int envelopeId)
{
if (sourceBuffer is null || sourceBuffer.Length == 0)
{
throw new BurnAnnotationException("Source document is empty");
}
try
{
using var inputStream = new MemoryStream(sourceBuffer);
using var outputStream = new MemoryStream();
using var reader = new PdfReader(inputStream);
using var writer = new PdfWriter(outputStream);
using var pdf = new PdfDocument(reader, writer);
foreach (var json in instantJsonList ?? Enumerable.Empty<string>())
{
if (string.IsNullOrWhiteSpace(json))
{
continue;
}
var annotationData = JsonConvert.DeserializeObject<AnnotationData>(json);
if (annotationData?.annotations is null)
{
continue;
}
annotationData.annotations.Reverse();
foreach (var annotation in annotationData.annotations)
{
try
{
switch (annotation.type)
{
case AnnotationType.Image:
AddImageAnnotation(pdf, annotation, annotationData.attachments);
break;
case AnnotationType.Ink:
AddInkAnnotation(pdf, annotation);
break;
case AnnotationType.Widget:
var formFieldValue = annotationData.formFieldValues?.FirstOrDefault(fv => fv.name == annotation.id);
if (formFieldValue is not null && !_pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value))
{
AddFormFieldValue(pdf, annotation, formFieldValue.value);
}
break;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error applying annotation {AnnotationId} on envelope {EnvelopeId}", annotation.id, envelopeId);
throw new BurnAnnotationException("Adding annotation failed", ex);
}
}
}
pdf.Close();
return outputStream.ToArray();
}
catch (BurnAnnotationException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to burn annotations for envelope {EnvelopeId}", envelopeId);
throw new BurnAnnotationException("Annotations could not be burned", ex);
}
}
private void AddImageAnnotation(PdfDocument pdf, Annotation annotation, Dictionary<string, Attachment>? attachments)
{
if (attachments is null || string.IsNullOrWhiteSpace(annotation.imageAttachmentId) || !attachments.TryGetValue(annotation.imageAttachmentId, out var attachment))
{
return;
}
var page = pdf.GetPage(annotation.pageIndex + 1);
var bounds = annotation.bbox.Select(ToInches).ToList();
var x = (float)bounds[0];
var y = (float)bounds[1];
var width = (float)bounds[2];
var height = (float)bounds[3];
var imageBytes = Convert.FromBase64String(attachment.binary);
var imageData = ImageDataFactory.Create(imageBytes);
var image = new LayoutImage(imageData)
.ScaleAbsolute(width, height)
.SetFixedPosition(annotation.pageIndex + 1, x, y);
using var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
canvas.Add(image);
}
private void AddInkAnnotation(PdfDocument pdf, Annotation annotation)
{
if (annotation.lines?.points is null)
{
return;
}
var page = pdf.GetPage(annotation.pageIndex + 1);
var canvas = new PdfCanvas(page);
var color = ParseColor(annotation.strokeColor);
canvas.SetStrokeColor(color);
canvas.SetLineWidth(1);
foreach (var segment in annotation.lines.points)
{
var first = true;
foreach (var point in segment)
{
var (px, py) = (ToInches(point[0]), ToInches(point[1]));
if (first)
{
canvas.MoveTo(px, py);
first = false;
}
else
{
canvas.LineTo(px, py);
}
}
canvas.Stroke();
}
}
private static FontProvider CreateFontProvider()
{
var provider = new FontProvider();
provider.AddStandardPdfFonts();
provider.AddSystemFonts();
return provider;
}
private void AddFormFieldValue(PdfDocument pdf, Annotation annotation, string value)
{
var bounds = annotation.bbox.Select(ToInches).ToList();
var x = (float)bounds[0];
var y = (float)bounds[1];
var width = (float)bounds[2];
var height = (float)bounds[3];
var page = pdf.GetPage(annotation.pageIndex + 1);
var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
canvas.SetProperty(Property.FONT_PROVIDER, FontProvider);
canvas.SetProperty(Property.FONT, FontProvider.GetFontSet());
var paragraph = new Paragraph(value)
.SetFontSize(_pdfBurnerParams.FontSize)
.SetFontColor(ColorConstants.BLACK)
.SetFontFamily(_pdfBurnerParams.FontName);
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Italic))
{
paragraph.SetItalic();
}
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Bold))
{
paragraph.SetBold();
}
canvas.ShowTextAligned(
paragraph,
x + (float)_pdfBurnerParams.TopMargin,
y + (float)_pdfBurnerParams.YOffset,
annotation.pageIndex + 1,
iText.Layout.Properties.TextAlignment.LEFT,
iText.Layout.Properties.VerticalAlignment.TOP,
0);
}
private static DeviceRgb ParseColor(string? color)
{
if (string.IsNullOrWhiteSpace(color))
{
return new DeviceRgb(0, 0, 0);
}
try
{
var drawingColor = ColorTranslator.FromHtml(color);
return new DeviceRgb(drawingColor.R, drawingColor.G, drawingColor.B);
}
catch
{
return new DeviceRgb(0, 0, 0);
}
}
private static double ToInches(double value) => value / 72d;
private static double ToInches(float value) => value / 72d;
#region Model
private static class AnnotationType
{
public const string Image = "pspdfkit/image";
public const string Ink = "pspdfkit/ink";
public const string Widget = "pspdfkit/widget";
}
private sealed class AnnotationData
{
public List<Annotation>? annotations { get; set; }
public Dictionary<string, Attachment>? attachments { get; set; }
public List<FormFieldValue>? formFieldValues { get; set; }
}
private sealed class Annotation
{
public string id { get; set; } = string.Empty;
public List<double> bbox { get; set; } = new();
public string type { get; set; } = string.Empty;
public string imageAttachmentId { get; set; } = string.Empty;
public Lines? lines { get; set; }
public int pageIndex { get; set; }
public string strokeColor { get; set; } = string.Empty;
public string egName { get; set; } = string.Empty;
}
private sealed class Lines
{
public List<List<List<float>>> points { get; set; } = new();
}
private sealed class Attachment
{
public string binary { get; set; } = string.Empty;
public string contentType { get; set; } = string.Empty;
}
private sealed class FormFieldValue
{
public string name { get; set; } = string.Empty;
public string value { get; set; } = string.Empty;
}
#endregion
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Drawing;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFBurnerParams
{
public List<string> IgnoredLabels { get; } = new() { "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" };
public double TopMargin { get; set; } = 0.1;
public double YOffset { get; set; } = -0.3;
public string FontName { get; set; } = "Arial";
public int FontSize { get; set; } = 8;
public FontStyle FontStyle { get; set; } = FontStyle.Italic;
}

View File

@@ -0,0 +1,46 @@
using System.IO;
using iText.Kernel.Pdf;
using iText.Kernel.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFMerger
{
private readonly ILogger<PDFMerger> _logger;
public PDFMerger() : this(NullLogger<PDFMerger>.Instance)
{
}
public PDFMerger(ILogger<PDFMerger> logger)
{
_logger = logger;
}
public byte[] MergeDocuments(byte[] document, byte[] report)
{
try
{
using var finalStream = new MemoryStream();
using var documentReader = new PdfReader(new MemoryStream(document));
using var reportReader = new PdfReader(new MemoryStream(report));
using var writer = new PdfWriter(finalStream);
using var targetDoc = new PdfDocument(documentReader, writer);
using var reportDoc = new PdfDocument(reportReader);
var merger = new PdfMerger(targetDoc);
merger.Merge(reportDoc, 1, reportDoc.GetNumberOfPages());
targetDoc.Close();
return finalStream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to merge PDF documents");
throw new MergeDocumentException("Documents could not be merged", ex);
}
}
}

View File

@@ -0,0 +1,91 @@
using System.Data;
using System.IO;
using EnvelopeGenerator.Domain.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
using LayoutDocument = iText.Layout.Document;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportCreator
{
private readonly ILogger<ReportCreator> _logger;
public ReportCreator() : this(NullLogger<ReportCreator>.Instance)
{
}
public ReportCreator(ILogger<ReportCreator> logger)
{
_logger = logger;
}
public byte[] CreateReport(SqlConnection connection, Envelope envelope)
{
try
{
var reportItems = LoadReportItems(connection, envelope.Id);
using var stream = new MemoryStream();
using var writer = new PdfWriter(stream);
using var pdf = new PdfDocument(writer);
using var document = new LayoutDocument(pdf);
document.Add(new Paragraph("Envelope Finalization Report").SetFontSize(16));
document.Add(new Paragraph($"Envelope Id: {envelope.Id}"));
document.Add(new Paragraph($"UUID: {envelope.Uuid}"));
document.Add(new Paragraph($"Title: {envelope.Title}"));
document.Add(new Paragraph($"Subject: {envelope.Comment}"));
document.Add(new Paragraph($"Generated: {DateTime.UtcNow:O}"));
document.Add(new Paragraph(" "));
var table = new Table(4).UseAllAvailableWidth();
table.AddHeaderCell("Date");
table.AddHeaderCell("Status");
table.AddHeaderCell("User");
table.AddHeaderCell("EnvelopeId");
foreach (var item in reportItems.OrderByDescending(r => r.ItemDate))
{
table.AddCell(item.ItemDate.ToString("u"));
table.AddCell(item.ItemStatus.ToString());
table.AddCell(item.ItemUserReference);
table.AddCell(item.EnvelopeId.ToString());
}
document.Add(table);
document.Close();
return stream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not create report for envelope {EnvelopeId}", envelope.Id);
throw new CreateReportException("Could not prepare report data", ex);
}
}
private List<ReportItem> LoadReportItems(SqlConnection connection, int envelopeId)
{
const string sql = "SELECT ENVELOPE_ID, POS_WHEN, POS_STATUS, POS_WHO FROM VWSIG_ENVELOPE_REPORT WHERE ENVELOPE_ID = @EnvelopeId";
var result = new List<ReportItem>();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new ReportItem
{
EnvelopeId = reader.GetInt32(0),
ItemDate = reader.IsDBNull(1) ? DateTime.MinValue : reader.GetDateTime(1),
ItemStatus = reader.IsDBNull(2) ? default : (EnvelopeGenerator.Domain.Constants.EnvelopeStatus)reader.GetInt32(2),
ItemUserReference = reader.IsDBNull(3) ? string.Empty : reader.GetString(3)
});
}
return result;
}
}

View File

@@ -0,0 +1,19 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportItem
{
public Envelope? Envelope { get; set; }
public int EnvelopeId { get; set; }
public string EnvelopeTitle { get; set; } = string.Empty;
public string EnvelopeSubject { get; set; } = string.Empty;
public EnvelopeStatus ItemStatus { get; set; }
public string ItemStatusTranslated => ItemStatus.ToString();
public string ItemUserReference { get; set; } = string.Empty;
public DateTime ItemDate { get; set; }
}

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportSource
{
public List<ReportItem> Items { get; set; } = new();
}

View File

@@ -5,6 +5,7 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Tests.Application;
@@ -44,9 +45,15 @@ public class DocSignedNotificationTests : TestBase
// Create envelope receiver
var envRcv = this.CreateEnvelopeReceiver(env!.Id, rcv.Id);
envRcv = await Repository.CreateAsync(envRcv, cancel);
var repo = GetRepository<EnvelopeReceiver>();
envRcv = await repo.CreateAsync(envRcv, cancel);
var envRcvDto = _mapper.Map<EnvelopeReceiverDto>(envRcv);
var docSignedNtf = envRcvDto.ToDocSignedNotification(new () { });
var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots);
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();

View File

@@ -1,8 +1,10 @@
using Bogus;
using CommandDotNet;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Histories.Commands;
@@ -11,6 +13,7 @@ using EnvelopeGenerator.Application.Users.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure;
using EnvelopeGenerator.Tests.Application;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -20,7 +23,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Tests.Application;
@@ -42,10 +44,29 @@ public class Fake
// add Application and Infrastructure services
#pragma warning disable CS0618
services.AddEnvelopeGeneratorServices(configuration);
services.AddEnvelopeGeneratorInfrastructureServices(
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
context.Configuration
);
var cnnStrName = "Default";
var connStr = context.Configuration.GetConnectionString(cnnStrName)
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
{
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase("EnvelopeGeneratorTestDb"));
opt.AddDbTriggerParams(context.Configuration);
opt.AddDbContext((provider, options) =>
{
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
options.UseSqlServer(connStr)
.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
});
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
});
var prodCnnStr = context.Configuration.GetConnectionString("Default");
services.AddDbContext<EGDbContext2Prod>(opt => opt.UseSqlServer(prodCnnStr));

View File

@@ -24,6 +24,8 @@ public abstract class TestBase : Faker
protected IRepository Repository => Host.Services.GetRequiredService<IRepository>();
protected IServiceProvider Services => Host.Services;
protected abstract void ConfigureServices(IServiceCollection services);
[SetUp]
@@ -32,9 +34,11 @@ public abstract class TestBase : Faker
Host = Fake.CreateHost(ConfigureServices);
await Host.AddSamples();
var repo = GetRepository<EmailTemplate>();
// Add seed email templates
foreach (var temp in SeedEmailTemplates)
await Repository.CreateAsync(temp);
await repo.CreateAsync(temp);
}
[TearDown]

View File

@@ -0,0 +1,17 @@
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

@@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />

View File

@@ -44,7 +44,7 @@ public class AnnotationController : ControllerBase
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation psPdfKitAnnotation, CancellationToken cancel = default)
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
{
// get claims
var signature = User.GetAuthReceiverSignature();
@@ -56,6 +56,12 @@ public class AnnotationController : ControllerBase
return Unauthorized("User authentication is incomplete. Missing required claims for processing this request.");
}
// check if non read-and-confirm envelope is signed without annotation
var er = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
if (!er.Envelope!.ReadOnly && psPdfKitAnnotation is null)
return BadRequest();
// Again check if receiver has already signed
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
return Problem(statusCode: 409);

View File

@@ -248,7 +248,9 @@ public class EnvelopeController : ViewControllerBase
{
if (er.Envelope!.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
{
using var pdf = Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
using var pdf = er.Envelope.ReadOnly
? Pdf.FromMemory(doc.ByteData)
: Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
doc.ByteData = pdf.ExportAsBytes();
@@ -262,6 +264,8 @@ public class EnvelopeController : ViewControllerBase
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
ViewData["ReadAndConfirm"] = er.Envelope.ReadOnly;
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Authentication.Cookies;
namespace EnvelopeGenerator.Web;
public class EnvelopeCookieManager : ICookieManager
{
private readonly IEnumerable<string> _envelopeKeyBasedCookieNames;
private readonly ChunkingCookieManager _inner = new();
public EnvelopeCookieManager(params string[] envelopeKeyBasedCookieNames)
{
_envelopeKeyBasedCookieNames = envelopeKeyBasedCookieNames;
}
private string GetCookieName(HttpContext context, string key)
{
if (!_envelopeKeyBasedCookieNames.Contains(key))
return key;
var envId = context.GetRouteValue("envelopeReceiverId")?.ToString();
if (string.IsNullOrEmpty(envId) && context.Request.Query.TryGetValue("envKey", out var envKeyValue))
envId = envKeyValue;
if (string.IsNullOrEmpty(envId))
return key;
return $"{key}-{envId}";
}
public string? GetRequestCookie(HttpContext context, string key)
{
var cookieName = GetCookieName(context, key);
return _inner.GetRequestCookie(context, cookieName);
}
public void AppendResponseCookie(HttpContext context, string key, string? value, CookieOptions options)
{
var cookieName = GetCookieName(context, key);
_inner.AppendResponseCookie(context, cookieName, value, options);
}
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
{
var cookieName = GetCookieName(context, key);
_inner.DeleteCookie(context, cookieName, options);
}
}

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.7.0</Version>
<AssemblyVersion>3.7.0</AssemblyVersion>
<FileVersion>3.7.0</FileVersion>
<Version>3.9.0</Version>
<AssemblyVersion>3.9.0</AssemblyVersion>
<FileVersion>3.9.0</FileVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup>

View File

@@ -17,6 +17,7 @@ using EnvelopeGenerator.Web.Models.Annotation;
using DigitalData.UserManager.DependencyInjection;
using EnvelopeGenerator.Web.Middleware;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Web;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
@@ -134,41 +135,22 @@ try
options.ConsentCookie.Name = "cookie-consent-settings";
});
var authCookieName = "env_auth";
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
options.Cookie.Name = authCookieName;
options.CookieManager = new EnvelopeCookieManager(authCookieName);
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Strict;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = context =>
{
// Dynamically calculate the redirection path, for example:
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
},
OnRedirectToLogout = context =>
{
// Apply a similar redirection logic for logout
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
}
};
});
builder.Services.AddSingleton(config.GetSection("ContactLink").Get<ContactLink>() ?? new());
builder.Services.AddCookieBasedLocalizer();
builder.Services.AddSingleton(HtmlEncoder.Default);
builder.Services.AddSingleton(UrlEncoder.Default);
builder.Services.AddSanitizer<HtmlSanitizer>();
@@ -249,7 +231,7 @@ try
app.UseAuthorization();
var cultures = app.Services.GetRequiredService<Cultures>();
if(!cultures.Any())
if (!cultures.Any())
throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
@@ -264,7 +246,7 @@ try
}
]");
if(!config.GetValue<bool>("DisableMultiLanguage"))
if (!config.GetValue<bool>("DisableMultiLanguage"))
app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
app.UseCors("SameOriginPolicy");
@@ -273,7 +255,7 @@ try
app.MapFallbackToController("Error404", "Home");
app.Run();
}
catch(Exception ex)
catch (Exception ex)
{
logger.Error(ex, "Stopped program because of exception");
throw;

View File

@@ -43,12 +43,14 @@
</svg>
<span>@_localizer.Reject()</span>
</button>
@if(!Model.Envelope!.ReadOnly){
<button class="btn_refresh btn btn-secondary btn-desktop" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
</svg>
</button>
}
</div>
}
<div class="dd-cards-container">

View File

@@ -42,6 +42,8 @@
const IS_DESKTOP_SIZE = DEVICE_SCREEN_TYPE == 'desktop'
const IS_MOBILE_DEVICE = /Mobi|Android/i.test(window.navigator.userAgent);
const READ_AND_CONFIRM = @((ViewData["ReadAndConfirm"] is bool readAndConfirm && readAndConfirm).ToString().ToLower())
</script>
<script src="~/lib/jquery/dist/jquery.min.js"></script>

View File

@@ -35,15 +35,18 @@ class App {
});
// Load annotations into PSPDFKit
try {
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
await this.pdfKit.create(signatures);
} catch (e) {
console.error("Error loading annotations:", e);
}
if (!READ_AND_CONFIRM)
try {
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
await this.pdfKit.create(signatures);
} catch (e) {
console.error("Error loading annotations:", e);
}
//add click events of external buttons
[...document.getElementsByClassName('btn_refresh')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('RESET')));
if (!READ_AND_CONFIRM) {
[...document.getElementsByClassName('btn_refresh')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('RESET')));
}
[...document.getElementsByClassName('btn_complete')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('FINISH')));
[...document.getElementsByClassName('btn_reject')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('REJECT')));
}
@@ -182,45 +185,70 @@ class App {
}
async handleFinish(event) {
const iJSON = await this.pdfKit.exportInstantJSON()
const iFormFieldValues = iJSON.formFieldValues;
let annotResult = undefined;
//check required
const iReqFields = iFormFieldValues.filter(f => isFieldRequired(f))
const hasEmptyReq = iReqFields.some(f => (f.value === undefined || f.value === null || f.value === ""))
// READ_AND_CONFIRM flow: require all pages viewed, skip annotation validations
if (READ_AND_CONFIRM) {
const allViewed = JSON.parse(sessionStorage.getItem('pspdf_all_pages_rendered') || 'false') === true
if (!allViewed) {
const unviewed = JSON.parse(sessionStorage.getItem('pspdf_unviewed_pages') || '[]')
const message = unviewed.length
? `Bitte sehen Sie sich die folgenden Seiten an: ${unviewed.join(', ')}`
: 'Bitte sehen Sie sich alle Seiten an.'
if (hasEmptyReq) {
Swal.fire({
title: 'Warnung',
text: 'Bitte füllen Sie alle Standortinformationen vollständig aus!',
icon: 'warning',
})
return false;
await Swal.fire({
title: 'Warnung',
text: message,
icon: 'warning'
})
return false
}
}
else {
const iJSON = await this.pdfKit.exportInstantJSON()
//check city
const city_regex = new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$")
const iCityFields = iFormFieldValues.filter(f => isCityField(f))
for (var f of iCityFields)
if (!IS_MOBILE_DEVICE && !city_regex.test(f.value)) {
const iFormFieldValues = iJSON.formFieldValues;
//check required
const iReqFields = iFormFieldValues.filter(f => isFieldRequired(f))
const hasEmptyReq = iReqFields.some(f => (f.value === undefined || f.value === null || f.value === ""))
if (hasEmptyReq) {
Swal.fire({
title: 'Warnung',
text: `Bitte überprüfen Sie die eingegebene Ortsangabe "${f.value}" auf korrekte Formatierung. Beispiele für richtige Formate sind: München, Île-de-France, Sauðárkrókur, San Francisco, St. Catharines usw.`,
text: 'Bitte füllen Sie alle Standortinformationen vollständig aus!',
icon: 'warning',
})
return false;
}
//check # of signature
const validationResult = await this.validateAnnotations(this.signatureCount)
if (validationResult === false) {
Swal.fire({
title: 'Warnung',
text: 'Es wurden nicht alle Signaturfelder ausgefüllt!',
icon: 'warning',
})
return false
//check city
const city_regex = new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$")
const iCityFields = iFormFieldValues.filter(f => isCityField(f))
for (var f of iCityFields)
if (!IS_MOBILE_DEVICE && !city_regex.test(f.value)) {
Swal.fire({
title: 'Warnung',
text: `Bitte überprüfen Sie die eingegebene Ortsangabe "${f.value}" auf korrekte Formatierung. Beispiele für richtige Formate sind: München, Île-de-France, Sauðárkrókur, San Francisco, St. Catharines usw.`,
icon: 'warning',
})
return false;
}
//check # of signature
const validationResult = await this.validateAnnotations(this.signatureCount)
if (validationResult === false) {
Swal.fire({
title: 'Warnung',
text: 'Es wurden nicht alle Signaturfelder ausgefüllt!',
icon: 'warning',
})
return false
}
// set annot-result if all validations passed
annotResult = { instant: iJSON, structured: mapSignature(iJSON) };
}
return Swal.fire({
@@ -249,10 +277,7 @@ class App {
// Export annotation data and save to database
try {
const res = await signEnvelope({
instant: iJSON,
structured: mapSignature(iJSON)
});
const res = READ_AND_CONFIRM ? await signEnvelope() : await signEnvelope(annotResult);
if (!res.ok) {
if (res.status === 409) {

View File

@@ -1,3 +1,3 @@
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(){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()});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)}[...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:"Erfolg",text:"Dokument wurde zurückgesetzt",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(`
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:"Erfolg",text:"Dokument wurde zurückgesetzt",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("Kopiert",{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify("Unerwarteter Fehler",{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){const n=await this.pdfKit.exportInstantJSON(),t=n.formFieldValues,r=t.filter(n=>isFieldRequired(n)),u=r.some(n=>n.value===undefined||n.value===null||n.value==="");if(u)return Swal.fire({title:"Warnung",text:"Bitte füllen Sie alle Standortinformationen vollständig aus!",icon:"warning"}),!1;const f=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),e=t.filter(n=>isCityField(n));for(var i of e)if(!IS_MOBILE_DEVICE&&!f.test(i.value))return Swal.fire({title:"Warnung",text:`Bitte überprüfen Sie die eingegebene Ortsangabe "${i.value}" auf korrekte Formatierung. Beispiele für richtige Formate sind: München, Île-de-France, Sauðárkrókur, San Francisco, St. Catharines usw.`,icon:"warning"}),!1;const o=await this.validateAnnotations(this.signatureCount);return o===!1?(Swal.fire({title:"Warnung",text:"Es wurden nicht alle Signaturfelder ausgefüllt!",icon:"warning"}),!1):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:"Fehler",text:"Umschlag konnte nicht signiert werden!",icon:"error"}),!1}try{const t=await signEnvelope({instant:n,structured:mapSignature(n)});if(t.ok)return!0;if(t.status===409)return Swal.fire({title:"Warnung",text:"Umschlag ist nicht mehr verfügbar.",icon:"warning"}),!1;if(t.status===423)Swal.fire({title:"Info",text:"Dokument wurde von einem Empfänger abgelehnt. Sie werden weitergeleitet...",icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:"Fehler",text:"Umschlag konnte nicht signiert werden!",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:"Sind sie sicher?",text:"Wollen Sie das Dokument und alle erstellten Signaturen zurücksetzen?",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("Kopiert",{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify("Unerwarteter Fehler",{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?`Bitte sehen Sie sich die folgenden Seiten an: ${n.join(", ")}`:"Bitte sehen Sie sich alle Seiten an.";return await Swal.fire({title:"Warnung",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:"Warnung",text:"Bitte füllen Sie alle Standortinformationen vollständig aus!",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:"Warnung",text:`Bitte überprüfen Sie die eingegebene Ortsangabe "${t.value}" auf korrekte Formatierung. Beispiele für richtige Formate sind: München, Île-de-France, Sauðárkrókur, San Francisco, St. Catharines usw.`,icon:"warning"}),!1;const s=await this.validateAnnotations(this.signatureCount);if(s===!1)return Swal.fire({title:"Warnung",text:"Es wurden nicht alle Signaturfelder ausgefüllt!",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:"Fehler",text:"Umschlag konnte nicht signiert werden!",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:"Warnung",text:"Umschlag ist nicht mehr verfügbar.",icon:"warning"}),!1;if(t.status===423)Swal.fire({title:"Info",text:"Dokument wurde von einem Empfänger abgelehnt. Sie werden weitergeleitet...",icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:"Fehler",text:"Umschlag konnte nicht signiert werden!",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:"Sind sie sicher?",text:"Wollen Sie das Dokument und alle erstellten Signaturen zurücksetzen?",icon:"question",showCancelButton:!0});if(n.isConfirmed){const n=await deleteAnnotations(this.pdfKit)}return n}fakeElementId=0;}

View File

@@ -1,106 +1,111 @@
//#region parameters
const env = Object.freeze({
__lazyXsrfToken: new Lazy(() => document.getElementsByName('__RequestVerificationToken')[0].value),
get xsrfToken() {
return this.__lazyXsrfToken.value;
}
__lazyXsrfToken: new Lazy(() => document.getElementsByName('__RequestVerificationToken')[0].value),
get xsrfToken() {
return this.__lazyXsrfToken.value;
}
})
const url = Object.freeze({
reject: `/api/annotation/reject`,
share: `/api/readonly`
reject: `/api/annotation/reject`,
share: `/api/readonly`
});
//#endregion
//#region request helper methods
function sendRequest(method, url, body = undefined) {
const options = {
credentials: 'include',
method: method,
headers: {
'X-XSRF-TOKEN': env.xsrfToken
const urlObj = new URL(url, window.location.origin);
if (!urlObj.searchParams.has("envKey")) {
urlObj.searchParams.set("envKey", ENV_KEY);
}
}
if (body !== undefined) {
options.body = JSON.stringify(body);
options.headers['Content-Type'] = 'application/json';
}
const options = {
credentials: 'include',
method: method,
headers: {
'X-XSRF-TOKEN': env.xsrfToken
}
}
return fetch(url, options);
if (body !== undefined) {
options.body = JSON.stringify(body);
options.headers['Content-Type'] = 'application/json';
}
return fetch(urlObj, options);
}
function getRequest(url) {
return sendRequest('GET', url);
return sendRequest('GET', url);
}
function getJson(url) {
return sendRequest('GET', url).then(res => {
if (res.ok)
return res.json();
throw new Error(`Request failed with status ${res.status}`);
});
return sendRequest('GET', url).then(res => {
if (res.ok)
return res.json();
throw new Error(`Request failed with status ${res.status}`);
});
}
function postRequest(url, body = undefined) {
return sendRequest('POST', url, body);
return sendRequest('POST', url, body);
}
function reload() {
window.location.reload();
window.location.reload();
}
function redirect(url) {
window.location.href = url;
window.location.href = url;
}
//#endregion
//#region envelope
function signEnvelope(annotations) {
return postRequest(`/api/annotation`, annotations)
return postRequest(`/api/annotation`, annotations)
}
async function getAnnotationParams(leftInInch = 0, topInInch = 0, inchToPointFactor = 72) {
const annotParams = await getJson("/api/Config/Annotations");
const annotParams = await getJson("/api/Config/Annotations");
for (var key in annotParams) {
var annot = annotParams[key];
annot.width *= inchToPointFactor;
annot.height *= inchToPointFactor;
annot.left += leftInInch - 0.7;
annot.left *= inchToPointFactor;
annot.top += topInInch - 0.5;
annot.top *= inchToPointFactor;
}
for (var key in annotParams) {
var annot = annotParams[key];
annot.width *= inchToPointFactor;
annot.height *= inchToPointFactor;
annot.left += leftInInch - 0.7;
annot.left *= inchToPointFactor;
annot.top += topInInch - 0.5;
annot.top *= inchToPointFactor;
}
return annotParams;
return annotParams;
}
function rejectEnvelope(reason) {
return postRequest(url.reject, reason);
return postRequest(url.reject, reason);
}
function shareEnvelope(receiverMail, dateValid) {
return postRequest(url.share, { receiverMail: receiverMail, dateValid: dateValid });
return postRequest(url.share, { receiverMail: receiverMail, dateValid: dateValid });
}
//#endregion
async function setLanguage(language) {
const hasLang = await getJson('/api/localization/lang')
.then(langs => langs.includes(language));
const hasLang = await getJson('/api/localization/lang')
.then(langs => langs.includes(language));
if (hasLang)
postRequest(`/api/localization/lang/${language}`)
.then(response => {
if (response.redirected)
redirect(response.url);
});
if (hasLang)
postRequest(`/api/localization/lang/${language}`)
.then(response => {
if (response.redirected)
redirect(response.url);
});
}
function logout() {
return postRequest(`/auth/logout`)
.then(res => {
if (res.ok)
window.location.href = "/";
});
return postRequest(`/auth/logout`)
.then(res => {
if (res.ok)
window.location.href = "/";
});
}

View File

@@ -1 +1 @@
function sendRequest(n,t,i=undefined){const r={credentials:"include",method:n,headers:{"X-XSRF-TOKEN":env.xsrfToken}};return i!==undefined&&(r.body=JSON.stringify(i),r.headers["Content-Type"]="application/json"),fetch(t,r)}function getRequest(n){return sendRequest("GET",n)}function getJson(n){return sendRequest("GET",n).then(n=>{if(n.ok)return n.json();throw new Error(`Request failed with status ${n.status}`);})}function postRequest(n,t=undefined){return sendRequest("POST",n,t)}function reload(){window.location.reload()}function redirect(n){window.location.href=n}function signEnvelope(n){return postRequest(`/api/annotation`,n)}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await getJson("/api/Config/Annotations");for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}function rejectEnvelope(n){return postRequest(url.reject,n)}function shareEnvelope(n,t){return postRequest(url.share,{receiverMail:n,dateValid:t})}async function setLanguage(n){const t=await getJson("/api/localization/lang").then(t=>t.includes(n));t&&postRequest(`/api/localization/lang/${n}`).then(n=>{n.redirected&&redirect(n.url)})}function logout(){return postRequest(`/auth/logout`).then(n=>{n.ok&&(window.location.href="/")})}const env=Object.freeze({__lazyXsrfToken:new Lazy(()=>document.getElementsByName("__RequestVerificationToken")[0].value),get xsrfToken(){return this.__lazyXsrfToken.value}}),url=Object.freeze({reject:`/api/annotation/reject`,share:`/api/readonly`});
function sendRequest(n,t,i=undefined){const r=new URL(t,window.location.origin);r.searchParams.has("envKey")||r.searchParams.set("envKey",ENV_KEY);const u={credentials:"include",method:n,headers:{"X-XSRF-TOKEN":env.xsrfToken}};return i!==undefined&&(u.body=JSON.stringify(i),u.headers["Content-Type"]="application/json"),fetch(r,u)}function getRequest(n){return sendRequest("GET",n)}function getJson(n){return sendRequest("GET",n).then(n=>{if(n.ok)return n.json();throw new Error(`Request failed with status ${n.status}`);})}function postRequest(n,t=undefined){return sendRequest("POST",n,t)}function reload(){window.location.reload()}function redirect(n){window.location.href=n}function signEnvelope(n){return postRequest(`/api/annotation`,n)}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await getJson("/api/Config/Annotations");for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}function rejectEnvelope(n){return postRequest(url.reject,n)}function shareEnvelope(n,t){return postRequest(url.share,{receiverMail:n,dateValid:t})}async function setLanguage(n){const t=await getJson("/api/localization/lang").then(t=>t.includes(n));t&&postRequest(`/api/localization/lang/${n}`).then(n=>{n.redirected&&redirect(n.url)})}function logout(){return postRequest(`/auth/logout`).then(n=>{n.ok&&(window.location.href="/")})}const env=Object.freeze({__lazyXsrfToken:new Lazy(()=>document.getElementsByName("__RequestVerificationToken")[0].value),get xsrfToken(){return this.__lazyXsrfToken.value}}),url=Object.freeze({reject:`/api/annotation/reject`,share:`/api/readonly`});

View File

@@ -16,7 +16,45 @@
isEditableAnnotation: function (annotation) {
return !(annotation.isSignature || annotation.description === 'FRAME')
},
});
}).then((instance) => {
if (READ_AND_CONFIRM) {
const totalPages = instance.totalPageCount || 0
const storageKeyAll = 'pspdf_all_pages_rendered'
const storageKeyUnviewed = 'pspdf_unviewed_pages'
let unviewed = totalPages > 0 ? Array.from({ length: totalPages }, (_, i) => i + 1) : []
const saveState = () => {
sessionStorage.setItem(storageKeyUnviewed, JSON.stringify(unviewed))
sessionStorage.setItem(storageKeyAll, JSON.stringify(unviewed.length === 0 && totalPages > 0))
}
const markPageViewed = (pageIndex) => {
const pageNumber = pageIndex + 1
if (pageNumber < 1 || pageNumber > totalPages) return
const idx = unviewed.indexOf(pageNumber)
if (idx >= 0) {
unviewed.splice(idx, 1)
saveState()
}
}
// initial state in session storage
saveState()
// mark the initially visible page
const initialPage = instance.viewState?.currentPageIndex ?? 0
markPageViewed(initialPage)
instance.addEventListener('viewState.currentPageIndex.change', (pageIndex) => {
console.log('Active page:', pageIndex + 1)
markPageViewed(pageIndex)
})
}
return instance
})
}
const allowedToolbarItems = [
@@ -103,7 +141,7 @@ function getReadOnlyItems(callback) {
}
function getMobileWritableItems(callback) {
return [
const items = [
{
type: 'custom',
id: 'button-finish',
@@ -127,8 +165,11 @@ function getMobileWritableItems(callback) {
icon: `<svg width="25px" height="25px" viewBox="43.5 43.5 512 512" version="1.1" fill="currentColor" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path class="st0" d="M263.24,43.5c-117.36,0-212.5,95.14-212.5,212.5s95.14,212.5,212.5,212.5s212.5-95.14,212.5-212.5 S380.6,43.5,263.24,43.5z M367.83,298.36c17.18,17.18,17.18,45.04,0,62.23v0c-17.18,17.18-45.04,17.18-62.23,0l-42.36-42.36 l-42.36,42.36c-17.18,17.18-45.04,17.18-62.23,0v0c-17.18-17.18-17.18-45.04,0-62.23L201.01,256l-42.36-42.36 c-17.18-17.18-17.18-45.04,0-62.23v0c17.18-17.18,45.04-17.18,62.23,0l42.36,42.36l42.36-42.36c17.18-17.18,45.04-17.18,62.23,0v0 c17.18,17.18,17.18,45.04,0,62.23L325.46,256L367.83,298.36z" />
</svg>`,
},
{
}
]
if (!READ_AND_CONFIRM) {
items.push({
type: 'custom',
id: 'button-reset',
className: 'button-reset',
@@ -139,9 +180,11 @@ function getMobileWritableItems(callback) {
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="-1 -1 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>
</svg>`,
}
];
</svg>`
})
}
return items
}
function getPresets() {

View File

@@ -1,4 +1,4 @@
function loadPSPDFKit(n,t,i,r){return PSPDFKit.load({inlineWorkers:!1,locale:r,licenseKey:i,styleSheets:["/css/site.css"],container:t,document:n,annotationPresets:getPresets(),electronicSignatures:{creationModes:["DRAW","TYPE","IMAGE"]},initialViewState:new PSPDFKit.ViewState({sidebarMode:PSPDFKit.SidebarMode.THUMBNAILS}),isEditableAnnotation:function(n){return!(n.isSignature||n.description==="FRAME")}})}function addToolbarItems(n,t){var i=n.toolbarItems.filter(n=>allowedToolbarItems.includes(n.type));i=IS_READONLY?i.concat(getReadOnlyItems(t)):i.concat(getWritableItems(t));IS_DESKTOP_SIZE||IS_READONLY||(i=i.concat(getMobileWritableItems(t)));n.setToolbarItems(i)}function getWritableItems(n){return[{type:"custom",id:"button-share",className:"button-share",title:"Teilen",onPress(){n("SHARE")},icon:`<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
function loadPSPDFKit(n,t,i,r){return PSPDFKit.load({inlineWorkers:!1,locale:r,licenseKey:i,styleSheets:["/css/site.css"],container:t,document:n,annotationPresets:getPresets(),electronicSignatures:{creationModes:["DRAW","TYPE","IMAGE"]},initialViewState:new PSPDFKit.ViewState({sidebarMode:PSPDFKit.SidebarMode.THUMBNAILS}),isEditableAnnotation:function(n){return!(n.isSignature||n.description==="FRAME")}}).then(n=>{if(READ_AND_CONFIRM){const t=n.totalPageCount||0,f="pspdf_all_pages_rendered",e="pspdf_unviewed_pages";let i=t>0?Array.from({length:t},(n,t)=>t+1):[];const r=()=>{sessionStorage.setItem(e,JSON.stringify(i)),sessionStorage.setItem(f,JSON.stringify(i.length===0&&t>0))},u=n=>{const u=n+1;if(!(u<1)&&!(u>t)){const f=i.indexOf(u);f>=0&&(i.splice(f,1),r())}};r();const o=n.viewState?.currentPageIndex??0;u(o);n.addEventListener("viewState.currentPageIndex.change",n=>{console.log("Active page:",n+1),u(n)})}return n})}function addToolbarItems(n,t){var i=n.toolbarItems.filter(n=>allowedToolbarItems.includes(n.type));i=IS_READONLY?i.concat(getReadOnlyItems(t)):i.concat(getWritableItems(t));IS_DESKTOP_SIZE||IS_READONLY||(i=i.concat(getMobileWritableItems(t)));n.setToolbarItems(i)}function getWritableItems(n){return[{type:"custom",id:"button-share",className:"button-share",title:"Teilen",onPress(){n("SHARE")},icon:`<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 13V17.5C20 20.5577 16 20.5 12 20.5C8 20.5 4 20.5577 4 17.5V13M12 3L12 15M12 3L16 7M12 3L8 7" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`},{type:"custom",id:"button-logout",className:"button-logout",title:"logout",onPress(){n("LOGOUT")},icon:`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-box-arrow-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6 12.5a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v2a.5.5 0 0 1-1 0v-2A1.5 1.5 0 0 1 6.5 2h8A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 5 12.5v-2a.5.5 0 0 1 1 0z"/>
@@ -6,12 +6,12 @@ function loadPSPDFKit(n,t,i,r){return PSPDFKit.load({inlineWorkers:!1,locale:r,l
</svg>`},{type:"custom",id:"mock",className:"mock",title:"Mock",icon:`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-box-arrow-left" viewBox="0 0 16 16"></svg>`}]}function getReadOnlyItems(n){return[{type:"custom",id:"button-copy-url",className:"button-copy-url",title:"Teilen",onPress(){n("COPY_URL")},icon:`<svg viewBox="4 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 3H9C6.79086 3 5 4.79086 5 7V15" stroke="#222222"/>
<path d="M8.5 11.5C8.5 10.3156 8.50074 9.46912 8.57435 8.81625C8.64681 8.17346 8.78457 7.78051 9.01662 7.4781C9.14962 7.30477 9.30477 7.14962 9.4781 7.01662C9.78051 6.78457 10.1735 6.64681 10.8163 6.57435C11.4691 6.50074 12.3156 6.5 13.5 6.5C14.6844 6.5 15.5309 6.50074 16.1837 6.57435C16.8265 6.64681 17.2195 6.78457 17.5219 7.01662C17.6952 7.14962 17.8504 7.30477 17.9834 7.4781C18.2154 7.78051 18.3532 8.17346 18.4257 8.81625C18.4993 9.46912 18.5 10.3156 18.5 11.5V15.5C18.5 16.6844 18.4993 17.5309 18.4257 18.1837C18.3532 18.8265 18.2154 19.2195 17.9834 19.5219C17.8504 19.6952 17.6952 19.8504 17.5219 19.9834C17.2195 20.2154 16.8265 20.3532 16.1837 20.4257C15.5309 20.4993 14.6844 20.5 13.5 20.5C12.3156 20.5 11.4691 20.4993 10.8163 20.4257C10.1735 20.3532 9.78051 20.2154 9.4781 19.9834C9.30477 19.8504 9.14962 19.6952 9.01662 19.5219C8.78457 19.2195 8.64681 18.8265 8.57435 18.1837C8.50074 17.5309 8.5 16.6844 8.5 15.5V11.5Z" stroke="#222222"/>
</svg>`}]}function getMobileWritableItems(n){return[{type:"custom",id:"button-finish",className:"button-finish",onPress(){n("FINISH")},icon:`<svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="-4 -4 26 26">
</svg>`}]}function getMobileWritableItems(n){const t=[{type:"custom",id:"button-finish",className:"button-finish",onPress(){n("FINISH")},icon:`<svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="-4 -4 26 26">
<path d="m10.036 8.278 9.258-7.79A1.979 1.979 0 0 0 18 0H2A1.987 1.987 0 0 0 .641.541l9.395 7.737Z" />
<path d="M11.241 9.817c-.36.275-.801.425-1.255.427-.428 0-.845-.138-1.187-.395L0 2.6V14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2.5l-8.759 7.317Z" />
</svg>`},{type:"custom",id:"button-reject",className:"button-reject",title:"Ablehnen",onPress(){n("REJECT")},icon:`<svg width="25px" height="25px" viewBox="43.5 43.5 512 512" version="1.1" fill="currentColor" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path class="st0" d="M263.24,43.5c-117.36,0-212.5,95.14-212.5,212.5s95.14,212.5,212.5,212.5s212.5-95.14,212.5-212.5 S380.6,43.5,263.24,43.5z M367.83,298.36c17.18,17.18,17.18,45.04,0,62.23v0c-17.18,17.18-45.04,17.18-62.23,0l-42.36-42.36 l-42.36,42.36c-17.18,17.18-45.04,17.18-62.23,0v0c-17.18-17.18-17.18-45.04,0-62.23L201.01,256l-42.36-42.36 c-17.18-17.18-17.18-45.04,0-62.23v0c17.18-17.18,45.04-17.18,62.23,0l42.36,42.36l42.36-42.36c17.18-17.18,45.04-17.18,62.23,0v0 c17.18,17.18,17.18,45.04,0,62.23L325.46,256L367.83,298.36z" />
</svg>`},{type:"custom",id:"button-reset",className:"button-reset",title:"Zurücksetzen",onPress(){n("RESET")},icon:`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="-1 -1 16 16">
</svg>`}];return READ_AND_CONFIRM||t.push({type:"custom",id:"button-reset",className:"button-reset",title:"Zurücksetzen",onPress(){n("RESET")},icon:`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="-1 -1 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>
</svg>`}]}function getPresets(){const n=PSPDFKit.defaultAnnotationPresets;return n.ink={lineWidth:10},n.widget={readOnly:!0},n}const allowedToolbarItems=["sidebar-thumbnails","sidebar-document-ouline","sidebar-bookmarks","pager","pan","zoom-out","zoom-in","zoom-mode","spacer","search","export-pdf"];
</svg>`}),t}function getPresets(){const n=PSPDFKit.defaultAnnotationPresets;return n.ink={lineWidth:10},n.widget={readOnly:!0},n}const allowedToolbarItems=["sidebar-thumbnails","sidebar-document-ouline","sidebar-bookmarks","pager","pan","zoom-out","zoom-in","zoom-mode","spacer","search","export-pdf"];

View File

@@ -4,14 +4,14 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutzinformation für das Fernsignatursystem signFLOW</title>
<title>Datenschutzinformation für das Fernsignatursystem: signFLOW</title>
<link rel="stylesheet" href="css/privacy-policy.min.css">
</head>
<body>
<header>
<h1>Datenschutzinformation für das Fernsignatursystem signFLOW</h1>
<p><strong>Stand:</strong> 19.09.2024</p>
<p><strong>Stand:</strong> 18.11.2025</p>
</header>
<section>
@@ -55,7 +55,7 @@
<h2>3. Datenerhebung</h2>
<h3>3.1 Die folgenden Kategorien personenbezogener Daten werden verarbeitet</h3>
<ul>
<li>Namen: Vor- und Zunamen sowie Ihre digitale Unterschrift</li>
<li>Namen: Benutzername, Vor- und Zunamen sowie Ihre digitale Unterschrift</li>
<li>Kontaktdaten: Telefonnummer, Mobilfunknummer und E-Mail-Adresse</li>
<li>Technische Daten: IP-Adresse, Zeitpunkt des Zugriffs oder Zugriffsversuchs</li>
</ul>
@@ -162,138 +162,6 @@
<a href="https://www.bfdi.bund.de/DE/Service/Anschriften/Laender/Laender-node.html">Laender-node.html</a>
</p>
</section>
<section>
<h2>6. Hinweisgebersystem</h2>
<p>
Die Einhaltung gesetzlicher Vorschriften und interner Richtlinien, einschließlich unseres Verhaltenskodexes
sowie des Verhaltenskodexes für Geschäftspartner, hat für uns (die verarbeitende Stelle) oberste Priorität.
Dies gilt sowohl für unseren eigenen Geschäftsbereich als auch für unsere Lieferketten.
</p>
<p>
Es ist uns wichtig, Risiken frühzeitig zu identifizieren und Verstöße zu vermeiden. Wir möchten rechtzeitig
geeignete Maßnahmen ergreifen, um mögliche Schäden für Betroffene, Kunden, Mitarbeiter, Geschäftspartner und
unsere Unternehmensgruppe zu verhindern.
</p>
<p>
Aus diesem Grund haben wir ein unabhängiges, neutrales und vertrauliches Hinweisgebersystem eingerichtet,
das es internen und externen Hinweisgebenden ermöglicht, auch anonym Meldungen abzugeben. Durch unser
transparentes Beschwerdeverfahren bieten wir insbesondere den Betroffenen, den Hinweisgebenden und den
Mitarbeitenden, die an der Aufklärung der gemeldeten Vorfälle mitwirken, den größtmöglichen Schutz.
</p>
<p>
Im Rahmen dieses Verfahrens können alle tatsächlichen und vermeintlichen Verstöße gegen gesetzliche
Vorgaben, unseren Verhaltenskodex sowie den Verhaltenskodex für Geschäftspartner gemeldet werden. Auch
menschenrechtliche oder umweltbezogene Risiken sowie Pflichtverletzungen entlang der gesamten Lieferkette
unserer Konzernunternehmen und in unserem eigenen Geschäftsbereich können Gegenstand einer Meldung sein.
</p>
<p>
Einheitliche und zügige Prozesse sowie eine vertrauliche und professionelle Bearbeitung der Hinweise durch
interne Experten bilden die Grundlage dieses fairen Verfahrens. Benachteiligungen oder Bestrafungen von
Hinweisgebenden sowie von Personen, die mit der Bearbeitung von Beschwerden und Hinweisen betraut sind,
werden nicht toleriert.
</p>
<h3>6.1 Zweck und Rechtsgrundlage der Datenverarbeitung</h3>
<p>
Der Zweck der Verarbeitung personenbezogener Daten besteht in der Verwaltung des Hinweisgebersystems, das
auch die Aufdeckung schwerwiegender Verstöße oder potenzieller Verstöße gegen geltendes Recht sowie anderer
ernsthafter Angelegenheiten umfasst. Die Verarbeitung dieser Daten ist notwendig, um rechtlichen
Verpflichtungen nachzukommen, die uns auferlegt sind, gemäß Art. 6 Abs. 1 S. 1 lit. c&#41; DSGVO. Dies
bezieht
sich auf das Gesetz, das den Schutz von Hinweisgebern verbessert (Hinweisgeberschutzgesetz - HinSchG).
</p>
<p>
Zudem dient die Verarbeitung dem berechtigten Interesse, schwerwiegende Verstöße oder mögliche Verstöße
gegen geltendes Recht sowie andere ernsthafte Angelegenheiten aufzudecken, gemäß Art. 6 Abs. 1 S. 1 lit.
f&#41;
DSGVO.
</p>
<p>
Im Hinblick auf die Verarbeitung besonderer Kategorien personenbezogener Daten ist diese auf Grundlage des
Hinweisgeberschutzgesetzes aus Gründen eines erheblichen öffentlichen Interesses erforderlich, gemäß Art. 9
Abs. 2 lit. g&#41; DSGVO. Die Verarbeitung dieser besonderen Daten erfolgt gemäß Art. 9 Abs. 2 lit. f&#41;
DSGVO in
Verbindung mit Art. 6 Abs. 1 S. 1 lit. f&#41; DSGVO, um Rechtsansprüche festzustellen, auszuüben oder zu
verteidigen.
</p>
<p>
Betroffene Personen sind diejenigen, über die eine Meldung gemacht wird. Dies können Mitarbeiter,
Vertragspartner oder andere Personen sein, die in beruflicher Verbindung zu der verarbeitenden Stelle
stehen. Darüber hinaus verarbeiten wir personenbezogene Daten der hinweisgebenden Person, wenn diese ihre
Kontaktinformationen oder andere identifizierende Informationen übermittelt. Hinweisgebende Personen sollten
sich daher bewusst sein, dass wir im Rahmen der Bearbeitung des gemeldeten Falls personenbezogene Daten über
sie verarbeiten können.
</p>
<h3>6.2 Kategorien personenbezogener Daten</h3>
<p>
Die Meldung kann anonym erfolgen, wodurch keine personenbezogenen Daten der meldenden Person verarbeitet
werden. Die Art der personenbezogenen Daten, die verarbeitet werden, hängt von den übermittelten
Informationen ab. Wenn die meldende Person personenbezogene Daten über eine andere Person, einschließlich
der gemeldeten Person oder Personen, angibt, werden auch diese Daten verarbeitet. Folgende Kategorien von
personenbezogenen Daten können verarbeitet werden:
</p>
<ul>
<li>Allgemeine personenbezogene Daten (z.B.: Vorname, Nachname, Adresse, E-Mail-Adresse, Telefonnummer,
usw.)</li>
<li>Personenbezogene Daten zu strafrechtlichen Verurteilungen oder Verdachtsmomenten</li>
<li>Besondere Kategorien personenbezogener Daten (Informationen über rassische oder ethnische Herkunft,
politische Meinungen, religiöse oder philosophische Überzeugungen, Gewerkschaftszugehörigkeit,
Gesundheitsdaten sowie Informationen über das Sexualleben oder die sexuelle Orientierung einer Person)
</li>
</ul>
<p>
Wir bitten die meldende Person, ausschließlich Informationen zu übermitteln, die für den jeweiligen Fall von
Bedeutung sind, und insbesondere keine sensiblen Informationen zu melden, es sei denn, diese sind für die
Bearbeitung des gemeldeten Falls von zentraler Relevanz.
</p>
<h3>6.3 Verpflichtung zur Bereitstellung personenbezogener Daten</h3>
<p>
Es ist nicht erforderlich, die im Abschnitt 6.2 genannten personenbezogenen Daten bereitzustellen, da auch
eine anonyme Meldung möglich ist. Bitte beachte jedoch, dass wir möglicherweise nicht in der Lage sind, die
Meldung zu bearbeiten, wenn keine personenbezogenen Daten angegeben werden.
</p>
<h3>6.4 Empfänger personenbezogener Daten</h3>
<p>Die Meldungen werden bei der verarbeitenden Stelle im System als Vorgänge erfasst. Nach einer Bewertung
werden diese Vorgänge intern an die zuständigen Fachabteilungen weitergeleitet, und gegebenenfalls werden
Folgemaßnahmen eingeleitet. Sollte eine Meldung eine der Konzerngesellschaften der verarbeitenden Stelle
betreffen, werden die relevanten Vorgänge an die zuständigen Personen der jeweiligen Gesellschaft
weitergegeben, die dann intern eine Bewertung vornehmen und gegebenenfalls Maßnahmen ergreifen. Bei der
Weitergabe personenbezogener Daten wird der Grundsatz der Datenminimierung beachtet, was bedeutet, dass nur
die unbedingt notwendigen Daten zur Bearbeitung der Meldung weitergegeben werden.</p>
<p>Personenbezogene Daten der hinweisgebenden Person werden an Behörden weitergeleitet, wenn dies erforderlich
ist, um schwerwiegende Verstöße oder Angelegenheiten zu behandeln oder das Recht auf Verteidigung der
betroffenen Personen zu sichern. In anderen Fällen erfolgt die Weitergabe personenbezogener Daten der
hinweisgebenden Person nur mit deren Zustimmung. Daten über andere Personen als die hinweisgebende Person
werden nur im Rahmen der Nachverfolgung eines gemeldeten Falls oder zur Bearbeitung schwerwiegender Verstöße
oder Angelegenheiten weitergegeben.</p>
<p>Die Meldeplattform wird von dem Auftragsverarbeiter WhistleB Whistleblowing Centre AB mit Sitz in Stockholm,
Schweden, bereitgestellt. Weitere Informationen zu WhistleB und den entsprechenden Nutzungsbedingungen sind
dort einsehbar.
<a
href="https://report.whistleb.com/content/documents/whistleb_terms_of_use.pdf">whistleb_terms_of_use.pdf</a>
</p>
<h3>6.5 Speicherdauer</h3>
<p>Personenbezogene Daten, die sich als nicht relevant für die Bearbeitung eines gemeldeten Falls herausstellen,
sowie Meldungen, die wir als unbegründet ansehen, werden umgehend als "nicht relevant" eingestuft. In diesem
Fall wird der Personenbezug entfernt, es sei denn, es handelt sich bereits um eine anonyme Meldung. Um die
gesetzlich vorgeschriebene Dokumentationspflicht und die Löschfristen gemäß § 11 Abs. 1 und Abs. 5 HinSchG
zu erfüllen, wird die Meldung zunächst ohne Personenbezug archiviert, jedoch noch nicht gelöscht.
Archivierte Fälle dienen ausschließlich der Erfüllung dieser Dokumentationspflichten und können danach nicht
mehr zur Bearbeitung herangezogen werden.</p>
<p>Die Meldungen und personenbezogenen Daten, die im Zuge der Bearbeitung einer Meldung erfasst werden, bilden
die Grundlage für die weitere Bearbeitung und werden so schnell wie möglich anonymisiert. Sollte es
notwendig sein, Folgemaßnahmen gemäß §§ 3 Abs. 8 und 18 HinSchG zu ergreifen, kann es jedoch erforderlich
sein, von der Anonymisierung abzuweichen, sei es aufgrund behördlicher Anordnungen oder zur Wahrung von
Rechtsansprüchen. In solchen Fällen wird in der Regel eine Pseudonymisierung angestrebt, es sei denn, es
gibt andere Vorgaben, wie etwa eine richterliche Anordnung. Die Dokumentation wird drei Jahre nach Abschluss
des Verfahrens gelöscht. Sie kann jedoch länger aufbewahrt werden, um den Anforderungen dieses Gesetzes oder
anderer Rechtsvorschriften gerecht zu werden, solange dies notwendig und angemessen ist.</p>
</section>
</body>
</html>

View File

@@ -10,8 +10,8 @@
<body>
<header>
<h1>Data Protection Information for the Remote Signature System signFLOW</h1>
<p><strong>As of:</strong> 19.09.2024</p>
<h1>Data Protection Information for the Remote Signature System: signFLOW</h1>
<p><strong>As of:</strong> 18.11.2025</p>
</header>
<section>
<h2>1. General Information</h2>
@@ -53,7 +53,7 @@
<h2>3. Data Collection</h2>
<h3>3.1 The following categories of personal data are processed</h3>
<ul>
<li>Names: First and last names as well as your digital signature</li>
<li>Names: Username, first and last names as well as your digital signature</li>
<li>Contact details: Phone number, mobile phone number, and email address</li>
<li>Technical data: IP address, time of access, or access attempts</li>
</ul>
@@ -153,133 +153,6 @@
<a href="https://www.bfdi.bund.de/DE/Service/Anschriften/Laender/Laender-node.html">Laender-node.html</a>
</p>
</section>
<section>
<h2>6. Whistleblower System</h2>
<p>
Compliance with legal regulations and internal guidelines, including our Code of Conduct and the Code of
Conduct for Business Partners, is our (the data processing entity's) top priority. This applies both to our
own business operations and to our supply chains.
</p>
<p>
It is important to us to identify risks early and avoid violations. We aim to take appropriate measures in a
timely manner to prevent potential harm to affected persons, customers, employees, business partners, and
our corporate group.
</p>
<p>
For this reason, we have established an independent, neutral, and confidential whistleblower system that
enables internal and external whistleblowers to submit reports, including anonymously. Through our
transparent complaint procedure, we offer the greatest possible protection, especially to the affected
persons, whistleblowers, and employees involved in investigating reported incidents.
</p>
<p>
Under this procedure, any actual or alleged violations of legal requirements, our Code of Conduct, or the
Code of Conduct for Business Partners may be reported. Human rights or environmental risks, as well as
breaches of duty along the entire supply chain of our group companies and in our own business operations,
can also be the subject of a report.
</p>
<p>
Standardized and swift processes, as well as confidential and professional handling of the reports by
internal experts, form the basis of this fair procedure. Discrimination or punishment of whistleblowers and
individuals responsible for handling complaints and reports will not be tolerated.
</p>
<h3>6.1 Purpose and Legal Basis of Data Processing</h3>
<p>
The purpose of processing personal data is to manage the whistleblower system, which also includes
identifying serious violations or potential violations of applicable law and other serious matters. The
processing of this data is necessary to comply with legal obligations imposed on us, in accordance with Art.
6 para. 1 sentence 1 lit. c) GDPR. This refers to the law that enhances the protection of whistleblowers
(Whistleblower Protection Act - HinSchG).
</p>
<p>
Additionally, the processing serves the legitimate interest of identifying serious violations or potential
violations of applicable law and other serious matters, in accordance with Art. 6 para. 1 sentence 1 lit. f)
GDPR.
</p>
<p>
Regarding the processing of special categories of personal data, this is necessary based on the
Whistleblower Protection Act for reasons of significant public interest, in accordance with Art. 9 para. 2
lit. g) GDPR. The processing of such special data is carried out in accordance with Art. 9 para. 2 lit. f)
GDPR in conjunction with Art. 6 para. 1 sentence 1 lit. f) GDPR to establish, exercise, or defend legal
claims.
</p>
<p>
Affected persons are those about whom a report is made. These can be employees, contractors, or other
individuals in a business relationship with the data processing entity. Furthermore, we process personal
data of the whistleblower if they provide their contact details or other identifying information.
Whistleblowers should be aware that we may process personal data about them during the handling of the
reported case.
</p>
<h3>6.2 Categories of Personal Data</h3>
<p>
Reports can be made anonymously, in which case no personal data of the reporting person will be processed.
The type of personal data processed depends on the information provided. If the reporting person provides
personal data about another individual, including the reported individual or persons, that data will also be
processed. The following categories of personal data may be processed:
</p>
<ul>
<li>General personal data (e.g., first name, last name, address, email address, phone number, etc.)</li>
<li>Personal data related to criminal convictions or suspicions</li>
<li>Special categories of personal data (information about racial or ethnic origin, political opinions,
religious or philosophical beliefs, trade union membership, health data, and information about a
person's sex life or sexual orientation)</li>
</ul>
<p>
We ask the reporting person to only provide information relevant to the case and to avoid reporting
sensitive information unless it is essential for handling the reported case.
</p>
<h3>6.3 Obligation to Provide Personal Data</h3>
<p>
It is not mandatory to provide the personal data mentioned in section 6.2, as anonymous reporting is also
possible. However, please note that we may be unable to process the report if no personal data is provided.
</p>
<h3>6.4 Recipients of Personal Data</h3>
<p>
Reports are logged in the system of the data processing entity as cases. After evaluation, these cases are
forwarded internally to the relevant departments, and follow-up actions may be initiated. If a report
involves one of the group companies of the data processing entity, the relevant cases are forwarded to the
responsible individuals at the respective company, who will then conduct an internal evaluation and take
action if necessary. When transferring personal data, the principle of data minimization is observed,
meaning only the data strictly necessary for handling the report is shared.
</p>
<p>
Personal data of the whistleblower will be shared with authorities when necessary to address serious
violations or issues, or to safeguard the right to defense of the affected persons. In other cases, personal
data of the whistleblower will only be shared with their consent. Data about persons other than the
whistleblower will only be shared in connection with the investigation of a reported case or to address
serious violations or issues.
</p>
<p>
The reporting platform is provided by the processor WhistleB Whistleblowing Centre AB, based in Stockholm,
Sweden. Further information about WhistleB and the corresponding terms of use can be found at:
<a
href="https://report.whistleb.com/content/documents/whistleb_terms_of_use.pdf">whistleb_terms_of_use.pdf</a>
</p>
<h3>6.5 Retention Period</h3>
<p>
Personal data that is found to be irrelevant to the processing of a reported case, as well as reports deemed
unfounded, will be immediately classified as "not relevant." In this case, the personal reference is removed
unless the report was anonymous from the outset. To meet the legally required documentation obligations and
deletion periods pursuant to § 11 para. 1 and para. 5 HinSchG, the report is initially archived without
personal reference but is not yet deleted. Archived cases serve solely to fulfill these documentation
obligations and can no longer be used for further processing.
</p>
<p>
Reports and personal data collected during the processing of a report form the basis for further handling
and are anonymized as soon as possible. However, if it is necessary to take follow-up actions pursuant to §§
3 para. 8 and 18 HinSchG, it may be necessary to deviate from anonymization, whether due to official orders
or to protect legal claims. In such cases, pseudonymization is generally sought, unless other directives
apply, such as a court order. Documentation is deleted three years after the conclusion of the process, but
it may be retained longer if required to meet the requirements of this law or other legal provisions, as
long as it remains necessary and appropriate.
</p>
</section>
</body>
</html>

View File

@@ -0,0 +1,14 @@
using EnvelopeGenerator.Jobs.FinalizeDocument;
namespace EnvelopeGenerator.WorkerService.Configuration;
public sealed class WorkerSettings
{
public string ConnectionString { get; set; } = string.Empty;
public bool Debug { get; set; }
public int IntervalMinutes { get; set; } = 1;
public PDFBurnerParams PdfBurner { get; set; } = new();
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-EnvelopeGenerator.WorkerService-0636abb8-6085-477d-9f56-1a9787e84dde</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Jobs\EnvelopeGenerator.Jobs.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,67 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Jobs.APIBackendJobs;
using EnvelopeGenerator.Jobs.FinalizeDocument;
using EnvelopeGenerator.WorkerService;
using EnvelopeGenerator.WorkerService.Configuration;
using EnvelopeGenerator.WorkerService.Services;
using Quartz;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.Configure<WorkerSettings>(builder.Configuration.GetSection("WorkerSettings"));
builder.Services.AddSingleton<TempFileManager>();
builder.Services.AddSingleton(provider =>
{
var settings = provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<WorkerSettings>>().Value;
var logger = provider.GetRequiredService<ILogger<PDFBurner>>();
return new PDFBurner(logger, settings.PdfBurner);
});
builder.Services.AddSingleton<PDFMerger>();
builder.Services.AddSingleton<ReportCreator>();
builder.Services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
q.UseDefaultThreadPool(tp => tp.MaxConcurrency = 5);
var settings = new WorkerSettings();
builder.Configuration.GetSection("WorkerSettings").Bind(settings);
var intervalMinutes = Math.Max(1, settings.IntervalMinutes);
var finalizeJobKey = new JobKey("FinalizeDocumentJob");
q.AddJob<FinalizeDocumentJob>(opts => opts
.WithIdentity(finalizeJobKey)
.UsingJobData(Value.DATABASE, settings.ConnectionString));
q.AddTrigger(opts => opts
.ForJob(finalizeJobKey)
.WithIdentity("FinalizeDocumentJob-trigger")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(intervalMinutes)
.RepeatForever()));
var apiJobKey = new JobKey("APIEnvelopeJob");
q.AddJob<APIEnvelopeJob>(opts => opts
.WithIdentity(apiJobKey)
.UsingJobData(Value.DATABASE, settings.ConnectionString));
q.AddTrigger(opts => opts
.ForJob(apiJobKey)
.WithIdentity("APIEnvelopeJob-trigger")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(intervalMinutes)
.RepeatForever()));
});
builder.Services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();

View File

@@ -0,0 +1,12 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"EnvelopeGenerator.WorkerService": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,74 @@
using System.IO;
using Microsoft.Extensions.Logging;
namespace EnvelopeGenerator.WorkerService.Services;
public sealed class TempFileManager
{
private readonly ILogger<TempFileManager> _logger;
public TempFileManager(ILogger<TempFileManager> logger)
{
_logger = logger;
TempPath = Path.Combine(Path.GetTempPath(), "EnvelopeGenerator");
}
public string TempPath { get; }
public Task CreateAsync(CancellationToken cancellationToken = default)
{
try
{
if (!Directory.Exists(TempPath))
{
Directory.CreateDirectory(TempPath);
_logger.LogDebug("Created temp folder {TempPath}", TempPath);
}
else
{
CleanUpFiles();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create temp folder {TempPath}", TempPath);
throw;
}
return Task.CompletedTask;
}
public Task CleanupAsync(CancellationToken cancellationToken = default)
{
try
{
if (Directory.Exists(TempPath))
{
_logger.LogDebug("Deleting temp folder {TempPath}", TempPath);
Directory.Delete(TempPath, recursive: true);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to clean up temp folder {TempPath}", TempPath);
}
return Task.CompletedTask;
}
private void CleanUpFiles()
{
foreach (var file in Directory.GetFiles(TempPath))
{
try
{
_logger.LogDebug("Deleting temp file {File}", file);
File.Delete(file);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete temp file {File}", file);
}
}
}
}

View File

@@ -0,0 +1,71 @@
using EnvelopeGenerator.WorkerService.Configuration;
using EnvelopeGenerator.WorkerService.Services;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.WorkerService;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly WorkerSettings _settings;
private readonly TempFileManager _tempFiles;
public Worker(
ILogger<Worker> logger,
IOptions<WorkerSettings> settings,
TempFileManager tempFiles)
{
_logger = logger;
_settings = settings.Value;
_tempFiles = tempFiles;
}
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting EnvelopeGenerator worker...");
_logger.LogInformation("Debug mode: {Debug}", _settings.Debug);
ValidateConfiguration();
await EnsureDatabaseConnectionAsync(cancellationToken);
await _tempFiles.CreateAsync(cancellationToken);
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("EnvelopeGenerator worker is running. Jobs are scheduled every {Interval} minute(s).", Math.Max(1, _settings.IntervalMinutes));
await Task.Delay(Timeout.Infinite, stoppingToken);
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping EnvelopeGenerator worker...");
await _tempFiles.CleanupAsync(cancellationToken);
await base.StopAsync(cancellationToken);
}
private void ValidateConfiguration()
{
if (string.IsNullOrWhiteSpace(_settings.ConnectionString))
{
throw new InvalidOperationException("Connection string cannot be empty. Configure 'WorkerSettings:ConnectionString'.");
}
}
private async Task EnsureDatabaseConnectionAsync(CancellationToken cancellationToken)
{
try
{
await using var connection = new SqlConnection(_settings.ConnectionString);
await connection.OpenAsync(cancellationToken);
_logger.LogInformation("Database connection established successfully.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Database connection could not be established.");
throw;
}
}
}

View File

@@ -0,0 +1,30 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WorkerSettings": {
"ConnectionString": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
"Debug": true,
"IntervalMinutes": 1,
"PdfBurner": {
"IgnoredLabels": [
"Date",
"Datum",
"ZIP",
"PLZ",
"Place",
"Ort",
"Position",
"Stellung"
],
"TopMargin": 0.1,
"YOffset": -0.3,
"FontName": "Arial",
"FontSize": 8,
"FontStyle": "Italic"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WorkerSettings": {
"ConnectionString": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
"Debug": false,
"IntervalMinutes": 1,
"PdfBurner": {
"IgnoredLabels": ["Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung"],
"TopMargin": 0.1,
"YOffset": -0.3,
"FontName": "Arial",
"FontSize": 8,
"FontStyle": "Italic"
}
}
}

View File

@@ -25,8 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Tests.Application", "EnvelopeGenerator.Tests.Application\EnvelopeGenerator.Tests.Application.csproj", "{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastructure", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "presentation", "presentation", "{E3C758DC-914D-4B7E-8457-0813F1FDB0CB}"
@@ -37,6 +35,12 @@ Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.Form", "E
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor", "EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj", "{211619F5-AE25-4BA5-A552-BACAFE0632D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Jobs", "EnvelopeGenerator.Jobs\EnvelopeGenerator.Jobs.csproj", "{3D0514EA-2681-4B13-AD71-35CC6363DBD7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.WorkerService", "EnvelopeGenerator.WorkerService\EnvelopeGenerator.WorkerService.csproj", "{E3676510-7030-4E85-86E1-51E483E2A3B6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -45,8 +49,8 @@ Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.Build.0 = Release|Any CPU
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.Build.0 = Debug|Any CPU
{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -75,10 +79,6 @@ Global
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.Build.0 = Debug|Any CPU
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Release|Any CPU.Build.0 = Release|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -91,6 +91,18 @@ Global
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Release|Any CPU.Build.0 = Release|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.Build.0 = Release|Any CPU
{3D0514EA-2681-4B13-AD71-35CC6363DBD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D0514EA-2681-4B13-AD71-35CC6363DBD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D0514EA-2681-4B13-AD71-35CC6363DBD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D0514EA-2681-4B13-AD71-35CC6363DBD7}.Release|Any CPU.Build.0 = Release|Any CPU
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -105,12 +117,14 @@ Global
{5A9984F8-51A2-4558-A415-EC5FEED7CF7D} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{E5E12BA4-60C1-48BA-9053-0F8B62B38124} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{9943209E-1744-4944-B1BA-4F87FC1A0EEB} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399} = {0CBC2432-A561-4440-89BC-671B66A24146}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{E3C758DC-914D-4B7E-8457-0813F1FDB0CB} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{A9F9B431-BB9B-49B8-9E2C-0703634A653A} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
{3D0514EA-2681-4B13-AD71-35CC6363DBD7} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{E3676510-7030-4E85-86E1-51E483E2A3B6} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}