From 833c06c7182458c2cc0648b3398437de65f968eb Mon Sep 17 00:00:00 2001 From: jekkos Date: Wed, 4 Mar 2026 19:52:20 +0000 Subject: [PATCH] Add Peppol (UBL) invoice support for Phase 1 Implementation of UBL 2.1 invoice generation to comply with Belgium's 2026 Peppol mandate. Key changes: - Add num-num/ubl-invoice dependency via composer.json - Create Ubl_generator library to convert OSPOS sale data to UBL format - Create country_helper.php to map country names to ISO 3166-1 alpha-2 codes - Extend Email_lib to support multiple attachments for PDF+UBL emails - Add getUblInvoice() method in Sales controller for UBL download - Modify getSendPdf() to optionally attach UBL based on invoice_format config - Add database migration for invoice_format configuration (pdf_only/ubl_only/both) - Add UBL download button to invoice view - Add UBL download link to sales manage table - Add language keys for UBL-related UI elements Data mapping: - Company name/address -> Supplier Party - account_number -> Company VAT number - Customer address/country -> Customer Party with ISO country code - Customer tax_id -> Customer VAT number - Cart items -> InvoiceLines - Taxes -> TaxCategory and TaxTotal - Totals -> LegalMonetaryTotal Features: - Generate valid UBL 2.1 XML invoices - Download UBL from invoice view and manage table - Email with PDF, UBL, or both based on configuration - Support for multiple customer countries with ISO code mapping - Graceful handling of missing optional customer fields --- app/Controllers/Sales.php | 72 +- .../20260304000000_addUblConfig.php | 24 + app/Helpers/country_helper.php | 229 +++ app/Helpers/tabular_helper.php | 14 +- app/Language/ar-EG/Sales.php | 1 + app/Language/ar-LB/Sales.php | 1 + app/Language/az/Sales.php | 1 + app/Language/bg/Sales.php | 1 + app/Language/bs/Sales.php | 1 + app/Language/ckb/Sales.php | 1 + app/Language/cs/Sales.php | 1 + app/Language/da/Sales.php | 1 + app/Language/de-CH/Sales.php | 1 + app/Language/el/Sales.php | 1 + app/Language/en-GB/Sales.php | 1 + app/Language/en/Sales.php | 4 + app/Language/es-MX/Sales.php | 1 + app/Language/fa/Sales.php | 1 + app/Language/he/Sales.php | 1 + app/Language/hr-HR/Sales.php | 1 + app/Language/hu/Sales.php | 1 + app/Language/hy/Sales.php | 1 + app/Language/id/Sales.php | 1 + app/Language/lo/Sales.php | 1 + app/Language/nb/Sales.php | 1 + app/Language/nl-BE/Sales.php | 1 + app/Language/ro/Sales.php | 1 + app/Language/ru/Sales.php | 1 + app/Language/sv/Sales.php | 1 + app/Language/sw-KE/Sales.php | 1 + app/Language/sw-TZ/Sales.php | 1 + app/Language/th/Sales.php | 1 + app/Language/tl/Sales.php | 1 + app/Language/tr/Sales.php | 1 + app/Language/uk/Sales.php | 1 + app/Language/vi/Sales.php | 1 + app/Language/zh-Hans/Sales.php | 1 + app/Language/zh-Hant/Sales.php | 1 + app/Libraries/Email_lib.php | 33 + .../InvoiceAttachment/InvoiceAttachment.php | 40 + .../InvoiceAttachmentGenerator.php | 75 + .../InvoiceAttachment/PdfAttachment.php | 61 + .../InvoiceAttachment/UblAttachment.php | 69 + app/Libraries/UBLGenerator.php | 243 +++ app/Views/sales/invoice.php | 1 + composer.json | 1 + composer.lock | 1717 +++++++++++++---- .../InvoiceAttachmentGeneratorTest.php | 121 ++ .../InvoiceAttachment/PdfAttachmentTest.php | 70 + .../InvoiceAttachment/UblAttachmentTest.php | 103 + 50 files changed, 2500 insertions(+), 410 deletions(-) create mode 100644 app/Database/Migrations/20260304000000_addUblConfig.php create mode 100644 app/Helpers/country_helper.php create mode 100644 app/Libraries/InvoiceAttachment/InvoiceAttachment.php create mode 100644 app/Libraries/InvoiceAttachment/InvoiceAttachmentGenerator.php create mode 100644 app/Libraries/InvoiceAttachment/PdfAttachment.php create mode 100644 app/Libraries/InvoiceAttachment/UblAttachment.php create mode 100644 app/Libraries/UBLGenerator.php create mode 100644 tests/Libraries/InvoiceAttachment/InvoiceAttachmentGeneratorTest.php create mode 100644 tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php create mode 100644 tests/Libraries/InvoiceAttachment/UblAttachmentTest.php diff --git a/app/Controllers/Sales.php b/app/Controllers/Sales.php index c618a82e0..1a273120c 100644 --- a/app/Controllers/Sales.php +++ b/app/Controllers/Sales.php @@ -4,9 +4,11 @@ namespace App\Controllers; use App\Libraries\Barcode_lib; use App\Libraries\Email_lib; +use App\Libraries\InvoiceAttachment\InvoiceAttachmentGenerator; use App\Libraries\Sale_lib; use App\Libraries\Tax_lib; use App\Libraries\Token_lib; +use App\Libraries\UBLGenerator; use App\Models\Customer; use App\Models\Customer_rewards; use App\Models\Dinner_table; @@ -904,15 +906,28 @@ class Sales extends Secure_Controller $text = $this->token_lib->render($text, $tokens); $sale_data['mimetype'] = mime_content_type(FCPATH . 'uploads/' . $this->config['company_logo']); - // Generate email attachment: invoice in PDF format - $view = Services::renderer(); - $html = $view->setData($sale_data)->render("sales/$type" . '_email', $sale_data); + // Add config and customer object for attachment generation + $sale_data['config'] = $this->config; + $customer_id = $this->sale_lib->get_customer(); + if ($customer_id && $customer_id != NEW_ENTRY) { + $sale_data['customer_object'] = $this->customer->get_info($customer_id); + } - // Load PDF helper - helper(['dompdf', 'file']); - $filename = sys_get_temp_dir() . '/' . lang('Sales.' . $type) . '-' . str_replace('/', '-', $number) . '.pdf'; - if (file_put_contents($filename, create_pdf($html)) !== false) { - $result = $this->email_lib->sendEmail($to, $subject, $text, $filename); + // Generate attachments based on config + $invoiceFormat = $this->config['invoice_format'] ?? 'pdf_only'; + $attachmentGenerator = InvoiceAttachmentGenerator::createFromConfig($invoiceFormat); + $attachments = $attachmentGenerator->generateAttachments($sale_data, $type); + + if (!empty($attachments)) { + try { + if (count($attachments) === 1) { + $result = $this->email_lib->sendEmail($to, $subject, $text, $attachments[0]); + } else { + $result = $this->email_lib->sendMultipleAttachments($to, $subject, $text, $attachments); + } + } finally { + InvoiceAttachmentGenerator::cleanup($attachments); + } } $message = lang($result ? "Sales." . $type . "_sent" : "Sales." . $type . "_unsent") . ' ' . $to; @@ -1273,6 +1288,47 @@ class Sales extends Secure_Controller return view('sales/' . $data['invoice_view'], $data); } + /** + * Generate and download UBL invoice + * + * @param int $sale_id + * @return ResponseInterface + */ + public function getUblInvoice($sale_id): ResponseInterface + { + $sale_info = $this->sale->get_info($sale_id)->getRowArray(); + + if (empty($sale_info) || empty($sale_info['invoice_number'])) { + return $this->response->setStatusCode(404)->setBody(lang('Sales.sale_not_found')); + } + + try { + $sale_data = $this->_load_sale_data($sale_id); + $sale_data['config'] = $this->config; + $customer_id = $this->sale_lib->get_customer(); + if ($customer_id && $customer_id != NEW_ENTRY) { + $sale_data['customer_object'] = $this->customer->get_info($customer_id); + } + $ublGenerator = new UBLGenerator(); + $xml = $ublGenerator->generateUblInvoice($sale_data); + + $rawFilename = lang('Sales.invoice') . '-' . str_replace('/', '-', $sale_data['invoice_number']) . '.xml'; + $rawFilename = str_replace(["\r", "\n"], '', $rawFilename); + $safeFilename = preg_replace('/[^A-Za-z0-9._-]/', '_', $rawFilename); + $contentDisposition = 'attachment; filename="' . $safeFilename . '"; filename*=UTF-8\'\'' . rawurlencode($rawFilename); + + return $this->response + ->setHeader('Content-Type', 'application/xml') + ->setHeader('Content-Disposition', $contentDisposition) + ->setBody($xml); + } catch (\Exception $e) { + log_message('error', 'UBL generation failed: ' . $e->getMessage()); + return $this->response->setStatusCode(500)->setBody(lang('Sales.ubl_generation_failed')); + } finally { + $this->sale_lib->clear_all(); + } + } + /** * Edits an existing sale or work order. Used in app/Views/sales/form.php * diff --git a/app/Database/Migrations/20260304000000_addUblConfig.php b/app/Database/Migrations/20260304000000_addUblConfig.php new file mode 100644 index 000000000..9165f673a --- /dev/null +++ b/app/Database/Migrations/20260304000000_addUblConfig.php @@ -0,0 +1,24 @@ + 'invoice_format', 'value' => 'pdf_only'] + ]; + + $this->db->table('app_config')->ignore(true)->insertBatch($config_values); + } + + public function down(): void + { + $this->db->table('app_config')->whereIn('key', ['invoice_format'])->delete(); + } +} \ No newline at end of file diff --git a/app/Helpers/country_helper.php b/app/Helpers/country_helper.php new file mode 100644 index 000000000..b56f21042 --- /dev/null +++ b/app/Helpers/country_helper.php @@ -0,0 +1,229 @@ + 'BE', + 'Belgique' => 'BE', + 'België' => 'BE', + 'United States' => 'US', + 'USA' => 'US', + 'United States of America' => 'US', + 'United Kingdom' => 'GB', + 'UK' => 'GB', + 'Great Britain' => 'GB', + 'France' => 'FR', + 'Germany' => 'DE', + 'Deutschland' => 'DE', + 'Netherlands' => 'NL', + 'The Netherlands' => 'NL', + 'Nederland' => 'NL', + 'Italy' => 'IT', + 'Italia' => 'IT', + 'Spain' => 'ES', + 'España' => 'ES', + 'Poland' => 'PL', + 'Polska' => 'PL', + 'Portugal' => 'PT', + 'Sweden' => 'SE', + 'Sverige' => 'SE', + 'Norway' => 'NO', + 'Norge' => 'NO', + 'Denmark' => 'DK', + 'Danmark' => 'DK', + 'Finland' => 'FI', + 'Suomi' => 'FI', + 'Switzerland' => 'CH', + 'Suisse' => 'CH', + 'Schweiz' => 'CH', + 'Austria' => 'AT', + 'Österreich' => 'AT', + 'Ireland' => 'IE', + 'Luxembourg' => 'LU', + 'Greece' => 'GR', + 'Czech Republic' => 'CZ', + 'Czechia' => 'CZ', + 'Hungary' => 'HU', + 'Romania' => 'RO', + 'Bulgaria' => 'BG', + 'Slovakia' => 'SK', + 'Slovenia' => 'SI', + 'Estonia' => 'EE', + 'Latvia' => 'LV', + 'Lithuania' => 'LT', + 'Croatia' => 'HR', + 'Serbia' => 'RS', + 'Montenegro' => 'ME', + 'Bosnia and Herzegovina' => 'BA', + 'North Macedonia' => 'MK', + 'Albania' => 'AL', + 'Kosovo' => 'XK', + 'Turkey' => 'TR', + 'Türkiye' => 'TR', + 'Russia' => 'RU', + 'Russian Federation' => 'RU', + 'Ukraine' => 'UA', + 'Belarus' => 'BY', + 'Moldova' => 'MD', + 'Georgia' => 'GE', + 'Armenia' => 'AM', + 'Azerbaijan' => 'AZ', + 'Kazakhstan' => 'KZ', + 'Uzbekistan' => 'UZ', + + // Other major economies + 'China' => 'CN', + 'Japan' => 'JP', + 'South Korea' => 'KR', + 'Korea' => 'KR', + 'India' => 'IN', + 'Australia' => 'AU', + 'New Zealand' => 'NZ', + 'Canada' => 'CA', + 'Mexico' => 'MX', + 'Brazil' => 'BR', + 'Argentina' => 'AR', + 'Chile' => 'CL', + 'Colombia' => 'CO', + 'Peru' => 'PE', + 'South Africa' => 'ZA', + 'Egypt' => 'EG', + 'Nigeria' => 'NG', + 'Kenya' => 'KE', + 'Morocco' => 'MA', + + // If already ISO code, return as-is + 'BE' => 'BE', + 'US' => 'US', + 'GB' => 'GB', + 'FR' => 'FR', + 'DE' => 'DE', + 'NL' => 'NL', + 'IT' => 'IT', + 'ES' => 'ES', + 'PT' => 'PT', + 'SE' => 'SE', + 'NO' => 'NO', + 'DK' => 'DK', + 'FI' => 'FI', + 'CH' => 'CH', + 'AT' => 'AT', + 'IE' => 'IE', + 'LU' => 'LU', + 'GR' => 'GR', + 'CZ' => 'CZ', + 'HU' => 'HU', + 'RO' => 'RO', + 'BG' => 'BG', + 'SK' => 'SK', + 'SI' => 'SI', + 'EE' => 'EE', + 'LV' => 'LV', + 'LT' => 'LT', + 'HR' => 'HR', + 'RS' => 'RS', + 'ME' => 'ME', + 'BA' => 'BA', + 'MK' => 'MK', + 'AL' => 'AL', + 'TR' => 'TR', + 'RU' => 'RU', + 'UA' => 'UA', + ]; + + // Try exact match first + $normalized = trim($countryName); + if (isset($countryMap[$normalized])) { + return $countryMap[$normalized]; + } + + // Try case-insensitive match + $normalizedLower = strtolower($normalized); + foreach ($countryMap as $key => $code) { + if (strtolower($key) === $normalizedLower) { + return $code; + } + } + + // Try partial match (e.g., "United States" → "US") + foreach ($countryMap as $key => $code) { + if (stripos($key, $normalized) !== false || stripos($normalized, $key) !== false) { + return $code; + } + } + + // Try matching ISO code directly + if (preg_match('/^[A-Z]{2}$/i', $normalized)) { + return strtoupper($normalized); + } + + // Check if the country_codes config has a default + $config = config(OSPOS::class)->settings; + if (isset($config['country_codes']) && !empty($config['country_codes'])) { + $countries = explode(',', $config['country_codes']); + if (!empty($countries)) { + return strtoupper(trim($countries[0])); + } + } + + // Default to Belgium (for Peppol compliance in Belgium) + return 'BE'; + } +} + +if (!function_exists('getCurrencyCode')) { + /** + * Get ISO 4217 currency code for a country + * + * @param string $countryCode ISO 3166-1 alpha-2 country code + * @return string ISO 4217 currency code + */ + function getCurrencyCode(string $countryCode): string + { + $currencyMap = [ + 'BE' => 'EUR', + 'FR' => 'EUR', + 'DE' => 'EUR', + 'NL' => 'EUR', + 'IT' => 'EUR', + 'ES' => 'EUR', + 'PT' => 'EUR', + 'IE' => 'EUR', + 'AT' => 'EUR', + 'LU' => 'EUR', + 'FI' => 'EUR', + 'GR' => 'EUR', + 'US' => 'USD', + 'GB' => 'GBP', + 'CH' => 'CHF', + 'JP' => 'JPY', + 'CN' => 'CNY', + 'CA' => 'CAD', + 'AU' => 'AUD', + 'NZ' => 'NZD', + 'IN' => 'INR', + 'BR' => 'BRL', + 'MX' => 'MXN', + 'ZA' => 'ZAR', + ]; + + return $currencyMap[$countryCode] ?? 'EUR'; // Default to EUR + } +} \ No newline at end of file diff --git a/app/Helpers/tabular_helper.php b/app/Helpers/tabular_helper.php index 70effeba7..d5d81a839 100644 --- a/app/Helpers/tabular_helper.php +++ b/app/Helpers/tabular_helper.php @@ -84,6 +84,7 @@ function get_sales_manage_table_headers(): string if ($config['invoice_enable']) { $headers[] = ['invoice_number' => lang('Sales.invoice_number')]; $headers[] = ['invoice' => '', 'sortable' => false, 'escape' => false]; + $headers[] = ['ubl' => '', 'sortable' => false, 'escape' => false]; } $headers[] = ['receipt' => '', 'sortable' => false, 'escape' => false]; @@ -120,6 +121,13 @@ function get_sale_data_row(object $sale): array '', ['title' => lang('Sales.show_invoice')] ); + $row['ubl'] = empty($sale->invoice_number) + ? '-' + : anchor( + "$controller/ublInvoice/$sale->sale_id", + '', + ['title' => lang('Sales.download_ubl'), 'target' => '_blank'] + ); } $row['receipt'] = anchor( @@ -654,7 +662,7 @@ function expand_attribute_values(array $definition_names, array $row): array foreach ($definition_names as $definition_id => $definitionInfo) { if (isset($indexed_values[$definition_id])) { $raw_value = $indexed_values[$definition_id]; - + // Format DECIMAL attributes according to locale if (is_array($definitionInfo) && isset($definitionInfo['type']) && $definitionInfo['type'] === DECIMAL) { $attribute_values["$definition_id"] = to_decimals($raw_value); @@ -934,7 +942,7 @@ function get_controller(): string /** * Restores filter values from URL query string. - * + * * @param CodeIgniter\HTTP\IncomingRequest $request The request object * @return array Array with 'start_date', 'end_date', and 'selected_filters' keys */ @@ -943,7 +951,7 @@ function restoreTableFilters($request): array $startDate = $request->getGet('start_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS); $endDate = $request->getGet('end_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS); $urlFilters = $request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - + return array_filter([ 'start_date' => $startDate ?: null, 'end_date' => $endDate ?: null, diff --git a/app/Language/ar-EG/Sales.php b/app/Language/ar-EG/Sales.php index bf8d5b7cd..330c9ffb7 100644 --- a/app/Language/ar-EG/Sales.php +++ b/app/Language/ar-EG/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "رقم طلب العمل يجب ان يكون فريد.", "work_order_sent" => "تم ارسال طلب العمل الى", "work_order_unsent" => "فشل في ارسال طلب العمل الى", + "sale_not_found" => "البيع غير موجود", ]; diff --git a/app/Language/ar-LB/Sales.php b/app/Language/ar-LB/Sales.php index 112838301..bf36afea2 100644 --- a/app/Language/ar-LB/Sales.php +++ b/app/Language/ar-LB/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "رقم طلب العمل يجب ان يكون فريد.", "work_order_sent" => "تم ارسال طلب العمل الى", "work_order_unsent" => "فشل في ارسال طلب العمل الى", + "sale_not_found" => "البيع غير موجود", ]; diff --git a/app/Language/az/Sales.php b/app/Language/az/Sales.php index 4ab9586c9..95ece31ba 100644 --- a/app/Language/az/Sales.php +++ b/app/Language/az/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "İş sifarişinin nömrəsi unikal olmalıdır.", "work_order_sent" => "İş sifarişi göndərildi", "work_order_unsent" => "İş Sifarişi göndərilməmişdi", + "sale_not_found" => "Satış tapılmadı", ]; diff --git a/app/Language/bg/Sales.php b/app/Language/bg/Sales.php index 43293ef6f..35ec420be 100644 --- a/app/Language/bg/Sales.php +++ b/app/Language/bg/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Номерът на работната поръчка трябва да е уникален.", "work_order_sent" => "Работната поръчка е изпратена до", "work_order_unsent" => "Работната поръчка не бе изпратена до", + "sale_not_found" => "Продажбата не е намерена", ]; diff --git a/app/Language/bs/Sales.php b/app/Language/bs/Sales.php index f623af079..a1b3ec84e 100644 --- a/app/Language/bs/Sales.php +++ b/app/Language/bs/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Broj radnog naloga mora biti jedinstven.", "work_order_sent" => "Radni nalog poslat na", "work_order_unsent" => "Slanje radnog naloga nije uspjelo", + "sale_not_found" => "Prodaja nije pronađena", ]; diff --git a/app/Language/ckb/Sales.php b/app/Language/ckb/Sales.php index 7f5d46dc2..d7b93080d 100644 --- a/app/Language/ckb/Sales.php +++ b/app/Language/ckb/Sales.php @@ -223,4 +223,5 @@ return [ 'work_order_number_duplicate' => "ژمارەی داواکاری کار دەبێت تایبەت بێت.", 'work_order_sent' => "داواکاری کار نێردرا بۆ", 'work_order_unsent' => "داواکاری کار شکستی هێنا لە ناردنی بۆ", + 'sale_not_found' => "فرۆش نەدۆزرایەوە", ]; diff --git a/app/Language/cs/Sales.php b/app/Language/cs/Sales.php index 623446543..418d8d8ad 100644 --- a/app/Language/cs/Sales.php +++ b/app/Language/cs/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "Prodej nenalezen", ]; diff --git a/app/Language/da/Sales.php b/app/Language/da/Sales.php index ee07a62eb..a4e50fcaf 100644 --- a/app/Language/da/Sales.php +++ b/app/Language/da/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "Salg ikke fundet", ]; diff --git a/app/Language/de-CH/Sales.php b/app/Language/de-CH/Sales.php index 32d1cc228..018beb709 100644 --- a/app/Language/de-CH/Sales.php +++ b/app/Language/de-CH/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "Verkauf nicht gefunden", ]; diff --git a/app/Language/el/Sales.php b/app/Language/el/Sales.php index 90dd0d5d0..d11b03c8f 100644 --- a/app/Language/el/Sales.php +++ b/app/Language/el/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Ο Αριθμός της Παραγγελίας Εργασίας πρέπει να είναι μοναδικός.", "work_order_sent" => "Εντολή Εργασίας απεστάλη σε", "work_order_unsent" => "Ανεπιτυχής αποστολή Εντολής Εργασίας", + "sale_not_found" => "Η πώληση δεν βρέθηκε", ]; diff --git a/app/Language/en-GB/Sales.php b/app/Language/en-GB/Sales.php index d9678ba44..bb2cc7947 100644 --- a/app/Language/en-GB/Sales.php +++ b/app/Language/en-GB/Sales.php @@ -223,4 +223,5 @@ return [ "work_order_number_duplicate" => "Work Order Number must be unique.", "work_order_sent" => "Work Order sent to", "work_order_unsent" => "Work Order failed to be sent to", + "sale_not_found" => "Sale not found", ]; diff --git a/app/Language/en/Sales.php b/app/Language/en/Sales.php index 6c7d93d48..0249d656e 100644 --- a/app/Language/en/Sales.php +++ b/app/Language/en/Sales.php @@ -223,4 +223,8 @@ return [ "work_order_number_duplicate" => "Work Order Number must be unique.", "work_order_sent" => "Work Order sent to", "work_order_unsent" => "Work Order failed to be sent to", + "ubl_invoice" => "UBL Invoice", + "download_ubl" => "Download UBL Invoice", + "ubl_generation_failed" => "Failed to generate UBL invoice", + "sale_not_found" => "Sale not found", ]; diff --git a/app/Language/es-MX/Sales.php b/app/Language/es-MX/Sales.php index b0e521c5b..837ed3faa 100644 --- a/app/Language/es-MX/Sales.php +++ b/app/Language/es-MX/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "El número de orden de trabajo debe ser único.", "work_order_sent" => "Orden de trabajo enviada a", "work_order_unsent" => "Falló la Orden de Trabajo al enviar a", + "sale_not_found" => "Venta no encontrada", ]; diff --git a/app/Language/fa/Sales.php b/app/Language/fa/Sales.php index 182a534e2..2479b5eef 100644 --- a/app/Language/fa/Sales.php +++ b/app/Language/fa/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "شماره سفارش کار باید منحصر به فرد باشد.", "work_order_sent" => "دستور کار ارسال شده به", "work_order_unsent" => "دستور کار نتوانست به ارسال شود", + "sale_not_found" => "فروش یافت نشد", ]; diff --git a/app/Language/he/Sales.php b/app/Language/he/Sales.php index f2bd399ba..07d909464 100644 --- a/app/Language/he/Sales.php +++ b/app/Language/he/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "מספר הזמנת עבודה חייב להיות ייחודי.", "work_order_sent" => "הזמנת עבודה נשלחה אל", "work_order_unsent" => "הזמנת עבודה לא נשלחה אל", + "sale_not_found" => "המכירה לא נמצאה", ]; diff --git a/app/Language/hr-HR/Sales.php b/app/Language/hr-HR/Sales.php index 9e2df3b99..64222c590 100644 --- a/app/Language/hr-HR/Sales.php +++ b/app/Language/hr-HR/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "Prodaja nije pronađena", ]; diff --git a/app/Language/hu/Sales.php b/app/Language/hu/Sales.php index 5928b2708..e60ea7ab9 100644 --- a/app/Language/hu/Sales.php +++ b/app/Language/hu/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "Értékesítés nem található", ]; diff --git a/app/Language/hy/Sales.php b/app/Language/hy/Sales.php index b38f57446..183a36f3d 100644 --- a/app/Language/hy/Sales.php +++ b/app/Language/hy/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "Վաճառքը գտնված չէ", ]; diff --git a/app/Language/id/Sales.php b/app/Language/id/Sales.php index e15191351..4cb36bdc8 100644 --- a/app/Language/id/Sales.php +++ b/app/Language/id/Sales.php @@ -222,5 +222,6 @@ return [ "work_order_number_duplicate" => "Nomor Perintah Kerja tidak boleh sama.", "work_order_sent" => "Perintah Kerja dikirim ke", "work_order_unsent" => "Perintah Kerja gagal dikirim ke", + "sale_not_found" => "Penjualan tidak ditemukan", "selected_customer" => "Pelanggan Terpilih", ]; diff --git a/app/Language/lo/Sales.php b/app/Language/lo/Sales.php index 66745059f..6d9a5f561 100644 --- a/app/Language/lo/Sales.php +++ b/app/Language/lo/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Work Order Number must be unique.", "work_order_sent" => "Work Order sent to", "work_order_unsent" => "Work Order failed to be sent to", + "sale_not_found" => "Sale not found", ]; diff --git a/app/Language/nb/Sales.php b/app/Language/nb/Sales.php index b38f57446..17deba1a0 100644 --- a/app/Language/nb/Sales.php +++ b/app/Language/nb/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "Salg ikke funnet", ]; diff --git a/app/Language/nl-BE/Sales.php b/app/Language/nl-BE/Sales.php index f2768e8ed..c002a2678 100644 --- a/app/Language/nl-BE/Sales.php +++ b/app/Language/nl-BE/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Werkorder Nummer moet uniek zijn.", "work_order_sent" => "Werkorder verzonder naar", "work_order_unsent" => "Werkorder kon niet verzonden worden naar", + "sale_not_found" => "Verkoop niet gevonden", ]; diff --git a/app/Language/ro/Sales.php b/app/Language/ro/Sales.php index 815b1176d..711076cb6 100644 --- a/app/Language/ro/Sales.php +++ b/app/Language/ro/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "Vânzarea nu a fost găsită", ]; diff --git a/app/Language/ru/Sales.php b/app/Language/ru/Sales.php index 7a3c2a977..f1bbcc947 100644 --- a/app/Language/ru/Sales.php +++ b/app/Language/ru/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Номер рабочего заказа должен быть уникальным.", "work_order_sent" => "Рабочий заказ отправлен", "work_order_unsent" => "Не удалось отправить рабочий заказ", + "sale_not_found" => "Продажа не найдена", ]; diff --git a/app/Language/sv/Sales.php b/app/Language/sv/Sales.php index b492a389f..c7e30258e 100644 --- a/app/Language/sv/Sales.php +++ b/app/Language/sv/Sales.php @@ -222,5 +222,6 @@ return [ "work_order_number_duplicate" => "Arbetsorder nummer måste vara unikt.", "work_order_sent" => "Arbetsorder skickad till", "work_order_unsent" => "Arbetsorder gick ej att skicka till", + "sale_not_found" => "Försäljning hittades inte", "selected_customer" => "Vald kund", ]; diff --git a/app/Language/sw-KE/Sales.php b/app/Language/sw-KE/Sales.php index 2dc789bda..44ada4399 100644 --- a/app/Language/sw-KE/Sales.php +++ b/app/Language/sw-KE/Sales.php @@ -224,4 +224,5 @@ return [ "work_order_number_duplicate" => "Nambari ya Agizo la Kazi lazima iwe ya kipekee.", "work_order_sent" => "Agizo la Kazi limetumwa kwa", "work_order_unsent" => "Imeshindikana kutuma Agizo la Kazi kwa", + "sale_not_found" => "Mauzo hayapatikani", ]; \ No newline at end of file diff --git a/app/Language/sw-TZ/Sales.php b/app/Language/sw-TZ/Sales.php index 2dc789bda..44ada4399 100644 --- a/app/Language/sw-TZ/Sales.php +++ b/app/Language/sw-TZ/Sales.php @@ -224,4 +224,5 @@ return [ "work_order_number_duplicate" => "Nambari ya Agizo la Kazi lazima iwe ya kipekee.", "work_order_sent" => "Agizo la Kazi limetumwa kwa", "work_order_unsent" => "Imeshindikana kutuma Agizo la Kazi kwa", + "sale_not_found" => "Mauzo hayapatikani", ]; \ No newline at end of file diff --git a/app/Language/th/Sales.php b/app/Language/th/Sales.php index 0a8ec17b1..32dc0924a 100644 --- a/app/Language/th/Sales.php +++ b/app/Language/th/Sales.php @@ -222,5 +222,6 @@ return [ "work_order_number_duplicate" => "หมายเลขคำสั่งงานต้องไม่ซ้ำกัน", "work_order_sent" => "คำสั่งงานส่งถึง", "work_order_unsent" => "ส่งคำสั่งงานล้มเหลว", + "sale_not_found" => "ไม่พบการขาย", "selected_customer" => "ลูกค้าที่เลือก", ]; diff --git a/app/Language/tl/Sales.php b/app/Language/tl/Sales.php index 3cb4d1653..fc1b00dd6 100644 --- a/app/Language/tl/Sales.php +++ b/app/Language/tl/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Work Order Number must be unique.", "work_order_sent" => "Work Order sent to", "work_order_unsent" => "Work Order failed to be sent to", + "sale_not_found" => "Sale not found", ]; diff --git a/app/Language/tr/Sales.php b/app/Language/tr/Sales.php index 8901fa02f..56f104dac 100644 --- a/app/Language/tr/Sales.php +++ b/app/Language/tr/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "İş Emri Numarası diğerinden farklı olmalı.", "work_order_sent" => "İş Emri gönderildi:", "work_order_unsent" => "İş Emri gönderilemedi:", + "sale_not_found" => "Satış bulunamadı", ]; diff --git a/app/Language/uk/Sales.php b/app/Language/uk/Sales.php index 2524d3a3d..cc266a2db 100644 --- a/app/Language/uk/Sales.php +++ b/app/Language/uk/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Такий номер робочого замовлення уже існує.", "work_order_sent" => "Замовлення відправлено", "work_order_unsent" => "Не вдалось отримати робоче замовлення", + "sale_not_found" => "Продаж не знайдено", ]; diff --git a/app/Language/vi/Sales.php b/app/Language/vi/Sales.php index d99083c28..79ecf2dfd 100644 --- a/app/Language/vi/Sales.php +++ b/app/Language/vi/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "Số giấy giao việc phải là duy nhất.", "work_order_sent" => "Gửi Giấy giao việc cho", "work_order_unsent" => "Gặp lỗi khi gửi Giấy giao việc cho", + "sale_not_found" => "Không tìm thấy giao dịch", ]; diff --git a/app/Language/zh-Hans/Sales.php b/app/Language/zh-Hans/Sales.php index 4e04d3d09..b15f00b2a 100644 --- a/app/Language/zh-Hans/Sales.php +++ b/app/Language/zh-Hans/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "", "work_order_sent" => "", "work_order_unsent" => "", + "sale_not_found" => "找不到销售", ]; diff --git a/app/Language/zh-Hant/Sales.php b/app/Language/zh-Hant/Sales.php index cb1af0dfb..7c5327c7b 100644 --- a/app/Language/zh-Hant/Sales.php +++ b/app/Language/zh-Hant/Sales.php @@ -222,4 +222,5 @@ return [ "work_order_number_duplicate" => "工作編號重複.", "work_order_sent" => "發送工作指示", "work_order_unsent" => "工作指示發送失敗", + "sale_not_found" => "找不到銷售", ]; diff --git a/app/Libraries/Email_lib.php b/app/Libraries/Email_lib.php index 914344567..607ffd523 100644 --- a/app/Libraries/Email_lib.php +++ b/app/Libraries/Email_lib.php @@ -82,4 +82,37 @@ class Email_lib return $result; } + + /** + * Send email with multiple attachments + * + * @param string $to + * @param string $subject + * @param string $message + * @param array $attachments + * @return bool + */ + public function sendMultipleAttachments(string $to, string $subject, string $message, array $attachments): bool + { + $email = $this->email; + + $email->setFrom($this->config['email'], $this->config['company']); + $email->setTo($to); + $email->setSubject($subject); + $email->setMessage($message); + + foreach ($attachments as $attachment) { + if (!empty($attachment) && file_exists($attachment)) { + $email->attach($attachment); + } + } + + $result = $email->send(); + + if (!$result) { + log_message('error', $email->printDebugger()); + } + + return $result; + } } diff --git a/app/Libraries/InvoiceAttachment/InvoiceAttachment.php b/app/Libraries/InvoiceAttachment/InvoiceAttachment.php new file mode 100644 index 000000000..d73edcdda --- /dev/null +++ b/app/Libraries/InvoiceAttachment/InvoiceAttachment.php @@ -0,0 +1,40 @@ +attachments[] = $attachment; + return $this; + } + + /** + * Create generator with attachments based on config. + * Factory method that instantiates the right attachments. + * + * @param string $invoiceFormat Config value: 'pdf_only', 'ubl_only', or 'both' + * @return self + */ + public static function createFromConfig(string $invoiceFormat): self + { + $generator = new self(); + + if (in_array($invoiceFormat, ['pdf_only', 'both'], true)) { + $generator->register(new PdfAttachment()); + } + + if (in_array($invoiceFormat, ['ubl_only', 'both'], true)) { + $generator->register(new UblAttachment()); + } + + return $generator; + } + + /** + * Generate all applicable attachments for a sale. + * + * @param array $saleData The sale data + * @param string $type The document type + * @return string[] Array of file paths to generated attachments + */ + public function generateAttachments(array $saleData, string $type): array + { + $files = []; + + foreach ($this->attachments as $attachment) { + if ($attachment->isApplicableForType($type, $saleData)) { + $filepath = $attachment->generate($saleData, $type); + if ($filepath !== null) { + $files[] = $filepath; + } + } + } + + return $files; + } + + /** + * Clean up temporary attachment files. + * + * @param string[] $files + */ + public static function cleanup(array $files): void + { + foreach ($files as $file) { + @unlink($file); + } + } +} \ No newline at end of file diff --git a/app/Libraries/InvoiceAttachment/PdfAttachment.php b/app/Libraries/InvoiceAttachment/PdfAttachment.php new file mode 100644 index 000000000..686fd16cd --- /dev/null +++ b/app/Libraries/InvoiceAttachment/PdfAttachment.php @@ -0,0 +1,61 @@ +setData($saleData)->render("sales/{$type}_email"); + + helper(['dompdf', 'file']); + + $tempPath = tempnam(sys_get_temp_dir(), 'ospos_pdf_'); + if ($tempPath === false) { + log_message('error', 'PDF attachment: failed to create temp file'); + return null; + } + + $filename = $tempPath . '.pdf'; + rename($tempPath, $filename); + + $pdfContent = create_pdf($html); + if (file_put_contents($filename, $pdfContent) === false) { + log_message('error', 'PDF attachment: failed to write content'); + @unlink($filename); + return null; + } + + return $filename; + } + + /** + * @inheritDoc + */ + public function isApplicableForType(string $type, array $saleData): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function getFileExtension(): string + { + return 'pdf'; + } + + /** + * @inheritDoc + */ + public function getEnabledConfigValues(): array + { + return ['pdf_only', 'both']; + } +} \ No newline at end of file diff --git a/app/Libraries/InvoiceAttachment/UblAttachment.php b/app/Libraries/InvoiceAttachment/UblAttachment.php new file mode 100644 index 000000000..369cd692a --- /dev/null +++ b/app/Libraries/InvoiceAttachment/UblAttachment.php @@ -0,0 +1,69 @@ +generateUblInvoice($saleData); + + $tempPath = tempnam(sys_get_temp_dir(), 'ospos_ubl_'); + if ($tempPath === false) { + log_message('error', 'UBL attachment: failed to create temp file'); + return null; + } + + $filename = $tempPath . '.xml'; + rename($tempPath, $filename); + + if (file_put_contents($filename, $xml) === false) { + log_message('error', 'UBL attachment: failed to write content'); + @unlink($filename); + return null; + } + + return $filename; + } catch (\Exception $e) { + log_message('error', 'UBL attachment generation failed: ' . $e->getMessage()); + return null; + } + } + + /** + * @inheritDoc + */ + public function isApplicableForType(string $type, array $saleData): bool + { + return in_array($type, ['invoice', 'tax_invoice'], true) + && !empty($saleData['invoice_number']); + } + + /** + * @inheritDoc + */ + public function getFileExtension(): string + { + return 'xml'; + } + + /** + * @inheritDoc + */ + public function getEnabledConfigValues(): array + { + return ['ubl_only', 'both']; + } +} \ No newline at end of file diff --git a/app/Libraries/UBLGenerator.php b/app/Libraries/UBLGenerator.php new file mode 100644 index 000000000..e268853de --- /dev/null +++ b/app/Libraries/UBLGenerator.php @@ -0,0 +1,243 @@ +setId('VAT'); + $supplierParty = $this->buildSupplierParty($saleData, $taxScheme); + $customerParty = $this->buildCustomerParty($saleData['customer_object'] ?? null, $taxScheme); + $invoiceLines = $this->buildInvoiceLines($saleData['cart'], $taxScheme); + $taxTotal = $this->buildTaxTotal($saleData['taxes'], $taxScheme); + $monetaryTotal = $this->buildMonetaryTotal($saleData); + + $invoice = (new Invoice()) + ->setUBLVersionId('2.1') + ->setCustomizationId('urn:cen.eu:en16931:2017') + ->setProfileId('urn:fdc:peppol.eu:2017:poacc:billing:01:1.0') + ->setId($saleData['invoice_number']) + ->setIssueDate(new \DateTime($saleData['transaction_date'])) + ->setInvoiceTypeCode(380) + ->setAccountingSupplierParty($supplierParty) + ->setAccountingCustomerParty($customerParty) + ->setInvoiceLines($invoiceLines) + ->setTaxTotal($taxTotal) + ->setLegalMonetaryTotal($monetaryTotal); + + $generator = new Generator(); + return $generator->invoice($invoice); + } + + protected function buildSupplierParty(array $saleData, TaxScheme $taxScheme): AccountingParty + { + $config = $saleData['config']; + + $addressParts = $this->parseAddress($config['address'] ?? ''); + $countryCode = 'BE'; // Default + + $country = (new Country())->setIdentificationCode($countryCode); + $address = (new Address()) + ->setStreetName($addressParts['street'] ?? '') + ->setBuildingNumber($addressParts['number'] ?? '') + ->setCityName($addressParts['city'] ?? '') + ->setPostalZone($addressParts['zip'] ?? '') + ->setCountrySubentity($config['state'] ?? '') + ->setCountry($country); + + $party = (new Party()) + ->setName($config['company']) + ->setPostalAddress($address); + + if (!empty($config['account_number'])) { + $partyTaxScheme = (new PartyTaxScheme()) + ->setCompanyId($config['account_number']) + ->setTaxScheme($taxScheme); + $party->setPartyTaxScheme($partyTaxScheme); + } + + $accountingParty = (new AccountingParty())->setParty($party); + + return $accountingParty; + } + + protected function buildCustomerParty(?object $customerInfo, TaxScheme $taxScheme): AccountingParty + { + if ($customerInfo === null) { + return (new AccountingParty())->setParty(new Party()); + } + + $countryCode = getCountryCode($customerInfo->country ?? ''); + + $country = (new Country())->setIdentificationCode($countryCode); + $address = (new Address()) + ->setStreetName($customerInfo->address_1 ?? '') + ->setAddressLine([$customerInfo->address_2 ?? '']) + ->setCityName($customerInfo->city ?? '') + ->setPostalZone($customerInfo->zip ?? '') + ->setCountrySubentity($customerInfo->state ?? '') + ->setCountry($country); + + $partyName = !empty($customerInfo->company_name) + ? $customerInfo->company_name + : trim($customerInfo->first_name . ' ' . $customerInfo->last_name); + + $party = (new Party()) + ->setName($partyName) + ->setPostalAddress($address); + + if (!empty($customerInfo->email)) { + $contact = (new Contact()) + ->setElectronicMail($customerInfo->email) + ->setTelephone($customerInfo->phone_number ?? ''); + $party->setContact($contact); + } + + if (!empty($customerInfo->account_number)) { + $accountingParty = (new AccountingParty()) + ->setParty($party) + ->setSupplierAssignedAccountId($customerInfo->account_number); + } else { + $accountingParty = (new AccountingParty())->setParty($party); + } + + if (!empty($customerInfo->tax_id)) { + $partyTaxScheme = (new PartyTaxScheme()) + ->setCompanyId($customerInfo->tax_id) + ->setTaxScheme($taxScheme); + $party->setPartyTaxScheme($partyTaxScheme); + } + + return $accountingParty; + } + + protected function buildInvoiceLines(array $cart, TaxScheme $taxScheme): array + { + $lines = []; + foreach ($cart as $item) { + $price = (new Price()) + ->setBaseQuantity(1.0) + ->setUnitCode(UnitCode::UNIT) + ->setPriceAmount($item['price'] ?? 0); + + $taxCategory = (new TaxCategory()) + ->setId('S') + ->setPercent((float)($item['tax_rate'] ?? 0)) + ->setTaxScheme($taxScheme); + + $itemObj = (new Item()) + ->setName($item['name'] ?? '') + ->setDescription($item['description'] ?? ''); + $line = (new InvoiceLine()) + ->setId(isset($item['line']) ? (string)$item['line'] : '1') + ->setItem($itemObj) + ->setPrice($price) + ->setInvoicedQuantity($item['quantity'] ?? 0); + + $lines[] = $line; + } + return $lines; + } + + protected function buildTaxTotal(array $taxes, TaxScheme $taxScheme): TaxTotal + { + $totalTax = '0'; + $taxSubTotals = []; + + foreach ($taxes as $tax) { + if (isset($tax['tax_rate'])) { + $taxRate = (string)$tax['tax_rate']; + $taxAmount = (string)($tax['sale_tax_amount'] ?? 0); + + $taxCategory = (new TaxCategory()) + ->setId('S') + ->setPercent((float)$taxRate) + ->setTaxScheme($taxScheme); + + $taxableAmount = '0'; + if (bccomp($taxRate, '0') > 0) { + $taxableAmount = bcdiv($taxAmount, bcdiv($taxRate, '100'), 10); + } + + $taxSubTotal = (new TaxSubTotal()) + ->setTaxableAmount((float)$taxableAmount) + ->setTaxAmount((float)$taxAmount) + ->setTaxCategory($taxCategory); + + $taxSubTotals[] = $taxSubTotal; + $totalTax = bcadd($totalTax, $taxAmount); + } + } + + $taxTotal = new TaxTotal(); + $taxTotal->setTaxAmount((float)$totalTax); + foreach ($taxSubTotals as $subTotal) { + $taxTotal->addTaxSubTotal($subTotal); + } + + return $taxTotal; + } + + protected function buildMonetaryTotal(array $saleData): LegalMonetaryTotal + { + $subtotal = (string)($saleData['subtotal'] ?? 0); + $total = (string)($saleData['total'] ?? 0); + $amountDue = (string)($saleData['amount_due'] ?? 0); + + return (new LegalMonetaryTotal()) + ->setLineExtensionAmount((float)$subtotal) + ->setTaxExclusiveAmount((float)$subtotal) + ->setTaxInclusiveAmount((float)$total) + ->setPayableAmount((float)$amountDue); + } + + protected function parseAddress(string $address): array + { + $parts = array_filter(array_map('trim', explode("\n", $address))); + + $result = [ + 'street' => '', + 'number' => '', + 'city' => '', + 'zip' => '' + ]; + + if (!empty($parts)) { + $result['street'] = $parts[0]; + if (isset($parts[1])) { + // Match 4-5 digit postal codes (e.g., 1234, 12345) followed by city name + // Note: This handles common European formats. International formats + // like UK postcodes (e.g., "SW1A 2AA") may need additional handling. + if (preg_match('/(\d{4,5})\s*(.+)/', $parts[1], $matches)) { + $result['zip'] = $matches[1]; + $result['city'] = $matches[2]; + } else { + $result['city'] = $parts[1]; + } + } + } + + return $result; + } +} \ No newline at end of file diff --git a/app/Views/sales/invoice.php b/app/Views/sales/invoice.php index 654dab96f..3f83dcf86 100644 --- a/app/Views/sales/invoice.php +++ b/app/Views/sales/invoice.php @@ -69,6 +69,7 @@ if (isset($error_message)) {
 ' . lang('Sales.send_invoice') ?>
+  ' . lang('Sales.download_ubl'), ['class' => 'btn btn-info btn-sm']) ?>  ' . lang('Sales.register'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_sales_button']) ?>  ' . lang('Sales.takings'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_takings_button']) ?> diff --git a/composer.json b/composer.json index 296c5b921..881a52cc8 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "dompdf/dompdf": "^2.0.3", "ezyang/htmlpurifier": "^4.17", "laminas/laminas-escaper": "2.17.0", + "num-num/ubl-invoice": "^2.4", "paragonie/random_compat": "^2.0.21", "picqer/php-barcode-generator": "^2.4.0", "tamtamchik/namecase": "^3.0.0" diff --git a/composer.lock b/composer.lock index 9817fb506..68411a85a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,89 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3fe9e4622879914bfa763b71c236c7fe", + "content-hash": "294dd1f7f42c4cfd6a151cdb68c07a9f", "packages": [ { - "name": "codeigniter4/framework", - "version": "v4.6.3", + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", "source": { "type": "git", - "url": "https://github.com/codeigniter4/framework.git", - "reference": "68d1a5896106f869452dd369a690dd5bc75160fb" + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeigniter4/framework/zipball/68d1a5896106f869452dd369a690dd5bc75160fb", - "reference": "68d1a5896106f869452dd369a690dd5bc75160fb", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "codeigniter4/framework", + "version": "v4.6.5", + "source": { + "type": "git", + "url": "https://github.com/codeigniter4/framework.git", + "reference": "116e0919590a412c09d2b9e4f6b8addda18224d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeigniter4/framework/zipball/116e0919590a412c09d2b9e4f6b8addda18224d8", + "reference": "116e0919590a412c09d2b9e4f6b8addda18224d8", "shasum": "" }, "require": { @@ -31,7 +100,7 @@ "codeigniter/coding-standard": "^1.7", "fakerphp/faker": "^1.24", "friendsofphp/php-cs-fixer": "^3.47.1", - "kint-php/kint": "^6.0", + "kint-php/kint": "^6.1", "mikey179/vfsstream": "^1.6.12", "nexusphp/cs-config": "^3.6", "phpunit/phpunit": "^10.5.16 || ^11.2", @@ -78,7 +147,141 @@ "slack": "https://codeigniterchat.slack.com", "source": "https://github.com/codeigniter4/CodeIgniter4" }, - "time": "2025-08-02T13:36:13+00:00" + "time": "2026-02-01T17:59:34+00:00" + }, + { + "name": "doctrine/collections", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-json": "*", + "phpstan/phpstan": "^2.1.30", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.6.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2026-01-15T10:01:58+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" }, { "name": "dompdf/dompdf", @@ -331,6 +534,186 @@ }, "time": "2025-07-25T09:04:22+00:00" }, + { + "name": "nesbot/carbon", + "version": "3.11.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-03-11T17:23:39+00:00" + }, + { + "name": "num-num/ubl-invoice", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/num-num/ubl-invoice.git", + "reference": "30fc1d9a1a8bd347630052addd6405942a1f0103" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/num-num/ubl-invoice/zipball/30fc1d9a1a8bd347630052addd6405942a1f0103", + "reference": "30fc1d9a1a8bd347630052addd6405942a1f0103", + "shasum": "" + }, + "require": { + "doctrine/collections": "^1.8 || ^2.0", + "nesbot/carbon": "^2.72 || ^3.11", + "php": "^7.4 || ^8.0", + "sabre/xml": "^4.0" + }, + "require-dev": { + "brianium/paratest": "^6.11", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "NumNum\\UBL\\": [ + "src" + ], + "NumNum\\UBL\\Tests\\": [ + "tests" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bert Devriese", + "email": "bert@numnum.be", + "homepage": "https://www.numnum.be", + "role": "Developer" + } + ], + "description": "A modern object-oriented PHP library to create valid UBL and Peppol BIS 3.0 files", + "homepage": "https://github.com/num-num/ubl-invoice", + "keywords": [ + "E-Invoice", + "digital invoice", + "efff", + "einvoice", + "electronic invoice", + "euinvoice", + "invoice", + "peppol", + "peppol bis", + "peppol invoice", + "peppolbis", + "ubl", + "ubl invoice", + "ublinvoice", + "xml", + "xml invoice" + ], + "support": { + "issues": "https://github.com/num-num/ubl-invoice/issues", + "source": "https://github.com/num-num/ubl-invoice/tree/v2.4.1" + }, + "time": "2026-02-09T10:22:18+00:00" + }, { "name": "paragonie/random_compat", "version": "v2.0.21", @@ -562,6 +945,54 @@ ], "time": "2024-09-18T08:09:33+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "sabberworm/php-css-parser", "version": "v8.9.0", @@ -628,6 +1059,709 @@ }, "time": "2025-07-11T13:20:48+00:00" }, + { + "name": "sabre/uri", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc", + "reference": "4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.94", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "rector/rector": "^2.3" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "time": "2026-04-01T08:19:11+00:00" + }, + { + "name": "sabre/xml", + "version": "4.0.7", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "53db7bad0953949fb61037fbf9b13b421492395c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/53db7bad0953949fb61037fbf9b13b421492395c", + "reference": "53db7bad0953949fb61037fbf9b13b421492395c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.4 || ^8.0", + "sabre/uri": ">=2.0,<4.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.94", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.6", + "rector/rector": "^2.3" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "time": "2026-04-02T11:40:41+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "33600f8489485425bfcddd0d983391038d3422e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7", + "reference": "33600f8489485425bfcddd0d983391038d3422e7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, { "name": "tamtamchik/namecase", "version": "3.0.0", @@ -760,28 +1894,28 @@ }, { "name": "codeigniter/coding-standard", - "version": "v1.8.8", + "version": "v1.9.1", "source": { "type": "git", "url": "https://github.com/CodeIgniter/coding-standard.git", - "reference": "410526fc1447a04fcdf5441b9c8507668b97b3a7" + "reference": "c7d227d3d3d0f2270405c8317c5e8c55f2262956" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CodeIgniter/coding-standard/zipball/410526fc1447a04fcdf5441b9c8507668b97b3a7", - "reference": "410526fc1447a04fcdf5441b9c8507668b97b3a7", + "url": "https://api.github.com/repos/CodeIgniter/coding-standard/zipball/c7d227d3d3d0f2270405c8317c5e8c55f2262956", + "reference": "c7d227d3d3d0f2270405c8317c5e8c55f2262956", "shasum": "" }, "require": { "ext-tokenizer": "*", - "friendsofphp/php-cs-fixer": "^3.76", - "nexusphp/cs-config": "^3.26", - "php": "^8.1" + "friendsofphp/php-cs-fixer": "^3.94", + "nexusphp/cs-config": "^3.28", + "php": "^8.2" }, "require-dev": { - "nexusphp/tachycardia": "^2.3", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.5 || ^11.2" + "nexusphp/tachycardia": "^2.4", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.5 || ^12.5" }, "type": "library", "autoload": { @@ -810,7 +1944,7 @@ "slack": "https://codeigniterchat.slack.com", "source": "https://github.com/CodeIgniter/coding-standard" }, - "time": "2025-09-27T13:54:11+00:00" + "time": "2026-02-17T18:41:24+00:00" }, { "name": "composer/pcre", @@ -1207,16 +2341,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.90.0", + "version": "v3.94.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "ad732c2e9299c9743f9c55ae53cc0e7642ab1155" + "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/ad732c2e9299c9743f9c55ae53cc0e7642ab1155", - "reference": "ad732c2e9299c9743f9c55ae53cc0e7642ab1155", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63", + "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63", "shasum": "" }, "require": { @@ -1233,7 +2367,7 @@ "react/event-loop": "^1.5", "react/socket": "^1.16", "react/stream": "^1.4", - "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0", "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", @@ -1247,17 +2381,18 @@ "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.7", - "infection/infection": "^0.31.0", - "justinrainbow/json-schema": "^6.5", - "keradus/cli-executor": "^2.2", + "facile-it/paraunit": "^1.3.1 || ^2.7.1", + "infection/infection": "^0.32.3", + "justinrainbow/json-schema": "^6.6.4", + "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.9", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", - "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2 || ^8.0", - "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2 || ^8.0" + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51", + "symfony/polyfill-php85": "^1.33", + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -1272,7 +2407,7 @@ "PhpCsFixer\\": "src/" }, "exclude-from-classmap": [ - "src/Fixer/Internal/*" + "src/**/Internal/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1298,7 +2433,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.90.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.2" }, "funding": [ { @@ -1306,7 +2441,7 @@ "type": "github" } ], - "time": "2025-11-20T15:15:16+00:00" + "time": "2026-02-20T16:13:53+00:00" }, { "name": "kint-php/kint", @@ -1487,33 +2622,30 @@ }, { "name": "nexusphp/cs-config", - "version": "v3.26.4", + "version": "v3.28.1", "source": { "type": "git", "url": "https://github.com/NexusPHP/cs-config.git", - "reference": "21cddae9917ec7b98e0b6890540222b658a4bf6e" + "reference": "4175b75a053b35dc64f37727df526520efb456f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NexusPHP/cs-config/zipball/21cddae9917ec7b98e0b6890540222b658a4bf6e", - "reference": "21cddae9917ec7b98e0b6890540222b658a4bf6e", + "url": "https://api.github.com/repos/NexusPHP/cs-config/zipball/4175b75a053b35dc64f37727df526520efb456f8", + "reference": "4175b75a053b35dc64f37727df526520efb456f8", "shasum": "" }, "require": { "ext-tokenizer": "*", - "friendsofphp/php-cs-fixer": "^3.84", - "php": "^8.1" - }, - "conflict": { - "liaison/cs-config": "*" + "friendsofphp/php-cs-fixer": "^3.94", + "php": "^8.2" }, "require-dev": { - "nexusphp/tachycardia": "^2.1", + "nexusphp/tachycardia": "^2.4", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^10.5 || ^11.0" + "phpunit/phpunit": "^11.5 || ^12.5" }, "type": "library", "autoload": { @@ -1537,20 +2669,20 @@ "slack": "https://nexusphp.slack.com", "source": "https://github.com/NexusPHP/cs-config.git" }, - "time": "2025-09-27T13:51:19+00:00" + "time": "2026-02-22T12:03:01+00:00" }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -1593,9 +2725,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -1717,35 +2849,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.11", + "version": "11.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", "sebastian/code-unit-reverse-lookup": "^4.0.1", "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/lines-of-code": "^3.0.1", "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^11.5.46" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -1783,7 +2915,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" }, "funding": [ { @@ -1803,32 +2935,32 @@ "type": "tidelift" } ], - "time": "2025-08-27T14:37:49+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1856,15 +2988,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2026-02-02T13:52:54+00:00" }, { "name": "phpunit/php-invoker", @@ -2052,16 +3196,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.44", + "version": "11.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82" + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", "shasum": "" }, "require": { @@ -2075,19 +3219,20 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.11", - "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.2", + "sebastian/comparator": "^6.3.3", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.1", "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" @@ -2133,7 +3278,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.44" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" }, "funding": [ { @@ -2157,7 +3302,7 @@ "type": "tidelift" } ], - "time": "2025-11-13T07:17:35+00:00" + "time": "2026-02-18T12:37:06+00:00" }, { "name": "predis/predis", @@ -2398,16 +3543,16 @@ }, { "name": "react/child-process", - "version": "v0.6.6", + "version": "v0.6.7", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", "shasum": "" }, "require": { @@ -2461,7 +3606,7 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.6" + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" }, "funding": [ { @@ -2469,7 +3614,7 @@ "type": "open_collective" } ], - "time": "2025-01-01T16:37:48+00:00" + "time": "2025-12-23T15:25:20+00:00" }, { "name": "react/dns", @@ -3022,16 +4167,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.2", + "version": "6.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", "shasum": "" }, "require": { @@ -3090,7 +4235,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" }, "funding": [ { @@ -3110,7 +4255,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T08:07:46+00:00" + "time": "2026-01-24T09:26:40+00:00" }, { "name": "sebastian/complexity", @@ -3890,16 +5035,16 @@ }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", "shasum": "" }, "require": { @@ -3907,7 +5052,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -3921,16 +5066,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3964,7 +5109,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v7.4.8" }, "funding": [ { @@ -3984,87 +5129,20 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.6.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "f57b899fa736fd71121168ef268f23c206083f0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a", + "reference": "f57b899fa736fd71121168ef268f23c206083f0a", "shasum": "" }, "require": { @@ -4081,13 +5159,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4115,7 +5194,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8" }, "funding": [ { @@ -4135,7 +5214,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4215,16 +5294,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", "shasum": "" }, "require": { @@ -4233,7 +5312,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4261,7 +5340,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.8" }, "funding": [ { @@ -4281,27 +5360,27 @@ "type": "tidelift" } ], - "time": "2025-11-05T09:52:27+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "e0be088d22278583a82da281886e8c3592fbf149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4329,7 +5408,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v7.4.8" }, "funding": [ { @@ -4349,20 +5428,20 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", "shasum": "" }, "require": { @@ -4400,7 +5479,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v7.4.8" }, "funding": [ { @@ -4420,7 +5499,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4672,91 +5751,6 @@ ], "time": "2024-09-09T11:45:10+00:00" }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-23T08:48:59+00:00" - }, { "name": "symfony/polyfill-php80", "version": "v1.33.0", @@ -4921,98 +5915,18 @@ ], "time": "2024-09-09T11:45:10+00:00" }, - { - "name": "symfony/polyfill-php84", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php84\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-06-24T13:30:11+00:00" - }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", "shasum": "" }, "require": { @@ -5044,7 +5958,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.8" }, "funding": [ { @@ -5064,7 +5978,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/service-contracts", @@ -5155,16 +6069,16 @@ }, { "name": "symfony/stopwatch", - "version": "v7.3.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89", "shasum": "" }, "require": { @@ -5197,7 +6111,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" + "source": "https://github.com/symfony/stopwatch/tree/v7.4.8" }, "funding": [ { @@ -5208,31 +6122,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-24T10:49:57+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "114ac57257d75df748eda23dd003878080b8e688" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688", + "reference": "114ac57257d75df748eda23dd003878080b8e688", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -5240,11 +6159,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5283,7 +6202,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v7.4.8" }, "funding": [ { @@ -5303,7 +6222,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "theseer/tokenizer", diff --git a/tests/Libraries/InvoiceAttachment/InvoiceAttachmentGeneratorTest.php b/tests/Libraries/InvoiceAttachment/InvoiceAttachmentGeneratorTest.php new file mode 100644 index 000000000..84fef6bf2 --- /dev/null +++ b/tests/Libraries/InvoiceAttachment/InvoiceAttachmentGeneratorTest.php @@ -0,0 +1,121 @@ +assertInstanceOf(InvoiceAttachmentGenerator::class, $generator); + } + + public function testCreateFromConfigUblOnly(): void + { + $generator = InvoiceAttachmentGenerator::createFromConfig('ubl_only'); + $this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator); + } + + public function testCreateFromConfigBoth(): void + { + $generator = InvoiceAttachmentGenerator::createFromConfig('both'); + $this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator); + } + + public function testCreateFromConfigPdfOnlyRegistersPdfAttachment(): void + { + $generator = InvoiceAttachmentGenerator::createFromConfig('pdf_only'); + $attachments = $this->getPrivateProperty($generator, 'attachments'); + + $this->assertCount(1, $attachments); + $this->assertInstanceOf(PdfAttachment::class, $attachments[0]); + } + + public function testCreateFromConfigUblOnlyRegistersUblAttachment(): void + { + $generator = InvoiceAttachmentGenerator::createFromConfig('ubl_only'); + $attachments = $this->getPrivateProperty($generator, 'attachments'); + + $this->assertCount(1, $attachments); + $this->assertInstanceOf(UblAttachment::class, $attachments[0]); + } + + public function testCreateFromConfigBothRegistersBothAttachments(): void + { + $generator = InvoiceAttachmentGenerator::createFromConfig('both'); + $attachments = $this->getPrivateProperty($generator, 'attachments'); + + $this->assertCount(2, $attachments); + $this->assertInstanceOf(PdfAttachment::class, $attachments[0]); + $this->assertInstanceOf(UblAttachment::class, $attachments[1]); + } + + public function testRegisterAddsAttachment(): void + { + $generator = new InvoiceAttachmentGenerator(); + $mockAttachment = new class implements InvoiceAttachment { + public function generate(array $saleData, string $type): ?string { return null; } + public function isApplicableForType(string $type, array $saleData): bool { return true; } + public function getFileExtension(): string { return 'test'; } + public function getEnabledConfigValues(): array { return ['test']; } + }; + + $result = $generator->register($mockAttachment); + + $this->assertSame($generator, $result); + $attachments = $this->getPrivateProperty($generator, 'attachments'); + $this->assertCount(1, $attachments); + } + + public function testRegisterIsChainable(): void + { + $generator = new InvoiceAttachmentGenerator(); + $mockAttachment = new class implements InvoiceAttachment { + public function generate(array $saleData, string $type): ?string { return null; } + public function isApplicableForType(string $type, array $saleData): bool { return true; } + public function getFileExtension(): string { return 'test'; } + public function getEnabledConfigValues(): array { return ['test']; } + }; + + $result = $generator->register($mockAttachment)->register($mockAttachment); + + $attachments = $this->getPrivateProperty($result, 'attachments'); + $this->assertCount(2, $attachments); + } + + public function testGenerateAttachmentsReturnsEmptyArrayWhenNoAttachmentsRegistered(): void + { + $generator = new InvoiceAttachmentGenerator(); + $result = $generator->generateAttachments([], 'invoice'); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + public function testCleanupRemovesFiles(): void + { + $tempFile1 = tempnam(sys_get_temp_dir(), 'test_'); + $tempFile2 = tempnam(sys_get_temp_dir(), 'test_'); + + $this->assertFileExists($tempFile1); + $this->assertFileExists($tempFile2); + + InvoiceAttachmentGenerator::cleanup([$tempFile1, $tempFile2]); + + $this->assertFileDoesNotExist($tempFile1); + $this->assertFileDoesNotExist($tempFile2); + } + + public function testCleanupHandlesNonExistentFiles(): void + { + // Should not throw an exception + InvoiceAttachmentGenerator::cleanup(['/non/existent/file1', '/non/existent/file2']); + $this->assertTrue(true); + } +} \ No newline at end of file diff --git a/tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php b/tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php new file mode 100644 index 000000000..6c8b92d07 --- /dev/null +++ b/tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php @@ -0,0 +1,70 @@ +attachment = new PdfAttachment(); + } + + public function testGetFileExtensionReturnsPdf(): void + { + $this->assertEquals('pdf', $this->attachment->getFileExtension()); + } + + public function testGetEnabledConfigValuesReturnsCorrectArray(): void + { + $values = $this->attachment->getEnabledConfigValues(); + + $this->assertIsArray($values); + $this->assertContains('pdf_only', $values); + $this->assertContains('both', $values); + $this->assertCount(2, $values); + } + + public function testIsApplicableForTypeReturnsTrueForInvoice(): void + { + $this->assertTrue($this->attachment->isApplicableForType('invoice', [])); + } + + public function testIsApplicableForTypeReturnsTrueForTaxInvoice(): void + { + $this->assertTrue($this->attachment->isApplicableForType('tax_invoice', [])); + } + + public function testIsApplicableForTypeReturnsTrueForQuote(): void + { + $this->assertTrue($this->attachment->isApplicableForType('quote', [])); + } + + public function testIsApplicableForTypeReturnsTrueForWorkOrder(): void + { + $this->assertTrue($this->attachment->isApplicableForType('work_order', [])); + } + + public function testIsApplicableForTypeReturnsTrueForReceipt(): void + { + $this->assertTrue($this->attachment->isApplicableForType('receipt', [])); + } + + public function testIsApplicableForTypeReturnsTrueForAnyType(): void + { + // PDF should work for any document type + $this->assertTrue($this->attachment->isApplicableForType('random_type', [])); + } + + public function testIsApplicableForTypeIgnoresSaleData(): void + { + // PDF attachment doesn't depend on invoice_number + $this->assertTrue($this->attachment->isApplicableForType('invoice', ['invoice_number' => null])); + $this->assertTrue($this->attachment->isApplicableForType('invoice', ['invoice_number' => 'INV-001'])); + } +} \ No newline at end of file diff --git a/tests/Libraries/InvoiceAttachment/UblAttachmentTest.php b/tests/Libraries/InvoiceAttachment/UblAttachmentTest.php new file mode 100644 index 000000000..3546e32d4 --- /dev/null +++ b/tests/Libraries/InvoiceAttachment/UblAttachmentTest.php @@ -0,0 +1,103 @@ +attachment = new UblAttachment(); + } + + public function testGetFileExtensionReturnsXml(): void + { + $this->assertEquals('xml', $this->attachment->getFileExtension()); + } + + public function testGetEnabledConfigValuesReturnsCorrectArray(): void + { + $values = $this->attachment->getEnabledConfigValues(); + + $this->assertIsArray($values); + $this->assertContains('ubl_only', $values); + $this->assertContains('both', $values); + $this->assertCount(2, $values); + } + + public function testIsApplicableForTypeReturnsTrueForInvoiceWithInvoiceNumber(): void + { + $saleData = ['invoice_number' => 'INV-001']; + + $this->assertTrue($this->attachment->isApplicableForType('invoice', $saleData)); + } + + public function testIsApplicableForTypeReturnsTrueForTaxInvoiceWithInvoiceNumber(): void + { + $saleData = ['invoice_number' => 'INV-001']; + + $this->assertTrue($this->attachment->isApplicableForType('tax_invoice', $saleData)); + } + + public function testIsApplicableForTypeReturnsFalseForInvoiceWithoutInvoiceNumber(): void + { + $saleData = ['invoice_number' => null]; + + $this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData)); + } + + public function testIsApplicableForTypeReturnsFalseForInvoiceWithEmptyInvoiceNumber(): void + { + $saleData = ['invoice_number' => '']; + + $this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData)); + } + + public function testIsApplicableForTypeReturnsFalseForInvoiceWithoutInvoiceNumberKey(): void + { + $saleData = []; + + $this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData)); + } + + public function testIsApplicableForTypeReturnsFalseForQuoteEvenWithInvoiceNumber(): void + { + $saleData = ['invoice_number' => 'INV-001']; + + $this->assertFalse($this->attachment->isApplicableForType('quote', $saleData)); + } + + public function testIsApplicableForTypeReturnsFalseForWorkOrderEvenWithInvoiceNumber(): void + { + $saleData = ['invoice_number' => 'INV-001']; + + $this->assertFalse($this->attachment->isApplicableForType('work_order', $saleData)); + } + + public function testIsApplicableForTypeReturnsFalseForReceiptEvenWithInvoiceNumber(): void + { + $saleData = ['invoice_number' => 'INV-001']; + + $this->assertFalse($this->attachment->isApplicableForType('receipt', $saleData)); + } + + public function testIsApplicableForTypeReturnsFalseForUnknownType(): void + { + $saleData = ['invoice_number' => 'INV-001']; + + $this->assertFalse($this->attachment->isApplicableForType('unknown_type', $saleData)); + } + + public function testGenerateReturnsNullForMissingConfig(): void + { + // Without proper sale_data, generate should fail gracefully + $result = $this->attachment->generate([], 'invoice'); + + $this->assertNull($result); + } +} \ No newline at end of file