Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
114 changes: 114 additions & 0 deletions Modules/Invoices/Actions/SendInvoiceToPeppolAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

namespace Modules\Invoices\Actions;

use Illuminate\Http\Client\RequestException;
use InvalidArgumentException;
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.
*/
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;
}

/**
* 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);
}

/**
* 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
}
}
233 changes: 233 additions & 0 deletions Modules/Invoices/Config/config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<?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
| SDK: https://github.com/e-invoice-be/e-invoice-php
|
*/
'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'),
'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', true),
'max_retries' => env('PEPPOL_MAX_RETRIES', 5),
],

/*
|--------------------------------------------------------------------------
| Country to Scheme Mapping
|--------------------------------------------------------------------------
|
| Mapping of country codes to default Peppol endpoint schemes.
| Used for auto-suggesting the appropriate scheme when onboarding customers.
|
*/
'country_scheme_mapping' => [
'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',
],

/*
|--------------------------------------------------------------------------
| Retry Policy
|--------------------------------------------------------------------------
|
| Configuration for automatic retries of failed transmissions.
| Uses exponential backoff strategy.
|
*/
'retry' => [
'max_attempts' => env('PEPPOL_MAX_RETRY_ATTEMPTS', 5),
'backoff_delays' => [60, 300, 1800, 7200, 21600], // 1min, 5min, 30min, 2h, 6h
'retry_transient_errors' => true,
'retry_unknown_errors' => true,
'retry_permanent_errors' => false,
],

/*
|--------------------------------------------------------------------------
| Storage Configuration
|--------------------------------------------------------------------------
|
| Configuration for storing Peppol artifacts (XML, PDF).
|
*/
'storage' => [
'disk' => env('PEPPOL_STORAGE_DISK', 'local'),
'path_template' => 'peppol/{integration_id}/{year}/{month}/{transmission_id}',
'retention_days' => env('PEPPOL_RETENTION_DAYS', 2555), // 7 years default
],

/*
|--------------------------------------------------------------------------
| Monitoring & Alerting
|--------------------------------------------------------------------------
|
| Thresholds and settings for monitoring Peppol operations.
|
*/
'monitoring' => [
'alert_on_dead_transmission' => true,
'dead_transmission_threshold' => 10, // Alert if > 10 dead in 1 hour
'alert_on_auth_failure' => true,
'status_check_interval' => 15, // minutes
'reconciliation_interval' => 60, // minutes
'old_transmission_threshold' => 168, // hours (7 days)
],
],
];
42 changes: 42 additions & 0 deletions Modules/Invoices/Console/Commands/PollPeppolStatusCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Modules\Invoices\Console\Commands;

use Exception;
use Illuminate\Console\Command;
use Modules\Invoices\Jobs\Peppol\PeppolStatusPoller;

/**
* Console command to poll Peppol transmission statuses.
*
* Should be scheduled to run periodically (e.g., every 15 minutes)
* Add to schedule: $schedule->command('peppol:poll-status')->everyFifteenMinutes();
*/
class PollPeppolStatusCommand extends Command
{
protected $signature = 'peppol:poll-status';

protected $description = 'Poll Peppol provider for transmission status updates';

/**
* Triggers a background job to poll Peppol transmission statuses and reports the result.
*
* @return int exit code: `self::SUCCESS` if the polling job was dispatched successfully, `self::FAILURE` if dispatch failed
*/
public function handle(): int
{
$this->info('Starting Peppol status polling...');

try {
PeppolStatusPoller::dispatch();

$this->info('Peppol status polling job dispatched successfully.');

return self::SUCCESS;
} catch (Exception $e) {
$this->error('Failed to dispatch status polling job: ' . $e->getMessage());

return self::FAILURE;
}
}
}
Loading