Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3e1c7d0
Initial plan
Copilot Sep 30, 2025
f5815b0
Add comprehensive Peppol integration with HTTP clients, services, act…
Copilot Sep 30, 2025
ff9a27d
Add comprehensive Peppol documentation and example configuration
Copilot Sep 30, 2025
f712c25
Add decorator tests and complete documentation with files summary
Copilot Sep 30, 2025
88add1e
Add Peppol fields to relations table and create supporting enums and …
Copilot Oct 1, 2025
7697e55
Refactor HTTP client architecture: simplify to single request() metho…
Copilot Oct 1, 2025
4f20516
Update Modules/Invoices/Providers/InvoicesServiceProvider.php
nielsdrost7 Oct 1, 2025
5289ec7
Update Modules/Invoices/Tests/Unit/Peppol/Services/PeppolServiceTest.php
nielsdrost7 Oct 1, 2025
b24d423
Update Modules/Invoices/Tests/Unit/Actions/SendInvoiceToPeppolActionT…
nielsdrost7 Oct 1, 2025
37c2454
Expand Peppol infrastructure: comprehensive format enum, endpoint sch…
Copilot Oct 1, 2025
bd5c4b5
Implement Strategy Pattern for format handlers with PEPPOL BIS and UB…
Copilot Oct 1, 2025
0c80842
Implement complete e-invoice.be API: Participants, Tracking, Webhooks…
Copilot Oct 1, 2025
bd4c6c9
Add comprehensive implementation summary documenting all Peppol featu…
Copilot Oct 1, 2025
3ae8ddf
Implement CII format handler with comprehensive UN/CEFACT standard co…
Copilot Oct 1, 2025
5378a81
Fix ApiClient references across test suite and register CII handler
Copilot Oct 1, 2025
0d8cca0
Add comprehensive tests for PeppolDocumentFormat enum with 15 test cases
Copilot Oct 1, 2025
4bc0c0b
Update Modules/Clients/Database/Migrations/2025_10_01_002042_add_pepp…
nielsdrost7 Oct 2, 2025
ba153f1
Update Modules/Invoices/Peppol/Clients/EInvoiceBe/EInvoiceBeClient.php
nielsdrost7 Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('relations', function (Blueprint $table) {
$table->string('peppol_id', 100)->nullable()->after('vat_number')
->comment('Peppol participant identifier (e.g., BE:0123456789)');

$table->string('peppol_format', 20)->nullable()->after('peppol_id')
->default('ubl')
->comment('Preferred Peppol document format (ubl or cii)');

$table->boolean('enable_e_invoicing')->default(false)->after('peppol_format')
->comment('Whether e-invoicing via Peppol is enabled for this customer');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('relations', function (Blueprint $table) {
$table->dropColumn(['peppol_id', 'peppol_format', 'enable_e_invoicing']);
});
}
};
8 changes: 6 additions & 2 deletions Modules/Clients/Models/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
* @property string|null $id_number
* @property string|null $coc_number
* @property string|null $vat_number
* @property string|null $peppol_id
* @property string|null $peppol_format
* @property bool $enable_e_invoicing
* @property Carbon $registered_at
* @property mixed $created_at
* @property mixed $updated_at
Expand All @@ -62,8 +65,9 @@ class Relation extends Model
protected $table = 'relations';

protected $casts = [
'relation_type' => RelationType::class,
'relation_status' => RelationStatus::class,
'relation_type' => RelationType::class,
'relation_status' => RelationStatus::class,
'enable_e_invoicing' => 'boolean',
];

protected $guarded = [];
Expand Down
111 changes: 111 additions & 0 deletions Modules/Invoices/Actions/SendInvoiceToPeppolAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace Modules\Invoices\Actions;

use Illuminate\Http\Client\RequestException;
use Modules\Invoices\Models\Invoice;
use Modules\Invoices\Peppol\Services\PeppolService;

