diff --git a/EnvelopeGenerator.Service/EnvelopeGenerator.Service.csproj b/EnvelopeGenerator.Service/EnvelopeGenerator.Service.csproj
index b0b45081..e5f42874 100644
--- a/EnvelopeGenerator.Service/EnvelopeGenerator.Service.csproj
+++ b/EnvelopeGenerator.Service/EnvelopeGenerator.Service.csproj
@@ -5,10 +5,37 @@
enable
enable
dotnet-EnvelopeGenerator.Service-816256a4-c636-4313-92c0-b62b04040c2b
- latest
+ latest
+
+
+
+
+
+
+
+ ..\packages\DigitalData.Modules.Database.2.3.5.4\lib\net462\DigitalData.Modules.Database.dll
+
+
+ ..\packages\DigitalData.Modules.Encryption.1.3.1\lib\net462\DigitalData.Modules.Encryption.dll
+
+
+ ..\packages\DigitalData.Modules.Logging.2.6.5\lib\net462\DigitalData.Modules.Logging.dll
+
+
+
+ ..\packages\Quartz.3.8.0\lib\net462\Quartz.dll
+
+
+
+
+
+
+
+
+
diff --git a/EnvelopeGenerator.Service/LogConfigFactory.cs b/EnvelopeGenerator.Service/LogConfigFactory.cs
new file mode 100644
index 00000000..4bbcdc3f
--- /dev/null
+++ b/EnvelopeGenerator.Service/LogConfigFactory.cs
@@ -0,0 +1,28 @@
+using DigitalData.Modules.Logging;
+
+namespace EnvelopeGenerator.Service;
+
+///
+/// Builds a from .
+/// Required because CommonServices jobs have a hard dependency on
+/// in their Quartz JobDataMap.
+/// Log path mirrors the legacy behavior: <BaseDirectory>\Log.
+///
+internal static class LogConfigFactory
+{
+ public static LogConfig Create(ServiceConfig config)
+ {
+ var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
+
+ var logConfig = new LogConfig(
+ LogConfig.PathType.CustomPath,
+ logPath,
+ null,
+ "Digital Data",
+ "EnvelopeGenerator.Service");
+
+ logConfig.Debug = config.Debug;
+
+ return logConfig;
+ }
+}
diff --git a/EnvelopeGenerator.Service/MIGRATION_PLAN.md b/EnvelopeGenerator.Service/MIGRATION_PLAN.md
new file mode 100644
index 00000000..2cd4c380
--- /dev/null
+++ b/EnvelopeGenerator.Service/MIGRATION_PLAN.md
@@ -0,0 +1,315 @@
+# Migration Plan: EnvelopeGenerator.Service_legacy ? EnvelopeGenerator.Service
+**Revision v3** — Full deep analysis complete
+
+## Overview
+
+Migrate the legacy VB.NET Windows Service (`EnvelopeGenerator.Service_legacy`) to a modern
+C# Worker Service (`EnvelopeGenerator.Service`) targeting **.NET Framework 4.6.2**, using
+`Microsoft.Extensions.Hosting`, `IConfiguration`, `ILogger` and **NLog**.
+
+---
+
+## Deep Analysis Findings (vs. v1/v2)
+
+### Finding 1 — Connection String: Plain Text, No Encryption Required
+`FinalizeDocumentJob` and `APIEnvelopeJob` call `MSSQLServer.DecryptConnectionString()` on the
+`JobDataMap[Value.DATABASE]` value. However, `MSSQLServer.DecryptConnectionString()` is a pass-through
+for plain-text strings — no wrapping/encryption step is needed.
+
+**Resolution**: The connection string is read from `IConfiguration` via the standard
+`ConnectionStrings:Default` key (same key as `appsettings.Database.json` in `EnvelopeGenerator.Web`).
+It is passed **as-is** to both the Worker-level `MSSQLServer` and the `JobDataMap`. No
+`EncryptConnectionString()` call is made anywhere in the new service.
+
+### Finding 2 — Dual TempFiles Architecture
+There are **two independent** `TempFiles` classes:
+- `EnvelopeGenerator.CommonServices.TempFiles` — used **inside each job** via `New TempFiles(LogConfig)`.
+ This is called on every job execution and manages the job's own temp lifecycle. It expects `LogConfig`.
+ **We do not touch this.**
+- `EnvelopeGenerator.Service_legacy.TempFiles` — used at the **service/host level** (startup cleanup,
+ shutdown cleanup). This is the one we rewrite in C# in the Service project using `ILogger`.
+
+### Finding 3 — LogConfig is a Hard Dependency of All Jobs
+`FinalizeDocumentJob.Execute()` and `APIEnvelopeJob.Execute()` both do:
+```vb
+LogConfig = pContext.MergedJobDataMap.Item(Value.LOGCONFIG)
+Logger = LogConfig.GetLogger()
+myTempFiles = New TempFiles(LogConfig) ' ? CommonServices.TempFiles
+```
+`LogConfig` is **not optional**. The schedulers must construct a valid `LogConfig` and place it in the
+`JobDataMap`. It must carry the correct log path, debug flag, and application identity.
+
+**Resolution**: A `LogConfigFactory` static helper class will be created in the Service project to
+build a `LogConfig` from `ServiceConfig` values:
+- `LogPath` = `Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log")` (mirrors legacy `My.Application.Info.DirectoryPath + "\Log"`)
+- `Debug` = `ServiceConfig.Debug`
+- Application name = `"EnvelopeGenerator.Service"`
+
+### Finding 4 — ProjectInstaller is Obsolete
+`ProjectInstaller.vb` was the old `installutil.exe` mechanism for Windows Service registration.
+With `AddWindowsService()` + `sc create` / PowerShell `New-Service`, this is **completely replaced**.
+No equivalent is needed in the new project.
+
+### Finding 5 — GDPicture License SQL
+The legacy service queries: `SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME = 'GDPICTURE' and ACTIVE = 1`
+This must be replicated exactly in the Worker startup sequence. If the result is null/empty, startup fails
+with a descriptive exception (matching legacy behavior).
+
+### Finding 6 — Quartz Version Mismatch
+`CommonServices` uses `Quartz 3.8.0`. `Service_legacy` uses `Quartz 3.15.0`.
+The new `EnvelopeGenerator.Service` must use **Quartz 3.8.0** to match `CommonServices` (same AppDomain,
+same version must be loaded). The legacy version bump in `Service_legacy` was inconsistent.
+
+---
+
+## 1. Project File (`EnvelopeGenerator.Service.csproj`)
+
+- Add **NuGet packages**:
+ - `NLog` (5.x, matching CommonServices' `NLog.5.0.5`)
+ - `NLog.Extensions.Logging` (latest net462-compatible)
+ - `Quartz` (**3.8.0** — must match CommonServices)
+ - `Microsoft.Extensions.Hosting.WindowsServices` (for `AddWindowsService()`)
+- Add **project reference** to `EnvelopeGenerator.CommonServices` (vbproj)
+- Add **project reference** to `EnvelopeGenerator.Domain` (csproj)
+
+---
+
+## 2. Configuration (`ServiceConfig.cs`)
+
+```csharp
+public class ServiceConfig
+{
+ public string ConnectionString { get; set; } = string.Empty;
+ public bool Debug { get; set; } = false;
+ public int IntervalInMin { get; set; } = 1;
+ public PDFBurnerParams PDFBurnerParams { get; set; } = new PDFBurnerParams();
+}
+```
+
+Connection string is read via the standard `ConnectionStrings:Default` key — consistent with
+`appsettings.Database.json` used by `EnvelopeGenerator.Web`:
+
+`appsettings.json`:
+```json
+{
+ "ConnectionStrings": {
+ "Default": ""
+ },
+ "ServiceConfig": {
+ "Debug": false,
+ "IntervalInMin": 1,
+ "PDFBurnerParams": {
+ "IgnoredLabels": [ "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" ],
+ "TopMargin": 0.1,
+ "YOffset": -0.3,
+ "FontName": "Arial",
+ "FontSize": 8,
+ "FontStyle": 2,
+ "TruncationSuffix": "..",
+ "TextMaxWidths": {
+ "position": 1.41,
+ "city": 1.41,
+ "date": 2.0
+ }
+ }
+ }
+}
+```
+
+> `FontStyle` integer: Regular=0, Bold=1, **Italic=2**, Underline=4, Strikeout=8.
+> All values mirror `PDFBurnerParams.vb` defaults exactly.
+
+---
+
+## 3. NLog Setup
+
+**`nlog.config`**: File target ? `${basedir}/Log/service-${shortdate}.log`, async wrapper,
+layout with timestamp + level + logger + message + exception.
+
+**`Program.cs`**: Wire via `NLog.Extensions.Logging`:
+```csharp
+builder.Logging.ClearProviders();
+builder.Logging.AddNLog("nlog.config");
+```
+
+---
+
+## 4. LogConfigFactory (`LogConfigFactory.cs`)
+
+New static helper — bridges `ServiceConfig` ? `LogConfig` (required by CommonServices jobs):
+
+```csharp
+internal static class LogConfigFactory
+{
+ public static LogConfig Create(ServiceConfig config)
+ {
+ var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
+ var logConfig = new LogConfig(
+ LogConfig.PathType.CustomPath, logPath, null,
+ "Digital Data", "EnvelopeGenerator.Service");
+ logConfig.Debug = config.Debug;
+ return logConfig;
+ }
+}
+```
+
+This `LogConfig` instance is what gets placed into every `JobDataMap[Value.LOGCONFIG]`.
+
+---
+
+## 5. Quartz LogProvider (`QuartzLogProvider.cs`)
+
+C# rewrite of `LogProvider.vb` — bridges Quartz `ILogProvider` ? `ILogger`:
+
+```csharp
+internal class QuartzLogProvider : ILogProvider
+{
+ private readonly ILogger _logger;
+ public QuartzLogProvider(ILogger logger) => _logger = logger;
+ public Logger GetLogger(string name) => (level, func, ex, parameters) => { ... };
+ // OpenNestedContext / OpenMappedContext ? NotImplementedException (same as legacy)
+}
+```
+
+---
+
+## 6. TempFiles (`TempFiles.cs`) — Service-level only
+
+Service-level rewrite of `Service_legacy/TempFiles.vb` using `ILogger`:
+- `Create()` ? create `%TEMP%\EnvelopeGenerator`, or clean existing files
+- `CleanUp()` ? delete the directory on service stop
+
+> **Does NOT replace** `CommonServices.TempFiles` — that one continues to be used
+> inside each job via `LogConfig`.
+
+---
+
+## 7. Schedulers
+
+### `Schedulers/SchedulerFinishEnvelope.cs`
+
+```csharp
+public class SchedulerFinishEnvelope
+{
+ // Constructor: ILogger, IOptions
+ // Start(licenseKey, logConfig):
+ // - Build JobDataMap:
+ // [GDPICTURE] = licenseKey
+ // [LOGCONFIG] = logConfig ? LogConfigFactory output
+ // [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString)
+ // [PDF_BURNER_PARAMS] = config.PDFBurnerParams
+ // - Schedule FinalizeDocumentJob with Quartz (interval from config)
+ // - Register QuartzLogProvider
+ // Stop(): Scheduler.Shutdown()
+}
+```
+
+### `Schedulers/SchedulerEnvelopeTaskApi.cs`
+
+```csharp
+public class SchedulerEnvelopeTaskApi
+{
+ // Constructor: ILogger, IOptions
+ // Start(logConfig):
+ // - Build JobDataMap:
+ // [LOGCONFIG] = logConfig
+ // [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString)
+ // - Schedule APIEnvelopeJob with Quartz (interval from config)
+ // - Register QuartzLogProvider
+ // Stop(): Scheduler.Shutdown()
+}
+```
+
+---
+
+## 8. Worker (`Worker.cs`)
+
+Full lifecycle replacing the placeholder:
+
+```
+ExecuteAsync:
+ 1. Read ServiceConfig from IOptions
+ 2. Validate ConnectionString ? throw if empty
+ 3. Build LogConfig via LogConfigFactory
+ 4. Connect to DB (plain connection string — Worker-level only)
+ 5. Query: SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME='GDPICTURE' AND ACTIVE=1
+ ? throw if null/empty (matches legacy behavior)
+ 6. TempFiles.Create()
+ 7. await SchedulerFinishEnvelope.Start(licenseKey, logConfig)
+ 8. await Task.Delay(2500, stoppingToken) ? preserved from legacy
+ 9. await SchedulerEnvelopeTaskApi.Start(logConfig)
+ 10. await Task.Delay(Timeout.Infinite, stoppingToken) ? wait for cancellation
+
+StopAsync override:
+ 11. await SchedulerFinishEnvelope.Stop()
+ 12. await SchedulerEnvelopeTaskApi.Stop()
+ 13. TempFiles.CleanUp()
+```
+
+---
+
+## 9. Program.cs
+
+```csharp
+var builder = Host.CreateApplicationBuilder(args);
+
+builder.Logging.ClearProviders();
+builder.Logging.AddNLog("nlog.config");
+
+builder.Services.Configure(
+ builder.Configuration.GetSection("ServiceConfig"));
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddHostedService();
+builder.Services.AddWindowsService(o => o.ServiceName = "EnvelopeGenerator.Service");
+
+var host = builder.Build();
+host.Run();
+```
+
+---
+
+## 10. Files to Create / Modify
+
+| File | Action | Notes |
+|------|--------|-------|
+| `EnvelopeGenerator.Service.csproj` | **Modify** | Add packages, project refs |
+| `Program.cs` | **Modify** | Full DI/NLog/WindowsService setup |
+| `Worker.cs` | **Modify** | Full service lifecycle |
+| `ServiceConfig.cs` | **Create** | |
+| `LogConfigFactory.cs` | **Create** | Critical bridge: ServiceConfig ? LogConfig |
+| `TempFiles.cs` | **Create** | Service-level only; CommonServices.TempFiles untouched |
+| `QuartzLogProvider.cs` | **Create** | |
+| `Schedulers/SchedulerFinishEnvelope.cs` | **Create** | |
+| `Schedulers/SchedulerEnvelopeTaskApi.cs` | **Create** | |
+| `appsettings.json` | **Create** | All PDFBurnerParams explicit |
+| `nlog.config` | **Create** | |
+
+**Not created** (obsolete in new architecture):
+- ~~`ProjectInstaller`~~ ? replaced by `sc create` / `New-Service`
+- ~~`Service.Designer.vb`~~ ? BackgroundService handles this
+- ~~`Service.resx`~~ ? not applicable
+
+---
+
+## 11. Key Design Decisions
+
+| Decision | Rationale |
+|----------|-----------|
+| **Quartz 3.8.0** (not 3.15.0) | Must match CommonServices; same AppDomain, one version |
+| **Connection string plain-text, no encryption** | `MSSQLServer.DecryptConnectionString()` is a pass-through for plain strings; `ConnectionStrings:Default` key used (same convention as Web project) |
+| **Connection string in `ConnectionStrings:Default`** | Consistent with rest of the solution (`appsettings.Database.json`); `ServiceConfig` holds only service-specific settings |
+| **LogConfigFactory** | Jobs have hard dependency on `LogConfig` in JobDataMap; this is the clean bridge without leaking LogConfig into the DI container |
+| **2500ms delay preserved** | Legacy behavior; ensures Scheduler1 is fully initialized before Scheduler2 starts |
+| **`AddWindowsService()`** | Native SCM integration; replaces ProjectInstaller/installutil entirely |
+| **NLog log path = BaseDirectory/Log** | Mirrors `My.Application.Info.DirectoryPath + "\Log"` from legacy |
+| **CommonServices.TempFiles untouched** | Jobs create their own TempFiles internally via LogConfig; service-level TempFiles is separate concern |
+
+---
+
+## Approval Required
+
+> Please review this plan and confirm before implementation begins.
+
diff --git a/EnvelopeGenerator.Service/Program.cs b/EnvelopeGenerator.Service/Program.cs
index a08adc48..b9f1d366 100644
--- a/EnvelopeGenerator.Service/Program.cs
+++ b/EnvelopeGenerator.Service/Program.cs
@@ -1,7 +1,41 @@
using EnvelopeGenerator.Service;
+using EnvelopeGenerator.Service.Schedulers;
+using Microsoft.Extensions.Options;
+using NLog.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
+
+// NLog
+builder.Logging.ClearProviders();
+builder.Logging.AddNLog("nlog.config");
+
+// Configuration
+builder.Services.Configure(
+ builder.Configuration.GetSection("ServiceConfig"));
+
+// Resolve connection string once for singleton registrations
+var connectionString = builder.Configuration.GetConnectionString("Default")
+ ?? throw new InvalidOperationException("ConnectionStrings:Default is not configured.");
+
+// Services
+builder.Services.AddSingleton();
+
+builder.Services.AddSingleton(sp => new SchedulerFinishEnvelope(
+ sp.GetRequiredService>(),
+ sp.GetRequiredService>(),
+ connectionString));
+
+builder.Services.AddSingleton(sp => new SchedulerEnvelopeTaskApi(
+ sp.GetRequiredService>(),
+ sp.GetRequiredService>(),
+ connectionString));
+
builder.Services.AddHostedService();
+// Windows Service (SCM) support — replaces legacy ProjectInstaller/installutil
+builder.Services.AddWindowsService(options =>
+ options.ServiceName = "EnvelopeGenerator.Service");
+
var host = builder.Build();
host.Run();
+
diff --git a/EnvelopeGenerator.Service/QuartzLogProvider.cs b/EnvelopeGenerator.Service/QuartzLogProvider.cs
new file mode 100644
index 00000000..d59a9333
--- /dev/null
+++ b/EnvelopeGenerator.Service/QuartzLogProvider.cs
@@ -0,0 +1,60 @@
+using Quartz.Logging;
+using QuartzLogLevel = Quartz.Logging.LogLevel;
+
+namespace EnvelopeGenerator.Service;
+
+///
+/// Bridges Quartz's to .
+/// Mirrors the legacy LogProvider.vb behavior.
+/// OpenNestedContext and OpenMappedContext are not implemented (same as legacy).
+///
+internal sealed class QuartzLogProvider : ILogProvider
+{
+ private readonly ILogger _logger;
+
+ public QuartzLogProvider(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public Logger GetLogger(string name)
+ {
+ return (level, func, exception, parameters) =>
+ {
+ if (exception != null)
+ {
+ _logger.LogError(exception, exception.Message);
+ }
+ else if (func != null)
+ {
+ switch (level)
+ {
+ case QuartzLogLevel.Trace:
+ case QuartzLogLevel.Debug:
+ if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
+ _logger.LogDebug(func(), parameters);
+ break;
+ case QuartzLogLevel.Info:
+ if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Information))
+ _logger.LogInformation(func(), parameters);
+ break;
+ case QuartzLogLevel.Warn:
+ if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning))
+ _logger.LogWarning(func(), parameters);
+ break;
+ case QuartzLogLevel.Error:
+ case QuartzLogLevel.Fatal:
+ if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Error))
+ _logger.LogError(func(), parameters);
+ break;
+ }
+ }
+
+ return true;
+ };
+ }
+
+ public IDisposable OpenNestedContext(string message) => throw new NotImplementedException();
+
+ public IDisposable OpenMappedContext(string key, object value, bool destructure = false) => throw new NotImplementedException();
+}
diff --git a/EnvelopeGenerator.Service/Schedulers/SchedulerEnvelopeTaskApi.cs b/EnvelopeGenerator.Service/Schedulers/SchedulerEnvelopeTaskApi.cs
new file mode 100644
index 00000000..de340acf
--- /dev/null
+++ b/EnvelopeGenerator.Service/Schedulers/SchedulerEnvelopeTaskApi.cs
@@ -0,0 +1,94 @@
+using System.Collections.Specialized;
+using DigitalData.Modules.Logging;
+using EnvelopeGenerator.CommonServices.Jobs;
+using EnvelopeGenerator.Domain.Constants;
+using Microsoft.Extensions.Options;
+using Quartz;
+using Quartz.Logging;
+
+namespace EnvelopeGenerator.Service.Schedulers;
+
+///
+/// Schedules via Quartz.
+/// C# rewrite of the legacy Scheduler_Envelopetask_API.vb.
+///
+public sealed class SchedulerEnvelopeTaskApi
+{
+ private readonly ILogger _logger;
+ private readonly ServiceConfig _config;
+ private readonly string _connectionString;
+
+ private IScheduler? _scheduler;
+
+ private const string JobName = "SendInvitationMailsAPI";
+
+ public SchedulerEnvelopeTaskApi(
+ ILogger logger,
+ IOptions config,
+ string connectionString)
+ {
+ _logger = logger;
+ _config = config.Value;
+ _connectionString = connectionString;
+ }
+
+ public async Task Start(LogConfig logConfig)
+ {
+ try
+ {
+ _logger.LogDebug("Starting SchedulerEnvelopeTaskApi...");
+
+ LogProvider.SetCurrentLogProvider(new QuartzLogProvider(_logger));
+
+ _scheduler = await SchedulerBuilder
+ .Create(new NameValueCollection())
+ .UseDefaultThreadPool(x => x.MaxConcurrency = 5)
+ .BuildScheduler();
+
+ var jobKey = new JobKey(JobName);
+
+ var jobData = new JobDataMap
+ {
+ { Value.LOGCONFIG, logConfig },
+ { Value.DATABASE, _connectionString }
+ };
+
+ _logger.LogDebug("Initialized Job [{JobName}]", JobName);
+
+ var job = JobBuilder.Create()
+ .UsingJobData(jobData)
+ .WithIdentity(jobKey)
+ .Build();
+
+ var trigger = TriggerBuilder.Create()
+ .ForJob(jobKey)
+ .WithIdentity($"{JobName}-trigger")
+ .WithSimpleSchedule(s => s
+ .RepeatForever()
+ .WithIntervalInMinutes(_config.IntervalInMin))
+ .StartNow()
+ .Build();
+
+ _logger.LogDebug("{JobName}-trigger initialized", JobName);
+
+ await _scheduler.ScheduleJob(job, trigger);
+ _logger.LogDebug("{JobName} scheduled", JobName);
+
+ await _scheduler.Start();
+ _logger.LogInformation("{JobName} started", JobName);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "SchedulerEnvelopeTaskApi failed to start");
+ throw;
+ }
+ }
+
+ public async Task Stop()
+ {
+ if (_scheduler is null) return;
+ _logger.LogInformation("Stopping SchedulerEnvelopeTaskApi...");
+ await _scheduler.Shutdown();
+ _logger.LogInformation("SchedulerEnvelopeTaskApi stopped!");
+ }
+}
diff --git a/EnvelopeGenerator.Service/Schedulers/SchedulerFinishEnvelope.cs b/EnvelopeGenerator.Service/Schedulers/SchedulerFinishEnvelope.cs
new file mode 100644
index 00000000..f6811cdb
--- /dev/null
+++ b/EnvelopeGenerator.Service/Schedulers/SchedulerFinishEnvelope.cs
@@ -0,0 +1,97 @@
+using System.Collections.Specialized;
+using DigitalData.Modules.Logging;
+using EnvelopeGenerator.CommonServices.Jobs;
+using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument;
+using EnvelopeGenerator.Domain.Constants;
+using Microsoft.Extensions.Options;
+using Quartz;
+using Quartz.Logging;
+
+namespace EnvelopeGenerator.Service.Schedulers;
+
+///
+/// Schedules via Quartz.
+/// C# rewrite of the legacy Scheduler_FinishEnvelope.vb.
+///
+public sealed class SchedulerFinishEnvelope
+{
+ private readonly ILogger _logger;
+ private readonly ServiceConfig _config;
+ private readonly string _connectionString;
+
+ private IScheduler? _scheduler;
+
+ private const string JobName = "CertificateDocumentJob";
+
+ public SchedulerFinishEnvelope(
+ ILogger logger,
+ IOptions config,
+ string connectionString)
+ {
+ _logger = logger;
+ _config = config.Value;
+ _connectionString = connectionString;
+ }
+
+ public async Task Start(string licenseKey, LogConfig logConfig)
+ {
+ try
+ {
+ _logger.LogDebug("Starting SchedulerFinishEnvelope...");
+
+ LogProvider.SetCurrentLogProvider(new QuartzLogProvider(_logger));
+
+ _scheduler = await SchedulerBuilder
+ .Create(new NameValueCollection())
+ .UseDefaultThreadPool(x => x.MaxConcurrency = 5)
+ .BuildScheduler();
+
+ var jobKey = new JobKey(JobName);
+
+ var jobData = new JobDataMap
+ {
+ { Value.GDPICTURE, licenseKey },
+ { Value.LOGCONFIG, logConfig },
+ { Value.DATABASE, _connectionString },
+ { Value.PDF_BURNER_PARAMS, _config.PDFBurnerParams }
+ };
+
+ _logger.LogDebug("Initialized Job [{JobName}]", JobName);
+
+ var job = JobBuilder.Create()
+ .UsingJobData(jobData)
+ .WithIdentity(jobKey)
+ .Build();
+
+ var trigger = TriggerBuilder.Create()
+ .ForJob(jobKey)
+ .WithIdentity($"{JobName}-trigger")
+ .WithSimpleSchedule(s => s
+ .RepeatForever()
+ .WithIntervalInMinutes(_config.IntervalInMin))
+ .StartNow()
+ .Build();
+
+ _logger.LogDebug("Initialized Trigger");
+
+ await _scheduler.ScheduleJob(job, trigger);
+ _logger.LogDebug("Job scheduled.");
+
+ await _scheduler.Start();
+ _logger.LogDebug("Scheduler started!");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "SchedulerFinishEnvelope failed to start");
+ throw;
+ }
+ }
+
+ public async Task Stop()
+ {
+ if (_scheduler is null) return;
+ _logger.LogInformation("Stopping SchedulerFinishEnvelope...");
+ await _scheduler.Shutdown();
+ _logger.LogInformation("SchedulerFinishEnvelope stopped!");
+ }
+}
diff --git a/EnvelopeGenerator.Service/ServiceConfig.cs b/EnvelopeGenerator.Service/ServiceConfig.cs
new file mode 100644
index 00000000..2d27b95d
--- /dev/null
+++ b/EnvelopeGenerator.Service/ServiceConfig.cs
@@ -0,0 +1,14 @@
+using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument;
+
+namespace EnvelopeGenerator.Service;
+
+///
+/// Service-level configuration bound from the "ServiceConfig" section of appsettings.json.
+/// Connection string is read separately via the standard "ConnectionStrings:Default" key.
+///
+public class ServiceConfig
+{
+ public bool Debug { get; set; } = false;
+ public int IntervalInMin { get; set; } = 1;
+ public PDFBurnerParams PDFBurnerParams { get; set; } = new PDFBurnerParams();
+}
diff --git a/EnvelopeGenerator.Service/TempFiles.cs b/EnvelopeGenerator.Service/TempFiles.cs
new file mode 100644
index 00000000..0db3b275
--- /dev/null
+++ b/EnvelopeGenerator.Service/TempFiles.cs
@@ -0,0 +1,75 @@
+namespace EnvelopeGenerator.Service;
+
+///
+/// Service-level temp file management. Mirrors TempFiles.vb from the legacy project.
+/// Manages %TEMP%\EnvelopeGenerator at host startup and shutdown.
+///
+/// NOTE: This is independent from EnvelopeGenerator.CommonServices.TempFiles, which is
+/// instantiated inside each Quartz job via .
+/// Do not confuse the two.
+///
+public sealed class TempFiles
+{
+ private readonly ILogger _logger;
+
+ public string TempPath { get; }
+
+ public TempFiles(ILogger logger)
+ {
+ _logger = logger;
+ TempPath = Path.Combine(Path.GetTempPath(), "EnvelopeGenerator");
+ }
+
+ public bool Create()
+ {
+ try
+ {
+ if (!Directory.Exists(TempPath))
+ {
+ Directory.CreateDirectory(TempPath);
+ }
+ else
+ {
+ CleanUpFiles();
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create temp directory: {TempPath}", TempPath);
+ return false;
+ }
+ }
+
+ private void CleanUpFiles()
+ {
+ try
+ {
+ foreach (var file in Directory.GetFiles(TempPath))
+ {
+ _logger.LogDebug("Deleting temp file: {File}", file);
+ File.Delete(file);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to clean up temp files in: {TempPath}", TempPath);
+ }
+ }
+
+ public bool CleanUp()
+ {
+ try
+ {
+ _logger.LogDebug("Deleting temp directory: {TempPath}", TempPath);
+ Directory.Delete(TempPath, recursive: true);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to delete temp directory: {TempPath}", TempPath);
+ return false;
+ }
+ }
+}
diff --git a/EnvelopeGenerator.Service/Worker.cs b/EnvelopeGenerator.Service/Worker.cs
index bf0a660b..32137e49 100644
--- a/EnvelopeGenerator.Service/Worker.cs
+++ b/EnvelopeGenerator.Service/Worker.cs
@@ -1,24 +1,111 @@
-namespace EnvelopeGenerator.Service
+using DigitalData.Modules.Database;
+using EnvelopeGenerator.Service.Schedulers;
+using Microsoft.Extensions.Options;
+
+namespace EnvelopeGenerator.Service;
+
+public sealed class Worker : BackgroundService
{
- public class Worker : BackgroundService
+ private readonly ILogger _logger;
+ private readonly ServiceConfig _config;
+ private readonly string _connectionString;
+ private readonly TempFiles _tempFiles;
+ private readonly SchedulerFinishEnvelope _schedulerFinishEnvelope;
+ private readonly SchedulerEnvelopeTaskApi _schedulerEnvelopeTaskApi;
+
+ public Worker(
+ ILogger logger,
+ IOptions config,
+ IConfiguration configuration,
+ TempFiles tempFiles,
+ SchedulerFinishEnvelope schedulerFinishEnvelope,
+ SchedulerEnvelopeTaskApi schedulerEnvelopeTaskApi)
{
- private readonly ILogger _logger;
+ _logger = logger;
+ _config = config.Value;
+ _connectionString = configuration.GetConnectionString("Default")
+ ?? throw new InvalidOperationException("ConnectionStrings:Default is not configured.");
+ _tempFiles = tempFiles;
+ _schedulerFinishEnvelope = schedulerFinishEnvelope;
+ _schedulerEnvelopeTaskApi = schedulerEnvelopeTaskApi;
+ }
- public Worker(ILogger logger)
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ try
{
- _logger = logger;
+ _logger.LogInformation("EnvelopeGenerator.Service starting...");
+
+ // 1. Validate connection string
+ if (string.IsNullOrWhiteSpace(_connectionString))
+ throw new InvalidOperationException("Connection string is empty!");
+
+ // 2. Build LogConfig (required by CommonServices jobs via JobDataMap)
+ var logConfig = LogConfigFactory.Create(_config);
+ _logger.LogInformation("DEBUG = {Debug}", _config.Debug);
+
+ // 3. Connect to DB and load GDPicture license
+ _logger.LogDebug("Initializing Database...");
+ var database = new MSSQLServer(logConfig, _connectionString);
+
+ if (!database.DBInitialized)
+ throw new InvalidOperationException("Database connection could not be established!");
+
+ _logger.LogDebug("Database initialized.");
+
+ var licenseKey = database.GetScalarValue(
+ "SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME = 'GDPICTURE' AND ACTIVE = 1") as string;
+
+ if (string.IsNullOrWhiteSpace(licenseKey))
+ throw new InvalidOperationException("GDPicture license could not be loaded!");
+
+ _logger.LogDebug("GDPicture license loaded.");
+
+ // 4. Create service-level temp directory
+ _tempFiles.Create();
+
+ // 5. Start FinalizeDocument scheduler
+ _logger.LogDebug("Initializing schedulers...");
+ await _schedulerFinishEnvelope.Start(licenseKey!, logConfig);
+
+ // 6. Delay between schedulers (legacy behavior preserved)
+ await Task.Delay(2500, stoppingToken);
+
+ // 7. Start API envelope scheduler
+ await _schedulerEnvelopeTaskApi.Start(logConfig);
+
+ _logger.LogInformation("EnvelopeGenerator.Service started!");
+
+ // 8. Wait for cancellation (service runs until stopped)
+ await Task.Delay(Timeout.Infinite, stoppingToken);
}
-
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ catch (OperationCanceledException)
{
- while (!stoppingToken.IsCancellationRequested)
- {
- if (_logger.IsEnabled(LogLevel.Information))
- {
- _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
- }
- await Task.Delay(1000, stoppingToken);
- }
+ // Normal shutdown � not an error
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "EnvelopeGenerator.Service encountered a fatal error during startup.");
+ throw;
}
}
+
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("EnvelopeGenerator.Service stopping...");
+
+ try
+ {
+ await _schedulerFinishEnvelope.Stop();
+ await _schedulerEnvelopeTaskApi.Stop();
+ _tempFiles.CleanUp();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during service shutdown.");
+ }
+
+ await base.StopAsync(cancellationToken);
+ _logger.LogInformation("EnvelopeGenerator.Service stopped!");
+ }
}
diff --git a/EnvelopeGenerator.Service/appsettings.json b/EnvelopeGenerator.Service/appsettings.json
index b2dcdb67..62ff4548 100644
--- a/EnvelopeGenerator.Service/appsettings.json
+++ b/EnvelopeGenerator.Service/appsettings.json
@@ -1,8 +1,24 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.Hosting.Lifetime": "Information"
+ "ConnectionStrings": {
+ "Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
+ },
+ "ServiceConfig": {
+ "Debug": false,
+ "IntervalInMin": 1,
+ "PDFBurnerParams": {
+ "IgnoredLabels": [ "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" ],
+ "TopMargin": 0.1,
+ "YOffset": -0.3,
+ "FontName": "Arial",
+ "FontSize": 8,
+ "FontStyle": 2,
+ "TruncationSuffix": "..",
+ "TextMaxWidths": {
+ "position": 1.41,
+ "city": 1.41,
+ "date": 2.0
+ }
}
}
}
+
diff --git a/EnvelopeGenerator.Service/nlog.config b/EnvelopeGenerator.Service/nlog.config
new file mode 100644
index 00000000..c054c6d8
--- /dev/null
+++ b/EnvelopeGenerator.Service/nlog.config
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+