Skip to content

Feature/ebilling generate then validate#1008

Open
jbagsik wants to merge 44 commits into
mainfrom
feature/ebilling-generate-then-validate
Open

Feature/ebilling generate then validate#1008
jbagsik wants to merge 44 commits into
mainfrom
feature/ebilling-generate-then-validate

Conversation

@jbagsik

@jbagsik jbagsik commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Inverts the e-billing conversion pipeline from validate-then-merge to generate-then-validate, so we validate the artifact we actually deliver (loose XRechnung XML or hybrid ZUGFeRD/Factur-X PDF). Introduces a format-strategy seam (XRechnung / ZUGFeRD / Factur-X), per-customer format resolution, a new moox/verapdf package for PDF/A-3 checks on hybrids, and hardening of the KoSIT installer/runtime.

What changed

moox/e-billing

  • Pipeline: GenerateXmlJob / ValidateXmlJob / MergeZugferdPdfJobGenerateArtifactJobValidateArtifactJob
  • Formats: FormatRegistry with three CII-based formats — XRechnung (pure XML, XRECHNUNG profile), ZUGFeRD and Factur-X (hybrid PDF, EN16931)
  • Format binding: EbillingFormatResolver reads companies.data.preferred_ebilling_format, falls back to default_format config; format frozen on ebilling_documents.format at generation time
  • Validation: KOSIT on the deliverable XML for every format; veraPDF on hybrid PDFs when moox/verapdf is installed (KOSIT-only degraded mode otherwise). Shared persistence via ArtifactValidationPersister
  • Hybrids: PDF built before validation; deliverable PDFs are unencrypted (removed post-merge qpdf --encrypt); artifact_content_hash stored on validation pass
  • Status model: format-agnostic gateway_status (generating, generation_failed, validating, validated, validation_failed, validator_error, ignored_foreign)
  • Events: renamed to ArtifactGenerated, ArtifactValidated, ArtifactValidationFailed
  • Filament: gateway status badge, failure/processing tabs, filter; artifact downloads gated on validated + stored hash
  • DTO boundaries: Invoice / InvoiceLine are pure DTOs; code resolution moved to adapter/mapper layer (DocumentTypeCodeResolver, UnitCodeResolver, VatIdNormalizer)
  • Storage schema: xml_storage_path + pdf_storage_path (hybrids only) + format column (fresh schema — no data migration, dev DB wipe expected)

moox/verapdf (new package)

  • Install/validate/doctor commands, VeraPdfService, VeraPdfResult, audit model + morph pivot, Pest tests

moox/kosit-validator

  • Hardened kosit:install (checksum verification, safe ZIP extraction, path/URL guards, scoped --force deletion)
  • Runtime integrity: JAR checksum + private temp execution, XRechnung bundle verification/extraction, centralized KositInstallPaths
  • Extracted KositInstaller, RecursiveFileFinder, and related installer support classes

moox/zugferd

  • Invoice contracts consume structured documentTypeCode and unitCode from adapters (no resolution inside DTOs)

Breaking changes

  • Job and event class renames (see above); MergeZugferdPdfJob removed as a pipeline stage
  • gateway_status enum values changed
  • Storage column rename (zugferd_storage_pathxml_storage_path / pdf_storage_path)
  • Invoice / InvoiceLine constructors no longer resolve UNTDID/Rec-20 codes — host parsers and mappers must supply resolved codes or use the new resolvers at boundaries
  • Hybrid PDFs are no longer re-encrypted after merge

Deploy note: republish config/migrations and wipe or recreate ebilling_documents in dev:

php artisan vendor:publish --tag="e-billing-config" --force
php artisan vendor:publish --tag="e-billing-migrations" --force
php artisan vendor:publish --tag="verapdf-config" --force
php artisan vendor:publish --tag="verapdf-migrations" --force
php artisan config:clear

jbagsik and others added 30 commits July 9, 2026 10:25
Introduce FormatRegistry with a zugferd generator strategy, additive
format/artifact_content_hash columns, and rename storage columns so the
later pipeline reorder can land without changing current XML→validate→merge
behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Prefactor for e-billing#5 so the upcoming BT-3 allowlist lands in one place.

Co-authored-by: Cursor <cursoragent@cursor.com>
Untrusted bill_data could previously steer UNTDID codes via raw
passthrough, numeric codelist lookup, or substring matching. Gate all
resolution paths on allowed_document_type_codes (default 380/381).

Co-authored-by: Cursor <cursoragent@cursor.com>
Pin SHA-256 digests for the validator JAR and XRechnung zip, reject zip-slip
entries and HTTP redirect downgrades, stage verified artifacts before install,
and resolve the JAR path deterministically from config.

Closes mooxphp/kosit-validator#1

Co-authored-by: Cursor <cursoragent@cursor.com>
Add InstallerBasePathGuard and InstallerDownloadUrlGuard so --force only
replaces validator/ and xrechnung/ subdirs and downloads are limited to
itplr-kosit GitHub release paths unless explicitly overridden for testing.

Closes mooxphp/kosit-validator#2

Co-authored-by: Cursor <cursoragent@cursor.com>
Happy-path install tests now use storage-scoped base paths and
allowlisted GitHub download URLs with both escape hatches disabled,
so guards are exercised on every install test run.