/**
* SendInvoiceToPeppolAction - Action for sending invoices to Peppol network.
*
* This action handles the process of gathering invoice information and
* sending it to the Peppol network through the PeppolService. It provides
* a clean interface for both the EditInvoice page and the ListInvoices table.
*
* @package Modules\Invoices\Actions
*/
class SendInvoiceToPeppolAction
{
/**
* The Peppol service instance.
*
* @var PeppolService
*/
protected PeppolService $peppolService;

/**
* Constructor.
*
* @param PeppolService $peppolService The Peppol service
*/
public function __construct(PeppolService $peppolService)
{
$this->peppolService = $peppolService;
}

/**
* Execute the action to send an invoice to Peppol.
*
* This method gathers all necessary information from the invoice and
* submits it to the Peppol network. It returns the result of the operation.
*
* @param Invoice $invoice The invoice to send
* @param array<string, mixed> $additionalData Optional additional data (e.g., Peppol ID)
* @return array<string, mixed> The result of the operation
*
* @throws RequestException If the Peppol API request fails
* @throws \InvalidArgumentException If the invoice data is invalid
*/
public function execute(Invoice $invoice, array $additionalData = []): array
{
// Load necessary relationships
$invoice->load(['customer', 'invoiceItems']);

// Validate that invoice is in a state that can be sent
$this->validateInvoiceState($invoice);

// Send to Peppol
$result = $this->peppolService->sendInvoiceToPeppol($invoice, $additionalData);

// Optionally, you could update the invoice record here
// to track that it was sent to Peppol (e.g., add a peppol_document_id field)
// $invoice->update(['peppol_document_id' => $result['document_id']]);

return $result;
}

/**
* Validate that the invoice is in a valid state for Peppol transmission.
*
* @param Invoice $invoice The invoice to validate
* @return void
*
* @throws \InvalidArgumentException If validation fails
*/
protected function validateInvoiceState(Invoice $invoice): void
{
// Check if invoice is in draft status - drafts should not be sent
if ($invoice->invoice_status === 'draft') {
throw new \InvalidArgumentException('Cannot send draft invoices to Peppol');
}

// Additional business logic validation can be added here
}

/**
* Get the status of a previously sent invoice from Peppol.
*
* @param string $documentId The Peppol document ID
* @return array<string, mixed> Status information
*
* @throws RequestException If the API request fails
*/
public function getStatus(string $documentId): array
{
return $this->peppolService->getDocumentStatus($documentId);
}

/**
* Cancel a Peppol document transmission.
*
* @param string $documentId The Peppol document ID
* @return bool True if cancellation was successful
*
* @throws RequestException If the API request fails
*/
public function cancel(string $documentId): bool
{
return $this->peppolService->cancelDocument($documentId);
}
}
68 changes: 68 additions & 0 deletions Modules/Invoices/Config/config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Peppol Configuration
|--------------------------------------------------------------------------
|
| Configuration for Peppol e-invoicing integration.
| Different providers can be configured here.
|
*/

