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/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..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 { @@ -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; } @@ -414,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"; + } } } @@ -434,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; @@ -688,6 +700,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 && (
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)