diff --git a/EnvelopeGenerator.ServiceHost/EnvelopeGenerator.ServiceHost.csproj b/EnvelopeGenerator.ServiceHost/EnvelopeGenerator.ServiceHost.csproj
index b16aff60..f861646b 100644
--- a/EnvelopeGenerator.ServiceHost/EnvelopeGenerator.ServiceHost.csproj
+++ b/EnvelopeGenerator.ServiceHost/EnvelopeGenerator.ServiceHost.csproj
@@ -8,6 +8,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/ActionService.cs b/EnvelopeGenerator.ServiceHost/Jobs/ActionService.cs
new file mode 100644
index 00000000..1c5e4a89
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/ActionService.cs
@@ -0,0 +1,31 @@
+using DigitalData.Modules.Database;
+using EnvelopeGenerator.Domain.Entities;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs;
+
+public class ActionService : BaseService
+{
+ public ActionService(State state, MSSQLServer database) : base(state)
+ {
+ }
+
+ public bool CreateReport(Envelope envelope)
+ {
+ return true;
+ }
+
+ public bool FinalizeEnvelope(Envelope envelope)
+ {
+ return true;
+ }
+
+ public bool CompleteEnvelope(Envelope envelope)
+ {
+ return true;
+ }
+
+ public bool CompleteEnvelope(Envelope envelope, Receiver receiver)
+ {
+ return true;
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/BaseModel.cs b/EnvelopeGenerator.ServiceHost/Jobs/BaseModel.cs
new file mode 100644
index 00000000..eaed1e1e
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/BaseModel.cs
@@ -0,0 +1,18 @@
+using DigitalData.Modules.Database;
+using DigitalData.Modules.Logging;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs;
+
+public abstract class BaseModel
+{
+ protected MSSQLServer Database { get; }
+ protected Logger Logger { get; }
+ protected State State { get; }
+
+ protected BaseModel(State state)
+ {
+ Logger = state.LogConfig!.GetLogger();
+ Database = state.Database!;
+ State = state;
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/BaseService.cs b/EnvelopeGenerator.ServiceHost/Jobs/BaseService.cs
new file mode 100644
index 00000000..481b6e4e
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/BaseService.cs
@@ -0,0 +1,13 @@
+using DigitalData.Modules.Base;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs;
+
+public class BaseService : BaseClass
+{
+ protected State State { get; }
+
+ public BaseService(State state) : base(state.LogConfig!)
+ {
+ State = state;
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/ConfigModel.cs b/EnvelopeGenerator.ServiceHost/Jobs/ConfigModel.cs
new file mode 100644
index 00000000..d0cc76a4
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/ConfigModel.cs
@@ -0,0 +1,37 @@
+using DigitalData.Modules.Base;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs;
+
+public class ConfigModel : BaseModel
+{
+ public ConfigModel(State state) : base(state)
+ {
+ }
+
+ 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.Error(ex);
+ return new DbConfig();
+ }
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/DbConfig.cs b/EnvelopeGenerator.ServiceHost/Jobs/DbConfig.cs
new file mode 100644
index 00000000..823a23e1
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/DbConfig.cs
@@ -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; }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/EnvelopeModel.cs b/EnvelopeGenerator.ServiceHost/Jobs/EnvelopeModel.cs
new file mode 100644
index 00000000..7c2180a1
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/EnvelopeModel.cs
@@ -0,0 +1,42 @@
+using DigitalData.Modules.Base;
+using EnvelopeGenerator.Domain.Constants;
+using EnvelopeGenerator.Domain.Entities;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs;
+
+public class EnvelopeModel : BaseModel
+{
+ public EnvelopeModel(State state) : base(state)
+ {
+ }
+
+ 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().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()
+ };
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex);
+ return null;
+ }
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFBurner.cs b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFBurner.cs
new file mode 100644
index 00000000..8a9ac595
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFBurner.cs
@@ -0,0 +1,445 @@
+using System.Collections.Immutable;
+using System.Drawing;
+using System.IO;
+using DigitalData.Modules.Base;
+using DigitalData.Modules.Logging;
+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;
+
+public class PDFBurner : BaseClass
+{
+ private readonly AnnotationManager _manager;
+ private readonly LicenseManager _licenseManager;
+ private readonly DbContextOptions _dbContextOptions;
+
+ private readonly PDFBurnerParams _pdfBurnerParams;
+
+ public PDFBurner(LogConfig logConfig, string gdPictureLicenseKey, PDFBurnerParams pdfBurnerParams, string connectionString) : base(logConfig)
+ {
+ _licenseManager = new LicenseManager();
+ _licenseManager.RegisterKEY(gdPictureLicenseKey);
+
+ _manager = new AnnotationManager();
+
+ _pdfBurnerParams = pdfBurnerParams;
+ _dbContextOptions = new DbContextOptionsBuilder()
+ .UseSqlServer(connectionString)
+ .EnableDetailedErrors()
+ .EnableSensitiveDataLogging()
+ .Options;
+ }
+
+ public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, List instantJsonList, int envelopeId)
+ {
+ using var context = new EGDbContext(_dbContextOptions, Options.Create(new DbTriggerParams()));
+
+ 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 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 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.Warn("Error in AddInstantJSONAnnotationToPDF - oJson: ");
+ Logger.Warn(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(instantJson);
+ if (annotationData is null)
+ {
+ return;
+ }
+
+ annotationData.annotations.Reverse();
+
+ foreach (var annotation in annotationData.annotations)
+ {
+ Logger.Debug("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 && !_pdfBurnerParams.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 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(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 = _pdfBurnerParams.FontName;
+ annot.FontSize = _pdfBurnerParams.FontSize;
+ annot.FontStyle = _pdfBurnerParams.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] + _pdfBurnerParams.YOffset * ffIndex + _pdfBurnerParams.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 = _pdfBurnerParams.FontName;
+ annot.FontSize = _pdfBurnerParams.FontSize;
+ annot.FontStyle = _pdfBurnerParams.FontStyle;
+ _manager.SaveAnnotationsToPage();
+ }
+
+ private static PointF ToPointF(List 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 annotations { get; set; } = new();
+
+ public IEnumerable> AnnotationsByReceiver => annotations
+ .Where(annot => annot.hasStructuredID)
+ .GroupBy(a => a.receiverId)
+ .Select(g => g.ToList());
+
+ public IEnumerable> UnstructuredAnnotations => annotations
+ .Where(annot => !annot.hasStructuredID)
+ .GroupBy(a => a.receiverId)
+ .Select(g => g.ToList());
+
+ public Dictionary attachments { get; set; } = new();
+ public List 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 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 Index = new Dictionary
+ {
+ { NoName, 0 },
+ { Seal, 0 },
+ { "position", 1 },
+ { "city", 2 },
+ { "date", 3 }
+ }.ToImmutableDictionary();
+ }
+
+ internal class Lines
+ {
+ public List>> 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;
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFBurnerParams.cs b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFBurnerParams.cs
new file mode 100644
index 00000000..2d54c50c
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFBurnerParams.cs
@@ -0,0 +1,18 @@
+using System.Drawing;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
+
+public class PDFBurnerParams
+{
+ public List IgnoredLabels { get; set; } = 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;
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFMerger.cs b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFMerger.cs
new file mode 100644
index 00000000..b2f1556a
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/PDFMerger.cs
@@ -0,0 +1,65 @@
+using System.IO;
+using DigitalData.Modules.Base;
+using DigitalData.Modules.Logging;
+using EnvelopeGenerator.ServiceHost.Exceptions;
+using GdPicture14;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
+
+public class PDFMerger : BaseClass
+{
+ 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(LogConfig logConfig, string gdPictureLicenseKey) : base(logConfig)
+ {
+ _licenseManager = new LicenseManager();
+ _licenseManager.RegisterKEY(gdPictureLicenseKey);
+
+ _manager = new 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();
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportCreator.cs b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportCreator.cs
new file mode 100644
index 00000000..ad31a841
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportCreator.cs
@@ -0,0 +1,107 @@
+using System.Data;
+using System.IO;
+using DigitalData.Modules.Base;
+using DigitalData.Modules.Logging;
+using EnvelopeGenerator.Domain.Constants;
+using EnvelopeGenerator.Domain.Entities;
+using EnvelopeGenerator.ServiceHost.Exceptions;
+using EnvelopeGenerator.ServiceHost.Jobs;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
+
+public class ReportCreator : BaseClass
+{
+ private Envelope? _envelope;
+ private readonly ReportModel _reportModel;
+
+ public ReportCreator(LogConfig logConfig, State state) : base(logConfig)
+ {
+ _reportModel = new ReportModel(state);
+ }
+
+ public byte[] CreateReport(Envelope envelope)
+ {
+ try
+ {
+ Logger.Debug("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.Debug("Creating report with [{0}] items..", items.Count);
+ var buffer = DoCreateReport(items);
+ Logger.Debug("Report created!");
+
+ return buffer;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex);
+ throw new CreateReportException("Could not prepare report data!", ex);
+ }
+ }
+
+ private List GetReportSource(DataTable dataTable)
+ {
+ Logger.Debug("Preparing report data");
+ return dataTable.Rows
+ .Cast()
+ .Select(ToReportItem)
+ .OrderByDescending(r => r.ItemDate)
+ .ToList();
+ }
+
+ private byte[] DoCreateReport(List reportItems)
+ {
+ var items = reportItems.Select(MergeEnvelope).ToList();
+ var source = new ReportSource { Items = items };
+ var report = new rptEnvelopeHistory { DataSource = source, DataMember = "Items" };
+
+ Logger.Debug("Creating report in memory..");
+ report.CreateDocument();
+
+ Logger.Debug("Exporting report to stream..");
+ using var stream = new MemoryStream();
+ report.ExportToPdf(stream);
+
+ Logger.Debug("Writing report to buffer..");
+ return stream.ToArray();
+ }
+
+ 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.Error(ex);
+ throw new CreateReportException("Could not read data from database!", ex);
+ }
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportItem.cs b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportItem.cs
new file mode 100644
index 00000000..40758489
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportItem.cs
@@ -0,0 +1,19 @@
+using EnvelopeGenerator.Domain.Constants;
+using EnvelopeGenerator.Domain.Entities;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
+
+public class ReportItem
+{
+ public Envelope? Envelope { get; set; }
+ public int EnvelopeId { get; set; }
+ public string EnvelopeTitle { get; set; } = string.Empty;
+ public string EnvelopeSubject { get; set; } = string.Empty;
+
+ public EnvelopeStatus ItemStatus { get; set; }
+
+ public string ItemStatusTranslated => ItemStatus.ToString();
+
+ public string ItemUserReference { get; set; } = string.Empty;
+ public DateTime ItemDate { get; set; }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportSource.cs b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportSource.cs
new file mode 100644
index 00000000..e65ea1ea
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/ReportSource.cs
@@ -0,0 +1,6 @@
+namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
+
+public class ReportSource
+{
+ public List Items { get; set; } = new();
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/rptEnvelopeHistory.cs b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/rptEnvelopeHistory.cs
new file mode 100644
index 00000000..2a57be60
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocument/rptEnvelopeHistory.cs
@@ -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(), 0, 0);
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocumentJob.cs b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocumentJob.cs
new file mode 100644
index 00000000..39a9eb88
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/FinalizeDocumentJob.cs
@@ -0,0 +1,432 @@
+using System.Data;
+using System.IO;
+using DigitalData.Modules.Base;
+using DigitalData.Modules.Database;
+using DigitalData.Modules.Logging;
+using EnvelopeGenerator.Domain.Constants;
+using EnvelopeGenerator.Domain.Entities;
+using EnvelopeGenerator.ServiceHost.Exceptions;
+using EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
+using GdPicture14;
+using Microsoft.Data.SqlClient;
+using Quartz;
+
+namespace EnvelopeGenerator.ServiceHost.Jobs;
+
+public class FinalizeDocumentJob : IJob
+{
+ private readonly LicenseManager _licenseManager = new();
+ private GdViewer? _gdViewer;
+
+ private LogConfig? _logConfig;
+ private Logger? _logger;
+ private MSSQLServer? _database;
+ private DbConfig? _config;
+ private string _databaseConnectionString = string.Empty;
+
+ private ConfigModel? _configModel;
+ private EnvelopeModel? _envelopeModel;
+ private ReportModel? _reportModel;
+
+ private ActionService? _actionService;
+
+ private PDFBurner? _pdfBurner;
+ private PDFMerger? _pdfMerger;
+ private ReportCreator? _reportCreator;
+
+ private const int CompleteWaitTime = 1;
+ private string _parentFolderUid = string.Empty;
+ private TempFiles? _tempFiles;
+
+ 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 AnnotationData { get; set; } = new();
+ public byte[]? DocAsByte { get; set; }
+ }
+
+ public Task Execute(IJobExecutionContext context)
+ {
+ var gdPictureKey = (string)context.MergedJobDataMap[Value.GDPICTURE];
+ _logConfig = (LogConfig)context.MergedJobDataMap[Value.LOGCONFIG];
+ _logger = _logConfig.GetLogger();
+ _tempFiles = new TempFiles(_logConfig);
+ _tempFiles.Create();
+ var jobId = context.JobDetail.Key;
+ _logger.Debug("Starting job {0}", jobId);
+
+ try
+ {
+ _logger.Debug("Loading GdViewer..");
+ _gdViewer = new GdViewer();
+ _licenseManager.RegisterKEY(gdPictureKey);
+
+ _logger.Debug("Loading Database..");
+ var connectionString = (string)context.MergedJobDataMap[Value.DATABASE];
+ _databaseConnectionString = MSSQLServer.DecryptConnectionString(connectionString);
+ _database = new MSSQLServer(_logConfig, _databaseConnectionString);
+
+ _logger.Debug("Loading Models & Services");
+ var state = GetState();
+ InitializeModels(state);
+
+ _logger.Debug("Loading Configuration..");
+ _config = _configModel?.LoadConfiguration() ?? new DbConfig();
+ state.DbConfig = _config;
+
+ InitializeServices(state);
+
+ _logger.Debug("Loading PDFBurner..");
+ var pdfBurnerParams = (PDFBurnerParams)context.MergedJobDataMap[Value.PDF_BURNER_PARAMS];
+ _pdfBurner = new PDFBurner(_logConfig, gdPictureKey, pdfBurnerParams, _databaseConnectionString);
+
+ _logger.Debug("Loading PDFMerger..");
+ _pdfMerger = new PDFMerger(_logConfig, gdPictureKey);
+
+ _logger.Debug("Loading ReportCreator..");
+ _reportCreator = new ReportCreator(_logConfig, state);
+
+ _config.DocumentPath = _config.DocumentPath;
+
+ _logger.Debug("DocumentPath: [{0}]", _config.DocumentPath);
+ _logger.Debug("ExportPath: [{0}]", _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()
+ .Select(r => r.Field("GUID"))
+ .ToList();
+
+ if (envelopeIds.Count > 0)
+ {
+ _logger.Info("Found [{0}] completed envelopes.", envelopeIds.Count);
+ }
+
+ var total = envelopeIds.Count;
+ var current = 1;
+
+ foreach (var id in envelopeIds)
+ {
+ _logger.Info("Finalizing Envelope [{0}] ({1}/{2})", id, current, total);
+ try
+ {
+ var envelope = _envelopeModel?.GetById(id);
+ if (envelope is null)
+ {
+ _logger.Warn("Envelope could not be loaded for Id [{0}]!", id);
+ throw new ArgumentNullException(nameof(EnvelopeData));
+ }
+
+ _logger.Debug("Loading Envelope Data..");
+ var envelopeData = GetEnvelopeData(id);
+
+ if (envelopeData is null)
+ {
+ _logger.Warn("EnvelopeData could not be loaded for Id [{0}]!", id);
+ throw new ArgumentNullException(nameof(EnvelopeData));
+ }
+
+ _logger.Debug("Burning Annotations to pdf ...");
+ var burnedDocument = BurnAnnotationsToPdf(envelopeData);
+ if (burnedDocument is null)
+ {
+ _logger.Warn("Document could not be finalized!");
+ throw new ApplicationException("Document could not be finalized");
+ }
+
+ if (_actionService?.CreateReport(envelope) == false)
+ {
+ _logger.Warn("Document Report could not be created!");
+ throw new ApplicationException("Document Report could not be created");
+ }
+
+ _logger.Debug("Creating report..");
+ var report = _reportCreator!.CreateReport(envelope);
+ _logger.Debug("Report created!");
+
+ _logger.Debug("Merging documents ...");
+ var mergedDocument = _pdfMerger!.MergeDocuments(burnedDocument, report);
+ _logger.Debug("Documents merged!");
+
+ var outputDirectoryPath = Path.Combine(_config.ExportPath, _parentFolderUid);
+ _logger.Debug("oOutputDirectoryPath is {0}", outputDirectoryPath);
+ if (!Directory.Exists(outputDirectoryPath))
+ {
+ _logger.Debug("Directory not existing. Creating ... ");
+ Directory.CreateDirectory(outputDirectoryPath);
+ }
+
+ var outputFilePath = Path.Combine(outputDirectoryPath, $"{envelope.Uuid}.pdf");
+ _logger.Debug("Writing finalized Pdf to disk..");
+ _logger.Info("Output path is [{0}]", outputFilePath);
+
+ try
+ {
+ File.WriteAllBytes(outputFilePath, mergedDocument);
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("Could not export final document to disk!");
+ throw new ExportDocumentException("Could not export final document to disk!", ex);
+ }
+
+ _logger.Debug("Writing EB-bytes to database...");
+ UpdateFileDb(outputFilePath, envelope.Id);
+
+ if (!SendFinalEmails(envelope))
+ {
+ throw new ApplicationException("Final emails could not be sent!");
+ }
+
+ _logger.Info("Report-mails successfully sent!");
+
+ _logger.Debug("Setting envelope status..");
+ if (_actionService?.FinalizeEnvelope(envelope) == false)
+ {
+ _logger.Warn("Envelope could not be finalized!");
+ throw new ApplicationException("Envelope could not be finalized");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex);
+ _logger.Warn(ex, "Unhandled exception while working envelope [{0}]", id);
+ }
+
+ current += 1;
+ _logger.Info("Envelope [{0}] finalized!", id);
+ }
+
+ _logger.Debug("Completed job {0} successfully!", jobId);
+ }
+ catch (MergeDocumentException ex)
+ {
+ _logger.Warn("Certificate Document job failed at step: Merging documents!");
+ _logger.Error(ex);
+ }
+ catch (ExportDocumentException ex)
+ {
+ _logger.Warn("Certificate Document job failed at step: Exporting document!");
+ _logger.Error(ex);
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("Certificate Document job failed!");
+ _logger.Error(ex);
+ }
+ finally
+ {
+ _logger.Debug("Job execution for [{0}] 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?.Error(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?.Debug("Sending email to creator ...");
+ SendFinalEmailToCreator(envelope, mailToCreator);
+ }
+ else
+ {
+ _logger?.Warn("No SendFinalEmailToCreator - oMailToCreator [{0}] <> [{1}] ", mailToCreator, FinalEmailType.No);
+ }
+
+ if (mailToReceivers != FinalEmailType.No)
+ {
+ _logger?.Debug("Sending emails to receivers..");
+ SendFinalEmailToReceivers(envelope, mailToReceivers);
+ }
+ else
+ {
+ _logger?.Warn("No SendFinalEmailToReceivers - oMailToCreator [{0}] <> [{1}] ", mailToReceivers, FinalEmailType.No);
+ }
+
+ return true;
+ }
+
+ private bool SendFinalEmailToCreator(Envelope envelope, FinalEmailType mailToCreator)
+ {
+ var includeAttachment = SendFinalEmailWithAttachment(mailToCreator);
+ _logger?.Debug("Attachment included: [{0}]", includeAttachment);
+
+ if (_actionService?.CompleteEnvelope(envelope) == false)
+ {
+ _logger?.Error(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?.Debug("Attachment included: [{0}]", includeAttachment);
+
+ foreach (var receiver in envelope.EnvelopeReceivers ?? Enumerable.Empty())
+ {
+ if (_actionService?.CompleteEnvelope(envelope, receiver.Receiver) == false)
+ {
+ _logger?.Error(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?.Info("Burning [{0}] signatures", envelopeData.AnnotationData.Count);
+ var annotations = envelopeData.AnnotationData;
+ var inputPath = string.Empty;
+ if (envelopeData.DocAsByte is null)
+ {
+ inputPath = envelopeData.DocumentPath;
+ _logger?.Info("Input path: [{0}]", inputPath);
+ }
+ else
+ {
+ _logger?.Debug("we got bytes..");
+ inputPath = _config!.DocumentPathOrigin;
+ _logger?.Debug("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?.Info("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().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_DATA"),
+ EnvelopeUuid = row.ItemEx("ENVELOPE_UUID", string.Empty)
+ };
+
+ _logger?.Debug("Document path: [{0}]", data.DocumentPath);
+
+ return data;
+ }
+
+ private List GetAnnotationData(int envelopeId)
+ {
+ var sql = $"SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = {envelopeId}";
+ var table = _database!.GetDatatable(sql);
+
+ return table.Rows.Cast()
+ .Select(r => r.ItemEx("VALUE", string.Empty))
+ .ToList();
+ }
+
+ private void InitializeServices(State state)
+ {
+ _actionService = new ActionService(state, _database!);
+ }
+
+ private void InitializeModels(State state)
+ {
+ _configModel = new ConfigModel(state);
+ _envelopeModel = new EnvelopeModel(state);
+ _reportModel = new ReportModel(state);
+ }
+
+ private State GetState()
+ {
+ return new State
+ {
+ LogConfig = _logConfig!,
+ Database = _database!,
+ UserId = 0,
+ Config = null,
+ DbConfig = null
+ };
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Base.cs b/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Base.cs
new file mode 100644
index 00000000..abccf9c5
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Base.cs
@@ -0,0 +1,63 @@
+using System.Data;
+using DigitalData.Modules.Logging;
+
+namespace DigitalData.Modules.Base;
+
+public abstract class BaseClass
+{
+ protected BaseClass(LogConfig logConfig)
+ {
+ LogConfig = logConfig;
+ Logger = logConfig.GetLogger();
+ }
+
+ protected LogConfig LogConfig { get; }
+ protected Logger Logger { get; }
+}
+
+public static class ObjectEx
+{
+ public static T ToEnum(object value) where T : struct, Enum
+ {
+ if (value is T enumValue)
+ {
+ return enumValue;
+ }
+
+ if (value is string stringValue && Enum.TryParse(stringValue, true, out var parsed))
+ {
+ return parsed;
+ }
+
+ if (int.TryParse(Convert.ToString(value), out var intValue))
+ {
+ return (T)Enum.ToObject(typeof(T), intValue);
+ }
+
+ return default;
+ }
+}
+
+public static class DataRowExtensions
+{
+ public static T ItemEx(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 ItemEx(row, columnName, defaultValue);
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Database.cs b/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Database.cs
new file mode 100644
index 00000000..c751d886
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Database.cs
@@ -0,0 +1,67 @@
+using System.Data;
+using Microsoft.Data.SqlClient;
+using DigitalData.Modules.Logging;
+
+namespace DigitalData.Modules.Database;
+
+public class MSSQLServer
+{
+ private readonly LogConfig _logConfig;
+ private readonly string _connectionString;
+
+ public MSSQLServer(LogConfig logConfig, string connectionString)
+ {
+ _logConfig = logConfig;
+ _connectionString = connectionString;
+ }
+
+ 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;
+ }
+}
diff --git a/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Logging.cs b/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Logging.cs
new file mode 100644
index 00000000..6a3f7a08
--- /dev/null
+++ b/EnvelopeGenerator.ServiceHost/Jobs/Infrastructure/Logging.cs
@@ -0,0 +1,26 @@
+using System.Globalization;
+
+namespace DigitalData.Modules.Logging;
+
+public class LogConfig
+{
+ public bool Debug { get; set; }
+
+ public Logger GetLogger() => new();
+}
+
+public class Logger
+{
+ public void Debug(string message, params object?[] args) => Write("DEBUG", message, args);
+ public void Info(string message, params object?[] args) => Write("INFO", message, args);
+ public void Warn(string message, params object?[] args) => Write("WARN", message, args);
+ public void Warn(Exception exception, string message, params object?[] args) => Write("WARN", message + " " + exception.Message, args);
+ public void Error(Exception exception) => Write("ERROR", exception.Message, Array.Empty