'peppol' => [
/*
|--------------------------------------------------------------------------
| Default Peppol Provider
|--------------------------------------------------------------------------
|
| The default Peppol access point provider to use.
| Supported: "e_invoice_be", "storecove", "custom"
|
*/
'default_provider' => env('PEPPOL_PROVIDER', 'e_invoice_be'),

/*
|--------------------------------------------------------------------------
| E-Invoice.be Configuration
|--------------------------------------------------------------------------
|
| Configuration for the e-invoice.be Peppol access point.
| See: https://api.e-invoice.be/docs
|
*/
'e_invoice_be' => [
'api_key' => env('PEPPOL_E_INVOICE_BE_API_KEY', ''),
'base_url' => env('PEPPOL_E_INVOICE_BE_BASE_URL', 'https://api.e-invoice.be'),
],

/*
|--------------------------------------------------------------------------
| Peppol Document Settings
|--------------------------------------------------------------------------
|
| Default settings for Peppol documents.
|
*/
'document' => [
'currency_code' => env('PEPPOL_CURRENCY_CODE', 'EUR'),
'default_unit_code' => 'C62', // Unit (piece)
'default_endpoint_scheme' => 'BE:CBE', // Belgian company number scheme
],

/*
|--------------------------------------------------------------------------
| Validation Settings
|--------------------------------------------------------------------------
|
| Settings for validating invoices before sending to Peppol.
|
*/
'validation' => [
'require_customer_peppol_id' => true,
'require_vat_number' => false,
'min_invoice_amount' => 0,
],
Comment on lines +37 to +158
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cast env-driven scalars to their intended types.

Every boolean/timeout setting here pulls straight from env(), so values like "false" or "120" remain strings. In PHP, 'false' is truthy, meaning operators can’t disable features or validation. Please normalize the inputs (e.g., filter_var(..., FILTER_VALIDATE_BOOL, ...) for booleans, (int) for timeouts) so configuration behaves as expected when toggled via .env.

-            'timeout' => env('PEPPOL_E_INVOICE_BE_TIMEOUT', 30),
+            'timeout' => (int) env('PEPPOL_E_INVOICE_BE_TIMEOUT', 30),
...
-            'require_customer_peppol_id' => env('PEPPOL_REQUIRE_PEPPOL_ID', true),
+            'require_customer_peppol_id' => filter_var(env('PEPPOL_REQUIRE_PEPPOL_ID', true), FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true,
...
-            'enable_tracking' => env('PEPPOL_ENABLE_TRACKING', true),
+            'enable_tracking' => filter_var(env('PEPPOL_ENABLE_TRACKING', true), FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true,

(Apply the same casting pattern to every boolean/number pulled from env.)

📝 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
'api_key' => env('PEPPOL_E_INVOICE_BE_API_KEY', ''),
'base_url' => env('PEPPOL_E_INVOICE_BE_BASE_URL', 'https://api.e-invoice.be'),
'timeout' => env('PEPPOL_E_INVOICE_BE_TIMEOUT', 30),
],
/*
|--------------------------------------------------------------------------
| Peppol Document Settings
|--------------------------------------------------------------------------
|
| Default settings for Peppol documents.
| These can be overridden per company or per invoice.
|
*/
'document' => [
// Currency settings
'currency_code' => env('PEPPOL_CURRENCY_CODE', 'EUR'),
'fallback_currency' => 'EUR',
// Unit codes (UN/CEFACT)
'default_unit_code' => env('PEPPOL_UNIT_CODE', 'C62'), // C62 = Unit (piece)
// Endpoint scheme settings
'endpoint_scheme_by_country' => [
'BE' => 'BE:CBE',
'DE' => 'DE:VAT',
'FR' => 'FR:SIRENE',
'IT' => 'IT:VAT',
'ES' => 'ES:VAT',
'NL' => 'NL:KVK',
'NO' => 'NO:ORGNR',
'DK' => 'DK:CVR',
'SE' => 'SE:ORGNR',
'FI' => 'FI:OVT',
'AT' => 'AT:VAT',
'CH' => 'CH:UIDB',
'GB' => 'GB:COH',
],
'default_endpoint_scheme' => env('PEPPOL_ENDPOINT_SCHEME', 'ISO_6523'),
],
/*
|--------------------------------------------------------------------------
| Supplier (Company) Configuration
|--------------------------------------------------------------------------
|
| Default supplier details for invoices.
| These will be pulled from company settings when available.
|
*/
'supplier' => [
'company_name' => env('PEPPOL_SUPPLIER_NAME', config('app.name')),
'vat_number' => env('PEPPOL_SUPPLIER_VAT', ''),
'street_name' => env('PEPPOL_SUPPLIER_STREET', ''),
'city_name' => env('PEPPOL_SUPPLIER_CITY', ''),
'postal_zone' => env('PEPPOL_SUPPLIER_POSTAL', ''),
'country_code' => env('PEPPOL_SUPPLIER_COUNTRY', ''),
'contact_name' => env('PEPPOL_SUPPLIER_CONTACT', ''),
'contact_phone' => env('PEPPOL_SUPPLIER_PHONE', ''),
'contact_email' => env('PEPPOL_SUPPLIER_EMAIL', ''),
],
/*
|--------------------------------------------------------------------------
| Format Configuration
|--------------------------------------------------------------------------
|
| Configuration for different e-invoice formats.
|
*/
'formats' => [
'default_format' => env('PEPPOL_DEFAULT_FORMAT', 'peppol_bis_3.0'),
// Country-specific mandatory formats
'mandatory_formats_by_country' => [
'IT' => 'fatturapa_1.2', // Italy requires FatturaPA
'ES' => 'facturae_3.2', // Spain requires Facturae for public sector
],
// Format-specific settings
'ubl' => [
'version' => '2.1',
'customization_id' => 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0',
],
'cii' => [
'version' => '16B',
'profile' => 'EN16931',
],
],
/*
|--------------------------------------------------------------------------
| Validation Settings
|--------------------------------------------------------------------------
|
| Settings for validating invoices before sending to Peppol.
|
*/
'validation' => [
'require_customer_peppol_id' => env('PEPPOL_REQUIRE_PEPPOL_ID', true),
'require_vat_number' => env('PEPPOL_REQUIRE_VAT', false),
'min_invoice_amount' => env('PEPPOL_MIN_AMOUNT', 0),
'validate_format_compliance' => env('PEPPOL_VALIDATE_FORMAT', true),
],
/*
|--------------------------------------------------------------------------
| Feature Flags
|--------------------------------------------------------------------------
|
| Enable or disable specific Peppol features.
|
*/
'features' => [
'enable_tracking' => env('PEPPOL_ENABLE_TRACKING', true),
'enable_webhooks' => env('PEPPOL_ENABLE_WEBHOOKS', false),
'enable_participant_search' => env('PEPPOL_ENABLE_PARTICIPANT_SEARCH', true),
'enable_health_checks' => env('PEPPOL_ENABLE_HEALTH_CHECKS', true),
'auto_retry_failed' => env('PEPPOL_AUTO_RETRY', false),
'max_retries' => env('PEPPOL_MAX_RETRIES', 3),
],
// … earlier settings …
'timeout' => (int) env('PEPPOL_E_INVOICE_BE_TIMEOUT', 30),
// … other settings …
'validation' => [
'require_customer_peppol_id' => filter_var(
env('PEPPOL_REQUIRE_PEPPOL_ID', true),
FILTER_VALIDATE_BOOL,
FILTER_NULL_ON_FAILURE
) ?? true,
'require_vat_number' => filter_var(
env('PEPPOL_REQUIRE_VAT', false),
FILTER_VALIDATE_BOOL,
FILTER_NULL_ON_FAILURE
) ?? false,
'min_invoice_amount' => (int) env('PEPPOL_MIN_AMOUNT', 0),
'validate_format_compliance' => filter_var(
env('PEPPOL_VALIDATE_FORMAT', true),
FILTER_VALIDATE_BOOL,
FILTER_NULL_ON_FAILURE
) ?? true,
],
// … other settings …
'features' => [
'enable_tracking' => filter_var(
env('PEPPOL_ENABLE_TRACKING', true),
FILTER_VALIDATE_BOOL,
FILTER_NULL_ON_FAILURE
) ?? true,
'enable_webhooks' => filter_var(
env('PEPPOL_ENABLE_WEBHOOKS', false),
FILTER_VALIDATE_BOOL,
FILTER_NULL_ON_FAILURE
) ?? false,
'enable_participant_search' => filter_var(
env('PEPPOL_ENABLE_PARTICIPANT_SEARCH', true),
FILTER_VALIDATE_BOOL,
FILTER_NULL_ON_FAILURE
) ?? true,
'enable_health_checks' => filter_var(
env('PEPPOL_ENABLE_HEALTH_CHECKS', true),
FILTER_VALIDATE_BOOL,
FILTER_NULL_ON_FAILURE
) ?? true,
'auto_retry_failed' => filter_var(
env('PEPPOL_AUTO_RETRY', false),
FILTER_VALIDATE_BOOL,
FILTER_NULL_ON_FAILURE
) ?? false,
'max_retries' => (int) env('PEPPOL_MAX_RETRIES', 3),
],
// … remaining settings …
🤖 Prompt for AI Agents
In Modules/Invoices/Config/config.php around lines 37 to 158, env() values for
booleans and numeric/timeout settings are left as raw strings which makes flags
and numeric settings behave incorrectly; update each env() call that represents
a boolean to use PHP boolean normalization (e.g. filter_var(...,
FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) or FILTER_VALIDATE_BOOL) and
cast numeric/timeout values to integers (e.g. (int) env(...)) so keys like
peppol api timeout, document fallback/default settings if numeric, validation
flags (require_customer_peppol_id, require_vat_number,
validate_format_compliance), feature flags (enable_tracking, enable_webhooks,
enable_participant_search, enable_health_checks, auto_retry_failed) and numeric
fields (timeout, min_invoice_amount, max_retries) return proper bool/int types
from env; apply this casting pattern consistently for every env-driven
boolean/number in this block.

],
];
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