Closes mooxphp/kosit-validator#8

Co-authored-by: Cursor <cursoragent@cursor.com>
Moves staging, backup, promote, and rollback out of InstallKositCommand
so the handler stays thin. Adds KositInstallPaths value object and
unifies guard throw-on-invalid methods to assertValid.

Closes mooxphp/kosit-validator#9

Co-authored-by: Cursor <cursoragent@cursor.com>
Deduplicate recursive filename lookup in resolveStagedJar and scenariosPath,
and consolidate repeated release-checksum config comments.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extract shared download Guzzle options and replace Http::fake redirect
coverage with MockHandler tests that exercise actual redirect protocol
enforcement offline in CI.

Closes mooxphp/kosit-validator#6

Co-authored-by: Cursor <cursoragent@cursor.com>
Add an optional filesOnly flag so scenariosPath keeps prior iterator
semantics while resolveStagedJar still skips directory name collisions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve existing base paths via realpath so symlink escapes fail containment,
validate paths.* as single safe directory names, and reject before download.

Closes mooxphp/kosit-validator#7

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep canonical tests/TestCase.php via composer autoload-dev; delete
src/Tests copy and symlink, drop in-file autoloader shim, fix migration path.

Closes mooxphp/kosit-validator#3

Co-authored-by: Cursor <cursoragent@cursor.com>
Defence-in-depth for issue #5: validate() re-hashes the on-disk validator
JAR before spawning Java, and SafeZipExtractor rejects entry names
containing null bytes alongside existing zip-slip protections.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add fromConfig() so runtime KositService path resolution uses the same
segment validation as kosit:install, with Pest coverage for rejections.
Closes mooxphp/kosit-validator#10.

Co-authored-by: Cursor <cursoragent@cursor.com>
Verify the pinned XRechnung ZIP at validate time and execute from a
private temp extract so tampering the on-disk tree cannot affect runs.
Hash the validator JAR in memory and spawn Java from a private temp copy.
Treat missing bundle as not installed so legacy layouts prompt reinstall.

Closes mooxphp/kosit-validator#12

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace GenerateXmlJob/ValidateXmlJob/MergeZugferdPdfJob with
GenerateArtifactJob and ValidateArtifactJob; build unencrypted hybrid PDFs
before KOSIT, store SHA-256 on pass, and gate artifact downloads on
validated status. Closes mooxphp/e-billing#2.

Co-authored-by: Cursor <cursoragent@cursor.com>
ValidateArtifactJob now runs veraPDF alongside KOSIT for hybrid formats when
installed, persists VeraPdfValidation records, and falls back to KOSIT-only
when veraPDF is not configured. Includes moox/verapdf package and Seam 1 tests.

Closes mooxphp/e-billing#3

Co-authored-by: Cursor <cursoragent@cursor.com>
Three-format registry (XRechnung / ZUGFeRD / Factur-X), EBillingFormatResolver
reads companies.data.preferred_ebilling_format with frozen-on-generation semantics,
profile-aware CII generation via ZugferdConverter. Closes mooxphp/e-billing#4.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extract ArtifactValidationPersister for shared KOSIT record+attach logic;
veraPDF verdicts persist via supplemental closures so the shared seam
does not type-hint optional validator packages. Closes mooxphp/e-billing#7.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extract PipelineFixtures::arrangeInvoice and named Kosit/veraPDF result factories so each scenario shows only what varies, cutting Sonar duplicated-lines noise without changing coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extract seedVerifiedKositInstall and kositInvoiceXmlPath so each
integrity test starts from a verified install and states only its
tampering step. Pure test refactor — no production behaviour change.

Closes mooxphp/kosit-validator#13

Co-authored-by: Cursor <cursoragent@cursor.com>
jbagsik and others added 14 commits July 23, 2026 15:39
Wire scenariosPath into the tampered-tree integrity test so the
returned handle is the tamper target at the call site.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wrap or extract over-long lines in the generate-then-validate surface and put empty-class braces on their own line; document that Pint cannot enforce a 120-col limit.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move Filament panel boot into opt-in FilamentTestCase so integrity tests no longer pull the duplicated Filament harness (#14).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace file-level Pint exclusions with single_line_empty_body: false so Sonar-compatible braces stick without skipping the two classes entirely.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hoist checksum messages and test download URLs to named constants, wrap jarPath errors, and document that Pint cannot enforce a 120-col limit.

Co-authored-by: Cursor <cursoragent@cursor.com>
Decompose validation orchestration and resolver lookup into named private stages without changing behaviour (closes mooxphp/e-billing#10).

Co-authored-by: Cursor <cursoragent@cursor.com>
…tion.

Extract backup, promote, rollback, and discard into private methods so
checksum-verified staging still completes before any final-location move (#16).

Co-authored-by: Cursor <cursoragent@cursor.com>
Extract named private phases so SonarQube complexity stays under threshold without changing install/validate behaviour or message shaping. Docs unchanged: pure internal refactor, no public API or CLI surface change (verapdf#9).

Co-authored-by: Cursor <cursoragent@cursor.com>
Adapters implement moox/zugferd contracts that use PHP 8.4 property hooks; without an explicit constraint the package inherited core's 8.2/8.3 support and would fatal-parse on those runtimes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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.

1 participant