Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions src/Server/Contracts/PdfFileValidationResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,19 @@ public sealed class PdfFileValidationResponse : FileValidationResponse
/// </summary>
[JsonPropertyName("isSignatureValid")]
public bool IsSignatureValid { get; set; } = false;

/// <summary>
/// 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 <see cref="FileValidationResponse.IsValid"/> may be
/// <c>true</c> even when this value is <c>false</c>.
/// </summary>
[JsonPropertyName("isPdfValid")]
public bool IsPdfValid { get; set; } = false;

/// <summary>
/// Indicates whether the embedded XML invoice data is valid according to the applicable standard.
/// </summary>
[JsonPropertyName("isXmlValid")]
public bool IsXmlValid { get; set; } = false;
}
26 changes: 21 additions & 5 deletions src/Server/Endpoints/PdfEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,29 @@ private static async Task<IResult> 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 <summary status="..."/>
// which represents the overall validation result aggregating both <pdf> and <xml> 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)
Expand All @@ -117,14 +129,18 @@ private static async Task<IResult> 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("<signature>valid</signature>"),
ValidationReport = mustangCliResult.StandardOutput,
DiagnosticsErrorMessage = mustangCliResult.ExitCode != (int)ErrorCode.Success ? mustangCliResult.StandardError : null
}, statusCode: statusCode);
};

return Results.Json(result, statusCode: statusCode);
}
catch (Exception ex)
{
Expand Down
54 changes: 48 additions & 6 deletions src/WebUI/src/pages/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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";
}
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -688,6 +700,36 @@ const Index = () => {
</div>
</div>

{/* PDF sub-validation breakdown */}
{operation === "validate-pdf" && (() => {
const pdfResult = result as PdfFileValidationResult;
return (
<div className="flex flex-wrap gap-3 text-sm">
<div className="flex items-center gap-1.5">
{pdfResult.isXmlValid
? <CheckCircle className="h-4 w-4 text-success" />
: <XCircle className="h-4 w-4 text-destructive" />}
<span>XML invoice</span>
</div>
<div className="flex items-center gap-1.5">
{pdfResult.isPdfValid
? <CheckCircle className="h-4 w-4 text-success" />
: <AlertCircle className="h-4 w-4 text-warning" />}
<span>PDF/A-3</span>
{!pdfResult.isPdfValid && pdfResult.isValid && (
<span className="text-xs text-muted-foreground">(warning only — valid under BR-FX-DE-03)</span>
)}
</div>
<div className="flex items-center gap-1.5">
{pdfResult.isSignatureValid
? <CheckCircle className="h-4 w-4 text-success" />
: <MinusCircle className="h-4 w-4 text-muted-foreground" />}
<span>Signature</span>
</div>
</div>
);
})()}

{((result as FileValidationResult | PdfFileValidationResult)?.validationReport !== undefined || (result as ExtractXmlFromPdfResult)?.xml !== undefined || (result as ConvertXmlToPdfResult)?.pdf) !== undefined && (
<div>
<div className="flex items-center justify-between mb-2">
Expand Down
2 changes: 1 addition & 1 deletion src/docker-build.ps1
Original file line number Diff line number Diff line change
@@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion src/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading