Skip to content

PDFCLOUD-6572 Add ZUGFeRD PDF validation - #58

Open
datalogics-tsmith wants to merge 15 commits into
mainfrom
pdfcloud-6572-add-zugferd-pdf-validation
Open

datalogics-tsmith wants to merge 15 commits into
mainfrom
pdfcloud-6572-add-zugferd-pdf-validation

Conversation

@datalogics-tsmith

@datalogics-tsmith datalogics-tsmith commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

PDFCLOUD-6572

Why this change

The Python SDK does not expose pdfRest's ZUGFeRD validation operation, so callers cannot use the typed clients to verify an existing ZUGFeRD or Factur-X PDF. This adds a supported way to check package structure, embedded invoice XML, visible-content consistency, and PDF/A-3 conformance before accepting or distributing an electronic invoice.

What changed (high level)

New synchronous and asynchronous validate_zugferd_pdf helpers accept an uploaded PDF resource and return a typed validation response. A dedicated Pydantic payload model owns resource validation and wire serialization, while public response models preserve status, content-match results, findings, and free-tier notices with forward-compatible handling of additional response fields.

The change also adds focused unit coverage, live endpoint coverage, a creation-to-validation live fixture, a runnable example, and API documentation. The branch has been refreshed against main after #57 merged, so the final diff contains only validation work and uses the split ZUGFeRD creation helpers introduced there.

Behavior changes

Callers can submit one uploaded PDF and receive its overall VALID or INVALID status, independent PDF/A result, content-match status, input resource metadata, and detailed findings. Non-PDF inputs and multiple resources fail locally before transport. Free-tier responses with redacted comparison details remain parseable through the typed models.

The live validation fixture creates its input with convert_invoice_xml_to_zugferd_pdf and then validates that output through the new route. The API is additive and does not change existing helper behavior.

Validation

The focused ZUGFeRD creation and validation unit suites passed all 501 tests. The complete non-live suite passed with 3,471 tests and 10 expected skips. Ruff formatting and linting passed, and basedpyright reported zero errors.

The focused live validation suite passed all 6 sync and async cases against the local service, including creation-to-validation, invalid-document reporting, and invalid resource IDs. GitHub Actions was triggered from the main repository branch after the conflict-resolution push.

Risks and follow-ups

PR #57 is merged and this branch now includes current main; GitHub reports the PR as mergeable. Demo-tier deployments can return DEMO_REDACTED for content comparison details, but callers still receive a parseable response and the overall validation status. Required review and the remaining CI jobs must complete before merge.

- Add typed rendering options for invoice appearance and localization.
- Validate uploaded resources, color channels, fonts, and regeneration.
- Serialize the public input shape into the pdfRest wire contract.

Assisted-by: Codex
- Add synchronous and asynchronous helpers for uploaded invoice XML.
- Support optional PDFs, logos, rendering controls, and request options.
- Cover serialization, validation, response mapping, and both transports.

Assisted-by: Codex
- Exercise every closed rendering option through both client transports.
- Verify regenerated PDFs, response metadata, and server-side rejection.
- Add a deterministic minimum-profile invoice fixture.

Assisted-by: Codex
- Add a standalone typed example with a deterministic invoice fixture.
- Verify the generated PDF and print useful response metadata.
- Link the helper from the API guide and example inventories.

Assisted-by: Codex
- Compose page ranges from typed ascending selections and reject invalid
  combinations before transport.
- Add deterministic sync and async annotation assertions with a printable
  PDF fixture.

Assisted-by: Codex
- Validate a single uploaded PDF resource before request serialization.
- Model validation findings and response status fields returned by pdfRest.
- Export the public response models for typed client consumers.

Assisted-by: Codex
- Send uploaded PDF resource IDs to the validation endpoint.
- Return typed results from synchronous and asynchronous clients.
- Preserve request customization and timeout support.

Assisted-by: Codex
- Verify payload serialization and local resource validation.
- Exercise response literals, aliases, findings, and demo redactions.
- Assert sync and async request customization and transport behavior.

Assisted-by: Codex
- Exercise valid and invalid validation requests through both clients.
- Confirm created and regenerated invoices pass validation and PDF/A checks.
- Accept confirmed and demo-redacted content comparison results.

Assisted-by: Codex
- Create a deterministic invoice before validating its conformance.
- Display typed status, PDF/A, content-match, and finding details.
- Add the workflow to the runnable example inventory.

Assisted-by: Codex
- Link the validation helper from the electronic invoice API guide.
- Describe package, content, and PDF/A conformance validation.

Assisted-by: Codex
@netlify

netlify Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for pdfrest-python ready!

