using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument.FinalizeDocumentExceptions; using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Entities; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Quartz; namespace EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument; public class FinalizeDocumentJob : IJob { private readonly ILogger _logger; private readonly PDFBurner _pdfBurner; private readonly PDFMerger _pdfMerger; private readonly ReportCreator _reportCreator; private record ConfigSettings(string DocumentPath, string DocumentPathOrigin, string ExportPath); public FinalizeDocumentJob() : this(NullLogger.Instance) { } public FinalizeDocumentJob(ILogger logger) { _logger = logger; _pdfBurner = new PDFBurner(); _pdfMerger = new PDFMerger(); _reportCreator = new 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 envelope = new Envelope { Id = envelopeId, Uuid = envelopeData.EnvelopeUuid ?? string.Empty, Title = envelopeData.Title ?? string.Empty, FinalEmailToCreator = FinalEmailType.No, FinalEmailToReceivers = FinalEmailType.No }; var burned = _pdfBurner.BurnAnnotsToPDF(envelopeData.DocumentBytes, envelopeData.AnnotationData, envelopeId); var report = _reportCreator.CreateReport(connection, envelope); var merged = _pdfMerger.MergeDocuments(burned, report); var outputDirectory = Path.Combine(config.ExportPath, envelopeData.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 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> 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(); 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 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[])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> GetAnnotationDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken) { const string sql = "SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = @EnvelopeId"; var result = new List(); 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); } }