From c0d3eb4b7f6a18d651543f6d785e04a33fa3c58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sr=C4=91an=20=C5=BDivojinovi=C4=87?= Date: Thu, 26 Mar 2026 17:02:25 +0100 Subject: [PATCH 1/3] #15 feat: expose isPdfValid/isXmlValid in API response and UI breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add isPdfValid and isXmlValid fields to PdfFileValidationResponse so callers can distinguish PDF/A warnings from XML errors without parsing the raw validation report XML. Under ZuGFeRD rule BR-FX-DE-03, PDF/A compliance failures are non-fatal warnings for German invoices, so isValid may be true while isPdfValid is false. The new fields make this transparent. The WebUI result card now shows a per-section breakdown (XML invoice, PDF/A-3, Signature) with appropriate icons — amber warning for PDF/A issues with an explanatory note when the overall invoice is still valid. --- .../Contracts/PdfFileValidationResponse.cs | 15 ++++++++ src/Server/Endpoints/PdfEndpoints.cs | 26 +++++++++++--- src/WebUI/src/pages/Index.tsx | 34 +++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/Server/Contracts/PdfFileValidationResponse.cs b/src/Server/Contracts/PdfFileValidationResponse.cs index df71660..bf1ec00 100644 --- a/src/Server/Contracts/PdfFileValidationResponse.cs +++ b/src/Server/Contracts/PdfFileValidationResponse.cs @@ -12,4 +12,19 @@ public sealed class PdfFileValidationResponse : FileValidationResponse /// [JsonPropertyName("isSignatureValid")] public bool IsSignatureValid { get; set; } = false; + + /// + /// Indicates whether the PDF part of the hybrid document is valid (e.g. PDF/A-3 conformant). + /// Note: under ZuGFeRD rule BR-FX-DE-03, PDF/A compliance errors are treated as warnings (not fatal) + /// when both buyer and seller are in Germany, so may be + /// true even when this value is false. + /// + [JsonPropertyName("isPdfValid")] + public bool IsPdfValid { get; set; } = false; + + /// + /// Indicates whether the embedded XML invoice data is valid according to the applicable standard. + /// + [JsonPropertyName("isXmlValid")] + public bool IsXmlValid { get; set; } = false; } diff --git a/src/Server/Endpoints/PdfEndpoints.cs b/src/Server/Endpoints/PdfEndpoints.cs index bb529f5..b8ce508 100644 --- a/src/Server/Endpoints/PdfEndpoints.cs +++ b/src/Server/Endpoints/PdfEndpoints.cs @@ -94,17 +94,29 @@ private static async Task ValidateZuGFeRDPdfHandler(HttpRequest request } string status = "unknown"; + string pdfStatus = "unknown"; + string xmlStatus = "unknown"; try { if (!string.IsNullOrWhiteSpace(mustangCliResult.StandardOutput)) { var mustangCliXmlResult = XDocument.Parse(mustangCliResult.StandardOutput); + XElement? root = mustangCliXmlResult.Root; + // Use direct child Elements() instead of Descendants() to target the top-level // which represents the overall validation result aggregating both and sub-results. // See: https://www.mustangproject.org/commandline/#validate - XElement? summary = mustangCliXmlResult.Root?.Elements("summary").FirstOrDefault(); - if (summary?.Attribute("status") is { } attributeValue) - status = attributeValue.Value; + if (root?.Elements("summary").FirstOrDefault()?.Attribute("status") is { } overallStatus) + status = overallStatus.Value; + + // Parse per-section statuses so callers can distinguish PDF/A warnings from XML errors. + // Under ZuGFeRD rule BR-FX-DE-03, PDF/A failures are warnings (not fatal) for German invoices, + // so the overall status may be "valid" even when the PDF section reports "invalid". + if (root?.Element("pdf")?.Elements("summary").FirstOrDefault()?.Attribute("status") is { } ps) + pdfStatus = ps.Value; + + if (root?.Element("xml")?.Elements("summary").FirstOrDefault()?.Attribute("status") is { } xs) + xmlStatus = xs.Value; } } catch (Exception ex) @@ -117,14 +129,18 @@ private static async Task ValidateZuGFeRDPdfHandler(HttpRequest request if (mustangCliResult.ExitCode == (int)ErrorCode.Success) statusCode = StatusCodes.Status200OK; - return Results.Json(new PdfFileValidationResponse + var result = new PdfFileValidationResponse { ErrorCode = (ErrorCode)mustangCliResult.ExitCode, IsValid = status == "valid", + IsPdfValid = pdfStatus == "valid", + IsXmlValid = xmlStatus == "valid", IsSignatureValid = mustangCliResult.StandardOutput.Contains("valid"), ValidationReport = mustangCliResult.StandardOutput, DiagnosticsErrorMessage = mustangCliResult.ExitCode != (int)ErrorCode.Success ? mustangCliResult.StandardError : null - }, statusCode: statusCode); + }; + + return Results.Json(result, statusCode: statusCode); } catch (Exception ex) { diff --git a/src/WebUI/src/pages/Index.tsx b/src/WebUI/src/pages/Index.tsx index 1cdcb8f..cf19e18 100644 --- a/src/WebUI/src/pages/Index.tsx +++ b/src/WebUI/src/pages/Index.tsx @@ -53,6 +53,10 @@ interface PdfFileValidationResult extends BaseFileOperationResult { isValid: boolean; /* Indicates if the PDF signature is valid */ isSignatureValid: boolean; + /* Indicates if the PDF/A part is valid */ + isPdfValid: boolean; + /* Indicates if the embedded XML invoice data is valid */ + isXmlValid: boolean; /* XML validation report as string */ validationReport: string; } @@ -688,6 +692,36 @@ const Index = () => { + {/* PDF sub-validation breakdown */} + {operation === "validate-pdf" && (() => { + const pdfResult = result as PdfFileValidationResult; + return ( +
+
+ {pdfResult.isXmlValid + ? + : } + XML invoice +
+
+ {pdfResult.isPdfValid + ? + : } + PDF/A-3 + {!pdfResult.isPdfValid && pdfResult.isValid && ( + (warning only — valid under BR-FX-DE-03) + )} +
+
+ {pdfResult.isSignatureValid + ? + : } + Signature +
+
+ ); + })()} + {((result as FileValidationResult | PdfFileValidationResult)?.validationReport !== undefined || (result as ExtractXmlFromPdfResult)?.xml !== undefined || (result as ConvertXmlToPdfResult)?.pdf) !== undefined && (
From 2dd6599389e918c21a3368c588b9b83d1e4ebfaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sr=C4=91an=20=C5=BDivojinovi=C4=87?= Date: Thu, 26 Mar 2026 17:05:11 +0100 Subject: [PATCH 2/3] #noissue Bump version to 2026.3.0 --- README.md | 2 +- src/docker-build.ps1 | 2 +- src/docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4694ad5..ce9200e 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ Pre-built Docker images are automatically published to Docker Hub: **Available Tags**: - `latest` - Latest stable release from the main branch -- `2026.2.0` - Specific version tags +- `20XX.Y.Z` - Specific version tags - `main` - Latest build from main branch - `develop` - Latest development build diff --git a/src/docker-build.ps1 b/src/docker-build.ps1 index a7524c6..e671599 100644 --- a/src/docker-build.ps1 +++ b/src/docker-build.ps1 @@ -1,6 +1,6 @@ #!/usr/bin/env pwsh -docker build -t docentric/e-invoice-validator:2026.2.0-dev -t docentric/e-invoice-validator:latest-dev . +docker build -t docentric/e-invoice-validator:2026.3.0-dev -t docentric/e-invoice-validator:latest-dev . if ($LASTEXITCODE -ne 0) { diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 0b88c36..941f436 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -12,7 +12,7 @@ services: dockerfile: Dockerfile # Build arguments for versioning args: - VERSION: ${VERSION:-2026.2.0} + VERSION: ${VERSION:-2026.3.0} BUILD_DATE: ${BUILD_DATE:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} VCS_REF: ${VCS_REF:-$(git rev-parse --short HEAD)} # Multi-platform builds (optional, requires buildx) From 65eae8001782f51e85398af9a15bfd2ea7e7fa7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sr=C4=91an=20=C5=BDivojinovi=C4=87?= Date: Thu, 26 Mar 2026 17:21:28 +0100 Subject: [PATCH 3/3] #15 feat: improve toast coloring and message for PDF validation results Invalid invoices now show a red (destructive) toast, valid invoices with PDF/A-3 warnings show amber with a BR-FX-DE-03 note, and fully valid invoices stay green. Also use MinusCircle for absent signatures instead of the misleading warning icon. --- src/WebUI/src/pages/Index.tsx | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/WebUI/src/pages/Index.tsx b/src/WebUI/src/pages/Index.tsx index cf19e18..4355cff 100644 --- a/src/WebUI/src/pages/Index.tsx +++ b/src/WebUI/src/pages/Index.tsx @@ -3,7 +3,7 @@ import { Card } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useToast } from "@/hooks/use-toast"; -import { AlertCircle, Beaker, CheckCircle, Download, FileText, Upload, XCircle } from "lucide-react"; +import { AlertCircle, Beaker, CheckCircle, Download, FileText, MinusCircle, Upload, XCircle } from "lucide-react"; import { useEffect, useState } from "react"; interface ValidationResult { @@ -418,8 +418,14 @@ const Index = () => { let toastVariant: toastVariantType = data.success ? "success" : "destructive"; if (operation === "validate-pdf" || operation === "validate-xml") { - if ((data as PdfFileValidationResult | FileValidationResult)?.isValid === false) { - toastVariant = "warning"; + const validationResult = data as PdfFileValidationResult | FileValidationResult; + if (validationResult?.isValid === false) { + toastVariant = "destructive"; + } else if (operation === "validate-pdf") { + const pdfResult = data as PdfFileValidationResult; + if (!pdfResult.isPdfValid) { + toastVariant = "warning"; + } } } @@ -438,9 +444,11 @@ const Index = () => { : `PDF generation failed after ${timeStr}. ${data.errorMessage || `Error code: ${data.errorCode}`}`; case "validate-pdf": { const pdfResult = data as PdfFileValidationResult; - return pdfResult.isValid - ? `${file.name} (${fileSize} KB) is valid. Processed in ${timeStr}` - : `${file.name} (${fileSize} KB) validation failed. Processed in ${timeStr}`; + if (!pdfResult.isValid) + return `${file.name} (${fileSize} KB) is invalid. Processed in ${timeStr}`; + if (!pdfResult.isPdfValid) + return `${file.name} (${fileSize} KB) is valid. PDF/A-3 has warnings (BR-FX-DE-03). Processed in ${timeStr}`; + return `${file.name} (${fileSize} KB) is valid. Processed in ${timeStr}`; } case "validate-xml": { const xmlResult = data as FileValidationResult; @@ -715,7 +723,7 @@ const Index = () => {
{pdfResult.isSignatureValid ? - : } + : } Signature