Introduced new job classes for envelope processing and document finalization, including APIEnvelopeJob and FinalizeDocumentJob, both implementing Quartz IJob. Added supporting utilities for PDF annotation burning (PDFBurner), PDF merging (PDFMerger), and report generation (ReportCreator), along with related data models and exception types. Updated project references and dependencies to support Quartz scheduling, SQL Server access, and PDF manipulation with iText. This establishes a modular, extensible job-processing framework for envelope management and reporting.
230 lines
10 KiB
C#
230 lines
10 KiB
C#
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<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()
|
|
: this(NullLogger<FinalizeDocumentJob>.Instance)
|
|
{
|
|
}
|
|
|
|
public FinalizeDocumentJob(ILogger<FinalizeDocumentJob> 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<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);
|
|
}
|
|
}
|