Name Link
🔨 Latest commit fae644b
🔍 Latest deploy log https://app.netlify.com/projects/pdfrest-python/deploys/6af5e21cd5be7045d1b453ee
😎 Deploy Preview https://deploy-preview-58--pdfrest-python.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@datalogics-kam datalogics-kam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed only PR #58's incremental changes over PR #57 (7d951b6 to 02cb915), excluding the inherited creation implementation.

Requesting changes for the two inline test-coverage findings: add a real nonconforming-document validation case to the live suite, and make mocked response scenarios and assertions match across the synchronous and asynchronous clients. No runtime or CI regression was found.

Validation passed: 26 focused unit tests, 16 focused live tests, the Python 3.10–3.14 non-live Nox matrix (3,257 passed and 10 expected skips per interpreter), all class-coverage gates, Ruff formatting/lint, and basedpyright. Exploratory validation of the existing report.pdf fixture returned INVALID with pdfa=false through both clients; please preserve that behavior in regression tests.

Comment thread tests/test_validate_zugferd_pdf.py Resolved

@datalogics-kam datalogics-kam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up to the existing review, scoped to the validation changes over PR #57 at the unchanged head 02cb915.

The existing findings remain recorded there. This review adds only the uncovered empty-resource-list boundary described inline; please add that regression coverage before merging.

No source files changed and no new runtime defect was found. Tests were not rerun for this posting step because the head is unchanged from the completed audit.

Comment thread tests/test_validate_zugferd_pdf.py Resolved
- Verify INVALID report.pdf responses live through both clients.
- Match valid and demo-redacted response assertions across transports.
- Cover empty resource-list rejection before request execution.

Assisted-by: Codex
@datalogics-kam

Copy link
Copy Markdown
Contributor

This will be approved once #58 is merged.

Thanks!

@datalogics-kam

Copy link
Copy Markdown
Contributor

@datalogics-tsmith This needs a merge or rebase with conflict resolution now that #58 is merged. Probably also a minor version bump.

- Bring the branch up to date with the merged ZUGFeRD creation API and
  current repository changes.
- Preserve the validation helper, response models, example, and tests
  against the split creation helpers.

Assisted-by: Codex
- Advance project metadata from 1.5.0 to 1.6.0.
- Keep the locked editable package version synchronized.

