Skip to content

Feature/126 implement peppol#471

Draft
nielsdrost7 wants to merge 2 commits into
developfrom
feature/126-implement-peppol
Draft

Feature/126 implement peppol#471
nielsdrost7 wants to merge 2 commits into
developfrom
feature/126-implement-peppol

Conversation

@nielsdrost7

@nielsdrost7 nielsdrost7 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Given the work already in PR #471, the remaining Sprint 5 deliverables are:

  • Make UblHandler::generateXml() produce real UBL 2.1 XML — this unblocks NLCIUS ([Invoices]: NLCIUS (Dutch CIUS) e-invoicing profile for UBL 2.1 export #487), BE, NL, and most of Europe
  • Fix recommendedForCountry('NL') → UBL_21 (one line)
  • Promote SendInvoiceToPeppolAction to a Filament Action and wire it to EditInvoice
  • Add Peppol fields to customer form (conditional on EU country)
  • Add LetsPeppolProvider directory + tests
  • Add feature test for the end-to-end send flow

Summary by CodeRabbit

Release Notes

New Features

  • Peppol E-Invoicing Integration: Send invoices to Peppol networks with support for multiple providers and document formats
  • Multi-Format Support: Automatically select appropriate invoice formats based on customer country (UBL, FatturaPA, Factur-X, EHF, OIOUBL, and others)
  • Transmission Tracking: Monitor invoice transmission status with automatic polling and retry capabilities for failed transmissions
  • Peppol ID Validation: Validate customer Peppol identifiers against the network before sending
  • Provider Management: Configure and test Peppol provider integrations with support for e-invoice.be and Storecove

Addresses #126

nielsdrost7 and others added 2 commits June 12, 2026 20:34
Restore full Peppol integration on this feature branch:
- Peppol models, migrations, enums, events, jobs, listeners, traits
- EInvoiceBe and Storecove provider implementations
- Format handlers (UBL, BIS, CII, Factur-X, ZUGFeRD, FatturaPA, etc.)
- Artisan commands: PollPeppolStatus, RetryFailedTransmissions, TestPeppolIntegration
- Client peppol-fields migrations for relations table
- Full unit test suite for Peppol integration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nielsdrost7 nielsdrost7 force-pushed the feature/126-implement-peppol branch from 6c38c5d to b3c2b7a Compare June 12, 2026 18:34
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 13, 2026
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 13, 2026
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 13, 2026
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 13, 2026
@nielsdrost7

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@b12f

b12f commented Jun 13, 2026

Copy link
Copy Markdown

Hey @nielsdrost7 both this and #473 seems like huge PRs do to in 2 commits with apparently little oversight. v1 has some overly generous AI coding as well, but it does not seem to be as bad. This feels like it will turn into a maintenance nightmare.

@nielsdrost7

nielsdrost7 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

@b12f thanks for tagging me.
You're correct about V1 and AI.
I'll definitely keep an eye on that one, but it has a specific reason.

Don't worry about #473 for a while, I had it working some time ago.
Basically those import menu items are in 2 locations and that blows up the PR. Above all it needed methods in the Services and it needed phpunit tests, so it blew up a bit more.

You're correct about this PR, but i'll keep an eye on it.
It's actually the reason why I kept if from the develop branch and I was thinking "too big".
If you want to help with this Peppol thing, just let me know, I could use some help.

I squashed some commits, therefore it looks big, but trust me, I developed it diligently

I can be found on Discord: https://discord.gg/PPzD2hTrXt

@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 13, 2026
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 14, 2026
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 14, 2026
@nielsdrost7 nielsdrost7 linked an issue Jun 14, 2026 that may be closed by this pull request
36 tasks
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 14, 2026
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jun 14, 2026
@nielsdrost7

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Peppol invoicing support across database schema, models, enums, HTTP/provider clients, document format handlers, services, actions, queue jobs, console commands, audit events, tests, documentation, and resets the PHPStan ignored-errors baseline.

Changes

Peppol platform

Layer / File(s) Summary
Schema, state enums, and persistence
Modules/Clients/Database/Migrations/*, Modules/Invoices/Database/Migrations/*, Modules/Invoices/Enums/*, Modules/Invoices/Models/*
Adds Peppol fields on relations, creates integration/transmission/validation tables, defines transmission and validation enums, and introduces models for integrations, transmissions, and provider responses.
HTTP client and provider infrastructure
Modules/Invoices/Http/*, Modules/Invoices/Peppol/Clients/..., Modules/Invoices/Peppol/Contracts/*, Modules/Invoices/Peppol/Providers/..., Modules/Invoices/Traits/LogsPeppolActivity.php
Adds request method typing, HTTP logging/error handling, e-invoice.be endpoint clients, provider contracts, provider discovery, a concrete e-invoice.be provider, and a placeholder Storecove provider.
Format contracts, selection, and handlers
Modules/Invoices/Peppol/Enums/*, Modules/Invoices/Peppol/FormatHandlers/*, Modules/Invoices/Peppol/Services/PeppolTransformerService.php
Adds document-format and endpoint-scheme enums, shared format-handler abstractions, handler selection logic, and format-specific invoice transformers for UBL, CII, Facturae, FatturaPA, EHF, OIOUBL, Factur-X, and ZUGFeRD.
Services, action entrypoints, and audit events
Modules/Invoices/Events/Peppol/*, Modules/Invoices/Listeners/Peppol/*, Modules/Invoices/Peppol/Services/*, Modules/Invoices/Actions/SendInvoiceToPeppolAction.php
Adds Peppol lifecycle events and audit logging, management and send services for integrations and document operations, and an action that exposes send, status, and cancel entrypoints for invoices.
Queued transmission, polling, retries, and commands
Modules/Invoices/Jobs/Peppol/*, Modules/Invoices/Console/Commands/*
Adds queued send, retry, and acknowledgement polling workflows plus console commands that trigger polling, retry, and integration test execution.
Coverage, docs, and static analysis cleanup
Modules/Invoices/Tests/Unit/..., Modules/Invoices/Peppol/*.md, phpstan-baseline.neon
Adds unit coverage for actions, enums, HTTP clients, providers, services, and format handlers; documents the Peppol implementation; and clears the PHPStan ignore-errors list.

Sequence Diagram(s)

sequenceDiagram
  participant Action as SendInvoiceToPeppolAction
  participant Service as PeppolService
  participant Factory as FormatHandlerFactory
  participant Client as DocumentsClient
  participant Provider as EInvoiceBe API

  Action->>Service: sendInvoiceToPeppol(invoice, additionalData)
  Service->>Factory: createForInvoice(invoice)
  Factory-->>Service: handler
  Service->>Client: submitDocument(documentData)
  Client->>Provider: POST api/documents
  Provider-->>Client: document response
  Client-->>Service: response
  Service-->>Action: success, document_id, status
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

Suggested labels

codex

Poem

🐇 I hopped through XML in moonlit rows,
Where Peppol routes and payloads rose.
With queues that wait and enums that sing,
I packed each invoice for a springtime fling.
Nibble by nibble, the docs now gleam—
A burrow of tests for an e-invoice dream.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Feature/126 implement peppol' clearly summarizes the main change: implementing Peppol e-invoicing integration as a feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/126-implement-peppol

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (28)
Modules/Invoices/Tests/Unit/Peppol/FormatHandlers/FormatHandlersTest.php-34-34 (1)

34-34: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add native parameter types and remove placeholder unused parameters in data-provider tests.

Several test methods use untyped arguments and placeholder $format = null, which violates the project typing rule and triggers avoidable static-analysis noise.

Proposed refactor
-    public function it_returns_correct_format($handlerClass, $expectedFormat): void
+    public function it_returns_correct_format(string $handlerClass, PeppolDocumentFormat $expectedFormat): void
@@
-    public function it_returns_correct_mime_type($handlerClass, $format = null): void
+    public function it_returns_correct_mime_type(string $handlerClass, PeppolDocumentFormat $expectedFormat): void
     {
         $handler  = new $handlerClass();
         $mimeType = $handler->getMimeType();
+        $this->assertInstanceOf(PeppolDocumentFormat::class, $expectedFormat);
@@
-    public function it_returns_correct_file_extension($handlerClass, $format = null): void
+    public function it_returns_correct_file_extension(string $handlerClass, PeppolDocumentFormat $expectedFormat): void

As per coding guidelines, **/*.php: Use native PHP type hints throughout the codebase.

Also applies to: 44-44, 55-55, 66-66, 80-80, 94-94, 115-115, 130-130, 145-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Tests/Unit/Peppol/FormatHandlers/FormatHandlersTest.php` at
line 34, Add native PHP type hints to all parameters in the test method
signatures and remove any unused placeholder parameters like `$format = null`.
The method at line 34 (it_returns_correct_format) and the related test methods
at lines 44, 55, 66, 80, 94, 115, 130, and 145 all need to have their parameter
types explicitly declared (e.g., string, int, array, etc.) according to PHP
type-hinting standards and remove any null-initialized placeholder parameters
that are not used within the method body.

Sources: Coding guidelines, Linters/SAST tools

Modules/Invoices/Peppol/FILES_CREATED.md-223-226 (1)

223-226: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dependency versions in docs conflict with the project target.

This section documents Laravel 12.x / PHP 8.2+, which conflicts with the repository target and can cause incorrect setup guidance.

As per coding guidelines, **/*.{php,json}: Use Laravel 11 (PHP 8.1+) — not Laravel 12 or PHP 8.2.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FILES_CREATED.md` around lines 223 - 226, The
Production section in the FILES_CREATED.md documentation lists incorrect
dependency versions that conflict with the project's actual target. Update the
version specifications in this section to match the project's coding guidelines:
change "Laravel 12.x" to "Laravel 11.x" and change "PHP 8.2+" to "PHP 8.1+".
Keep the Filament 4.x reference as is, as it does not conflict with the project
target.

Source: Coding guidelines

Modules/Invoices/Tests/Unit/Peppol/Enums/PeppolDocumentFormatTest.php-116-117 (1)

116-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove intentionally disabled tests and make invalid-value checks executable.

At Line 151 and Line 159, both invalid-value test methods are effectively disabled by markTestIncomplete(...), and they also omit #[Test]. Combined with #[Group('failing')] usage (Line 116 and Line 168), this leaves key negative-path assertions non-enforced in normal test execution.

As per coding guidelines, "All tests must use PHPUnit exclusively with class-based approach using #[Test] attributes; never use Pest syntax (it(), test(), describe(), uses(), or expect() chains)".

Suggested fix
-    #[Group('failing')]
     public function it_identifies_mandatory_formats_correctly(
         PeppolDocumentFormat $format,
         string $countryCode,
         bool $expectedMandatory
     ): void {
         $isMandatory = $format->isMandatoryFor($countryCode);

         $this->assertEquals($expectedMandatory, $isMandatory);
     }

+    #[Test]
     public function test_it_throws_on_invalid_enum_value(): void
     {
-        $this->markTestIncomplete('weird test');
-
         $this->expectException(ValueError::class);
         PeppolDocumentFormat::from('invalid_value');
     }

+    #[Test]
     public function test_it_throws_on_invalid_enum_value_name(): void
     {
-        $this->markTestIncomplete('weird test');
-
         $this->expectException(ValueError::class);
         PeppolDocumentFormat::from('not_a_real_enum');
     }

-    #[Group('failing')]
     public function it_provides_description_for_formats(): void
     {
         $description = PeppolDocumentFormat::PEPPOL_BIS_30->description();

Also applies to: 151-165, 168-169

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Tests/Unit/Peppol/Enums/PeppolDocumentFormatTest.php` around
lines 116 - 117, Remove the #[Group('failing')] attributes from test methods
like it_identifies_mandatory_formats_correctly and other test methods to allow
them to execute in normal test runs. Replace the markTestIncomplete(...) calls
in the invalid-value test methods with proper test implementations, and add the
missing #[Test] attributes to all test methods that currently omit them. Ensure
all test methods follow the required PHPUnit class-based approach with #[Test]
attributes for proper test discovery and execution, removing any barriers that
prevent key negative-path assertions from being enforced during normal test
execution.

Source: Coding guidelines

Modules/Invoices/Tests/Unit/Peppol/Clients/DocumentsClientTest.php-196-209 (1)

196-209: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Error-path assertions conflict with the configured client stack.

These tests assert returned unsuccessful responses, but this class constructs DocumentsClient with HttpClientExceptionHandler, which (per its own unit tests) throws on 4xx/5xx. The current assertions validate the wrong contract.

Use expectException(RequestException::class) + response assertions in catch blocks (or remove the exception decorator from this test fixture if you want raw-response behavior).

Also applies to: 230-244, 246-260, 262-276

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Tests/Unit/Peppol/Clients/DocumentsClientTest.php` around
lines 196 - 209, The test method it_handles_validation_errors asserts that a 422
response is returned and checks response properties, but the DocumentsClient is
configured with HttpClientExceptionHandler which throws a RequestException on
4xx/5xx status codes instead of returning the response object. Refactor this
test to use expectException(RequestException::class) to catch the thrown
exception, then move any response assertions into a catch block if validation of
the error details is needed. Apply the same fix pattern to the other similar
test methods (the ones that assert on unsuccessful responses and check status
codes or error messages) to align with the actual behavior of the
HttpClientExceptionHandler.
Modules/Invoices/Peppol/FormatHandlers/FormatHandlerFactory.php-98-103 (1)

98-103: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard missing-customer invoices before dereferencing $customer.

Line 102 dereferences $customer->peppol_format without a null check. If an invoice has no customer, this will crash the factory path.

💡 Proposed fix
     public static function createForInvoice(Invoice $invoice): InvoiceFormatHandlerInterface
     {
         $customer    = $invoice->customer;
-        $countryCode = $customer->country_code ?? null;
+        if ($customer === null) {
+            throw new RuntimeException("Cannot resolve Peppol format: invoice {$invoice->id} has no customer.");
+        }
+
+        $countryCode = $customer->country_code ?? null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/FormatHandlerFactory.php` around lines
98 - 103, The code in the FormatHandlerFactory.php file retrieves a customer
from the invoice and immediately dereferences its properties without verifying
the customer exists. Add a null check to guard against the case where an invoice
has no customer by verifying that $customer is not null before attempting to
access $customer->peppol_format and other customer properties in the subsequent
conditional logic.
Modules/Invoices/Peppol/Enums/PeppolDocumentFormat.php-87-97 (1)

87-97: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

recommendedForCountry('NL') returns the wrong format.

Line 95 maps NL to UBL_24, but the PR objective states NL must resolve to UBL_21. This breaks the intended country-format contract and can mis-route handler selection.

💡 Proposed fix
-            'NL', 'BE', 'GB', 'SE', 'FI', 'XX', '' => self::UBL_24,
-            default => self::UBL_24,
+            'NL', 'BE', 'GB', 'SE', 'FI', 'XX', '' => self::UBL_21,
+            default => self::UBL_21,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Enums/PeppolDocumentFormat.php` around lines 87 - 97,
The recommendedForCountry method incorrectly maps Netherlands (NL) to UBL_24
when it should map to UBL_21. In the match statement, remove 'NL' from the group
on line 95 that maps to self::UBL_24, and create a new case for 'NL' that maps
to self::UBL_21 instead. This ensures the country-format contract is honored and
correct handler selection occurs.
Modules/Invoices/Database/Migrations/2025_10_02_000002_create_peppol_integration_config_table.php-25-25 (1)

25-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Both key/value tables allow duplicate keys per parent due to non-unique indexes. This creates ambiguous state and non-deterministic reads.

  • Modules/Invoices/Database/Migrations/2025_10_02_000002_create_peppol_integration_config_table.php#L25-L25: replace the composite index with a composite unique key on (integration_id, config_key).
  • Modules/Invoices/Database/Migrations/2025_10_02_000004_create_peppol_transmission_responses_table.php#L25-L25: replace the composite index with a composite unique key on (transmission_id, response_key).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Modules/Invoices/Database/Migrations/2025_10_02_000002_create_peppol_integration_config_table.php`
at line 25, Both key/value configuration tables allow duplicate keys per parent
due to non-unique composite indexes, creating ambiguous state. In
Modules/Invoices/Database/Migrations/2025_10_02_000002_create_peppol_integration_config_table.php
at line 25, replace the $table->index() call with $table->unique() on the same
columns (integration_id, config_key) to enforce uniqueness. Similarly, in
Modules/Invoices/Database/Migrations/2025_10_02_000004_create_peppol_transmission_responses_table.php
at line 25, replace the $table->index() call with $table->unique() on
(transmission_id, response_key) to ensure only one response_key value can exist
per transmission_id.
Modules/Invoices/Peppol/FormatHandlers/EhfHandler.php-109-109 (1)

109-109: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use native parameter types on helper methods.

Untyped helper params ($endpointScheme, $item) should be typed to satisfy repository standards and tighten contracts.

As per coding guidelines, "**/*.php: Use native PHP type hints throughout the codebase".

Also applies to: 173-173, 492-492

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/EhfHandler.php` at line 109, Add
native PHP type hints to untyped parameters in helper methods throughout the
file to satisfy coding standards. In the buildSupplierParty method, add an
appropriate type hint to the $endpointScheme parameter (likely string). Apply
the same type-hinting approach to other helper methods in the file that have
untyped parameters (such as methods with $item parameters) to ensure all method
signatures include complete type information as per the repository's PHP coding
guidelines.

Source: Coding guidelines

Modules/Invoices/Peppol/FormatHandlers/CiiHandler.php-137-137 (1)

137-137: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add native parameter type hints to helper methods.

Several helper signatures still use untyped parameters ($customer, $company, $items), which violates the project rule and weakens static safety.

Example direction
-    protected function buildHeaderTradeAgreement(Invoice $invoice, $customer): array
+    protected function buildHeaderTradeAgreement(Invoice $invoice, object $customer): array

-    protected function buildSellerParty($company): array
+    protected function buildSellerParty(object $company): array

-    protected function buildBuyerParty($customer): array
+    protected function buildBuyerParty(object $customer): array

-    protected function buildHeaderTradeSettlement(Invoice $invoice, $customer, $company): array
+    protected function buildHeaderTradeSettlement(Invoice $invoice, object $customer, object $company): array

-    protected function buildLineItems($items, string $currencyCode): array
+    protected function buildLineItems(iterable $items, string $currencyCode): array

As per coding guidelines, "**/*.php: Use native PHP type hints throughout the codebase".

Also applies to: 153-153, 190-190, 233-233, 317-317

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/CiiHandler.php` at line 137, Add
native PHP type hints to all untyped parameters in the helper methods throughout
the CiiHandler.php file. The methods affected are at lines 137, 153, 190, 233,
and 317. For each method, identify untyped parameters like $customer, $company,
and $items, determine their expected types based on the method's logic and class
context, and add appropriate native type hints (e.g., Customer, Company, array,
etc.) to match the project's coding guidelines requiring type hints throughout
the codebase.

Source: Coding guidelines

Modules/Invoices/Peppol/FormatHandlers/CiiHandler.php-42-59 (1)

42-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard missing customer relation before field access in validation.

validate() reads $customer->name/$customer->country_code without verifying $invoice->customer is present, which can break the validation path.

Suggested fix
     public function validate(Invoice $invoice): array
     {
         $errors   = [];
         $customer = $invoice->customer;
+        if (! $customer) {
+            $errors[] = 'Customer is required for CII format';
+            return $errors;
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/CiiHandler.php` around lines 42 - 59,
The validate() method accesses $customer->name and $customer->country_code
properties without first verifying that the $invoice->customer relation exists.
Add a guard clause at the beginning of the validation block (right after
assigning $customer = $invoice->customer) to check if $customer is null or
empty, and if so, add an appropriate error message to the $errors array. This
ensures the customer relation is present before attempting to access its fields.
Modules/Invoices/Peppol/Providers/EInvoiceBe/EInvoiceBeProvider.php-44-47 (1)

44-47: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

testConnection() ignores its $config input, so connection checks can be misleading.

The method receives provider config but always uses pre-resolved clients (Lines 44-47). This can test default credentials instead of the credentials being validated.

Also applies to: 67-70

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Providers/EInvoiceBe/EInvoiceBeProvider.php` around
lines 44 - 47, The testConnection() method receives a $config parameter but
always uses the pre-resolved client properties (documentsClient,
participantsClient, trackingClient, healthClient) that were initialized in the
constructor with default credentials, ignoring the passed configuration. To fix
this, refactor testConnection() to instantiate new client instances from the
provided $config parameter instead of using the instance properties, ensuring
that the connection test actually validates the credentials being provided in
the $config input rather than testing default credentials.
Modules/Invoices/Peppol/Providers/Storecove/StorecoveProvider.php-33-121 (1)

33-121: ⚠️ Potential issue | 🟠 Major

Prevent Storecove selection until implementation is complete.

All core provider methods are placeholders that return "not yet implemented". Storecove is currently listed as a supported provider in the config and can be instantiated via ProviderFactory, meaning users can select it and trigger production failures across send/status/cancel flows (used in SendInvoiceToPeppolJob, PeppolStatusPoller, and PeppolManagementService).

Recommend either: (1) Remove "storecove" from supported providers list until implementation is ready, or (2) Add an explicit unsupported exception at factory instantiation level to prevent runtime failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Providers/Storecove/StorecoveProvider.php` around
lines 33 - 121, The StorecoveProvider class contains only placeholder
implementations for all methods (testConnection, validatePeppolId, sendInvoice,
getTransmissionStatus, cancelDocument) that return "not yet implemented"
messages, yet the provider is still selectable in production. To prevent users
from selecting an incomplete provider and triggering failures in
SendInvoiceToPeppolJob, PeppolStatusPoller, and PeppolManagementService, either
remove "storecove" from the list of supported providers in the provider
configuration or add an explicit exception throw in the ProviderFactory at the
point where StorecoveProvider would be instantiated to block its use until
implementation is complete.
Modules/Invoices/Peppol/Providers/EInvoiceBe/EInvoiceBeProvider.php-35-43 (1)

35-43: ⚠️ Potential issue | 🟠 Major

Narrow constructor type to ?PeppolIntegration to prevent runtime type failures.

Line 36 accepts ?object, but line 42 passes it to BaseProvider::__construct(?PeppolIntegration ...). Any non-PeppolIntegration object will fail at runtime.

🐛 Suggested fix
+use Modules\Invoices\Models\PeppolIntegration;
 ...
     public function __construct(
-        ?object $integration = null,
+        ?PeppolIntegration $integration = null,
         ?DocumentsClient $documentsClient = null,
         ?ParticipantsClient $participantsClient = null,
         ?TrackingClient $trackingClient = null,
         ?HealthClient $healthClient = null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Providers/EInvoiceBe/EInvoiceBeProvider.php` around
lines 35 - 43, The EInvoiceBeProvider::__construct method declares the
$integration parameter with an overly broad type of ?object, but then passes it
to parent::__construct() which expects ?PeppolIntegration. This type mismatch
allows incorrect object types to be passed, causing runtime failures. Change the
type hint of the $integration parameter in EInvoiceBeProvider::__construct from
?object to ?PeppolIntegration to properly enforce type safety and match the
parent class requirement.
Modules/Invoices/Peppol/Services/PeppolService.php-23-24 (1)

23-24: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Make this service extend Modules\Core\Services\BaseService.

Line 23 currently declares a standalone service class, but services in this codebase must inherit the shared base service.

Suggested change
 namespace Modules\Invoices\Peppol\Services;
 
 use Illuminate\Http\Client\RequestException;
 use InvalidArgumentException;
+use Modules\Core\Services\BaseService;
 ...
-class PeppolService
+class PeppolService extends BaseService
 {

As per coding guidelines, "All services must extend Modules\Core\Services\BaseService and use array-based parameters without DTO layer".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Services/PeppolService.php` around lines 23 - 24, The
PeppolService class is currently declared as a standalone service without
extending the required base service class. Modify the class declaration to
extend Modules\Core\Services\BaseService so that PeppolService inherits the
shared base service functionality as per the codebase's coding guidelines
requiring all services to extend BaseService.

Source: Coding guidelines

Modules/Invoices/Peppol/Services/PeppolManagementService.php-29-30 (1)

29-30: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Align this service with the module base-service contract.

Line 29 defines PeppolManagementService without extending Modules\Core\Services\BaseService, which breaks the Services-layer contract in this repository.

As per coding guidelines "**/Services/*.php: All services must extend Modules\Core\Services\BaseService and use array-based parameters without DTO layer".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Services/PeppolManagementService.php` around lines 29
- 30, The PeppolManagementService class definition does not extend the required
Modules\Core\Services\BaseService base class, violating the repository's
service-layer contract. Modify the PeppolManagementService class declaration to
extend Modules\Core\Services\BaseService so it adheres to the coding guidelines
that mandate all service classes must inherit from this base service.

Source: Coding guidelines

Modules/Invoices/Models/PeppolIntegration.php-119-124 (1)

119-124: 🛠️ Refactor suggestion | 🟠 Major

Add native type hints to getConfigValue.

The method signature at line 119 omits native type hints for the $default parameter and return type, violating the project guideline to use native PHP type hints throughout the codebase.

Proposed fix
-    public function getConfigValue(string $key, $default = null)
+    public function getConfigValue(string $key, mixed $default = null): mixed
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Models/PeppolIntegration.php` around lines 119 - 124, Add
native PHP type hints to the getConfigValue method to comply with project
guidelines. Add a return type hint to the method signature (use mixed as the
return type since it can return either the config_value or the default parameter
which could be any type). Add a type hint to the $default parameter (use mixed
as the type since it defaults to null but can accept any value). This ensures
the method signature is fully type-hinted as required by the project standard.

Source: Coding guidelines

Modules/Invoices/Peppol/Services/PeppolManagementService.php-61-63 (1)

61-63: ⚠️ Potential issue | 🟠 Major

Dispatch Peppol domain events only after database transaction commits.

Events at lines 61 and 184 are dispatched before DB::commit() (lines 63 and 189). If the commit fails, event listeners may execute against unpersisted state, compromising data integrity.

Move event dispatches to occur only after successful commit using DB::afterCommit():

Proposed fix pattern
-            event(new PeppolIntegrationCreated($integration));
-
-            DB::commit();
+            DB::commit();
+            DB::afterCommit(fn () => event(new PeppolIntegrationCreated($integration)));
-            event(new PeppolIdValidationCompleted($customer, $validationStatus->value, [
-                'history_id' => $history->id,
-                'present'    => $result['present'],
-            ]));
-
-            DB::commit();
+            DB::commit();
+            DB::afterCommit(fn () => event(new PeppolIdValidationCompleted($customer, $validationStatus->value, [
+                'history_id' => $history->id,
+                'present'    => $result['present'],
+            ])));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Services/PeppolManagementService.php` around lines 61
- 63, The PeppolIntegrationCreated event dispatch occurs before DB::commit(),
which means if the transaction fails, event listeners will execute against
uncommitted data, compromising integrity. Wrap the event dispatch call in
DB::afterCommit() to ensure it executes only after the transaction successfully
commits. Apply this same fix to the second event dispatch mentioned in the
comment (around line 184) to ensure consistent transactional behavior across all
Peppol domain events in this service.
Modules/Invoices/Models/PeppolTransmission.php-114-121 (1)

114-121: ⚠️ Potential issue | 🟠 Major

Add JSON error handling and implement BelongsToCompany trait to ensure data integrity and meet model guidelines.

Line 119 stores the result of json_encode() without error handling, which silently persists false on encoding failure, corrupting response data. Additionally, this model is in Modules/Invoices/Models/ and must use the BelongsToCompany trait per coding guidelines to auto-inject company context on create and apply global scope—however, the database schema currently lacks a company_id column.

JSON encoding fix
-                ['response_value' => is_array($value) ? json_encode($value) : $value]
+                [
+                    'response_value' => is_array($value)
+                        ? json_encode($value, JSON_THROW_ON_ERROR)
+                        : $value,
+                ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Models/PeppolTransmission.php` around lines 114 - 121, In
the setProviderResponse method of the PeppolTransmission model, wrap the
json_encode call with error handling to detect and handle encoding failures
instead of silently storing false values—check the return value and either throw
an exception or log the error before persisting. Additionally, add the
BelongsToCompany trait to the PeppolTransmission class to ensure company context
is automatically injected and global scoping is applied, and ensure the
corresponding database migration or schema includes a company_id column to
support this relationship.
Modules/Invoices/Jobs/Peppol/SendInvoiceToPeppolJob.php-122-126 (1)

122-126: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid logging raw exception traces in this worker path.

At Line 125, full stack traces can expose sensitive internals in centralized logs. Keep structured error metadata and reserve traces for guarded debug-only channels.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Jobs/Peppol/SendInvoiceToPeppolJob.php` around lines 122 -
126, The logPeppolError method call in SendInvoiceToPeppolJob is logging the
full exception stack trace via getTraceAsString(), which can expose sensitive
internals in centralized logs. Remove the 'trace' key and its corresponding
$e->getTraceAsString() value from the error metadata array passed to
logPeppolError, keeping only the invoice_id and error message for structured
error tracking.
Modules/Invoices/Jobs/Peppol/SendInvoiceToPeppolJob.php-96-99 (1)

96-99: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat validation failures as non-retriable job failures.

At Line 97 and Line 121-133, InvalidArgumentException is rethrown like transient runtime failures. That causes repeated queue retries for permanently invalid invoices.

Proposed fix
     public function handle(): void
     {
         try {
             ...
-        } catch (Exception $e) {
+        } catch (InvalidArgumentException $e) {
+            $this->logPeppolWarning('Peppol sending aborted due to invalid invoice data', [
+                'invoice_id' => $this->invoice->id,
+                'error'      => $e->getMessage(),
+            ]);
+            $this->fail($e);
+            return;
+        } catch (Exception $e) {
             $this->logPeppolError('Peppol sending job failed', [
                 'invoice_id' => $this->invoice->id,
                 'error'      => $e->getMessage(),
-                'trace'      => $e->getTraceAsString(),
             ]);

Also applies to: 121-133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Jobs/Peppol/SendInvoiceToPeppolJob.php` around lines 96 -
99, The validateInvoice() method call and subsequent validation logic are
allowing InvalidArgumentException to propagate as retriable errors when they
represent permanent failures. Catch InvalidArgumentException thrown from
validateInvoice() at the call site and handle it as a permanent job failure by
failing the job immediately without queue retry capability. Apply the same
treatment to the validation error handling that occurs in the 121-133 line range
to ensure all validation exceptions are marked as non-retriable and prevent the
queue system from automatically retrying jobs with invalid invoices.
Modules/Invoices/Jobs/Peppol/RetryFailedTransmissions.php-44-47 (1)

44-47: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Atomically claim each due transmission before re-dispatching send jobs.

At Line 44-47 and Line 87-92, due rows are read then dispatched without a claim transition. Concurrent workers can dispatch the same transmission more than once.

Proposed fix
         foreach ($transmissions as $transmission) {
             try {
+                $claimed = PeppolTransmission::query()
+                    ->whereKey($transmission->id)
+                    ->where('status', PeppolTransmissionStatus::RETRYING)
+                    ->where('next_retry_at', '<=', now())
+                    ->update([
+                        'status'        => PeppolTransmissionStatus::PROCESSING,
+                        'next_retry_at' => null,
+                    ]);
+
+                if ($claimed === 0) {
+                    continue; // already claimed by another worker
+                }
+
                 $this->retryTransmission($transmission);
             } catch (Exception $e) {

Also applies to: 87-92

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Jobs/Peppol/RetryFailedTransmissions.php` around lines 44 -
47, The code reads transmissions with status RETRYING and due for retry, then
dispatches jobs without first atomically claiming each transmission. This allows
concurrent workers to process the same transmission record multiple times.
Implement an atomic claim mechanism by updating each transmission's status to a
claimed state (or using a database-level lock/update pattern) within a
transaction before dispatching the send job in the RetryFailedTransmissions job
handler. This ensures only one worker can claim and process each transmission,
eliminating duplicate dispatches across concurrent workers.
Modules/Invoices/Jobs/Peppol/SendInvoiceToPeppolJob.php-437-457 (1)

437-457: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use one retry scheduler path to prevent duplicate resend attempts.

At Line 455-456, this job self-dispatches delayed retries, while Modules/Invoices/Jobs/Peppol/RetryFailedTransmissions.php also dispatches due retries. Running both paths can duplicate sends for the same transmission.

Proposed fix (if scanner job is the chosen scheduler)
-        // Re-dispatch the job
-        static::dispatch($this->invoice, $this->integration, false, $transmission->id)
-            ->delay($nextRetryAt);
+        // Retry dispatch is handled by RetryFailedTransmissions scanner.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Jobs/Peppol/SendInvoiceToPeppolJob.php` around lines 437 -
457, The scheduleRetry method in SendInvoiceToPeppolJob is self-dispatching
delayed job retries while RetryFailedTransmissions.php also handles retries,
creating duplicate send attempts. Remove the static::dispatch call and ->delay()
logic from the scheduleRetry method (the last two lines of the diff) so that
only RetryFailedTransmissions manages dispatching retries. Keep the transmission
scheduling logic (the call to $transmission->scheduleRetry($nextRetryAt)) so the
transmission state is updated and RetryFailedTransmissions can pick it up.
Modules/Invoices/Listeners/Peppol/LogPeppolEventToAudit.php-36-41 (1)

36-41: ⚠️ Potential issue | 🟠 Major

Register Peppol models in morphMap or use morph-compatible audit_type values.

LogPeppolEventToAudit stores audit_type values (peppol_transmission, peppol_integration, peppol_validation, peppol_event) that are not registered in the morph map. The AuditLog::audit() method uses morphTo(), which expects discriminators to be either registered morph aliases or fully qualified class names. When getFormattedActivityAttribute() accesses $this->audit, the relation resolution will fail for these values, breaking audit log access at runtime.

Add PeppolTransmission and PeppolIntegration to the morph map in AppServiceProvider:

Relation::morphMap([
    'invoice' => Invoice::class,
+   'peppol_transmission' => PeppolTransmission::class,
+   'peppol_integration' => PeppolIntegration::class,
]);

Then update getAuditType() in LogPeppolEventToAudit to return the corresponding morph alias:

protected function getAuditType(PeppolEvent $event): string
{
    $eventName = $event->getEventName();

    if (str_contains($eventName, 'transmission')) {
-       return 'peppol_transmission';
+       return 'peppol_transmission';  // matches morph alias
    }
    if (str_contains($eventName, 'integration')) {
-       return 'peppol_integration';
+       return 'peppol_integration';   // matches morph alias
    }
    if (str_contains($eventName, 'validation')) {
-       return 'peppol_validation';
+       return 'peppol_transmission';  // should this be transmission or create separate entry?
    }

-   return 'peppol_event';
+   return 'peppol_transmission';  // fallback—clarify intent
}

Also add cases to getFormattedActivityAttribute() in AuditLog to handle Peppol model types.

Also applies to: 83-98

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Listeners/Peppol/LogPeppolEventToAudit.php` around lines 36
- 41, The audit_type values for Peppol events (peppol_transmission,
peppol_integration, peppol_validation, peppol_event) are not registered in the
morph map, causing runtime failures when the morphTo() relation tries to resolve
them. Register PeppolTransmission and PeppolIntegration models in the morph map
within AppServiceProvider. Then update the getAuditType() method in
LogPeppolEventToAudit to return the corresponding morph alias strings (e.g.,
'peppol_transmission') instead of custom identifiers. Finally, add cases to the
getFormattedActivityAttribute() method in AuditLog to handle the Peppol model
types and properly format their activity display.
Modules/Invoices/Http/Traits/LogsApiRequests.php-124-130 (1)

124-130: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Header redaction is case-sensitive and can leak credentials.

Line 124 onward only redacts exact keys (Authorization, X-API-Key, X-Auth-Token). Lowercase/mixed-case variants (e.g., authorization) will be logged unredacted.

Suggested fix
-        if (isset($sanitized['headers'])) {
-            $sensitiveHeaders = ['Authorization', 'X-API-Key', 'X-Auth-Token'];
-            foreach ($sensitiveHeaders as $header) {
-                if (isset($sanitized['headers'][$header])) {
-                    $sanitized['headers'][$header] = '***REDACTED***';
-                }
-            }
-        }
+        if (isset($sanitized['headers']) && is_array($sanitized['headers'])) {
+            $sensitiveHeaders = ['authorization', 'x-api-key', 'x-auth-token'];
+            foreach ($sanitized['headers'] as $header => $value) {
+                if (in_array(mb_strtolower((string) $header), $sensitiveHeaders, true)) {
+                    $sanitized['headers'][$header] = '***REDACTED***';
+                }
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Http/Traits/LogsApiRequests.php` around lines 124 - 130, The
header redaction logic in LogsApiRequests.php uses case-sensitive exact key
matching, which means HTTP headers with different case variants (e.g.,
'authorization' instead of 'Authorization') will not be redacted and credentials
will leak. Change the redaction approach to iterate through the actual headers
in the $sanitized['headers'] array and compare each header key against the
sensitive headers list using case-insensitive comparison (such as strtolower or
strcasecmp), ensuring all variants of Authorization, X-API-Key, and X-Auth-Token
are properly redacted regardless of casing.
Modules/Invoices/Http/Clients/ApiClient.php-38-40 (1)

38-40: ⚠️ Potential issue | 🟠 Major

Remove unconditional ->throw() to allow status-code-based error handling in downstream callers.

The request() method unconditionally calls ->throw() on line 40, which throws a RequestException for all 4xx/5xx responses before they can reach callers. This breaks the 404 handling in EInvoiceBeProvider::validatePeppolId() (lines 125–129), where code checks $response->status() === 404 to determine if a participant is not found—this branch is now unreachable because the exception is thrown instead.

Suggested fix
-        return $client
-            ->{$method->value}($uri, $options['payload'] ?? [])
-            ->throw();
+        $response = $client
+            ->{$method->value}($uri, $options['payload'] ?? []);
+
+        if (($options['throw'] ?? false) === true) {
+            $response->throw();
+        }
+
+        return $response;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Http/Clients/ApiClient.php` around lines 38 - 40, Remove the
unconditional ->throw() call from the return statement in the request() method
of ApiClient. The ->throw() on the return chain prevents callers like
EInvoiceBeProvider::validatePeppolId() from receiving the response object to
check status codes (such as checking for 404). Instead of throwing immediately,
return the response object directly so downstream callers can handle specific
HTTP status codes through conditional logic on $response->status().
Modules/Invoices/Peppol/FormatHandlers/FacturXHandler.php-45-55 (1)

45-55: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

generateXml() currently returns JSON instead of XML payload.

This breaks the format contract when Factur-X is selected: downstream code expecting XML/PDF-A-3 content will receive JSON text.

🔧 Suggested minimal safe fix (fail fast until XML generation is implemented)
 public function generateXml(Invoice $invoice, array $options = []): string
 {
-    $data = $this->transform($invoice, $options);
-
-    // Placeholder - would generate proper CII XML embedded in PDF/A-3
-    // For Factur-X, this would:
-    // 1. Generate the CII XML
-    // 2. Generate a PDF from the invoice
-    // 3. Embed the XML into the PDF as PDF/A-3 attachment
-    return json_encode($data, JSON_PRETTY_PRINT);
+    throw new \RuntimeException('Factur-X XML generation is not implemented yet.');
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/FacturXHandler.php` around lines 45 -
55, The generateXml() method in the FacturXHandler class currently returns
JSON-encoded data instead of XML/PDF-A-3 content, which breaks the format
contract when Factur-X is selected. Replace the json_encode($data,
JSON_PRETTY_PRINT) return statement with code that throws an exception to fail
fast and explicitly indicate that Factur-X XML generation is not yet
implemented, rather than silently returning incorrect JSON format that would
cause downstream failures.
Modules/Invoices/Peppol/FormatHandlers/FacturXHandler.php-361-364 (1)

361-364: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hardcoded 20% tax fallback can generate incorrect invoice totals.

Defaulting missing tax_rate to 20.0 will overstate tax for exempt/zero-tax lines and non-20% jurisdictions, which can corrupt transmitted fiscal data.

🔧 Suggested fix
 protected function getTaxRate(mixed $item): float
 {
-    return $item->tax_rate ?? 20.0; // Default French VAT rate
+    return isset($item->tax_rate) ? (float) $item->tax_rate : 0.0;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/FacturXHandler.php` around lines 361 -
364, The getTaxRate method in the FacturXHandler class uses a hardcoded 20.0
percent default for missing tax_rate values, which incorrectly assumes all items
should default to French VAT and can corrupt fiscal data. Replace the hardcoded
default with either a 0.0 default (for tax-exempt items), a configurable default
retrieved from settings, or remove the default entirely and throw an exception
to ensure tax_rate is always explicitly provided before processing the invoice.
Modules/Invoices/Peppol/FormatHandlers/UblHandler.php-65-71 (1)

65-71: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

All generateXml() methods return JSON — stored artifacts will be invalid XML.

All six format handlers implement generateXml() as a placeholder that returns json_encode($data, JSON_PRETTY_PRINT) instead of actual XML. Per SendInvoiceToPeppolJob::prepareArtifacts() (context snippet), this output is stored at stored_xml_path. The mismatch will cause downstream XML validators or Peppol network receivers to reject the documents.

  • Modules/Invoices/Peppol/FormatHandlers/UblHandler.php#L65-L71: Replace JSON placeholder with proper UBL XML generation (e.g., using sabre/xml or a dedicated UBL library).
  • Modules/Invoices/Peppol/FormatHandlers/FacturaeHandler.php#L58-L64: Implement Facturae 3.2 XML generation.
  • Modules/Invoices/Peppol/FormatHandlers/FatturaPaHandler.php#L57-L63: Implement FatturaPA 1.2 XML generation.
  • Modules/Invoices/Peppol/FormatHandlers/OioublHandler.php#L90-L96: Implement OIOUBL XML generation.
  • Modules/Invoices/Peppol/FormatHandlers/PeppolBisHandler.php#L150-L157: Implement PEPPOL BIS 3.0 XML generation.
  • Modules/Invoices/Peppol/FormatHandlers/ZugferdHandler.php#L58-L64: Implement ZUGFeRD CII XML generation (and PDF/A-3 embedding for production).

If this is intentionally deferred, consider throwing NotImplementedException or gating transmission behind a feature flag to prevent invalid artifacts from being stored/sent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/UblHandler.php` around lines 65 - 71,
All six format handlers have placeholder generateXml() methods that return
json_encode() instead of proper XML, causing invalid artifacts to be stored. In
Modules/Invoices/Peppol/FormatHandlers/UblHandler.php (lines 65-71), replace the
json_encode placeholder with proper UBL XML generation using a library like
sabre/xml. Apply the same pattern to the remaining five sibling handlers:
FacturaeHandler.php (lines 58-64) for Facturae 3.2 XML, FatturaPaHandler.php
(lines 57-63) for FatturaPA 1.2 XML, OioublHandler.php (lines 90-96) for OIOUBL
XML, PeppolBisHandler.php (lines 150-157) for PEPPOL BIS 3.0 XML, and
ZugferdHandler.php (lines 58-64) for ZUGFeRD CII XML generation. If
implementation is deferred, throw NotImplementedException in the generateXml()
method at all six locations to prevent invalid artifacts from being stored or
transmitted to the Peppol network.
🧹 Nitpick comments (9)
Modules/Invoices/Peppol/FormatHandlers/BaseFormatHandler.php (1)

150-156: ⚡ Quick win

Remove the unused variadic parameter from getCurrencyCode().

Line 155 defines ...$args but never uses it. This keeps PHPMD noise and widens the method signature without value.

💡 Proposed refactor
-    protected function getCurrencyCode(Invoice $invoice, ...$args): string
+    protected function getCurrencyCode(Invoice $invoice): string
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/BaseFormatHandler.php` around lines
150 - 156, The getCurrencyCode() method in BaseFormatHandler.php accepts a
variadic parameter ...$args but never uses it in the method body, creating
unnecessary complexity and PHPMD warnings. Simply remove the ...$args parameter
from the method signature on line 155, keeping only the Invoice parameter, to
clean up the code and reduce noise.

Source: Linters/SAST tools

Modules/Invoices/Enums/PeppolConnectionStatus.php (1)

21-27: ⚡ Quick win

Localize enum labels via trans() instead of hardcoded strings.

label() currently hardcodes English values. Move these to translation keys so enum labels are i18n-ready.

♻️ Suggested change
     public function label(): string
     {
         return match ($this) {
-            self::UNTESTED => 'Untested',
-            self::SUCCESS  => 'Success',
-            self::FAILED   => 'Failed',
+            self::UNTESTED => trans('invoices::peppol.connection_status.untested'),
+            self::SUCCESS  => trans('invoices::peppol.connection_status.success'),
+            self::FAILED   => trans('invoices::peppol.connection_status.failed'),
         };
     }

As per coding guidelines, "Use trans() function for internationalization, never use __()".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Enums/PeppolConnectionStatus.php` around lines 21 - 27, The
label() method in the PeppolConnectionStatus enum is returning hardcoded English
strings for the match cases (UNTESTED, SUCCESS, FAILED). Replace each of these
hardcoded string values with trans() function calls using appropriate
translation keys (following your project's i18n naming conventions, typically
something like 'invoices.peppol_connection_status.untested'). Ensure you use the
trans() function as specified in the coding guidelines, not __().

Source: Coding guidelines

Modules/Invoices/Peppol/Clients/EInvoiceBe/DocumentsClient.php (1)

183-191: The code is functionally correct—Laravel's HTTP client automatically treats the second parameter as query parameters for GET requests, so filtering works as intended. However, using the 'payload' key for GET request filters is semantically confusing since 'payload' conventionally refers to request bodies (POST/PUT/PATCH). Rename to 'query' for clarity:

Suggested change
-        $options = array_merge($this->getRequestOptions(), [
-            'payload' => $filters,
-        ]);
+        $options = array_merge($this->getRequestOptions(), [
+            'query' => $filters,
+        ]);

This improves code readability without changing functionality.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Clients/EInvoiceBe/DocumentsClient.php` around lines
183 - 191, In the method that calls $this->client->request() with
RequestMethod::GET and builds the URL 'api/documents', change the 'payload' key
in the options array to 'query'. This semantic change clarifies that these are
query parameters for a GET request rather than a request body payload, making
the code's intent more obvious to future readers. No functional change is
needed—just rename the key from 'payload' to 'query' in the array_merge call.
Modules/Invoices/Peppol/FormatHandlers/OioublHandler.php (3)

40-77: ⚡ Quick win

Unused $customer variable.

$customer is assigned on line 42 but never used in transform().

♻️ Suggested fix
     public function transform(Invoice $invoice, array $options = []): array
     {
-        $customer       = $invoice->customer;
         $currencyCode   = $this->getCurrencyCode($invoice);
         $endpointScheme = $this->getEndpointScheme($invoice);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/OioublHandler.php` around lines 40 -
77, The variable `$customer` is assigned in the `transform()` method of the
`OioublHandler` class but is never used anywhere in the method body. Remove the
line that assigns `$customer = $invoice->customer;` to eliminate the unused
variable.

Source: Linters/SAST tools


334-356: ⚡ Quick win

Unused $taxAmount variable.

$taxAmount is computed on line 336 but never used in buildMonetaryTotal().

♻️ Suggested fix
     protected function buildMonetaryTotal(Invoice $invoice, string $currencyCode): array
     {
-        $taxAmount = $invoice->invoice_total - $invoice->invoice_subtotal;
-
         return [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/OioublHandler.php` around lines 334 -
356, The buildMonetaryTotal() method calculates a $taxAmount variable but never
uses it anywhere in the function body. Remove the line that assigns $taxAmount
to eliminate this unused variable and clean up the code.

Source: Linters/SAST tools


371-410: ⚡ Quick win

Unused $taxAmount in invoice line closure.

$taxAmount is computed on line 375 but never used in the returned line structure.

♻️ Suggested fix
         return $invoice->invoiceItems->map(function ($item, $index) use ($currencyCode) {
             $taxRate   = $this->getTaxRate($item);
-            $taxAmount = $item->subtotal * ($taxRate / 100);

             return [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/OioublHandler.php` around lines 371 -
410, The variable $taxAmount is calculated in the closure within the
buildInvoiceLines method but is never referenced in the returned array
structure. Remove the unused $taxAmount variable assignment (the line that
calculates item->subtotal * (taxRate / 100)) from the closure, as it serves no
purpose and constitutes dead code.

Source: Linters/SAST tools

Modules/Invoices/Peppol/FormatHandlers/FatturaPaHandler.php (1)

38-47: ⚡ Quick win

Unused $customer variable.

$customer is assigned on line 40 but never referenced in transform(). Remove the dead assignment.

♻️ Suggested fix
     public function transform(Invoice $invoice, array $options = []): array
     {
-        $customer     = $invoice->customer;
         $currencyCode = $this->getCurrencyCode($invoice);

         return [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/FatturaPaHandler.php` around lines 38
- 47, The transform() method in the FatturaPaHandler class assigns the variable
$customer from $invoice->customer but never uses it anywhere in the method body.
Remove the unused variable assignment line that assigns $customer to eliminate
the dead code.

Source: Linters/SAST tools

Modules/Invoices/Peppol/FormatHandlers/UblHandler.php (1)

32-60: ⚡ Quick win

Unused $customer and $endpointScheme variables in transform().

Both are assigned but only used indirectly through helper methods that re-fetch them.

♻️ Suggested fix
     public function transform(Invoice $invoice, array $options = []): array
     {
-        $customer       = $invoice->customer;
         $currencyCode   = $this->getCurrencyCode($invoice);
-        $endpointScheme = $this->getEndpointScheme($invoice);

         return [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/UblHandler.php` around lines 32 - 60,
The transform() method in UblHandler assigns two variables, $customer and
$endpointScheme, that are never directly used within the method body. Remove
both variable assignments since the helper methods (buildSupplierParty(),
buildCustomerParty(), buildInvoiceLines(), and buildMonetaryTotals()) re-fetch
these values themselves rather than accepting them as parameters.

Source: Linters/SAST tools

Modules/Invoices/Peppol/FormatHandlers/FacturaeHandler.php (1)

362-388: 💤 Low value

Unused $index parameter in closure.

The $index parameter is received but never used. If line numbering is needed for Facturae (similar to FatturaPA's NumeroLinea), add it; otherwise remove the parameter.

♻️ Suggested fix
-    protected function buildItems(Invoice $invoice, string $currencyCode): array
-    {
-        $items = $invoice->invoiceItems->map(function ($item, $index) {
+    protected function buildItems(Invoice $invoice, string $currencyCode): array
+    {
+        $items = $invoice->invoiceItems->map(function ($item) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/FacturaeHandler.php` around lines 362
- 388, The closure function in the map() call within the invoice items
processing block receives an `$index` parameter that is not used. Either remove
the unused `$index` parameter from the closure signature if line numbering is
not required for the Facturae format, or if line numbering is needed (similar to
FatturaPA's NumeroLinea), use the index value and include it in the returned
InvoiceLine structure to properly number each invoice line in the formatted
output.

Comment on lines +108 to +109
if ($invoice->invoice_status === 'draft') {
throw new InvalidArgumentException('Cannot send draft invoices to Peppol');

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Invoice model status fields/casts/accessors and draft references.
fd -i 'Invoice.php' Modules/Invoices/Models -t f
rg -n -C3 '\binvoice_status\b|\binvoice_status_id\b|casts|Attribute::make|draft' Modules/Invoices/Models/Invoice.php Modules/Invoices

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 46310


Replace string literal with enum constant for invoice status check.

Line 108 compares $invoice->invoice_status to the string 'draft', but the field is cast to the InvoiceStatus enum in the Invoice model. This comparison will always fail, silently allowing draft invoices to pass validation. Use InvoiceStatus::DRAFT instead:

if ($invoice->invoice_status === InvoiceStatus::DRAFT) {
    throw new InvalidArgumentException('Cannot send draft invoices to Peppol');
}

See InvoiceService.php line 183 for the correct pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Actions/SendInvoiceToPeppolAction.php` around lines 108 -
109, In the SendInvoiceToPeppolAction class, the condition that checks if the
invoice status is draft compares the invoice_status field to the string literal
'draft', but since invoice_status is cast to the InvoiceStatus enum in the
Invoice model, this comparison will always fail. Replace the string literal
'draft' with the enum constant InvoiceStatus::DRAFT in the if condition to
properly validate that draft invoices cannot be sent to Peppol.

Comment on lines +31 to +67
/**
* Polls providers for status updates of recently sent Peppol transmissions and updates local records.
*
* Retrieves up to 100 transmissions that are marked SENT, have an external ID, are not yet acknowledged,
* and were sent more than five minutes ago. For each transmission it checks the provider status, updates
* the transmission state accordingly, and logs per-transmission errors without aborting the batch.
*
* Logs the start and completion of the polling job and includes the number of transmissions checked.
* /
* public function handle(): void
* {
* $this->logPeppolInfo('Starting Peppol status polling job');
*
* // Get all transmissions awaiting acknowledgement
* $transmissions = PeppolTransmission::query()->where('status', PeppolTransmissionStatus::SENT)
* ->whereNotNull('external_id')
* ->whereNull('acknowledged_at')
* ->where('sent_at', '<', now()->subMinutes(5)) // Allow 5 min grace period
* ->limit(100) // Process in batches
* ->get();
*
* foreach ($transmissions as $transmission) {
* try {
* $this->checkStatus($transmission);
* } catch (\Exception $e) {
* $this->logPeppolError('Failed to check transmission status', [
* 'transmission_id' => $transmission->id,
* 'error' => $e->getMessage(),
* ]);
* }
* }
*
* $this->logPeppolInfo('Completed Peppol status polling', [
* 'checked' => $transmissions->count(),
* ]);
* }
*

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Confirm no executable handle() currently exists.
rg -n -C2 '^\s*public function handle\(' Modules/Invoices/Jobs/Peppol/PeppolStatusPoller.php || true

# 2) Confirm malformed docblock marker.
rg -n -C2 '\* /' Modules/Invoices/Jobs/Peppol/PeppolStatusPoller.php

# 3) Confirm referenced status enum usage/import state.
rg -n -C2 'PeppolTransmissionStatus' Modules/Invoices/Jobs/Peppol/PeppolStatusPoller.php

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 961


🏁 Script executed:

head -30 Modules/Invoices/Jobs/Peppol/PeppolStatusPoller.php

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 957


handle() is commented out due to malformed PHPDoc, making the queued job non-functional.

Line 39 contains * / instead of */, which closes the docblock prematurely and comments out the entire handle() method (lines 40–66). The class implements ShouldQueue, so the queue worker expects an executable handle() method. Additionally, the commented code references PeppolTransmissionStatus but the import is missing from the top of the file.

Suggested fix
 use Modules\Invoices\Events\Peppol\PeppolAcknowledgementReceived;
+use Modules\Invoices\Enums\PeppolTransmissionStatus;
 use Modules\Invoices\Models\PeppolTransmission;
 use Modules\Invoices\Peppol\Providers\ProviderFactory;
 use Modules\Invoices\Traits\LogsPeppolActivity;
@@
     /**
      * Polls providers for status updates of recently sent Peppol transmissions and updates local records.
@@
      * Logs the start and completion of the polling job and includes the number of transmissions checked.
-     * /
-     * public function handle(): void
-     * {
-     * $this->logPeppolInfo('Starting Peppol status polling job');
-     *
-     * // Get all transmissions awaiting acknowledgement
-     * $transmissions = PeppolTransmission::query()->where('status', PeppolTransmissionStatus::SENT)
-     * ->whereNotNull('external_id')
-     * ->whereNull('acknowledged_at')
-     * ->where('sent_at', '<', now()->subMinutes(5)) // Allow 5 min grace period
-     * ->limit(100) // Process in batches
-     * ->get();
-     *
-     * foreach ($transmissions as $transmission) {
-     * try {
-     * $this->checkStatus($transmission);
-     * } catch (\Exception $e) {
-     * $this->logPeppolError('Failed to check transmission status', [
-     * 'transmission_id' => $transmission->id,
-     * 'error' => $e->getMessage(),
-     * ]);
-     * }
-     * }
-     *
-     * $this->logPeppolInfo('Completed Peppol status polling', [
-     * 'checked' => $transmissions->count(),
-     * ]);
-     * }
-     *
-     * /**
+     */
+    public function handle(): void
+    {
+        $this->logPeppolInfo('Starting Peppol status polling job');
+
+        $transmissions = PeppolTransmission::query()
+            ->where('status', PeppolTransmissionStatus::SENT)
+            ->whereNotNull('external_id')
+            ->whereNull('acknowledged_at')
+            ->where('sent_at', '<', now()->subMinutes(5))
+            ->limit(100)
+            ->get();
+
+        foreach ($transmissions as $transmission) {
+            try {
+                $this->checkStatus($transmission);
+            } catch (\Exception $e) {
+                $this->logPeppolError('Failed to check transmission status', [
+                    'transmission_id' => $transmission->id,
+                    'error' => $e->getMessage(),
+                ]);
+            }
+        }
+
+        $this->logPeppolInfo('Completed Peppol status polling', [
+            'checked' => $transmissions->count(),
+        ]);
+    }
+
+    /**
      * Polls the external provider for a transmission's delivery status and updates the local record accordingly.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Polls providers for status updates of recently sent Peppol transmissions and updates local records.
*
* Retrieves up to 100 transmissions that are marked SENT, have an external ID, are not yet acknowledged,
* and were sent more than five minutes ago. For each transmission it checks the provider status, updates
* the transmission state accordingly, and logs per-transmission errors without aborting the batch.
*
* Logs the start and completion of the polling job and includes the number of transmissions checked.
* /
* public function handle(): void
* {
* $this->logPeppolInfo('Starting Peppol status polling job');
*
* // Get all transmissions awaiting acknowledgement
* $transmissions = PeppolTransmission::query()->where('status', PeppolTransmissionStatus::SENT)
* ->whereNotNull('external_id')
* ->whereNull('acknowledged_at')
* ->where('sent_at', '<', now()->subMinutes(5)) // Allow 5 min grace period
* ->limit(100) // Process in batches
* ->get();
*
* foreach ($transmissions as $transmission) {
* try {
* $this->checkStatus($transmission);
* } catch (\Exception $e) {
* $this->logPeppolError('Failed to check transmission status', [
* 'transmission_id' => $transmission->id,
* 'error' => $e->getMessage(),
* ]);
* }
* }
*
* $this->logPeppolInfo('Completed Peppol status polling', [
* 'checked' => $transmissions->count(),
* ]);
* }
*
/**
* Polls providers for status updates of recently sent Peppol transmissions and updates local records.
*
* Retrieves up to 100 transmissions that are marked SENT, have an external ID, are not yet acknowledged,
* and were sent more than five minutes ago. For each transmission it checks the provider status, updates
* the transmission state accordingly, and logs per-transmission errors without aborting the batch.
*
* Logs the start and completion of the polling job and includes the number of transmissions checked.
*/
public function handle(): void
{
$this->logPeppolInfo('Starting Peppol status polling job');
$transmissions = PeppolTransmission::query()
->where('status', PeppolTransmissionStatus::SENT)
->whereNotNull('external_id')
->whereNull('acknowledged_at')
->where('sent_at', '<', now()->subMinutes(5))
->limit(100)
->get();
foreach ($transmissions as $transmission) {
try {
$this->checkStatus($transmission);
} catch (\Exception $e) {
$this->logPeppolError('Failed to check transmission status', [
'transmission_id' => $transmission->id,
'error' => $e->getMessage(),
]);
}
}
$this->logPeppolInfo('Completed Peppol status polling', [
'checked' => $transmissions->count(),
]);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Jobs/Peppol/PeppolStatusPoller.php` around lines 31 - 67,
Fix the malformed PHPDoc closing tag in the handle() method by changing the `*
/` to `*/` on line 39 to properly close the docblock and uncomment the method
body. Additionally, add the missing import statement for
PeppolTransmissionStatus at the top of the PeppolStatusPoller.php file so that
the class reference used in the handle() method is available. This will make the
queued job functional again since the class implements ShouldQueue and the queue
worker expects an executable handle() method.

{
$this->client = $client;
$this->apiKey = $apiKey;
$this->baseUrl = mb_rtrim($baseUrl, '/');

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify whether mb_ltrim/mb_rtrim are defined anywhere in the repository.
rg -n -C2 'function\s+mb_(ltrim|rtrim)\s*\('

# Show all call sites to confirm impact surface.
rg -n -C2 '\bmb_(ltrim|rtrim)\s*\('

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 54


🏁 Script executed:

# Check if file exists and read the relevant lines
find . -name "BasePeppolClient.php" -type f | head -5

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 127


🌐 Web query:

mb_ltrim mb_rtrim PHP functions standard library

💡 Result:

The mb_ltrim and mb_rtrim functions are part of the PHP standard library, specifically within the Multibyte String (mbstring) extension [1]. They were introduced in PHP 8.4.0 [2][3]. These functions provide multibyte-safe versions of the traditional ltrim and rtrim functions, allowing for the correct trimming of whitespace or specified characters from the beginning or end of strings that contain multibyte characters (such as UTF-8) [4][5][6]. Key details include: - Functionality: Like their standard counterparts, mb_ltrim strips characters from the beginning of a string, while mb_rtrim strips characters from the end [4][5]. - Parameters: Both functions accept the input string, an optional string of characters to strip, and an optional encoding parameter [4][5][6]. If no encoding is specified, the internal character encoding is used [4][5]. - Default Behavior: When the second parameter is omitted, these functions automatically strip a comprehensive set of Unicode whitespace characters, which is an improvement over the standard trim functions that only handle a more limited set of ASCII whitespace [7][6]. Before the introduction of these functions in PHP 8.4, developers typically had to use workarounds like regular expressions with the /u (Unicode) modifier to achieve similar behavior [8].

Citations:


🏁 Script executed:

# Read the file to see lines 56 and 109
cat -n ./Modules/Invoices/Peppol/Clients/BasePeppolClient.php | sed -n '50,115p'

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 2529


🏁 Script executed:

# Check project's PHP version requirement in composer.json
cat composer.json | grep -A5 '"php"'

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 277


Replace mb_rtrim / mb_ltrim with standard rtrim / ltrim functions.

mb_rtrim() and mb_ltrim() are PHP 8.4+ functions, but the project supports PHP 8.2+. Using them causes a fatal error on PHP 8.2 and 8.3. Use the standard rtrim() and ltrim() functions instead.

Suggested change
-        $this->baseUrl = mb_rtrim($baseUrl, '/');
+        $this->baseUrl = rtrim($baseUrl, '/');
-        return $this->baseUrl . '/' . mb_ltrim($path, '/');
+        return $this->baseUrl . '/' . ltrim($path, '/');

Also applies to: 109-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Clients/BasePeppolClient.php` at line 56, The code
uses mb_rtrim and mb_ltrim functions which are only available in PHP 8.4+, but
the project supports PHP 8.2+, causing a fatal error on earlier versions.
Replace all occurrences of mb_rtrim with the standard rtrim function and
mb_ltrim with the standard ltrim function throughout the BasePeppolClient class.
This change appears at multiple locations in the file where these multibyte
versions are currently being used for string trimming operations.

Comment on lines +74 to +78
public function generateXml(Invoice $invoice, array $options = []): string
{
// Implement XML generation logic
return '';
}

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.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

XML generation is unimplemented across handlers, blocking reliable transmission output. Both methods violate the generateXml() contract with placeholder payloads.

  • Modules/Invoices/Peppol/FormatHandlers/CiiHandler.php#L74-L78: implement real CII XML generation (or fail explicitly until implemented), never return empty string.
  • Modules/Invoices/Peppol/FormatHandlers/EhfHandler.php#L89-L95: implement real EHF XML generation (or fail explicitly until implemented), never return JSON from an XML API.
📍 Affects 2 files
  • Modules/Invoices/Peppol/FormatHandlers/CiiHandler.php#L74-L78 (this comment)
  • Modules/Invoices/Peppol/FormatHandlers/EhfHandler.php#L89-L95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/FormatHandlers/CiiHandler.php` around lines 74 - 78,
The generateXml() methods in both handler classes are returning placeholder
values instead of proper XML content. In
Modules/Invoices/Peppol/FormatHandlers/CiiHandler.php at lines 74-78, replace
the empty string return with actual CII XML generation logic based on the
Invoice parameter, or throw an explicit exception if the feature is not yet
implemented. In Modules/Invoices/Peppol/FormatHandlers/EhfHandler.php at lines
89-95, replace the JSON return value with actual EHF XML generation logic that
produces valid XML content, or throw an explicit exception if not yet
implemented. Both methods must honor their generateXml() contract by returning
valid XML strings or failing explicitly, never returning empty strings or JSON
from an XML API method.

Comment on lines +23 to +43
public function transform(Invoice $invoice, string $format): array
{
return [
'invoice_type_code' => $this->getInvoiceTypeCode($invoice),
'invoice_number' => $invoice->number,
'issue_date' => $invoice->invoice_date->format('Y-m-d'),
'due_date' => $invoice->due_date?->format('Y-m-d'),
'currency_code' => config('invoices.peppol.currency_code', 'EUR'),

'supplier' => $this->transformSupplier($invoice),
'customer' => $this->transformCustomer($invoice),
'invoice_lines' => $this->transformInvoiceLines($invoice),
'tax_totals' => $this->transformTaxTotals($invoice),
'monetary_totals' => $this->transformMonetaryTotals($invoice),
'payment_terms' => $this->transformPaymentTerms($invoice),

// Metadata
'format' => $format,
'invoice_id' => $invoice->id,
];
}

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: transformSupplier() method is missing — runtime crash.

Line 32 calls $this->transformSupplier($invoice), but no such method exists in this class. The implementation appears to be accidentally embedded in a docblock (lines 75-87) rather than as executable code. This will throw Error: Call to undefined method when transform() is invoked.

🐛 Fix: Extract the method from the docblock
+    /**
+     * Build an array representing the supplier (company) information for Peppol output.
+     *
+     * `@param` Invoice $invoice the invoice used to source supplier data
+     *
+     * `@return` array Supplier structure with address fields mapped for Peppol.
+     */
+    protected function transformSupplier(Invoice $invoice): array
+    {
+        return [
+            'name'       => config('invoices.peppol.supplier.name', $invoice->company->name ?? ''),
+            'vat_number' => config('invoices.peppol.supplier.vat'),
+            'address'    => [
+                'street'       => config('invoices.peppol.supplier.street'),
+                'city'         => config('invoices.peppol.supplier.city'),
+                'postal_code'  => config('invoices.peppol.supplier.postal'),
+                'country_code' => config('invoices.peppol.supplier.country'),
+            ],
+        ];
+    }
+
     /**
-     * Build an array representing the supplier (company) information for Peppol output.
-     *
-     * `@param` Invoice $invoice the invoice used to source supplier data; company name will fall back to $invoice->company->name when not configured
-     *
-     * `@return` array{
-     *     name: string,
-     *     vat_number: null|string,
-     *     address: array{
-     *         street: null|string,
-     *         city: null|string,
-     *         postal_code: null|string,
-     *         country_code: null|string
-     *     }
-     * } Supplier structure with address fields mapped for Peppol.
-     * protected function transformSupplier(Invoice $invoice): array
-     * {
-     * return [
-     * 'name' => config('invoices.peppol.supplier.name', $invoice->company->name ?? ''),
-     * 'vat_number' => config('invoices.peppol.supplier.vat'),
-     * 'address' => [
-     * 'street' => config('invoices.peppol.supplier.street'),
-     * 'city' => config('invoices.peppol.supplier.city'),
-     * 'postal_code' => config('invoices.peppol.supplier.postal'),
-     * 'country_code' => config('invoices.peppol.supplier.country'),
-     * ],
-     * ];
-     * }
-     *
-     * `@param` Invoice $invoice the invoice containing the customer and address data to transform
+     * Transform customer data for Peppol output.
      ...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function transform(Invoice $invoice, string $format): array
{
return [
'invoice_type_code' => $this->getInvoiceTypeCode($invoice),
'invoice_number' => $invoice->number,
'issue_date' => $invoice->invoice_date->format('Y-m-d'),
'due_date' => $invoice->due_date?->format('Y-m-d'),
'currency_code' => config('invoices.peppol.currency_code', 'EUR'),
'supplier' => $this->transformSupplier($invoice),
'customer' => $this->transformCustomer($invoice),
'invoice_lines' => $this->transformInvoiceLines($invoice),
'tax_totals' => $this->transformTaxTotals($invoice),
'monetary_totals' => $this->transformMonetaryTotals($invoice),
'payment_terms' => $this->transformPaymentTerms($invoice),
// Metadata
'format' => $format,
'invoice_id' => $invoice->id,
];
}
/**
* Build an array representing the supplier (company) information for Peppol output.
*
* `@param` Invoice $invoice the invoice used to source supplier data
*
* `@return` array Supplier structure with address fields mapped for Peppol.
*/
protected function transformSupplier(Invoice $invoice): array
{
return [
'name' => config('invoices.peppol.supplier.name', $invoice->company->name ?? ''),
'vat_number' => config('invoices.peppol.supplier.vat'),
'address' => [
'street' => config('invoices.peppol.supplier.street'),
'city' => config('invoices.peppol.supplier.city'),
'postal_code' => config('invoices.peppol.supplier.postal'),
'country_code' => config('invoices.peppol.supplier.country'),
],
];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Services/PeppolTransformerService.php` around lines
23 - 43, The transform() method calls transformSupplier() on line 32, but this
method does not exist as executable code in the PeppolTransformerService class —
it is currently embedded in a docblock at lines 75-87. Extract the
transformSupplier() implementation from the docblock and create it as a proper
method within the class with the correct visibility (likely private or
protected) and proper PHP syntax to ensure the transform() method can
successfully invoke it at runtime without throwing an undefined method error.

Comment on lines +27 to +29
'invoice_number' => $invoice->number,
'issue_date' => $invoice->invoice_date->format('Y-m-d'),
'due_date' => $invoice->due_date?->format('Y-m-d'),

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Invoice model properties
ast-grep --pattern 'class Invoice {
  $$$
}'

# Also check for property definitions
rg -n 'invoice_number|invoiced_at|number|invoice_date' --type php -g '**/Models/Invoice.php'

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 864


🏁 Script executed:

cat -n Modules/Invoices/Peppol/Services/PeppolTransformerService.php | sed -n '20,35p'

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 975


🏁 Script executed:

rg -n 'due_date' --type php -g '**/Models/Invoice.php'

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 54


🏁 Script executed:

cat -n Modules/Invoices/Models/Invoice.php | head -100

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 4523


Fix property name mismatches in PeppolTransformerService.

The service uses incorrect property names that do not exist in the Invoice model:

  • Line 28: $invoice->invoice_date should be $invoice->invoiced_at
  • Line 29: $invoice->due_date should be $invoice->due_at

These will cause null/undefined access errors at runtime. The model defines these properties as invoiced_at (line 40) and due_at (line 42) respectively.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Peppol/Services/PeppolTransformerService.php` around lines
27 - 29, The PeppolTransformerService is using incorrect property names that
don't match the Invoice model. In the array being constructed, replace the
property name `invoice_date` with `invoiced_at` and replace `due_date` with
`due_at` to correctly reference the Invoice model's properties. Both properties
use the nullable safe operator for formatting, so ensure the format calls are
preserved after updating the property names.

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.

[Invoices]: Peppol e-invoicing — pluggable multi-provider architecture

2 participants