diff --git a/Modules/Invoices/Http/Traits/LogsApiRequests.php b/Modules/Invoices/Http/Traits/LogsApiRequests.php index 49f9aebd7..5893e1459 100644 --- a/Modules/Invoices/Http/Traits/LogsApiRequests.php +++ b/Modules/Invoices/Http/Traits/LogsApiRequests.php @@ -58,7 +58,7 @@ protected function logRequest(string $method, string $uri, array $options): void return; } - Log::info('API Request', [ + Log::info('HTTP Request', [ 'method' => $method, 'uri' => $uri, 'options' => $this->sanitizeForLogging($options), @@ -81,7 +81,7 @@ protected function logResponse(string $method, string $uri, int $status, mixed $ return; } - Log::info('API Response', [ + Log::info('HTTP Response', [ 'method' => $method, 'uri' => $uri, 'status' => $status, @@ -102,7 +102,7 @@ protected function logResponse(string $method, string $uri, int $status, mixed $ */ protected function logError(string $type, string $method, string $uri, string $message, array $context = []): void { - Log::error("API {$type} Error", array_merge([ + Log::error("HTTP {$type} Error", array_merge([ 'method' => $method, 'uri' => $uri, 'message' => $message, diff --git a/Modules/Invoices/Peppol/Clients/BasePeppolClient.php b/Modules/Invoices/Peppol/Clients/BasePeppolClient.php index 4af305bcd..65f0e0157 100644 --- a/Modules/Invoices/Peppol/Clients/BasePeppolClient.php +++ b/Modules/Invoices/Peppol/Clients/BasePeppolClient.php @@ -85,7 +85,15 @@ public function getClient(): HttpClientExceptionHandler */ public function getRequestOptions(array $options = []): array { - // Implement logic or return options as needed + // Merge authentication headers with any existing headers + // Auth headers are merged AFTER existing headers to ensure they take precedence + // and cannot be overridden by caller-provided headers for security + $authHeaders = $this->getAuthenticationHeaders(); + $existingHeaders = $options['headers'] ?? []; + + $options['headers'] = array_merge($existingHeaders, $authHeaders); + $options['timeout'] = $options['timeout'] ?? $this->getTimeout(); + return $options; } diff --git a/Modules/Invoices/Peppol/Enums/PeppolDocumentFormat.php b/Modules/Invoices/Peppol/Enums/PeppolDocumentFormat.php index 33ccdca01..ed32a08cb 100644 --- a/Modules/Invoices/Peppol/Enums/PeppolDocumentFormat.php +++ b/Modules/Invoices/Peppol/Enums/PeppolDocumentFormat.php @@ -159,8 +159,8 @@ public function description(): string self::ZUGFERD_10 => 'German standard combining PDF with embedded XML invoice data.', self::ZUGFERD_20 => 'Updated ZUGFeRD compatible with Factur-X. Uses CII format.', self::OIOUBL => 'Danish UBL-based format with national extensions.', - self::EHF_30 => 'Norwegian EHF 3.0 format for Peppol network.', - self::PEPPOL_BIS_30 => 'Default Peppol format for most countries. Based on UBL.', + self::EHF_30 => 'Norwegian EHF 3.0 format for PEPPOL network.', + self::PEPPOL_BIS_30 => 'Default PEPPOL format for most countries. Based on UBL.', }; } @@ -220,7 +220,8 @@ public function isMandatoryFor(?string $countryCode): bool return match ($this) { self::FATTURAPA_12 => $country === 'IT', - self::FACTURAE_32 => $country === 'ES', // For public administration + // Note: FACTURAE_32 is only mandatory for Spanish public administration + // Not for all invoices in Spain, so we return false default => false, }; } diff --git a/Modules/Invoices/Peppol/FormatHandlers/BaseFormatHandler.php b/Modules/Invoices/Peppol/FormatHandlers/BaseFormatHandler.php index 586d11750..4f6644915 100644 --- a/Modules/Invoices/Peppol/FormatHandlers/BaseFormatHandler.php +++ b/Modules/Invoices/Peppol/FormatHandlers/BaseFormatHandler.php @@ -5,6 +5,7 @@ use Modules\Invoices\Models\Invoice; use Modules\Invoices\Peppol\Enums\PeppolDocumentFormat; use Modules\Invoices\Peppol\Enums\PeppolEndpointScheme; +use RuntimeException; /** * BaseFormatHandler - Abstract base class for invoice format handlers. @@ -17,16 +18,32 @@ abstract class BaseFormatHandler implements InvoiceFormatHandlerInterface /** * The format this handler supports. * - * @var PeppolDocumentFormat + * @var PeppolDocumentFormat|null */ - protected PeppolDocumentFormat $format; + protected ?PeppolDocumentFormat $format = null; /** * Constructor. * + * @param PeppolDocumentFormat|null $format The format this handler supports (optional) + */ + public function __construct(?PeppolDocumentFormat $format = null) + { + if ($format !== null) { + $this->format = $format; + } + } + + /** + * Set the format for this handler. + * + * This method is called by the factory after instantiation. + * * @param PeppolDocumentFormat $format The format this handler supports + * + * @return void */ - public function __construct(PeppolDocumentFormat $format) + public function setFormat(PeppolDocumentFormat $format): void { $this->format = $format; } @@ -45,6 +62,10 @@ abstract protected function validateFormatSpecific(Invoice $invoice): array; */ public function getFormat(): PeppolDocumentFormat { + if ($this->format === null) { + throw new RuntimeException('Format has not been set on this handler. Call setFormat() first.'); + } + return $this->format; } @@ -53,18 +74,20 @@ public function getFormat(): PeppolDocumentFormat */ public function supports(Invoice $invoice): bool { + $format = $this->getFormat(); + // Check if customer's country matches format requirements $customerCountry = $invoice->customer?->country_code ?? null; // Mandatory formats must be used for their countries - if ($this->format->isMandatoryFor($customerCountry)) { + if ($format->isMandatoryFor($customerCountry)) { return true; } // Check if format is suitable for customer's country $suitableFormats = PeppolDocumentFormat::formatsForCountry($customerCountry); - return in_array($this->format, $suitableFormats, true); + return in_array($format, $suitableFormats, true); } /** @@ -106,7 +129,9 @@ public function validate(Invoice $invoice): array */ public function getMimeType(): string { - return $this->format->requiresPdfEmbedding() + $format = $this->getFormat(); + + return $format->requiresPdfEmbedding() ? 'application/pdf' : 'application/xml'; } @@ -116,7 +141,7 @@ public function getMimeType(): string */ public function getFileExtension(): string { - return $this->format->extension(); + return $this->getFormat()->extension(); } /** diff --git a/Modules/Invoices/Peppol/FormatHandlers/FormatHandlerFactory.php b/Modules/Invoices/Peppol/FormatHandlers/FormatHandlerFactory.php index 6e45c044e..8b8f6c169 100644 --- a/Modules/Invoices/Peppol/FormatHandlers/FormatHandlerFactory.php +++ b/Modules/Invoices/Peppol/FormatHandlers/FormatHandlerFactory.php @@ -53,7 +53,14 @@ public static function create(PeppolDocumentFormat $format): InvoiceFormatHandle throw new RuntimeException("No handler available for format: {$format->value}"); } - return app($handlerClass); + /** @var BaseFormatHandler $handler */ + $handler = app($handlerClass); + + // Set the format on the handler to ensure it matches what was requested + // This is especially important for handlers that can handle multiple formats (UBL, ZUGFeRD) + $handler->setFormat($format); + + return $handler; } /** diff --git a/Modules/Invoices/Tests/Unit/Http/Decorators/HttpClientExceptionHandlerTest.php b/Modules/Invoices/Tests/Unit/Http/Decorators/HttpClientExceptionHandlerTest.php index 23c6b4ced..6a4d02ae4 100644 --- a/Modules/Invoices/Tests/Unit/Http/Decorators/HttpClientExceptionHandlerTest.php +++ b/Modules/Invoices/Tests/Unit/Http/Decorators/HttpClientExceptionHandlerTest.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Mockery; -use Modules\Core\Tests\TestCase; +use Modules\Core\Tests\AbstractTestCase; use Modules\Invoices\Http\Clients\ApiClient; use Modules\Invoices\Http\Decorators\HttpClientExceptionHandler; use Modules\Invoices\Http\RequestMethod; @@ -22,7 +22,7 @@ * Uses HTTP fakes to simulate various scenarios. */ #[Group('peppol')] -class HttpClientExceptionHandlerTest extends TestCase +class HttpClientExceptionHandlerTest extends AbstractTestCase { protected HttpClientExceptionHandler $handler; @@ -212,14 +212,6 @@ public function it_sanitizes_auth_credentials_in_logs(): void })); } - #[Test] - #[Group('http_client_failing')] - public function it_forwards_method_calls_to_wrapped_client(): void - { - // This test is not applicable since ApiClient only exposes request(). - $this->assertTrue(true); - } - #[Test] #[Group('http_client_failing')] #[Group('failing')] @@ -370,7 +362,7 @@ public function it_handles_http_exceptions(): void 'https://api.example.com/*' => Http::response(['error' => 'Not Found'], 404), ]); - /* act & assert */ + /* Act & Assert */ $this->expectException(RequestException::class); $this->handler->request(RequestMethod::GET, 'test'); } diff --git a/Modules/Invoices/Tests/Unit/Peppol/Clients/DocumentsClientTest.php b/Modules/Invoices/Tests/Unit/Peppol/Clients/DocumentsClientTest.php index dada36cb4..8a489f10c 100644 --- a/Modules/Invoices/Tests/Unit/Peppol/Clients/DocumentsClientTest.php +++ b/Modules/Invoices/Tests/Unit/Peppol/Clients/DocumentsClientTest.php @@ -4,7 +4,7 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Support\Facades\Http; -use Modules\Core\Tests\TestCase; +use Modules\Core\Tests\AbstractTestCase; use Modules\Invoices\Http\Clients\ApiClient; use Modules\Invoices\Http\Decorators\HttpClientExceptionHandler; use Modules\Invoices\Peppol\Clients\EInvoiceBe\DocumentsClient; @@ -18,7 +18,7 @@ * Verifies proper API integration and error handling. */ #[Group('peppol')] -class DocumentsClientTest extends TestCase +class DocumentsClientTest extends AbstractTestCase { protected DocumentsClient $client; diff --git a/pint_output.log b/pint_output.log new file mode 100644 index 000000000..79f26c47c --- /dev/null +++ b/pint_output.log @@ -0,0 +1,5 @@ + + + No dirty files found. + +