diff --git a/DocumentService.Application/Common/DTOs/PdfValidationResult.cs b/DocumentService.Application/Common/DTOs/PdfValidationResult.cs index a868831..34929af 100644 --- a/DocumentService.Application/Common/DTOs/PdfValidationResult.cs +++ b/DocumentService.Application/Common/DTOs/PdfValidationResult.cs @@ -17,4 +17,7 @@ public record PdfValidationResult( string PdfVersion, bool HasAttachments, int AttachmentCount, - bool IsEncrypted); + bool IsEncrypted) +{ + public bool IsValid => !IsEncrypted && PageCount > 0 && FileSizeBytes > 0; +} diff --git a/DocumentService.Client/Client.cs b/DocumentService.Client/Client.cs index c984d46..2bc6f4b 100644 --- a/DocumentService.Client/Client.cs +++ b/DocumentService.Client/Client.cs @@ -89,5 +89,7 @@ public static class Client public static ISwissQrCodeClient SwissQrCode => GetRequiredServiceOfScope(); public static IZugferdClient Zugferd => GetRequiredServiceOfScope(); + + public static IWorkflowsClient Workflows => GetRequiredServiceOfScope(); #endregion } diff --git a/DocumentService.Client/Clients/WorkflowsClient.cs b/DocumentService.Client/Clients/WorkflowsClient.cs new file mode 100644 index 0000000..8d248ec --- /dev/null +++ b/DocumentService.Client/Clients/WorkflowsClient.cs @@ -0,0 +1,84 @@ +using DocumentService.Application.Common.DTOs; +using DocumentService.Client.Interfaces; + +namespace DocumentService.Client.Clients; + +/// +/// Orchestrated multi-step workflows that compose multiple DocumentService clients +/// into single, higher-level operations. +/// +public class WorkflowsClient(IPdfValidationClient validation, ISwissQrCodeClient swissQrCode) : IWorkflowsClient +{ + /// + public async Task ExtractSwissQrCodeValidatedAsync( + byte[] pdfBytes, + bool raw = false, + bool throwIfInvalid = true, + CancellationToken cancellationToken = default) + { + var validationResult = await validation.ValidatePdfAsync(pdfBytes, cancellationToken); + + if (!validationResult.IsValid) + return throwIfInvalid + ? throw new InvalidOperationException( + $"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " + + $"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).") + : (SwissQrCodeExtractionResult?)null; + + return await swissQrCode.ExtractSwissQrCodeAsync(pdfBytes, raw, cancellationToken); + } + + /// + public async Task ExtractSwissQrCodeValidatedAsync( + Stream pdfStream, + bool raw = false, + bool throwIfInvalid = true, + CancellationToken cancellationToken = default) + { + var validationResult = await validation.ValidatePdfAsync(pdfStream, cancellationToken); + + if (!validationResult.IsValid) + return throwIfInvalid + ? throw new InvalidOperationException( + $"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " + + $"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).") + : (SwissQrCodeExtractionResult?)null; + + // Stream was consumed by validation – reset if possible, otherwise re-open is caller's responsibility. + // Since callers typically pass a FileStream or MemoryStream, Position reset covers the common case. + if (pdfStream.CanSeek) + pdfStream.Position = 0; + + return await swissQrCode.ExtractSwissQrCodeAsync(pdfStream, raw, cancellationToken); + } + + /// + public async Task ExtractSwissQrCodeValidatedAsync( + string filePath, + bool raw = false, + bool throwIfInvalid = true, + CancellationToken cancellationToken = default) + { +#if NETFRAMEWORK + using var stream = File.OpenRead(filePath); + var validationResult = await validation.ValidatePdfAsync(stream, cancellationToken); +#else + var pdfBytes = await File.ReadAllBytesAsync(filePath, cancellationToken); + var validationResult = await validation.ValidatePdfAsync(pdfBytes, cancellationToken); +#endif + + if (!validationResult.IsValid) + return throwIfInvalid + ? throw new InvalidOperationException( + $"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " + + $"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).") + : (SwissQrCodeExtractionResult?)null; + +#if NETFRAMEWORK + using var extractStream = File.OpenRead(filePath); + return await swissQrCode.ExtractSwissQrCodeAsync(extractStream, raw, cancellationToken); +#else + return await swissQrCode.ExtractSwissQrCodeAsync(pdfBytes, raw, cancellationToken); +#endif + } +} diff --git a/DocumentService.Client/Extensions/ServiceCollectionExtensions.cs b/DocumentService.Client/Extensions/ServiceCollectionExtensions.cs index 9e8a1a8..f045ba1 100644 --- a/DocumentService.Client/Extensions/ServiceCollectionExtensions.cs +++ b/DocumentService.Client/Extensions/ServiceCollectionExtensions.cs @@ -58,6 +58,9 @@ public static class ServiceCollectionExtensions services.AddHttpClient(ConfigureClient) .ConfigurePrimaryHttpMessageHandler(configureHandler); + // WorkflowsClient composes existing clients – no HttpClient needed. + services.AddScoped(); + return services; } } diff --git a/DocumentService.Client/Interfaces/IWorkflowsClient.cs b/DocumentService.Client/Interfaces/IWorkflowsClient.cs new file mode 100644 index 0000000..eacf232 --- /dev/null +++ b/DocumentService.Client/Interfaces/IWorkflowsClient.cs @@ -0,0 +1,59 @@ +using DocumentService.Application.Common.DTOs; + +namespace DocumentService.Client.Interfaces; + +/// +/// Orchestrated multi-step workflows that combine multiple DocumentService operations into +/// a single, higher-level call. Prefer these over manually chaining individual clients. +/// +public interface IWorkflowsClient +{ + /// + /// Validates the PDF and extracts its Swiss QR Code in a single call. + /// Validation runs first; extraction is only attempted on valid documents. + /// + /// PDF file as byte array. + /// + /// false (default) – returns a parsed object.
+ /// true – returns raw QR text lines without parsing. + /// + /// + /// true (default) – throws when the PDF fails validation.
+ /// false – returns null instead, allowing the caller to handle the case without a try/catch. + /// + /// Cancellation token. + /// + /// Swiss QR Code extraction result, or null if the document is invalid and + /// is false. + /// + /// + /// Thrown when the PDF fails validation and is true. + /// + Task ExtractSwissQrCodeValidatedAsync( + byte[] pdfBytes, + bool raw = false, + bool throwIfInvalid = true, + CancellationToken cancellationToken = default); + + /// + /// PDF file as a stream. + /// + /// + /// Cancellation token. + Task ExtractSwissQrCodeValidatedAsync( + Stream pdfStream, + bool raw = false, + bool throwIfInvalid = true, + CancellationToken cancellationToken = default); + + /// + /// Absolute or relative path to the PDF file. + /// + /// + /// Cancellation token. + Task ExtractSwissQrCodeValidatedAsync( + string filePath, + bool raw = false, + bool throwIfInvalid = true, + CancellationToken cancellationToken = default); +}