Compare commits
43 Commits
refactor/a
...
feat/servi
| Author | SHA1 | Date | |
|---|---|---|---|
| 41dde6f016 | |||
| c5b167f0d4 | |||
| a70faebde6 | |||
| 3e01052579 | |||
| f49b907574 | |||
| 86ed96ae76 | |||
| 9dbfdaa15e | |||
| 9d66f1d19e | |||
| 14cef05d02 | |||
| bdfb973d55 | |||
| 15a18b1bfd | |||
| 6fac1cd96a | |||
| 79d2636c14 | |||
| 2172ce8203 | |||
| 51ab9fb094 | |||
| d8a002cd22 | |||
| e36684820e | |||
| 2a5d953623 | |||
| 0aba9e91e2 | |||
| 1a0973075b | |||
| b8fd26611c | |||
| 0ca372bf45 | |||
| 5230076d5d | |||
| b28084bf19 | |||
| cbc983e070 | |||
| 3b06f3fdac | |||
| a12d74871d | |||
| 45b715ed74 | |||
| 9d5e2e6ad2 | |||
| c5d2d79563 | |||
| 15d4573321 | |||
| eb46590c1d | |||
| c93c32307a | |||
| 41cca7fa64 | |||
| b01c17ab18 | |||
| c8834dc3be | |||
| e385fdda95 | |||
| 898097cdb5 | |||
| 689a1b355a | |||
| 3b3330bd54 | |||
| 511fad3950 | |||
| f5f137396e | |||
| 0d78e9b8f5 |
@@ -7,11 +7,10 @@ Imports GdPicture14
|
|||||||
Imports Newtonsoft.Json.Linq
|
Imports Newtonsoft.Json.Linq
|
||||||
Imports EnvelopeGenerator.Infrastructure
|
Imports EnvelopeGenerator.Infrastructure
|
||||||
Imports Microsoft.EntityFrameworkCore
|
Imports Microsoft.EntityFrameworkCore
|
||||||
Imports System.Text
|
|
||||||
Imports DigitalData.Core.Abstractions
|
Imports DigitalData.Core.Abstractions
|
||||||
|
|
||||||
Public Class frmFinalizePDF
|
Public Class frmFinalizePDF
|
||||||
Private Const CONNECTIONSTRING = "Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=+bk8oAbbQP1AzoHtvZUbd+Mbok2f8Fl4miEx1qssJ5yEaEWoQJ9prg4L14fURpPnqi1WMNs9fE4=;"
|
Private Const CONNECTIONSTRING = "Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=+bk8oAbbQP1AzoHtvZUbd+Mbok2f8Fl4miEx1qssJ5yEaEWoQJ9prg4L14fURpPnqi1WMNs9fE4=;" + "Encrypt=True;TrustServerCertificate=True;"
|
||||||
|
|
||||||
Private Database As MSSQLServer
|
Private Database As MSSQLServer
|
||||||
Private LogConfig As LogConfig
|
Private LogConfig As LogConfig
|
||||||
@@ -93,8 +92,6 @@ Public Class frmFinalizePDF
|
|||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
|
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
|
||||||
Try
|
|
||||||
|
|
||||||
Dim oTable = LoadAnnotationDataForEnvelope()
|
Dim oTable = LoadAnnotationDataForEnvelope()
|
||||||
Dim oJsonList = oTable.Rows.
|
Dim oJsonList = oTable.Rows.
|
||||||
Cast(Of DataRow).
|
Cast(Of DataRow).
|
||||||
@@ -110,22 +107,9 @@ Public Class frmFinalizePDF
|
|||||||
File.WriteAllBytes(oNewPath, oNewBuffer)
|
File.WriteAllBytes(oNewPath, oNewBuffer)
|
||||||
|
|
||||||
Process.Start(oNewPath)
|
Process.Start(oNewPath)
|
||||||
Catch ex As Exception
|
|
||||||
Dim exMsg As StringBuilder = New StringBuilder(ex.Message).AppendLine()
|
|
||||||
|
|
||||||
Dim innerEx = ex.InnerException
|
|
||||||
While (innerEx IsNot Nothing)
|
|
||||||
exMsg.AppendLine(innerEx.Message)
|
|
||||||
innerEx = innerEx.InnerException
|
|
||||||
End While
|
|
||||||
|
|
||||||
MsgBox(exMsg.ToString(), MsgBoxStyle.Critical)
|
|
||||||
End Try
|
|
||||||
|
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
|
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
|
||||||
Try
|
|
||||||
Dim oTable = LoadAnnotationDataForEnvelope()
|
Dim oTable = LoadAnnotationDataForEnvelope()
|
||||||
Dim oJsonList = oTable.Rows.
|
Dim oJsonList = oTable.Rows.
|
||||||
Cast(Of DataRow).
|
Cast(Of DataRow).
|
||||||
@@ -139,10 +123,5 @@ Public Class frmFinalizePDF
|
|||||||
oJObject1.Merge(oJObject2)
|
oJObject1.Merge(oJObject2)
|
||||||
|
|
||||||
txtResult.Text = oJObject1.ToString()
|
txtResult.Text = oJObject1.ToString()
|
||||||
|
|
||||||
|
|
||||||
Catch ex As Exception
|
|
||||||
MsgBox(ex.Message, MsgBoxStyle.Critical)
|
|
||||||
End Try
|
|
||||||
End Sub
|
End Sub
|
||||||
End Class
|
End Class
|
||||||
@@ -35,7 +35,9 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public DateTime AddedWhen { get; set; }
|
public DateTime AddedWhen { get; set; }
|
||||||
|
|
||||||
[Column("ACTION_DATE", TypeName = "datetime")]
|
[Column("ACTION_DATE", TypeName = "datetime")]
|
||||||
public DateTime? ChangedWhen { get; set; }
|
public DateTime? ActionDate { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ChangedWhen { get => ActionDate; set => ActionDate = value; }
|
||||||
|
|
||||||
[Column("COMMENT", TypeName = "nvarchar(max)")]
|
[Column("COMMENT", TypeName = "nvarchar(max)")]
|
||||||
public string
|
public string
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ using System.ComponentModel;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
||||||
|
#if NET
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
#endif
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
#endif
|
#endif
|
||||||
@@ -109,22 +112,25 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
#if nullable
|
#if nullable
|
||||||
?
|
?
|
||||||
#endif
|
#endif
|
||||||
Receiver
|
Receiver { get; set; }
|
||||||
{ get; set; }
|
|
||||||
|
|
||||||
public virtual IEnumerable<ElementAnnotation>
|
public virtual IEnumerable<ElementAnnotation>
|
||||||
#if nullable
|
#if nullable
|
||||||
?
|
?
|
||||||
#endif
|
#endif
|
||||||
Annotations
|
Annotations { get; set; }
|
||||||
{ get; set; }
|
|
||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NET
|
||||||
|
[JsonIgnore]
|
||||||
|
#endif
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public double Top => Math.Round(Y, 5);
|
public double Top => Math.Round(Y, 5);
|
||||||
|
|
||||||
|
|
||||||
|
#if NET
|
||||||
|
[JsonIgnore]
|
||||||
|
#endif
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public double Left => Math.Round(X, 5);
|
public double Left => Math.Round(X, 5);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
|
||||||
<PackageReference Include="Quartz" Version="3.9.0" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
|
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
|
||||||
</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>
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
<PackageReference Include="GdPicture" Version="14.3.3" />
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.17" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="Quartz" Version="3.8.0" />
|
||||||
|
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Controllers\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace EnvelopeGenerator.ServiceHost.Exceptions;
|
||||||
|
|
||||||
|
public class BurnAnnotationException : ApplicationException
|
||||||
|
{
|
||||||
|
public BurnAnnotationException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BurnAnnotationException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace EnvelopeGenerator.ServiceHost.Exceptions;
|
||||||
|
|
||||||
|
public class CreateReportException : ApplicationException
|
||||||
|
{
|
||||||
|
public CreateReportException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CreateReportException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace EnvelopeGenerator.ServiceHost.Exceptions;
|
||||||
|
|
||||||
|
public class ExportDocumentException : ApplicationException
|
||||||
|
{
|
||||||
|
public ExportDocumentException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExportDocumentException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace EnvelopeGenerator.ServiceHost.Exceptions;
|
||||||
|
|
||||||
|
public class MergeDocumentException : ApplicationException
|
||||||
|
{
|
||||||
|
public MergeDocumentException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MergeDocumentException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
public static class DataRowExtensions
|
||||||
|
{
|
||||||
|
public static T ItemEx<T>(this DataRow row, string columnName, T defaultValue)
|
||||||
|
{
|
||||||
|
if (!row.Table.Columns.Contains(columnName))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = row[columnName];
|
||||||
|
if (value is DBNull or null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (T)Convert.ChangeType(value, typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ItemEx(this DataRow row, string columnName, string defaultValue)
|
||||||
|
{
|
||||||
|
return row.ItemEx<string>(columnName, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using DigitalData.Modules.Database;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
using GdPicture14;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
[Obsolete("Check obsoleted services")]
|
||||||
|
public static IServiceCollection AddFinalizeDocumentJob(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.Configure<WorkerOptions>(configuration.GetSection(nameof(WorkerOptions)));
|
||||||
|
services.AddSingleton<FinalizeDocumentJob>();
|
||||||
|
services.AddScoped<ActionService>();
|
||||||
|
services.AddSingleton<TempFiles>();
|
||||||
|
services.AddScoped<PDFBurner>();
|
||||||
|
services.AddScoped<PDFMerger>();
|
||||||
|
services.AddScoped<ReportModel>();
|
||||||
|
services.AddSingleton<State>();
|
||||||
|
services.AddScoped<MSSQLServer>();
|
||||||
|
|
||||||
|
//TODO: Check lifetime of services. They might be singleton or scoped.
|
||||||
|
services.AddTransient<GdViewer>();
|
||||||
|
// Add LicenseManager
|
||||||
|
services.AddTransient(provider =>
|
||||||
|
{
|
||||||
|
var options = provider.GetRequiredService<IOptions<WorkerOptions>>().Value;
|
||||||
|
var licenseManager = new LicenseManager();
|
||||||
|
licenseManager.RegisterKEY(options.GdPictureLicenseKey);
|
||||||
|
return licenseManager;
|
||||||
|
});
|
||||||
|
services.AddTransient<AnnotationManager>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
EnvelopeGenerator.ServiceHost/Extensions/LoggerExtensions.cs
Normal file
11
EnvelopeGenerator.ServiceHost/Extensions/LoggerExtensions.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
public static class LoggerExtensions
|
||||||
|
{
|
||||||
|
public static void LogError(this ILogger logger, Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(exception, "{message}", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
EnvelopeGenerator.ServiceHost/Jobs/ActionService.cs
Normal file
31
EnvelopeGenerator.ServiceHost/Jobs/ActionService.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
|
||||||
|
public class ActionService
|
||||||
|
{
|
||||||
|
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
|
||||||
|
public bool CreateReport(Envelope envelope)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
|
||||||
|
public bool FinalizeEnvelope(Envelope envelope)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
|
||||||
|
public bool CompleteEnvelope(Envelope envelope)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("This is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
|
||||||
|
public bool CompleteEnvelope(Envelope envelope, Receiver receiver)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
34
EnvelopeGenerator.ServiceHost/Jobs/ConfigModel.cs
Normal file
34
EnvelopeGenerator.ServiceHost/Jobs/ConfigModel.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using DigitalData.Modules.Database;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
public class ConfigModel(MSSQLServer Database, ILogger Logger)
|
||||||
|
{
|
||||||
|
public DbConfig LoadConfiguration()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const string sql = "SELECT TOP 1 * FROM TBSIG_CONFIG";
|
||||||
|
var table = Database.GetDatatable(sql);
|
||||||
|
var row = table.Rows[0];
|
||||||
|
|
||||||
|
return new DbConfig
|
||||||
|
{
|
||||||
|
DocumentPath = row.ItemEx("DOCUMENT_PATH", string.Empty),
|
||||||
|
DocumentPathOrigin = row.ItemEx("DOCUMENT_PATH", string.Empty),
|
||||||
|
ExportPath = row.ItemEx("EXPORT_PATH", string.Empty),
|
||||||
|
SendingProfile = row.ItemEx("SENDING_PROFILE", 0),
|
||||||
|
SignatureHost = row.ItemEx("SIGNATURE_HOST", string.Empty),
|
||||||
|
ExternalProgramName = row.ItemEx("EXTERNAL_PROGRAM_NAME", string.Empty),
|
||||||
|
Default_Tfa_Enabled = row.ItemEx("DEF_TFA_ENABLED", false),
|
||||||
|
Default_Tfa_WithPhone = row.ItemEx("DEF_TFA_WITH_PHONE", false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex);
|
||||||
|
return new DbConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
EnvelopeGenerator.ServiceHost/Jobs/DbConfig.cs
Normal file
13
EnvelopeGenerator.ServiceHost/Jobs/DbConfig.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
public class DbConfig
|
||||||
|
{
|
||||||
|
public string ExternalProgramName { get; set; } = "signFLOW";
|
||||||
|
public string DocumentPathOrigin { get; set; } = string.Empty;
|
||||||
|
public string DocumentPath { get; set; } = string.Empty;
|
||||||
|
public string ExportPath { get; set; } = string.Empty;
|
||||||
|
public int SendingProfile { get; set; }
|
||||||
|
public string SignatureHost { get; set; } = string.Empty;
|
||||||
|
public bool Default_Tfa_Enabled { get; set; }
|
||||||
|
public bool Default_Tfa_WithPhone { get; set; }
|
||||||
|
}
|
||||||
38
EnvelopeGenerator.ServiceHost/Jobs/EnvelopeModel.cs
Normal file
38
EnvelopeGenerator.ServiceHost/Jobs/EnvelopeModel.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using DigitalData.Modules.Database;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
public class EnvelopeModel(MSSQLServer Database, ILogger Logger)
|
||||||
|
{
|
||||||
|
public Envelope? GetById(int envelopeId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = $"SELECT * FROM [dbo].[TBSIG_ENVELOPE] WHERE GUID = {envelopeId}";
|
||||||
|
var table = Database.GetDatatable(sql);
|
||||||
|
var row = table.Rows.Cast<System.Data.DataRow>().SingleOrDefault();
|
||||||
|
if (row is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Envelope
|
||||||
|
{
|
||||||
|
Id = row.ItemEx("GUID", 0),
|
||||||
|
Uuid = row.ItemEx("ENVELOPE_UUID", string.Empty),
|
||||||
|
FinalEmailToCreator = row.ItemEx("FINAL_EMAIL_TO_CREATOR", 0),
|
||||||
|
FinalEmailToReceivers = row.ItemEx("FINAL_EMAIL_TO_RECEIVERS", 0),
|
||||||
|
UserId = row.ItemEx("USER_ID", 0),
|
||||||
|
User = null!,
|
||||||
|
EnvelopeReceivers = new List<EnvelopeReceiver>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
430
EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFBurner.cs
Normal file
430
EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFBurner.cs
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Drawing;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.Infrastructure;
|
||||||
|
using EnvelopeGenerator.PdfEditor;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Exceptions;
|
||||||
|
using GdPicture14;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
|
||||||
|
//TODO: check if licence manager is needed as a dependency to
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="workerOptions"></param>
|
||||||
|
/// <param name="context2"></param>
|
||||||
|
/// <param name="logger2"></param>
|
||||||
|
/// <param name="licenseManager"></param>
|
||||||
|
/// <param name="annotationManager2"></param>
|
||||||
|
public class PDFBurner(IOptions<WorkerOptions> workerOptions, EGDbContext context, ILogger<PDFBurner> logger, LicenseManager licenseManager, AnnotationManager manager)
|
||||||
|
{
|
||||||
|
private readonly WorkerOptions.PDFBurnerOptions _options = workerOptions.Value.PdfBurner;
|
||||||
|
|
||||||
|
public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, List<string> instantJsonList, int envelopeId)
|
||||||
|
{
|
||||||
|
var envelope = context.Envelopes.FirstOrDefault(env => env.Id == envelopeId);
|
||||||
|
if (envelope is null)
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"Envelope with Id {envelopeId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envelope.ReadOnly)
|
||||||
|
{
|
||||||
|
return sourceBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elements = context.DocumentReceiverElements
|
||||||
|
.Where(sig => sig.Document.EnvelopeId == envelopeId)
|
||||||
|
.Include(sig => sig.Annotations)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return elements.Any()
|
||||||
|
? BurnElementAnnotsToPDF(sourceBuffer, elements)
|
||||||
|
: BurnInstantJSONAnnotsToPDF(sourceBuffer, instantJsonList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] BurnElementAnnotsToPDF(byte[] sourceBuffer, List<Signature> elements)
|
||||||
|
{
|
||||||
|
using (var doc = Pdf.FromMemory(sourceBuffer))
|
||||||
|
{
|
||||||
|
sourceBuffer = doc.Background(elements, 1.9500000000000002 * 0.93, 2.52 * 0.67).ExportStream().ToArray();
|
||||||
|
|
||||||
|
using var sourceStream = new MemoryStream(sourceBuffer);
|
||||||
|
var result = manager.InitFromStream(sourceStream);
|
||||||
|
if (result != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"Could not open document for burning: [{result}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
var margin = 0.2;
|
||||||
|
var inchFactor = 72d;
|
||||||
|
|
||||||
|
var keys = new[] { "position", "city", "date" };
|
||||||
|
var unitYOffsets = 0.2;
|
||||||
|
var yOffsetsOfFF = keys
|
||||||
|
.Select((k, i) => new { Key = k, Value = unitYOffsets * i + 1 })
|
||||||
|
.ToDictionary(x => x.Key, x => x.Value);
|
||||||
|
|
||||||
|
foreach (var element in elements)
|
||||||
|
{
|
||||||
|
var frameX = element.Left - 0.7 - margin;
|
||||||
|
|
||||||
|
var frame = element.Annotations.FirstOrDefault(a => a.Name == "frame");
|
||||||
|
var frameY = element.Top - 0.5 - margin;
|
||||||
|
var frameYShift = (frame?.Y ?? 0) - frameY * inchFactor;
|
||||||
|
var frameXShift = (frame?.X ?? 0) - frameX * inchFactor;
|
||||||
|
|
||||||
|
foreach (var annot in element.Annotations)
|
||||||
|
{
|
||||||
|
var yOffsetOfFF = yOffsetsOfFF.TryGetValue(annot.Name, out var offset) ? offset : 0;
|
||||||
|
var y = frameY + yOffsetOfFF;
|
||||||
|
|
||||||
|
if (annot.Type == AnnotationType.FormField)
|
||||||
|
{
|
||||||
|
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value);
|
||||||
|
}
|
||||||
|
else if (annot.Type == AnnotationType.Image)
|
||||||
|
{
|
||||||
|
AddImageAnnotation(
|
||||||
|
annot.X / inchFactor,
|
||||||
|
annot.Name == "signature" ? (annot.Y - frameYShift) / inchFactor : y,
|
||||||
|
annot.Width / inchFactor,
|
||||||
|
annot.Height / inchFactor,
|
||||||
|
element.Page,
|
||||||
|
annot.Value);
|
||||||
|
}
|
||||||
|
else if (annot.Type == AnnotationType.Ink)
|
||||||
|
{
|
||||||
|
AddInkAnnotation(element.Page, annot.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var newStream = new MemoryStream();
|
||||||
|
result = manager.SaveDocumentToPDF(newStream);
|
||||||
|
if (result != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"Could not save document to stream: [{result}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.Close();
|
||||||
|
|
||||||
|
return newStream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] BurnInstantJSONAnnotsToPDF(byte[] sourceBuffer, List<string> instantJsonList)
|
||||||
|
{
|
||||||
|
using var sourceStream = new MemoryStream(sourceBuffer);
|
||||||
|
var result = manager.InitFromStream(sourceStream);
|
||||||
|
if (result != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"Could not open document for burning: [{result}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var json in instantJsonList)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AddInstantJSONAnnotationToPDF(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Error in AddInstantJSONAnnotationToPDF - oJson: ");
|
||||||
|
logger.LogWarning(json);
|
||||||
|
throw new BurnAnnotationException("Adding Annotation failed", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = manager.BurnAnnotationsToPage(RemoveInitialAnnots: true, VectorMode: true);
|
||||||
|
if (result != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"Could not burn annotations to file: [{result}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var newStream = new MemoryStream();
|
||||||
|
result = manager.SaveDocumentToPDF(newStream);
|
||||||
|
if (result != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"Could not save document to stream: [{result}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.Close();
|
||||||
|
|
||||||
|
return newStream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddInstantJSONAnnotationToPDF(string instantJson)
|
||||||
|
{
|
||||||
|
var annotationData = JsonConvert.DeserializeObject<AnnotationData>(instantJson);
|
||||||
|
if (annotationData is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
annotationData.annotations.Reverse();
|
||||||
|
|
||||||
|
foreach (var annotation in annotationData.annotations)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Adding AnnotationID: " + annotation.id);
|
||||||
|
|
||||||
|
switch (annotation.type)
|
||||||
|
{
|
||||||
|
case AnnotationType.Image:
|
||||||
|
AddImageAnnotation(annotation, annotationData.attachments);
|
||||||
|
break;
|
||||||
|
case AnnotationType.Ink:
|
||||||
|
AddInkAnnotation(annotation);
|
||||||
|
break;
|
||||||
|
case AnnotationType.Widget:
|
||||||
|
var formFieldValue = annotationData.formFieldValues.FirstOrDefault(fv => fv.name == annotation.id);
|
||||||
|
if (formFieldValue is not null && !_options.IgnoredLabels.Contains(formFieldValue.value))
|
||||||
|
{
|
||||||
|
AddFormFieldValue(annotation, formFieldValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddImageAnnotation(double x, double y, double width, double height, int page, string base64)
|
||||||
|
{
|
||||||
|
manager.SelectPage(page);
|
||||||
|
manager.AddEmbeddedImageAnnotFromBase64(base64, (float) x, (float) y, (float) width, (float) height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddImageAnnotation(Annotation annotation, Dictionary<string, Attachment> attachments)
|
||||||
|
{
|
||||||
|
var attachment = attachments.Where(a => a.Key == annotation.imageAttachmentId).SingleOrDefault();
|
||||||
|
|
||||||
|
var bounds = annotation.bbox.Select(ToInches).ToList();
|
||||||
|
|
||||||
|
var x = bounds[0];
|
||||||
|
var y = bounds[1];
|
||||||
|
var width = bounds[2];
|
||||||
|
var height = bounds[3];
|
||||||
|
|
||||||
|
manager.SelectPage(annotation.pageIndex + 1);
|
||||||
|
manager.AddEmbeddedImageAnnotFromBase64(attachment.Value.binary, (float) x, (float) y, (float) width, (float) height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddInkAnnotation(int page, string value)
|
||||||
|
{
|
||||||
|
var ink = JsonConvert.DeserializeObject<Ink>(value);
|
||||||
|
if (ink is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segments = ink.lines.points;
|
||||||
|
var color = ColorTranslator.FromHtml(ink.strokeColor);
|
||||||
|
manager.SelectPage(page);
|
||||||
|
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
var points = segment.Select(ToPointF).ToArray();
|
||||||
|
manager.AddFreeHandAnnot(color, points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddInkAnnotation(Annotation annotation)
|
||||||
|
{
|
||||||
|
var segments = annotation.lines.points;
|
||||||
|
var color = ColorTranslator.FromHtml(annotation.strokeColor);
|
||||||
|
manager.SelectPage(annotation.pageIndex + 1);
|
||||||
|
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
var points = segment.Select(ToPointF).ToArray();
|
||||||
|
manager.AddFreeHandAnnot(color, points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddFormFieldValue(double x, double y, double width, double height, int page, string value)
|
||||||
|
{
|
||||||
|
manager.SelectPage(page);
|
||||||
|
|
||||||
|
var annot = manager.AddTextAnnot((float) x, (float) y, (float) width, (float) height, value);
|
||||||
|
annot.FontName = _options.FontName;
|
||||||
|
annot.FontSize = _options.FontSize;
|
||||||
|
annot.FontStyle = _options.FontStyle;
|
||||||
|
manager.SaveAnnotationsToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddFormFieldValue(Annotation annotation, FormFieldValue formFieldValue)
|
||||||
|
{
|
||||||
|
var ffIndex = EGName.Index[annotation.egName];
|
||||||
|
|
||||||
|
var bounds = annotation.bbox.Select(ToInches).ToList();
|
||||||
|
|
||||||
|
var x = bounds[0];
|
||||||
|
var y = bounds[1] + _options.YOffset * ffIndex + _options.TopMargin;
|
||||||
|
var width = bounds[2];
|
||||||
|
var height = bounds[3];
|
||||||
|
|
||||||
|
manager.SelectPage(annotation.pageIndex + 1);
|
||||||
|
var annot = manager.AddTextAnnot((float) x, (float) y, (float) width, (float) height, formFieldValue.value);
|
||||||
|
annot.FontName = _options.FontName;
|
||||||
|
annot.FontSize = _options.FontSize;
|
||||||
|
annot.FontStyle = _options.FontStyle;
|
||||||
|
manager.SaveAnnotationsToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PointF ToPointF(List<float> points)
|
||||||
|
{
|
||||||
|
var convertedPoints = points.Select(ToInches).ToList();
|
||||||
|
return new PointF(convertedPoints[0], convertedPoints[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ToInches(double value) => value / 72;
|
||||||
|
|
||||||
|
private static float ToInches(float value) => value / 72;
|
||||||
|
|
||||||
|
internal static class AnnotationType
|
||||||
|
{
|
||||||
|
public const string Image = "pspdfkit/image";
|
||||||
|
public const string Ink = "pspdfkit/ink";
|
||||||
|
public const string Widget = "pspdfkit/widget";
|
||||||
|
public const string FormField = "pspdfkit/form-field-value";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AnnotationData
|
||||||
|
{
|
||||||
|
public List<Annotation> annotations { get; set; } = new();
|
||||||
|
|
||||||
|
public IEnumerable<List<Annotation>> AnnotationsByReceiver => annotations
|
||||||
|
.Where(annot => annot.hasStructuredID)
|
||||||
|
.GroupBy(a => a.receiverId)
|
||||||
|
.Select(g => g.ToList());
|
||||||
|
|
||||||
|
public IEnumerable<List<Annotation>> UnstructuredAnnotations => annotations
|
||||||
|
.Where(annot => !annot.hasStructuredID)
|
||||||
|
.GroupBy(a => a.receiverId)
|
||||||
|
.Select(g => g.ToList());
|
||||||
|
|
||||||
|
public Dictionary<string, Attachment> attachments { get; set; } = new();
|
||||||
|
public List<FormFieldValue> formFieldValues { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Annotation
|
||||||
|
{
|
||||||
|
private string? _id;
|
||||||
|
|
||||||
|
public int envelopeId;
|
||||||
|
public int receiverId;
|
||||||
|
public int index;
|
||||||
|
public string egName = EGName.NoName;
|
||||||
|
public bool hasStructuredID;
|
||||||
|
|
||||||
|
public bool isLabel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(egName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = egName.Split('_');
|
||||||
|
return parts.Length > 1 && parts[1] == "label";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string id
|
||||||
|
{
|
||||||
|
get => _id ?? string.Empty;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_id = value;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_id))
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException("The identifier of annotation is null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = value.Split('#');
|
||||||
|
|
||||||
|
if (parts.Length != 4)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(parts[0], out envelopeId))
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"The envelope ID of annotation is not integer. Id: {_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(parts[1], out receiverId))
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"The receiver ID of annotation is not integer. Id: {_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(parts[2], out index))
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException($"The index of annotation is not integer. Id: {_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
egName = parts[3];
|
||||||
|
|
||||||
|
hasStructuredID = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<double> bbox { get; set; } = new();
|
||||||
|
|
||||||
|
public string type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool isSignature { get; set; }
|
||||||
|
|
||||||
|
public string imageAttachmentId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Lines lines { get; set; } = new();
|
||||||
|
|
||||||
|
public int pageIndex { get; set; }
|
||||||
|
|
||||||
|
public string strokeColor { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Ink
|
||||||
|
{
|
||||||
|
public Lines lines { get; set; } = new();
|
||||||
|
|
||||||
|
public string strokeColor { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EGName
|
||||||
|
{
|
||||||
|
public static readonly string NoName = Guid.NewGuid().ToString();
|
||||||
|
public static readonly string Seal = "signature";
|
||||||
|
|
||||||
|
public static readonly ImmutableDictionary<string, int> Index = new Dictionary<string, int>
|
||||||
|
{
|
||||||
|
{ NoName, 0 },
|
||||||
|
{ Seal, 0 },
|
||||||
|
{ "position", 1 },
|
||||||
|
{ "city", 2 },
|
||||||
|
{ "date", 3 }
|
||||||
|
}.ToImmutableDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Lines
|
||||||
|
{
|
||||||
|
public List<List<List<float>>> points { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Attachment
|
||||||
|
{
|
||||||
|
public string binary { get; set; } = string.Empty;
|
||||||
|
public string contentType { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class FormFieldValue
|
||||||
|
{
|
||||||
|
public string name { get; set; } = string.Empty;
|
||||||
|
public string value { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using EnvelopeGenerator.ServiceHost.Exceptions;
|
||||||
|
using GdPicture14;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
|
||||||
|
public class PDFMerger
|
||||||
|
{
|
||||||
|
private readonly AnnotationManager _manager;
|
||||||
|
private readonly LicenseManager _licenseManager;
|
||||||
|
|
||||||
|
private const bool AllowRasterization = true;
|
||||||
|
private const bool AllowVectorization = true;
|
||||||
|
|
||||||
|
private readonly PdfConversionConformance _pdfaConformanceLevel = PdfConversionConformance.PDF_A_1b;
|
||||||
|
|
||||||
|
public PDFMerger(LicenseManager licenseManager, AnnotationManager annotationManager)
|
||||||
|
{
|
||||||
|
_licenseManager = licenseManager;
|
||||||
|
_manager = annotationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] MergeDocuments(byte[] document, byte[] report)
|
||||||
|
{
|
||||||
|
using var documentStream = new MemoryStream(document);
|
||||||
|
using var reportStream = new MemoryStream(report);
|
||||||
|
using var finalStream = new MemoryStream();
|
||||||
|
using var documentPdf = new GdPicturePDF();
|
||||||
|
using var reportPdf = new GdPicturePDF();
|
||||||
|
|
||||||
|
documentPdf.LoadFromStream(documentStream, true);
|
||||||
|
var status = documentPdf.GetStat();
|
||||||
|
if (status != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new MergeDocumentException($"Document could not be loaded: {status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
reportPdf.LoadFromStream(reportStream, true);
|
||||||
|
status = reportPdf.GetStat();
|
||||||
|
if (status != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new MergeDocumentException($"Report could not be loaded: {status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedPdf = documentPdf.Merge2Documents(documentPdf, reportPdf);
|
||||||
|
status = mergedPdf.GetStat();
|
||||||
|
if (status != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new MergeDocumentException($"Documents could not be merged: {status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedPdf.ConvertToPDFA(finalStream, _pdfaConformanceLevel, AllowVectorization, AllowRasterization);
|
||||||
|
status = documentPdf.GetStat();
|
||||||
|
if (status != GdPictureStatus.OK)
|
||||||
|
{
|
||||||
|
throw new MergeDocumentException($"Document could not be converted to PDF/A: {status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalStream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using System.Data;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Exceptions;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
|
||||||
|
[Obsolete("Instead of ReportModel create and use EnvelopeReport mediator queries")]
|
||||||
|
public class ReportCreator(ReportModel ReportModel, ILogger<ReportCreator> Logger)
|
||||||
|
{
|
||||||
|
[Obsolete("Solve the spaghetti...")]
|
||||||
|
private Envelope? _envelope;
|
||||||
|
|
||||||
|
[Obsolete("Instead of ReportModel create and use EnvelopeReport mediator queries and solve this spaghetti...")]
|
||||||
|
public byte[] CreateReport(Envelope envelope)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Loading report data..");
|
||||||
|
var table = ReportModel.List(envelope.Id);
|
||||||
|
var items = GetReportSource(table);
|
||||||
|
|
||||||
|
_envelope = envelope;
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
throw new CreateReportException("No report data found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("Creating report with [{count}] items..", items.Count);
|
||||||
|
var buffer = DoCreateReport(items);
|
||||||
|
Logger.LogDebug("Report created!");
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex);
|
||||||
|
throw new CreateReportException("Could not prepare report data!", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ReportItem> GetReportSource(DataTable dataTable)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Preparing report data");
|
||||||
|
return dataTable.Rows
|
||||||
|
.Cast<DataRow>()
|
||||||
|
.Select(ToReportItem)
|
||||||
|
.OrderByDescending(r => r.ItemDate)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] DoCreateReport(List<ReportItem> reportItems)
|
||||||
|
{
|
||||||
|
var items = reportItems.Select(MergeEnvelope).ToList();
|
||||||
|
var source = new ReportSource { Items = items };
|
||||||
|
var report = new rptEnvelopeHistory { DataSource = source, DataMember = "Items" };
|
||||||
|
|
||||||
|
Logger.LogDebug("Creating report in memory..");
|
||||||
|
report.CreateDocument();
|
||||||
|
|
||||||
|
Logger.LogDebug("Exporting report to stream..");
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
report.ExportToPdf(stream);
|
||||||
|
|
||||||
|
Logger.LogDebug("Writing report to buffer..");
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("Solve this spaghetti...")]
|
||||||
|
private ReportItem MergeEnvelope(ReportItem item)
|
||||||
|
{
|
||||||
|
if (item.Envelope is null)
|
||||||
|
{
|
||||||
|
item.Envelope = _envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReportItem ToReportItem(DataRow row)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new ReportItem
|
||||||
|
{
|
||||||
|
EnvelopeId = row.ItemEx("ENVELOPE_ID", 0),
|
||||||
|
EnvelopeTitle = row.ItemEx("HEAD_TITLE", string.Empty),
|
||||||
|
EnvelopeSubject = row.ItemEx("HEAD_SUBJECT", string.Empty),
|
||||||
|
ItemDate = row.ItemEx("POS_WHEN", DateTime.MinValue),
|
||||||
|
ItemStatus = (EnvelopeStatus)row.ItemEx("POS_STATUS", 0),
|
||||||
|
ItemUserReference = row.ItemEx("POS_WHO", string.Empty)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex);
|
||||||
|
throw new CreateReportException("Could not read data from database!", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
|
||||||
public class ReportItem
|
public class ReportItem
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
|
||||||
|
|
||||||
public class ReportSource
|
public class ReportSource
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
|
||||||
|
public class rptEnvelopeHistory
|
||||||
|
{
|
||||||
|
public object? DataSource { get; set; }
|
||||||
|
public string? DataMember { get; set; }
|
||||||
|
|
||||||
|
public void CreateDocument()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExportToPdf(Stream stream)
|
||||||
|
{
|
||||||
|
stream.Write(Array.Empty<byte>(), 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
360
EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocumentJob.cs
Normal file
360
EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocumentJob.cs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
using System.Data;
|
||||||
|
using DigitalData.Modules.Database;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Exceptions;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
using GdPicture14;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
[Obsolete("ActionService is a placeholder service added by copilot. Migrate the actual logic from CommonServices.Jobs")]
|
||||||
|
public class FinalizeDocumentJob(IOptions<WorkerOptions> options, IConfiguration config, ILogger<FinalizeDocumentJob> logger, TempFiles tempFiles, ActionService actionService, PDFBurner pdfBurner, PDFMerger pdfMerger, ReportCreator reportCreator, ConfigModel _configModel, EnvelopeModel _envelopeModel, ReportModel _reportModel, MSSQLServer _database, GdViewer? _gdViewer, LicenseManager licenseManager)
|
||||||
|
{
|
||||||
|
private readonly WorkerOptions _options = options.Value;
|
||||||
|
|
||||||
|
private DbConfig? _config;
|
||||||
|
|
||||||
|
private const int CompleteWaitTime = 1;
|
||||||
|
private string _parentFolderUid = string.Empty;
|
||||||
|
|
||||||
|
private sealed class EnvelopeData
|
||||||
|
{
|
||||||
|
public int EnvelopeId { get; set; }
|
||||||
|
public string EnvelopeUuid { get; set; } = string.Empty;
|
||||||
|
public string DocumentPath { get; set; } = string.Empty;
|
||||||
|
public List<string> AnnotationData { get; set; } = new();
|
||||||
|
public byte[]? DocAsByte { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var gdPictureKey = _options.GdPictureLicenseKey;
|
||||||
|
tempFiles.Create();
|
||||||
|
var jobId = typeof(FinalizeDocumentJob).FullName;
|
||||||
|
logger.LogDebug("Starting job {jobId}", jobId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogDebug("Loading Configuration..");
|
||||||
|
_config = _configModel?.LoadConfiguration() ?? throw new InvalidOperationException("Configuration could not be loaded because there is no record");
|
||||||
|
|
||||||
|
logger.LogDebug("DocumentPath: [{documentPath}]", _config.DocumentPath);
|
||||||
|
logger.LogDebug("ExportPath: [{exportPath}]", _config.ExportPath);
|
||||||
|
|
||||||
|
var completeStatus = EnvelopeStatus.EnvelopeCompletelySigned;
|
||||||
|
var sql = $"SELECT * FROM TBSIG_ENVELOPE WHERE STATUS = {completeStatus} AND DATEDIFF(minute, CHANGED_WHEN, GETDATE()) >= {CompleteWaitTime} ORDER BY GUID";
|
||||||
|
var table = _database.GetDatatable(sql);
|
||||||
|
|
||||||
|
var envelopeIds = table.Rows.Cast<DataRow>()
|
||||||
|
.Select(r => r.Field<int>("GUID"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (envelopeIds.Count > 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Found [{count}] completed envelopes.", envelopeIds.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = envelopeIds.Count;
|
||||||
|
var current = 1;
|
||||||
|
|
||||||
|
foreach (var id in envelopeIds)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Finalizing Envelope [{id}] ({current}/{total})", id, current, total);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var envelope = _envelopeModel?.GetById(id);
|
||||||
|
if (envelope is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Envelope could not be loaded for Id [{id}]!", id);
|
||||||
|
throw new ArgumentNullException(nameof(EnvelopeData));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Loading Envelope Data..");
|
||||||
|
var envelopeData = GetEnvelopeData(id);
|
||||||
|
|
||||||
|
if (envelopeData is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("EnvelopeData could not be loaded for Id [{id}]!", id);
|
||||||
|
throw new ArgumentNullException(nameof(EnvelopeData));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Burning Annotations to pdf ...");
|
||||||
|
var burnedDocument = BurnAnnotationsToPdf(envelopeData);
|
||||||
|
if (burnedDocument is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Document could not be finalized!");
|
||||||
|
throw new ApplicationException("Document could not be finalized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionService?.CreateReport(envelope) == false)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Document Report could not be created!");
|
||||||
|
throw new ApplicationException("Document Report could not be created");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Creating report..");
|
||||||
|
var report = reportCreator!.CreateReport(envelope);
|
||||||
|
logger.LogDebug("Report created!");
|
||||||
|
|
||||||
|
logger.LogDebug("Merging documents ...");
|
||||||
|
var mergedDocument = pdfMerger!.MergeDocuments(burnedDocument, report);
|
||||||
|
logger.LogDebug("Documents merged!");
|
||||||
|
|
||||||
|
var outputDirectoryPath = Path.Combine(_config.ExportPath, _parentFolderUid);
|
||||||
|
logger.LogDebug("oOutputDirectoryPath is {outputDirectoryPath}", outputDirectoryPath);
|
||||||
|
if (!Directory.Exists(outputDirectoryPath))
|
||||||
|
{
|
||||||
|
logger.LogDebug("Directory not existing. Creating ... ");
|
||||||
|
Directory.CreateDirectory(outputDirectoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputFilePath = Path.Combine(outputDirectoryPath, $"{envelope.Uuid}.pdf");
|
||||||
|
logger.LogDebug("Writing finalized Pdf to disk..");
|
||||||
|
logger.LogInformation("Output path is [{outputFilePath}]", outputFilePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllBytes(outputFilePath, mergedDocument);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Could not export final document to disk!");
|
||||||
|
throw new ExportDocumentException("Could not export final document to disk!", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Writing EB-bytes to database...");
|
||||||
|
UpdateFileDb(outputFilePath, envelope.Id);
|
||||||
|
|
||||||
|
if (!SendFinalEmails(envelope))
|
||||||
|
{
|
||||||
|
throw new ApplicationException("Final emails could not be sent!");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Report-mails successfully sent!");
|
||||||
|
|
||||||
|
logger.LogDebug("Setting envelope status..");
|
||||||
|
if (actionService?.FinalizeEnvelope(envelope) == false)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Envelope could not be finalized!");
|
||||||
|
throw new ApplicationException("Envelope could not be finalized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex);
|
||||||
|
logger.LogWarning(ex, "Unhandled exception while working envelope [{id}]", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
current += 1;
|
||||||
|
logger.LogInformation("Envelope [{id}] finalized!", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Completed job {jobId} successfully!", jobId);
|
||||||
|
}
|
||||||
|
catch (MergeDocumentException ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Certificate Document job failed at step: Merging documents!");
|
||||||
|
logger.LogError(ex);
|
||||||
|
}
|
||||||
|
catch (ExportDocumentException ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Certificate Document job failed at step: Exporting document!");
|
||||||
|
logger.LogError(ex);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Certificate Document job failed!");
|
||||||
|
logger.LogError(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
logger.LogDebug("Job execution for [{jobId}] ended", jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFileDb(string filePath, long envelopeId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var imageData = ReadFile(filePath);
|
||||||
|
if (imageData is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = $"UPDATE TBSIG_ENVELOPE SET DOC_RESULT = @ImageData WHERE GUID = {envelopeId}";
|
||||||
|
using var command = new SqlCommand(query, _database!.GetConnection);
|
||||||
|
command.Parameters.Add(new SqlParameter("@ImageData", imageData));
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[]? ReadFile(string path)
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(path);
|
||||||
|
var numBytes = fileInfo.Length;
|
||||||
|
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
return reader.ReadBytes((int)numBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SendFinalEmails(Envelope envelope)
|
||||||
|
{
|
||||||
|
var mailToCreator = (FinalEmailType)(envelope.FinalEmailToCreator ?? 0);
|
||||||
|
var mailToReceivers = (FinalEmailType)(envelope.FinalEmailToReceivers ?? 0);
|
||||||
|
|
||||||
|
if (mailToCreator != FinalEmailType.No)
|
||||||
|
{
|
||||||
|
logger?.LogDebug("Sending email to creator ...");
|
||||||
|
SendFinalEmailToCreator(envelope, mailToCreator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger?.LogWarning("No SendFinalEmailToCreator - oMailToCreator [{0}] <> [{1}] ", mailToCreator, FinalEmailType.No);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mailToReceivers != FinalEmailType.No)
|
||||||
|
{
|
||||||
|
logger?.LogDebug("Sending emails to receivers..");
|
||||||
|
SendFinalEmailToReceivers(envelope, mailToReceivers);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger?.LogWarning("No SendFinalEmailToReceivers - oMailToCreator [{0}] <> [{1}] ", mailToReceivers, FinalEmailType.No);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SendFinalEmailToCreator(Envelope envelope, FinalEmailType mailToCreator)
|
||||||
|
{
|
||||||
|
var includeAttachment = SendFinalEmailWithAttachment(mailToCreator);
|
||||||
|
logger?.LogDebug("Attachment included: [{0}]", includeAttachment);
|
||||||
|
|
||||||
|
if (actionService?.CompleteEnvelope(envelope) == false)
|
||||||
|
{
|
||||||
|
logger?.LogError(new Exception("CompleteEnvelope failed"), "Envelope could not be completed for receiver [{0}]", envelope.User?.Email);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SendFinalEmailToReceivers(Envelope envelope, FinalEmailType mailToReceivers)
|
||||||
|
{
|
||||||
|
var includeAttachment = SendFinalEmailWithAttachment(mailToReceivers);
|
||||||
|
logger?.LogDebug("Attachment included: [{0}]", includeAttachment);
|
||||||
|
|
||||||
|
foreach (var receiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
||||||
|
{
|
||||||
|
if (actionService?.CompleteEnvelope(envelope, receiver.Receiver) == false)
|
||||||
|
{
|
||||||
|
logger?.LogError(new Exception("CompleteEnvelope failed"), "Envelope could not be completed for receiver [{0}]", receiver.Receiver?.EmailAddress);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SendFinalEmailWithAttachment(FinalEmailType type)
|
||||||
|
{
|
||||||
|
return type == FinalEmailType.YesWithAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] BurnAnnotationsToPdf(EnvelopeData envelopeData)
|
||||||
|
{
|
||||||
|
var envelopeId = envelopeData.EnvelopeId;
|
||||||
|
|
||||||
|
logger?.LogInformation("Burning [{0}] signatures", envelopeData.AnnotationData.Count);
|
||||||
|
var annotations = envelopeData.AnnotationData;
|
||||||
|
var inputPath = string.Empty;
|
||||||
|
if (envelopeData.DocAsByte is null)
|
||||||
|
{
|
||||||
|
inputPath = envelopeData.DocumentPath;
|
||||||
|
logger?.LogInformation("Input path: [{0}]", inputPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger?.LogDebug("we got bytes..");
|
||||||
|
inputPath = _config!.DocumentPathOrigin;
|
||||||
|
logger?.LogDebug("oInputPath: {0}", _config.DocumentPathOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envelopeData.DocAsByte is null)
|
||||||
|
{
|
||||||
|
var directorySource = Path.GetDirectoryName(inputPath) ?? string.Empty;
|
||||||
|
var split = directorySource.Split('\\');
|
||||||
|
_parentFolderUid = split[^1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_parentFolderUid = envelopeData.EnvelopeUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger?.LogInformation("ParentFolderUID: [{0}]", _parentFolderUid);
|
||||||
|
byte[] inputDocumentBuffer;
|
||||||
|
if (envelopeData.DocAsByte is not null)
|
||||||
|
{
|
||||||
|
inputDocumentBuffer = envelopeData.DocAsByte;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
inputDocumentBuffer = File.ReadAllBytes(inputPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new BurnAnnotationException("Source document could not be read from disk!", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfBurner!.BurnAnnotsToPDF(inputDocumentBuffer, annotations, envelopeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnvelopeData? GetEnvelopeData(int envelopeId)
|
||||||
|
{
|
||||||
|
var sql = $"SELECT T.GUID, T.ENVELOPE_UUID, T.ENVELOPE_TYPE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T\n JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID\n WHERE T.GUID = {envelopeId}";
|
||||||
|
var table = _database!.GetDatatable(sql);
|
||||||
|
var row = table.Rows.Cast<DataRow>().SingleOrDefault();
|
||||||
|
if (row is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var annotationData = GetAnnotationData(envelopeId);
|
||||||
|
var data = new EnvelopeData
|
||||||
|
{
|
||||||
|
EnvelopeId = envelopeId,
|
||||||
|
DocumentPath = row.ItemEx("FILEPATH", string.Empty),
|
||||||
|
AnnotationData = annotationData,
|
||||||
|
DocAsByte = row.Field<byte[]?>("BYTE_DATA"),
|
||||||
|
EnvelopeUuid = row.ItemEx("ENVELOPE_UUID", string.Empty)
|
||||||
|
};
|
||||||
|
|
||||||
|
logger?.LogDebug("Document path: [{0}]", data.DocumentPath);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetAnnotationData(int envelopeId)
|
||||||
|
{
|
||||||
|
var sql = $"SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = {envelopeId}";
|
||||||
|
var table = _database!.GetDatatable(sql);
|
||||||
|
|
||||||
|
return table.Rows.Cast<DataRow>()
|
||||||
|
.Select(r => r.ItemEx("VALUE", string.Empty))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace DigitalData.Modules.Database;
|
||||||
|
|
||||||
|
public class MSSQLServer(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
private readonly string _connectionString = configuration.GetConnectionString("Default")
|
||||||
|
?? throw new InvalidOperationException("Connection string 'Default' not found.");
|
||||||
|
|
||||||
|
public static string DecryptConnectionString(string connectionString) => connectionString;
|
||||||
|
|
||||||
|
public SqlConnection GetConnection
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var connection = new SqlConnection(_connectionString);
|
||||||
|
if (connection.State != ConnectionState.Open)
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataTable GetDatatable(string sql)
|
||||||
|
{
|
||||||
|
using var connection = new SqlConnection(_connectionString);
|
||||||
|
using var command = new SqlCommand(sql, connection);
|
||||||
|
using var adapter = new SqlDataAdapter(command);
|
||||||
|
var table = new DataTable();
|
||||||
|
adapter.Fill(table);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? GetScalarValue(string sql)
|
||||||
|
{
|
||||||
|
using var connection = new SqlConnection(_connectionString);
|
||||||
|
using var command = new SqlCommand(sql, connection);
|
||||||
|
connection.Open();
|
||||||
|
return command.ExecuteScalar();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ExecuteNonQuery(string sql)
|
||||||
|
{
|
||||||
|
using var connection = new SqlConnection(_connectionString);
|
||||||
|
using var command = new SqlCommand(sql, connection);
|
||||||
|
connection.Open();
|
||||||
|
return command.ExecuteNonQuery() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ExecuteNonQuery(SqlCommand command)
|
||||||
|
{
|
||||||
|
using var connection = new SqlConnection(_connectionString);
|
||||||
|
command.Connection = connection;
|
||||||
|
connection.Open();
|
||||||
|
return command.ExecuteNonQuery() > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
EnvelopeGenerator.ServiceHost/Jobs/ReportModel.cs
Normal file
15
EnvelopeGenerator.ServiceHost/Jobs/ReportModel.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using DigitalData.Modules.Database;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
[Obsolete("Create and use EnvelopeReport mediator queries")]
|
||||||
|
public class ReportModel(MSSQLServer Database)
|
||||||
|
{
|
||||||
|
[Obsolete("Create and use EnvelopeReport mediator queries")]
|
||||||
|
public DataTable List(int envelopeId)
|
||||||
|
{
|
||||||
|
var sql = $"SELECT * FROM VWSIG_ENVELOPE_REPORT WHERE ENVELOPE_ID = {envelopeId}";
|
||||||
|
return Database.GetDatatable(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
EnvelopeGenerator.ServiceHost/Jobs/State.cs
Normal file
14
EnvelopeGenerator.ServiceHost/Jobs/State.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using DigitalData.Modules.Database;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
[Obsolete("Use DbContext")]
|
||||||
|
public class State
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public FormUser? User { get; set; }
|
||||||
|
public Config? Config { get; set; }
|
||||||
|
public DbConfig? DbConfig { get; set; }
|
||||||
|
public MSSQLServer? Database { get; set; }
|
||||||
|
}
|
||||||
73
EnvelopeGenerator.ServiceHost/Jobs/TempFiles.cs
Normal file
73
EnvelopeGenerator.ServiceHost/Jobs/TempFiles.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
public class TempFiles
|
||||||
|
{
|
||||||
|
public string TempPath { get; }
|
||||||
|
|
||||||
|
private readonly ILogger<TempFiles> _logger;
|
||||||
|
|
||||||
|
public TempFiles(ILogger<TempFiles> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
var tempDirectoryPath = Path.GetTempPath();
|
||||||
|
TempPath = Path.Combine(tempDirectoryPath, "EnvelopeGenerator");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Create()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(TempPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(TempPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CleanUpFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CleanUpFiles()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var fileItem in Directory.GetFiles(TempPath))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Deleting tempPath-file: {fileItem} ...", fileItem);
|
||||||
|
File.Delete(fileItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CleanUp()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Deleting tempPath-Data: {TempPath} ...", TempPath);
|
||||||
|
Directory.Delete(TempPath, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
EnvelopeGenerator.ServiceHost/Jobs/WorkerOptions.cs
Normal file
29
EnvelopeGenerator.ServiceHost/Jobs/WorkerOptions.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
public class WorkerOptions
|
||||||
|
{
|
||||||
|
public string GdPictureLicenseKey { get; set; } = null!;
|
||||||
|
|
||||||
|
[Obsolete("Use ILogger.")]
|
||||||
|
public bool Debug { get; set; }
|
||||||
|
|
||||||
|
public PDFBurnerOptions PdfBurner { get; set; } = new();
|
||||||
|
|
||||||
|
public record PDFBurnerOptions
|
||||||
|
{
|
||||||
|
public List<string> IgnoredLabels { get; set; } = ["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;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
EnvelopeGenerator.ServiceHost/Program.cs
Normal file
31
EnvelopeGenerator.ServiceHost/Program.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using EnvelopeGenerator.ServiceHost;
|
||||||
|
using EnvelopeGenerator.ServiceHost.Extensions;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddFinalizeDocumentJob(builder.Configuration);
|
||||||
|
builder.Services.AddHostedService<Worker>();
|
||||||
|
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
41
EnvelopeGenerator.ServiceHost/Properties/launchSettings.json
Normal file
41
EnvelopeGenerator.ServiceHost/Properties/launchSettings.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:27420",
|
||||||
|
"sslPort": 44372
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5088",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7234;http://localhost:5088",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
EnvelopeGenerator.ServiceHost/Worker.cs
Normal file
31
EnvelopeGenerator.ServiceHost/Worker.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using EnvelopeGenerator.ServiceHost.Jobs;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ServiceHost;
|
||||||
|
|
||||||
|
public class Worker : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<Worker> _logger;
|
||||||
|
private readonly int _delayMilliseconds;
|
||||||
|
private readonly FinalizeDocumentJob _finalizeDocumentJob;
|
||||||
|
|
||||||
|
public Worker(ILogger<Worker> logger, IConfiguration configuration, FinalizeDocumentJob finalizeDocumentJob)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_delayMilliseconds = Math.Max(1, configuration.GetValue("Worker:DelayMilliseconds", 1000));
|
||||||
|
_finalizeDocumentJob = finalizeDocumentJob;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (_logger.IsEnabled(LogLevel.Information))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _finalizeDocumentJob.ExecuteAsync(stoppingToken);
|
||||||
|
await Task.Delay(_delayMilliseconds, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
EnvelopeGenerator.ServiceHost/appsettings.json
Normal file
12
EnvelopeGenerator.ServiceHost/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Worker": {
|
||||||
|
"DelayMilliseconds": 1000
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -1,56 +1,32 @@
|
|||||||
using EnvelopeGenerator.Domain.Entities;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Web.Controllers.Test;
|
namespace EnvelopeGenerator.Web.Controllers.Test;
|
||||||
|
|
||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public class TestEnvelopeController : TestControllerBase<IEnvelopeService, EnvelopeDto, Envelope, int>
|
public class TestEnvelopeController : ControllerBase
|
||||||
{
|
{
|
||||||
public TestEnvelopeController(ILogger<TestEnvelopeController> logger, IEnvelopeService service) : base(logger, service)
|
private readonly IMediator _mediator;
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[NonAction]
|
/// <summary>
|
||||||
public override Task<IActionResult> GetAll() => base.GetAll();
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
public TestEnvelopeController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetAll([FromQuery] string? envelopeKey = default, [FromQuery] bool withDocuments = false, [FromQuery] bool withHistory = false, [FromQuery] bool withDocumentReceiverElement = false, [FromQuery] bool withUser = false, [FromQuery] bool withAll = true)
|
public async Task<IActionResult> GetAll([FromQuery] ReadEnvelopeQuery query, CancellationToken cancel) => Ok(await _mediator.Send(query, cancel));
|
||||||
{
|
|
||||||
if (envelopeKey is not null)
|
|
||||||
{
|
|
||||||
(var uuid, var signature) = envelopeKey.DecodeEnvelopeReceiverId();
|
|
||||||
if (uuid is null)
|
|
||||||
return BadRequest("UUID is null");
|
|
||||||
var envlopeServiceResult = await _service.ReadByUuidAsync(
|
|
||||||
uuid: uuid,
|
|
||||||
withDocuments: withDocuments, withHistory: withHistory, withDocumentReceiverElement: withDocumentReceiverElement, withUser: withUser, withAll: withAll);
|
|
||||||
|
|
||||||
if (envlopeServiceResult.IsSuccess)
|
|
||||||
{
|
|
||||||
return Ok(envlopeServiceResult.Data);
|
|
||||||
}
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _service.ReadAllWithAsync(documents: withDocuments, history: withHistory);
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
return NotFound(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("decode")]
|
[HttpGet("decode")]
|
||||||
public IActionResult DecodeEnvelopeReceiverId(string envelopeReceiverId, int type = 0)
|
public IActionResult DecodeEnvelopeReceiverId(string envelopeReceiverId, int type = 0) => type switch
|
||||||
{
|
|
||||||
return type switch
|
|
||||||
{
|
{
|
||||||
1 => Ok(envelopeReceiverId.GetEnvelopeUuid()),
|
1 => Ok(envelopeReceiverId.GetEnvelopeUuid()),
|
||||||
2 => Ok(envelopeReceiverId.GetReceiverSignature()),
|
2 => Ok(envelopeReceiverId.GetReceiverSignature()),
|
||||||
_ => Ok(envelopeReceiverId.DecodeEnvelopeReceiverId()),
|
_ => Ok(envelopeReceiverId.DecodeEnvelopeReceiverId()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<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="BouncyCastle.Cryptography" Version="2.6.2" />
|
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.9.0" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Jobs\EnvelopeGenerator.Jobs.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"profiles": {
|
|
||||||
"EnvelopeGenerator.WorkerService": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"DOTNET_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,12 +33,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
|
||||||
EndProject
|
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
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ServiceHost", "EnvelopeGenerator.ServiceHost\EnvelopeGenerator.ServiceHost.csproj", "{5DD82482-E560-405F-BF55-04D647B05BEF}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -85,18 +83,14 @@ Global
|
|||||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.Build.0 = 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
|
|
||||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU
|
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5DD82482-E560-405F-BF55-04D647B05BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5DD82482-E560-405F-BF55-04D647B05BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5DD82482-E560-405F-BF55-04D647B05BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5DD82482-E560-405F-BF55-04D647B05BEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -115,9 +109,8 @@ Global
|
|||||||
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {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}
|
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
|
{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}
|
|
||||||
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||||
|
{5DD82482-E560-405F-BF55-04D647B05BEF} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}
|
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}
|
||||||
|
|||||||
Reference in New Issue
Block a user