namespace Modules\Invoices\Filament\Company\Resources\Invoices\Pages;

use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
use Modules\Invoices\Actions\SendInvoiceToPeppolAction;
use Modules\Invoices\Filament\Company\Resources\Invoices\InvoiceResource;
use Modules\Invoices\Services\InvoiceService;

Expand Down Expand Up @@ -49,6 +53,38 @@ protected function handleRecordUpdate(Model $record, array $data): Model
protected function getHeaderActions(): array
{
return [
Action::make('send_to_peppol')
->label(trans('ip.send_to_peppol'))
->icon('heroicon-o-paper-airplane')
->color('success')
->requiresConfirmation()
->form([
TextInput::make('customer_peppol_id')
->label(trans('ip.customer_peppol_id'))
->helperText(trans('ip.customer_peppol_id_helper'))
->placeholder('BE:0123456789')
->required(),
])
->action(function (array $data) {
try {
$action = app(SendInvoiceToPeppolAction::class);
$result = $action->execute($this->getRecord(), $data);

Notification::make()
->title(trans('ip.peppol_success_title'))
->body(trans('ip.peppol_success_body', [
'document_id' => $result['document_id'] ?? 'N/A',
]))
->success()
->send();
} catch (\Exception $e) {
Notification::make()
->title(trans('ip.peppol_error_title'))
->body(trans('ip.peppol_error_body', ['error' => $e->getMessage()]))
->danger()
->send();
}
}),
DeleteAction::make(),
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Modules\Core\Support\DateHelpers;
use Modules\Invoices\Actions\SendInvoiceToPeppolAction;
use Modules\Invoices\Enums\InvoiceStatus;
use Modules\Invoices\Models\Invoice;
use Modules\Invoices\Services\InvoiceService;
Expand Down Expand Up @@ -121,6 +123,38 @@ public static function configure(Table $table): Table
->success()
->send();
}),
Action::make('send_to_peppol')
->label(trans('ip.send_to_peppol'))
->icon('heroicon-o-paper-airplane')
->color('success')
->requiresConfirmation()
->form([
TextInput::make('customer_peppol_id')
->label(trans('ip.customer_peppol_id'))
->helperText(trans('ip.customer_peppol_id_helper'))
->placeholder('BE:0123456789')
->required(),
])
->action(function (Invoice $record, array $data): void {
try {
$action = app(SendInvoiceToPeppolAction::class);
$result = $action->execute($record, $data);

\Filament\Notifications\Notification::make()
->title(trans('ip.peppol_success_title'))
->body(trans('ip.peppol_success_body', [
'document_id' => $result['document_id'] ?? 'N/A',
]))
->success()
->send();
} catch (\Exception $e) {
\Filament\Notifications\Notification::make()
->title(trans('ip.peppol_error_title'))
->body(trans('ip.peppol_error_body', ['error' => $e->getMessage()]))
->danger()
->send();
}
}),
DeleteAction::make('delete')
->action(function (Invoice $record, array $data) {
app(InvoiceService::class)->deleteInvoice($record);
Expand Down
Loading