Assisted-by: Codex
base_url=pdfrest_live_base_url,
) as client:
invoice = client.files.create_from_paths(invoice_path)[0]
response = client.convert_invoice_xml_to_zugferd_pdf(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Restore PR #58 validation checks lost when merging main

These are checks that PR #58 already added, not new requirements for PR #57. The merge 1d69528 resolved the add/add conflict in tests/live/test_live_create_zugferd_pdf.py by retaining main's creation suite, but did not carry forward #58's _assert_zugferd_validation helper or its four validation call sites: synchronous/asynchronous render-option scenarios and synchronous/asynchronous mismatched-PDF regeneration scenarios.

The current fixture here only creates a default invoice with convert_invoice_xml_to_zugferd_pdf. Neither it nor the current creation suite validates the output of convert_pdf_to_zugferd_pdf(..., allow_regeneration=True). Consequently, the checked-in tests no longer protect the regeneration-to-validation behavior that this PR previously exercised.

Please restore the lost checks in PR #58, adapting them to the split creation helpers already merged through #57, or put equivalent distinct sync/async scenarios in tests/live/test_live_validate_zugferd_pdf.py. This does not ask to reopen #57 or expand its creation API/test requirements.

1. What #58 added before the merge

This is the actual diff of the creation live suite between the historical #57 snapshot 7d951b6 and #58's pre-merge head 8b5cda0, showing the validator-specific additions separately from inherited creation work (compare, pre-merge source):

diff --git a/tests/live/test_live_create_zugferd_pdf.py b/tests/live/test_live_create_zugferd_pdf.py
index aa1e835..b197532 100644
--- a/tests/live/test_live_create_zugferd_pdf.py
+++ b/tests/live/test_live_create_zugferd_pdf.py
@@ -5,7 +5,7 @@
 import pytest
 
 from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient
-from pdfrest.models import PdfRestFile
+from pdfrest.models import PdfRestFile, PdfZugferdValidationResponse
 from pdfrest.types import PdfZugferdRenderOptions
 
 from ..resources import get_test_resource_path
@@ -104,6 +104,17 @@ def _assert_zugferd_output(
     assert output_bytes.startswith(b"%PDF-")
 
 
+def _assert_zugferd_validation(
+    validation: PdfZugferdValidationResponse,
+    output: PdfRestFile,
+) -> None:
+    assert validation.status == "VALID"
+    assert validation.pdfa is True
+    assert validation.content_match_status in {"CONFIRMED", "DEMO_REDACTED"}
+    assert validation.input_id == output.id
+    assert f"/resource/{output.id}" in validation.input_url
+
+
 @pytest.mark.parametrize(("render_options", "output"), RENDER_OPTION_CASES)
 def test_live_create_zugferd_pdf_render_options(
     pdfrest_api_key: str,
@@ -125,10 +136,12 @@ def test_live_create_zugferd_pdf_render_options(
             output=output,
         )
         output_bytes = client.files.read_bytes(response.output_file)
+        validation = client.validate_zugferd_pdf(response.output_file)
 
     assert response.input_ids == [invoice.id, logo.id]
     assert response.warning is None
     _assert_zugferd_output(response.output_file, output_bytes, output)
+    _assert_zugferd_validation(validation, response.output_file)
 
 
 @pytest.mark.parametrize(("render_options", "output"), RENDER_OPTION_CASES)
@@ -153,10 +166,12 @@ async def test_live_async_create_zugferd_pdf_render_options(
             output=f"async-{output}",
         )
         output_bytes = await client.files.read_bytes(response.output_file)
+        validation = await client.validate_zugferd_pdf(response.output_file)
 
     assert response.input_ids == [invoice.id, logo.id]
     assert response.warning is None
     _assert_zugferd_output(response.output_file, output_bytes, f"async-{output}")
+    _assert_zugferd_validation(validation, response.output_file)
 
 
 def test_live_create_zugferd_pdf_regenerates_mismatched_pdf(
@@ -179,9 +194,11 @@ def test_live_create_zugferd_pdf_regenerates_mismatched_pdf(
             output="regenerated-zugferd",
         )
         output_bytes = client.files.read_bytes(response.output_file)
+        validation = client.validate_zugferd_pdf(response.output_file)
 
     assert response.input_ids == [invoice.id, pdf.id, logo.id]
     _assert_zugferd_output(response.output_file, output_bytes, "regenerated-zugferd")
+    _assert_zugferd_validation(validation, response.output_file)
 
 
 @pytest.mark.asyncio
@@ -205,11 +222,13 @@ async def test_live_async_create_zugferd_pdf_regenerates_mismatched_pdf(
             output="async-regenerated-zugferd",
         )
         output_bytes = await client.files.read_bytes(response.output_file)
+        validation = await client.validate_zugferd_pdf(response.output_file)
 
     assert response.input_ids == [invoice.id, pdf.id, logo.id]
     _assert_zugferd_output(
         response.output_file, output_bytes, "async-regenerated-zugferd"
     )
+    _assert_zugferd_validation(validation, response.output_file)
 
 
 def test_live_create_zugferd_pdf_rejects_invalid_render_option(

2. What the merge dropped

The following are selected deleted lines from the actual first-parent merge diff, git diff 8b5cda0 1d69528 -- tests/live/test_live_create_zugferd_pdf.py. Intervening unrelated changes from #57's creation-suite rewrite are omitted; the paired sync/async call/assertion lines occurred in the render-option tests and then the regeneration tests (merge, current creation suite):

--- a/tests/live/test_live_create_zugferd_pdf.py
+++ b/tests/live/test_live_create_zugferd_pdf.py
-def _assert_zugferd_validation(
-        validation = client.validate_zugferd_pdf(response.output_file)
-    _assert_zugferd_validation(validation, response.output_file)
-        validation = await client.validate_zugferd_pdf(response.output_file)
-    _assert_zugferd_validation(validation, response.output_file)
-        validation = client.validate_zugferd_pdf(response.output_file)
-    _assert_zugferd_validation(validation, response.output_file)
-        validation = await client.validate_zugferd_pdf(response.output_file)
-    _assert_zugferd_validation(validation, response.output_file)

The missing helper asserted status == "VALID", pdfa is True, confirmed/demo-redacted content matching, and matching input resource ID/URL. The new fixture's default XML-created invoice does not replace those assertions on rendered/regenerated outputs.

This is a loss of regression coverage, not an observed runtime misclassification. Reverification at fae644b passed all 501 focused creation/validation unit tests and all six retained live validation tests. Additional ad-hoc sync and async regeneration-to-validation probes also returned VALID, pdfa=True, and CONFIRMED; those successful probes are not committed regression tests. The three earlier review findings remain addressed.

The relevant test-layering guidance is TESTING_GUIDELINES.md; the reason for this comment is specifically the removal of #58's existing assertions during merge resolution.

This branch was successfully deployed

1 active deployment
ci-live — fae644bc Deployed Nov 12, 2026 by datalogics-tsmith via Live Tests (Python 3.11) #623
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants