From a5fec9c530c013490a47fa1a34ff714b55494ca9 Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:14:06 -0600 Subject: [PATCH 001/177] Add webhook secret and signature verification Added webhook_secret property and implemented signature verification for Stripe webhook requests. Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- app/PaymentDrivers/StripePaymentDriver.php | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 6607ceb38e..1ebaa0b6e0 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -85,6 +85,8 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac public $stripe_connect_auth = []; + public $webhook_secret = ""; + public static $methods = [ GatewayType::CREDIT_CARD => CreditCard::class, GatewayType::BANK_TRANSFER => ACH::class, @@ -122,6 +124,8 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac throw new StripeConnectFailure('Stripe Connect has not been configured'); } } else { + $this->webhook_secret = $this->company_gateway->getConfigField('webhookSecret'); + $this->stripe = new StripeClient( $this->company_gateway->getConfigField('apiKey') ); @@ -698,6 +702,27 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac public function processWebhookRequest(PaymentWebhookRequest $request) { + // Validate webhook signature if webhook_secret is configured + if ($this->webhook_secret) { + $sig_header = $_SERVER["HTTP_STRIPE_SIGNATURE"] ?? $request->header('Stripe-Signature'); + + if (!$sig_header) { + nlog("Stripe webhook signature verification failed: No signature header"); + return response()->json(['error' => 'No signature header'], 403); + } + + try { + \Stripe\Webhook::constructEvent( + $request->getContent(), + $sig_header, + $this->webhook_secret + ); + } catch (\Stripe\Exception\SignatureVerificationException $e) { + nlog("Stripe webhook signature verification failed: " . $e->getMessage()); + return response()->json(['error' => 'Invalid signature'], 403); + } + } + nlog($request->all()); if ($request->type === 'customer.source.updated') { From d7d9559db60b6fde35c233d95cecb0915350d7ad Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:16:38 -0600 Subject: [PATCH 002/177] Modify Stripe fields in PaymentLibrariesSeeder Updated Stripe payment provider fields to include webhookSecret. Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- database/seeders/PaymentLibrariesSeeder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index 1cbd33d488..dab19b70b2 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -47,7 +47,7 @@ class PaymentLibrariesSeeder extends Seeder ['id' => 17, 'name' => 'Pin', 'provider' => 'Pin', 'key' => '0749cb92a6b36c88bd9ff8aabd2efcab', 'fields' => '{"secretKey":"","testMode":false}'], ['id' => 18, 'name' => 'SagePay Direct', 'provider' => 'SagePay_Direct', 'key' => '4c8f4e5d0f353a122045eb9a60cc0f2d', 'fields' => '{"vendor":"","testMode":false,"referrerId":""}'], ['id' => 19, 'name' => 'SecurePay DirectPost', 'provider' => 'SecurePay_DirectPost', 'key' => '8036a5aadb2bdaafb23502da8790b6a2', 'fields' => '{"merchantId":"","transactionPassword":"","testMode":false,"enable_ach":"","enable_sofort":"","enable_apple_pay":"","enable_alipay":""}'], - ['id' => 20, 'name' => 'Stripe', 'provider' => 'Stripe', 'sort_order' => 1, 'key' => 'd14dd26a37cecc30fdd65700bfb55b23', 'fields' => '{"publishableKey":"","apiKey":"","appleDomainVerification":""}'], + ['id' => 20, 'name' => 'Stripe', 'provider' => 'Stripe', 'sort_order' => 1, 'key' => 'd14dd26a37cecc30fdd65700bfb55b23', 'fields' => '{"publishableKey":"","apiKey":"","webhookSecret":"","appleDomainVerification":""}'], ['id' => 21, 'name' => 'TargetPay Direct eBanking', 'provider' => 'TargetPay_Directebanking', 'key' => 'd14dd26a37cdcc30fdd65700bfb55b23', 'fields' => '{"subAccountId":""}'], ['id' => 22, 'name' => 'TargetPay Ideal', 'provider' => 'TargetPay_Ideal', 'key' => 'ea3b328bd72d381387281c3bd83bd97c', 'fields' => '{"subAccountId":""}'], ['id' => 23, 'name' => 'TargetPay Mr Cash', 'provider' => 'TargetPay_Mrcash', 'key' => 'a0035fc0d87c4950fb82c73e2fcb825a', 'fields' => '{"subAccountId":""}'], From 132c7529a0ebbbe4c8f72f4e2df92dce00b4f019 Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:19:27 -0600 Subject: [PATCH 003/177] Initialize webhook secret in processWebhookRequest Add initialization for webhook secret handling in Stripe payment driver. Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- app/PaymentDrivers/StripePaymentDriver.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 1ebaa0b6e0..c566976b42 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -702,6 +702,15 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac public function processWebhookRequest(PaymentWebhookRequest $request) { + // Initialize to load webhook_secret and other config + try { + $this->init(); + } catch (\Exception $e) { + nlog("Stripe webhook init failed: " . $e->getMessage()); + // Continue without webhook secret verification if init fails + } + + // Validate webhook signature if webhook_secret is configured if ($this->webhook_secret) { $sig_header = $_SERVER["HTTP_STRIPE_SIGNATURE"] ?? $request->header('Stripe-Signature'); From 249f4bb5c4adcb8817e8aa3efb84d5134e5d80fc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 28 Nov 2025 17:34:39 +1100 Subject: [PATCH 004/177] Adjustments for validating payments, preventing a negative amount being applied to a positive invoice --- app/Http/ValidationRules/Payment/ValidInvoicesRules.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Http/ValidationRules/Payment/ValidInvoicesRules.php b/app/Http/ValidationRules/Payment/ValidInvoicesRules.php index cc6fb42712..8c649bbb6a 100644 --- a/app/Http/ValidationRules/Payment/ValidInvoicesRules.php +++ b/app/Http/ValidationRules/Payment/ValidInvoicesRules.php @@ -85,6 +85,9 @@ class ValidInvoicesRules implements Rule } elseif ($inv->status_id == Invoice::STATUS_DRAFT && floatval($invoice['amount']) > floatval($inv->amount)) { $this->error_msg = 'Amount cannot be greater than invoice balance'; return false; + } elseif($invoice['amount'] < 0 && $inv->amount >= 0) { + $this->error_msg = 'Amount cannot be negative'; + return false; } elseif (floatval($invoice['amount']) > floatval($inv->balance)) { $this->error_msg = ctrans('texts.amount_greater_than_balance_v5'); return false; From e9b5781fe9943fbd5221680e0e32142783dbe296 Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Sat, 29 Nov 2025 12:59:50 -0600 Subject: [PATCH 005/177] Refactor Stripe webhook request processing Refactor webhook processing to simplify signature verification and log request data. Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- app/PaymentDrivers/StripePaymentDriver.php | 27 +++++----------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index c566976b42..0cba964040 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -133,6 +133,7 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac Stripe::setApiKey($this->company_gateway->getConfigField('apiKey')); Stripe::setAPiVersion('2023-10-16'); } + $this->webhook_secret = $this->company_gateway->getConfigField('webhookSecret'); return $this; } @@ -702,38 +703,22 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac public function processWebhookRequest(PaymentWebhookRequest $request) { - // Initialize to load webhook_secret and other config - try { - $this->init(); - } catch (\Exception $e) { - nlog("Stripe webhook init failed: " . $e->getMessage()); - // Continue without webhook secret verification if init fails - } - - - // Validate webhook signature if webhook_secret is configured - if ($this->webhook_secret) { + nlog($request->all()); + $webhook_secret = $this->company_gateway->getConfigField('webhookSecret'); + + if ($webhook_secret) { $sig_header = $_SERVER["HTTP_STRIPE_SIGNATURE"] ?? $request->header('Stripe-Signature'); - if (!$sig_header) { nlog("Stripe webhook signature verification failed: No signature header"); return response()->json(['error' => 'No signature header'], 403); } - try { \Stripe\Webhook::constructEvent( $request->getContent(), $sig_header, - $this->webhook_secret - ); - } catch (\Stripe\Exception\SignatureVerificationException $e) { - nlog("Stripe webhook signature verification failed: " . $e->getMessage()); - return response()->json(['error' => 'Invalid signature'], 403); - } + $webhook_secret } - nlog($request->all()); - if ($request->type === 'customer.source.updated') { $ach = new ACH($this); $ach->updateBankAccount($request->all()); From 359abcfb5c6fc33976ee68e72a8c0738fd95c7b7 Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Sat, 29 Nov 2025 16:07:52 -0600 Subject: [PATCH 006/177] Fix Stripe webhook signature verification handling Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- app/PaymentDrivers/StripePaymentDriver.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 0cba964040..0c24a03101 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -717,8 +717,13 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac $request->getContent(), $sig_header, $webhook_secret + }; + } catch (\Stripe\Exception\SignatureVerificationException $e) { + nlog("Stripe webhook signature verification failed: " . $e->getMessage()); + return response()->json(['error' => 'Invalid signature'], 403); } - + } + if ($request->type === 'customer.source.updated') { $ach = new ACH($this); $ach->updateBankAccount($request->all()); From 8a6bfcd0a3a8ec9d4249867ddfac496cca3c8000 Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Sat, 29 Nov 2025 16:24:11 -0600 Subject: [PATCH 007/177] Refactor StripePaymentDriver webhook handling Removed assignment of webhook_secret and fixed syntax error in webhook event construction. Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- app/PaymentDrivers/StripePaymentDriver.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 0c24a03101..784fa74152 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -133,7 +133,6 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac Stripe::setApiKey($this->company_gateway->getConfigField('apiKey')); Stripe::setAPiVersion('2023-10-16'); } - $this->webhook_secret = $this->company_gateway->getConfigField('webhookSecret'); return $this; } @@ -717,12 +716,11 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac $request->getContent(), $sig_header, $webhook_secret - }; + ); } catch (\Stripe\Exception\SignatureVerificationException $e) { nlog("Stripe webhook signature verification failed: " . $e->getMessage()); return response()->json(['error' => 'Invalid signature'], 403); } - } if ($request->type === 'customer.source.updated') { $ach = new ACH($this); From 90b135e21124e58cba46a2734ca847ba02fe8a11 Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:31:21 -0600 Subject: [PATCH 008/177] Fix webhook signature verification logic Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- app/PaymentDrivers/StripePaymentDriver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 784fa74152..578aebb17c 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -711,6 +711,7 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac nlog("Stripe webhook signature verification failed: No signature header"); return response()->json(['error' => 'No signature header'], 403); } + } try { \Stripe\Webhook::constructEvent( $request->getContent(), From b30dbf633a8c4141a8cb1bd150a37e306d976703 Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:43:17 -0600 Subject: [PATCH 009/177] Fix webhook signature verification handling Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- app/PaymentDrivers/StripePaymentDriver.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 578aebb17c..acb21e4b09 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -711,16 +711,16 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac nlog("Stripe webhook signature verification failed: No signature header"); return response()->json(['error' => 'No signature header'], 403); } - } try { \Stripe\Webhook::constructEvent( $request->getContent(), $sig_header, $webhook_secret - ); - } catch (\Stripe\Exception\SignatureVerificationException $e) { - nlog("Stripe webhook signature verification failed: " . $e->getMessage()); - return response()->json(['error' => 'Invalid signature'], 403); + ); + } catch (\Stripe\Exception\SignatureVerificationException $e) { + nlog("Stripe webhook signature verification failed: " . $e->getMessage()); + return response()->json(['error' => 'Invalid signature'], 403); + } } if ($request->type === 'customer.source.updated') { From ffaf8d03b341db5ee3fcdfd2246b9b561cd4e1fa Mon Sep 17 00:00:00 2001 From: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:22:20 -0600 Subject: [PATCH 010/177] Remove webhook_secret from StripePaymentDriver Removed webhook_secret property and its assignment. Signed-off-by: TechNoNerd87 <113461509+TechNoNerd87@users.noreply.github.com> --- app/PaymentDrivers/StripePaymentDriver.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index acb21e4b09..8ce3c9ad7f 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -85,8 +85,6 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac public $stripe_connect_auth = []; - public $webhook_secret = ""; - public static $methods = [ GatewayType::CREDIT_CARD => CreditCard::class, GatewayType::BANK_TRANSFER => ACH::class, @@ -124,7 +122,6 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac throw new StripeConnectFailure('Stripe Connect has not been configured'); } } else { - $this->webhook_secret = $this->company_gateway->getConfigField('webhookSecret'); $this->stripe = new StripeClient( $this->company_gateway->getConfigField('apiKey') From 727005237bc3f69b48fa0b775b986eaa17e81242 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 4 Dec 2025 22:57:10 +1100 Subject: [PATCH 011/177] Updated domains --- app/Http/ValidationRules/Account/BlackListRule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 88a16cc8f4..320e9f4528 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -22,6 +22,7 @@ class BlackListRule implements ValidationRule { /** Bad domains +/- disposable email domains */ private array $blacklist = [ + "comfythings.com", "edu.pk", "bablace.com", "moonfee.com", From 02c1dbd00a5bd49a6148f2de4e93bea76c316630 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 5 Dec 2025 08:46:16 +1100 Subject: [PATCH 012/177] QR code with balance, not amount --- app/Utils/HtmlEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 47977b273e..66f736210e 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -816,7 +816,7 @@ class HtmlEngine } if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') { - $data['$sepa_qr_code'] = ['value' => (new EpcQrGenerator($this->company, $this->entity, $data['$amount_raw']['value']))->getQrCode(), 'label' => '']; + $data['$sepa_qr_code'] = ['value' => (new EpcQrGenerator($this->company, $this->entity, $data['$balance_due_raw']['value']))->getQrCode(), 'label' => '']; $data['$sepa_qr_code_raw'] = ['value' => html_entity_decode($data['$sepa_qr_code']['value']), 'label' => '']; } From b29a0cf1d6a637cf79e31acf00f9e7390b3ba3d3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 5 Dec 2025 09:49:27 +1100 Subject: [PATCH 013/177] Fixes for logic on credit numbering --- app/Services/Invoice/ApplyNumber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/Invoice/ApplyNumber.php b/app/Services/Invoice/ApplyNumber.php index 3b2665216f..1f78ca8239 100644 --- a/app/Services/Invoice/ApplyNumber.php +++ b/app/Services/Invoice/ApplyNumber.php @@ -75,7 +75,7 @@ class ApplyNumber extends AbstractService do { try { - if($peppol_enabled && strlen(trim($this->client->getSetting('credit_number_pattern'))) > 0) { + if($peppol_enabled && strlen(trim($this->client->getSetting('credit_number_pattern'))) > 0 && $this->invoice->amount < 0) { $this->invoice->number = $this->getPeppolCreditNumber($this->client, $this->invoice); } else { From 4cabafbddf5f4fbf5c2e91ab465601f5fee2fc7f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 5 Dec 2025 09:49:46 +1100 Subject: [PATCH 014/177] v5.12.39 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 63e89ae548..5b37f41f83 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.12.38 \ No newline at end of file +5.12.39 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 2b7ba5a361..6cefee3210 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.12.38'), - 'app_tag' => env('APP_TAG', '5.12.38'), + 'app_version' => env('APP_VERSION', '5.12.39'), + 'app_tag' => env('APP_TAG', '5.12.39'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), From 5684e68893410634fad95892723de6c3b9627d5d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 5 Dec 2025 10:15:36 +1100 Subject: [PATCH 015/177] Fixes for no invitations present yet! --- app/Utils/Traits/Notifications/UserNotifies.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Utils/Traits/Notifications/UserNotifies.php b/app/Utils/Traits/Notifications/UserNotifies.php index 4b684bff5f..e1786110af 100644 --- a/app/Utils/Traits/Notifications/UserNotifies.php +++ b/app/Utils/Traits/Notifications/UserNotifies.php @@ -31,6 +31,10 @@ trait UserNotifies { public function findUserNotificationTypes($invitation, $company_user, $entity_name, $required_permissions): array { + if(!$invitation) { + return []; + } + $notifiable_methods = []; $notifications = $company_user->notifications; From 74342ec3f0a2a26e9dc1b61be376265ff08f7eec Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 5 Dec 2025 11:01:05 +1100 Subject: [PATCH 016/177] Add subtotal as a report option --- app/Export/CSV/BaseExport.php | 4 ++++ app/Export/CSV/CreditExport.php | 3 +++ app/Export/CSV/InvoiceExport.php | 3 +++ app/Export/CSV/InvoiceItemExport.php | 5 +++++ app/Export/CSV/PurchaseOrderExport.php | 3 +++ app/Export/CSV/PurchaseOrderItemExport.php | 4 ++++ app/Export/CSV/QuoteExport.php | 3 +++ app/Export/CSV/QuoteItemExport.php | 3 +++ 8 files changed, 28 insertions(+) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 180f6011de..1c405c1f17 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -144,6 +144,7 @@ class BaseExport 'name' => 'client.name', "currency" => "client.currency_id", "invoice_number" => "invoice.number", + "subtotal" => "invoice.subtotal", "amount" => "invoice.amount", "balance" => "invoice.balance", "paid_to_date" => "invoice.paid_to_date", @@ -259,6 +260,7 @@ class BaseExport 'terms' => 'purchase_order.terms', 'total_taxes' => 'purchase_order.total_taxes', 'currency_id' => 'purchase_order.currency_id', + 'subtotal' => 'purchase_order.subtotal', ]; protected array $product_report_keys = [ @@ -348,6 +350,7 @@ class BaseExport 'tax_rate1' => 'quote.tax_rate1', 'tax_rate2' => 'quote.tax_rate2', 'tax_rate3' => 'quote.tax_rate3', + 'subtotal' => 'quote.subtotal', ]; protected array $credit_report_keys = [ @@ -382,6 +385,7 @@ class BaseExport "tax_amount" => "credit.total_taxes", "assigned_user" => "credit.assigned_user_id", "user" => "credit.user_id", + 'subtotal' => 'credit.subtotal', ]; protected array $payment_report_keys = [ diff --git a/app/Export/CSV/CreditExport.php b/app/Export/CSV/CreditExport.php index c8ca976d68..0933b15697 100644 --- a/app/Export/CSV/CreditExport.php +++ b/app/Export/CSV/CreditExport.php @@ -251,6 +251,9 @@ class CreditExport extends BaseExport $entity['credit.user_id'] = $credit->user ? $credit->user->present()->name() : ''; //@phpstan-ignore-line } + if (in_array('credit.subtotal', $this->input['report_keys'])) { + $entity['credit.subtotal'] = $credit->calc()->getSubTotal(); + } return $entity; } } diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 318073c838..4d209cb451 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -249,7 +249,10 @@ class InvoiceExport extends BaseExport if (in_array('invoice.user_id', $this->input['report_keys'])) { $entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : ''; // @phpstan-ignore-line + } + if (in_array('invoice.subtotal', $this->input['report_keys'])) { + $entity['invoice.subtotal'] = $invoice->calc()->getSubTotal(); } return $entity; diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index c0eff39803..a0d5cf747b 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -282,6 +282,11 @@ class InvoiceItemExport extends BaseExport $entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';// @phpstan-ignore-line } + if (in_array('invoice.subtotal', $this->input['report_keys'])) { + $entity['invoice.subtotal'] = $invoice->calc()->getSubTotal(); + } + + return $entity; } diff --git a/app/Export/CSV/PurchaseOrderExport.php b/app/Export/CSV/PurchaseOrderExport.php index 13bf41a2cf..9b71da2167 100644 --- a/app/Export/CSV/PurchaseOrderExport.php +++ b/app/Export/CSV/PurchaseOrderExport.php @@ -184,6 +184,9 @@ class PurchaseOrderExport extends BaseExport $entity['purchase_order.assigned_user_id'] = $purchase_order->assigned_user ? $purchase_order->assigned_user->present()->name() : ''; } + if (in_array('purchase_order.subtotal', $this->input['report_keys'])) { + $entity['purchase_order.subtotal'] = $purchase_order->calc()->getSubTotal(); + } return $entity; } diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php index 3ec6141ba5..6194b312ea 100644 --- a/app/Export/CSV/PurchaseOrderItemExport.php +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -249,6 +249,10 @@ class PurchaseOrderItemExport extends BaseExport $entity['purchase_order.assigned_user_id'] = $purchase_order->assigned_user ? $purchase_order->assigned_user->present()->name() : ''; } + if (in_array('purchase_order.subtotal', $this->input['report_keys'])) { + $entity['purchase_order.subtotal'] = $purchase_order->calc()->getSubTotal(); + } + return $entity; } diff --git a/app/Export/CSV/QuoteExport.php b/app/Export/CSV/QuoteExport.php index 98f300e231..2f1b422417 100644 --- a/app/Export/CSV/QuoteExport.php +++ b/app/Export/CSV/QuoteExport.php @@ -186,6 +186,9 @@ class QuoteExport extends BaseExport $entity['quote.user_id'] = $quote->user ? $quote->user->present()->name() : ''; } + if (in_array('quote.subtotal', $this->input['report_keys'])) { + $entity['quote.subtotal'] = $quote->calc()->getSubTotal(); + } return $entity; } diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index 7ad5649789..c5c10397af 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -249,6 +249,9 @@ class QuoteItemExport extends BaseExport } + if (in_array('quote.subtotal', $this->input['report_keys'])) { + $entity['quote.subtotal'] = $quote->calc()->getSubTotal(); + } return $entity; } From 9f65f355731f005ea6658f6ac631bff6e451b329 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 6 Dec 2025 09:50:27 +1100 Subject: [PATCH 017/177] Improve sorting of company gateways --- app/Services/Client/PaymentMethod.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/Services/Client/PaymentMethod.php b/app/Services/Client/PaymentMethod.php index ea04d38e34..1a882c9f70 100644 --- a/app/Services/Client/PaymentMethod.php +++ b/app/Services/Client/PaymentMethod.php @@ -29,6 +29,8 @@ class PaymentMethod private $payment_urls = []; + private $gateway_order = []; + public function __construct(private Client $client, private float $amount) { } @@ -51,7 +53,14 @@ class PaymentMethod return $methods->reject(function ($item) { return $item['gateway_type_id'] == '29'; //PayPal advanced credit cards, needs to be excluded here }); - })->toArray(); + }) + ->sortBy('sort_order') + ->map(function ($item) { + unset($item['sort_order']); // Remove the temporary sort field before returning + return $item; + }) + ->values() // Reset array keys + ->toArray(); return $this->payment_urls; @@ -75,6 +84,9 @@ class PaymentMethod $transformed_ids = []; } + // Store the gateway order: gateway_id => priority + $this->gateway_order = array_flip($transformed_ids); + $this->gateways = $this->client ->company ->company_gateways @@ -198,6 +210,7 @@ class PaymentMethod 'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT, 'gateway_type_id' => GatewayType::CREDIT, 'is_paypal' => false, + 'sort_order' => 9999, // Credits always appear last ]; } @@ -211,12 +224,16 @@ class PaymentMethod $fee_label = $gateway->calcGatewayFeeLabel($this->amount, $this->client, $type); + // Get the priority from gateway_order, default to 999 for unordered gateways + $priority = $this->gateway_order[$gateway->id] ?? 999; + if (! $type || (GatewayType::CUSTOM == $type)) { $this->payment_urls[] = [ 'label' => $gateway->getConfigField('name').$fee_label, 'company_gateway_id' => $gateway->id, 'gateway_type_id' => GatewayType::CREDIT_CARD, 'is_paypal' => $gateway->isPayPal(), + 'sort_order' => $priority, ]; } else { $this->payment_urls[] = [ @@ -224,6 +241,7 @@ class PaymentMethod 'company_gateway_id' => $gateway->id, 'gateway_type_id' => $type, 'is_paypal' => $gateway->isPayPal(), + 'sort_order' => $priority, ]; } From 3b5fee34e6b05ff416ec1dac22546182439cdcc3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 10 Dec 2025 11:53:13 +1100 Subject: [PATCH 018/177] Fixes for quarters --- app/Utils/Helpers.php | 20 ++++++++++++++++++++ app/Utils/Traits/Invoice/ActionsInvoice.php | 19 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/Utils/Helpers.php b/app/Utils/Helpers.php index f6fa5d8eec..67d371d95d 100644 --- a/app/Utils/Helpers.php +++ b/app/Utils/Helpers.php @@ -362,6 +362,26 @@ class Helpers ); } + if ($matches->keys()->first() == ':QUARTER') { + // Use date math to properly handle quarter wrapping (Q4+1 = Q1 of next year) + if ($_operation == '+') { + $final_date = $currentDateTime->copy()->addQuarters((int) $_value[1]); + } elseif ($_operation == '-') { + $final_date = $currentDateTime->copy()->subQuarters((int) $_value[1]); + } else { + // For division/multiplication, calculate target quarter and use date math + // Calculate how many quarters to add/subtract from current quarter + $quarters_to_add = $output - $currentDateTime->quarter; + $final_date = $currentDateTime->copy(); + if ($quarters_to_add != 0) { + $final_date = $quarters_to_add > 0 + ? $final_date->addQuarters($quarters_to_add) + : $final_date->subQuarters(abs($quarters_to_add)); + } + } + $output = $final_date->quarter; + } + $value = preg_replace( $target, $output, diff --git a/app/Utils/Traits/Invoice/ActionsInvoice.php b/app/Utils/Traits/Invoice/ActionsInvoice.php index 8947ea648f..e50b518e18 100644 --- a/app/Utils/Traits/Invoice/ActionsInvoice.php +++ b/app/Utils/Traits/Invoice/ActionsInvoice.php @@ -22,7 +22,24 @@ trait ActionsInvoice if($invoice->company->verifactuEnabled() && $invoice->amount < 0) { return false; } - return $invoice->isPayable(); + elseif($invoice->is_deleted) { + return false; + } + elseif(in_array($invoice->status_id, [Invoice::STATUS_CANCELLED, Invoice::STATUS_REVERSED])) { + return false; + } + elseif ($invoice->status_id == Invoice::STATUS_PAID) { + return false; + } + elseif ($invoice->status_id == Invoice::STATUS_DRAFT) { + return true; + } + elseif (in_array($invoice->status_id, [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) && $invoice->balance != 0) { + return true; + } + + return false; + } public function invoiceDeletable($invoice): bool From 98bc09be2f359478f37915818dec2059a1f2cd53 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 10 Dec 2025 11:54:26 +1100 Subject: [PATCH 019/177] Updated translations --- lang/en/texts.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lang/en/texts.php b/lang/en/texts.php index 65acdb85dd..7c524acb58 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5668,6 +5668,7 @@ $lang = array( 'use_legacy_editor_help' => 'Use the TinyMCE editor.', 'enable_e_invoice_received_notification' => 'Enable E-Invoice Received Notification', 'enable_e_invoice_received_notification_help' => 'Receive an email notification when a new E-Invoice is received.', + 'price_changes' => 'Plan Price Changes from January 1st 2026', ); return $lang; From 2699a3bb32003e54c25f0fbc7b91a8d3b7bf6180 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 10 Dec 2025 12:30:51 +1100 Subject: [PATCH 020/177] cleanup for deprecation notices --- app/Utils/Helpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Utils/Helpers.php b/app/Utils/Helpers.php index 67d371d95d..e6ba97e253 100644 --- a/app/Utils/Helpers.php +++ b/app/Utils/Helpers.php @@ -23,7 +23,7 @@ class Helpers { use MakesDates; - public static function sharedEmailVariables(?Client $client, array $settings = null): array + public static function sharedEmailVariables(?Client $client, ?array $settings = null): array { if (! $client) { $elements['signature'] = ''; From 003a5b3c4f6554d027e03d325177c0ebed937841 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 10 Dec 2025 12:32:28 +1100 Subject: [PATCH 021/177] Invoice designer v2 --- tests/Feature/Design/DesignParserTest.php | 122 +++ .../Feature/Design/InvoiceDesignRenderer.php | 992 ++++++++++++++++++ tests/Feature/Design/stubs/test_design_1.html | 145 +++ tests/Feature/Design/stubs/test_design_1.json | 213 ++++ .../Design/stubs/test_design_1_mock.html | 145 +++ 5 files changed, 1617 insertions(+) create mode 100644 tests/Feature/Design/DesignParserTest.php create mode 100644 tests/Feature/Design/InvoiceDesignRenderer.php create mode 100644 tests/Feature/Design/stubs/test_design_1.html create mode 100644 tests/Feature/Design/stubs/test_design_1.json create mode 100644 tests/Feature/Design/stubs/test_design_1_mock.html diff --git a/tests/Feature/Design/DesignParserTest.php b/tests/Feature/Design/DesignParserTest.php new file mode 100644 index 0000000000..a454da945a --- /dev/null +++ b/tests/Feature/Design/DesignParserTest.php @@ -0,0 +1,122 @@ +makeTestData(); + } + + public function test_design_parser() + { + + $designjson = file_get_contents(base_path('tests/Feature/Design/stubs/test_design_1.json')); + $design = json_decode($designjson, true); + + $renderer = new InvoiceDesignRenderer(); + $html = $renderer->render($design['blocks']); + $this->assertNotNull($html); + file_put_contents(base_path('tests/Feature/Design/stubs/test_design_1.html'), $html); + + + $design = [ + 'body' => $html, + 'includes' => '', + 'product' => '', + 'task' => '', + 'footer' => '', + 'header' => '', + ]; + + $data = [ + 'name' => $this->faker->firstName(), + 'design' => $design, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/designs', $data); + + + $arr = $response->json(); + $design_id = $arr['data']['id']; + + // $mock = new PdfMock([ + // 'entity_type' => 'invoice', + // 'settings_type' => 'company', + // 'design' => ['includes' => $html, 'header' => '', 'body' => '', 'footer' => ''], + // 'settings' => CompanySettings::defaults(), + // ], $company); + + // $mock->build(); + // $html = $mock->getHtml(); + // $this->assertNotNull($html); + + + + $item = InvoiceItemFactory::create(); + $item->quantity = 1.75; + $item->cost = 49.58; + $item->tax_name1 = 'mwst'; + $item->tax_rate1 = 19; + $item->type_id = '1'; + $item->tax_id = '1'; + $line_items[] = $item; + + + $i = Invoice::factory()->create([ + 'discount' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'line_items' => $line_items, + 'status_id' => 1, + 'uses_inclusive_taxes' => false, + 'is_amount_discount' => true, + 'design_id' => $this->decodePrimaryKey($design_id), + ]); + + $invoice_calc = new InvoiceSum($i); + $ii = $invoice_calc->build()->getInvoice(); + $ii = $ii->service()->createInvitations()->markSent()->save(); + + + $ps = new PdfService($ii->invitations()->first(), 'product', [ + 'client' => $this->client ?? false, + 'vendor' => false, + "invoices" => [$ii], + ]); + + $html = $ps->boot()->getHtml(); + + file_put_contents(base_path('tests/Feature/Design/stubs/test_design_1_mock.html'), $html); + } +} \ No newline at end of file diff --git a/tests/Feature/Design/InvoiceDesignRenderer.php b/tests/Feature/Design/InvoiceDesignRenderer.php new file mode 100644 index 0000000000..da7e66518e --- /dev/null +++ b/tests/Feature/Design/InvoiceDesignRenderer.php @@ -0,0 +1,992 @@ + tags containing Twig loop syntax for the backend to process. + * + * Variable Syntax (in JSON): + * - Line item fields: "item.product_key", "item.quantity", "item.cost", etc. + * - These are converted to Twig: {{ item.product_key }} + * + * Output Example (table): + * + * {% for item in invoice.line_items %} + * + * {{ item.product_key }} + * {{ item.quantity }} + * + * {% endfor %} + * + * + * @example + * $renderer = new InvoiceDesignRenderer(); + * $html = $renderer->render($designBlocks); + */ +class InvoiceDesignRenderer +{ + /** + * Grid system constants (must match frontend exactly) + */ + const GRID_COLS = 12; + const ROW_HEIGHT = 60; // pixels + const CANVAS_WIDTH = 794; // pixels (210mm at 96dpi) + const MARGIN_H = 10; // horizontal margin between columns + const MARGIN_V = 10; // vertical margin between rows + const PADDING_H = 30; // container horizontal padding + const PADDING_V = 30; // container vertical padding + + /** + * Page sizes in mm (portrait dimensions) + */ + const PAGE_SIZES = [ + 'a4' => ['width' => 210, 'height' => 297], + 'letter' => ['width' => 216, 'height' => 279], + 'legal' => ['width' => 216, 'height' => 356], + 'a3' => ['width' => 297, 'height' => 420], + 'a5' => ['width' => 148, 'height' => 210], + ]; + + /** + * Default page settings + */ + const DEFAULT_PAGE_SETTINGS = [ + 'pageSize' => 'a4', + 'orientation' => 'portrait', + 'marginTop' => '10mm', + 'marginRight' => '10mm', + 'marginBottom' => '10mm', + 'marginLeft' => '10mm', + 'backgroundColor' => '#ffffff', + 'fontFamily' => 'Inter, sans-serif', + 'fontSize' => '12px', + 'textColor' => '#374151', + 'lineHeight' => '1.5', + ]; + + /** + * Current page settings + */ + private array $pageSettings; + + /** + * Constructor + */ + public function __construct(array $pageSettings = []) + { + $this->pageSettings = array_merge(self::DEFAULT_PAGE_SETTINGS, $pageSettings); + } + + /** + * Render complete HTML document from blocks using flow-based layout + * This ensures content can grow and push other elements down naturally + * + * @param array $blocks Array of block objects from frontend + * @param array|null $pageSettings Optional page settings override + * @return string Complete HTML document + */ + public function render(array $blocks, ?array $pageSettings = null): string + { + // Merge page settings if provided + if ($pageSettings !== null) { + $this->pageSettings = array_merge(self::DEFAULT_PAGE_SETTINGS, $pageSettings); + } + // Sort blocks by Y position, then by X position + usort($blocks, function($a, $b) { + if ($a['gridPosition']['y'] !== $b['gridPosition']['y']) { + return $a['gridPosition']['y'] - $b['gridPosition']['y']; + } + return $a['gridPosition']['x'] - $b['gridPosition']['x']; + }); + + // Group blocks into rows + $rows = $this->groupBlocksIntoRows($blocks); + $rowsHTML = ''; + foreach ($rows as $row) { + $rowsHTML .= $this->renderRow($row); + } + + return $this->generateDocument($rowsHTML); + } + + /** + * Group blocks into rows based on similar Y positions + */ + private function groupBlocksIntoRows(array $blocks): array + { + $rows = []; + $currentRow = []; + $currentY = -1; + + foreach ($blocks as $block) { + $blockY = $block['gridPosition']['y']; + + if ($currentY === -1 || abs($blockY - $currentY) >= 1) { + if (!empty($currentRow)) { + $rows[] = $currentRow; + } + $currentRow = [$block]; + $currentY = $blockY; + } else { + $currentRow[] = $block; + } + } + + if (!empty($currentRow)) { + $rows[] = $currentRow; + } + + return $rows; + } + + /** + * Render a row of blocks + */ + private function renderRow(array $blocks): string + { + $blocksHTML = ''; + foreach ($blocks as $block) { + $blocksHTML .= $this->renderBlock($block); + } + + $rowClass = 'row'; + $rowStyle = ''; + + if (count($blocks) > 1) { + // Multiple blocks - use flex with gap + $rowClass = 'row flex-row'; + $rowStyle = 'gap: ' . self::MARGIN_H . 'px;'; + } elseif (count($blocks) === 1) { + // Single block - check if it needs alignment + $block = $blocks[0]; + $xPos = $block['gridPosition']['x']; + $width = $block['gridPosition']['w']; + + if ($xPos > 0) { + // Block is not at left edge - use flex for positioning + $rowClass = 'row flex-row'; + + if ($xPos + $width >= self::GRID_COLS) { + // Block is at right edge + $rowStyle = 'justify-content: flex-end;'; + } elseif ($xPos >= (self::GRID_COLS - $width) / 2 - 1 && $xPos <= (self::GRID_COLS - $width) / 2 + 1) { + // Block is roughly centered + $rowStyle = 'justify-content: center;'; + } else { + // Block has specific left offset - use padding + $leftPercent = ($xPos / self::GRID_COLS) * 100; + $rowStyle = "padding-left: {$leftPercent}%;"; + } + } + } + + return "
{$blocksHTML}
\n"; + } + + /** + * Get CSS page size string based on settings + */ + private function getPageSizeCSS(): string + { + $pageSize = $this->pageSettings['pageSize'] ?? 'a4'; + $orientation = $this->pageSettings['orientation'] ?? 'portrait'; + + if ($pageSize === 'custom') { + $width = $this->pageSettings['customWidth'] ?? '210mm'; + $height = $this->pageSettings['customHeight'] ?? '297mm'; + return "{$width} {$height}"; + } + + $size = self::PAGE_SIZES[$pageSize] ?? self::PAGE_SIZES['a4']; + $width = $orientation === 'landscape' ? $size['height'] : $size['width']; + $height = $orientation === 'landscape' ? $size['width'] : $size['height']; + + return "{$width}mm {$height}mm"; + } + + /** + * Get CSS page margins string based on settings + */ + private function getPageMarginsCSS(): string + { + $top = $this->pageSettings['marginTop'] ?? '10mm'; + $right = $this->pageSettings['marginRight'] ?? '10mm'; + $bottom = $this->pageSettings['marginBottom'] ?? '10mm'; + $left = $this->pageSettings['marginLeft'] ?? '10mm'; + + return "{$top} {$right} {$bottom} {$left}"; + } + + /** + * Generate complete HTML document structure with flow-based CSS + */ + private function generateDocument(string $content): string + { + $padding = self::PADDING_V . 'px ' . self::PADDING_H . 'px'; + $marginBottom = self::MARGIN_V . 'px'; + + // Page settings + $pageSize = $this->getPageSizeCSS(); + $pageMargins = $this->getPageMarginsCSS(); + $fontFamily = $this->pageSettings['fontFamily'] ?? "Inter, sans-serif"; + $fontSize = $this->pageSettings['fontSize'] ?? '12px'; + $textColor = $this->pageSettings['textColor'] ?? '#374151'; + $lineHeight = $this->pageSettings['lineHeight'] ?? '1.5'; + $backgroundColor = $this->pageSettings['backgroundColor'] ?? '#ffffff'; + + return << + + + + + Invoice + + + +
+ {$content} +
+ + +HTML; + } + + /** + * Generate CSS styles from block.properties.styles + */ + private function generateBlockStyles(array $block): array + { + $props = $block['properties'] ?? []; + $styles = $props['styles'] ?? []; + $cssStyles = []; + + // Background + if (!empty($styles['backgroundColor']) && $styles['backgroundColor'] !== 'transparent') { + $cssStyles[] = "background-color: {$styles['backgroundColor']}"; + } + if (!empty($styles['opacity'])) { + $cssStyles[] = "opacity: {$styles['opacity']}"; + } + + // Borders + if (!empty($styles['borderTopStyle']) && $styles['borderTopStyle'] !== 'none') { + $width = $styles['borderTopWidth'] ?? '1px'; + $color = $styles['borderTopColor'] ?? '#000000'; + $cssStyles[] = "border-top: {$width} {$styles['borderTopStyle']} {$color}"; + } + if (!empty($styles['borderBottomStyle']) && $styles['borderBottomStyle'] !== 'none') { + $width = $styles['borderBottomWidth'] ?? '1px'; + $color = $styles['borderBottomColor'] ?? '#000000'; + $cssStyles[] = "border-bottom: {$width} {$styles['borderBottomStyle']} {$color}"; + } + if (!empty($styles['borderLeftStyle']) && $styles['borderLeftStyle'] !== 'none') { + $width = $styles['borderLeftWidth'] ?? '1px'; + $color = $styles['borderLeftColor'] ?? '#000000'; + $cssStyles[] = "border-left: {$width} {$styles['borderLeftStyle']} {$color}"; + } + if (!empty($styles['borderRightStyle']) && $styles['borderRightStyle'] !== 'none') { + $width = $styles['borderRightWidth'] ?? '1px'; + $color = $styles['borderRightColor'] ?? '#000000'; + $cssStyles[] = "border-right: {$width} {$styles['borderRightStyle']} {$color}"; + } + if (!empty($styles['borderRadius'])) { + $cssStyles[] = "border-radius: {$styles['borderRadius']}"; + } + + // Spacing + if (!empty($styles['padding'])) { + $cssStyles[] = "padding: {$styles['padding']}"; + } + if (!empty($styles['margin'])) { + $cssStyles[] = "margin: {$styles['margin']}"; + } + + // Page break behavior + if (!empty($styles['pageBreak']) && $styles['pageBreak'] !== 'auto') { + switch ($styles['pageBreak']) { + case 'before': + $cssStyles[] = 'page-break-before: always'; + break; + case 'after': + $cssStyles[] = 'page-break-after: always'; + break; + case 'avoid': + $cssStyles[] = 'page-break-inside: avoid'; + break; + case 'always': + $cssStyles[] = 'page-break-before: always'; + $cssStyles[] = 'page-break-after: always'; + break; + } + } + + return $cssStyles; + } + + /** + * Render a single block with flow-based layout + */ + private function renderBlock(array $block): string + { + $gridPos = $block['gridPosition']; + $content = $this->renderBlockContent($block); + + // Calculate width as percentage of 12 columns + $widthPercent = ($gridPos['w'] / self::GRID_COLS) * 100; + $isFullWidth = $gridPos['w'] === self::GRID_COLS; + + // Expandable blocks (tables, totals) should not have min-height constraints + $isExpandable = in_array($block['type'], ['table', 'total']); + + $className = $isFullWidth ? 'block full-width' : 'block'; + + $styles = []; + if (!$isFullWidth) { + $styles[] = "width: {$widthPercent}%"; + } + if (!$isExpandable) { + $minHeight = $gridPos['h'] * self::ROW_HEIGHT; + $styles[] = "min-height: {$minHeight}px"; + } + + // Add custom block styles + $blockStyles = $this->generateBlockStyles($block); + $styles = array_merge($styles, $blockStyles); + + $styleAttr = !empty($styles) ? ' style="' . implode('; ', $styles) . ';"' : ''; + + return "
{$content}
\n"; + } + + /** + * Render block content based on type + */ + private function renderBlockContent(array $block): string + { + $type = $block['type']; + $props = $block['properties']; + + return match($type) { + 'text' => $this->renderText($props), + 'logo', 'image' => $this->renderImage($props, $type), + 'company-info' => $this->renderCompanyInfo($props), + 'client-info' => $this->renderClientInfo($props), + 'invoice-details' => $this->renderInvoiceDetails($props), + 'table' => $this->renderTable($props), + 'total' => $this->renderTotal($props), + 'divider' => $this->renderDivider($props), + 'spacer' => $this->renderSpacer($props), + 'qrcode' => $this->renderQRCode($props), + 'signature' => $this->renderSignature($props), + default => "
Unknown block: {$type}
" + }; + } + + /** + * TEXT BLOCK + */ + private function renderText(array $props): string + { + $content = $this->escape($props['content'] ?? ''); + + return sprintf( + '
%s
', + $this->buildStyle([ + 'font-size' => $props['fontSize'], + 'font-weight' => $props['fontWeight'], + 'color' => $props['color'], + 'text-align' => $props['align'], + 'line-height' => $props['lineHeight'], + 'height' => '100%', + 'display' => 'flex', + 'align-items' => 'center', + ]), + nl2br($content) + ); + } + + /** + * IMAGE/LOGO BLOCK + */ + private function renderImage(array $props, string $type): string + { + $source = $props['source'] ?? ''; + + if (empty($source)) { + $placeholder = $type === 'logo' ? 'Company Logo' : 'Image'; + return sprintf( + '
%s
', + $this->buildStyle([ + 'width' => '100%', + 'height' => '100%', + 'background' => '#f3f4f6', + 'display' => 'flex', + 'align-items' => 'center', + 'justify-content' => 'center', + 'color' => '#9ca3af', + 'font-size' => '12px', + ]), + $placeholder + ); + } + + return sprintf( + '
%s
', + $this->buildStyle([ + 'text-align' => $props['align'], + 'height' => '100%', + 'display' => 'flex', + 'align-items' => 'center', + 'justify-content' => $props['align'], + ]), + $this->escape($source), + $this->buildStyle([ + 'max-width' => $props['maxWidth'], + 'max-height' => '100%', + 'object-fit' => $props['objectFit'], + ]), + $type + ); + } + + /** + * COMPANY INFO BLOCK + */ + private function renderCompanyInfo(array $props): string + { + $content = $this->escape($props['content'] ?? ''); + + return sprintf( + '
%s
', + $this->buildStyle([ + 'font-size' => $props['fontSize'], + 'line-height' => $props['lineHeight'], + 'text-align' => $props['align'], + 'color' => $props['color'], + 'white-space' => 'pre-line', + ]), + nl2br($content) + ); + } + + /** + * CLIENT INFO BLOCK + */ + private function renderClientInfo(array $props): string + { + $content = $this->escape($props['content'] ?? ''); + $html = '
'; + + if ($props['showTitle'] ?? false) { + $html .= sprintf( + '
%s
', + $this->buildStyle([ + 'font-size' => $props['fontSize'], + 'font-weight' => $props['titleFontWeight'], + 'color' => $props['color'], + 'margin-bottom' => '8px', + ]), + $this->escape($props['title'] ?? '') + ); + } + + $html .= sprintf( + '
%s
', + $this->buildStyle([ + 'font-size' => $props['fontSize'], + 'line-height' => $props['lineHeight'], + 'text-align' => $props['align'], + 'color' => $props['color'], + 'white-space' => 'pre-line', + ]), + nl2br($content) + ); + + $html .= '
'; + return $html; + } + + /** + * INVOICE DETAILS BLOCK + */ + private function renderInvoiceDetails(array $props): string + { + $content = $this->escape($props['content'] ?? ''); + + return sprintf( + '
%s
', + $this->buildStyle([ + 'font-size' => $props['fontSize'], + 'line-height' => $props['lineHeight'], + 'text-align' => $props['align'], + 'color' => $props['color'], + 'white-space' => 'pre-line', + ]), + nl2br($content) + ); + } + + /** + * TABLE BLOCK + * + * Column fields use "item.field" notation (e.g., "item.product_key"). + * The entire table body is wrapped in tags with Twig loop syntax. + */ + private function renderTable(array $props): string + { + $columns = $props['columns']; + $borderStyle = ($props['showBorders'] ?? true) + ? "1px solid {$props['borderColor']}" + : 'none'; + + $html = sprintf( + '', + $this->buildStyle([ + 'width' => '100%', + 'border-collapse' => 'collapse', + 'font-size' => $props['fontSize'], + ]) + ); + + // Header + $html .= sprintf( + '', + $this->buildStyle([ + 'background' => $props['headerBg'], + 'color' => $props['headerColor'], + 'font-weight' => $props['headerFontWeight'], + ]) + ); + + foreach ($columns as $col) { + $html .= sprintf( + '', + $this->buildStyle([ + 'padding' => $props['padding'], + 'text-align' => $col['align'], + 'width' => $col['width'] ?? 'auto', + 'border' => $borderStyle, + ]), + $this->escape($col['header']) + ); + } + + $html .= ''; + + // Body - wrapped in tags for Twig processing + $html .= ''; + $html .= ''; + $html .= '{% set invoice = invoices|first %}'; + $html .= '{% for item in invoice.line_items %}'; + + // Alternate row background using Twig + if ($props['alternateRows'] ?? false) { + $html .= sprintf( + '', + $this->escape($props['alternateRowBg']), + $this->escape($props['rowBg']) + ); + } else { + $html .= sprintf('', $props['rowBg']); + } + + foreach ($columns as $col) { + // Convert "item.field" to Twig variable "{{ item.field }}" + $twigVar = '{{ ' . $col['field'] . ' }}'; + + $html .= sprintf( + '', + $this->buildStyle([ + 'padding' => $props['padding'], + 'text-align' => $col['align'], + 'border' => $borderStyle, + ]), + $twigVar + ); + } + + $html .= ''; + $html .= '{% endfor %}'; + $html .= ''; + $html .= '
%s
%s
'; + + return $html; + } + + /** + * TOTAL BLOCK + */ + private function renderTotal(array $props): string + { + // Use table for proper label/value alignment + $tableAlign = match($props['align']) { + 'right' => 'margin-left: auto;', + 'center' => 'margin: 0 auto;', + default => '', + }; + + $gap = $props['labelValueGap'] ?? '20px'; + $labelPadding = $props['labelPadding'] ?? null; + $valuePadding = $props['valuePadding'] ?? null; + $valueMinWidth = $props['valueMinWidth'] ?? null; + + $html = sprintf('', $tableAlign); + + foreach ($props['items'] as $item) { + if (!($item['show'] ?? true)) { + continue; + } + + $isTotal = $item['isTotal'] ?? false; + $isBalance = $item['isBalance'] ?? false; + + $fontSize = $isTotal ? $props['totalFontSize'] : $props['fontSize']; + $fontWeight = $isTotal ? $props['totalFontWeight'] : 'normal'; + + $valueColor = $isBalance + ? $props['balanceColor'] + : ($isTotal ? $props['totalColor'] : $props['amountColor']); + + $html .= sprintf( + '', + $fontSize, + $fontWeight + ); + + // Label cell - apply user padding if set, otherwise use defaults + $labelStyles = [ + 'color' => $props['labelColor'], + 'text-align' => 'right', + 'white-space' => 'nowrap', + ]; + if ($labelPadding) { + $labelStyles['padding'] = $labelPadding; + $labelStyles['padding-right'] = $gap; // Override right padding with gap + } else { + $labelStyles['padding-right'] = $gap; + $labelStyles['padding-bottom'] = $props['spacing']; + } + + $html .= sprintf( + '', + $this->buildStyle($labelStyles), + $this->escape($item['label']) + ); + + // Value cell - apply user padding if set, otherwise use defaults + $valueStyles = [ + 'color' => $valueColor, + 'text-align' => 'right', + 'white-space' => 'nowrap', + ]; + if ($valueMinWidth) { + $valueStyles['min-width'] = $valueMinWidth; + } + if ($valuePadding) { + $valueStyles['padding'] = $valuePadding; + } else { + $valueStyles['padding-bottom'] = $props['spacing']; + } + + $html .= sprintf( + '', + $this->buildStyle($valueStyles), + $item['field'] // Keep variable like $invoice.total as-is + ); + + $html .= ''; + } + + $html .= '
%s:%s
'; + return $html; + } + + /** + * DIVIDER BLOCK + */ + private function renderDivider(array $props): string + { + return sprintf( + '
', + $this->buildStyle([ + 'border' => 'none', + 'border-top' => "{$props['thickness']} {$props['style']} {$props['color']}", + 'margin-top' => $props['marginTop'], + 'margin-bottom' => $props['marginBottom'], + ]) + ); + } + + /** + * SPACER BLOCK + */ + private function renderSpacer(array $props): string + { + return sprintf( + '
', + $this->buildStyle(['height' => $props['height']]) + ); + } + + /** + * QR CODE BLOCK + * Backend should replace {{QR_CODE:data}} with actual QR code image + */ + private function renderQRCode(array $props): string + { + return sprintf( + '
{{QR_CODE:%s}}
', + $this->buildStyle(['text-align' => $props['align']]), + $props['data'] ?? '$invoice.public_url' + ); + } + + /** + * SIGNATURE BLOCK + */ + private function renderSignature(array $props): string + { + $html = sprintf( + '
', + $this->buildStyle(['text-align' => $props['align']]) + ); + + $html .= '
'; + + if ($props['showLine'] ?? true) { + $html .= sprintf( + '
', + $this->buildStyle([ + 'border-top' => '1px solid #000', + 'width' => '200px', + 'margin-bottom' => '8px', + 'display' => $props['align'] === 'center' ? 'inline-block' : 'block', + ]) + ); + } + + $html .= sprintf( + '
%s
', + $this->buildStyle([ + 'font-size' => $props['fontSize'], + 'color' => $props['color'], + ]), + $this->escape($props['label'] ?? '') + ); + + if ($props['showDate'] ?? false) { + $html .= sprintf( + '
Date: ________________
', + $this->buildStyle([ + 'font-size' => $props['fontSize'], + 'color' => $props['color'], + 'margin-top' => '4px', + ]) + ); + } + + $html .= '
'; + return $html; + } + + /** + * Convert grid coordinates to absolute pixels + */ + private function gridToPixels(array $gridPosition): array + { + $x = $gridPosition['x']; + $y = $gridPosition['y']; + $w = $gridPosition['w']; + $h = $gridPosition['h']; + + // Calculate column width + $availableWidth = self::CANVAS_WIDTH - (self::PADDING_H * 2); + $colWidth = $availableWidth / self::GRID_COLS; + + // Calculate positions including margins + $left = self::PADDING_H + ($x * $colWidth) + ($x * self::MARGIN_H); + $top = self::PADDING_V + ($y * self::ROW_HEIGHT) + ($y * self::MARGIN_V); + + // Calculate dimensions + $width = ($w * $colWidth) + (($w - 1) * self::MARGIN_H); + $height = ($h * self::ROW_HEIGHT) + (($h - 1) * self::MARGIN_V); + + return [ + 'left' => round($left), + 'top' => round($top), + 'width' => round($width), + 'height' => round($height), + ]; + } + + /** + * Format position styles for absolute positioning + */ + private function formatPositionStyle(array $position): string + { + return $this->buildStyle([ + 'left' => $position['left'] . 'px', + 'top' => $position['top'] . 'px', + 'width' => $position['width'] . 'px', + 'height' => $position['height'] . 'px', + ]); + } + + /** + * Build inline CSS style string from array + */ + private function buildStyle(array $styles): string + { + $parts = []; + + foreach ($styles as $property => $value) { + if ($value !== null && $value !== '') { + $parts[] = "{$property}: {$value}"; + } + } + + return implode('; ', $parts) . ';'; + } + + /** + * Calculate total document height + */ + private function calculateDocumentHeight(array $blocks): int + { + if (empty($blocks)) { + return 1122; // A4 height at 96dpi (297mm) + } + + $maxBottom = 0; + + foreach ($blocks as $block) { + $position = $this->gridToPixels($block['gridPosition']); + $bottom = $position['top'] + $position['height']; + + if ($bottom > $maxBottom) { + $maxBottom = $bottom; + } + } + + return max($maxBottom + self::PADDING_V, 1122); + } + + /** + * Escape HTML special characters + */ + private function escape(string $text): string + { + return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); + } +} \ No newline at end of file diff --git a/tests/Feature/Design/stubs/test_design_1.html b/tests/Feature/Design/stubs/test_design_1.html new file mode 100644 index 0000000000..bc631be00e --- /dev/null +++ b/tests/Feature/Design/stubs/test_design_1.html @@ -0,0 +1,145 @@ + + + + + + Invoice + + + +
+
logo
+
+
$company.name
+$company.address
+$company.city_state_postal
+$company.phone
+$company.email
+
Bill To:
$client.name
+$client.address
+$client.city_state_postal
+$client.phone
+$client.email
+
+

+
+
Invoice #: $invoice.number
+Date: $invoice.date
+Due Date: $invoice.due_date
+PO Number: $invoice.po_number
+
+
{% set invoice = invoices|first %}{% for item in invoice.line_items %}{% endfor %}
ItemDescriptionQtyRateAmount
{{ item.product_key }}{{ item.notes }}{{ item.quantity }}{{ item.cost }}{{ item.line_total }}
+
+
Subtotal:$invoice.subtotal
Discount:$invoice.discount
Tax:$invoice.tax
Total:$invoice.total
Amount Paid:$invoice.paid_to_date
Balance Due:$invoice.balance
+
+ +
+ + \ No newline at end of file diff --git a/tests/Feature/Design/stubs/test_design_1.json b/tests/Feature/Design/stubs/test_design_1.json new file mode 100644 index 0000000000..41bd459134 --- /dev/null +++ b/tests/Feature/Design/stubs/test_design_1.json @@ -0,0 +1,213 @@ +{ + "blocks": [ + { + "id": "logo-1765268278392", + "type": "logo", + "gridPosition": { + "x": 3, + "y": 0, + "w": 6, + "h": 2 + }, + "properties": { + "source": "$company.logo", + "align": "center", + "maxWidth": "150px", + "objectFit": "contain" + } + }, + { + "id": "company-info-1765268293389", + "type": "company-info", + "gridPosition": { + "x": 0, + "y": 2, + "w": 4, + "h": 3 + }, + "properties": { + "content": "$company.name\n$company.address\n$company.city_state_postal\n$company.phone\n$company.email", + "fontSize": "12px", + "lineHeight": "1.6", + "align": "left", + "color": "#374151" + } + }, + { + "id": "client-info-1765268300017", + "type": "client-info", + "gridPosition": { + "x": 8, + "y": 2, + "w": 4, + "h": 3 + }, + "properties": { + "content": "$client.name\n$client.address\n$client.city_state_postal\n$client.phone\n$client.email", + "fontSize": "12px", + "lineHeight": "1.6", + "align": "left", + "color": "#374151", + "showTitle": true, + "title": "Bill To:", + "titleFontWeight": "bold" + } + }, + { + "id": "divider-1765268318961", + "type": "divider", + "gridPosition": { + "x": 0, + "y": 5, + "w": 12, + "h": 1 + }, + "properties": { + "thickness": "1px", + "color": "#E5E7EB", + "style": "solid", + "marginTop": "10px", + "marginBottom": "10px" + } + }, + { + "id": "invoice-details-1765268323354", + "type": "invoice-details", + "gridPosition": { + "x": 9, + "y": 6, + "w": 3, + "h": 3 + }, + "properties": { + "content": "Invoice #: $invoice.number\nDate: $invoice.date\nDue Date: $invoice.due_date\nPO Number: $invoice.po_number", + "fontSize": "12px", + "lineHeight": "1.8", + "align": "left", + "color": "#374151", + "labelColor": "#6B7280", + "showLabels": true + } + }, + { + "id": "table-1765268328782", + "type": "table", + "gridPosition": { + "x": 0, + "y": 9, + "w": 12, + "h": 3 + }, + "properties": { + "columns": [ + { + "id": "product_key", + "header": "Item", + "field": "item.product_key", + "width": "25%", + "align": "left" + }, + { + "id": "notes", + "header": "Description", + "field": "item.notes", + "width": "30%", + "align": "left" + }, + { + "id": "quantity", + "header": "Qty", + "field": "item.quantity", + "width": "10%", + "align": "center" + }, + { + "id": "cost", + "header": "Rate", + "field": "item.cost", + "width": "15%", + "align": "right" + }, + { + "id": "line_total", + "header": "Amount", + "field": "item.line_total", + "width": "15%", + "align": "right" + } + ], + "headerBg": "#F3F4F6", + "headerColor": "#111827", + "headerFontWeight": "bold", + "rowBg": "#FFFFFF", + "alternateRowBg": "#F9FAFB", + "borderColor": "#E5E7EB", + "fontSize": "12px", + "padding": "8px", + "showBorders": true, + "alternateRows": true + } + }, + { + "id": "total-1765268486405", + "type": "total", + "gridPosition": { + "x": 6, + "y": 12, + "w": 6, + "h": 6 + }, + "properties": { + "items": [ + { + "label": "Subtotal", + "field": "$invoice.subtotal", + "show": true + }, + { + "label": "Discount", + "field": "$invoice.discount", + "show": true + }, + { + "label": "Tax", + "field": "$invoice.tax", + "show": true + }, + { + "label": "Total", + "field": "$invoice.total", + "show": true, + "isTotal": true + }, + { + "label": "Amount Paid", + "field": "$invoice.paid_to_date", + "show": true + }, + { + "label": "Balance Due", + "field": "$invoice.balance", + "show": true, + "isBalance": true + } + ], + "fontSize": "13px", + "align": "right", + "labelColor": "#6B7280", + "amountColor": "#111827", + "totalFontSize": "18px", + "totalFontWeight": "bold", + "totalColor": "#111827", + "balanceColor": "#DC2626", + "spacing": "8px", + "labelPadding": "", + "valuePadding": "", + "labelValueGap": "20px", + "valueMinWidth": "", + "showLabels": true + } + } + ], + "templateId": "modern" + } \ No newline at end of file diff --git a/tests/Feature/Design/stubs/test_design_1_mock.html b/tests/Feature/Design/stubs/test_design_1_mock.html new file mode 100644 index 0000000000..77f38de754 --- /dev/null +++ b/tests/Feature/Design/stubs/test_design_1_mock.html @@ -0,0 +1,145 @@ + + + + + + Invoice + + + +
+
logo
+
+
Untitled Company
+Address 1
Address 2
City, State Postal Code
United States
Phone: 555-343-2323
Email: nikolaus.jaren@romaguera.info

+City, State Postal Code
+555-343-2323
+nikolaus.jaren@romaguera.info
+
Bill To:
Doyle-O'Conner
+ 19569
424 Osinski Crescent Suite 168
West Reta, Michigan 79669-8875
Afghanistan

+West Reta, Michigan 79669-8875
+
+
+
+

+
+
Invoice #: 0595759909341
+Date: 09/Sep/1982
+Due Date: 07/Jan/2018
+PO Number: Quo at.
+
+
ItemDescriptionQtyRateAmount
1.75$49.58$86.77
+
+
Subtotal:$86.77
Discount:$0.00
Tax:0595759909341.tax
Total:$103.26
Amount Paid:0595759909341.paid_to_date
Balance Due:$103.26
+
+ +
+ + From f358765661b8e3eb14b9fa02228df10340297cd8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 10 Dec 2025 13:03:33 +1100 Subject: [PATCH 022/177] Updated tests --- .../Feature/Design/InvoiceDesignRenderer.php | 51 +- tests/Feature/Design/stubs/test_design_1.html | 8 +- tests/Feature/Design/stubs/test_design_1.json | 438 +++++++++--------- .../Design/stubs/test_design_1_mock.html | 24 +- 4 files changed, 272 insertions(+), 249 deletions(-) diff --git a/tests/Feature/Design/InvoiceDesignRenderer.php b/tests/Feature/Design/InvoiceDesignRenderer.php index da7e66518e..3ab25abeb0 100644 --- a/tests/Feature/Design/InvoiceDesignRenderer.php +++ b/tests/Feature/Design/InvoiceDesignRenderer.php @@ -1,6 +1,7 @@ pageSettings['pageSize'] ?? 'a4'; $orientation = $this->pageSettings['orientation'] ?? 'portrait'; @@ -210,7 +211,7 @@ class InvoiceDesignRenderer /** * Get CSS page margins string based on settings */ - private function getPageMarginsCSS(): string + public function getPageMarginsCSS(): string { $top = $this->pageSettings['marginTop'] ?? '10mm'; $right = $this->pageSettings['marginRight'] ?? '10mm'; @@ -223,7 +224,7 @@ class InvoiceDesignRenderer /** * Generate complete HTML document structure with flow-based CSS */ - private function generateDocument(string $content): string + public function generateDocument(string $content): string { $padding = self::PADDING_V . 'px ' . self::PADDING_H . 'px'; $marginBottom = self::MARGIN_V . 'px'; @@ -365,7 +366,7 @@ HTML; /** * Generate CSS styles from block.properties.styles */ - private function generateBlockStyles(array $block): array + public function generateBlockStyles(array $block): array { $props = $block['properties'] ?? []; $styles = $props['styles'] ?? []; @@ -437,7 +438,7 @@ HTML; /** * Render a single block with flow-based layout */ - private function renderBlock(array $block): string + public function renderBlock(array $block): string { $gridPos = $block['gridPosition']; $content = $this->renderBlockContent($block); @@ -472,7 +473,7 @@ HTML; /** * Render block content based on type */ - private function renderBlockContent(array $block): string + public function renderBlockContent(array $block): string { $type = $block['type']; $props = $block['properties']; @@ -496,7 +497,7 @@ HTML; /** * TEXT BLOCK */ - private function renderText(array $props): string + public function renderText(array $props): string { $content = $this->escape($props['content'] ?? ''); @@ -519,7 +520,7 @@ HTML; /** * IMAGE/LOGO BLOCK */ - private function renderImage(array $props, string $type): string + public function renderImage(array $props, string $type): string { $source = $props['source'] ?? ''; @@ -563,7 +564,7 @@ HTML; /** * COMPANY INFO BLOCK */ - private function renderCompanyInfo(array $props): string + public function renderCompanyInfo(array $props): string { $content = $this->escape($props['content'] ?? ''); @@ -583,7 +584,7 @@ HTML; /** * CLIENT INFO BLOCK */ - private function renderClientInfo(array $props): string + public function renderClientInfo(array $props): string { $content = $this->escape($props['content'] ?? ''); $html = '
'; @@ -620,7 +621,7 @@ HTML; /** * INVOICE DETAILS BLOCK */ - private function renderInvoiceDetails(array $props): string + public function renderInvoiceDetails(array $props): string { $content = $this->escape($props['content'] ?? ''); @@ -643,7 +644,7 @@ HTML; * Column fields use "item.field" notation (e.g., "item.product_key"). * The entire table body is wrapped in tags with Twig loop syntax. */ - private function renderTable(array $props): string + public function renderTable(array $props): string { $columns = $props['columns']; $borderStyle = ($props['showBorders'] ?? true) @@ -727,7 +728,7 @@ HTML; /** * TOTAL BLOCK */ - private function renderTotal(array $props): string + public function renderTotal(array $props): string { // Use table for proper label/value alignment $tableAlign = match($props['align']) { @@ -815,7 +816,7 @@ HTML; /** * DIVIDER BLOCK */ - private function renderDivider(array $props): string + public function renderDivider(array $props): string { return sprintf( '
', @@ -831,7 +832,7 @@ HTML; /** * SPACER BLOCK */ - private function renderSpacer(array $props): string + public function renderSpacer(array $props): string { return sprintf( '
', @@ -843,7 +844,7 @@ HTML; * QR CODE BLOCK * Backend should replace {{QR_CODE:data}} with actual QR code image */ - private function renderQRCode(array $props): string + public function renderQRCode(array $props): string { return sprintf( '
{{QR_CODE:%s}}
', @@ -855,7 +856,7 @@ HTML; /** * SIGNATURE BLOCK */ - private function renderSignature(array $props): string + public function renderSignature(array $props): string { $html = sprintf( '
', @@ -903,7 +904,7 @@ HTML; /** * Convert grid coordinates to absolute pixels */ - private function gridToPixels(array $gridPosition): array + public function gridToPixels(array $gridPosition): array { $x = $gridPosition['x']; $y = $gridPosition['y']; @@ -933,7 +934,7 @@ HTML; /** * Format position styles for absolute positioning */ - private function formatPositionStyle(array $position): string + public function formatPositionStyle(array $position): string { return $this->buildStyle([ 'left' => $position['left'] . 'px', @@ -946,7 +947,7 @@ HTML; /** * Build inline CSS style string from array */ - private function buildStyle(array $styles): string + public function buildStyle(array $styles): string { $parts = []; @@ -962,7 +963,7 @@ HTML; /** * Calculate total document height */ - private function calculateDocumentHeight(array $blocks): int + public function calculateDocumentHeight(array $blocks): int { if (empty($blocks)) { return 1122; // A4 height at 96dpi (297mm) @@ -985,8 +986,8 @@ HTML; /** * Escape HTML special characters */ - private function escape(string $text): string + public function escape(string $text): string { return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); } -} \ No newline at end of file +} diff --git a/tests/Feature/Design/stubs/test_design_1.html b/tests/Feature/Design/stubs/test_design_1.html index bc631be00e..24778b7fc3 100644 --- a/tests/Feature/Design/stubs/test_design_1.html +++ b/tests/Feature/Design/stubs/test_design_1.html @@ -130,14 +130,16 @@ $client.email

-
Invoice #: $invoice.number
+
Invoice #: $invoice.number
Date: $invoice.date
+PO Number: $invoice.po_number
Due Date: $invoice.due_date
-PO Number: $invoice.po_number
+Amount: $invoice.amount
{% set invoice = invoices|first %}{% for item in invoice.line_items %}{% endfor %}
ItemDescriptionQtyRateAmount
{{ item.product_key }}{{ item.notes }}{{ item.quantity }}{{ item.cost }}{{ item.line_total }}
-
Subtotal:$invoice.subtotal
Discount:$invoice.discount
Tax:$invoice.tax
Total:$invoice.total
Amount Paid:$invoice.paid_to_date
Balance Due:$invoice.balance
+
private notes generally go here.
+
Subtotal:$invoice.subtotal
Discount:$invoice.discount
Tax:$invoice.tax
Total:$invoice.total
Amount Paid:$invoice.paid_to_date
Balance Due:$invoice.balance
diff --git a/tests/Feature/Design/stubs/test_design_1.json b/tests/Feature/Design/stubs/test_design_1.json index 41bd459134..a281cf2a75 100644 --- a/tests/Feature/Design/stubs/test_design_1.json +++ b/tests/Feature/Design/stubs/test_design_1.json @@ -1,213 +1,231 @@ { - "blocks": [ - { - "id": "logo-1765268278392", - "type": "logo", - "gridPosition": { - "x": 3, - "y": 0, - "w": 6, - "h": 2 - }, - "properties": { - "source": "$company.logo", - "align": "center", - "maxWidth": "150px", - "objectFit": "contain" - } + "blocks": [ + { + "id": "logo-1765268278392", + "type": "logo", + "gridPosition": { + "x": 3, + "y": 0, + "w": 6, + "h": 2 }, - { - "id": "company-info-1765268293389", - "type": "company-info", - "gridPosition": { - "x": 0, - "y": 2, - "w": 4, - "h": 3 - }, - "properties": { - "content": "$company.name\n$company.address\n$company.city_state_postal\n$company.phone\n$company.email", - "fontSize": "12px", - "lineHeight": "1.6", - "align": "left", - "color": "#374151" - } - }, - { - "id": "client-info-1765268300017", - "type": "client-info", - "gridPosition": { - "x": 8, - "y": 2, - "w": 4, - "h": 3 - }, - "properties": { - "content": "$client.name\n$client.address\n$client.city_state_postal\n$client.phone\n$client.email", - "fontSize": "12px", - "lineHeight": "1.6", - "align": "left", - "color": "#374151", - "showTitle": true, - "title": "Bill To:", - "titleFontWeight": "bold" - } - }, - { - "id": "divider-1765268318961", - "type": "divider", - "gridPosition": { - "x": 0, - "y": 5, - "w": 12, - "h": 1 - }, - "properties": { - "thickness": "1px", - "color": "#E5E7EB", - "style": "solid", - "marginTop": "10px", - "marginBottom": "10px" - } - }, - { - "id": "invoice-details-1765268323354", - "type": "invoice-details", - "gridPosition": { - "x": 9, - "y": 6, - "w": 3, - "h": 3 - }, - "properties": { - "content": "Invoice #: $invoice.number\nDate: $invoice.date\nDue Date: $invoice.due_date\nPO Number: $invoice.po_number", - "fontSize": "12px", - "lineHeight": "1.8", - "align": "left", - "color": "#374151", - "labelColor": "#6B7280", - "showLabels": true - } - }, - { - "id": "table-1765268328782", - "type": "table", - "gridPosition": { - "x": 0, - "y": 9, - "w": 12, - "h": 3 - }, - "properties": { - "columns": [ - { - "id": "product_key", - "header": "Item", - "field": "item.product_key", - "width": "25%", - "align": "left" - }, - { - "id": "notes", - "header": "Description", - "field": "item.notes", - "width": "30%", - "align": "left" - }, - { - "id": "quantity", - "header": "Qty", - "field": "item.quantity", - "width": "10%", - "align": "center" - }, - { - "id": "cost", - "header": "Rate", - "field": "item.cost", - "width": "15%", - "align": "right" - }, - { - "id": "line_total", - "header": "Amount", - "field": "item.line_total", - "width": "15%", - "align": "right" - } - ], - "headerBg": "#F3F4F6", - "headerColor": "#111827", - "headerFontWeight": "bold", - "rowBg": "#FFFFFF", - "alternateRowBg": "#F9FAFB", - "borderColor": "#E5E7EB", - "fontSize": "12px", - "padding": "8px", - "showBorders": true, - "alternateRows": true - } - }, - { - "id": "total-1765268486405", - "type": "total", - "gridPosition": { - "x": 6, - "y": 12, - "w": 6, - "h": 6 - }, - "properties": { - "items": [ - { - "label": "Subtotal", - "field": "$invoice.subtotal", - "show": true - }, - { - "label": "Discount", - "field": "$invoice.discount", - "show": true - }, - { - "label": "Tax", - "field": "$invoice.tax", - "show": true - }, - { - "label": "Total", - "field": "$invoice.total", - "show": true, - "isTotal": true - }, - { - "label": "Amount Paid", - "field": "$invoice.paid_to_date", - "show": true - }, - { - "label": "Balance Due", - "field": "$invoice.balance", - "show": true, - "isBalance": true - } - ], - "fontSize": "13px", - "align": "right", - "labelColor": "#6B7280", - "amountColor": "#111827", - "totalFontSize": "18px", - "totalFontWeight": "bold", - "totalColor": "#111827", - "balanceColor": "#DC2626", - "spacing": "8px", - "labelPadding": "", - "valuePadding": "", - "labelValueGap": "20px", - "valueMinWidth": "", - "showLabels": true - } + "properties": { + "source": "$company.logo", + "align": "center", + "maxWidth": "150px", + "objectFit": "contain" } - ], - "templateId": "modern" - } \ No newline at end of file + }, + { + "id": "company-info-1765268293389", + "type": "company-info", + "gridPosition": { + "x": 0, + "y": 2, + "w": 4, + "h": 3 + }, + "properties": { + "content": "$company.name\n$company.address\n$company.city_state_postal\n$company.phone\n$company.email", + "fontSize": "12px", + "lineHeight": "1.6", + "align": "left", + "color": "#374151" + } + }, + { + "id": "client-info-1765268300017", + "type": "client-info", + "gridPosition": { + "x": 8, + "y": 2, + "w": 4, + "h": 3 + }, + "properties": { + "content": "$client.name\n$client.address\n$client.city_state_postal\n$client.phone\n$client.email", + "fontSize": "12px", + "lineHeight": "1.6", + "align": "left", + "color": "#374151", + "showTitle": true, + "title": "Bill To:", + "titleFontWeight": "bold" + } + }, + { + "id": "divider-1765268318961", + "type": "divider", + "gridPosition": { + "x": 0, + "y": 5, + "w": 12, + "h": 1 + }, + "properties": { + "thickness": "1px", + "color": "#E5E7EB", + "style": "solid", + "marginTop": "10px", + "marginBottom": "10px" + } + }, + { + "id": "invoice-details-1765268323354", + "type": "invoice-details", + "gridPosition": { + "x": 0, + "y": 6, + "w": 3, + "h": 3 + }, + "properties": { + "content": "Invoice #: $invoice.number\nDate: $invoice.date\nPO Number: $invoice.po_number\nDue Date: $invoice.due_date\nAmount: $invoice.amount", + "fontSize": "12px", + "lineHeight": "1.8", + "align": "left", + "color": "#374151", + "labelColor": "#6B7280", + "showLabels": true + } + }, + { + "id": "table-1765268328782", + "type": "table", + "gridPosition": { + "x": 0, + "y": 9, + "w": 12, + "h": 3 + }, + "properties": { + "columns": [ + { + "id": "product_key", + "header": "Item", + "field": "item.product_key", + "width": "25%", + "align": "left" + }, + { + "id": "notes", + "header": "Description", + "field": "item.notes", + "width": "30%", + "align": "left" + }, + { + "id": "quantity", + "header": "Qty", + "field": "item.quantity", + "width": "10%", + "align": "center" + }, + { + "id": "cost", + "header": "Rate", + "field": "item.cost", + "width": "15%", + "align": "right" + }, + { + "id": "line_total", + "header": "Amount", + "field": "item.line_total", + "width": "15%", + "align": "right" + } + ], + "headerBg": "#F3F4F6", + "headerColor": "#111827", + "headerFontWeight": "bold", + "rowBg": "#FFFFFF", + "alternateRowBg": "#F9FAFB", + "borderColor": "#E5E7EB", + "fontSize": "12px", + "padding": "8px", + "showBorders": true, + "alternateRows": true + } + }, + { + "id": "total-1765268486405", + "type": "total", + "gridPosition": { + "x": 6, + "y": 12, + "w": 6, + "h": 6 + }, + "properties": { + "items": [ + { + "label": "Subtotal", + "field": "$invoice.subtotal", + "show": true + }, + { + "label": "Discount", + "field": "$invoice.discount", + "show": true + }, + { + "label": "Tax", + "field": "$invoice.tax", + "show": true + }, + { + "label": "Total", + "field": "$invoice.total", + "show": true, + "isTotal": true + }, + { + "label": "Amount Paid", + "field": "$invoice.paid_to_date", + "show": true + }, + { + "label": "Balance Due", + "field": "$invoice.balance", + "show": true, + "isBalance": true + } + ], + "fontSize": "13px", + "align": "right", + "labelColor": "#6B7280", + "amountColor": "#111827", + "totalFontSize": "18px", + "totalFontWeight": "bold", + "totalColor": "#111827", + "balanceColor": "#DC2626", + "spacing": "8px", + "labelPadding": "", + "valuePadding": "", + "labelValueGap": "20px", + "valueMinWidth": "", + "showLabels": true + } + }, + { + "id": "text-1765327448954", + "type": "text", + "gridPosition": { + "x": 0, + "y": 12, + "w": 6, + "h": 4 + }, + "properties": { + "content": "private notes generally go here.", + "fontSize": "16px", + "fontWeight": "normal", + "lineHeight": "1.5", + "color": "#000000", + "align": "left" + } + } + ], + "templateId": "modern" +} \ No newline at end of file diff --git a/tests/Feature/Design/stubs/test_design_1_mock.html b/tests/Feature/Design/stubs/test_design_1_mock.html index 77f38de754..7d08690f38 100644 --- a/tests/Feature/Design/stubs/test_design_1_mock.html +++ b/tests/Feature/Design/stubs/test_design_1_mock.html @@ -118,26 +118,28 @@
logo
Untitled Company
-Address 1
Address 2
City, State Postal Code
United States
Phone: 555-343-2323
Email: nikolaus.jaren@romaguera.info

+Address 1
Address 2
City, State Postal Code
United States
Phone: 555-343-2323
Email: phamill@monahan.info

City, State Postal Code
555-343-2323
-nikolaus.jaren@romaguera.info
-
Bill To:
Doyle-O'Conner
- 19569
424 Osinski Crescent Suite 168
West Reta, Michigan 79669-8875
Afghanistan

-West Reta, Michigan 79669-8875
+phamill@monahan.info
+
Bill To:
Jacobi, Greenfelder and Fisher
+ 747
83848 Domingo Corners Apt. 281
North Kelliton, Arizona 89888
Afghanistan

+North Kelliton, Arizona 89888

-
+gail.brekke@example.org

-
Invoice #: 0595759909341
-Date: 09/Sep/1982
-Due Date: 07/Jan/2018
-PO Number: Quo at.
+
Invoice #: 7215645438696
+Date: 27/Jun/1978
+PO Number: Quasi in.
+Due Date: 22/May/2018
+Amount: $103.26
ItemDescriptionQtyRateAmount
1.75$49.58$86.77
-
Subtotal:$86.77
Discount:$0.00
Tax:0595759909341.tax
Total:$103.26
Amount Paid:0595759909341.paid_to_date
Balance Due:$103.26
+
private notes generally go here.
+
Subtotal:$86.77
Discount:$0.00
Tax:7215645438696.tax
Total:$103.26
Amount Paid:7215645438696.paid_to_date
Balance Due:$103.26
From 185942839172ea7f481022dfe5aa41ab7aa2ff8c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 11 Dec 2025 08:13:25 +1100 Subject: [PATCH 023/177] Fixes for payment types --- app/Models/PaymentType.php | 8 +- app/Services/Pdf/PdfBuilder.php | 1 + tests/Feature/Design/DesignParserTest.php | 8 + .../Feature/Design/InvoiceDesignRenderer.php | 557 ++++++++++++++---- tests/Feature/Design/stubs/test_design_1.html | 35 +- tests/Feature/Design/stubs/test_design_1.json | 206 ++++++- .../Design/stubs/test_design_1_mock.html | 35 +- 7 files changed, 666 insertions(+), 184 deletions(-) diff --git a/app/Models/PaymentType.php b/app/Models/PaymentType.php index 8ea9c81af7..4156f2b1ff 100644 --- a/app/Models/PaymentType.php +++ b/app/Models/PaymentType.php @@ -37,6 +37,7 @@ class PaymentType extends StaticModel public const BANK_TRANSFER = 1; public const CASH = 2; + public const DEBIT = 3; public const ACH = 4; public const VISA = 5; public const MASTERCARD = 6; @@ -47,6 +48,7 @@ class PaymentType extends StaticModel public const NOVA = 11; public const CREDIT_CARD_OTHER = 12; public const PAYPAL = 13; + public const GOOGLE_WALLET = 14; public const CHECK = 15; public const CARTE_BLANCHE = 16; public const UNIONPAY = 17; @@ -55,7 +57,10 @@ class PaymentType extends StaticModel public const MAESTRO = 20; public const SOLO = 21; public const SWITCH = 22; - public const VENMO = 24; + public const iZETTLE = 23; + public const SWISH = 24; + public const VENMO = 25; + public const MONEY_ORDER = 26; public const ALIPAY = 27; public const SOFORT = 28; public const SEPA = 29; @@ -63,6 +68,7 @@ class PaymentType extends StaticModel public const CRYPTO = 31; public const CREDIT = 32; public const ZELLE = 33; + public const MOLLIE_BANK_TRANSFER = 34; public const KBC = 35; public const BANCONTACT = 36; diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index 3458b11079..2d5f362aa7 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -1374,6 +1374,7 @@ class PdfBuilder PdfService::DELIVERY_NOTE => $this->getDeliveryNoteSections(), PdfService::STATEMENT => $this->getStatementSections(), PdfService::PURCHASE_ORDER => $this->getPurchaseOrderSections(), + 'json_design' => $this, // JSON designs have sections pre-injected default => $this->getProductSections(), }; } diff --git a/tests/Feature/Design/DesignParserTest.php b/tests/Feature/Design/DesignParserTest.php index a454da945a..6276f76b62 100644 --- a/tests/Feature/Design/DesignParserTest.php +++ b/tests/Feature/Design/DesignParserTest.php @@ -79,11 +79,19 @@ class DesignParserTest extends TestCase $item = InvoiceItemFactory::create(); $item->quantity = 1.75; $item->cost = 49.58; + $item->product_key = 'test_product'; + $item->notes = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.'; $item->tax_name1 = 'mwst'; $item->tax_rate1 = 19; $item->type_id = '1'; $item->tax_id = '1'; $line_items[] = $item; + $line_items[] = $item; + $line_items[] = $item; + $line_items[] = $item; + $line_items[] = $item; + $line_items[] = $item; + $line_items[] = $item; $i = Invoice::factory()->create([ diff --git a/tests/Feature/Design/InvoiceDesignRenderer.php b/tests/Feature/Design/InvoiceDesignRenderer.php index 3ab25abeb0..d333730b29 100644 --- a/tests/Feature/Design/InvoiceDesignRenderer.php +++ b/tests/Feature/Design/InvoiceDesignRenderer.php @@ -116,7 +116,7 @@ class InvoiceDesignRenderer /** * Group blocks into rows based on similar Y positions */ - public function groupBlocksIntoRows(array $blocks): array + private function groupBlocksIntoRows(array $blocks): array { $rows = []; $currentRow = []; @@ -146,7 +146,7 @@ class InvoiceDesignRenderer /** * Render a row of blocks */ - public function renderRow(array $blocks): string + private function renderRow(array $blocks): string { $blocksHTML = ''; foreach ($blocks as $block) { @@ -190,7 +190,7 @@ class InvoiceDesignRenderer /** * Get CSS page size string based on settings */ - public function getPageSizeCSS(): string + private function getPageSizeCSS(): string { $pageSize = $this->pageSettings['pageSize'] ?? 'a4'; $orientation = $this->pageSettings['orientation'] ?? 'portrait'; @@ -211,7 +211,7 @@ class InvoiceDesignRenderer /** * Get CSS page margins string based on settings */ - public function getPageMarginsCSS(): string + private function getPageMarginsCSS(): string { $top = $this->pageSettings['marginTop'] ?? '10mm'; $right = $this->pageSettings['marginRight'] ?? '10mm'; @@ -224,7 +224,7 @@ class InvoiceDesignRenderer /** * Generate complete HTML document structure with flow-based CSS */ - public function generateDocument(string $content): string + private function generateDocument(string $content): string { $padding = self::PADDING_V . 'px ' . self::PADDING_H . 'px'; $marginBottom = self::MARGIN_V . 'px'; @@ -366,7 +366,7 @@ HTML; /** * Generate CSS styles from block.properties.styles */ - public function generateBlockStyles(array $block): array + private function generateBlockStyles(array $block): array { $props = $block['properties'] ?? []; $styles = $props['styles'] ?? []; @@ -437,20 +437,28 @@ HTML; /** * Render a single block with flow-based layout + * Each block has a unique ID for CSS targeting */ - public function renderBlock(array $block): string + private function renderBlock(array $block): string { $gridPos = $block['gridPosition']; - $content = $this->renderBlockContent($block); + $blockId = $block['id'] ?? $this->generateBlockId($block['type']); + $blockType = $block['type']; + + $content = $this->renderBlockContent($block, $blockId); // Calculate width as percentage of 12 columns $widthPercent = ($gridPos['w'] / self::GRID_COLS) * 100; $isFullWidth = $gridPos['w'] === self::GRID_COLS; // Expandable blocks (tables, totals) should not have min-height constraints - $isExpandable = in_array($block['type'], ['table', 'total']); + $isExpandable = in_array($blockType, ['table', 'total', 'invoice-details']); - $className = $isFullWidth ? 'block full-width' : 'block'; + // CSS classes for targeting + $classes = ['block', "block-{$blockType}"]; + if ($isFullWidth) { + $classes[] = 'full-width'; + } $styles = []; if (!$isFullWidth) { @@ -465,69 +473,99 @@ HTML; $blockStyles = $this->generateBlockStyles($block); $styles = array_merge($styles, $blockStyles); + $classAttr = implode(' ', $classes); $styleAttr = !empty($styles) ? ' style="' . implode('; ', $styles) . ';"' : ''; - return "
{$content}
\n"; + return "
{$content}
\n"; + } + + /** + * Generate a unique block ID if not provided + */ + private function generateBlockId(string $type): string + { + static $counter = 0; + $counter++; + return "{$type}-{$counter}"; } /** * Render block content based on type */ - public function renderBlockContent(array $block): string + private function renderBlockContent(array $block, string $blockId): string { $type = $block['type']; $props = $block['properties']; return match($type) { - 'text' => $this->renderText($props), - 'logo', 'image' => $this->renderImage($props, $type), - 'company-info' => $this->renderCompanyInfo($props), - 'client-info' => $this->renderClientInfo($props), - 'invoice-details' => $this->renderInvoiceDetails($props), - 'table' => $this->renderTable($props), - 'total' => $this->renderTotal($props), - 'divider' => $this->renderDivider($props), - 'spacer' => $this->renderSpacer($props), - 'qrcode' => $this->renderQRCode($props), - 'signature' => $this->renderSignature($props), + 'text' => $this->renderText($props, $blockId), + 'logo', 'image' => $this->renderImage($props, $type, $blockId), + 'company-info' => $this->renderCompanyInfo($props, $blockId), + 'client-info' => $this->renderClientInfo($props, $blockId), + 'invoice-details' => $this->renderInvoiceDetails($props, $blockId), + 'table' => $this->renderTable($props, $blockId), + 'total' => $this->renderTotal($props, $blockId), + 'divider' => $this->renderDivider($props, $blockId), + 'spacer' => $this->renderSpacer($props, $blockId), + 'qrcode' => $this->renderQRCode($props, $blockId), + 'signature' => $this->renderSignature($props, $blockId), default => "
Unknown block: {$type}
" }; } /** * TEXT BLOCK + * Renders multi-line text using div elements instead of br tags */ - public function renderText(array $props): string + private function renderText(array $props, string $blockId): string { - $content = $this->escape($props['content'] ?? ''); + $content = $props['content'] ?? ''; + $lines = explode("\n", $content); + + $containerStyle = $this->buildStyle([ + 'font-size' => $props['fontSize'] ?? '14px', + 'font-weight' => $props['fontWeight'] ?? 'normal', + 'font-style' => $props['fontStyle'] ?? 'normal', + 'color' => $props['color'] ?? '#000000', + 'text-align' => $props['align'] ?? 'left', + 'line-height' => $props['lineHeight'] ?? '1.5', + ]); - return sprintf( - '
%s
', - $this->buildStyle([ - 'font-size' => $props['fontSize'], - 'font-weight' => $props['fontWeight'], - 'color' => $props['color'], - 'text-align' => $props['align'], - 'line-height' => $props['lineHeight'], - 'height' => '100%', - 'display' => 'flex', - 'align-items' => 'center', - ]), - nl2br($content) - ); + $html = "
"; + + foreach ($lines as $index => $line) { + $lineId = "{$blockId}-line-{$index}"; + $escapedLine = $this->escape(trim($line)); + + // Use span for inline elements, div for block-level lines + if (empty(trim($line))) { + $html .= "
 
"; + } else { + $html .= "
{$escapedLine}
"; + } + } + + $html .= '
'; + return $html; } /** * IMAGE/LOGO BLOCK + * Supports: + * - Base64 encoded images (data:image/...) + * - External URLs (https://...) + * - Variables for backend replacement ($company.logo) */ - public function renderImage(array $props, string $type): string + private function renderImage(array $props, string $type, string $blockId): string { $source = $props['source'] ?? ''; + $imageId = "{$blockId}-img"; if (empty($source)) { $placeholder = $type === 'logo' ? 'Company Logo' : 'Image'; return sprintf( - '
%s
', + '
%s
', + $imageId, $this->buildStyle([ 'width' => '100%', 'height' => '100%', @@ -542,100 +580,342 @@ HTML; ); } + // Determine the image source format + $imageSrc = $this->resolveImageSource($source); + return sprintf( - '
%s
', + '
%s
', $this->buildStyle([ - 'text-align' => $props['align'], + 'text-align' => $props['align'] ?? 'left', 'height' => '100%', 'display' => 'flex', 'align-items' => 'center', - 'justify-content' => $props['align'], + 'justify-content' => $props['align'] ?? 'left', ]), - $this->escape($source), + $imageId, + $type === 'logo' ? 'company-logo' : 'block-image', + $imageSrc, $this->buildStyle([ - 'max-width' => $props['maxWidth'], - 'max-height' => '100%', - 'object-fit' => $props['objectFit'], + 'max-width' => $props['maxWidth'] ?? '100%', + 'max-height' => $props['maxHeight'] ?? '100%', + 'object-fit' => $props['objectFit'] ?? 'contain', ]), - $type + $this->escape($type) ); } + /** + * Resolve image source based on format + * - Base64: Return as-is (already embedded) + * - Variable ($company.logo): Return for backend replacement + * - URL: Escape and return + */ + private function resolveImageSource(string $source): string + { + // Base64 encoded image - return as-is (don't escape) + if (str_starts_with($source, 'data:image/')) { + return $source; + } + + // Variable for backend replacement - return as-is + if (str_starts_with($source, '$')) { + return $source; + } + + // External URL - escape for HTML safety + return $this->escape($source); + } + /** * COMPANY INFO BLOCK + * Renders each field as a separate div for proper layout control */ - public function renderCompanyInfo(array $props): string + private function renderCompanyInfo(array $props, string $blockId): string { - $content = $this->escape($props['content'] ?? ''); + $content = $props['content'] ?? ''; + $fieldConfigs = $props['fieldConfigs'] ?? null; + + $containerStyle = $this->buildStyle([ + 'font-size' => $props['fontSize'] ?? '12px', + 'font-weight' => $props['fontWeight'] ?? 'normal', + 'font-style' => $props['fontStyle'] ?? 'normal', + 'line-height' => $props['lineHeight'] ?? '1.5', + 'text-align' => $props['align'] ?? 'left', + 'color' => $props['color'] ?? '#374151', + ]); - return sprintf( - '
%s
', - $this->buildStyle([ - 'font-size' => $props['fontSize'], - 'line-height' => $props['lineHeight'], - 'text-align' => $props['align'], - 'color' => $props['color'], - 'white-space' => 'pre-line', - ]), - nl2br($content) - ); + $html = "
"; + + if ($fieldConfigs && is_array($fieldConfigs)) { + // New structured format with fieldConfigs + foreach ($fieldConfigs as $index => $config) { + $fieldId = "{$blockId}-field-{$index}"; + $prefix = $this->escape($config['prefix'] ?? ''); + $variable = $config['variable'] ?? ''; + $suffix = $this->escape($config['suffix'] ?? ''); + + $html .= "
"; + if (!empty($prefix)) { + $html .= "{$prefix}"; + } + $html .= "{$variable}"; + if (!empty($suffix)) { + $html .= "{$suffix}"; + } + $html .= "
"; + } + } else { + // Legacy content string - split by lines + $lines = explode("\n", $content); + foreach ($lines as $index => $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + $fieldId = "{$blockId}-field-{$index}"; + // Don't escape - may contain variables like $company.name + $html .= "
{$line}
"; + } + } + + $html .= '
'; + return $html; } /** * CLIENT INFO BLOCK + * Renders with optional title and each field as a separate div */ - public function renderClientInfo(array $props): string + private function renderClientInfo(array $props, string $blockId): string { - $content = $this->escape($props['content'] ?? ''); - $html = '
'; + $content = $props['content'] ?? ''; + $fieldConfigs = $props['fieldConfigs'] ?? null; + + $html = '
'; + // Optional title if ($props['showTitle'] ?? false) { + $titleId = "{$blockId}-title"; $html .= sprintf( - '
%s
', + '
%s
', + $titleId, $this->buildStyle([ - 'font-size' => $props['fontSize'], - 'font-weight' => $props['titleFontWeight'], - 'color' => $props['color'], + 'font-size' => $props['fontSize'] ?? '12px', + 'font-weight' => $props['titleFontWeight'] ?? 'bold', + 'color' => $props['color'] ?? '#374151', 'margin-bottom' => '8px', ]), $this->escape($props['title'] ?? '') ); } - $html .= sprintf( - '
%s
', - $this->buildStyle([ - 'font-size' => $props['fontSize'], - 'line-height' => $props['lineHeight'], - 'text-align' => $props['align'], - 'color' => $props['color'], - 'white-space' => 'pre-line', - ]), - nl2br($content) - ); + $containerStyle = $this->buildStyle([ + 'font-size' => $props['fontSize'] ?? '12px', + 'font-weight' => $props['fontWeight'] ?? 'normal', + 'font-style' => $props['fontStyle'] ?? 'normal', + 'line-height' => $props['lineHeight'] ?? '1.5', + 'text-align' => $props['align'] ?? 'left', + 'color' => $props['color'] ?? '#374151', + ]); + $html .= "
"; + + if ($fieldConfigs && is_array($fieldConfigs)) { + // New structured format with fieldConfigs + foreach ($fieldConfigs as $index => $config) { + $fieldId = "{$blockId}-field-{$index}"; + $prefix = $this->escape($config['prefix'] ?? ''); + $variable = $config['variable'] ?? ''; + $suffix = $this->escape($config['suffix'] ?? ''); + + $html .= "
"; + if (!empty($prefix)) { + $html .= "{$prefix}"; + } + $html .= "{$variable}"; + if (!empty($suffix)) { + $html .= "{$suffix}"; + } + $html .= "
"; + } + } else { + // Legacy content string - split by lines + $lines = explode("\n", $content); + foreach ($lines as $index => $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + $fieldId = "{$blockId}-field-{$index}"; + // Don't escape - may contain variables like $client.name + $html .= "
{$line}
"; + } + } + + $html .= '
'; $html .= '
'; return $html; } /** * INVOICE DETAILS BLOCK + * Renders as a table with label/value pairs (similar to Total block) + * Supports both new 'items' array format and legacy 'content' string */ - public function renderInvoiceDetails(array $props): string + private function renderInvoiceDetails(array $props, string $blockId): string { - $content = $this->escape($props['content'] ?? ''); + $align = $props['align'] ?? 'left'; + $fontSize = $props['fontSize'] ?? '12px'; + $lineHeight = $props['lineHeight'] ?? '1.5'; + $color = $props['color'] ?? '#374151'; + $labelColor = $props['labelColor'] ?? '#6B7280'; + $rowSpacing = $props['rowSpacing'] ?? '4px'; + $labelWidth = $props['labelWidth'] ?? 'auto'; + $displayAsGrid = $props['displayAsGrid'] ?? true; - return sprintf( - '
%s
', - $this->buildStyle([ - 'font-size' => $props['fontSize'], - 'line-height' => $props['lineHeight'], - 'text-align' => $props['align'], - 'color' => $props['color'], - 'white-space' => 'pre-line', - ]), - nl2br($content) + // Check if we have items array (new format) or content string (legacy) + $items = $props['items'] ?? null; + + $styleContext = [ + 'align' => $align, + 'fontSize' => $fontSize, + 'lineHeight' => $lineHeight, + 'color' => $color, + 'labelColor' => $labelColor, + 'rowSpacing' => $rowSpacing, + 'labelWidth' => $labelWidth, + 'blockId' => $blockId, + ]; + + if ($items && is_array($items) && $displayAsGrid) { + return $this->renderInvoiceDetailsTable($items, $styleContext); + } + + // Legacy format: parse content string + $content = $props['content'] ?? ''; + + if ($displayAsGrid && !empty($content)) { + $parsedItems = $this->parseInvoiceDetailsContent($content); + return $this->renderInvoiceDetailsTable($parsedItems, $styleContext); + } + + // Fallback: render each line as a div + $lines = explode("\n", $content); + $html = "
"; + foreach ($lines as $index => $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + $lineId = "{$blockId}-line-{$index}"; + $html .= "
{$line}
"; + } + $html .= '
'; + return $html; + } + + /** + * Parse legacy content string into items array + */ + private function parseInvoiceDetailsContent(string $content): array + { + $items = []; + $lines = explode("\n", $content); + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + + $colonPos = strpos($line, ':'); + if ($colonPos !== false) { + $label = trim(substr($line, 0, $colonPos + 1)); + $variable = trim(substr($line, $colonPos + 1)); + $items[] = [ + 'label' => $label, + 'variable' => $variable, + 'show' => true, + ]; + } else { + $items[] = [ + 'label' => '', + 'variable' => $line, + 'show' => true, + ]; + } + } + + return $items; + } + + /** + * Render invoice details as a table with IDs for CSS targeting + */ + private function renderInvoiceDetailsTable(array $items, array $styles): string + { + $blockId = $styles['blockId'] ?? 'invoice-details'; + $tableId = "{$blockId}-table"; + + $tableAlign = match($styles['align']) { + 'right' => 'margin-left: auto;', + 'center' => 'margin: 0 auto;', + default => '', + }; + + $html = sprintf( + '', + $tableId, + $tableAlign ); + + $rowIndex = 0; + foreach ($items as $item) { + if (!($item['show'] ?? true)) { + continue; + } + + $label = $item['label'] ?? ''; + $variable = $item['variable'] ?? ''; + $rowId = "{$blockId}-row-{$rowIndex}"; + + $html .= ""; + + // Label cell + $html .= sprintf( + '', + $this->buildStyle([ + 'font-size' => $styles['fontSize'], + 'line-height' => $styles['lineHeight'], + 'color' => $styles['labelColor'], + 'text-align' => $styles['align'] === 'right' ? 'right' : 'left', + 'padding-bottom' => $styles['rowSpacing'], + 'padding-right' => '12px', + 'white-space' => 'nowrap', + 'width' => $styles['labelWidth'], + ]), + $this->escape($label) + ); + + // Value cell + $html .= sprintf( + '', + $this->buildStyle([ + 'font-size' => $styles['fontSize'], + 'line-height' => $styles['lineHeight'], + 'color' => $styles['color'], + 'text-align' => $styles['align'] === 'right' ? 'right' : 'left', + 'padding-bottom' => $styles['rowSpacing'], + ]), + $variable + ); + + $html .= ''; + $rowIndex++; + } + + $html .= '
%s%s
'; + return $html; } /** @@ -644,15 +924,17 @@ HTML; * Column fields use "item.field" notation (e.g., "item.product_key"). * The entire table body is wrapped in tags with Twig loop syntax. */ - public function renderTable(array $props): string + private function renderTable(array $props, string $blockId): string { $columns = $props['columns']; + $tableId = "{$blockId}-table"; $borderStyle = ($props['showBorders'] ?? true) ? "1px solid {$props['borderColor']}" : 'none'; $html = sprintf( - '', + '
', + $tableId, $this->buildStyle([ 'width' => '100%', 'border-collapse' => 'collapse', @@ -662,7 +944,8 @@ HTML; // Header $html .= sprintf( - '', + '', + $blockId, $this->buildStyle([ 'background' => $props['headerBg'], 'color' => $props['headerColor'], @@ -670,9 +953,11 @@ HTML; ]) ); - foreach ($columns as $col) { + foreach ($columns as $colIndex => $col) { + $colId = "{$blockId}-col-{$colIndex}"; $html .= sprintf( - '', + '', + $colId, $this->buildStyle([ 'padding' => $props['padding'], 'text-align' => $col['align'], @@ -686,7 +971,7 @@ HTML; $html .= ''; // Body - wrapped in tags for Twig processing - $html .= ''; + $html .= ''; $html .= ''; $html .= '{% set invoice = invoices|first %}'; $html .= '{% for item in invoice.line_items %}'; @@ -694,20 +979,19 @@ HTML; // Alternate row background using Twig if ($props['alternateRows'] ?? false) { $html .= sprintf( - '', + '', $this->escape($props['alternateRowBg']), $this->escape($props['rowBg']) ); } else { - $html .= sprintf('', $props['rowBg']); + $html .= sprintf('', $props['rowBg']); } foreach ($columns as $col) { - // Convert "item.field" to Twig variable "{{ item.field }}" $twigVar = '{{ ' . $col['field'] . ' }}'; $html .= sprintf( - '', + '', $this->buildStyle([ 'padding' => $props['padding'], 'text-align' => $col['align'], @@ -728,9 +1012,10 @@ HTML; /** * TOTAL BLOCK */ - public function renderTotal(array $props): string + private function renderTotal(array $props, string $blockId): string { - // Use table for proper label/value alignment + $tableId = "{$blockId}-table"; + $tableAlign = match($props['align']) { 'right' => 'margin-left: auto;', 'center' => 'margin: 0 auto;', @@ -742,8 +1027,13 @@ HTML; $valuePadding = $props['valuePadding'] ?? null; $valueMinWidth = $props['valueMinWidth'] ?? null; - $html = sprintf('
%s%s
%s%s
', $tableAlign); + $html = sprintf( + '
', + $tableId, + $tableAlign + ); + $rowIndex = 0; foreach ($props['items'] as $item) { if (!($item['show'] ?? true)) { continue; @@ -751,6 +1041,10 @@ HTML; $isTotal = $item['isTotal'] ?? false; $isBalance = $item['isBalance'] ?? false; + $rowId = "{$blockId}-row-{$rowIndex}"; + $rowClass = 'totals-row'; + if ($isTotal) $rowClass .= ' totals-row-total'; + if ($isBalance) $rowClass .= ' totals-row-balance'; $fontSize = $isTotal ? $props['totalFontSize'] : $props['fontSize']; $fontWeight = $isTotal ? $props['totalFontWeight'] : 'normal'; @@ -760,12 +1054,14 @@ HTML; : ($isTotal ? $props['totalColor'] : $props['amountColor']); $html .= sprintf( - '', + '', + $rowId, + $rowClass, $fontSize, $fontWeight ); - // Label cell - apply user padding if set, otherwise use defaults + // Label cell $labelStyles = [ 'color' => $props['labelColor'], 'text-align' => 'right', @@ -773,19 +1069,19 @@ HTML; ]; if ($labelPadding) { $labelStyles['padding'] = $labelPadding; - $labelStyles['padding-right'] = $gap; // Override right padding with gap + $labelStyles['padding-right'] = $gap; } else { $labelStyles['padding-right'] = $gap; $labelStyles['padding-bottom'] = $props['spacing']; } $html .= sprintf( - '', + '', $this->buildStyle($labelStyles), $this->escape($item['label']) ); - // Value cell - apply user padding if set, otherwise use defaults + // Value cell $valueStyles = [ 'color' => $valueColor, 'text-align' => 'right', @@ -801,12 +1097,13 @@ HTML; } $html .= sprintf( - '', + '', $this->buildStyle($valueStyles), - $item['field'] // Keep variable like $invoice.total as-is + $item['field'] ); $html .= ''; + $rowIndex++; } $html .= '
%s:%s:%s%s
'; @@ -816,10 +1113,11 @@ HTML; /** * DIVIDER BLOCK */ - public function renderDivider(array $props): string + private function renderDivider(array $props, string $blockId): string { return sprintf( - '
', + '
', + $blockId, $this->buildStyle([ 'border' => 'none', 'border-top' => "{$props['thickness']} {$props['style']} {$props['color']}", @@ -832,10 +1130,11 @@ HTML; /** * SPACER BLOCK */ - public function renderSpacer(array $props): string + private function renderSpacer(array $props, string $blockId): string { return sprintf( - '
', + '
', + $blockId, $this->buildStyle(['height' => $props['height']]) ); } @@ -844,10 +1143,11 @@ HTML; * QR CODE BLOCK * Backend should replace {{QR_CODE:data}} with actual QR code image */ - public function renderQRCode(array $props): string + private function renderQRCode(array $props, string $blockId): string { return sprintf( - '
{{QR_CODE:%s}}
', + '
{{QR_CODE:%s}}
', + $blockId, $this->buildStyle(['text-align' => $props['align']]), $props['data'] ?? '$invoice.public_url' ); @@ -856,18 +1156,19 @@ HTML; /** * SIGNATURE BLOCK */ - public function renderSignature(array $props): string + private function renderSignature(array $props, string $blockId): string { $html = sprintf( - '
', + '
', + $blockId, $this->buildStyle(['text-align' => $props['align']]) ); - $html .= '
'; + $html .= '
'; if ($props['showLine'] ?? true) { $html .= sprintf( - '
', + '
', $this->buildStyle([ 'border-top' => '1px solid #000', 'width' => '200px', @@ -878,7 +1179,7 @@ HTML; } $html .= sprintf( - '
%s
', + '
%s
', $this->buildStyle([ 'font-size' => $props['fontSize'], 'color' => $props['color'], @@ -904,7 +1205,7 @@ HTML; /** * Convert grid coordinates to absolute pixels */ - public function gridToPixels(array $gridPosition): array + private function gridToPixels(array $gridPosition): array { $x = $gridPosition['x']; $y = $gridPosition['y']; @@ -934,7 +1235,7 @@ HTML; /** * Format position styles for absolute positioning */ - public function formatPositionStyle(array $position): string + private function formatPositionStyle(array $position): string { return $this->buildStyle([ 'left' => $position['left'] . 'px', @@ -947,7 +1248,7 @@ HTML; /** * Build inline CSS style string from array */ - public function buildStyle(array $styles): string + private function buildStyle(array $styles): string { $parts = []; @@ -963,7 +1264,7 @@ HTML; /** * Calculate total document height */ - public function calculateDocumentHeight(array $blocks): int + private function calculateDocumentHeight(array $blocks): int { if (empty($blocks)) { return 1122; // A4 height at 96dpi (297mm) @@ -986,7 +1287,7 @@ HTML; /** * Escape HTML special characters */ - public function escape(string $text): string + private function escape(string $text): string { return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); } diff --git a/tests/Feature/Design/stubs/test_design_1.html b/tests/Feature/Design/stubs/test_design_1.html index 24778b7fc3..d99b71360d 100644 --- a/tests/Feature/Design/stubs/test_design_1.html +++ b/tests/Feature/Design/stubs/test_design_1.html @@ -115,31 +115,26 @@
-
logo
+
-
$company.name
-$company.address
-$company.city_state_postal
-$company.phone
-$company.email
-
Bill To:
$client.name
-$client.address
-$client.city_state_postal
-$client.phone
-$client.email
+
$company.name
$company.address1
$company.city_state_postal
$company.phone
$company.id_number
$company.website
$company.email
+
$client.name
$client.address1
VAT: $client.vat_number
$client.city_state_postal
$client.country
$client.phone
$client.email
-

+

-
Invoice #: $invoice.number
-Date: $invoice.date
-PO Number: $invoice.po_number
-Due Date: $invoice.due_date
-Amount: $invoice.amount
+
image
+
Invoice #:$invoice.number
PO Number:$invoice.po_number
Date:$invoice.date
Due Date:$invoice.due_date
Amount:$invoice.amount
Bo Bo Balance MoFo:$invoice.balance
-
{% set invoice = invoices|first %}{% for item in invoice.line_items %}{% endfor %}
ItemDescriptionQtyRateAmount
{{ item.product_key }}{{ item.notes }}{{ item.quantity }}{{ item.cost }}{{ item.line_total }}
+
{% set invoice = invoices|first %}{% for item in invoice.line_items %}{% endfor %}
ItemDescriptionQtyRateAmount
{{ item.product_key }}{{ item.notes }}{{ item.quantity }}{{ item.cost }}{{ item.line_total }}
-
private notes generally go here.
-
Subtotal:$invoice.subtotal
Discount:$invoice.discount
Tax:$invoice.tax
Total:$invoice.total
Amount Paid:$invoice.paid_to_date
Balance Due:$invoice.balance
+
private notes generally go here.
 
$invoice.terms
 
line spaces dont work that great?
+
Subtotal:$invoice.subtotal
Discount:$invoice.discount
Tax:$invoice.tax
Total:$invoice.total
Amount Paid:$invoice.paid_to_date
Balance Due:$invoice.balance
+
+
$entity.footer
+
+
+
+
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
 
 
diff --git a/tests/Feature/Design/stubs/test_design_1.json b/tests/Feature/Design/stubs/test_design_1.json index a281cf2a75..3754c7d15c 100644 --- a/tests/Feature/Design/stubs/test_design_1.json +++ b/tests/Feature/Design/stubs/test_design_1.json @@ -26,11 +26,48 @@ "h": 3 }, "properties": { - "content": "$company.name\n$company.address\n$company.city_state_postal\n$company.phone\n$company.email", + "content": "$company.name\n$company.address1\n$company.city_state_postal\n$company.phone\n$company.id_number\n$company.website\n$company.email", "fontSize": "12px", "lineHeight": "1.6", "align": "left", - "color": "#374151" + "color": "#374151", + "fieldConfigs": [ + { + "variable": "$company.name", + "prefix": "", + "suffix": "" + }, + { + "variable": "$company.address1", + "prefix": "", + "suffix": "" + }, + { + "variable": "$company.city_state_postal", + "prefix": "", + "suffix": "" + }, + { + "variable": "$company.phone", + "prefix": "", + "suffix": "" + }, + { + "variable": "$company.id_number", + "prefix": "", + "suffix": "" + }, + { + "variable": "$company.website", + "prefix": "", + "suffix": "" + }, + { + "variable": "$company.email", + "prefix": "", + "suffix": "" + } + ] } }, { @@ -43,14 +80,51 @@ "h": 3 }, "properties": { - "content": "$client.name\n$client.address\n$client.city_state_postal\n$client.phone\n$client.email", + "content": "$client.name\n$client.address1\nVAT: $client.vat_number\n$client.city_state_postal\n$client.country\n$client.phone\n$client.email", "fontSize": "12px", "lineHeight": "1.6", "align": "left", "color": "#374151", "showTitle": true, - "title": "Bill To:", - "titleFontWeight": "bold" + "title": "", + "titleFontWeight": "bold", + "fieldConfigs": [ + { + "variable": "$client.name", + "prefix": "", + "suffix": "" + }, + { + "variable": "$client.address1", + "prefix": "", + "suffix": "" + }, + { + "variable": "$client.vat_number", + "prefix": "VAT: ", + "suffix": "" + }, + { + "variable": "$client.city_state_postal", + "prefix": "", + "suffix": "" + }, + { + "variable": "$client.country", + "prefix": "", + "suffix": "" + }, + { + "variable": "$client.phone", + "prefix": "", + "suffix": "" + }, + { + "variable": "$client.email", + "prefix": "", + "suffix": "" + } + ] } }, { @@ -74,19 +148,56 @@ "id": "invoice-details-1765268323354", "type": "invoice-details", "gridPosition": { - "x": 0, + "x": 7, "y": 6, - "w": 3, + "w": 5, "h": 3 }, "properties": { - "content": "Invoice #: $invoice.number\nDate: $invoice.date\nPO Number: $invoice.po_number\nDue Date: $invoice.due_date\nAmount: $invoice.amount", + "content": "Invoice #: $invoice.number\nPO Number: $invoice.po_number\nDate: $invoice.date\nDue Date: $invoice.due_date\nAmount: $invoice.amount\nBo Bo Balance MoFo: $invoice.balance", "fontSize": "12px", "lineHeight": "1.8", "align": "left", "color": "#374151", "labelColor": "#6B7280", - "showLabels": true + "showLabels": true, + "items": [ + { + "variable": "$invoice.number", + "label": "Invoice #:", + "show": true + }, + { + "variable": "$invoice.po_number", + "label": "PO Number:", + "show": true + }, + { + "variable": "$invoice.date", + "label": "Date:", + "show": true + }, + { + "variable": "$invoice.due_date", + "label": "Due Date:", + "show": true + }, + { + "variable": "$invoice.amount", + "label": "Amount:", + "show": true + }, + { + "variable": "$invoice.balance", + "label": "Bo Bo Balance MoFo:", + "show": true + } + ], + "rowSpacing": "1px", + "padding": "0", + "styles": { + "borderTopStyle": "none" + } } }, { @@ -136,14 +247,14 @@ "align": "right" } ], - "headerBg": "#F3F4F6", + "headerBg": "#739ae8", "headerColor": "#111827", "headerFontWeight": "bold", "rowBg": "#FFFFFF", "alternateRowBg": "#F9FAFB", "borderColor": "#E5E7EB", "fontSize": "12px", - "padding": "8px", + "padding": "3px", "showBorders": true, "alternateRows": true } @@ -155,7 +266,7 @@ "x": 6, "y": 12, "w": 6, - "h": 6 + "h": 4 }, "properties": { "items": [ @@ -200,10 +311,10 @@ "totalFontWeight": "bold", "totalColor": "#111827", "balanceColor": "#DC2626", - "spacing": "8px", + "spacing": "0", "labelPadding": "", "valuePadding": "", - "labelValueGap": "20px", + "labelValueGap": "0", "valueMinWidth": "", "showLabels": true } @@ -218,13 +329,78 @@ "h": 4 }, "properties": { - "content": "private notes generally go here.", + "content": "private notes generally go here.\n\n$invoice.terms\n\nline spaces dont work that great?", "fontSize": "16px", "fontWeight": "normal", "lineHeight": "1.5", "color": "#000000", "align": "left" } + }, + { + "id": "text-1765334642409", + "type": "text", + "gridPosition": { + "x": 0, + "y": 16, + "w": 12, + "h": 3 + }, + "properties": { + "content": "$entity.footer", + "fontSize": "16px", + "fontWeight": "normal", + "lineHeight": "1.5", + "color": "#000000", + "align": "left" + } + }, + { + "id": "image-1765335399202", + "type": "image", + "gridPosition": { + "x": 0, + "y": 6, + "w": 7, + "h": 3 + }, + "properties": { + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAksAAACYCAYAAAD0iK2qAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJzsvXl4lOW9///6PLMkmayQzGSZCQmBZJIAAmUTQZHFtXjqVm2rXaTW1m7q6abtaU/bb8/p+vNYe3q01dpW22K1dasLKmpFEKi4sSdhCWQhGwkJZJ957t8fTyZCGMgkmWxwv65rLrjmuee+P5NMZt7zWUUpxdmMiEwAMoBJgA/IBDw9t4lAMpAEJADxgAHE9NwCQBfQDrT13Dp6bq1AC1DXc6sFKoGKnn+blFLdI/EcNRqNRqPRDB45W8SSiCQBqUA6kHbcLaXn1lcUxQNxQCyWMHICDkAAO2ADTCCIJZq6e24BPhBRnViiKXRrAZp7bk1AI9DQcwuJqmalVHC4fg4ajUaj0WgGxhkplkTEhiVwXFiCJw7LazQZKADygNyeW2LP2pEkCBwFqoH9PbcyYA+W16mFD7xVHdoDpdFoNBrN6HHGiSURESxP0RRgFnAOMB1wY3mOQiE0Z8/N6LmNJArLK9WN5X0KeaHagHos4bQNeA8oAQ4ppcwRtlGj0Wg0Gg1niFgSERdWjlEeH3iNsrG8SVlYeUgurDDaWCaI5VFqwPI6hXKcDgD7em5VwDEtnjQajUajGRnGrVgSESdWXlESlijyA/OBuVhepWSs/KLxTitwCHir57YVS0A1AcewwnTj85eo0Wg0Gs04YDyLpRxgJrAcmI3lTQolZYfCa2eCWAqF60I5TPXA+8DrwEZgr1Kqa/TM02g0Go3mzGZciSURScMSRecARVjepHysMFtSaNmoGDcyKKy2BLVYIblSYDeWt6kUqNGVdBqNRqPRRJcxL5ZExI7lMUoFirFCbRcBU7FK/89m2oByLC/TG1hJ4bVY7Qe0t0mj0Wg0migwHsRSGpYn6VpgDlYCdzxWqM02iqaNBUIhujYskbQVeBpLPGkvk0aj0Wg0UcA+2gaEo6dPUhpW6f8crJykGZwYbtOc2E08DktEJmOJy7dF5D2gXPdp0mg0Go1m8Iwpz1KPSHJhlftPBy4HFmGF3M6UhO3hxsTqBL4ZWIOVBF4JHNWhOY1Go9FoBs5YE0uJWCLpWuBiwIvlLQn1R9JiqX8UVr+mDqyWA28DfwbeUUpVj6ZhGo1Go9GMR8ZEGE5EDKzqtgXAhVhhtzysuWwj3V17vBOaXZeA1Zgzpuf/60RkA5Z46tK9mTQajUajiYxRFUs9YbcErKaSFwGXYYXdXGgvUjSIxRJM2VgDhFOxwnT7RaRR5zJpNBqNRtM/oxqGE5EkYBrweWAeVg+lWCyhpMVSdAj9gruwcpl2AH8AXgEOaw+TRqPRaDSnZ1Q8SyIS8nhcACzDCr+lY3mUNNElJDpjsObnObG8S7nAiyJSrpQ6Mkq2aTQajUYz5hlxsdSTxD0JSyh9BDgfnZs0UsRgidJlWMnzTuB1EdkBNGovk0aj0Wg0JzPiYTgRmQN8GLgBK1dJh91GHoUVlmsF/gY8Cbysm1hqNBqNRnMyI+ZZ6slPmgdcgjX81ofVSFGLpJFHsLxKDqzqw1ggUUQ2K6UOjqZhGo1Go9GMNUZELIlIKlZrgCuBpVhJ3ZrRJeTNKwASsfpZiYgEgEM6JKfRaDQajcVIeZbmANcDlwLuETpTEzkerG7pTizR9CesmXMajUaj0Zz1DKtYEpFkYDGwEliC1efHcdoHaUYDG1ZI9ENY3b8DIvKGUqp8VK3SaDQajWYMMGxiSUQmYoXbrsSqfJsyXGdpooYXSzg5gS4R6VRKHRplmzQajUajGVWG07N0DnAdVlfu9GE8RxNdPFgJ+B2AISKP6vwljUaj0ZzNRF0siYgLmIXVHmA5VuhtTMyg00SEgdWPaR7QBtSIyHalVP3omqXRaDQazegQVRHTI5RysLxJS7EqrTTjk2xgIXAEMEXkXaVUyyjbpNFoNBrNiBNtj08G1gfsNcDUKO+tGXlygFVYgqkFeHd0zdFoNBqNZuSJilgSERuQglXxdiOQFa29NaOKDath5WVYFXLVwBGlVOfomqXRaDQazcgxZEEjIoLVm2cuVuhtAR+MMNGMb0KNK6dhjUYpATaKyEGd9K3RaDSas4VoDK+1YTWa/AzWgFY9wuTMIwaYAXwTSzjpXlkajUajOWuIhlgqAq4GpgMT0UNxz0RC3sPJwMXAeaNrjkaj0Wg0I8egw3AiYgDJWF2fL8eqnoqLkl2asYez53Y+0CQiO4Fmnb+k0Wg0mjOdoXiWYoBCYBHWB2hSVCzSjHVmYiXyfwhLLGs0Go1Gc0YzKM9ST/VbGnAVcC7RCedpxgcGVluIVcAxEWlSSumhuxqNRqM5YxmsyPEAs7G8SnnoPKWzCcH6/S/BqoDMGV1zNBqNRqMZXgabs1SE5VXKA1zRM0czTnBieRZXAI3AntE1R6PRaDSa4WNAniURsYmIBytv5XysnBXtUTr7CHkSZwELRMQnIrGjbJNGo9FoNMPCQMNwTqzw2xwgF6v5pObsJROr/9JirIHJGo1Go9GccQxULMUDH8aaSG9De5XOZoQPkr0/BfhG1xyNRqPRaIaHiMWSiKRiNZ6cgeVR0GgAJmC9JmaIiBZMGo1GoznjGIhnKRu4ACv8ljgs1mjGI7FAOpa3sXiUbdFoNBqNJuoMRCz5gX9D56ZoTsaGlfC/YLQN0Wg0Go0m2vQrlkTELiKTsdoF5KFHmmhORrBylopEpFhEEkbbII1Go9FookUkniUHcA5WiGUCQ5gnpzljEazQ7GSsIbsTR9ccjUaj0WiiRyRiKRZYiOVZ0mhOhw+4FKvDt0aj0Wg0ZwSn9RKJiAvIwvIsTRoRizTjmVSsHlw5IlKqlGoZbYMGg2dSwQViyg1R3vYoqDYTaRJUk6GMJgzZV1uZsUup1wJRPkujGTXSvQUXIXJt3/tN1Dv1laW/GQ2bADy+gv8QJLvv/Z22mDuPHNjaNKC9sv2Xo3iu7/0KdX99ZemtQ7FzvJM+qegSZZprTr6i7qmrLL1j5C2KDv2F1ELtArIZYgVcUlISCxYsYOLEiTgcDgBEdJumsYBSCoCuri6qq6t555136OzsJBgMDnSrWCADKAR2AzuiaugIISbFCm4Zhp17GpMJpihQCre3usPj828TeEMwH6+t2rNZhX4hGs04xBQ5R8L8/QgSzPDlv19TWbZpNOwCuUZZUwdOwBZs/3/AgMSS5uyjP7GUjVXllMIQG1B6PB5uv/12Zs6cSVJSEjExMdhstl7BpIXTyBL6PFZKEQgE6OzspKWlhbVr17J//34OHz48GLEkWJVxs4BSxqlYGmFigXkK5imMf3d7C8rdvsKHzPbgLw8fLhuXnjmN5hTYTIw/ZGdnz66oqGgfbWM0moFwSrEklnrxAYuwZsANiUOHDvGDH/yApKQkcnJyWLVqFfn5+UyYMAHDGGgjcU00ME2TxsZGtmzZwkMPPcTRo0epra3l8OHDdHV1DXZbAytsu1VERHtJBkyuoH5ojzNu9/j8v4iRtnv0B4vmDMLfpVw/Ar422oZoNAMhrFgSEQOromkSUWoX0Nrayr/+9S8A0tPTiY2NZfLkySQlJWEYBvHx8WRkZDBr1iwSEhKw2WwhW4Z69FlNSKsEg0EaGhrYtm0bDQ0NtLW1YZomzc3NbNu2jeeff56Ojg6ioG0MwIvVvDRdRJqUUp1D3fRsQ1l/f//dqVyfTM8u/Fhtxe6to22TRhMNFNzumVTwdN3B0nWjbYtGEymn8izZsMrAc4GkaB9aW1vLr3/96xPuy8jIYNGiRdx5551kZ2fjdDoxDAOn04nD4egVTyG0iDqR40WOUgqlFJ2dnXR3d6OUoqOjg3fffZd77rmH999/n9ra2lPuJSJDEU2CNUPQi1VBuRXQYmnwFCmlNnm8hV+uq9r90Ggbo9FEAQOTB71e76yqqqq20TZGo4mEU4klBzANSzBFBREhNjaWQCBAd3f3SdebmprYsGEDt912G3Fxcb2huZUrV7JixQpyc3OJiYnRIbt+CAaDtLa2cujQIR577DHWr18P0OtFKi8vp6Xl1KkwDoeDhIQEWltbhxKKA6t9wALgAHB4KBtpiEPUg+newrTaqt0/G21jNJqhI/ndJP4YuG20LRkMhtgOKhX8bd/7lRjrR8OesUSgO1Bhtxkn/2wwNo6GPdHiVGLJjuUVyI3WQRMmTOCyyy6jq6uLffv2sWPHDjo6Onqvh7whSiliYmKw2+0cPXqUTZs2UVVVhdfrxel0YrPZcDgczJ07l/z8fBwOByJy1nmalFKYpkkgEGD79u2UlJTQ2tpKIBCgvb2d+vp69uzZQyAQIDk5ufdnfTqP0YwZMygoKMDn87FmzRpKSkqGYqIbmAu8OJRNNL2IEvXTdF9BQm1l6fdG2xiNZsiI+rI7O/+J+oqy10fblIFSc3DnduDzo23HWOTwobKdnIE/m5PEUk9itxOYipXgHRUyMjL45je/STAYZO3atTzwwANUVFT0foi7XC7y8vL4xCc+QXFxMfHx8Rw8eJDVq1fzwAMPoJQiGAyilMIwDO666y6uv/56XC5Xr4CKi4vDZrOd4H0a7yLqeHETEkdtbW0Eg8FeYfT444/z6KOP0tjYCNBbZXjVVVdx/fXXM2XKFOrr69myZQuPPvooBw4c6PUahX52sbGxXHHFFVx11VVMmzaNmpqaoYqliViJ3ik60Tt6KOS77mx/RX1FyQOjbYtGM0QMUcYf3O7Cc+rrdx8dbWM0mtMRzrOUgJXY7QZc0TpIRLDb7eTm5pKQkEBRURGPPPII//jHP+jo6ODYsWO0tbXh9XrJyclhwoQJZGdnM3nyZD772c/S3t5ORUUF77zzDk8//TQPPfQQzz//fK8wKCws5NOf/jSTJ09mwoQJ2O1nzlSWUHl/Y2MjW7du5cEHH6ShoaFXQFZWVmKz2bjhhhsoKirC6/XicrnIzMzE4/EQFxdHQkIC5eXl1NbW0tb2QZpAbm4uCxcu5Morr6S4uJisrCycTmc0RGYMlmDyYvXrahjqhmOQJxD512lXKNOBsmblKVHJhkgyiinKCnPHD+ZQUfwq3Ze/vbaybFy7tTUaIFdi1E+AL422IRrN6QinKCYA+VjtAqKqOAzDIDExEZfLRUZGBp2dnaSlpVFbW8v7779PXV0dr776KqWlpeTl5XHRRRcxc+ZMTNOks7OTmpoa8vLymDhxIm1tbXR2dtLa2srbb7/Nvn37EBGysrJITEzEZrMRHx9PTk4O8+fP7813Gqt9nY7vexQMBmlpaeH999+nurqa5ubm3vvKysp46aWX8Hq9FBUVkZCQgN/vJyMjgwsuuIC8vDzcbjexsbGICNXV1Tz//PM0NDSwa9cuWlpaCAQCZGZmcv7551NUVMSHPvQhLrzwQlwuV6/IjMLPx4ZVRZmD1ajyjBNLSlhTX7F7UB4emTvX4T50dIEIVyn4LANrzxEDtkc9nunT6uq2HxvM+RrNKNCNlQ/bl1szfP4naipLXhlpgzSaSAknhlKxhuYO6ltvf4Q8TImJiVx55ZUsX76csrIyfvvb3/L4449z3333EQwGmTNnDnl5eUyePBmXy0VCQgL5+flMmTKFyy67rLeJYnV1Nffccw8vvvgif/3rX3tDVe3t7aSlpbF8+XI8Hg9JSUm9FXYul6u3KWY4+4aTvtGoUO5Re3s77e3tmKZJV1cX+/fv58EHH2TTpk1UVlaeEGK02+1ceumlfOITnyA7O5v4+PjefK5QJVt3dzdHjx7lrbfe4pvf/CZNTU0YhkFMTAzJycnMmTOH733ve/h8PhITE4cr78vAynvzAtujvfl4Rm3Z0g2sB9a73YXfN2LUfyq4HUtk9v941CRxdv8A3a9GM15Q3I9wI9YX8uMREx7U4TjNWCacWErB8iwNi1g6nlB/pYKCAr70pS8xY8YM7r77bqqrqyktLeVLX/oSS5cuZdmyZSxatIjY2Nhe75BhGL2VW9/61re46aabCAQCHDt2jB07dvDEE09w6NAhXnnlFfbt24fdbu8VBKtWreKCCy4gMzOzV4CMtKcp5EHq7Oykrq6OJ598kn/84x+9Se7t7e1UVlYCMHv2bD72sY8xadIk4uPjcTgceL1esrKyiI2N7RVJoedw5MgRtm3bxjPPPMPatWs5etR6/0lPT+/9Wc6dO5ecnJxeD9QwYWCFdDOG64AzgZ4PiK+7s4ueFWU+RYReJgVf9fgKHqmrLH1veC3UaKKAQbUoblPwcJiruUaM+gVnYGKw5szgVGG4qUShEWV/iAgOh4Pk5GSmTZtGQkICzc3NHDx4kPLycjZs2EBrayvV1dW8//77LF26lBkzZvT2XbLb7cTGxjJ9+nRM0yQYDNLe3s6UKVNISkri0KFDtLS0EAwGefPNN9mzZw9dXV0kJCRQUlJCWlpar2BLTU1l1qxZZGZmnlBhNxQhcXxoLeTtKS8vZ/fu3dTW1tLR0UFXVxdNTU2sW7eODRs2YBgGs2fPZvbs2cydO5fExERycnK46KKLSE9PP8HDFLLt+P03bNjAxo0b2bdvH+vXr+9N0l60aBEXXHAB5557Lueccw4+n++EcTPDhIE1MkeLpQior9j1T7c3f5kYxmuoiPqb2VHyPeDq4batL+npeenK5ihQhkpGSBLTiENoMZRqMjEPxxgdu8dL5/GMjMLJQYfkYKokwyBJKdNuKKMpaASPGNia6tITdvV4AjVDpLay5JF0X8G1Cvm3vtcUfC7dm//32qqyl0bDtvGMiNjS0/2TgoZMVIZKtitlYKijdBt1tbWZFWfTsG6v1+vqlvgCQ0maMlSyMo1kEdWNqDZTGY22AOW1tSXlAy06OkEs9XTuTsEKnQy7WDoeh8PBlClT+Pa3v019fT3r16+nurqa/fv3s2PHDpRS/Md//AcTJ05kwoQJuFyu3oG8hmH0hqdiYmKYNWsWM2fO7K0Y6+zs5Otf/zrV1dV0dXXx3HPP8dxzHwyMTk1NpaioiFtuuYV58+b1VtjFxsaSkJCA3W4fcH+n0NlHjx6lq6urNzT48ssv8/jjj/Pee+/1enxChEKUl1xyCd/4xjeIjY3tPTuccDt+AG5raytNTU38/ve/55FHHgGsCsPQ4OIbb7yRG264gfj4+JFstWAAWVidvA2llDkSh45n6qvK3nF7828UMZ7C+vmdHuEjbm+hv75q95BKF/vDM3l6hnR1X62ElShm43BkWMcLKECs16IpAAadyhVMz/aXKMW/FOrvKXHq5bKysjHRnNSTXZyPClwHrACZhZ0UUQoErD8pa9CxKAOFwl1ztMPjK3wXzH8pUU82VO5ZF83qznSff6ZpGqfNDzVVoL2nJDtqeLz5s5Wyn/Y1Jrbu5rqKPXuiea5yOD9Pd/cirJSPE45TYjw4ccqUGY179zZH88xoM3HKlGRbu3Nq3/uD0lXfWLX34ED3y5hUPD0YUDHH3ycxtqq6/dtrwq0XEcPjnbrAFNtVYqqlbm/BNBMVJyiEnr9DJWBXeLzVXR5f4dvAK6Zp/qWhunTXQO0bCKmp+UlGjD2/7/2mdNcdrtpTEe3zZPp0Z3pT4DJT1NUizFWS4AdsvT8DUSis/wsK0w5ur7/N4/O/B+p5QZ6trSx5v79z+v6BJmGJpagnd0eKYRhMmDCBxYsXc//99/Ob3/yGZ555hmPHjvGnP/2Jbdu2sWTJEpYvX05xcXHYvCOwhEfommmapxU7LS0tbNu2jZ/85Ce9yeEiwvz587nxxhvx+XwkJSX129Pp+N5HTU1NlJWV8cADD7B///7esFtjYyN1dXUnVKT1JVTOb7fbT/n8QgSDQbZv3866det47rnn2LHjg9m1l1xyCZdddhl5eXkUFBQQFxc30uHGUDfvFGCCiLQopfQ39H6oryr7h8fr/zXCVyJYbhiorxFmyns0SPcWLlKY30XkIiU94i2yl5BNKYqBYkE+09wuzR5fwR+U3f7T+vKdh4bD1tMhIuLx5V+lkG+jmDPAueCxoBaCLBQlt7m9+WXp3sIHzS7ui0aOjYJXxDD7CocTMJSxA5g+1LNOQIw3RczY065RtueBD0fz2Lr922vc3oKviMhfwlzOtnfZ7sYqehiz2LvsizDM5/reb8N2P3DrQPczzcATYsiJAiOg7gR+evxdVmHIsU+6vfl3KiQ/JPJPhwKn9fploWHIdzxe/1M2u/ndQwfKhmXQuT3evlCZ5pq+99sw7gHuiNY5KTnnTHCYXd92K3WTKZbwjvwrjHIB54Gcp+BHHp9/vaHkF7XVJc+c6otQX0GUgVXu7RzsExgKoQ/ymJgYPB4PqampHDlyBK/XS2NjIxs2bOD111+nsbGR8vJyFi5cyMUXX0xiYuIJVVyhfYLBIHv37uW5555j+/btdHaG/2KbmZnJeeedh8PhoLy8nDfeeAOAhoYGOjs7cbvdxMfHY7PZsNlspKenk5eXR0JCAqZpcvToUfbv309TUxMdHR0Eg0GOHTtGVVUVa9eupaamhvj4eJYsWUJBQQFHjx5l3bp1vX2RQoQE1ebNm3n00Ue5/PLLcbvdJ7RBCP0eu7u7qa6u5sUXX2Tr1q1s27aNt956i46ODrKysrj00ku59NJLmT9//gmhxRFGsF5jSVivrU6sihhNP6gu+Y7EqGuBzH7XGlw/efLkr+7fv7+jv7WR4vYW+sXgXkRdPEBhcSqSQW6TQPAWd3bh/zrNo98fqVEX6d6i89xZBb9UirnR2VHylaifSgxf9fgK76ir3P14dPY9e6ivKl3t8fmvJVwIWckqT7b/73UVJc+PvGVjl3Rf/kI3xkMIhUP4mxSEq4JB4yPpXv/vJBh7R03N+63RtHO4ERFJ8xXc7FT8F1aLo2iw2BS1OM1b8Ione+oXwnlT+4qlLCyxNOqEQmsrV67koosuorGxkZ/97Gf85S9/YdOmTaxfv55NmzaRmZlJbm4uEyZMICEh4YQ96uvree2117jrrrvCjlgJUVhYyDe+8Q0SExN57bXX2LdvHwBHjx7l73//O0eOHOkVWg6HgxkzZrBixQoyMzMJBAJUVVXx6quvsm/fPo4dsyq5XS5X75Bgr9dLRkYGX/ziF8nPz6e8vJySkpKTxBJYAu+ll15i3759pKens2DBAtLS0k643tbWxuHDh9mwYQPf//73qampweFwkJKSwsSJE5k/fz4//OEPSU9PHyv9puKxGpzWALrUPQLq63cfdfv8Pxe4u9/FiqS2LuelwFNDPVdExO3L/4qI/AQ1LKH4OFHqG92ScIV7UuEn6w/u3jIMZwAgstTu8VV/VwnfIcIqwwHiBfWYx+d/NhATuHGsh47GGkYgeKtpt51PuA88xW9Tcs6ZceTA1qaRt2zs4fEVfAeMHxC917GhhM+Zto7zUn35lx6uLKuM0r7DSn5+fkyat+ABUXxyOPYXWI6yvefJ9n+0rqLkheOvhfMspQyHEUPB6XSSmprKF7/4RYqKirj77ruprKxk165d3HbbbcybN4+lS5dyxRVX4HK5ekNu999/P3/5y18IBE6f2xYbG9vbXmDlypXMmDED0zTp6Ojg8OHD/OxnP+Ptt98GIBAIUFpaSn19PU6ns3dg7ZEjR04Y37J06VJuvvlmkpKSiImJITY2ltzcXOx2O83Nzf2KmMrKSu666y5uv/12brrpJkQE0zSpq6vjn//8J2vXrmXjxo0cPmyNXcvLy+PrX/86ubm5ZGRkkJaW1m8IbwSJx/KQxPS3UPMBtkDsb017x38SSXWcwXUMUSzJ9OlOt7fgTyg+OpR9IqRQTPWmO7vwpvqK3X+O9uYez/QEj7f7aaVYFu29w7DS3mlbn5VVvLK6eueBETjvjKCmZk+dx1f4ZVB/DXPZ6wx23gN8eqTtGkuIiLizCn6JSCQh+UHszzQbxuuZmfnnHjpUVj8cZ0QLK3E78XlBLRnmo+JRPJ2eXfDx2orSv4fu7PuJnQYRVeGMGKGwWkxMDFOnTsXhcPQOii0pKeHFF1+kubmZmpoa9u7dy4oVK5g6dSrd3d3s3LmTPRHkJnZ1ddHS0kJSUhKZmZlkZmZimmZvr6LDhw+zZIn1+zm+wq2vncf/O3/+fJYsWdLb6DEk4Jqbm2lra8M0T5/r3NHRwfbt29m9ezeHDh0iNjaWt956i3Xr1rFt2za2bdtGZWUliYmJzJ8/n0suuYQVK1aQnp5ObGzsCbaMAVxYg3VHJbw7Xqmpeb/Vk+3/G6r//A2lWDGUsTIez/QEt7P7CeCiwTx+kDhEqYc9Xn9KXVXJr6O16YTJk1MczpjnFSyM1p79I9MDRnBjZmbReYcO7SofuXPHN3WVux/z+PzXANeFufwpt7fw7/VVu58ZabvGCh5fwX8qddrcxSOCWmcKu1FSg0idmCogkIqQp+BcrIHmp/t2nhe0GU/k5+evGCtFGOEIGAkPok4vlAS6FKwFtVEpY6uBNCBma9DAaQuqFBPSBJmJsEhgvjr1Z5JDKflDamb+rlBhRTixlBiF5zUsGIZBbm4ud9xxB8eOHWPdunXs37+fqqoqXnzxRZ599lmOHj3KsmXLeofJRkJbWxsVFRWkpqaSlGRpxVB+UkxMDF/4whd6xZFpmr2J3CGObzMQEkWnSgRva2ujoaHhtGHB46moqGDz5s0kJSXx8MMPs3r1agBSUlLIy8sjMzOTVatWcc0114xEK4DBEoflatdiaYCIqf6qRCJJdnWnZfkLgAFXxYmI4fb6/8LICqUQBsL/ur0FjfVVpauHupnIUrvbG/NUT0LrSJMZtJlPZWTMXDTe8kBGE4fiS93ChVhfqE5ARN2fnD19fXPF9pNzFs541IeVYnHYK4odwI9SXOaT/Qkcz+TpGXQHvgzc0ZPYHI7FzR3GLyCiopIRx+0rvF3g46e6LtCIUj/pMJy/i+C1shrAnVucSdD8d1HqVsL3lUyw2Yw/ichcpZTZVyylMobF0vG4XC7OPfdcHnroIR5++GHWrFnDvn37WL16NS+88AKmaVJVVRXRXqFmlqcL14UByRuwAAAgAElEQVREiM1mQyk16O7foVYGkToAXn31VbZu3Yrdbqem5oMq0o9+9KPccMMNJCcn4/V6x1LILRyxWEJci6UB4jTa13cqVwfWz/C0CCxiEGLJ7S34EagrBmNftBAxHvR483fXVZW9O5R93N6qn4MMt5v+dMxUjo4HgE+Mog3jiqqqkob07IJblZK/h7mcGWsGfgXcMNJ2jTqK88PcGwT1/Ybqsh8rpYKRbNPTfuA/MjIKf2c6eNyqBg173q3p2YUP1Fbs3joEq6NORsZUj9htPzzNkhc6xHFjc+XABHVPVe433N7CB0XUs1j9Jfsy2+31Xws81lcsJRPF4bnDwfGiJdRzqbW1ldzcXCorK3nxxRfZvn1gkzVcLhc+n4+4uJPzWcMJoKF4b1wuF2lpaREnXtfX1/d6yCZPnszcuXOZOnUqV1xxBQsWLOgd4TLGicHKhRsT2ebjiYqKinaP1/8vhAv6XWyoDwEPDWT/NF/hMgPuHIxtAo1KsR04jKggGG4R5VaKfMLPADsNyoUYj02ePHnGYKv6PNn+y0BuH8xjgWqB3QrVCGIguFHMxhosPiCU4uPu7Pzf1FeUvT5IW846aitKn0jP9q9W6mTvgRL1ifTsgr/XVpQ+MRq2jSG6RXFdbVXpoHITa2p27/d6vRd0S8KzwNIwS2wo9T/A8iFZGWVMm+0/OLUT5+H6qtJVkQrHcNRX7S7JyCi82LSr98OeI+qbhBFLiYxwM8qhEAp1XXjhhSxevJi2tjaUUrS0tNDS0kJ7e3u/yd1JSUn4fD78fj/x8cM+4YXk5GQmTZpEVlYWlZWVJzWmDIfT6cTj8XDBBRewcuVKVqxYQXJy8lgNuYXDySj27hr/yHug+hdLJlMGsmt+fn6MgfF/DKwO+SiK/zNstj/VHNwZ9lvJhMmTU+xdjssQ+awM7I136rEu57eAHwzgMUBPcrringE+7IgIvxSDh2sOlOw7aU9ZavdkVS5TGHcgXDqgnZXxUxFZGM3mlWc6DmV8pUuZS5GTO/4rJfd5vf51VVUlZ9xA7khRwhfrKkuGVMRRVVXVNnHKlKvsnfZ3gLyTzoBl7kmFc4ezSnUgpKbmJ9nijM+Fu6Zgc0qceUvdEIRSiJqa3fs9Xv+3EX4V5qA56ZPyp/R1SbgYpxVLNpuN+Ph4vvrVr/Lzn/+cZcuWkZ6e3u/jLr30Ui6//PLePkojYWdGRgZf+MIXWLYsskKdnJwc7r77bu68806WLl16UouEcYADKyY8pmOFYxVlqG0RLRQmD2TfljbbbYB/AA95Ttlt/rqqkjtPJZQAmvbvP1JfVbq6vrJkhYi6FkXYLsThEOHOrKzinAHYBEDake6vAAUDeMiz0t1dWFtR8v1wQglAqdcCtVVlL9VVlVwmoq4BIi5jF1jg9vkvGYA9Zz2VlbsOK+RUs+E83ULUigDGHYo19RUlD0Zjq8a9e5tFqS+c6roRVMPS4HYw2F22fyN8CoIpyrw1mgnp9dVZ9wO14a6poHFp32/68YxTsRTq2D158mRiY2NxOp2ce+65bN68mWeeeeak6jOv18t5553HRz7yEebMmTNi/YhCs+gWLFjQ25Pp9ddf58iRIyesS0lJ4YILLiA7O5uCggIuuOCCsdYOYCCExJL2LA0Cw1QHVGRexNxI98zPz49RYkQcshLkN3VVJV8c6Mia2orSv6d5/TsM+CfQ/7cXiA0YgdsZQKdfK6mbyJ+L4oG66tIvDOS51FaUPpGWVbDLMOQlrJ5h/aPUx4CTOhlrTk191e5n3D7/I0LYPjrXeXyFf6+r3P3YiBs2urTbTGPAXcFPR21V6ctun3+twIq+15Twsezs7NvGwmxHpdSpPNOPDTW/8eSzXgt4fAWrw4XyRWRGX89SHOM8CVdEyMrK4pprruGOO+7gc5/7HIWFhXi93t5KN4CMjAwuvvhi5s2bR3Z29ojaaLfbyczMZN68eSxfvvwEu8CaVTd79mxuvvlmvvOd7/CVr3wFj8czXoUSWCLJRSSzzjQnYdhVdYRLY7KzsyMKoze3y8eJoDs4AIo1dVUlXxrsbL+GqpLdApcIdEX0AJFVqan5EbcwcXsPXU2EAkbBK3XVWQMWfQAN1aW7lGlcCUT6IfIRmT59XL+fjgbdtpjbgFO85tWvMzKmnlQ1dyaj4G/D0Y7CwPzeKS4ltqv40agmPYm6ypKbBGYB3wY2AEEAUebvh+M8A3k23P0KCvt+eBmcQR9oDoeD8847j9WrV/Od73yH5cs/EKn79u3j/vvv5+233z7JqzPcBINBmpqaeP3113n44Yd7G0uKCA6Hg+uuu457772XxYsXk5p62pFR4wXBCsGNmySrsUS3aUT8AjVNV4SJd3JzhFseDYr5uaEkUALUVpa8r5CfR7RYkWS4jI9FvLlEOkdM2mwB+dxQJrDXV+96Wyl+EsHSclE8nt7QMeaa/I51jhzY2qQ4ZSgozbTb7htRg0YbMX83HNvWVpZtVBC2iaoRPgF8VKitLHm/rrLkx3WVJYudGOlK8fG66j2vDMdZqrv7FOkFKqVvWMTJGRQqMQyD5ORkZsyYQVxcHBkZGRQWFvLCCy9QVlZGSUkJq1evZvfu3eTl5XH++eczadKkYfHghHoz7dy5k02bNlFeXs7mzZspKyvDNE0WL17MwoULcblcXHjhhRQVFWEYxnhK4j4dNqzX1hkjxEeSbhztMRGO1FPKFg+cNgnW6/WnIZwb0X4ivzlcEZ1RCDHS+l+dynUzEYTjRMlK4Lf9rcvImBmPPcKOvqJ+X1NTsj+itach1mj7eadyfZkTx3Q0K+E1w+RlZdherqvYWTbUc85m6itLn/P4Cn8P6qYwl692Zxd8or6iNNwg3jONyobKPeuGa3NBPQVy28lXzLD9nUabyspdh4FHh2v/2tp9tR6fv56TR/AkntFiCT4o88/PzycvL4/LLrsMm83GmjVraGho4LXXXuPNN99kypQpmKbJ+eefT0ZGBjExMUNu8hgqhAkEArS3t1NTU8MLL7zAn//8Z/bt24eIkJKSQlpaGtdeey2f/vSnSUhIGCvz3KJJSCydEcpvpLE524N0RvaaCNq7+10YMORSlIrkG4GyByRqSbUVFRXt7uzCR0Spr0ewfHl2dnZcf3kTQXv7UkEiyrO0GWZUPBI97Rx+h3CBCC9jGi/VVWf8aygeK83JBGK677B32i8iTIjVUPIrd27xaz29cs5k/jWsFZVirEGpMGJJBlL4cabRSARi6YzGMAycTief//znufzyy9m3bx+///3vefvtt9m+fTv//d//zbJly1i1ahVTp05lwoQJUTm3qamJbdu2cd9997FlyxYaGhoQEebNm8cll1zCokWLmDJlCgkJCeM5L0kzTNjNuDgi9SzZYvrtHG0qc5FEplu3RDtXwm4E/xAMGhGIJeXqNF1zgPWnWyVKFkcowXcfOlC2I6KVEVBXVXJXtPbShKdx797m9ElFNyvTfIE+X7QUTDQCgfuBj4yOdSOE8NZwbh80AztsEvYzJ8PjmZ5QV7f9rBl8np2dHdctiVMI7zBK6HtnFxA4xeJxT6gvk9frZeLEiWRnZ2MYBoWFhWzevJmDBw/y2muv0dHRgcfj4dxzz+WKK65ARGhtbaWmpobXX3+d6urwuYf5+fnMnz+fjIwMYmNjaWlp4YknnmD37t1UV1ezadMmAGbPns38+fOZM2cOM2bMIC8vD5fLdaaE3MIRxHpt6Z4zgyHQkRBp1wV1LNCvWBIlxREJDMWrER06AA4dKNtxCjf3yccbFNOPWEKkOJKXlUK9FqmNmrFD7cFdL/Z48U7KsVPIv6X7/J+qrSx5eDRsGyHeH87NG6v3Vrq9BUc5uRmjBG2dk4Cdw3n+SJOcPX2ig+6pYqopIkwBmYLVuXsKuDI5dWpm3FkllkKICC6XC5fLxUc/+lGmTZtGeno6b7zxBjt27OCPf/wjACtXruwVVI2NjZSUlHDfffexc2f418/5559PS0sLhYWFJCUlUVtby7333sv27dt7K+Bmz57NkiVLuPrqq/F6vTgcA2x0PD4JiaVBVVOd9YiRFaHMVA0NJW3978e0yM6V4WlMp9Q7iPTbg0iUKo5gs6JIjhQ1vN/QNcNHsMP8mhFnXCRwUv8tBfekZftfaagoiWy21TjDlNPnHw4VpZTy+ArLQH2o7zWHXcbF6LO+yNy5jolVzfk2m226iJqmFAUopiJMiQErXDQIx0RfUWRyln2g2Ww28vPzufnmmzn//PN58sknuffeewFYt24dN954I2BVsHV0dPRWroXjnXfeYf/+/b35ToFAoNcLlZyczCc/+UmWL1/OtGnTSElJORNzk06FwhJM2rM0CJTCF+Gfdk1/VWtWQjQRlliaeyJbNzCUYWwTpfoVS0okN4LtIlkDNvZGtE4z5jh8uKwlw+u/2RRe4uS8xwmG4rfAh0fBtGFHTKN52M9AHQn7xmyOD7GUkVE4WdnVYiUsAM71KGYom+EERW+2VxSCNn0/rduxPABjej5cNBER4uLiiI2NxTAM3n33gz5XobEpYHXRXrhwIWlpaSQmJvbOZAtVuXV1ddHS0kJNTQ2bN2+mvr7+hEaYMTExTJs2jcLCQjweT+/ZZwkBrNfWWSXEo4UoicwTBGE7UR+PGRdMjDD9CVtQDcu3dVEqooGXojhtryWv1+tCTkolCL8Xw/NcNCNDTVXJWrev4DeChOs8fbnHW7iqrmr3gOYijgeM7s5hF0umcFTCqKWgqH6Hd48WWVnFOQGb+TEU12Hv8Yr1PIfh+kbe942mFYha+/CxTKjAINTzqLKyko6ODhobrfdxwzBISkrC4/HgcDiYN28e1157Lbm5uaSlpeFyubDZbCileqvd6urqKC0txeFwUFZWxrFjx6ivr6etrY1gMEhzczMHDhygsbERr9d7QuXbGS6cuoFjWKJJM3BmRrJIRSCWCJgRf1s0jLZ+858GgyBHVCRvaeqUwzMB6HZOSKI7QuXXGRzXiaoiuu2GdDm/QUz3JagwY31E3Z3qnfry4ao9FaNg2rDR5VLD/3msCD+gVKkx96GUlu332hTfUwY3oQY6rDsiWrEqt0/au69YauMsEUshWlpaePHFF/n+97+PUqp3sG1MTAwLFy7kc5/7HFlZWXg8HtLS0nA4HNjt9t5kcbCEl8vlIiUlhZycHGbPnk1NTQ2lpaX87ne/Y/v27Rw+fJgf/ehHxMbGkpmZyV133cW8efNIS0s704USWGKplZ7uq5rIERGb21twXkRrYVd/awxUbKTuveoJE4ZF3CpU/3lVgOpnqLetIxgbjLB4tMPpilBVjVnG3UDIaFNXt/2Y21f4WUG9wsmBlWSbYX9QRC7Vw4sHhojqYuzpohMQEcPt83/DUPxnf+8Lp90HuhSUo9iHsE/BPkPUfjNoO+AU80BVVUmDx+ffAycPJe8rlo4SeSv/cYdSCqUUjY2NbN++nTVr1tDe3s6uXbvYu3cv6enpTJs2jXPPPZekpCT8fn/v/2NjY09b1m+z2XA4HMTExJCQkIDH4yEzM5OYmBgOHjzIoUOH2LJlC3v27GHr1q389re/5YUXXsDj8TBnzhyKi4uZNGnSmdSI8ni6gGa0Z2nApGX7Z2OqyHpYGGpDf0uUBNpQkTkpvEeCicCpk/QGiyIxohwCoR9vUKA10j6n8UEzEat/ynglqp3ARcRwewvG3SiW+srdr3my/b9G8eWTLip1cZqv4HNE0MxUM37Iz8+PcXsL/oJSV0f8IKEFeF8ptd2ArSZGSVB1722q3lfZ76gjwQjn+O4rlpqxvEtnDKEvGS0tLTQ1NVFXV0dlZSWvvvoqv/71r0lJSWHChAnMmjWL/Px8lixZwjXXXENSUlJvovZACA30TUxMJCEhgUmTJtHe3s6hQ4d4+umn2bx5M6WlpWzdupV//vOfGIbBypUrWbJkCTNmzMDpdJKRkUF6enqvaDoDxFMncAQtlgaMmFwZ0Trocqr2fiu+pEsdVRHWFQRsZjLDIZZEJUeYcRk+PNBDbGzgaGt3ZJ/3Qeu5jGcSRJbao9X4MiU3N4nu8RnaM7pj7zTtHZcR5tu/KH6RmVn00nDMUtOMPCIibq//z9CvUOoGXkKxBlHr6yvLtg16RJMibJPbvm+bh+nnDWq8oZQiGAyydetWXnjhBVavXk13dzetrVY6xrnnnsvFF1/M0qVLSUpKIjk5meTk5Kh5eGw2G/Hx8eTk5HDTTTdx9dVXU1tby8aNG3n22WdZt24dzzzzDGvXrsXlcpGcnMzNN9/MqlWrcDgcGMa4fD/rSwfWCI7IBqlqgNAbRcHHI1mr4K1IpoTHxQVbWrttikjUStD0EEke1EARyYwkZUmQ0ya37t+/v8Pj83cA/SaiiqnG8vDVSLz54vXWRE28xnbEpgRt47Peoqbm/VbPpIJVmPIaJ7sWEwM280ERuUiH48Y/nqz8rynUNadZ0gDqnk5x3tdcsT1KnmNJCZcmfsaJpVCoLRgM8uabb7J161YOHjzIgQMHKC0tpby8nOXLlzN9+nTcbjdFRUX4/X7y8vJwOBxDHnFyPMd7hpxOJ6mpqaSkpOB2u0lMTMTn83HhhRfS2NjIxo0b2bJlC7GxsTz66KPs2bMHm83GsmXLWLJkCTExMeM5RNeOJZbGe97IiJLmzf8wkBfJWgVPRLKuR2BUEWaExMl7yixgUyT7DgjF7MjWqQhaF6g9INP7P1LNAtZGdO4IoxTNIv3/ProxfURJLAWE1HH5TtJD3cHSdR5fwa/CzTUTWO7JKrgV+L9RME0TJVJ9+T6bGD88zZI/BmICtzXu3Ru1isHs7Ow4cIXtBtBXLNUDLdE6eKQICSSlFPv376e+vp5AIMATTzzB2rVr2bVrFz6fj9TUVObOncu1117L0qVLycnJwW63R1UgnYrQ/na7ncTERKZPn05RURHt7e1UVVXhdrsxTZNgMEhpaSlvvPEGAPX19TgcDlwuF16vl4yMDOx2+3gTTm1AHWdZ8cBQEeTOCJcqUwUfj3xj2YlS/YslJXMi3jPio5fa3V6ZFUmBr0gk3YNlF9CvWMKQk5rujRVEiOjNXsEsotXR2WbOGe9dzxyq9dvdEn85SH7fa0r4aUaOf03NgZLoe0Y1I4Id445TJXMr+Pf6ypL/ifaZXcpVcGp7TqQGK7dkXBHyJHV2dnLvvffy1FNPoZTi2LFjtLW1oZTiM5/5DCtXrmTixIlMnDixt2x/NAVHKESXl5fH5z//eT760Y/S3NzMb37zGx56yGoZ8uSTT7J27VoMw+CWW27h05/+NGlpaTidzvE0R64NqEaLpYhxZxfeILAowuUbBlQyrcydIBf3u0y4SESMfhMiB4Anq3KZwoioj5upzO39rVGKHSJ8NIJ1K2TuXIfasmUsejcj+2YszAX+GI0DRRFRheVYpqqqqi3dW3iTErWOk8NxCWZQHhKRZdF8/WpGhp4UhOvDXlT8qr4q+kIJwBSZLqeI3vYVS4eApuEwItqE+ht1dnby1FNPsWXLFgKBAK+88goVFdbnxtVXX82cOXNwOp0sXboUv99PXFzcqHtljj+7b4ius7OTG2+8kcLCQrq6unj55Zd5/fXXAXjqqacoLy/H5XIxb9485s2bx6RJk3pDdH33HkMcAyrRYiki3LnFmaLU/xf5I+SXA9lfKeM1EXV7v7tCTrq3YCnwykD2Py2G8ZkIPRoNDdV7+vWiiOJVhO9HsJ877dCxy4BnIjq9v818/q+BBG1ibqxNT3pnKCJMFJURVW4rrhSR24b64d9TCbdkKHuMFWqrdm9I9/n/R8HXTr6qlrh9+V8G7h1xwzRDIi3LXwDKG+bSUdUl3xmuc8Vk/qmyOcN5lhqxcktsRFqXO0KEPEjd3d0cOHCAmpoa2traeOSRR1i7di1Op5Ps7GwWLFiAYRh84hOf4JJLLhkTAqk/DMPAMAwcDgdLlixh8eLFdHR0kJSURFdXF6ZpUl9fz1//+lfa2tpYsWIFDQ0NFBUVkZqaSmpqKllZWTidzrFURRcac9KC9drSYqkfZPp0pzsY/CuQHtkjVFl9VemTAzqj2/4qzu5OCF/1cTym4ktESSxN9E6ZZBf7VZGsVbAmkmqW+qzETe7aoy300+0bQAy+RBTEUkbGVI9ht/23QjlNJbhrjnZ4fP63lchGA/WmabNtqi/feSjS/UyRnRKZgvS5s/MXA+sGbTyQluVfCSp3KHuMJZzS9t1O5fowUHjSRWX82JNd/EJdxc6ykbdMM1gMQxWHc/Ao4dX6+t3DlletUMtP9akZrnVAU8+/SVidLMcMwWCQ9vZ2Dh8+zC9/+UuefvppTNOkpaWF2NhYMjIyuP3227n00ktxOp2kpKTgcrnGgmgYECKC3W4nPj6eT33qU1x55ZV0dXXxt7/9jccee4ydO3fyxhtvsGXLFpxOJ1OnTuXCCy/klltuwe12j6UqOoXVjPII0KTd4acnOzs7zq1cfwPOj/hByvjpQEtk6+q2H/N4/a8hXNrvYuGqdG/+xbVVZS8N5Ixw2MX+CyKoXLOOlacjWae2bOn2+PxrgOv6X6wudnsL/62+aveQBJNpN77Nie+NscAiUWqRAiQQxOPzl4uSNyUYuKOmZk/dac2CrRG/Q5nG1xmCWOoJb3xjsI8fi1RUVLRn+PJvMjHWY33JPw7lQgUfEpElbu8p01E0YwwT0sL9TRhqGKpze8jILpohpxkyfoJYUkqZInIEKAfyGUWxdPw4kra2Nurq6nj11Vd55513aG9vZ+PGjXR2djJr1ixmzpxJXl4eSUlJLFiwgEmTJvXm84w3oQQnVtElJSWRmJhIMBjk8ssvJzc3l9raWvbt28f27dvZuHEj27dvp6WlhYqKit4cqI9//ONMnDixVziN0s/BxMpVqtVC6fRk5uRPC5quP/TkpUSEwL/qqkt+P5jzFHKfoPoXS4AS4/8yM/MXHjpUVj+YswA8Xv/1RJBb1ENVfUZCRGIJwFA8YEoEYglA1L3p6Xmba2v31Ua6//GkZfvnG8gXI1iaq0RRV7u336nxcdK6sVO5ImqBAOqK9Gz/VbUVJQPyJoZw+/xfRanFg3nsWKamsmyTx+v/BcK3wlxenOYt6DfsrBk7iElMuHCYsorQhgWT4O2n66gSrj3dEaAMq7R4RBu5hQSSaZp0dnbS2NhITU0NlZWVVFRU8Nhjj/Huu+8yadIkJkyYwOzZs1myZAlLliyhoKDgjJyzFhqrYhgG06dPZ9q0aQSDQXbv3s2GDRuIiYmhoaGBpqYm/vznPxMIBCguLiY1NZWMjAxSU1OZOnUqLpcLh8Mx0iE6E6gABvXBdDbg9hb6EfM2wViF9B8WO46AUuYXBitCG6pLnnV7C/YSprFfGKYEbcbLPl/R8srKXQMuXXf7Cj5siDwccfGV8L8DyQGqrS59xe0tKAX6dR0I5CiH48UJkydf2LR//4CKWTIzi3ING38jzNyocCjF7yP5/VRUVLS7ff4NAssj3dfjzS+vqyp7t//VH5DuLbhIRH42zovgTkmyy/zPlg7jCqUo7ntN4EeM87Y4ZxWijoUVLkLWcByXmplfbLMZnzzdmnBiqQlLLC0YDqP6QylFV1cXVVVV/POf/+Spp55i8+bNvR6mqVOncscddzB79myys7OJi4sjNja2Vyid6YRCdH6/n9zcXK666ioqKytZt24dP/rRj2hsbKS0tJSvfe1rGIbBzJkz+e53v0tBQUGvp2kExWRILNWM1IEjiSCulJxzTjuKJK6r3a6chjUQNmh6lEiWAh9IvsAyEYoj7Gbd9/SfDPTD8niUUqY72/8TUTwQ4UNmdon5lttbeHukYaz8/PyY5nb5uoF8T0Xupa7vMmJ+E+FaAJRSyuMt+AkikU6dn+nodr6V4fXfWlNVElHvJXd20YViMx8hgv5UPXQHCfwhwrWIqGdQEpFYApIR4zW3t/BTkfwuRJbaPVlVtyuRHxP+Pf+MoKysrDNtkv8zhuJNTn6ecQxhpphmZDExDoadOaKIegsQmTvX4bYZD9DPl6BwfziHsQZyjsiUbqVUryeppqaGTZs2sXv3bvbu3UtFRQVlZWUkJCRw/fXX43a78Xq9zJs3j/T0dOLj408YaHumc/zzdDqdOBwO4uPjewVjYmIiTU1NlJaWsnr1alpbW9m6dSt33303EydOJCcnh/PPP5+pU6fi8XiIiYkZ7p+fCezHqoQ781DqHmew857TLQnajOPGBxsntBcawjf8Z+urSv5z8A+3aKgsfcjtLfgscG5ED1BMFlFPe3z+9Yj8DQLP1Vfu3Xe898Tr9bq6bfFzxZSPKIzrAN+AnqdS3zpyYOuAK3Lrq8v+6PYVfAlFpL2hpprCyx5vwYvKkL8peKGhoqTq+AXp6XnpOO2LlJKbBD7MgFSt+r/Gqr0HI13dZcQ+4gx2/RhURG0VgGQR9bTHW/AiYjzoUOqfVVUlvSE/ETE8Pv90U6mL3F6+rJDcyG0fvzQcLHnL4/P/DPj2aNuiGTzdYn8vRnWbnFxkttDtLfTXV+0uicY5IiIeb8H/KvpvpXEqz1IpHww+jfo3kVADya6uLo4ePUpFRQX19fXs2bOHNWvW8N5779Hc3ExRUREFBQUUFxfz2c9+Fq/XS1xc3FklkE5H6OeQlJREcXExxcXFtLa28s4771BXV0dTUxNHjhzh+eefJxAIMGnSJOrr65kxYwZTpkwhMzMTt9tNcnJybxVdFH+uQazO3Qc4Qz1Lo4EIO7udgRujkQOmlDI93vwvihibBuD5AVhs5b3Y7nF7C0yP11+HQTsKN5KQgDlYISiv11eX/mEwj1RKmeneoq8qMddxUpLv6Y6US0RxiQAen7+bD5qnpuJwJA/yiRzussX+YCAPOHJga5Mnu+BRlKwa0Ekil4C6pFsg3edvVFYahdPtLXArpWLOxnfJ+hTHD9xHuq8AZoy2LVjRhq8AABFdSURBVJrB0VyxvdHj82/iZBEjIuqHQPgeTANARGwer//XCnVLn0thdU84IXQMK3TSgNVMsN+S3IES6pF0+PBh3nvvPR566CF27NhBTU0NHR0ddHd3M2vWLH75y18yadIk4uLiSEhIGJFO2+Mdl8vFggUL+N3vfkdnZyfr16/n1ltvpampiaqqKv74xz9it9vxer2cd955fPjDH2bevHm43e5oN+nsxGpDUcX4nvY+lthKV/fFjRX7otbev66q7F13tv8rohhQ6Os4DISMKHSDrlZ24+NDmedVW7XrTY/P/13gvwe5hQMI19tlQAj8+2C8Y4Yh/2UG+TiDDBcpmIh1O6tR27d3ub35nxExNhFhfplm7KFQDwsSzuNzndvr39FQXfr/Bvt+kZlZlOv2FTyslOpbeayAnwN39X3MSWJJKaVEpBMrb8kPJyfLDYbjc5HKysp45ZVXqKyspLq6mh07dtDR0UFOTg7XXXcdEyZMICMjg8LCQhITE8d6w8UxQbg5dKZpsnDhQu6++27a2to4cOAAzz//PAcPHqS8vJzOzk7Ky8vJycnp7U9VXFyMx+PBNIfsuGgEtgHNuhJu6Aj8q0MclzXXlkRdeNZXlPzWk+0/B6un0mjQYYi6umYAvYlORX1V6U/SfAULRPGRaBg2cNQ9tZWlDw/mkTUHSvZ5sv0/RPHjaFt1tlFfVfZOerb/x0rxvdG2RTM4UuLUH5rb5buE+QIjwg883vw5Pl/RqoEUnbhzizOlO/hVbNyGOvlLiULdL6a8gBGBWOohgJW3NIMoiaVjx46xYcMG7HY727Zt48n/v727j63yvg44/j332sa8mmL8AjYGJxiH1EmWpYvGtk6TYGPVlvQlm5pNzRYp2pQtaqWlaxRpm9ZOzda9tJv6x6Yl7ZSqWRVtaiFL6NJmSUtDICRAXni3Q4zxveBXjMExtq/vc/bHuRduqQ2278tzr30+0iNefHmeH/gKH5/f+Z2zYwdBELBq1So2btzIypUrue2223jooYeoqakppVEeRScdOEWjUZqamnjwwQevzJwrLy+nvb2dc+fOMTw8zMGDB9m7dy91dXXEYjE6OztpaGjg3Lnsv24BB5npKAc3HQX9+pLyxOO9nSfH8vWQ/lj752oaNgkwk2PxuSNcVCIf7+k+vj8Xt1NVbWlp+fTw5ch3sTqjQtrVH+/482xu0F+3/Ks1PSO/C5r3WXYiHFNlETM7EVly+uqWf7mm59K92Ew9V2I6OjrG6xtueSQQ3TnVxxW5d0KC92sbWr8l6PNlyFuZdXsANTW3LKdcNokEWxC2C2xHps42qrBzINbwuZo1Z7dM9fHpgqUEcBRy1wAqFovx2GOPISJXxpRs376d++67j61bt1JVVUV5efmVbtsutyKRCC0tLTz++OMkEgl6enp48803eeqpp9izZw9dXV0888wzPPvss0SjUUZHR7N9ZB+wnxKcNVg0hE6QP+3rPvlivh+Vyv49UreutT/13Xj+07hKj0bknv4zxw/k8rYdHR3j0tb2qZoLiW8zk2aVuSD6H/11Kx7W2OwahF5LDxxIVDds/ERUovvIwZbgdZ50RBLBVi2L5v29FRY9cCBR19j6IPDGLGvyXJHoiZ94LjXO5s+mfIGyAuGzinw2AdQ2to5wtUXEEllE1QwrKF9YWRnc368/mqxd2zrlC6YLlpLYKaYubFTFkuu8dkaCIGBkZIRt27axbds2qqqquOmmm64UGmeO6XC5l245sGzZMlSViooKKisrqa6upquri5GREXbs2MGhQ4eYmJjI5lHK1cG5BTtVOZ8odCE8MVC3/OlCD37t7T75xfrG1lcDeJqZH5OfNRWeK0sGf3QuPvdGl9e9/5EjE8Cn6xpad6vwVWbYOXy2BCZQ+ave2Ml/yNU9B+Pvddevb/3VIMkPgI25um+G71cQ/YNYT/tgbePUXxjmi97YyXdqGm/5sqB/E/Za3Nz0xds/X9OwaREzy3ovS10zlQT9Yn+842/7blAuMmUAlOrkPQicwYKmm4Dls1jAz4hGoyxfvpwtW7bwwAMPUF1d/VMFxekTcq4wFi9ezPr162lqamJ8fJyLFy8Sj8eJxWLEYlmd9E937T4N9Hi90owNgexE+K+BumUvFzpIytQTO/nyyvW3374oOf6Xijw8i+PsM6AdKpEv9Xef+M/c3XN6vfGT/7pmfcvuZDL6d6D35Pj2L6hEH+2L5X7uWE/XyfdX3XzzR8onyv5Nld/L0W0vKXxpIN7+tWwK6UvNQP2yr9T0jHyiEFubLvdS79VHata1viPKP5FlLJJhr2jkC73x43tn8uJps0WpQu9uYC9Qne0CKysr2bx5M83NzVRVVREEAYlEwrNJIVPVK+0H2traOHHiBGfPns2mwDsA3gVOeKCUSUZBR7DWHEPAeZRToG+JyKG++Nqjqj+aDHmRV6ROc32+trntH3Vy8lFRvR9YN8fbBcA+gSf74g3fKfTf81xXx1Hg3vrGll8MiDwK8ltzDwBlVNHvSkS/0XemPauBtjdy/tSpYeD36xtbv5mEJ2TujYKHVOSbFYH+/bU1HQuBHjiQqG+69Q+DIHmAGQyPdsWpv/vkk6vXte6KBvy1Cp9hbqdGJxF5RTX4en+sfdds/qBc7xsMEWnC+hz8BfBhsqhjKCsrY9WqVWzatIkNGzZkPmOut3Q5kPn57+zs5PTp08Tj8ev8ievfDmsZ8ASwU1WPZL9CVwxERGrX3rJFRT8GeidwB9Nv0yWAYwrvirI/iLDz2oaPYaqtbVum5YnfFuGj2N/jdqb9ZlBGQd8CPaDK/mBMdw0Odlws4HKvqFm7+S6J6CdBtwJtTLvdIKOgRwU5FBA8P7Cy4qXUtqRzeVfXtHm7BsEUtXD6L32x9qlrj2apsXFz9YTqxxHdDtwFbGDq/mrp+uu3VdhXNhnsmOuMyxsFS0uAZqzvwC+Th55Lbl65jM2B+wLwQ1UN5YuKK4zm5ubK0VGpCsoXVQnJJZGoXBxj0dDwmcMl1y6iurplBZVaVVEmKwASQeTComBkKB6PZ33SIV9qm9vqo8F49aRGF6tqRAIZloqy4f7TR/tVsys0d26uChEsXUva2ipWDY3VCdGlEYLFkbLIcOr/ogu52nK+btG2qo6KyFmsX04Tll1ybjqDwCHgtAdK819nZ+cYMMY8GJScyhaV1Hu2r/NID94d37n0gY7ufD5jJmf0x4B9wLF8LsTNC3HgRaxtgHPOOTcvzCRYSmAFu8ewnjlFU4TqioZivS3eB17Dx5s455ybR24YLKnqJNY+4DhwCqtLcS6TYlmlE8BxVfXeSs455+aNGbXKThVItQPPY3UpzmVKAq8C+xdS/xbnnHMLw2zmipwBfoJ19fbMgUtLF/i+iR3RdM45t1BpkMMmtsVjxsGSqg4CR7CTcVlPWXXzxgXsPXFYVbNq/e2cc660BdbE+mcIUlLtRK4123lvHwC7gA9hI1AiFGLgpitGmrreA75Nno9tOuecK34R5RemqsUIlOGCLyaHZrMNB9ad+S2sl05X6tdu4erBskqv4rVszjm3oNXX37FU4ZNTfUwiOlTo9eTSrIIlVU2qai/wDrAHSjtSdFlR7H2wX1VjqjoW9oKcc86FR8vGvgbUTP1Rea+gi8mx2W7DpR0Dvge0YPOJluZsRa4UjGOB8ktYXyXnnHML1Jo1mzcko8FXgU9N8xIdp3x/IdeUa3MNlvqwrbjXsAGU6TEoXr+0MPQD+7ETcF0hr8U551wBVFe3rIguif4JgAa6SGA1wp1E2cLUg2zT9g53HynpZsVzCpZUNSkiA8AOrNh7M7Ovf3KlKcCKur8BdKhqIuT1OOecKwCtDFaika8AyCxSIyL6z/laU6FkE+CMYx2b96SukhpC6ebsHWA3Vuh/IeS1OOecK2ICr/TFOnaGvY5szXUbDlUNgPMichCoBVYDi4DKHK3NFZcE1jriVWB3qtDfOeecm5IIxyKTwf2qmgx7Ldmac7CU4TgwCvw8tiW3JvX7Xr80fygWKJ0GfoAXdTvnnJteICrPBuM83NvfcSnsxeRCLoKlJFbw+y0s+3AflmHyYGn+GAfeBf4dG2nidUrOOecyJYHDoLuTSX1y8FzHsbAXlEtZB0uqqiLyAfAGllVqBO4EVuABU6lLd+k+CvwYeAW44MNynXNu4Rn60OKe1eeTH8n8vbLyybFJjQ4ti46f7+zsnLf99nKRWSK1HzkoIruxAKkeWAKU5+L+LjQBMAH8L7BTVXtCXo9zzrmQ6JEjE8DBsNcRhlwf9z8H7MMaVh7O8b1d4XVh26uvAKdCXotzzjkXipxkltJUdVRETmOZiEqsYeU6/IRcKeoGXscC36Oq6q0hnHPOLUg5DZbAAiZgj4iUY2NQ7gHquH53T1c8AmASOAB8H/g/r1Fyzjm3kOU8WMrwLlYdXwZ8FNiUx2e53OnHRtn8N9ZPyQMl55xzC1regiVVHRSRd4CdXA2aGrC2Aq44ncUySt8DXlfVsyGvxznnnAtdPjNLqOqwiOzC+vJUANuBmnw/181aEvscvQX8D/Adn/nmnHPOmbwHLak+TAeBy1in718Dbs33c92s9AF7sa2317CaJeecc85RoAyPqg6IyNvYFtxlrCN0K7AYb1wZlnTDyfeA/cAL2NZbLNRVOeecc0WmYNthqaPnL4nIEHAJ+AxWw5RuK+BBU+Eotu02CuwGdgAvqapnlJxzzrlrhFE71I5llgaAe4FfwQImD5YKZxw4iQVJPwaOYHVLzjnnnLtGwYMlVb0oIh3YF+wJ4DxwN9aLaWmh17PAjAMXsELuV4EXgU5VHQp1Vc4551wRkzDb6IhIFdAG/DEWMK3H6poEzzTlSvoTPIH1UDoKPA28DAx4HyXnnHPu+sIOlqJcHYny68BvAr+EDeH1YCk30p/gg9iMt51AJzDo7QGcc865Gwu135GqJoELInIR+6I+jB1j/zmgGatlyvWw34VkDPv3PAz8BGsLcAiY8IySc845NzOhZpauJSLLgduA3wF+AzsttwQoT78kpKWVEsWKtceBHiyj9AxwSFXjYS7MOeecK0XFFixFsSLvtVjQ9DFsW24jlmHyYOnGAqw26XXgh8A+4AxwSVUnwlyYc845V4qKauxIalvuooiMYqe2LgGngDux4KkeWBHeCovaGBYkHQfexjJKb2On3bw2yTnnnJujososTUVEVgN3YFtzd2G1TEuxWXPREJdWDAKuNpfsBd4FnsMaTfakgk/nnHPOZaEUgqVyLDhajc2UuxvYhm3NVYe4tGIwCpzGirf3YIXc54Bh33JzzjnncqPog6VMqSxTM5ZpuhWbL7cRa2iZ3p6bz3VNihVu92LH/zuAY1iQdALPJjnnnHM5V1LBUiYRacZqmbZirQbWYyfnKrEtuvlSEB4Ak9gA4stYXdJhbKttL9DuWSTnnHMuf0o5WFqEbc9VAY1YlulurK7pZizTNB+CpQ+wrbVDwBtYXVI3kB5IfNl7JjnnnHP5U7LBUiYRWQrUYkHSTViWaR0WRK3FTtFl9msqVknsVNsAcBaIpa4u4H3sZGAMGFHVIKxFOueccwvJvAiWMomIACuxWqY7gduBD2PB1Aps9lwFFjilt+sK3SVcuXqSbSLjGsUCpXZsq+1Q6udnPThyzjnnwjHvgiW40tyyEssmLQEWY5mmZqAl9eOG1LUcC5oKKQmMAHHsNFsnljU6hW2xDWOB0yi2zeZ9kpxzzrmQzMtgaSoiUoW1GqjH2hCkr5WpqwrLPC3DaqHSQVYlV7NRZVhvp/QFFvgkuVqIncBOrI2lrlGs7ugD4CIWCA1jTTcHsYLtAWyGWz9wQVUn8/KP4JxzzrlZWzDB0nREZBUWQDVh2ac12JZdDbCKnw6i0nVP5VgABVe30hJcDY4yA6J+LBDqxeqNzqR+PO8ZI+ecc674/T9N+qBbGoY4QgAAAABJRU5ErkJggg==", + "align": "center", + "maxWidth": "200px", + "objectFit": "contain" + } + }, + { + "id": "text-1765337703347", + "type": "text", + "gridPosition": { + "x": 0, + "y": 29, + "w": 12, + "h": 3 + }, + "properties": { + "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\n\n", + "fontSize": "16px", + "fontWeight": "normal", + "lineHeight": "1.5", + "color": "#000000", + "align": "left" + } + }, + { + "id": "spacer-1765337731196", + "type": "spacer", + "gridPosition": { + "x": 0, + "y": 19, + "w": 12, + "h": 10 + }, + "properties": { + "height": "40px" + } } ], "templateId": "modern" diff --git a/tests/Feature/Design/stubs/test_design_1_mock.html b/tests/Feature/Design/stubs/test_design_1_mock.html index 7d08690f38..e0db6bcc2d 100644 --- a/tests/Feature/Design/stubs/test_design_1_mock.html +++ b/tests/Feature/Design/stubs/test_design_1_mock.html @@ -115,31 +115,26 @@
-
logo
+
-
Untitled Company
-Address 1
Address 2
City, State Postal Code
United States
Phone: 555-343-2323
Email: phamill@monahan.info

-City, State Postal Code
-555-343-2323
-phamill@monahan.info
-
Bill To:
Jacobi, Greenfelder and Fisher
- 747
83848 Domingo Corners Apt. 281
North Kelliton, Arizona 89888
Afghanistan

-North Kelliton, Arizona 89888
-
-gail.brekke@example.org
+
Untitled Company
Address 1
City, State Postal Code
555-343-2323
id number
http://staging.invoicing.co
deja15@deckow.com
+
Homenick-Gleason
309
VAT: 334205845
West Alexannebury, Montana 03782-6966
Afghanistan
mitchel36@example.net
-

+

-
Invoice #: 7215645438696
-Date: 27/Jun/1978
-PO Number: Quasi in.
-Due Date: 22/May/2018
-Amount: $103.26
+
image
+
Invoice #:1362670894990
PO Number:Eos.
Date:19/Apr/1995
Due Date:30/Aug/2023
Amount:$722.82
Bo Bo Balance MoFo:$722.82
-
ItemDescriptionQtyRateAmount
1.75$49.58$86.77
+
ItemDescriptionQtyRateAmount
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
-
private notes generally go here.
-
Subtotal:$86.77
Discount:$0.00
Tax:7215645438696.tax
Total:$103.26
Amount Paid:7215645438696.paid_to_date
Balance Due:$103.26
+
private notes generally go here.
 
1362670894990.terms
 
line spaces dont work that great?
+
Subtotal:$607.39
Discount:$0.00
Tax:1362670894990.tax
Total:$722.82
Amount Paid:1362670894990.paid_to_date
Balance Due:$722.82
+
+
Invoice.footer
+
+
+
+
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
 
 
From d591d2a81328997d4f56d9c759f1f2a17242824f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 11 Dec 2025 08:25:00 +1100 Subject: [PATCH 024/177] Fixes for expense tax calculations --- app/Export/CSV/ExpenseExport.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Export/CSV/ExpenseExport.php b/app/Export/CSV/ExpenseExport.php index 781fb474cb..668a4d8d21 100644 --- a/app/Export/CSV/ExpenseExport.php +++ b/app/Export/CSV/ExpenseExport.php @@ -276,7 +276,11 @@ class ExpenseExport extends BaseExport $total_tax_amount = ($this->calcInclusiveLineTax($expense->tax_rate1 ?? 0, $expense->amount, $precision)) + ($this->calcInclusiveLineTax($expense->tax_rate2 ?? 0, $expense->amount, $precision)) + ($this->calcInclusiveLineTax($expense->tax_rate3 ?? 0, $expense->amount, $precision)); $entity['expense.net_amount'] = round(($expense->amount - round($total_tax_amount, $precision)), $precision); } else { - $total_tax_amount = ($expense->amount * (($expense->tax_rate1 ?? 0) / 100)) + ($expense->amount * (($expense->tax_rate2 ?? 0) / 100)) + ($expense->amount * (($expense->tax_rate3 ?? 0) / 100)); + $tax_amount1 = $expense->amount * (($expense->tax_rate1 ?? 0) / 100); + $tax_amount2 = $expense->amount * (($expense->tax_rate2 ?? 0) / 100); + $tax_amount3 = $expense->amount * (($expense->tax_rate3 ?? 0) / 100); + + $total_tax_amount = round($tax_amount1, $precision) + round($tax_amount2, $precision) + round($tax_amount3, $precision); $entity['expense.net_amount'] = round($expense->amount, $precision); $entity['expense.amount'] = round($expense->amount, $precision) + $total_tax_amount; } From 1467d668e8af26b1e65aff8f1c90686764905b9b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 11 Dec 2025 13:01:50 +1100 Subject: [PATCH 025/177] Stubs for Json Invoice Designer --- .../ValidationRules/Account/BlackListRule.php | 5 + app/Services/Pdf/JsonDesignService.php | 361 +++++++ app/Services/Pdf/JsonToSectionsAdapter.php | 930 ++++++++++++++++++ .../Feature/Design/JsonDesignServiceTest.php | 245 +++++ .../Design/stubs/test_design_1_mock.html | 10 +- 5 files changed, 1546 insertions(+), 5 deletions(-) create mode 100644 app/Services/Pdf/JsonDesignService.php create mode 100644 app/Services/Pdf/JsonToSectionsAdapter.php create mode 100644 tests/Feature/Design/JsonDesignServiceTest.php diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 320e9f4528..331037da3e 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -22,6 +22,11 @@ class BlackListRule implements ValidationRule { /** Bad domains +/- disposable email domains */ private array $blacklist = [ + "asurad.com", + "isb.nu.edu.pk", + "edux3.us", + "bwmyga.com", + "asurad.com", "comfythings.com", "edu.pk", "bablace.com", diff --git a/app/Services/Pdf/JsonDesignService.php b/app/Services/Pdf/JsonDesignService.php new file mode 100644 index 0000000000..770146afb0 --- /dev/null +++ b/app/Services/Pdf/JsonDesignService.php @@ -0,0 +1,361 @@ +pdfService = $pdfService; + $this->jsonDesign = $jsonDesign; + $this->adapter = new JsonToSectionsAdapter($jsonDesign, $pdfService); + } + + /** + * Build PDF using JSON design + * + * @return string Compiled HTML + */ + public function build(): string + { + // Ensure PdfService is initialized + if (!isset($this->pdfService->designer)) { + $this->pdfService->init(); + } + + // Convert JSON blocks to PdfBuilder sections + $sections = $this->adapter->toSections(); + + // Generate base template for JSON design + $baseTemplate = $this->generateBaseTemplate(); + + // Set the template on the designer + $this->pdfService->designer->template = $baseTemplate; + + // Create PdfBuilder instance + $builder = new PdfBuilder($this->pdfService); + + // Override the document type to use custom sections + // This prevents buildSections() from generating default sections + $this->pdfService->document_type = 'json_design'; + + // Populate table bodies before injecting sections + $sections = $this->populateTableBodies($sections, $builder); + + // Inject our sections before build + $builder->setSections($sections); + + // Now build normally - buildSections() will be a no-op for 'json_design' type + // We need to manually run the pipeline steps since build() is private + // Actually, let's just use the existing build process + $builder->build(); + + // Get the compiled HTML + return $builder->getCompiledHTML(); + } + + /** + * Populate table bodies in sections using PdfBuilder + * + * Finds table elements in sections and populates their tbody + * using PdfBuilder's buildTableBody() method. + * + * @param array $sections + * @param PdfBuilder $builder + * @return array + */ + private function populateTableBodies(array $sections, PdfBuilder $builder): array + { + foreach ($sections as $sectionId => &$section) { + if (isset($section['elements'])) { + $section['elements'] = $this->populateTableBodyElements($section['elements'], $builder); + } + } + + return $sections; + } + + /** + * Recursively populate table body elements + * + * @param array $elements + * @param PdfBuilder $builder + * @return array + */ + private function populateTableBodyElements(array $elements, PdfBuilder $builder): array + { + foreach ($elements as &$element) { + // Check if this is a table element + if (isset($element['element']) && $element['element'] === 'table') { + // Check if it has a data-table-type attribute + $tableType = $element['properties']['data-table-type'] ?? null; + + if ($tableType && isset($element['elements'])) { + // Find tbody in table elements + foreach ($element['elements'] as &$tableChild) { + if (isset($tableChild['element']) && $tableChild['element'] === 'tbody') { + // Populate tbody with rows from PdfBuilder + $tableChild['elements'] = $builder->buildTableBody('$' . $tableType); + } + } + } + } + + // Recurse into nested elements + if (isset($element['elements'])) { + $element['elements'] = $this->populateTableBodyElements($element['elements'], $builder); + } + } + + return $elements; + } + + /** + * Generate base HTML template structure for JSON designs + * + * Creates a minimal HTML skeleton with placeholders for each + * JSON block, respecting row grouping for blocks at the same Y position. + * + * @return string + */ + private function generateBaseTemplate(): string + { + $blocks = $this->jsonDesign['blocks'] ?? []; + $pageSettings = $this->jsonDesign['pageSettings'] ?? []; + + // Build page CSS from settings + $pageCSS = $this->buildPageCSS($pageSettings); + + // Get blocks grouped by row for layout + $rows = $this->adapter->getRowGroupedBlocks(); + + // Build container divs with flex row wrapping for multi-block rows + $blockContainers = ''; + foreach ($rows as $rowBlocks) { + if (count($rowBlocks) === 1) { + // Single block - render normally + $block = $rowBlocks[0]; + $blockContainers .= "
\n"; + } else { + // Multiple blocks on same row - wrap in flex container + $blockContainers .= "
\n"; + foreach ($rowBlocks as $block) { + $widthPercent = ($block['gridPosition']['w'] / 12) * 100; + $blockContainers .= "
\n"; + $blockContainers .= "
\n"; + $blockContainers .= "
\n"; + } + $blockContainers .= "
\n"; + } + } + + return << + + + + + Invoice + + + +
+ {$blockContainers} +
+ + +HTML; + } + + /** + * Build CSS from page settings + * + * @param array $pageSettings + * @return string + */ + private function buildPageCSS(array $pageSettings): string + { + $pageSize = $this->getPageSizeCSS($pageSettings); + $pageMargins = $this->getPageMarginsCSS($pageSettings); + $fontFamily = $pageSettings['fontFamily'] ?? 'Inter, sans-serif'; + $fontSize = $pageSettings['fontSize'] ?? '12px'; + $textColor = $pageSettings['textColor'] ?? '#374151'; + $lineHeight = $pageSettings['lineHeight'] ?? '1.5'; + $backgroundColor = $pageSettings['backgroundColor'] ?? '#ffffff'; + + return << ['width' => 210, 'height' => 297], + 'letter' => ['width' => 216, 'height' => 279], + 'legal' => ['width' => 216, 'height' => 356], + 'a3' => ['width' => 297, 'height' => 420], + 'a5' => ['width' => 148, 'height' => 210], + ]; + + $size = $sizes[$pageSize] ?? $sizes['a4']; + $width = $orientation === 'landscape' ? $size['height'] : $size['width']; + $height = $orientation === 'landscape' ? $size['width'] : $size['height']; + + return "{$width}mm {$height}mm"; + } + + /** + * Get CSS page margins string based on settings + * + * @param array $pageSettings + * @return string + */ + private function getPageMarginsCSS(array $pageSettings): string + { + $top = $pageSettings['marginTop'] ?? '10mm'; + $right = $pageSettings['marginRight'] ?? '10mm'; + $bottom = $pageSettings['marginBottom'] ?? '10mm'; + $left = $pageSettings['marginLeft'] ?? '10mm'; + + return "{$top} {$right} {$bottom} {$left}"; + } + + /** + * Get page settings from JSON design + * + * @return array + */ + public function getPageSettings(): array + { + return $this->jsonDesign['pageSettings'] ?? []; + } + + /** + * Get blocks from JSON design + * + * @return array + */ + public function getBlocks(): array + { + return $this->jsonDesign['blocks'] ?? []; + } + + /** + * Validate JSON design structure + * + * @return bool + */ + public function isValid(): bool + { + if (!isset($this->jsonDesign['blocks']) || !is_array($this->jsonDesign['blocks'])) { + return false; + } + + // Basic validation of block structure + foreach ($this->jsonDesign['blocks'] as $block) { + if (!isset($block['id']) || !isset($block['type']) || !isset($block['gridPosition'])) { + return false; + } + } + + return true; + } +} diff --git a/app/Services/Pdf/JsonToSectionsAdapter.php b/app/Services/Pdf/JsonToSectionsAdapter.php new file mode 100644 index 0000000000..03dff065a5 --- /dev/null +++ b/app/Services/Pdf/JsonToSectionsAdapter.php @@ -0,0 +1,930 @@ +jsonBlocks = $jsonDesign['blocks'] ?? []; + $this->pageSettings = $jsonDesign['pageSettings'] ?? []; + $this->service = $service; + } + + /** + * Convert JSON blocks to PdfBuilder sections format + * + * @return array Sections array compatible with PdfBuilder::setSections() + */ + public function toSections(): array + { + $sections = []; + + // Sort blocks by grid position (Y-axis primary, X-axis secondary) + $sortedBlocks = $this->sortBlocksByPosition($this->jsonBlocks); + + // Convert each block to a section (no row grouping here - that's done in template) + foreach ($sortedBlocks as $block) { + $section = $this->convertBlockToSection($block); + if ($section !== null) { + $sections[$block['id']] = $section; + } + } + + return $sections; + } + + /** + * Get blocks grouped by row for template generation + * + * @return array + */ + public function getRowGroupedBlocks(): array + { + $sortedBlocks = $this->sortBlocksByPosition($this->jsonBlocks); + return $this->groupBlocksIntoRows($sortedBlocks); + } + + /** + * Sort blocks by grid position (Y, then X) + * + * @param array $blocks + * @return array + */ + private function sortBlocksByPosition(array $blocks): array + { + usort($blocks, function ($a, $b) { + $aY = $a['gridPosition']['y'] ?? 0; + $bY = $b['gridPosition']['y'] ?? 0; + + if ($aY !== $bY) { + return $aY - $bY; + } + + $aX = $a['gridPosition']['x'] ?? 0; + $bX = $b['gridPosition']['x'] ?? 0; + + return $aX - $bX; + }); + + return $blocks; + } + + /** + * Group blocks into rows based on similar Y positions + * Matches InvoiceDesignRenderer logic - blocks within 1 grid unit are considered same row + * + * @param array $blocks + * @return array Array of rows, each containing array of blocks + */ + private function groupBlocksIntoRows(array $blocks): array + { + $rows = []; + $currentRow = []; + $currentY = -1; + + foreach ($blocks as $block) { + $blockY = $block['gridPosition']['y'] ?? 0; + + // Start new row if Y position differs by >= 1 grid unit + if ($currentY === -1 || abs($blockY - $currentY) >= 1) { + if (!empty($currentRow)) { + $rows[] = $currentRow; + } + $currentRow = [$block]; + $currentY = $blockY; + } else { + // Same row - add to current + $currentRow[] = $block; + } + } + + if (!empty($currentRow)) { + $rows[] = $currentRow; + } + + return $rows; + } + + /** + * Convert a single JSON block to PdfBuilder section format + * + * @param array $block + * @return array|null + */ + private function convertBlockToSection(array $block): ?array + { + return match ($block['type']) { + 'logo', 'image' => $this->convertImageBlock($block), + 'company-info' => $this->convertCompanyInfoBlock($block), + 'client-info' => $this->convertClientInfoBlock($block), + 'invoice-details' => $this->convertInvoiceDetailsBlock($block), + 'table' => $this->convertTableBlock($block), + 'total' => $this->convertTotalBlock($block), + 'text' => $this->convertTextBlock($block), + 'divider' => $this->convertDividerBlock($block), + 'spacer' => $this->convertSpacerBlock($block), + 'qrcode' => $this->convertQRCodeBlock($block), + 'signature' => $this->convertSignatureBlock($block), + default => null + }; + } + + /** + * Convert logo/image block + */ + private function convertImageBlock(array $block): array + { + $props = $block['properties']; + $blockId = $block['id']; + + return [ + 'id' => $blockId, + 'elements' => [ + [ + 'element' => 'div', + 'properties' => [ + 'data-ref' => "{$blockId}-container", + 'style' => $this->buildImageContainerStyle($props), + ], + 'elements' => [ + [ + 'element' => 'img', + 'properties' => [ + 'src' => $props['source'] ?? '', + 'alt' => $block['type'] === 'logo' ? 'Company Logo' : 'Image', + 'data-ref' => $blockId, + 'style' => $this->buildImageStyle($props), + ], + ], + ], + ], + ], + ]; + } + + /** + * Convert company-info block + */ + private function convertCompanyInfoBlock(array $block): array + { + $props = $block['properties']; + $elements = []; + $fieldConfigs = $props['fieldConfigs'] ?? null; + + if ($fieldConfigs && is_array($fieldConfigs)) { + // New structured format with fieldConfigs + foreach ($fieldConfigs as $index => $config) { + $prefix = $config['prefix'] ?? ''; + $variable = $config['variable'] ?? ''; + $suffix = $config['suffix'] ?? ''; + + $content = ''; + if (!empty($prefix)) { + $content .= $prefix; + } + $content .= $variable; + if (!empty($suffix)) { + $content .= $suffix; + } + + $elements[] = [ + 'element' => 'div', + 'content' => $content, + 'show_empty' => false, + 'properties' => [ + 'data-ref' => "{$block['id']}-field-{$index}", + 'style' => $this->buildTextStyle($props), + ], + ]; + } + } else { + // Legacy content string + $lines = explode("\n", $props['content'] ?? ''); + foreach ($lines as $index => $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + + $elements[] = [ + 'element' => 'div', + 'content' => $line, + 'show_empty' => false, + 'properties' => [ + 'data-ref' => "{$block['id']}-line-{$index}", + 'style' => $this->buildTextStyle($props), + ], + ]; + } + } + + return [ + 'id' => $block['id'], + 'elements' => $elements, + ]; + } + + /** + * Convert client-info block + */ + private function convertClientInfoBlock(array $block): array + { + $props = $block['properties']; + $elements = []; + + // Optional title + if ($props['showTitle'] ?? false) { + $elements[] = [ + 'element' => 'div', + 'content' => $props['title'] ?? '', + 'properties' => [ + 'data-ref' => "{$block['id']}-title", + 'style' => $this->buildTitleStyle($props), + ], + ]; + } + + // Field configs + $fieldConfigs = $props['fieldConfigs'] ?? null; + + if ($fieldConfigs && is_array($fieldConfigs)) { + foreach ($fieldConfigs as $index => $config) { + $prefix = $config['prefix'] ?? ''; + $variable = $config['variable'] ?? ''; + $suffix = $config['suffix'] ?? ''; + + $content = ''; + if (!empty($prefix)) { + $content .= $prefix; + } + $content .= $variable; + if (!empty($suffix)) { + $content .= $suffix; + } + + $elements[] = [ + 'element' => 'div', + 'content' => $content, + 'show_empty' => false, + 'properties' => [ + 'data-ref' => "{$block['id']}-field-{$index}", + 'style' => $this->buildTextStyle($props), + ], + ]; + } + } else { + // Legacy content string + $lines = explode("\n", $props['content'] ?? ''); + foreach ($lines as $index => $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + + $elements[] = [ + 'element' => 'div', + 'content' => $line, + 'show_empty' => false, + 'properties' => [ + 'data-ref' => "{$block['id']}-line-{$index}", + 'style' => $this->buildTextStyle($props), + ], + ]; + } + } + + return [ + 'id' => $block['id'], + 'elements' => $elements, + ]; + } + + /** + * Convert invoice-details block + */ + private function convertInvoiceDetailsBlock(array $block): array + { + $props = $block['properties']; + $items = $props['items'] ?? null; + $elements = []; + + if ($items && is_array($items)) { + foreach ($items as $index => $item) { + if (!($item['show'] ?? true)) { + continue; + } + + $elements[] = [ + 'element' => 'tr', + 'properties' => [ + 'data-ref' => "{$block['id']}-row-{$index}", + ], + 'elements' => [ + [ + 'element' => 'th', + 'content' => $item['label'] ?? '', + 'properties' => [ + 'data-ref' => "{$block['id']}-label-{$index}", + 'style' => $this->buildLabelStyle($props), + ], + ], + [ + 'element' => 'th', + 'content' => $item['variable'] ?? '', + 'properties' => [ + 'data-ref' => "{$block['id']}-value-{$index}", + 'style' => $this->buildValueStyle($props), + ], + ], + ], + ]; + } + } + + return [ + 'id' => $block['id'], + 'elements' => [[ + 'element' => 'table', + 'properties' => [ + 'style' => $this->buildTableStyle($props), + ], + 'elements' => $elements, + ]], + ]; + } + + /** + * Convert table block - maps to PdfBuilder's product/task table format + */ + private function convertTableBlock(array $block): array + { + $props = $block['properties']; + $columns = $props['columns'] ?? []; + + // Determine table type from column fields + $tableType = $this->detectTableType($columns); + + // Build header elements + $headerElements = []; + foreach ($columns as $column) { + $headerElements[] = [ + 'element' => 'th', + 'content' => $column['header'] ?? '', + 'properties' => [ + 'data-ref' => "{$tableType}_table-{$column['id']}-th", + 'style' => $this->buildTableHeaderStyle($props, $column), + ], + ]; + } + + return [ + 'id' => $block['id'], + 'elements' => [[ + 'element' => 'table', + 'properties' => [ + 'style' => $this->buildTableContainerStyle($props), + 'data-table-type' => $tableType, + ], + 'elements' => [ + [ + 'element' => 'thead', + 'properties' => [ + 'style' => $this->buildTheadStyle($props), + ], + 'elements' => [ + [ + 'element' => 'tr', + 'elements' => $headerElements, + ], + ], + ], + [ + 'element' => 'tbody', + 'elements' => [], // Will be populated by PdfBuilder::buildTableBody() + ], + ], + ]], + ]; + } + + /** + * Detect table type from column fields (product or task) + */ + private function detectTableType(array $columns): string + { + foreach ($columns as $column) { + $field = $column['field'] ?? ''; + if (str_starts_with($field, 'item.')) { + // Generic line items + return 'product'; + } + } + + return 'product'; + } + + /** + * Convert total block + */ + private function convertTotalBlock(array $block): array + { + $props = $block['properties']; + $items = $props['items'] ?? []; + $rowElements = []; + + foreach ($items as $index => $item) { + if (!($item['show'] ?? true)) { + continue; + } + + $isTotal = $item['isTotal'] ?? false; + $isBalance = $item['isBalance'] ?? false; + + // Create table row with label and value cells + $rowElements[] = [ + 'element' => 'tr', + 'properties' => [ + 'data-ref' => "{$block['id']}-row-{$index}", + 'class' => $this->buildTotalRowClass($isTotal, $isBalance), + 'style' => $this->buildTotalRowStyle($props, $isTotal), + ], + 'elements' => [ + [ + 'element' => 'td', + 'content' => $item['label'] . ':', + 'properties' => [ + 'data-ref' => "{$block['id']}-label-{$index}", + 'class' => 'totals-label', + 'style' => $this->buildTotalLabelStyle($props), + ], + ], + [ + 'element' => 'td', + 'content' => $item['field'], + 'properties' => [ + 'data-ref' => "{$block['id']}-value-{$index}", + 'class' => 'totals-value', + 'style' => $this->buildTotalValueStyle($props, $isTotal, $isBalance), + ], + ], + ], + ]; + } + + return [ + 'id' => $block['id'], + 'elements' => [[ + 'element' => 'table', + 'properties' => [ + 'class' => 'totals-table', + 'style' => $this->buildTotalContainerStyle($props), + ], + 'elements' => [ + [ + 'element' => 'tbody', + 'elements' => $rowElements, + ], + ], + ]], + ]; + } + + /** + * Convert text block + */ + private function convertTextBlock(array $block): array + { + $props = $block['properties']; + $content = $props['content'] ?? ''; + $lines = explode("\n", $content); + $elements = []; + + foreach ($lines as $index => $line) { + $elements[] = [ + 'element' => 'div', + 'content' => trim($line), + 'properties' => [ + 'data-ref' => "{$block['id']}-line-{$index}", + 'style' => $this->buildTextStyle($props), + ], + ]; + } + + return [ + 'id' => $block['id'], + 'elements' => $elements, + ]; + } + + /** + * Convert divider block + */ + private function convertDividerBlock(array $block): array + { + $props = $block['properties']; + + return [ + 'id' => $block['id'], + 'elements' => [ + [ + 'element' => 'hr', + 'properties' => [ + 'data-ref' => "{$block['id']}-hr", + 'style' => $this->buildDividerStyle($props), + ], + ], + ], + ]; + } + + /** + * Convert spacer block + */ + private function convertSpacerBlock(array $block): array + { + $props = $block['properties']; + + return [ + 'id' => $block['id'], + 'elements' => [ + [ + 'element' => 'div', + 'content' => '', + 'properties' => [ + 'data-ref' => "{$block['id']}-spacer", + 'style' => "height: {$props['height']};", + ], + ], + ], + ]; + } + + /** + * Convert QR code block + */ + private function convertQRCodeBlock(array $block): array + { + $props = $block['properties']; + + return [ + 'id' => $block['id'], + 'elements' => [ + [ + 'element' => 'div', + 'content' => '{{QR_CODE:' . ($props['data'] ?? '$invoice.public_url') . '}}', + 'properties' => [ + 'data-ref' => "{$block['id']}-qr", + 'style' => "text-align: " . ($props['align'] ?? 'left') . ";", + ], + ], + ], + ]; + } + + /** + * Convert signature block + */ + private function convertSignatureBlock(array $block): array + { + $props = $block['properties']; + $elements = []; + + // Signature space + $elements[] = [ + 'element' => 'div', + 'content' => '', + 'properties' => [ + 'data-ref' => "{$block['id']}-space", + 'style' => 'margin-bottom: 40px;', + ], + ]; + + // Signature line + if ($props['showLine'] ?? true) { + $elements[] = [ + 'element' => 'div', + 'content' => '', + 'properties' => [ + 'data-ref' => "{$block['id']}-line", + 'style' => $this->buildSignatureLineStyle($props), + ], + ]; + } + + // Label + $elements[] = [ + 'element' => 'div', + 'content' => $props['label'] ?? '', + 'properties' => [ + 'data-ref' => "{$block['id']}-label", + 'style' => $this->buildSignatureLabelStyle($props), + ], + ]; + + // Date field + if ($props['showDate'] ?? false) { + $elements[] = [ + 'element' => 'div', + 'content' => 'Date: ________________', + 'properties' => [ + 'data-ref' => "{$block['id']}-date", + 'style' => $this->buildSignatureLabelStyle($props), + ], + ]; + } + + return [ + 'id' => $block['id'], + 'elements' => $elements, + 'properties' => [ + 'style' => "text-align: " . ($props['align'] ?? 'left') . ";", + ], + ]; + } + + // Style building methods + + private function buildImageContainerStyle(array $props): string + { + $styles = []; + $styles[] = 'text-align: ' . ($props['align'] ?? 'left'); + $styles[] = 'height: 100%'; + $styles[] = 'display: flex'; + $styles[] = 'align-items: center'; + $styles[] = 'justify-content: ' . ($props['align'] ?? 'left'); + + return implode('; ', $styles) . ';'; + } + + private function buildImageStyle(array $props): string + { + $styles = []; + $styles[] = 'max-width: ' . ($props['maxWidth'] ?? '100%'); + $styles[] = 'max-height: ' . ($props['maxHeight'] ?? '100%'); + $styles[] = 'object-fit: ' . ($props['objectFit'] ?? 'contain'); + + return implode('; ', $styles) . ';'; + } + + private function buildTextStyle(array $props): string + { + $styles = []; + if (isset($props['fontSize'])) { + $styles[] = 'font-size: ' . $props['fontSize']; + } + if (isset($props['fontWeight'])) { + $styles[] = 'font-weight: ' . $props['fontWeight']; + } + if (isset($props['fontStyle'])) { + $styles[] = 'font-style: ' . $props['fontStyle']; + } + if (isset($props['color'])) { + $styles[] = 'color: ' . $props['color']; + } + if (isset($props['align'])) { + $styles[] = 'text-align: ' . $props['align']; + } + if (isset($props['lineHeight'])) { + $styles[] = 'line-height: ' . $props['lineHeight']; + } + + return implode('; ', $styles) . ';'; + } + + private function buildTitleStyle(array $props): string + { + $styles = []; + $styles[] = 'font-size: ' . ($props['fontSize'] ?? '12px'); + $styles[] = 'font-weight: ' . ($props['titleFontWeight'] ?? 'bold'); + $styles[] = 'color: ' . ($props['color'] ?? '#374151'); + $styles[] = 'margin-bottom: 8px'; + + return implode('; ', $styles) . ';'; + } + + private function buildLabelStyle(array $props): string + { + $styles = []; + $styles[] = 'font-size: ' . ($props['fontSize'] ?? '12px'); + $styles[] = 'color: ' . ($props['labelColor'] ?? '#6B7280'); + $styles[] = 'text-align: ' . ($props['align'] ?? 'left'); + $styles[] = 'padding-right: 12px'; + $styles[] = 'white-space: nowrap'; + + return implode('; ', $styles) . ';'; + } + + private function buildValueStyle(array $props): string + { + $styles = []; + $styles[] = 'font-size: ' . ($props['fontSize'] ?? '12px'); + $styles[] = 'color: ' . ($props['color'] ?? '#374151'); + $styles[] = 'text-align: ' . ($props['align'] ?? 'left'); + + return implode('; ', $styles) . ';'; + } + + private function buildTableStyle(array $props): string + { + $styles = []; + $styles[] = 'border-collapse: collapse'; + if (isset($props['align'])) { + $align = $props['align']; + if ($align === 'right') { + $styles[] = 'margin-left: auto'; + } elseif ($align === 'center') { + $styles[] = 'margin: 0 auto'; + } + } + + return implode('; ', $styles) . ';'; + } + + private function buildTableHeaderStyle(array $props, array $column): string + { + $styles = []; + $styles[] = 'padding: ' . ($props['padding'] ?? '8px'); + $styles[] = 'text-align: ' . ($column['align'] ?? 'left'); + if (isset($column['width'])) { + $styles[] = 'width: ' . $column['width']; + } + if ($props['showBorders'] ?? true) { + $styles[] = 'border: 1px solid ' . ($props['borderColor'] ?? '#E5E7EB'); + } + + return implode('; ', $styles) . ';'; + } + + private function buildTheadStyle(array $props): string + { + $styles = []; + $styles[] = 'background: ' . ($props['headerBg'] ?? '#F9FAFB'); + $styles[] = 'color: ' . ($props['headerColor'] ?? '#111827'); + $styles[] = 'font-weight: ' . ($props['headerFontWeight'] ?? 'bold'); + + return implode('; ', $styles) . ';'; + } + + private function buildTableContainerStyle(array $props): string + { + $styles = []; + $styles[] = 'width: 100%'; + $styles[] = 'border-collapse: collapse'; + $styles[] = 'font-size: ' . ($props['fontSize'] ?? '12px'); + + return implode('; ', $styles) . ';'; + } + + private function buildTotalRowClass(bool $isTotal, bool $isBalance): string + { + $classes = ['totals-row']; + if ($isTotal) { + $classes[] = 'totals-row-total'; + } + if ($isBalance) { + $classes[] = 'totals-row-balance'; + } + + return implode(' ', $classes); + } + + private function buildTotalRowStyle(array $props, bool $isTotal): string + { + $styles = []; + $styles[] = 'font-size: ' . ($isTotal ? ($props['totalFontSize'] ?? '14px') : ($props['fontSize'] ?? '12px')); + $styles[] = 'font-weight: ' . ($isTotal ? ($props['totalFontWeight'] ?? 'bold') : 'normal'); + + return implode('; ', $styles) . ';'; + } + + private function buildTotalLabelStyle(array $props): string + { + $styles = []; + $styles[] = 'color: ' . ($props['labelColor'] ?? '#6B7280'); + $styles[] = 'text-align: right'; + $styles[] = 'white-space: nowrap'; + $styles[] = 'padding-right: ' . ($props['labelValueGap'] ?? '20px'); + $styles[] = 'padding-bottom: ' . ($props['spacing'] ?? '4px'); + + return implode('; ', $styles) . ';'; + } + + private function buildTotalValueStyle(array $props, bool $isTotal, bool $isBalance): string + { + $color = $props['amountColor'] ?? '#374151'; + if ($isTotal) { + $color = $props['totalColor'] ?? $color; + } + if ($isBalance) { + $color = $props['balanceColor'] ?? $color; + } + + $styles = []; + $styles[] = 'color: ' . $color; + $styles[] = 'text-align: right'; + $styles[] = 'white-space: nowrap'; + $styles[] = 'padding-bottom: ' . ($props['spacing'] ?? '4px'); + + return implode('; ', $styles) . ';'; + } + + private function buildTotalContainerStyle(array $props): string + { + $styles = []; + if (isset($props['align'])) { + $align = $props['align']; + if ($align === 'right') { + $styles[] = 'margin-left: auto'; + } elseif ($align === 'center') { + $styles[] = 'margin: 0 auto'; + } + } + + return implode('; ', $styles) . ';'; + } + + private function buildDividerStyle(array $props): string + { + $styles = []; + $styles[] = 'border: none'; + $styles[] = 'border-top: ' . ($props['thickness'] ?? '1px') . ' ' . ($props['style'] ?? 'solid') . ' ' . ($props['color'] ?? '#E5E7EB'); + $styles[] = 'margin-top: ' . ($props['marginTop'] ?? '10px'); + $styles[] = 'margin-bottom: ' . ($props['marginBottom'] ?? '10px'); + + return implode('; ', $styles) . ';'; + } + + private function buildSignatureLineStyle(array $props): string + { + $styles = []; + $styles[] = 'border-top: 1px solid #000'; + $styles[] = 'width: 200px'; + $styles[] = 'margin-bottom: 8px'; + $align = $props['align'] ?? 'left'; + if ($align === 'center') { + $styles[] = 'display: inline-block'; + } + + return implode('; ', $styles) . ';'; + } + + private function buildSignatureLabelStyle(array $props): string + { + $styles = []; + $styles[] = 'font-size: ' . ($props['fontSize'] ?? '12px'); + $styles[] = 'color: ' . ($props['color'] ?? '#374151'); + + return implode('; ', $styles) . ';'; + } +} diff --git a/tests/Feature/Design/JsonDesignServiceTest.php b/tests/Feature/Design/JsonDesignServiceTest.php new file mode 100644 index 0000000000..357012d055 --- /dev/null +++ b/tests/Feature/Design/JsonDesignServiceTest.php @@ -0,0 +1,245 @@ +testDesign = json_decode(file_get_contents($jsonPath), true); + + $this->makeTestData(); + } + + public function testJsonDesignValidation() + { + $mockService = $this->createMock(PdfService::class); + + $service = new JsonDesignService($mockService, $this->testDesign); + + $this->assertTrue($service->isValid()); + $this->assertIsArray($service->getBlocks()); + $this->assertNotEmpty($service->getBlocks()); + $this->assertIsArray($service->getPageSettings()); + } + + public function testInvalidJsonDesign() + { + $mockService = $this->createMock(PdfService::class); + + $invalidDesign = ['invalid' => 'structure']; + $service = new JsonDesignService($mockService, $invalidDesign); + + $this->assertFalse($service->isValid()); + } + + public function testJsonToSectionsConversion() + { + $mockService = $this->createMock(PdfService::class); + + $adapter = new JsonToSectionsAdapter($this->testDesign, $mockService); + $sections = $adapter->toSections(); + + $this->assertIsArray($sections); + $this->assertNotEmpty($sections); + + // Verify section structure + foreach ($sections as $sectionId => $section) { + $this->assertArrayHasKey('id', $section); + $this->assertArrayHasKey('elements', $section); + $this->assertIsArray($section['elements']); + } + } + + public function testBlockTypeConversions() + { + $mockService = $this->createMock(PdfService::class); + + $adapter = new JsonToSectionsAdapter($this->testDesign, $mockService); + $sections = $adapter->toSections(); + + // Verify that sections exist (some may be grouped into rows) + $this->assertNotEmpty($sections, 'Sections should not be empty'); + + // Count block types that should be present + $hasLogoBlock = false; + $hasTableBlock = false; + $hasTotalBlock = false; + + foreach ($this->testDesign['blocks'] as $block) { + if ($block['type'] === 'logo') { + $hasLogoBlock = true; + } + if ($block['type'] === 'table') { + $hasTableBlock = true; + } + if ($block['type'] === 'total') { + $hasTotalBlock = true; + } + } + + $this->assertTrue($hasLogoBlock, 'Logo block should exist in test design'); + $this->assertTrue($hasTableBlock, 'Table block should exist in test design'); + $this->assertTrue($hasTotalBlock, 'Total block should exist in test design'); + + // Verify some blocks are in sections (either as standalone or grouped in rows) + $hasLogoSection = isset($sections['logo-1765268278392']); + $hasTableSection = isset($sections['table-1765268328782']); + $hasRowSections = false; + + foreach (array_keys($sections) as $sectionId) { + if (str_starts_with($sectionId, 'row-')) { + $hasRowSections = true; + break; + } + } + + $this->assertTrue($hasLogoSection || $hasRowSections, 'Should have either block sections or row sections'); + } + + public function testDataRefAttributesPresent() + { + $mockService = $this->createMock(PdfService::class); + + $adapter = new JsonToSectionsAdapter($this->testDesign, $mockService); + $sections = $adapter->toSections(); + + // Check that data-ref attributes are present for CSS targeting + $hasDataRef = false; + + foreach ($sections as $section) { + if (isset($section['elements'])) { + foreach ($section['elements'] as $element) { + if (isset($element['properties']['data-ref'])) { + $hasDataRef = true; + break 2; + } + } + } + } + + $this->assertTrue($hasDataRef, 'Sections should contain data-ref attributes for CSS targeting'); + } + + public function testPageSettingsExtraction() + { + $mockService = $this->createMock(PdfService::class); + + $service = new JsonDesignService($mockService, $this->testDesign); + $pageSettings = $service->getPageSettings(); + + $this->assertIsArray($pageSettings); + + // Verify expected page settings keys + if (!empty($pageSettings)) { + $this->assertArrayHasKey('pageSize', $pageSettings); + $this->assertArrayHasKey('orientation', $pageSettings); + } + } + + public function testBlockSorting() + { + $mockService = $this->createMock(PdfService::class); + + // Create test design with unsorted blocks + $unsortedDesign = [ + 'blocks' => [ + [ + 'id' => 'block-3', + 'type' => 'text', + 'gridPosition' => ['x' => 0, 'y' => 10, 'w' => 12, 'h' => 1], + 'properties' => ['content' => 'Third block'], + ], + [ + 'id' => 'block-1', + 'type' => 'text', + 'gridPosition' => ['x' => 0, 'y' => 0, 'w' => 12, 'h' => 1], + 'properties' => ['content' => 'First block'], + ], + [ + 'id' => 'block-2', + 'type' => 'text', + 'gridPosition' => ['x' => 0, 'y' => 5, 'w' => 12, 'h' => 1], + 'properties' => ['content' => 'Second block'], + ], + ], + ]; + + $adapter = new JsonToSectionsAdapter($unsortedDesign, $mockService); + $sections = $adapter->toSections(); + + $sectionIds = array_keys($sections); + + // Verify blocks are processed in Y-position order + $this->assertEquals('block-1', $sectionIds[0]); + $this->assertEquals('block-2', $sectionIds[1]); + $this->assertEquals('block-3', $sectionIds[2]); + } + + public function testStyleGeneration() + { + $mockService = $this->createMock(PdfService::class); + + $adapter = new JsonToSectionsAdapter($this->testDesign, $mockService); + $sections = $adapter->toSections(); + + // Check that styles are generated + $hasStyles = false; + + foreach ($sections as $section) { + if (isset($section['properties']['style'])) { + $hasStyles = true; + $this->assertIsString($section['properties']['style']); + break; + } + + if (isset($section['elements'])) { + foreach ($section['elements'] as $element) { + if (isset($element['properties']['style'])) { + $hasStyles = true; + $this->assertIsString($element['properties']['style']); + break 2; + } + } + } + } + + $this->assertTrue($hasStyles, 'Sections should contain inline styles'); + } + + public function test_json_design_service() + { + $this->assertNotNull($this->invoice->invitations()->first()); + + $designjson = file_get_contents(base_path('tests/Feature/Design/stubs/test_design_1.json')); + $design = json_decode($designjson, true); + + $pdfService = new PdfService($this->invoice->invitations()->first(), 'product'); + $service = new JsonDesignService($pdfService, $design); + + $html = $service->build(); + + $this->assertNotNull($html); + file_put_contents(base_path('tests/artifacts/json_service_output.html'), $html); + } + +} diff --git a/tests/Feature/Design/stubs/test_design_1_mock.html b/tests/Feature/Design/stubs/test_design_1_mock.html index e0db6bcc2d..9e2f233512 100644 --- a/tests/Feature/Design/stubs/test_design_1_mock.html +++ b/tests/Feature/Design/stubs/test_design_1_mock.html @@ -117,18 +117,18 @@
-
Untitled Company
Address 1
City, State Postal Code
555-343-2323
id number
http://staging.invoicing.co
deja15@deckow.com
-
Homenick-Gleason
309
VAT: 334205845
West Alexannebury, Montana 03782-6966
Afghanistan
mitchel36@example.net
+
Untitled Company
Address 1
City, State Postal Code
555-343-2323
id number
http://staging.invoicing.co
tkunze@hotmail.com
+
Leannon-Runte
603
VAT: 592860357
Lake Terrell, California 93516
Afghanistan
rcruickshank@example.net

image
-
Invoice #:1362670894990
PO Number:Eos.
Date:19/Apr/1995
Due Date:30/Aug/2023
Amount:$722.82
Bo Bo Balance MoFo:$722.82
+
Invoice #:6963662206941
PO Number:Sint quos.
Date:29/Jun/2009
Due Date:31/Jul/1976
Amount:$722.82
Bo Bo Balance MoFo:$722.82
ItemDescriptionQtyRateAmount
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
test_productLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.1.75$49.58$86.77
-
private notes generally go here.
 
1362670894990.terms
 
line spaces dont work that great?
-
Subtotal:$607.39
Discount:$0.00
Tax:1362670894990.tax
Total:$722.82
Amount Paid:1362670894990.paid_to_date
Balance Due:$722.82
+
private notes generally go here.
 
6963662206941.terms
 
line spaces dont work that great?
+
Subtotal:$607.39
Discount:$0.00
Tax:6963662206941.tax
Total:$722.82
Amount Paid:6963662206941.paid_to_date
Balance Due:$722.82
Invoice.footer
From fc9b563711f0d189f2f1eec0416cef5853ab8b0b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 12 Dec 2025 08:43:25 +1100 Subject: [PATCH 026/177] casts for true/false in query parameters for charts query --- app/Http/Requests/Chart/ShowChartRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/Chart/ShowChartRequest.php b/app/Http/Requests/Chart/ShowChartRequest.php index 7b62f4ae88..62b84ac3c6 100644 --- a/app/Http/Requests/Chart/ShowChartRequest.php +++ b/app/Http/Requests/Chart/ShowChartRequest.php @@ -38,7 +38,7 @@ class ShowChartRequest extends Request 'date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom', 'start_date' => 'bail|sometimes|date', 'end_date' => 'bail|sometimes|date', - 'include_drafts' => 'bail|sometimes|boolean', + 'include_drafts' => 'bail|sometimes|in:true,false', ]; } From 04a786391e0d53fb455548f6c4ff75c36a861be0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 12 Dec 2025 08:51:56 +1100 Subject: [PATCH 027/177] Additional catch for mollie is dupe Payment detected --- app/PaymentDrivers/Mollie/CreditCard.php | 1 + app/PaymentDrivers/MolliePaymentDriver.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index 58b844bf78..1ac1b31409 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -183,6 +183,7 @@ class CreditCard implements LivewireMethodInterface if (property_exists($payment_hash->data, 'shouldStoreToken') && $payment_hash->data->shouldStoreToken) { try { + /** @var \Mollie\Api\Resources\Mandate[] $mandates */ $mandates = \iterator_to_array($this->mollie->gateway->mandates->listForId($payment_hash->data->mollieCustomerId)); } catch (\Mollie\Api\Exceptions\ApiException $e) { return $this->processUnsuccessfulPayment($e); diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php index 035bc3c0aa..799954b513 100644 --- a/app/PaymentDrivers/MolliePaymentDriver.php +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -420,6 +420,8 @@ class MolliePaymentDriver extends BaseDriver return (new CreditCard($this))->processSuccessfulPayment($payment); } catch (\Mollie\Api\Exceptions\ApiException $e) { return (new CreditCard($this))->processUnsuccessfulPayment($e); + } catch(\Throwable $e){ + nlog("Mollie:: Failure - ? Duplicate Payment? - {$e->getMessage()}"); } } From 0860247c8d25a60c9582e049038f7f1b46f0b16a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 13 Dec 2025 09:35:12 +1100 Subject: [PATCH 028/177] Cleanup for Mollie --- app/PaymentDrivers/Mollie/CreditCard.php | 13 +++--- app/PaymentDrivers/MolliePaymentDriver.php | 47 +++++++++++++--------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index 1ac1b31409..417257512f 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -51,6 +51,7 @@ class CreditCard implements LivewireMethodInterface */ public function paymentResponse(PaymentResponseRequest $request) { + $amount = $this->mollie->convertToMollieAmount((float) $this->mollie->payment_hash->data->amount_with_fee); $description = sprintf('%s: %s', ctrans('texts.invoices'), \implode(', ', collect($this->mollie->payment_hash->invoices())->pluck('invoice_number')->toArray())); @@ -101,7 +102,7 @@ class CreditCard implements LivewireMethodInterface return redirect()->away($payment->getCheckoutUrl()); } } - } catch (\Exception $e) { + } catch (\Throwable $e) { return $this->processUnsuccessfulPayment($e); } } @@ -161,9 +162,6 @@ class CreditCard implements LivewireMethodInterface if ($payment->status === 'open') { $this->mollie->payment_hash->withData('payment_id', $payment->id); - nlog("Mollie"); - nlog($payment); - if (!$payment->getCheckoutUrl()) { return render('gateways.mollie.mollie_placeholder'); } else { @@ -185,10 +183,15 @@ class CreditCard implements LivewireMethodInterface try { /** @var \Mollie\Api\Resources\Mandate[] $mandates */ $mandates = \iterator_to_array($this->mollie->gateway->mandates->listForId($payment_hash->data->mollieCustomerId)); + } catch (\Mollie\Api\Exceptions\ApiException $e) { return $this->processUnsuccessfulPayment($e); } + if(empty($mandates)){ + return render('gateways.mollie.mollie_placeholder'); + } + $payment_meta = new \stdClass(); $payment_meta->exp_month = (string) $mandates[0]->details->cardExpiryDate; $payment_meta->exp_year = (string) ''; @@ -224,7 +227,7 @@ class CreditCard implements LivewireMethodInterface return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]); } - public function processUnsuccessfulPayment(\Exception $e) + public function processUnsuccessfulPayment(\Throwable $e) { $this->mollie->sendFailureMail($e->getMessage()); diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php index 799954b513..3f7b4e4903 100644 --- a/app/PaymentDrivers/MolliePaymentDriver.php +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -12,27 +12,28 @@ namespace App\PaymentDrivers; -use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; -use App\Http\Requests\Gateways\Mollie\Mollie3dsRequest; -use App\Http\Requests\Payments\PaymentWebhookRequest; -use App\Jobs\Util\SystemLogger; use App\Models\Client; -use App\Models\ClientGatewayToken; -use App\Models\GatewayType; use App\Models\Invoice; use App\Models\Payment; +use App\Models\SystemLog; +use App\Models\GatewayType; use App\Models\PaymentHash; use App\Models\PaymentType; -use App\Models\SystemLog; -use App\PaymentDrivers\Mollie\Bancontact; -use App\PaymentDrivers\Mollie\BankTransfer; -use App\PaymentDrivers\Mollie\CreditCard; -use App\PaymentDrivers\Mollie\IDEAL; -use App\PaymentDrivers\Mollie\KBC; +use App\Jobs\Util\SystemLogger; use App\Utils\Traits\MakesHash; -use Illuminate\Support\Facades\Validator; -use Mollie\Api\Exceptions\ApiException; use Mollie\Api\MollieApiClient; +use App\Models\ClientGatewayToken; +use App\PaymentDrivers\BaseDriver; +use App\PaymentDrivers\Mollie\KBC; +use App\PaymentDrivers\Mollie\IDEAL; +use Mollie\Api\Exceptions\ApiException; +use App\PaymentDrivers\Mollie\Bancontact; +use App\PaymentDrivers\Mollie\CreditCard; +use Illuminate\Support\Facades\Validator; +use App\PaymentDrivers\Mollie\BankTransfer; +use App\Http\Requests\Payments\PaymentWebhookRequest; +use App\Http\Requests\Gateways\Mollie\Mollie3dsRequest; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; class MolliePaymentDriver extends BaseDriver { @@ -287,7 +288,9 @@ class MolliePaymentDriver extends BaseDriver { // Allow app to catch up with webhook request. // sleep(4); - usleep(rand(1500000, 4000000)); + nlog("Mollie:: processWebhookRequest"); + // nlog($request->all()); + usleep(rand(1500000, 2000000)); $validator = Validator::make($request->all(), [ 'id' => ['required', 'starts_with:tr'], @@ -365,11 +368,16 @@ class MolliePaymentDriver extends BaseDriver $record->service()->deletePayment(false); + $this->sendFailureMail($payment->details->failureMessage ?? "There was a problem processing your payment."); + + } + else { + $response = SystemLog::EVENT_GATEWAY_SUCCESS; } $record->status_id = $codes[$payment->status]; $record->save(); - $response = SystemLog::EVENT_GATEWAY_SUCCESS; + } SystemLogger::dispatch( @@ -384,6 +392,9 @@ class MolliePaymentDriver extends BaseDriver return response()->json([], 200); } catch (ApiException $e) { return response()->json(['message' => $e->getMessage(), 'gatewayStatusCode' => $e->getCode()], 500); + } catch(\Throwable $e){ + nlog("Mollie:: Failure - In payment Response? - {$e->getMessage()}"); + return response()->json(['message' => $e->getMessage(), 'gatewayStatusCode' => $e->getCode()], 500); } } @@ -420,9 +431,7 @@ class MolliePaymentDriver extends BaseDriver return (new CreditCard($this))->processSuccessfulPayment($payment); } catch (\Mollie\Api\Exceptions\ApiException $e) { return (new CreditCard($this))->processUnsuccessfulPayment($e); - } catch(\Throwable $e){ - nlog("Mollie:: Failure - ? Duplicate Payment? - {$e->getMessage()}"); - } + } } public function detach(ClientGatewayToken $token) From 6213fda2de7aa4ebfb1d3310cba3615a49f3e191 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 14 Dec 2025 13:27:26 +1100 Subject: [PATCH 029/177] default amount value for expense --- app/Http/Requests/Expense/StoreExpenseRequest.php | 4 ++++ app/Mail/TemplateEmail.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Http/Requests/Expense/StoreExpenseRequest.php b/app/Http/Requests/Expense/StoreExpenseRequest.php index a85cac327a..3b2295a264 100644 --- a/app/Http/Requests/Expense/StoreExpenseRequest.php +++ b/app/Http/Requests/Expense/StoreExpenseRequest.php @@ -85,6 +85,10 @@ class StoreExpenseRequest extends Request $this->files->set('file', [$this->file('file')]); } + if(!array_key_exists('amount', $input)){ + $input['amount'] = 0; + } + if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { $input['currency_id'] = (string) $user->company()->settings->currency_id; } diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 2cbcf393d6..fd1398ddca 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -182,7 +182,7 @@ class TemplateEmail extends Mailable } } elseif ($this->invitation->credit) {//@phpstan-ignore-line - if (!$this->invitation->credit->client->getSetting('merge_e_invoice_to_pdf') && $this->invitation->invoice->client->getSetting('ubl_email_attachment') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { + if (!$this->invitation->credit->client->getSetting('merge_e_invoice_to_pdf') && $this->invitation->credit->client->getSetting('ubl_email_attachment') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { $xml_string = $this->invitation->credit->service()->getECredit($this->invitation->contact); if ($xml_string) { From 6f0417a93339c26f0f2c5081a5112f5b555c06cb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 Dec 2025 13:07:19 +1100 Subject: [PATCH 030/177] Set default scale for bcmath to 10 for greater precision --- app/Helpers/Invoice/InvoiceSum.php | 3 +++ app/Helpers/Invoice/Taxer.php | 7 ++++++- app/Utils/BcMath.php | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 7291eeec35..c1cd7c3cac 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -141,11 +141,14 @@ class InvoiceSum } if (is_string($this->invoice->tax_name1) && strlen($this->invoice->tax_name1) >= 2) { + $tax = $this->taxer($this->total, $this->invoice->tax_rate1); + $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1); $this->total_taxes += $tax; $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->client).'%', 'total' => $tax, 'tax_rate' => $this->invoice->tax_rate1]; + } if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) >= 2) { diff --git a/app/Helpers/Invoice/Taxer.php b/app/Helpers/Invoice/Taxer.php index 9a564ad350..c9e3629b3a 100644 --- a/app/Helpers/Invoice/Taxer.php +++ b/app/Helpers/Invoice/Taxer.php @@ -19,7 +19,12 @@ trait Taxer { public function taxer($amount, $tax_rate) { - return round($amount * (($tax_rate ? $tax_rate : 0) / 100), 2); + if(!$tax_rate || $tax_rate == 0) { + return 0; + } + + return round(\App\Utils\BcMath::mul($amount, $tax_rate/100), 2, PHP_ROUND_HALF_UP); + // return round(($amount * (($tax_rate ? $tax_rate : 0) / 100)), 2); } public function calcAmountLineTax($tax_rate, $amount) diff --git a/app/Utils/BcMath.php b/app/Utils/BcMath.php index 2b68052c5c..ddc38ca679 100644 --- a/app/Utils/BcMath.php +++ b/app/Utils/BcMath.php @@ -26,7 +26,7 @@ class BcMath /** * Default scale for currency calculations (2 decimal places) */ - private const DEFAULT_SCALE = 2; + private const DEFAULT_SCALE = 10; /** * Add two numbers using bcmath From 0521cc3df916be96b3b59313bbf021bc4ca069e6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 Dec 2025 16:16:09 +1100 Subject: [PATCH 031/177] Add back turbo124 repo --- composer.json | 4 + composer.lock | 1574 ++++++++++++++++++++++++++----------------------- 2 files changed, 848 insertions(+), 730 deletions(-) diff --git a/composer.json b/composer.json index cad686c04e..121bad8ecf 100644 --- a/composer.json +++ b/composer.json @@ -219,6 +219,10 @@ "type": "vcs", "url": "https://github.com/beganovich/php-ansible" }, + { + "type": "vcs", + "url": "https://github.com/turbo124/snappdf" + }, { "type": "path", "url": "../admin-api" diff --git a/composer.lock b/composer.lock index 7ceb8bbd26..ae0384c271 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9a8f043b7584acdd2884d1ede7909e33", + "content-hash": "bbc9c82c8904f23fad218ab0485d2de5", "packages": [ { "name": "afosto/yaac", @@ -381,16 +381,16 @@ }, { "name": "awobaz/compoships", - "version": "2.5.1", + "version": "2.5.2", "source": { "type": "git", "url": "https://github.com/topclaudy/compoships.git", - "reference": "d8de30b57949d6021bb0312105f6d9d0920266b6" + "reference": "e953cad2a0a9d0e035784c1d24c6b97f8609c542" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/topclaudy/compoships/zipball/d8de30b57949d6021bb0312105f6d9d0920266b6", - "reference": "d8de30b57949d6021bb0312105f6d9d0920266b6", + "url": "https://api.github.com/repos/topclaudy/compoships/zipball/e953cad2a0a9d0e035784c1d24c6b97f8609c542", + "reference": "e953cad2a0a9d0e035784c1d24c6b97f8609c542", "shasum": "" }, "require": { @@ -431,7 +431,7 @@ ], "support": { "issues": "https://github.com/topclaudy/compoships/issues", - "source": "https://github.com/topclaudy/compoships/tree/2.5.1" + "source": "https://github.com/topclaudy/compoships/tree/2.5.2" }, "funding": [ { @@ -439,7 +439,7 @@ "type": "custom" } ], - "time": "2025-10-10T14:07:12+00:00" + "time": "2025-11-04T21:05:57+00:00" }, { "name": "aws/aws-crt-php", @@ -497,16 +497,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.363.3", + "version": "3.367.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "0ec2218d32e291b988b1602583032ca5d11f8e8d" + "reference": "3dc26d6f26a4d6380d82b2be4d8ae80fefb23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0ec2218d32e291b988b1602583032ca5d11f8e8d", - "reference": "0ec2218d32e291b988b1602583032ca5d11f8e8d", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3dc26d6f26a4d6380d82b2be4d8ae80fefb23d71", + "reference": "3dc26d6f26a4d6380d82b2be4d8ae80fefb23d71", "shasum": "" }, "require": { @@ -519,7 +519,8 @@ "guzzlehttp/psr7": "^2.4.5", "mtdowling/jmespath.php": "^2.8.0", "php": ">=8.1", - "psr/http-message": "^1.0 || ^2.0" + "psr/http-message": "^1.0 || ^2.0", + "symfony/filesystem": "^v6.4.3 || ^v7.1.0 || ^v8.0.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -530,13 +531,11 @@ "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", - "ext-pcntl": "*", "ext-sockets": "*", - "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "phpunit/phpunit": "^9.6", "psr/cache": "^2.0 || ^3.0", "psr/simple-cache": "^2.0 || ^3.0", "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", - "symfony/filesystem": "^v6.4.0 || ^v7.1.0", "yoast/phpunit-polyfills": "^2.0" }, "suggest": { @@ -544,6 +543,7 @@ "doctrine/cache": "To use the DoctrineCacheAdapter", "ext-curl": "To send requests using cURL", "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-pcntl": "To use client-side monitoring", "ext-sockets": "To use client-side monitoring" }, "type": "library", @@ -588,9 +588,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.363.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.367.2" }, - "time": "2025-11-26T19:05:22+00:00" + "time": "2025-12-12T19:14:10+00:00" }, { "name": "babenkoivan/elastic-adapter", @@ -1012,13 +1012,13 @@ "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/beganovich/snappdf.git", - "reference": "340e877e63ef98db82766a8d8a853d7759cf79fa" + "url": "https://github.com/turbo124/snappdf.git", + "reference": "73997afb327fb9cd99686368769d2f0562cb3a9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/beganovich/snappdf/zipball/340e877e63ef98db82766a8d8a853d7759cf79fa", - "reference": "340e877e63ef98db82766a8d8a853d7759cf79fa", + "url": "https://api.github.com/repos/turbo124/snappdf/zipball/73997afb327fb9cd99686368769d2f0562cb3a9f", + "reference": "73997afb327fb9cd99686368769d2f0562cb3a9f", "shasum": "" }, "require": { @@ -1043,7 +1043,16 @@ "Beganovich\\Snappdf\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Test\\Snappdf\\": "tests/" + } + }, + "scripts": { + "tests": [ + "@php vendor/bin/phpunit --testdox" + ] + }, "license": [ "MIT" ], @@ -1055,23 +1064,22 @@ ], "description": "Convert webpages or HTML into the PDF file using Chromium or Google Chrome.", "support": { - "issues": "https://github.com/beganovich/snappdf/issues", - "source": "https://github.com/beganovich/snappdf/tree/v5.0.1" + "source": "https://github.com/turbo124/snappdf/tree/master" }, - "time": "2024-11-20T17:31:20+00:00" + "time": "2025-01-04T00:35:22+00:00" }, { "name": "braintree/braintree_php", - "version": "6.30.0", + "version": "6.31.0", "source": { "type": "git", "url": "https://github.com/braintree/braintree_php.git", - "reference": "26554ef16234fd1bf054fc9dcff5c7e33c236be7" + "reference": "5c41da561a821d4131bcd336322e196e0198ef83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/braintree/braintree_php/zipball/26554ef16234fd1bf054fc9dcff5c7e33c236be7", - "reference": "26554ef16234fd1bf054fc9dcff5c7e33c236be7", + "url": "https://api.github.com/repos/braintree/braintree_php/zipball/5c41da561a821d4131bcd336322e196e0198ef83", + "reference": "5c41da561a821d4131bcd336322e196e0198ef83", "shasum": "" }, "require": { @@ -1105,9 +1113,9 @@ "description": "Braintree PHP Client Library", "support": { "issues": "https://github.com/braintree/braintree_php/issues", - "source": "https://github.com/braintree/braintree_php/tree/6.30.0" + "source": "https://github.com/braintree/braintree_php/tree/6.31.0" }, - "time": "2025-10-23T15:43:34+00:00" + "time": "2025-12-11T16:27:18+00:00" }, { "name": "brick/math", @@ -1428,16 +1436,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.9", + "version": "1.5.10", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "1905981ee626e6f852448b7aaa978f8666c5bc54" + "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/1905981ee626e6f852448b7aaa978f8666c5bc54", - "reference": "1905981ee626e6f852448b7aaa978f8666c5bc54", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/961a5e4056dd2e4a2eedcac7576075947c28bf63", + "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63", "shasum": "" }, "require": { @@ -1484,7 +1492,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.9" + "source": "https://github.com/composer/ca-bundle/tree/1.5.10" }, "funding": [ { @@ -1496,7 +1504,7 @@ "type": "github" } ], - "time": "2025-11-06T11:46:17+00:00" + "time": "2025-12-08T15:06:51+00:00" }, { "name": "composer/pcre", @@ -2710,31 +2718,31 @@ }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -2765,7 +2773,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -2777,7 +2785,7 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "getbrevo/brevo-php", @@ -3028,16 +3036,16 @@ }, { "name": "google/apiclient-services", - "version": "v0.421.0", + "version": "v0.424.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client-services.git", - "reference": "d84e7301a52405677807564dab6b1a112dfd03bd" + "reference": "b368dcc5dce8043fed1b248a66020747d5cec353" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/d84e7301a52405677807564dab6b1a112dfd03bd", - "reference": "d84e7301a52405677807564dab6b1a112dfd03bd", + "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/b368dcc5dce8043fed1b248a66020747d5cec353", + "reference": "b368dcc5dce8043fed1b248a66020747d5cec353", "shasum": "" }, "require": { @@ -3066,9 +3074,9 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client-services/issues", - "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.421.0" + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.424.0" }, - "time": "2025-11-23T01:06:22+00:00" + "time": "2025-12-15T00:58:22+00:00" }, { "name": "google/auth", @@ -4026,16 +4034,16 @@ }, { "name": "horstoeko/zugferd", - "version": "v1.0.117", + "version": "v1.0.118", "source": { "type": "git", "url": "https://github.com/horstoeko/zugferd.git", - "reference": "7c2fdb58e0910e199b1fd2162ae5d33a1e62e933" + "reference": "5259a34da6a5e5e92a764e7dd39cd7330335a579" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/7c2fdb58e0910e199b1fd2162ae5d33a1e62e933", - "reference": "7c2fdb58e0910e199b1fd2162ae5d33a1e62e933", + "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/5259a34da6a5e5e92a764e7dd39cd7330335a579", + "reference": "5259a34da6a5e5e92a764e7dd39cd7330335a579", "shasum": "" }, "require": { @@ -4048,10 +4056,10 @@ "setasign/fpdf": "^1", "setasign/fpdi": "^2", "smalot/pdfparser": "^0|^2", - "symfony/finder": "^5|^6|^7", - "symfony/process": "^5|^6|^7", - "symfony/validator": "^5|^6|^7", - "symfony/yaml": "^5|^6|^7" + "symfony/finder": "^5|^6|^7|^8", + "symfony/process": "^5|^6|^7|^8", + "symfony/validator": "^5|^6|^7|^8", + "symfony/yaml": "^5|^6|^7|^8" }, "require-dev": { "goetas-webservices/xsd2php": "^0", @@ -4094,9 +4102,9 @@ ], "support": { "issues": "https://github.com/horstoeko/zugferd/issues", - "source": "https://github.com/horstoeko/zugferd/tree/v1.0.117" + "source": "https://github.com/horstoeko/zugferd/tree/v1.0.118" }, - "time": "2025-11-17T14:09:15+00:00" + "time": "2025-12-10T06:08:16+00:00" }, { "name": "horstoeko/zugferdvisualizer", @@ -4662,29 +4670,29 @@ }, { "name": "jms/metadata", - "version": "2.8.0", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/metadata.git", - "reference": "7ca240dcac0c655eb15933ee55736ccd2ea0d7a6" + "reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/7ca240dcac0c655eb15933ee55736ccd2ea0d7a6", - "reference": "7ca240dcac0c655eb15933ee55736ccd2ea0d7a6", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/554319d2e5f0c5d8ccaeffe755eac924e14da330", + "reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330", "shasum": "" }, "require": { "php": "^7.2|^8.0" }, "require-dev": { - "doctrine/cache": "^1.0", + "doctrine/cache": "^1.0|^2.0", "doctrine/coding-standard": "^8.0", "mikey179/vfsstream": "^1.6.7", - "phpunit/phpunit": "^8.5|^9.0", + "phpunit/phpunit": "^8.5.42|^9.6.23", "psr/container": "^1.0|^2.0", - "symfony/cache": "^3.1|^4.0|^5.0", - "symfony/dependency-injection": "^3.1|^4.0|^5.0" + "symfony/cache": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0", + "symfony/dependency-injection": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0" }, "type": "library", "extra": { @@ -4720,22 +4728,22 @@ ], "support": { "issues": "https://github.com/schmittjoh/metadata/issues", - "source": "https://github.com/schmittjoh/metadata/tree/2.8.0" + "source": "https://github.com/schmittjoh/metadata/tree/2.9.0" }, - "time": "2023-02-15T13:44:18+00:00" + "time": "2025-11-30T20:12:26+00:00" }, { "name": "jms/serializer", - "version": "3.32.5", + "version": "3.32.6", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "7c88b1b02ff868eecc870eeddbb3b1250e4bd89c" + "reference": "b02a6c00d8335ef68c163bf7c9e39f396dc5853f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/7c88b1b02ff868eecc870eeddbb3b1250e4bd89c", - "reference": "7c88b1b02ff868eecc870eeddbb3b1250e4bd89c", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/b02a6c00d8335ef68c163bf7c9e39f396dc5853f", + "reference": "b02a6c00d8335ef68c163bf7c9e39f396dc5853f", "shasum": "" }, "require": { @@ -4758,16 +4766,15 @@ "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0", "psr/container": "^1.0 || ^2.0", - "rector/rector": "^1.0.0 || ^2.0@dev", - "slevomat/coding-standard": "dev-master#f2cc4c553eae68772624ffd7dd99022343b69c31 as 8.11.9999", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", - "symfony/expression-language": "^5.4 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", - "symfony/form": "^5.4 || ^6.0 || ^7.0", - "symfony/translation": "^5.4 || ^6.0 || ^7.0", - "symfony/uid": "^5.4 || ^6.0 || ^7.0", - "symfony/validator": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0", + "rector/rector": "^1.0.0 || ^2.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/form": "^5.4.45 || ^6.4.27 || ^7.0 || ^8.0", + "symfony/translation": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/uid": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/validator": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0", "twig/twig": "^1.34 || ^2.4 || ^3.0" }, "suggest": { @@ -4812,7 +4819,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/serializer/issues", - "source": "https://github.com/schmittjoh/serializer/tree/3.32.5" + "source": "https://github.com/schmittjoh/serializer/tree/3.32.6" }, "funding": [ { @@ -4824,7 +4831,7 @@ "type": "github" } ], - "time": "2025-05-26T15:55:41+00:00" + "time": "2025-11-28T12:37:32+00:00" }, { "name": "josemmo/facturae-php", @@ -5078,16 +5085,16 @@ }, { "name": "laravel/framework", - "version": "v11.46.2", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "d6b16e72a98c2ad3257ec6b3f1f00532c3b1c2fc" + "reference": "86693ffa1ba32f56f8c44e31416c6665095a62c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/d6b16e72a98c2ad3257ec6b3f1f00532c3b1c2fc", - "reference": "d6b16e72a98c2ad3257ec6b3f1f00532c3b1c2fc", + "url": "https://api.github.com/repos/laravel/framework/zipball/86693ffa1ba32f56f8c44e31416c6665095a62c5", + "reference": "86693ffa1ba32f56f8c44e31416c6665095a62c5", "shasum": "" }, "require": { @@ -5289,20 +5296,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-11-25T19:02:06+00:00" + "time": "2025-11-28T18:20:11+00:00" }, { "name": "laravel/octane", - "version": "v2.13.1", + "version": "v2.13.2", "source": { "type": "git", "url": "https://github.com/laravel/octane.git", - "reference": "20b741badaa22cae73b87ffc4d979f3a7f06db25" + "reference": "5b963d2da879f2cad3a84f22bafd3d8be7170988" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/octane/zipball/20b741badaa22cae73b87ffc4d979f3a7f06db25", - "reference": "20b741badaa22cae73b87ffc4d979f3a7f06db25", + "url": "https://api.github.com/repos/laravel/octane/zipball/5b963d2da879f2cad3a84f22bafd3d8be7170988", + "reference": "5b963d2da879f2cad3a84f22bafd3d8be7170988", "shasum": "" }, "require": { @@ -5379,7 +5386,7 @@ "issues": "https://github.com/laravel/octane/issues", "source": "https://github.com/laravel/octane" }, - "time": "2025-10-27T12:05:17+00:00" + "time": "2025-11-28T20:13:00+00:00" }, { "name": "laravel/prompts", @@ -5648,16 +5655,16 @@ }, { "name": "laravel/socialite", - "version": "v5.23.2", + "version": "v5.24.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "41e65d53762d33d617bf0253330d672cb95e624b" + "reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/41e65d53762d33d617bf0253330d672cb95e624b", - "reference": "41e65d53762d33d617bf0253330d672cb95e624b", + "url": "https://api.github.com/repos/laravel/socialite/zipball/1d19358c28e8951dde6e36603b89d8f09e6cfbfd", + "reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd", "shasum": "" }, "require": { @@ -5716,7 +5723,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-11-21T14:00:38+00:00" + "time": "2025-12-09T15:37:06+00:00" }, { "name": "laravel/tinker", @@ -6655,20 +6662,20 @@ }, { "name": "league/uri", - "version": "7.6.0", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "f625804987a0a9112d954f9209d91fec52182344" + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", - "reference": "f625804987a0a9112d954f9209d91fec52182344", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.6", + "league/uri-interfaces": "^7.7", "php": "^8.1", "psr/http-factory": "^1" }, @@ -6741,7 +6748,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.6.0" + "source": "https://github.com/thephpleague/uri/tree/7.7.0" }, "funding": [ { @@ -6749,20 +6756,20 @@ "type": "github" } ], - "time": "2025-11-18T12:17:23+00:00" + "time": "2025-12-07T16:02:06+00:00" }, { "name": "league/uri-interfaces", - "version": "7.6.0", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", - "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c", + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c", "shasum": "" }, "require": { @@ -6825,7 +6832,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0" }, "funding": [ { @@ -6833,20 +6840,20 @@ "type": "github" } ], - "time": "2025-11-18T12:17:23+00:00" + "time": "2025-12-07T16:03:21+00:00" }, { "name": "livewire/livewire", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb" + "reference": "214da8f3a1199a88b56ab2fe901d4a607f784805" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/f5f9efe6d5a7059116bd695a89d95ceedf33f3cb", - "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb", + "url": "https://api.github.com/repos/livewire/livewire/zipball/214da8f3a1199a88b56ab2fe901d4a607f784805", + "reference": "214da8f3a1199a88b56ab2fe901d4a607f784805", "shasum": "" }, "require": { @@ -6901,7 +6908,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.7.0" + "source": "https://github.com/livewire/livewire/tree/v3.7.1" }, "funding": [ { @@ -6909,7 +6916,7 @@ "type": "github" } ], - "time": "2025-11-12T17:58:16+00:00" + "time": "2025-12-03T22:41:13+00:00" }, { "name": "maennchen/zipstream-php", @@ -7528,16 +7535,16 @@ }, { "name": "mpdf/mpdf", - "version": "v8.2.6", + "version": "v8.2.7", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3", + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3", "shasum": "" }, "require": { @@ -7605,7 +7612,7 @@ "type": "custom" } ], - "time": "2025-08-18T08:51:51+00:00" + "time": "2025-12-01T10:18:02+00:00" }, { "name": "mpdf/psr-http-message-shim", @@ -7900,16 +7907,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.3", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -7917,9 +7924,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.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" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -8001,7 +8008,7 @@ "type": "tidelift" } ], - "time": "2025-09-06T13:39:36+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nette/schema", @@ -8070,20 +8077,20 @@ }, { "name": "nette/utils", - "version": "v4.0.9", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "505a30ad386daa5211f08a318e47015b501cad30" + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/505a30ad386daa5211f08a318e47015b501cad30", - "reference": "505a30ad386daa5211f08a318e47015b501cad30", + "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", "shasum": "" }, "require": { - "php": "8.0 - 8.5" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", @@ -8106,7 +8113,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -8153,22 +8160,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.9" + "source": "https://github.com/nette/utils/tree/v4.1.0" }, - "time": "2025-10-31T00:45:47+00:00" + "time": "2025-12-01T17:49:23+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": { @@ -8211,9 +8218,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": "nordigen/nordigen-php", @@ -8923,16 +8930,16 @@ }, { "name": "php-http/client-common", - "version": "2.7.2", + "version": "2.7.3", "source": { "type": "git", "url": "https://github.com/php-http/client-common.git", - "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46" + "reference": "dcc6de29c90dd74faab55f71b79d89409c4bf0c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/client-common/zipball/0cfe9858ab9d3b213041b947c881d5b19ceeca46", - "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "url": "https://api.github.com/repos/php-http/client-common/zipball/dcc6de29c90dd74faab55f71b79d89409c4bf0c1", + "reference": "dcc6de29c90dd74faab55f71b79d89409c4bf0c1", "shasum": "" }, "require": { @@ -8942,15 +8949,13 @@ "psr/http-client": "^1.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.0 || ^2.0", - "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0 || ^8.0", "symfony/polyfill-php80": "^1.17" }, "require-dev": { "doctrine/instantiator": "^1.1", "guzzlehttp/psr7": "^1.4", "nyholm/psr7": "^1.2", - "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", - "phpspec/prophecy": "^1.10.2", "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" }, "suggest": { @@ -8986,9 +8991,9 @@ ], "support": { "issues": "https://github.com/php-http/client-common/issues", - "source": "https://github.com/php-http/client-common/tree/2.7.2" + "source": "https://github.com/php-http/client-common/tree/2.7.3" }, - "time": "2024-09-24T06:21:48+00:00" + "time": "2025-11-29T19:12:34+00:00" }, { "name": "php-http/discovery", @@ -9414,16 +9419,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.4", + "version": "5.6.5", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "90a04bcbf03784066f16038e87e23a0a83cee3c2" + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90a04bcbf03784066f16038e87e23a0a83cee3c2", - "reference": "90a04bcbf03784066f16038e87e23a0a83cee3c2", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", "shasum": "" }, "require": { @@ -9472,9 +9477,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.4" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" }, - "time": "2025-11-17T21:13:10+00:00" + "time": "2025-11-27T19:50:05+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -10448,16 +10453,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.14", + "version": "v0.12.17", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "95c29b3756a23855a30566b745d218bee690bef2" + "reference": "85fbbd9f3064e157fc21fe4362b2b5c19f2ea631" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", - "reference": "95c29b3756a23855a30566b745d218bee690bef2", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85fbbd9f3064e157fc21fe4362b2b5c19f2ea631", + "reference": "85fbbd9f3064e157fc21fe4362b2b5c19f2ea631", "shasum": "" }, "require": { @@ -10465,8 +10470,8 @@ "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" @@ -10521,9 +10526,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.17" }, - "time": "2025-10-27T17:15:31+00:00" + "time": "2025-12-15T04:55:34+00:00" }, { "name": "pusher/pusher-php-server", @@ -10768,20 +10773,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -10840,9 +10845,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "razorpay/razorpay", @@ -10911,16 +10916,16 @@ }, { "name": "rmccue/requests", - "version": "v2.0.16", + "version": "v2.0.17", "source": { "type": "git", "url": "https://github.com/WordPress/Requests.git", - "reference": "babd303d2084cf2690db536aeb6eef58326ee3ff" + "reference": "74d1648cc34e16a42ea25d548fc73ec107a90421" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/Requests/zipball/babd303d2084cf2690db536aeb6eef58326ee3ff", - "reference": "babd303d2084cf2690db536aeb6eef58326ee3ff", + "url": "https://api.github.com/repos/WordPress/Requests/zipball/74d1648cc34e16a42ea25d548fc73ec107a90421", + "reference": "74d1648cc34e16a42ea25d548fc73ec107a90421", "shasum": "" }, "require": { @@ -10993,20 +10998,20 @@ "issues": "https://github.com/WordPress/Requests/issues", "source": "https://github.com/WordPress/Requests" }, - "time": "2025-11-21T23:56:29+00:00" + "time": "2025-12-12T17:47:19+00:00" }, { "name": "robrichards/xmlseclibs", - "version": "3.1.3", + "version": "3.1.4", "source": { "type": "git", "url": "https://github.com/robrichards/xmlseclibs.git", - "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07" + "reference": "bc87389224c6de95802b505e5265b0ec2c5bcdbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/2bdfd742624d739dfadbd415f00181b4a77aaf07", - "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/bc87389224c6de95802b505e5265b0ec2c5bcdbd", + "reference": "bc87389224c6de95802b505e5265b0ec2c5bcdbd", "shasum": "" }, "require": { @@ -11033,9 +11038,9 @@ ], "support": { "issues": "https://github.com/robrichards/xmlseclibs/issues", - "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.3" + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.4" }, - "time": "2024-11-20T21:13:56+00:00" + "time": "2025-12-08T11:57:53+00:00" }, { "name": "sabberworm/php-css-parser", @@ -11234,16 +11239,16 @@ }, { "name": "sentry/sentry", - "version": "4.18.1", + "version": "4.19.1", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "04dcf20b39742b731b676f8b8d4f02d1db488af8" + "reference": "1c21d60bebe67c0122335bd3fe977990435af0a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/04dcf20b39742b731b676f8b8d4f02d1db488af8", - "reference": "04dcf20b39742b731b676f8b8d4f02d1db488af8", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/1c21d60bebe67c0122335bd3fe977990435af0a3", + "reference": "1c21d60bebe67c0122335bd3fe977990435af0a3", "shasum": "" }, "require": { @@ -11306,7 +11311,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/4.18.1" + "source": "https://github.com/getsentry/sentry-php/tree/4.19.1" }, "funding": [ { @@ -11318,27 +11323,27 @@ "type": "custom" } ], - "time": "2025-11-11T09:34:53+00:00" + "time": "2025-12-02T15:57:41+00:00" }, { "name": "sentry/sentry-laravel", - "version": "4.19.0", + "version": "4.20.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "7fdffd57e8fff0a6f9a18d9a83f32e960af63e3f" + "reference": "95f2542ee1ebc993529b63f5c8543184abd00650" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/7fdffd57e8fff0a6f9a18d9a83f32e960af63e3f", - "reference": "7fdffd57e8fff0a6f9a18d9a83f32e960af63e3f", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/95f2542ee1ebc993529b63f5c8543184abd00650", + "reference": "95f2542ee1ebc993529b63f5c8543184abd00650", "shasum": "" }, "require": { "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", "nyholm/psr7": "^1.0", "php": "^7.2 | ^8.0", - "sentry/sentry": "^4.18.0", + "sentry/sentry": "^4.19.0", "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0" }, "require-dev": { @@ -11396,7 +11401,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.19.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.20.0" }, "funding": [ { @@ -11408,7 +11413,7 @@ "type": "custom" } ], - "time": "2025-11-11T09:01:14+00:00" + "time": "2025-12-02T10:37:40+00:00" }, { "name": "setasign/fpdf", @@ -12033,16 +12038,16 @@ }, { "name": "symfony/cache", - "version": "v7.3.6", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701" + "reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/1277a1ec61c8d93ea61b2a59738f1deb9bfb6701", - "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701", + "url": "https://api.github.com/repos/symfony/cache/zipball/21e0755783bbbab58f2bb6a7a57896d21d27a366", + "reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366", "shasum": "" }, "require": { @@ -12050,12 +12055,14 @@ "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^3.6", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "conflict": { "doctrine/dbal": "<3.6", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1", "symfony/dependency-injection": "<6.4", "symfony/http-kernel": "<6.4", "symfony/var-dumper": "<6.4" @@ -12070,13 +12077,13 @@ "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/filesystem": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12111,7 +12118,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.3.6" + "source": "https://github.com/symfony/cache/tree/v7.4.1" }, "funding": [ { @@ -12131,7 +12138,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T13:22:58+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "symfony/cache-contracts", @@ -12211,16 +12218,16 @@ }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", "shasum": "" }, "require": { @@ -12265,7 +12272,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v7.4.0" }, "funding": [ { @@ -12276,31 +12283,35 @@ "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-09-25T14:21:43+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/config", - "version": "v7.3.6", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "9d18eba95655a3152ae4c1d53c6cc34eb4d4a0b7" + "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/9d18eba95655a3152ae4c1d53c6cc34eb4d4a0b7", - "reference": "9d18eba95655a3152ae4c1d53c6cc34eb4d4a0b7", + "url": "https://api.github.com/repos/symfony/config/zipball/2c323304c354a43a48b61c5fa760fc4ed60ce495", + "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^7.1", + "symfony/filesystem": "^7.1|^8.0", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -12308,11 +12319,11 @@ "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12340,7 +12351,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.3.6" + "source": "https://github.com/symfony/config/tree/v7.4.1" }, "funding": [ { @@ -12360,20 +12371,20 @@ "type": "tidelift" } ], - "time": "2025-11-02T08:04:43+00:00" + "time": "2025-12-05T07:52:08+00:00" }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", "shasum": "" }, "require": { @@ -12381,7 +12392,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", @@ -12395,16 +12406,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": { @@ -12438,7 +12449,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v7.4.1" }, "funding": [ { @@ -12458,20 +12469,20 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2025-12-05T15:23:39+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "84321188c4754e64273b46b406081ad9b18e8614" + "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/84321188c4754e64273b46b406081ad9b18e8614", - "reference": "84321188c4754e64273b46b406081ad9b18e8614", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/ab862f478513e7ca2fe9ec117a6f01a8da6e1135", + "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135", "shasum": "" }, "require": { @@ -12507,7 +12518,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.6" + "source": "https://github.com/symfony/css-selector/tree/v7.4.0" }, "funding": [ { @@ -12527,28 +12538,28 @@ "type": "tidelift" } ], - "time": "2025-10-29T17:24:25+00:00" + "time": "2025-10-30T13:39:42+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.3.6", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "98af8bb46c56aedd9dd5a7f0414fc72bf2dcfe69" + "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/98af8bb46c56aedd9dd5a7f0414fc72bf2dcfe69", - "reference": "98af8bb46c56aedd9dd5a7f0414fc72bf2dcfe69", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", + "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", "shasum": "" }, "require": { "php": ">=8.2", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^3.5", - "symfony/var-exporter": "^6.4.20|^7.2.5" + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -12561,9 +12572,9 @@ "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12591,7 +12602,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.3.6" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.2" }, "funding": [ { @@ -12611,7 +12622,7 @@ "type": "tidelift" } ], - "time": "2025-10-31T10:11:11+00:00" + "time": "2025-12-08T06:57:04+00:00" }, { "name": "symfony/deprecation-contracts", @@ -12682,32 +12693,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "bbe40bfab84323d99dab491b716ff142410a92a8" + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/bbe40bfab84323d99dab491b716ff142410a92a8", - "reference": "bbe40bfab84323d99dab491b716ff142410a92a8", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -12739,7 +12751,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.6" + "source": "https://github.com/symfony/error-handler/tree/v7.4.0" }, "funding": [ { @@ -12759,20 +12771,20 @@ "type": "tidelift" } ], - "time": "2025-10-31T19:12:50+00:00" + "time": "2025-11-05T14:29:59+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" }, "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/9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", "shasum": "" }, "require": { @@ -12789,13 +12801,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": { @@ -12823,7 +12836,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.0" }, "funding": [ { @@ -12843,7 +12856,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-28T09:38:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -12923,16 +12936,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", "shasum": "" }, "require": { @@ -12941,7 +12954,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": { @@ -12969,7 +12982,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.0" }, "funding": [ { @@ -12989,27 +13002,27 @@ "type": "tidelift" } ], - "time": "2025-11-05T09:52:27+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -13037,7 +13050,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.0" }, "funding": [ { @@ -13057,38 +13070,39 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/framework-bundle", - "version": "v7.3.6", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "cabfdfa82bc4f75d693a329fe263d96937636b77" + "reference": "2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/cabfdfa82bc4f75d693a329fe263d96937636b77", - "reference": "cabfdfa82bc4f75d693a329fe263d96937636b77", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3", + "reference": "2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3", "shasum": "" }, "require": { "composer-runtime-api": ">=2.1", "ext-xml": "*", "php": ">=8.2", - "symfony/cache": "^6.4|^7.0", - "symfony/config": "^7.3", - "symfony/dependency-injection": "^7.2", + "symfony/cache": "^6.4.12|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^7.3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/filesystem": "^7.1", - "symfony/finder": "^6.4|^7.0", - "symfony/http-foundation": "^7.3", - "symfony/http-kernel": "^7.2", + "symfony/error-handler": "^7.3|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/routing": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/routing": "^7.4|^8.0" }, "conflict": { "doctrine/persistence": "<1.3", @@ -13100,14 +13114,12 @@ "symfony/console": "<6.4", "symfony/dom-crawler": "<6.4", "symfony/dotenv": "<6.4", - "symfony/form": "<6.4", + "symfony/form": "<7.4", "symfony/http-client": "<6.4", - "symfony/json-streamer": ">=7.4", "symfony/lock": "<6.4", "symfony/mailer": "<6.4", - "symfony/messenger": "<6.4", + "symfony/messenger": "<7.4", "symfony/mime": "<6.4", - "symfony/object-mapper": ">=7.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", @@ -13122,51 +13134,52 @@ "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", "symfony/webhook": "<7.2", - "symfony/workflow": "<7.3.0-beta2" + "symfony/workflow": "<7.4" }, "require-dev": { "doctrine/persistence": "^1.3|^2|^3", "dragonmantank/cron-expression": "^3.1", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "seld/jsonlint": "^1.10", - "symfony/asset": "^6.4|^7.0", - "symfony/asset-mapper": "^6.4|^7.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/dotenv": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", - "symfony/html-sanitizer": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/json-streamer": "7.3.*", - "symfony/lock": "^6.4|^7.0", - "symfony/mailer": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/notifier": "^6.4|^7.0", - "symfony/object-mapper": "^v7.3.0-beta2", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/json-streamer": "^7.3|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/object-mapper": "^7.3|^8.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/scheduler": "^6.4.4|^7.0.4", - "symfony/security-bundle": "^6.4|^7.0", - "symfony/semaphore": "^6.4|^7.0", - "symfony/serializer": "^7.2.5", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/string": "^6.4|^7.0", - "symfony/translation": "^7.3", - "symfony/twig-bundle": "^6.4|^7.0", - "symfony/type-info": "^7.1.8", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/web-link": "^6.4|^7.0", - "symfony/webhook": "^7.2", - "symfony/workflow": "^7.3", - "symfony/yaml": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/scheduler": "^6.4.4|^7.0.4|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/semaphore": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.2.5|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/webhook": "^7.2|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.3|^8.0", "twig/twig": "^3.12" }, "type": "symfony-bundle", @@ -13195,7 +13208,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v7.3.6" + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.1" }, "funding": [ { @@ -13215,20 +13228,20 @@ "type": "tidelift" } ], - "time": "2025-10-30T09:42:24+00:00" + "time": "2025-12-05T14:04:53+00:00" }, { "name": "symfony/http-client", - "version": "v7.3.6", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de" + "reference": "26cc224ea7103dda90e9694d9e139a389092d007" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", - "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", + "url": "https://api.github.com/repos/symfony/http-client/zipball/26cc224ea7103dda90e9694d9e139a389092d007", + "reference": "26cc224ea7103dda90e9694d9e139a389092d007", "shasum": "" }, "require": { @@ -13259,12 +13272,13 @@ "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -13295,7 +13309,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.6" + "source": "https://github.com/symfony/http-client/tree/v7.4.1" }, "funding": [ { @@ -13315,7 +13329,7 @@ "type": "tidelift" } ], - "time": "2025-11-05T17:41:46+00:00" + "time": "2025-12-04T21:12:57+00:00" }, { "name": "symfony/http-client-contracts", @@ -13397,23 +13411,22 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.7", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4" + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4", - "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -13422,13 +13435,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -13456,7 +13469,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.7" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.1" }, "funding": [ { @@ -13476,29 +13489,29 @@ "type": "tidelift" } ], - "time": "2025-11-08T16:41:12+00:00" + "time": "2025-12-07T11:13:10+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.7", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce" + "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/10b8e9b748ea95fa4539c208e2487c435d3c87ce", - "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f", + "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -13508,6 +13521,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -13525,27 +13539,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -13574,7 +13588,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.7" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.2" }, "funding": [ { @@ -13594,20 +13608,20 @@ "type": "tidelift" } ], - "time": "2025-11-12T11:38:40+00:00" + "time": "2025-12-08T07:43:37+00:00" }, { "name": "symfony/intl", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "9eccaaa94ac6f9deb3620c9d47a057d965baeabf" + "reference": "2fa074de6c7faa6b54f2891fc22708f42245ed5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/9eccaaa94ac6f9deb3620c9d47a057d965baeabf", - "reference": "9eccaaa94ac6f9deb3620c9d47a057d965baeabf", + "url": "https://api.github.com/repos/symfony/intl/zipball/2fa074de6c7faa6b54f2891fc22708f42245ed5c", + "reference": "2fa074de6c7faa6b54f2891fc22708f42245ed5c", "shasum": "" }, "require": { @@ -13618,8 +13632,8 @@ "symfony/string": "<7.1" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -13664,7 +13678,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v7.3.5" + "source": "https://github.com/symfony/intl/tree/v7.4.0" }, "funding": [ { @@ -13684,7 +13698,7 @@ "type": "tidelift" } ], - "time": "2025-10-01T06:11:17+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/mailer", @@ -13837,20 +13851,21 @@ }, { "name": "symfony/mime", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -13865,11 +13880,11 @@ "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -13901,7 +13916,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.4" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -13921,20 +13936,20 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "b38026df55197f9e39a44f3215788edf83187b80" }, "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/b38026df55197f9e39a44f3215788edf83187b80", + "reference": "b38026df55197f9e39a44f3215788edf83187b80", "shasum": "" }, "require": { @@ -13972,7 +13987,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v7.4.0" }, "funding": [ { @@ -13992,7 +14007,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/polyfill-ctype", @@ -14828,6 +14843,86 @@ ], "time": "2025-06-24T13:30:11+00:00" }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "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\\Php85\\": "" + }, + "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.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/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-23T16:12:55+00:00" + }, { "name": "symfony/polyfill-uuid", "version": "v1.33.0", @@ -14983,16 +15078,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { @@ -15024,7 +15119,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.0" }, "funding": [ { @@ -15044,28 +15139,29 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T11:21:06+00:00" }, { "name": "symfony/property-access", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "4a4389e5c8bd1d0320d80a23caa6a1ac71cb81a7" + "reference": "537626149d2910ca43eb9ce465654366bf4442f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/4a4389e5c8bd1d0320d80a23caa6a1ac71cb81a7", - "reference": "4a4389e5c8bd1d0320d80a23caa6a1ac71cb81a7", + "url": "https://api.github.com/repos/symfony/property-access/zipball/537626149d2910ca43eb9ce465654366bf4442f4", + "reference": "537626149d2910ca43eb9ce465654366bf4442f4", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/property-info": "^6.4|^7.0" + "symfony/property-info": "^6.4|^7.0|^8.0" }, "require-dev": { - "symfony/cache": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" }, "type": "library", "autoload": { @@ -15104,7 +15200,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.3.3" + "source": "https://github.com/symfony/property-access/tree/v7.4.0" }, "funding": [ { @@ -15124,27 +15220,27 @@ "type": "tidelift" } ], - "time": "2025-08-04T15:15:28+00:00" + "time": "2025-09-08T21:14:32+00:00" }, { "name": "symfony/property-info", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "0b346ed259dc5da43535caf243005fe7d4b0f051" + "reference": "912aafe70bee5cfd09fec5916fe35b83f04ae6ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/0b346ed259dc5da43535caf243005fe7d4b0f051", - "reference": "0b346ed259dc5da43535caf243005fe7d4b0f051", + "url": "https://api.github.com/repos/symfony/property-info/zipball/912aafe70bee5cfd09fec5916fe35b83f04ae6ae", + "reference": "912aafe70bee5cfd09fec5916fe35b83f04ae6ae", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0", - "symfony/type-info": "^7.3.5" + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "~7.3.8|^7.4.1|^8.0.1" }, "conflict": { "phpdocumentor/reflection-docblock": "<5.2", @@ -15156,9 +15252,9 @@ "require-dev": { "phpdocumentor/reflection-docblock": "^5.2", "phpstan/phpdoc-parser": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -15194,7 +15290,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.3.5" + "source": "https://github.com/symfony/property-info/tree/v7.4.1" }, "funding": [ { @@ -15214,26 +15310,26 @@ "type": "tidelift" } ], - "time": "2025-10-05T22:12:41+00:00" + "time": "2025-12-05T14:04:53+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f" + "reference": "0101ff8bd0506703b045b1670960302d302a726c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", - "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/0101ff8bd0506703b045b1670960302d302a726c", + "reference": "0101ff8bd0506703b045b1670960302d302a726c", "shasum": "" }, "require": { "php": ">=8.2", "psr/http-message": "^1.0|^2.0", - "symfony/http-foundation": "^6.4|^7.0" + "symfony/http-foundation": "^6.4|^7.0|^8.0" }, "conflict": { "php-http/discovery": "<1.15", @@ -15243,11 +15339,12 @@ "nyholm/psr7": "^1.1", "php-http/discovery": "^1.15", "psr/log": "^1.1.4|^2|^3", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" }, "type": "symfony-bridge", "autoload": { @@ -15281,7 +15378,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.3.0" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.0" }, "funding": [ { @@ -15292,25 +15389,29 @@ "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-09-26T08:57:56+00:00" + "time": "2025-11-13T08:38:49+00:00" }, { "name": "symfony/routing", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "c97abe725f2a1a858deca629a6488c8fc20c3091" + "reference": "4720254cb2644a0b876233d258a32bf017330db7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/c97abe725f2a1a858deca629a6488c8fc20c3091", - "reference": "c97abe725f2a1a858deca629a6488c8fc20c3091", + "url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7", + "reference": "4720254cb2644a0b876233d258a32bf017330db7", "shasum": "" }, "require": { @@ -15324,11 +15425,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -15362,7 +15463,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.6" + "source": "https://github.com/symfony/routing/tree/v7.4.0" }, "funding": [ { @@ -15382,20 +15483,20 @@ "type": "tidelift" } ], - "time": "2025-11-05T07:57:47+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/serializer", - "version": "v7.3.5", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "ba2e50a5f2870c93f0f47ca1a4e56e4bbe274035" + "reference": "1a957acb613b520e443c2c659a67c782b67794bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/ba2e50a5f2870c93f0f47ca1a4e56e4bbe274035", - "reference": "ba2e50a5f2870c93f0f47ca1a4e56e4bbe274035", + "url": "https://api.github.com/repos/symfony/serializer/zipball/1a957acb613b520e443c2c659a67c782b67794bc", + "reference": "1a957acb613b520e443c2c659a67c782b67794bc", "shasum": "" }, "require": { @@ -15418,26 +15519,26 @@ "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", "phpstan/phpdoc-parser": "^1.0|^2.0", "seld/jsonlint": "^1.10", - "symfony/cache": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^7.2", - "symfony/error-handler": "^6.4|^7.0", - "symfony/filesystem": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^7.2|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/form": "^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/messenger": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.1.8", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -15465,7 +15566,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.3.5" + "source": "https://github.com/symfony/serializer/tree/v7.4.2" }, "funding": [ { @@ -15485,7 +15586,7 @@ "type": "tidelift" } ], - "time": "2025-10-08T11:26:21+00:00" + "time": "2025-12-07T17:35:40+00:00" }, { "name": "symfony/service-contracts", @@ -15576,22 +15677,23 @@ }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", "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" }, @@ -15599,11 +15701,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": { @@ -15642,7 +15744,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v7.4.0" }, "funding": [ { @@ -15662,27 +15764,27 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", @@ -15701,17 +15803,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "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", - "symfony/intl": "^6.4|^7.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", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -15742,7 +15844,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v7.4.0" }, "funding": [ { @@ -15762,7 +15864,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation-contracts", @@ -15848,16 +15950,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v7.3.6", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "d1aaec8eee1f5591f56b9efe00194d73a8e38319" + "reference": "9103559ef3e9f06708d8bff6810f6335b8f1eee8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/d1aaec8eee1f5591f56b9efe00194d73a8e38319", - "reference": "d1aaec8eee1f5591f56b9efe00194d73a8e38319", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/9103559ef3e9f06708d8bff6810f6335b8f1eee8", + "reference": "9103559ef3e9f06708d8bff6810f6335b8f1eee8", "shasum": "" }, "require": { @@ -15882,33 +15984,33 @@ "egulias/email-validator": "^2.1.10|^3|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/asset": "^6.4|^7.0", - "symfony/asset-mapper": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/emoji": "^7.1", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/form": "^6.4.20|^7.2.5", - "symfony/html-sanitizer": "^6.4|^7.0", - "symfony/http-foundation": "^7.3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4.30|~7.3.8|^7.4.1|^8.0.1", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.3|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/routing": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/security-acl": "^2.8|^3.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/security-csrf": "^6.4|^7.0", - "symfony/security-http": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/web-link": "^6.4|^7.0", - "symfony/workflow": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/workflow": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", "twig/cssinliner-extra": "^3", "twig/inky-extra": "^3", "twig/markdown-extra": "^3" @@ -15939,7 +16041,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v7.3.6" + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.1" }, "funding": [ { @@ -15959,30 +16061,30 @@ "type": "tidelift" } ], - "time": "2025-11-04T15:37:51+00:00" + "time": "2025-12-05T14:04:53+00:00" }, { "name": "symfony/twig-bundle", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "da5c778a8416fcce5318737c4d944f6fa2bb3f81" + "reference": "f83f530d00d1bbc6f7fafeb433077887c83326ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/da5c778a8416fcce5318737c4d944f6fa2bb3f81", - "reference": "da5c778a8416fcce5318737c4d944f6fa2bb3f81", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/f83f530d00d1bbc6f7fafeb433077887c83326ef", + "reference": "f83f530d00d1bbc6f7fafeb433077887c83326ef", "shasum": "" }, "require": { "composer-runtime-api": ">=2.1", "php": ">=8.2", - "symfony/config": "^7.3", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/twig-bridge": "^7.3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/twig-bridge": "^7.3|^8.0", "twig/twig": "^3.12" }, "conflict": { @@ -15990,16 +16092,17 @@ "symfony/translation": "<6.4" }, "require-dev": { - "symfony/asset": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/routing": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", - "symfony/web-link": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "symfony-bundle", "autoload": { @@ -16027,7 +16130,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v7.3.4" + "source": "https://github.com/symfony/twig-bundle/tree/v7.4.0" }, "funding": [ { @@ -16047,20 +16150,20 @@ "type": "tidelift" } ], - "time": "2025-09-10T12:00:31+00:00" + "time": "2025-10-02T07:41:02+00:00" }, { "name": "symfony/type-info", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "8b36f41421160db56914f897b57eaa6a830758b3" + "reference": "ac5ab66b21c758df71b7210cf1033d1ac807f202" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/8b36f41421160db56914f897b57eaa6a830758b3", - "reference": "8b36f41421160db56914f897b57eaa6a830758b3", + "url": "https://api.github.com/repos/symfony/type-info/zipball/ac5ab66b21c758df71b7210cf1033d1ac807f202", + "reference": "ac5ab66b21c758df71b7210cf1033d1ac807f202", "shasum": "" }, "require": { @@ -16110,7 +16213,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.3.5" + "source": "https://github.com/symfony/type-info/tree/v7.4.1" }, "funding": [ { @@ -16130,20 +16233,20 @@ "type": "tidelift" } ], - "time": "2025-10-16T12:30:12+00:00" + "time": "2025-12-05T14:04:53+00:00" }, { "name": "symfony/uid", - "version": "v7.3.1", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", "shasum": "" }, "require": { @@ -16151,7 +16254,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -16188,7 +16291,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.0" }, "funding": [ { @@ -16199,25 +16302,29 @@ "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-27T19:55:54+00:00" + "time": "2025-09-25T11:02:55+00:00" }, { "name": "symfony/validator", - "version": "v7.3.7", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "8290a095497c3fe5046db21888d1f75b54ddf39d" + "reference": "569b71d1243ccc58e8f1d21e279669239e78f60d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/8290a095497c3fe5046db21888d1f75b54ddf39d", - "reference": "8290a095497c3fe5046db21888d1f75b54ddf39d", + "url": "https://api.github.com/repos/symfony/validator/zipball/569b71d1243ccc58e8f1d21e279669239e78f60d", + "reference": "569b71d1243ccc58e8f1d21e279669239e78f60d", "shasum": "" }, "require": { @@ -16237,27 +16344,29 @@ "symfony/intl": "<6.4", "symfony/property-info": "<6.4", "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/var-exporter": "<6.4.25|>=7.0,<7.3.3", "symfony/yaml": "<6.4" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", - "symfony/cache": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/string": "^6.4|^7.0", - "symfony/translation": "^6.4.3|^7.0.3", + "symfony/cache": "^6.4|^7.0|^8.0", + "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/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client": "^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/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", "symfony/type-info": "^7.1.8", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -16286,7 +16395,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.3.7" + "source": "https://github.com/symfony/validator/tree/v7.4.2" }, "funding": [ { @@ -16306,20 +16415,20 @@ "type": "tidelift" } ], - "time": "2025-11-08T16:29:29+00:00" + "time": "2025-12-07T17:35:40+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -16331,10 +16440,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -16373,7 +16482,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -16393,20 +16502,20 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", "shasum": "" }, "require": { @@ -16414,9 +16523,9 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -16454,7 +16563,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -16474,32 +16583,32 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -16530,7 +16639,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.5" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -16550,27 +16659,27 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -16603,9 +16712,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "turbo124/beacon", @@ -16723,16 +16832,16 @@ }, { "name": "twig/extra-bundle", - "version": "v3.22.1", + "version": "v3.22.2", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "b6534bc925bec930004facca92fccebd0c809247" + "reference": "09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/b6534bc925bec930004facca92fccebd0c809247", - "reference": "b6534bc925bec930004facca92fccebd0c809247", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e", + "reference": "09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e", "shasum": "" }, "require": { @@ -16781,7 +16890,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.22.1" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.22.2" }, "funding": [ { @@ -16793,7 +16902,7 @@ "type": "tidelift" } ], - "time": "2025-11-02T11:00:49+00:00" + "time": "2025-12-05T08:51:53+00:00" }, { "name": "twig/intl-extra", @@ -16933,16 +17042,16 @@ }, { "name": "twig/twig", - "version": "v3.22.0", + "version": "v3.22.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "4509984193026de413baf4ba80f68590a7f2c51d" + "reference": "1de2ec1fc43ab58a4b7e80b214b96bfc895750f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/4509984193026de413baf4ba80f68590a7f2c51d", - "reference": "4509984193026de413baf4ba80f68590a7f2c51d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/1de2ec1fc43ab58a4b7e80b214b96bfc895750f3", + "reference": "1de2ec1fc43ab58a4b7e80b214b96bfc895750f3", "shasum": "" }, "require": { @@ -16996,7 +17105,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.22.0" + "source": "https://github.com/twigphp/Twig/tree/v3.22.1" }, "funding": [ { @@ -17008,7 +17117,7 @@ "type": "tidelift" } ], - "time": "2025-10-29T15:56:47+00:00" + "time": "2025-11-16T16:01:12+00:00" }, { "name": "twilio/sdk", @@ -17462,16 +17571,16 @@ }, { "name": "barryvdh/laravel-ide-helper", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "8d00250cba25728373e92c1d8dcebcbf64623d29" + "reference": "b106f7ee85f263c4f103eca49e7bf3862c2e5e75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/8d00250cba25728373e92c1d8dcebcbf64623d29", - "reference": "8d00250cba25728373e92c1d8dcebcbf64623d29", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/b106f7ee85f263c4f103eca49e7bf3862c2e5e75", + "reference": "b106f7ee85f263c4f103eca49e7bf3862c2e5e75", "shasum": "" }, "require": { @@ -17540,7 +17649,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-ide-helper/issues", - "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.6.0" + "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.6.1" }, "funding": [ { @@ -17552,7 +17661,7 @@ "type": "github" } ], - "time": "2025-07-17T20:11:57+00:00" + "time": "2025-12-10T09:11:07+00:00" }, { "name": "barryvdh/reflection-docblock", @@ -18156,16 +18265,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.90.0", + "version": "v3.92.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "ad732c2e9299c9743f9c55ae53cc0e7642ab1155" + "reference": "5646c2cd99b7cb4b658ff681fe27069ba86c7280" }, "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/5646c2cd99b7cb4b658ff681fe27069ba86c7280", + "reference": "5646c2cd99b7cb4b658ff681fe27069ba86c7280", "shasum": "" }, "require": { @@ -18221,7 +18330,7 @@ "PhpCsFixer\\": "src/" }, "exclude-from-classmap": [ - "src/Fixer/Internal/*" + "src/**/Internal/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -18247,7 +18356,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.92.0" }, "funding": [ { @@ -18255,7 +18364,7 @@ "type": "github" } ], - "time": "2025-11-20T15:15:16+00:00" + "time": "2025-12-12T10:29:19+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -18351,19 +18460,20 @@ }, { "name": "illuminate/json-schema", - "version": "v12.40.2", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/json-schema.git", - "reference": "5a8ab3e084c91305196888cb9964b238cce3055b" + "reference": "d161f398dab36f08cf131997362bc2e3ecb0309a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/json-schema/zipball/5a8ab3e084c91305196888cb9964b238cce3055b", - "reference": "5a8ab3e084c91305196888cb9964b238cce3055b", + "url": "https://api.github.com/repos/illuminate/json-schema/zipball/d161f398dab36f08cf131997362bc2e3ecb0309a", + "reference": "d161f398dab36f08cf131997362bc2e3ecb0309a", "shasum": "" }, "require": { + "illuminate/contracts": "^10.50.0|^11.47.0|^12.40.2", "php": "^8.1" }, "type": "library", @@ -18393,7 +18503,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-11-26T16:51:20+00:00" + "time": "2025-11-28T18:45:48+00:00" }, { "name": "laracasts/cypress", @@ -18545,16 +18655,16 @@ }, { "name": "laravel/boost", - "version": "v1.8.3", + "version": "v1.8.5", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "26572e858e67334952779c0110ca4c378a44d28d" + "reference": "99c15c392f3c6f049f0671dd5dc7b6e9a75cfe7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/26572e858e67334952779c0110ca4c378a44d28d", - "reference": "26572e858e67334952779c0110ca4c378a44d28d", + "url": "https://api.github.com/repos/laravel/boost/zipball/99c15c392f3c6f049f0671dd5dc7b6e9a75cfe7e", + "reference": "99c15c392f3c6f049f0671dd5dc7b6e9a75cfe7e", "shasum": "" }, "require": { @@ -18563,16 +18673,16 @@ "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "laravel/mcp": "^0.3.4", + "laravel/mcp": "^0.4.1", "laravel/prompts": "0.1.25|^0.3.6", "laravel/roster": "^0.2.9", "php": "^8.1" }, "require-dev": { - "laravel/pint": "1.20", + "laravel/pint": "^1.20.0", "mockery/mockery": "^1.6.12", "orchestra/testbench": "^8.36.0|^9.15.0|^10.6", - "pestphp/pest": "^2.36.0|^3.8.4", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.1" }, @@ -18607,38 +18717,38 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-11-26T14:12:52+00:00" + "time": "2025-12-08T21:54:49+00:00" }, { "name": "laravel/mcp", - "version": "v0.3.4", + "version": "v0.4.2", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "0b86fb613a0df971cec89271c674a677c2cb4f77" + "reference": "1c7878be3931a19768f791ddf141af29f43fb4ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/0b86fb613a0df971cec89271c674a677c2cb4f77", - "reference": "0b86fb613a0df971cec89271c674a677c2cb4f77", + "url": "https://api.github.com/repos/laravel/mcp/zipball/1c7878be3931a19768f791ddf141af29f43fb4ef", + "reference": "1c7878be3931a19768f791ddf141af29f43fb4ef", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/container": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/http": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/json-schema": "^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/validation": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/console": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/container": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/http": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/json-schema": "^12.41.1", + "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/support": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/validation": "^10.49.0|^11.45.3|^12.41.1", "php": "^8.1" }, "require-dev": { - "laravel/pint": "1.20.0", - "orchestra/testbench": "^8.36.0|^9.15.0|^10.6.0", + "laravel/pint": "^1.20", + "orchestra/testbench": "^8.36|^9.15|^10.8", "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.2.4" @@ -18680,7 +18790,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-11-18T14:41:05+00:00" + "time": "2025-12-07T15:49:15+00:00" }, { "name": "laravel/roster", @@ -19506,16 +19616,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.44", + "version": "11.5.46", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82" + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/75dfe79a2aa30085b7132bb84377c24062193f33", + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33", "shasum": "" }, "require": { @@ -19587,7 +19697,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.46" }, "funding": [ { @@ -19611,7 +19721,7 @@ "type": "tidelift" } ], - "time": "2025-11-13T07:17:35+00:00" + "time": "2025-12-06T08:01:15+00:00" }, { "name": "react/cache", @@ -21702,16 +21812,16 @@ }, { "name": "symfony/stopwatch", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + "reference": "8a24af0a2e8a872fb745047180649b8418303084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084", + "reference": "8a24af0a2e8a872fb745047180649b8418303084", "shasum": "" }, "require": { @@ -21744,7 +21854,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.0" }, "funding": [ { @@ -21755,12 +21865,16 @@ "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": "2025-08-04T07:05:15+00:00" }, { "name": "theseer/tokenizer", From eaa929e55c697202ca142a200b0179973e5b3dd2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 Dec 2025 16:34:04 +1100 Subject: [PATCH 032/177] tests for cloning a design --- app/Http/Controllers/DesignController.php | 20 +++++++++++++-- app/Repositories/DesignRepository.php | 12 +++++++++ tests/Feature/DesignApiTest.php | 30 +++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/DesignController.php b/app/Http/Controllers/DesignController.php index cf9900a712..2ae6992297 100644 --- a/app/Http/Controllers/DesignController.php +++ b/app/Http/Controllers/DesignController.php @@ -527,11 +527,27 @@ class DesignController extends BaseController $ids = request()->input('ids'); - $designs = Design::withTrashed()->company()->whereIn('id', $this->transformKeys($ids)); - /** @var \App\Models\User $user */ $user = auth()->user(); + + if($action == 'clone') { + $design = Design::withTrashed() + ->whereIn('id', $this->transformKeys($ids)) + ->where(function ($q){ + $q->where('company_id', auth()->user()->company()->id) + ->orWhereNull('company_id'); + })->first(); + + if($design){ + $this->design_repo->clone($design, $user); + } + + return response()->noContent(); + } + + $designs = Design::withTrashed()->company()->whereIn('id', $this->transformKeys($ids)); + $designs->each(function ($design, $key) use ($action, $user) { if ($user->can('edit', $design)) { $this->design_repo->{$action}($design); diff --git a/app/Repositories/DesignRepository.php b/app/Repositories/DesignRepository.php index fa67d2bc69..53e8aa10f2 100644 --- a/app/Repositories/DesignRepository.php +++ b/app/Repositories/DesignRepository.php @@ -75,5 +75,17 @@ class DesignRepository extends BaseRepository } + public function clone($design, $user) + { + + $new_design = $design->replicate(); + $new_design->company_id = $user->company()->id; + $new_design->user_id = $user->id; + $new_design->name = $new_design->name.' clone '.date('Y-m-d H:i:s'); + $new_design->save(); + + return $new_design; + } + } diff --git a/tests/Feature/DesignApiTest.php b/tests/Feature/DesignApiTest.php index d9f8ce207b..e37c9c91c4 100644 --- a/tests/Feature/DesignApiTest.php +++ b/tests/Feature/DesignApiTest.php @@ -45,6 +45,36 @@ class DesignApiTest extends TestCase $this->makeTestData(); } + public function testCloneDesign() + { + + $design = Design::find(2); + + $this->assertNotNull($design); + + $data = [ + 'ids' => [$design->hashed_id], + 'action' => 'clone', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/designs/bulk', $data); + + + $response->assertStatus(204); + + + $d = Design::query()->latest()->first(); + + + $this->assertEquals($this->user->id, $d->user_id); + $this->assertEquals($this->company->id, $d->company_id); + $this->assertEquals($design->name.' clone '.date('Y-m-d H:i:s'), $d->name); + // $dsd = Design::all()->pluck('name')->toArray(); + } + public function testSelectiveDefaultDesignUpdatesInvoice() { $settings = ClientSettings::defaults(); From c1555211e9805fb96f69f40eda638aa8ebeb1f98 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 Dec 2025 16:37:12 +1100 Subject: [PATCH 033/177] Skip json parsing in CI --- tests/Feature/Design/JsonDesignServiceTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Feature/Design/JsonDesignServiceTest.php b/tests/Feature/Design/JsonDesignServiceTest.php index 357012d055..26d51891f8 100644 --- a/tests/Feature/Design/JsonDesignServiceTest.php +++ b/tests/Feature/Design/JsonDesignServiceTest.php @@ -23,6 +23,7 @@ class JsonDesignServiceTest extends TestCase { parent::setUp(); + $this->markTestSkipped('Skipping JsonDesignServiceTest'); // Load test design $jsonPath = base_path('tests/Feature/Design/stubs/test_design_1.json'); $this->testDesign = json_decode(file_get_contents($jsonPath), true); From c209246c8354dfbf40b7eb7c2ad3d04055393e66 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 Dec 2025 17:00:08 +1100 Subject: [PATCH 034/177] Quote Rejection --- app/Events/Quote/QuoteWasRejected.php | 54 ++++++++++ app/Filters/InvoiceFilters.php | 10 +- app/Listeners/Quote/QuoteRejectedActivity.php | 61 +++++++++++ .../Quote/QuoteRejectedNotification.php | 80 ++++++++++++++ app/Mail/Admin/QuoteRejectedObject.php | 101 ++++++++++++++++++ app/Models/Activity.php | 2 + app/Models/Quote.php | 2 + app/Providers/EventServiceProvider.php | 5 + lang/en/texts.php | 15 ++- 9 files changed, 324 insertions(+), 6 deletions(-) create mode 100644 app/Events/Quote/QuoteWasRejected.php create mode 100644 app/Listeners/Quote/QuoteRejectedActivity.php create mode 100644 app/Listeners/Quote/QuoteRejectedNotification.php create mode 100644 app/Mail/Admin/QuoteRejectedObject.php diff --git a/app/Events/Quote/QuoteWasRejected.php b/app/Events/Quote/QuoteWasRejected.php new file mode 100644 index 0000000000..760d030af3 --- /dev/null +++ b/app/Events/Quote/QuoteWasRejected.php @@ -0,0 +1,54 @@ +contact = $contact; + $this->quote = $quote; + $this->company = $company; + $this->notes = $notes; + $this->event_vars = $event_vars; + } +} diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 8c9d5c11e1..2d8d78bd84 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -287,9 +287,15 @@ class InvoiceFilters extends QueryFilters if ($sort_col[0] == 'client_id') { - return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir) + + //2025-12-16: Better filtering for clients. + return $this->builder->orderByRaw('client_id IS NULL') ->orderBy(\App\Models\Client::select('name') - ->whereColumn('clients.id', 'invoices.client_id'), $dir); + ->whereColumn('clients.id', 'invoices.client_id') + ->limit(1), $dir); + // return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir) + // ->orderBy(\App\Models\Client::select('name') + // ->whereColumn('clients.id', 'invoices.client_id'), $dir); } diff --git a/app/Listeners/Quote/QuoteRejectedActivity.php b/app/Listeners/Quote/QuoteRejectedActivity.php new file mode 100644 index 0000000000..ba511a2966 --- /dev/null +++ b/app/Listeners/Quote/QuoteRejectedActivity.php @@ -0,0 +1,61 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $fields = new stdClass(); + + $user_id = isset($event->event_vars['user_id']) ? $event->event_vars['user_id'] : $event->quote->user_id; + + $fields->user_id = $user_id; + $fields->quote_id = $event->quote->id; + $fields->client_id = $event->quote->client_id; + $fields->client_contact_id = $event->contact->id; + $fields->company_id = $event->quote->company_id; + $fields->activity_type_id = Activity::QUOTE_REJECTED; + $fields->notes = $event->notes ?? ''; + + $this->activity_repo->save($fields, $event->quote, $event->event_vars); + } +} diff --git a/app/Listeners/Quote/QuoteRejectedNotification.php b/app/Listeners/Quote/QuoteRejectedNotification.php new file mode 100644 index 0000000000..bec9e481ed --- /dev/null +++ b/app/Listeners/Quote/QuoteRejectedNotification.php @@ -0,0 +1,80 @@ +company->db); + + $first_notification_sent = true; + + $quote = $event->quote; + + + /* We loop through each user and determine whether they need to be notified */ + foreach ($event->company->company_users as $company_user) { + /* The User */ + $user = $company_user->user; + + if (! $user) { + continue; + } + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($quote->invitations()->first(), $company_user, 'quote', ['all_notifications', 'quote_rejected', 'quote_rejected_all', 'quote_rejected_user']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + $nmo = new NinjaMailerObject(); + $nmo->mailable = new NinjaMailer((new QuoteRejectedObject($quote, $event->company, $company_user->portalType(), $event->notes))->build()); + $nmo->company = $quote->company; + $nmo->settings = $quote->company->settings; + + $nmo->to_user = $user; + + (new NinjaMailerJob($nmo))->handle(); + + $nmo = null; + + /* This prevents more than one notification being sent */ + $first_notification_sent = false; + } + } + } +} diff --git a/app/Mail/Admin/QuoteRejectedObject.php b/app/Mail/Admin/QuoteRejectedObject.php new file mode 100644 index 0000000000..f008e49df6 --- /dev/null +++ b/app/Mail/Admin/QuoteRejectedObject.php @@ -0,0 +1,101 @@ +company->db); + + if (! $this->quote) { + return; + } + + App::forgetInstance('translator'); + /* Init a new copy of the translator*/ + $t = app('translator'); + /* Set the locale*/ + App::setLocale($this->company->getLocale()); + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->company->settings)); + + $mail_obj = new stdClass(); + $mail_obj->amount = $this->getAmount(); + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + $mail_obj->text_view = 'email.template.text'; + + return $mail_obj; + } + + private function getAmount() + { + return Number::formatMoney($this->quote->amount, $this->quote->client); + } + + private function getSubject() + { + return + ctrans( + 'texts.notification_quote_rejected_subject', + [ + 'client' => $this->quote->client->present()->name(), + 'quote' => $this->quote->number, + ] + ); + } + + private function getData() + { + $settings = $this->quote->client->getMergedSettings(); + $content = ctrans( + 'texts.notification_quote_rejected', + [ + 'amount' => $this->getAmount(), + 'client' => $this->quote->client->present()->name(), + 'quote' => $this->quote->number, + 'notes' => $this->notes, + ] + ); + + $data = [ + 'title' => $this->getSubject(), + 'content' => $content, + 'url' => $this->quote->invitations->first()->getAdminLink($this->use_react_url), + 'button' => ctrans('texts.view_quote'), + 'signature' => $settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + 'text_body' => $content, + 'template' => $this->company->account->isPremium() ? 'email.template.admin_premium' : 'email.template.admin', + ]; + + return $data; + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php index c6f9ec5be2..edd8bdd192 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -296,6 +296,8 @@ class Activity extends StaticModel public const VERIFACTU_CANCELLATION_SENT_FAILURE = 157; + public const QUOTE_REJECTED = 158; + protected $casts = [ 'is_system' => 'boolean', 'updated_at' => 'timestamp', diff --git a/app/Models/Quote.php b/app/Models/Quote.php index a2a10aed3f..73b7b29677 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -213,6 +213,8 @@ class Quote extends BaseModel public const STATUS_CONVERTED = 4; + public const STATUS_REJECTED = 5; + public const STATUS_EXPIRED = -1; public function toSearchableArray() diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 803a4a0b56..457510fddd 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -74,6 +74,7 @@ use App\Events\Credit\CreditWasViewed; use App\Events\Invoice\InvoiceWasPaid; use App\Events\Quote\QuoteWasApproved; use App\Events\Quote\QuoteWasArchived; +use App\Events\Quote\QuoteWasRejected; use App\Events\Quote\QuoteWasRestored; use App\Events\Vendor\VendorWasMerged; use App\Listeners\LogResponseReceived; @@ -162,6 +163,7 @@ use App\Listeners\Invoice\InvoicePaidActivity; use App\Listeners\Payment\PaymentNotification; use App\Listeners\Quote\QuoteApprovedActivity; use App\Listeners\Quote\QuoteArchivedActivity; +use App\Listeners\Quote\QuoteRejectedActivity; use App\Listeners\Quote\QuoteRestoredActivity; use App\Listeners\Quote\ReachWorkflowSettings; use App\Events\Company\CompanyDocumentsDeleted; @@ -570,6 +572,9 @@ class EventServiceProvider extends ServiceProvider CreatedQuoteActivity::class, QuoteCreatedNotification::class, ], + QuoteWasRejected::class => [ + QuoteRejectedActivity::class, + ], QuoteWasUpdated::class => [ QuoteUpdatedActivity::class, ], diff --git a/lang/en/texts.php b/lang/en/texts.php index 7c524acb58..f329b7e94f 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5644,10 +5644,6 @@ $lang = array( 'verifactu_cancellation_send_success' => 'Invoice cancellation for :invoice sent to AEAT successfully', 'verifactu_cancellation_send_failure' => 'Invoice cancellation for :invoice failed to send to AEAT :notes', 'verifactu' => 'Verifactu', - 'activity_150' => 'E-Invoice :invoice for :client sent to AEAT successfully', - 'activity_151' => 'E-Invoice :invoice for :client failed to send to AEAT :notes', - 'activity_152' => 'Invoice cancellation for :invoice sent to AEAT successfully', - 'activity_153' => 'Invoice cancellation for :invoice failed to send to AEAT :notes', 'justify' => 'Justify', 'outdent' => 'Outdent', 'indent' => 'Indent', @@ -5669,6 +5665,17 @@ $lang = array( 'enable_e_invoice_received_notification' => 'Enable E-Invoice Received Notification', 'enable_e_invoice_received_notification_help' => 'Receive an email notification when a new E-Invoice is received.', 'price_changes' => 'Plan Price Changes from January 1st 2026', + 'notification_quote_rejected_subject' => 'Quote :quote was rejected by :client', + 'notification_quote_rejected' => 'The following client :client rejected Quote :quote for :amount :notes.', + 'activity_150' => 'Account was deleted :notes', + 'activity_151' => 'Client :notes was merged into :client by :user', + 'activity_152' => 'Vendor :notes was merged into :vendor by :user', + 'activity_153' => 'Client :notes was purged by :user', + 'activity_154' => 'E-Invoice :invoice for :client sent to AEAT successfully', + 'activity_155' => 'E-Invoice :invoice for :client failed to send to AEAT :notes', + 'activity_156' => 'Invoice cancellation for :invoice sent to AEAT successfully', + 'activity_157' => 'Invoice cancellation for :invoice failed to send to AEAT :notes', + 'activity_158' => 'Quote :quote was rejected by :client :notes', ); return $lang; From b689e8b6c265f9a5564957a0451fccd9687f594d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 Dec 2025 09:01:04 +1100 Subject: [PATCH 035/177] Updates for rejection --- .../ClientPortal/QuoteController.php | 42 +++++++ .../Quotes/ProcessQuotesInBulkRequest.php | 2 - app/Models/Quote.php | 13 +++ app/Providers/EventServiceProvider.php | 2 + app/Services/Quote/QuoteService.php | 19 +++ lang/en/texts.php | 8 +- .../{app-55cdafc9.css => app-2350ca5d.css} | 2 +- public/build/assets/app-aa93be80.js | 109 ------------------ public/build/assets/app-e5ec2fdc.js | 104 +++++++++++++++++ public/build/assets/reject-dae85928.js | 9 ++ public/build/manifest.json | 9 +- resources/js/clients/quotes/reject.js | 82 +++++++++++++ .../quotes/includes/actions.blade.php | 32 +++-- .../quotes/includes/reject-input.blade.php | 49 ++++++++ .../portal/ninja2020/quotes/index.blade.php | 4 + .../portal/ninja2020/quotes/reject.blade.php | 90 +++++++++++++++ .../portal/ninja2020/quotes/show.blade.php | 31 +++-- vite.config.ts | 1 + 18 files changed, 475 insertions(+), 133 deletions(-) rename public/build/assets/{app-55cdafc9.css => app-2350ca5d.css} (82%) delete mode 100644 public/build/assets/app-aa93be80.js create mode 100644 public/build/assets/app-e5ec2fdc.js create mode 100644 public/build/assets/reject-dae85928.js create mode 100644 resources/js/clients/quotes/reject.js create mode 100644 resources/views/portal/ninja2020/quotes/includes/reject-input.blade.php create mode 100644 resources/views/portal/ninja2020/quotes/reject.blade.php diff --git a/app/Http/Controllers/ClientPortal/QuoteController.php b/app/Http/Controllers/ClientPortal/QuoteController.php index a0b1577604..522c5d5dc2 100644 --- a/app/Http/Controllers/ClientPortal/QuoteController.php +++ b/app/Http/Controllers/ClientPortal/QuoteController.php @@ -90,6 +90,10 @@ class QuoteController extends Controller return $this->approve((array) $transformed_ids, $request->has('process')); } + if ($request->action == 'reject') { + return $this->reject((array) $transformed_ids, $request->has('process')); + } + return back(); } @@ -171,6 +175,44 @@ class QuoteController extends Controller } } + protected function reject(array $ids, $process = false) + { + $quotes = Quote::query() + ->whereIn('id', $ids) + ->where('client_id', auth()->guard('contact')->user()->client_id) + ->where('company_id', auth()->guard('contact')->user()->company_id) + ->where('status_id', Quote::STATUS_SENT) + ->withTrashed() + ->get(); + + if (! $quotes || $quotes->count() == 0) { + return redirect() + ->route('client.quotes.index') + ->with('message', ctrans('texts.quotes_with_status_sent_can_be_rejected')); + } + + if ($process) { + foreach ($quotes as $quote) { + + $quote->service()->reject(auth()->guard('contact')->user(), request()->input('user_input', ''))->save(); + + } + + return redirect() + ->route('client.quotes.index') + ->withSuccess('Quote(s) rejected successfully.'); + } + + + $variables = false; + + return $this->render('quotes.reject', [ + 'quotes' => $quotes, + 'variables' => $variables, + ]); + + } + protected function approve(array $ids, $process = false) { $quotes = Quote::query() diff --git a/app/Http/Requests/ClientPortal/Quotes/ProcessQuotesInBulkRequest.php b/app/Http/Requests/ClientPortal/Quotes/ProcessQuotesInBulkRequest.php index 8bcf99268b..324639895b 100644 --- a/app/Http/Requests/ClientPortal/Quotes/ProcessQuotesInBulkRequest.php +++ b/app/Http/Requests/ClientPortal/Quotes/ProcessQuotesInBulkRequest.php @@ -15,8 +15,6 @@ namespace App\Http\Requests\ClientPortal\Quotes; use App\Http\ViewComposers\PortalComposer; use Illuminate\Foundation\Http\FormRequest; -use function auth; - class ProcessQuotesInBulkRequest extends FormRequest { public function authorize() diff --git a/app/Models/Quote.php b/app/Models/Quote.php index 73b7b29677..94ee5126fa 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -384,6 +384,8 @@ class Quote extends BaseModel return '
'.ctrans('texts.expired').'
'; case self::STATUS_CONVERTED: return '
'.ctrans('texts.converted').'
'; + case self::STATUS_REJECTED: + return '
'.ctrans('texts.rejected').'
'; default: return '
'.ctrans('texts.draft').'
'; } @@ -402,6 +404,8 @@ class Quote extends BaseModel return ctrans('texts.expired'); case self::STATUS_CONVERTED: return ctrans('texts.converted'); + case self::STATUS_REJECTED: + return ctrans('texts.rejected'); default: return ctrans('texts.draft'); @@ -422,6 +426,15 @@ class Quote extends BaseModel return false; } + public function isRejected(): bool + { + if ($this->status_id === $this::STATUS_REJECTED) { + return true; + } + + return false; + } + public function getValidUntilAttribute() { return $this->due_date; diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 457510fddd..e25a4461a5 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -223,6 +223,7 @@ use App\Listeners\Invoice\InvoiceRestoredActivity; use App\Listeners\Invoice\InvoiceReversedActivity; use App\Listeners\Payment\PaymentRestoredActivity; use App\Listeners\Quote\QuoteApprovedNotification; +use App\Listeners\Quote\QuoteRejectedNotification; use SocialiteProviders\Apple\AppleExtendSocialite; use SocialiteProviders\Manager\SocialiteWasCalled; use App\Events\Subscription\SubscriptionWasCreated; @@ -574,6 +575,7 @@ class EventServiceProvider extends ServiceProvider ], QuoteWasRejected::class => [ QuoteRejectedActivity::class, + QuoteRejectedNotification::class, ], QuoteWasUpdated::class => [ QuoteUpdatedActivity::class, diff --git a/app/Services/Quote/QuoteService.php b/app/Services/Quote/QuoteService.php index b3fc08515f..1e38fcfcb6 100644 --- a/app/Services/Quote/QuoteService.php +++ b/app/Services/Quote/QuoteService.php @@ -19,6 +19,7 @@ use App\Utils\Traits\MakesHash; use App\Exceptions\QuoteConversion; use App\Repositories\QuoteRepository; use App\Events\Quote\QuoteWasApproved; +use App\Events\Quote\QuoteWasRejected; use App\Services\Invoice\LocationData; use App\Services\Quote\UpdateReminder; use App\Jobs\EDocument\CreateEDocument; @@ -148,6 +149,24 @@ class QuoteService } + public function reject($contact = null, ?string $notes = null): self + { + + if($this->quote->status_id != Quote::STATUS_SENT) { + return $this; + } + + $this->setStatus(Quote::STATUS_REJECTED)->save(); + + if (! $contact) { + $contact = $this->quote->invitations->first()->contact; + } + + event(new QuoteWasRejected($contact, $this->quote, $this->quote->company, $notes ?? '', Ninja::eventVars())); + + return $this; + } + public function approveWithNoCoversion($contact = null): self { diff --git a/lang/en/texts.php b/lang/en/texts.php index f329b7e94f..e632ddfd0e 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5635,7 +5635,6 @@ $lang = array( 'einvoice_received_subject' => 'E-Invoice/s Received', 'einvoice_received_body' => 'You have received :count new E-Invoice/s.

Login to view.', 'download_files_too_large' => 'Some files were too large to attach directly to the email. Please use the links below to download these individually.', - 'restore_disabled_verifactu' => 'You cannot restore an invoice once it has been deleted', 'delete_disabled_verifactu' => 'You cannot delete an invoice once it has been cancelled or modified', 'rectify' => 'Rectificar', @@ -5676,6 +5675,13 @@ $lang = array( 'activity_156' => 'Invoice cancellation for :invoice sent to AEAT successfully', 'activity_157' => 'Invoice cancellation for :invoice failed to send to AEAT :notes', 'activity_158' => 'Quote :quote was rejected by :client :notes', + 'quotes_with_status_sent_can_be_rejected' => 'Only quotes with "Sent" status can be rejected.', + 'reject' => 'Reject', + 'rejected' => 'Rejected', + 'reject_quote' => 'Reject Quote', + 'reject_quote_confirmation' => 'Are you sure you want to reject this quote?', + 'reason' => 'Reason', + 'enter_reason' => 'Enter a reason...', ); return $lang; diff --git a/public/build/assets/app-55cdafc9.css b/public/build/assets/app-2350ca5d.css similarity index 82% rename from public/build/assets/app-55cdafc9.css rename to public/build/assets/app-2350ca5d.css index a4c5823f8d..92c7df7eb9 100644 --- a/public/build/assets/app-55cdafc9.css +++ b/public/build/assets/app-2350ca5d.css @@ -1 +1 @@ -*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Open Sans,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[type=text],input:where(:not([type])),[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}[type=text]:focus,input:where(:not([type])):focus,[type=email]:focus,[type=url]:focus,[type=password]:focus,[type=number]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=week]:focus,[multiple]:focus,textarea:focus,select:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size="1"])){background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow: 0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}@media (forced-colors: active){[type=checkbox]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=radio]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}@media (forced-colors: active){[type=radio]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:checked:hover,[type=checkbox]:checked:focus,[type=radio]:checked:hover,[type=radio]:checked:focus{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}@media (forced-colors: active){[type=checkbox]:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:indeterminate:hover,[type=checkbox]:indeterminate:focus{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.form-input,.form-textarea,.form-select,.form-multiselect{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}.form-input:focus,.form-textarea:focus,.form-select:focus,.form-multiselect:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}.form-select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}.form-select:where([size]:not([size="1"])){background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}.form-checkbox,.form-radio{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow: 0 0 #0000}.form-checkbox{border-radius:0}.form-radio{border-radius:100%}.form-checkbox:focus,.form-radio:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.form-checkbox:checked,.form-radio:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}.form-checkbox:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}@media (forced-colors: active){.form-checkbox:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}.form-radio:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}@media (forced-colors: active){.form-radio:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}.form-checkbox:checked:hover,.form-checkbox:checked:focus,.form-radio:checked:hover,.form-radio:checked:focus{border-color:transparent;background-color:currentColor}.form-checkbox:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}@media (forced-colors: active){.form-checkbox:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}.form-checkbox:indeterminate:hover,.form-checkbox:indeterminate:focus{border-color:transparent;background-color:currentColor}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding-top:.8571429em;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body: #374151;--tw-prose-headings: #111827;--tw-prose-lead: #4b5563;--tw-prose-links: #111827;--tw-prose-bold: #111827;--tw-prose-counters: #6b7280;--tw-prose-bullets: #d1d5db;--tw-prose-hr: #e5e7eb;--tw-prose-quotes: #111827;--tw-prose-quote-borders: #e5e7eb;--tw-prose-captions: #6b7280;--tw-prose-kbd: #111827;--tw-prose-kbd-shadows: 17 24 39;--tw-prose-code: #111827;--tw-prose-pre-code: #e5e7eb;--tw-prose-pre-bg: #1f2937;--tw-prose-th-borders: #d1d5db;--tw-prose-td-borders: #e5e7eb;--tw-prose-invert-body: #d1d5db;--tw-prose-invert-headings: #fff;--tw-prose-invert-lead: #9ca3af;--tw-prose-invert-links: #fff;--tw-prose-invert-bold: #fff;--tw-prose-invert-counters: #9ca3af;--tw-prose-invert-bullets: #4b5563;--tw-prose-invert-hr: #374151;--tw-prose-invert-quotes: #f3f4f6;--tw-prose-invert-quote-borders: #374151;--tw-prose-invert-captions: #9ca3af;--tw-prose-invert-kbd: #fff;--tw-prose-invert-kbd-shadows: 255 255 255;--tw-prose-invert-code: #fff;--tw-prose-invert-pre-code: #d1d5db;--tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);--tw-prose-invert-th-borders: #4b5563;--tw-prose-invert-td-borders: #374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-sm{font-size:.875rem;line-height:1.7142857}.prose-sm :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em}.prose-sm :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.2857143em;line-height:1.5555556;margin-top:.8888889em;margin-bottom:.8888889em}.prose-sm :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.3333333em;margin-bottom:1.3333333em;padding-inline-start:1.1111111em}.prose-sm :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:2.1428571em;margin-top:0;margin-bottom:.8em;line-height:1.2}.prose-sm :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.4285714em;margin-top:1.6em;margin-bottom:.8em;line-height:1.4}.prose-sm :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.2857143em;margin-top:1.5555556em;margin-bottom:.4444444em;line-height:1.5555556}.prose-sm :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.4285714em;margin-bottom:.5714286em;line-height:1.4285714}.prose-sm :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;border-radius:.3125rem;padding-top:.1428571em;padding-inline-end:.3571429em;padding-bottom:.1428571em;padding-inline-start:.3571429em}.prose-sm :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em}.prose-sm :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.9em}.prose-sm :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8888889em}.prose-sm :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.6666667;margin-top:1.6666667em;margin-bottom:1.6666667em;border-radius:.25rem;padding-top:.6666667em;padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em;padding-inline-start:1.5714286em}.prose-sm :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em;padding-inline-start:1.5714286em}.prose-sm :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.2857143em;margin-bottom:.2857143em}.prose-sm :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.4285714em}.prose-sm :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.4285714em}.prose-sm :where(.prose-sm>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(.prose-sm>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose-sm>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em}.prose-sm :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.2857143em;padding-inline-start:1.5714286em}.prose-sm :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2.8571429em;margin-bottom:2.8571429em}.prose-sm :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.5}.prose-sm :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose-sm :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose-sm :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.6666667em;padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose-sm :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose-sm :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.3333333;margin-top:.6666667em}.prose-sm :where(.prose-sm>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(.prose-sm>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.button{border-radius:.25rem;padding:.75rem 1rem;font-size:.875rem;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}button:disabled{cursor:not-allowed;opacity:.5}.button-primary{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.button-primary:hover{font-weight:600}.button-block{display:block;width:100%}.button-danger{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.button-danger:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.button-secondary{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.button-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.button-link{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.button-link:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity));text-decoration-line:underline}.button-link:focus{text-decoration-line:underline;outline:2px solid transparent;outline-offset:2px}.validation{margin-top:.5rem;margin-bottom:.25rem;border-left-width:2px;--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));padding:.25rem .75rem}.validation-fail{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity));font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.validation-pass{--tw-border-opacity: 1;border-color:rgb(16 185 129 / var(--tw-border-opacity));font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.input{margin-top:.5rem;align-items:center;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem}.input:focus{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity));outline:2px solid transparent;outline-offset:2px}.input-label{font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.input-slim{padding-top:.5rem;padding-bottom:.5rem}.form-checkbox{cursor:pointer;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.form-select{border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.alert{margin-top:.5rem;margin-bottom:.25rem;border-left-width:2px;--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem}.alert-success{--tw-border-opacity: 1;border-color:rgb(16 185 129 / var(--tw-border-opacity))}.alert-failure{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .625rem;font-size:.75rem;font-weight:500;line-height:1rem}.badge-light{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.badge-primary{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.badge-danger{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.badge-success{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity))}.badge-secondary{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.badge-warning{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity))}.badge-info{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}@media (min-width: 640px){.dataTables_length{margin-top:1.25rem!important;margin-bottom:1.25rem!important}}@media (min-width: 1024px){.dataTables_length{margin-top:1rem!important;margin-bottom:1rem!important}}.dataTables_length select{margin-left:.5rem!important;margin-right:.5rem!important;--tw-bg-opacity: 1 !important;background-color:rgb(255 255 255 / var(--tw-bg-opacity))!important;margin-top:.5rem;align-items:center;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem}.dataTables_length select:focus{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity));outline:2px solid transparent;outline-offset:2px}.dataTables_filter{margin-bottom:1rem}.dataTables_filter input{margin-top:.5rem;align-items:center;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem}.dataTables_filter input:focus{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity));outline:2px solid transparent;outline-offset:2px}@media (min-width: 1024px){.dataTables_filter{margin-top:-3rem!important}}.dataTables_paginate{padding-bottom:1.5rem!important;padding-top:.5rem!important}.dataTables_paginate .paginate_button{margin-right:.25rem!important;cursor:pointer!important;border-width:1px!important;--tw-border-opacity: 1 !important;border-color:rgb(209 213 219 / var(--tw-border-opacity))!important;--tw-bg-opacity: 1 !important;background-color:rgb(255 255 255 / var(--tw-bg-opacity))!important;font-weight:500!important;--tw-text-opacity: 1 !important;color:rgb(55 65 81 / var(--tw-text-opacity))!important;border-radius:.25rem;padding:.75rem 1rem;font-size:.875rem;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dataTables_paginate .current{--tw-bg-opacity: 1 !important;background-color:rgb(37 99 235 / var(--tw-bg-opacity))!important;--tw-text-opacity: 1 !important;color:rgb(255 255 255 / var(--tw-text-opacity))!important}.dataTables_info{font-size:.875rem!important;line-height:1.25rem!important}.dataTables_empty{padding-top:1rem!important;padding-bottom:1rem!important}.pagination{display:flex!important;align-items:center!important}.pagination .page-link{margin-top:-1px!important;display:inline-flex!important;cursor:pointer!important;align-items:center!important;border-top-width:2px!important;border-color:transparent!important;padding-left:1rem!important;padding-right:1rem!important;padding-top:1rem!important;font-size:.875rem!important;font-weight:500!important;line-height:1.25rem!important;--tw-text-opacity: 1 !important;color:rgb(107 114 128 / var(--tw-text-opacity))!important;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter!important;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter!important;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter!important;transition-duration:.15s!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important}.pagination .page-link:hover{--tw-border-opacity: 1 !important;border-color:rgb(209 213 219 / var(--tw-border-opacity))!important;--tw-text-opacity: 1 !important;color:rgb(55 65 81 / var(--tw-text-opacity))!important}.pagination .page-link:focus{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity));--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity));outline:2px solid transparent;outline-offset:2px}.pagination .active>span{--tw-border-opacity: 1 !important;border-color:rgb(37 99 235 / var(--tw-border-opacity))!important;--tw-text-opacity: 1 !important;color:rgb(37 99 235 / var(--tw-text-opacity))!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1{left:.25rem}.right-0{right:0}.top-0{top:0}.top-1{top:.25rem}.z-0{z-index:0}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.col-auto{grid-column:auto}.col-span-1{grid-column:span 1 / span 1}.col-span-12{grid-column:span 12 / span 12}.col-span-2{grid-column:span 2 / span 2}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-6{grid-column:span 6 / span 6}.col-span-8{grid-column:span 8 / span 8}.float-right{float:right}.m-0{margin:0}.m-auto{margin:auto}.-my-2{margin-top:-.5rem;margin-bottom:-.5rem}.-my-6{margin-top:-1.5rem;margin-bottom:-1.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.mx-\[22px\]{margin-left:22px;margin-right:22px}.mx-\[40px\]{margin-left:40px;margin-right:40px}.mx-\[auto\],.mx-auto{margin-left:auto;margin-right:auto}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.-ml-1{margin-left:-.25rem}.-ml-4{margin-left:-1rem}.-ml-px{margin-left:-1px}.-mr-1{margin-right:-.25rem}.-mr-14{margin-right:-3.5rem}.-mt-4{margin-top:-1rem}.-mt-6{margin-top:-1.5rem}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mb-\[10px\]{margin-bottom:10px}.mb-\[11px\]{margin-bottom:11px}.mb-\[20px\]{margin-bottom:20px}.mb-\[25px\]{margin-bottom:25px}.mb-\[26px\]{margin-bottom:26px}.mb-\[36px\]{margin-bottom:36px}.mb-\[40px\]{margin-bottom:40px}.mb-\[5px\]{margin-bottom:5px}.ml-0{margin-left:0}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.ml-\[10px\]{margin-left:10px}.mr-0{margin-right:0}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-5{margin-right:1.25rem}.mr-6{margin-right:1.5rem}.mt-0{margin-top:0}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[30px\]{margin-top:30px}.mt-\[50px\]{margin-top:50px}.mt-\[auto\]{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-0{height:0px}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-\[1px\]{height:1px}.h-\[40px\]{height:40px}.h-auto{height:auto}.h-fit{height:-moz-fit-content;height:fit-content}.h-full{height:100%}.h-screen{height:100vh}.min-h-32{min-height:8rem}.min-h-\[450px\]{min-height:450px}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-1{width:.25rem}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-1\/6{width:16.666667%}.w-10{width:2.5rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-4{width:1rem}.w-4\/5{width:80%}.w-4\/6{width:66.666667%}.w-48{width:12rem}.w-5{width:1.25rem}.w-5\/6{width:83.333333%}.w-56{width:14rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[87px\]{width:87px}.w-auto{width:auto}.w-full{width:100%}.w-screen{width:100vw}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-\[212px\]{max-width:212px}.max-w-\[350px\]{max-width:350px}.max-w-\[450px\]{max-width:450px}.max-w-\[625px\]{max-width:625px}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-1\/2{flex-basis:50%}.basis-full{flex-basis:100%}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.origin-top-right{transform-origin:top right}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-4{--tw-translate-y: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-content-end{place-content:end}.place-items-center{place-items:center}.content-center{align-content:center}.content-start{align-content:flex-start}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-\[13px\]{gap:13px}.gap-\[44px\]{gap:44px}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-\[20px\]{row-gap:20px}.space-x-0>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(0px * var(--tw-space-x-reverse));margin-left:calc(0px * calc(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-\[10px\]{border-radius:10px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-sm{border-radius:.125rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l-md{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-t-4{border-top-width:4px}.border-t-\[0px\]{border-top-width:0px}.border-t-\[10px\]{border-top-width:10px}.border-t-\[1px\]{border-top-width:1px}.border-solid{border-style:solid}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-\[\#E5E7EB\]{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.border-emerald-500{--tw-border-opacity: 1;border-color:rgb(16 185 129 / var(--tw-border-opacity))}.border-fuchsia-600{--tw-border-opacity: 1;border-color:rgb(192 38 211 / var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity))}.border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.border-red-300{--tw-border-opacity: 1;border-color:rgb(252 165 165 / var(--tw-border-opacity))}.border-red-400{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.border-red-900{--tw-border-opacity: 1;border-color:rgb(127 29 29 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-opacity-50{--tw-border-opacity: .5}.bg-\[\#F2F9FE\]{--tw-bg-opacity: 1;background-color:rgb(242 249 254 / var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity))}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-opacity-100{--tw-bg-opacity: 1}.bg-opacity-75{--tw-bg-opacity: .75}.bg-clip-padding{background-clip:padding-box}.fill-current{fill:currentColor}.object-cover{-o-object-fit:cover;object-fit:cover}.object-scale-down{-o-object-fit:scale-down;object-fit:scale-down}.object-center{-o-object-position:center;object-position:center}.p-1{padding:.25rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.p-\[12px\]{padding:12px}.p-\[20px\]{padding:20px}.px-0{padding-left:0;padding-right:0}.px-1{padding-left:.25rem;padding-right:.25rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-\[12px\]{padding-left:12px;padding-right:12px}.px-\[20px\]{padding-left:20px;padding-right:20px}.px-\[22px\]{padding-left:22px;padding-right:22px}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-\[33px\]{padding-top:33px;padding-bottom:33px}.py-\[36px\]{padding-top:36px;padding-bottom:36px}.py-\[9\.5px\]{padding-top:9.5px;padding-bottom:9.5px}.pb-10{padding-bottom:2.5rem}.pb-2{padding-bottom:.5rem}.pb-20{padding-bottom:5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pb-\[20px\]{padding-bottom:20px}.pb-\[56px\]{padding-bottom:56px}.pb-\[58px\]{padding-bottom:58px}.pl-0{padding-left:0}.pl-1\.5{padding-left:.375rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-\[18px\]{padding-left:18px}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pr-\[18px\]{padding-right:18px}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-6{padding-top:1.5rem}.pt-\[20px\]{padding-top:20px}.pt-\[29px\]{padding-top:29px}.pt-\[35px\]{padding-top:35px}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.font-\[\'Open_Sans\'\]{font-family:Open Sans}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[12px\]{font-size:12px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[22px\]{font-size:22px}.text-\[24px\]{font-size:24px}.text-\[35px\]{font-size:35px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-\[16px\]{font-weight:16px}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-\[1\.2rem\]{line-height:1.2rem}.leading-\[1\.35em\]{line-height:1.35em}.leading-\[1\.36em\]{line-height:1.36em}.leading-\[1\.375em\]{line-height:1.375em}.leading-\[1\.3em\]{line-height:1.3em}.leading-\[1\.5em\]{line-height:1.5em}.leading-\[1\.75em\]{line-height:1.75em}.leading-normal{line-height:1.5}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#212529\]{--tw-text-opacity: 1;color:rgb(33 37 41 / var(--tw-text-opacity))}.text-\[\#6C727F\]{--tw-text-opacity: 1;color:rgb(108 114 127 / var(--tw-text-opacity))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-black{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity: .05}.grayscale{--tw-grayscale: grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-1000{transition-duration:1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-linear{transition-timing-function:linear}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.placeholder\:text-gray-500::-moz-placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.placeholder\:text-gray-500::placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:left-\[8px\]:after{content:var(--tw-content);left:8px}.after\:top-\[5px\]:after{content:var(--tw-content);top:5px}.after\:h-\[30px\]:after{content:var(--tw-content);height:30px}.after\:w-\[30px\]:after{content:var(--tw-content);width:30px}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content: "";content:var(--tw-content)}.focus-within\:z-10:focus-within{z-index:10}.hover\:list-disc:hover{list-style-type:disc}.hover\:border-blue-600:hover{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}.hover\:border-gray-600:hover{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.hover\:border-gray-800:hover{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity))}.hover\:border-transparent:hover{border-color:transparent}.hover\:bg-blue-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.hover\:bg-red-900:hover{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.hover\:font-semibold:hover{font-weight:600}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.hover\:text-indigo-900:hover{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-2xl:hover{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-blue-300:focus{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity))}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.focus\:border-fuchsia-300:focus{--tw-border-opacity: 1;border-color:rgb(240 171 252 / var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.focus\:border-red-500:focus{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.focus\:bg-gray-100:focus{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.focus\:bg-gray-600:focus{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}.focus\:bg-white:focus{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.focus\:font-semibold:focus{font-weight:600}.focus\:text-gray-600:focus{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.focus\:text-gray-900:focus{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.focus\:underline:focus{text-decoration-line:underline}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-0:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity))}.focus\:ring-gray-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\:ring-indigo-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity))}.focus\:ring-red-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity))}.focus\:ring-opacity-50:focus{--tw-ring-opacity: .5}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.active\:bg-gray-50:active{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.active\:text-gray-800:active{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.active\:outline-none:active{outline:2px solid transparent;outline-offset:2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-50:disabled{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-75:disabled{opacity:.75}.group:hover .group-hover\:border-transparent{border-color:transparent}.group:hover .group-hover\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.group:hover .group-hover\:opacity-100{opacity:1}.peer:checked~.peer-checked\:text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.peer:checked~.peer-checked\:after\:translate-x-\[140\%\]:after{content:var(--tw-content);--tw-translate-x: 140%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.peer:focus~.peer-focus\:outline-none{outline:2px solid transparent;outline-offset:2px}@media (min-width: 640px){.sm\:inset-0{top:0;right:0;bottom:0;left:0}.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:col-span-3{grid-column:span 3 / span 3}.sm\:col-span-4{grid-column:span 4 / span 4}.sm\:col-span-6{grid-column:span 6 / span 6}.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:mx-0{margin-left:0;margin-right:0}.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:ml-3{margin-left:.75rem}.sm\:ml-4{margin-left:1rem}.sm\:ml-6{margin-left:1.5rem}.sm\:mt-0{margin-top:0}.sm\:mt-4{margin-top:1rem}.sm\:mt-6{margin-top:1.5rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:h-10{height:2.5rem}.sm\:h-screen{height:100vh}.sm\:w-10{width:2.5rem}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-4xl{max-width:56rem}.sm\:max-w-lg{max-width:32rem}.sm\:max-w-sm{max-width:24rem}.sm\:flex-shrink-0{flex-shrink:0}.sm\:translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:flex-nowrap{flex-wrap:nowrap}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:1rem}.sm\:rounded-lg{border-radius:.5rem}.sm\:p-0{padding:0}.sm\:p-6{padding:1.5rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pb-4{padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:col-span-2{grid-column:span 2 / span 2}.md\:col-span-4{grid-column:span 4 / span 4}.md\:col-span-5{grid-column:span 5 / span 5}.md\:col-span-6{grid-column:span 6 / span 6}.md\:col-start-2{grid-column-start:2}.md\:col-start-4{grid-column-start:4}.md\:mx-0,.md\:mx-\[0\]{margin-left:0;margin-right:0}.md\:-mr-1{margin-right:-.25rem}.md\:mb-6{margin-bottom:1.5rem}.md\:mb-\[46px\]{margin-bottom:46px}.md\:ml-2{margin-left:.5rem}.md\:ml-6{margin-left:1.5rem}.md\:mr-0{margin-right:0}.md\:mr-2{margin-right:.5rem}.md\:mt-0{margin-top:0}.md\:mt-10{margin-top:2.5rem}.md\:mt-5{margin-top:1.25rem}.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:min-h-\[411px\]{min-height:411px}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.333333%}.md\:max-w-3xl{max-width:48rem}.md\:max-w-xl{max-width:36rem}.md\:flex-shrink-0{flex-shrink:0}.md\:shrink{flex-shrink:1}.md\:grow-0{flex-grow:0}.md\:basis-1\/2{flex-basis:50%}.md\:basis-\[449px\]{flex-basis:449px}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:items-center{align-items:center}.md\:justify-center{justify-content:center}.md\:justify-between{justify-content:space-between}.md\:gap-6{gap:1.5rem}.md\:gap-x-\[21px\]{-moz-column-gap:21px;column-gap:21px}.md\:gap-y-6{row-gap:1.5rem}.md\:border-r{border-right-width:1px}.md\:border-\[\#E5E7EB\]{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.md\:p-24{padding:6rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:px-\[40px\]{padding-left:40px;padding-right:40px}.md\:pb-\[40px\]{padding-bottom:40px}.md\:pl-4{padding-left:1rem}.md\:pl-\[52px\]{padding-left:52px}.md\:pl-\[61px\]{padding-left:61px}.md\:pr-\[20px\]{padding-right:20px}.md\:pr-\[48px\]{padding-right:48px}.md\:pt-0{padding-top:0}.md\:pt-\[58px\]{padding-top:58px}.md\:text-left{text-align:left}.md\:text-center{text-align:center}.md\:text-2xl{font-size:1.5rem;line-height:2rem}.md\:text-\[30px\]{font-size:30px}.md\:text-\[32px\]{font-size:32px}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 1024px){.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-6{grid-column:span 6 / span 6}.lg\:col-span-7{grid-column:span 7 / span 7}.lg\:col-span-8{grid-column:span 8 / span 8}.lg\:col-start-3{grid-column-start:3}.lg\:col-start-4{grid-column-start:4}.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:-mb-1{margin-bottom:-.25rem}.lg\:-ml-5{margin-left:-1.25rem}.lg\:mt-24{margin-top:6rem}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:hidden{display:none}.lg\:h-screen{height:100vh}.lg\:w-1\/2{width:50%}.lg\:w-1\/3{width:33.333333%}.lg\:w-1\/4{width:25%}.lg\:w-1\/5{width:20%}.lg\:max-w-\[80\%\]{max-width:80%}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:items-center{align-items:center}.lg\:gap-4{gap:1rem}.lg\:rounded-lg{border-radius:.5rem}.lg\:px-16{padding-left:4rem;padding-right:4rem}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:px-4{padding-left:1rem;padding-right:1rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-2{padding-top:.5rem;padding-bottom:.5rem}}@media (min-width: 1280px){.xl\:col-span-4{grid-column:span 4 / span 4}.xl\:col-span-6{grid-column:span 6 / span 6}.xl\:col-span-8{grid-column:span 8 / span 8}.xl\:col-span-9{grid-column:span 9 / span 9}.xl\:col-start-4{grid-column-start:4}.xl\:ml-5{margin-left:1.25rem}.xl\:mt-0{margin-top:0}.xl\:mt-32{margin-top:8rem}.xl\:flex{display:flex}.xl\:w-auto{width:auto}.xl\:basis-auto{flex-basis:auto}.xl\:flex-row{flex-direction:row}.xl\:flex-nowrap{flex-wrap:nowrap}.xl\:justify-center{justify-content:center}.xl\:border-r{border-right-width:1px}.xl\:border-\[\#E5E7EB\]{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.xl\:px-16{padding-left:4rem;padding-right:4rem}.xl\:px-20{padding-left:5rem;padding-right:5rem}.xl\:px-5{padding-left:1.25rem;padding-right:1.25rem}.xl\:pr-20{padding-right:5rem}}@media (prefers-color-scheme: dark){.dark\:border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.dark\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\:placeholder-gray-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity))}.dark\:placeholder-gray-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity))}.dark\:focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.dark\:focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity))}} +*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Open Sans,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[type=text],input:where(:not([type])),[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}[type=text]:focus,input:where(:not([type])):focus,[type=email]:focus,[type=url]:focus,[type=password]:focus,[type=number]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=week]:focus,[multiple]:focus,textarea:focus,select:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size="1"])){background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow: 0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}@media (forced-colors: active){[type=checkbox]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=radio]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}@media (forced-colors: active){[type=radio]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:checked:hover,[type=checkbox]:checked:focus,[type=radio]:checked:hover,[type=radio]:checked:focus{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}@media (forced-colors: active){[type=checkbox]:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:indeterminate:hover,[type=checkbox]:indeterminate:focus{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.form-input,.form-textarea,.form-select,.form-multiselect{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}.form-input:focus,.form-textarea:focus,.form-select:focus,.form-multiselect:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}.form-select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}.form-select:where([size]:not([size="1"])){background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}.form-checkbox,.form-radio{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow: 0 0 #0000}.form-checkbox{border-radius:0}.form-radio{border-radius:100%}.form-checkbox:focus,.form-radio:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.form-checkbox:checked,.form-radio:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}.form-checkbox:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}@media (forced-colors: active){.form-checkbox:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}.form-radio:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}@media (forced-colors: active){.form-radio:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}.form-checkbox:checked:hover,.form-checkbox:checked:focus,.form-radio:checked:hover,.form-radio:checked:focus{border-color:transparent;background-color:currentColor}.form-checkbox:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}@media (forced-colors: active){.form-checkbox:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}.form-checkbox:indeterminate:hover,.form-checkbox:indeterminate:focus{border-color:transparent;background-color:currentColor}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding-top:.8571429em;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body: #374151;--tw-prose-headings: #111827;--tw-prose-lead: #4b5563;--tw-prose-links: #111827;--tw-prose-bold: #111827;--tw-prose-counters: #6b7280;--tw-prose-bullets: #d1d5db;--tw-prose-hr: #e5e7eb;--tw-prose-quotes: #111827;--tw-prose-quote-borders: #e5e7eb;--tw-prose-captions: #6b7280;--tw-prose-kbd: #111827;--tw-prose-kbd-shadows: 17 24 39;--tw-prose-code: #111827;--tw-prose-pre-code: #e5e7eb;--tw-prose-pre-bg: #1f2937;--tw-prose-th-borders: #d1d5db;--tw-prose-td-borders: #e5e7eb;--tw-prose-invert-body: #d1d5db;--tw-prose-invert-headings: #fff;--tw-prose-invert-lead: #9ca3af;--tw-prose-invert-links: #fff;--tw-prose-invert-bold: #fff;--tw-prose-invert-counters: #9ca3af;--tw-prose-invert-bullets: #4b5563;--tw-prose-invert-hr: #374151;--tw-prose-invert-quotes: #f3f4f6;--tw-prose-invert-quote-borders: #374151;--tw-prose-invert-captions: #9ca3af;--tw-prose-invert-kbd: #fff;--tw-prose-invert-kbd-shadows: 255 255 255;--tw-prose-invert-code: #fff;--tw-prose-invert-pre-code: #d1d5db;--tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);--tw-prose-invert-th-borders: #4b5563;--tw-prose-invert-td-borders: #374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-sm{font-size:.875rem;line-height:1.7142857}.prose-sm :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em}.prose-sm :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.2857143em;line-height:1.5555556;margin-top:.8888889em;margin-bottom:.8888889em}.prose-sm :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.3333333em;margin-bottom:1.3333333em;padding-inline-start:1.1111111em}.prose-sm :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:2.1428571em;margin-top:0;margin-bottom:.8em;line-height:1.2}.prose-sm :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.4285714em;margin-top:1.6em;margin-bottom:.8em;line-height:1.4}.prose-sm :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.2857143em;margin-top:1.5555556em;margin-bottom:.4444444em;line-height:1.5555556}.prose-sm :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.4285714em;margin-bottom:.5714286em;line-height:1.4285714}.prose-sm :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;border-radius:.3125rem;padding-top:.1428571em;padding-inline-end:.3571429em;padding-bottom:.1428571em;padding-inline-start:.3571429em}.prose-sm :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em}.prose-sm :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.9em}.prose-sm :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8888889em}.prose-sm :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.6666667;margin-top:1.6666667em;margin-bottom:1.6666667em;border-radius:.25rem;padding-top:.6666667em;padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em;padding-inline-start:1.5714286em}.prose-sm :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em;padding-inline-start:1.5714286em}.prose-sm :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.2857143em;margin-bottom:.2857143em}.prose-sm :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.4285714em}.prose-sm :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.4285714em}.prose-sm :where(.prose-sm>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(.prose-sm>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose-sm>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em}.prose-sm :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.2857143em;padding-inline-start:1.5714286em}.prose-sm :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2.8571429em;margin-bottom:2.8571429em}.prose-sm :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.5}.prose-sm :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose-sm :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose-sm :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.6666667em;padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose-sm :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose-sm :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.3333333;margin-top:.6666667em}.prose-sm :where(.prose-sm>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(.prose-sm>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.button{border-radius:.25rem;padding:.75rem 1rem;font-size:.875rem;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}button:disabled{cursor:not-allowed;opacity:.5}.button-primary{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.button-primary:hover{font-weight:600}.button-block{display:block;width:100%}.button-danger{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.button-danger:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.button-secondary{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.button-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.button-link{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.button-link:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity));text-decoration-line:underline}.button-link:focus{text-decoration-line:underline;outline:2px solid transparent;outline-offset:2px}.validation{margin-top:.5rem;margin-bottom:.25rem;border-left-width:2px;--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));padding:.25rem .75rem}.validation-fail{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity));font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.validation-pass{--tw-border-opacity: 1;border-color:rgb(16 185 129 / var(--tw-border-opacity));font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.input{margin-top:.5rem;align-items:center;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem}.input:focus{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity));outline:2px solid transparent;outline-offset:2px}.input-label{font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.input-slim{padding-top:.5rem;padding-bottom:.5rem}.form-checkbox{cursor:pointer;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.form-select{border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.alert{margin-top:.5rem;margin-bottom:.25rem;border-left-width:2px;--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem}.alert-success{--tw-border-opacity: 1;border-color:rgb(16 185 129 / var(--tw-border-opacity))}.alert-failure{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .625rem;font-size:.75rem;font-weight:500;line-height:1rem}.badge-light{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.badge-primary{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.badge-danger{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.badge-success{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity))}.badge-secondary{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.badge-warning{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity))}.badge-info{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}@media (min-width: 640px){.dataTables_length{margin-top:1.25rem!important;margin-bottom:1.25rem!important}}@media (min-width: 1024px){.dataTables_length{margin-top:1rem!important;margin-bottom:1rem!important}}.dataTables_length select{margin-left:.5rem!important;margin-right:.5rem!important;--tw-bg-opacity: 1 !important;background-color:rgb(255 255 255 / var(--tw-bg-opacity))!important;margin-top:.5rem;align-items:center;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem}.dataTables_length select:focus{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity));outline:2px solid transparent;outline-offset:2px}.dataTables_filter{margin-bottom:1rem}.dataTables_filter input{margin-top:.5rem;align-items:center;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem}.dataTables_filter input:focus{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity));outline:2px solid transparent;outline-offset:2px}@media (min-width: 1024px){.dataTables_filter{margin-top:-3rem!important}}.dataTables_paginate{padding-bottom:1.5rem!important;padding-top:.5rem!important}.dataTables_paginate .paginate_button{margin-right:.25rem!important;cursor:pointer!important;border-width:1px!important;--tw-border-opacity: 1 !important;border-color:rgb(209 213 219 / var(--tw-border-opacity))!important;--tw-bg-opacity: 1 !important;background-color:rgb(255 255 255 / var(--tw-bg-opacity))!important;font-weight:500!important;--tw-text-opacity: 1 !important;color:rgb(55 65 81 / var(--tw-text-opacity))!important;border-radius:.25rem;padding:.75rem 1rem;font-size:.875rem;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dataTables_paginate .current{--tw-bg-opacity: 1 !important;background-color:rgb(37 99 235 / var(--tw-bg-opacity))!important;--tw-text-opacity: 1 !important;color:rgb(255 255 255 / var(--tw-text-opacity))!important}.dataTables_info{font-size:.875rem!important;line-height:1.25rem!important}.dataTables_empty{padding-top:1rem!important;padding-bottom:1rem!important}.pagination{display:flex!important;align-items:center!important}.pagination .page-link{margin-top:-1px!important;display:inline-flex!important;cursor:pointer!important;align-items:center!important;border-top-width:2px!important;border-color:transparent!important;padding-left:1rem!important;padding-right:1rem!important;padding-top:1rem!important;font-size:.875rem!important;font-weight:500!important;line-height:1.25rem!important;--tw-text-opacity: 1 !important;color:rgb(107 114 128 / var(--tw-text-opacity))!important;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter!important;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter!important;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter!important;transition-duration:.15s!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important}.pagination .page-link:hover{--tw-border-opacity: 1 !important;border-color:rgb(209 213 219 / var(--tw-border-opacity))!important;--tw-text-opacity: 1 !important;color:rgb(55 65 81 / var(--tw-text-opacity))!important}.pagination .page-link:focus{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity));--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity));outline:2px solid transparent;outline-offset:2px}.pagination .active>span{--tw-border-opacity: 1 !important;border-color:rgb(37 99 235 / var(--tw-border-opacity))!important;--tw-text-opacity: 1 !important;color:rgb(37 99 235 / var(--tw-text-opacity))!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1{left:.25rem}.right-0{right:0}.top-0{top:0}.top-1{top:.25rem}.z-0{z-index:0}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.col-auto{grid-column:auto}.col-span-1{grid-column:span 1 / span 1}.col-span-12{grid-column:span 12 / span 12}.col-span-2{grid-column:span 2 / span 2}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-6{grid-column:span 6 / span 6}.col-span-8{grid-column:span 8 / span 8}.float-right{float:right}.m-0{margin:0}.m-auto{margin:auto}.-my-2{margin-top:-.5rem;margin-bottom:-.5rem}.-my-6{margin-top:-1.5rem;margin-bottom:-1.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.mx-\[22px\]{margin-left:22px;margin-right:22px}.mx-\[40px\]{margin-left:40px;margin-right:40px}.mx-\[auto\],.mx-auto{margin-left:auto;margin-right:auto}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.-ml-1{margin-left:-.25rem}.-ml-4{margin-left:-1rem}.-ml-px{margin-left:-1px}.-mr-1{margin-right:-.25rem}.-mr-14{margin-right:-3.5rem}.-mt-4{margin-top:-1rem}.-mt-6{margin-top:-1.5rem}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mb-\[10px\]{margin-bottom:10px}.mb-\[11px\]{margin-bottom:11px}.mb-\[20px\]{margin-bottom:20px}.mb-\[25px\]{margin-bottom:25px}.mb-\[26px\]{margin-bottom:26px}.mb-\[36px\]{margin-bottom:36px}.mb-\[40px\]{margin-bottom:40px}.mb-\[5px\]{margin-bottom:5px}.ml-0{margin-left:0}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.ml-\[10px\]{margin-left:10px}.mr-0{margin-right:0}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-5{margin-right:1.25rem}.mr-6{margin-right:1.5rem}.mt-0{margin-top:0}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[30px\]{margin-top:30px}.mt-\[50px\]{margin-top:50px}.mt-\[auto\]{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-0{height:0px}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-\[1px\]{height:1px}.h-\[40px\]{height:40px}.h-auto{height:auto}.h-fit{height:-moz-fit-content;height:fit-content}.h-full{height:100%}.h-screen{height:100vh}.min-h-32{min-height:8rem}.min-h-\[450px\]{min-height:450px}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-1{width:.25rem}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-1\/6{width:16.666667%}.w-10{width:2.5rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-4{width:1rem}.w-4\/5{width:80%}.w-4\/6{width:66.666667%}.w-48{width:12rem}.w-5{width:1.25rem}.w-5\/6{width:83.333333%}.w-56{width:14rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[87px\]{width:87px}.w-auto{width:auto}.w-full{width:100%}.w-screen{width:100vw}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-\[212px\]{max-width:212px}.max-w-\[350px\]{max-width:350px}.max-w-\[450px\]{max-width:450px}.max-w-\[625px\]{max-width:625px}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-1\/2{flex-basis:50%}.basis-full{flex-basis:100%}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.origin-top-right{transform-origin:top right}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-4{--tw-translate-y: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-content-end{place-content:end}.place-items-center{place-items:center}.content-center{align-content:center}.content-start{align-content:flex-start}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-\[13px\]{gap:13px}.gap-\[44px\]{gap:44px}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-\[20px\]{row-gap:20px}.space-x-0>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(0px * var(--tw-space-x-reverse));margin-left:calc(0px * calc(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-\[10px\]{border-radius:10px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-sm{border-radius:.125rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l-md{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-t-4{border-top-width:4px}.border-t-\[0px\]{border-top-width:0px}.border-t-\[10px\]{border-top-width:10px}.border-t-\[1px\]{border-top-width:1px}.border-solid{border-style:solid}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-\[\#E5E7EB\]{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.border-emerald-500{--tw-border-opacity: 1;border-color:rgb(16 185 129 / var(--tw-border-opacity))}.border-fuchsia-600{--tw-border-opacity: 1;border-color:rgb(192 38 211 / var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity))}.border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.border-red-300{--tw-border-opacity: 1;border-color:rgb(252 165 165 / var(--tw-border-opacity))}.border-red-400{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.border-red-900{--tw-border-opacity: 1;border-color:rgb(127 29 29 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-opacity-50{--tw-border-opacity: .5}.bg-\[\#F2F9FE\]{--tw-bg-opacity: 1;background-color:rgb(242 249 254 / var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity))}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-opacity-100{--tw-bg-opacity: 1}.bg-opacity-75{--tw-bg-opacity: .75}.bg-clip-padding{background-clip:padding-box}.fill-current{fill:currentColor}.object-cover{-o-object-fit:cover;object-fit:cover}.object-scale-down{-o-object-fit:scale-down;object-fit:scale-down}.object-center{-o-object-position:center;object-position:center}.p-1{padding:.25rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.p-\[12px\]{padding:12px}.p-\[20px\]{padding:20px}.px-0{padding-left:0;padding-right:0}.px-1{padding-left:.25rem;padding-right:.25rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-\[12px\]{padding-left:12px;padding-right:12px}.px-\[20px\]{padding-left:20px;padding-right:20px}.px-\[22px\]{padding-left:22px;padding-right:22px}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-\[33px\]{padding-top:33px;padding-bottom:33px}.py-\[36px\]{padding-top:36px;padding-bottom:36px}.py-\[9\.5px\]{padding-top:9.5px;padding-bottom:9.5px}.pb-10{padding-bottom:2.5rem}.pb-2{padding-bottom:.5rem}.pb-20{padding-bottom:5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pb-\[20px\]{padding-bottom:20px}.pb-\[56px\]{padding-bottom:56px}.pb-\[58px\]{padding-bottom:58px}.pl-0{padding-left:0}.pl-1\.5{padding-left:.375rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-\[18px\]{padding-left:18px}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pr-\[18px\]{padding-right:18px}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-6{padding-top:1.5rem}.pt-\[20px\]{padding-top:20px}.pt-\[29px\]{padding-top:29px}.pt-\[35px\]{padding-top:35px}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.font-\[\'Open_Sans\'\]{font-family:Open Sans}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[12px\]{font-size:12px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[22px\]{font-size:22px}.text-\[24px\]{font-size:24px}.text-\[35px\]{font-size:35px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-\[16px\]{font-weight:16px}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-\[1\.2rem\]{line-height:1.2rem}.leading-\[1\.35em\]{line-height:1.35em}.leading-\[1\.36em\]{line-height:1.36em}.leading-\[1\.375em\]{line-height:1.375em}.leading-\[1\.3em\]{line-height:1.3em}.leading-\[1\.5em\]{line-height:1.5em}.leading-\[1\.75em\]{line-height:1.75em}.leading-normal{line-height:1.5}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#212529\]{--tw-text-opacity: 1;color:rgb(33 37 41 / var(--tw-text-opacity))}.text-\[\#6C727F\]{--tw-text-opacity: 1;color:rgb(108 114 127 / var(--tw-text-opacity))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-black{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity: .05}.grayscale{--tw-grayscale: grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-1000{transition-duration:1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-linear{transition-timing-function:linear}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.placeholder\:text-gray-500::-moz-placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.placeholder\:text-gray-500::placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:left-\[8px\]:after{content:var(--tw-content);left:8px}.after\:top-\[5px\]:after{content:var(--tw-content);top:5px}.after\:h-\[30px\]:after{content:var(--tw-content);height:30px}.after\:w-\[30px\]:after{content:var(--tw-content);width:30px}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content: "";content:var(--tw-content)}.focus-within\:z-10:focus-within{z-index:10}.hover\:list-disc:hover{list-style-type:disc}.hover\:border-blue-600:hover{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}.hover\:border-gray-600:hover{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.hover\:border-gray-800:hover{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity))}.hover\:border-transparent:hover{border-color:transparent}.hover\:bg-blue-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.hover\:bg-red-900:hover{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.hover\:font-semibold:hover{font-weight:600}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.hover\:text-indigo-900:hover{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-2xl:hover{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-blue-300:focus{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity))}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.focus\:border-fuchsia-300:focus{--tw-border-opacity: 1;border-color:rgb(240 171 252 / var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.focus\:border-red-500:focus{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.focus\:bg-gray-100:focus{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.focus\:bg-gray-600:focus{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}.focus\:bg-white:focus{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.focus\:font-semibold:focus{font-weight:600}.focus\:text-gray-600:focus{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.focus\:text-gray-900:focus{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.focus\:underline:focus{text-decoration-line:underline}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-0:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity))}.focus\:ring-gray-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\:ring-indigo-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity))}.focus\:ring-red-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity))}.focus\:ring-opacity-50:focus{--tw-ring-opacity: .5}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.active\:bg-gray-50:active{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.active\:text-gray-800:active{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.active\:outline-none:active{outline:2px solid transparent;outline-offset:2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-50:disabled{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-75:disabled{opacity:.75}.group:hover .group-hover\:border-transparent{border-color:transparent}.group:hover .group-hover\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.group:hover .group-hover\:opacity-100{opacity:1}.peer:checked~.peer-checked\:text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.peer:checked~.peer-checked\:after\:translate-x-\[140\%\]:after{content:var(--tw-content);--tw-translate-x: 140%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.peer:focus~.peer-focus\:outline-none{outline:2px solid transparent;outline-offset:2px}@media (min-width: 640px){.sm\:inset-0{top:0;right:0;bottom:0;left:0}.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:col-span-3{grid-column:span 3 / span 3}.sm\:col-span-4{grid-column:span 4 / span 4}.sm\:col-span-6{grid-column:span 6 / span 6}.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:mx-0{margin-left:0;margin-right:0}.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:ml-3{margin-left:.75rem}.sm\:ml-4{margin-left:1rem}.sm\:ml-6{margin-left:1.5rem}.sm\:mt-0{margin-top:0}.sm\:mt-4{margin-top:1rem}.sm\:mt-6{margin-top:1.5rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:h-10{height:2.5rem}.sm\:h-screen{height:100vh}.sm\:w-10{width:2.5rem}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-4xl{max-width:56rem}.sm\:max-w-lg{max-width:32rem}.sm\:max-w-sm{max-width:24rem}.sm\:flex-shrink-0{flex-shrink:0}.sm\:translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:flex-nowrap{flex-wrap:nowrap}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:1rem}.sm\:rounded-lg{border-radius:.5rem}.sm\:p-0{padding:0}.sm\:p-6{padding:1.5rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pb-4{padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:col-span-2{grid-column:span 2 / span 2}.md\:col-span-4{grid-column:span 4 / span 4}.md\:col-span-5{grid-column:span 5 / span 5}.md\:col-span-6{grid-column:span 6 / span 6}.md\:col-start-2{grid-column-start:2}.md\:col-start-4{grid-column-start:4}.md\:mx-0,.md\:mx-\[0\]{margin-left:0;margin-right:0}.md\:-mr-1{margin-right:-.25rem}.md\:mb-6{margin-bottom:1.5rem}.md\:mb-\[46px\]{margin-bottom:46px}.md\:ml-2{margin-left:.5rem}.md\:ml-6{margin-left:1.5rem}.md\:mr-0{margin-right:0}.md\:mr-2{margin-right:.5rem}.md\:mt-0{margin-top:0}.md\:mt-10{margin-top:2.5rem}.md\:mt-5{margin-top:1.25rem}.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:min-h-\[411px\]{min-height:411px}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.333333%}.md\:max-w-3xl{max-width:48rem}.md\:max-w-xl{max-width:36rem}.md\:flex-shrink-0{flex-shrink:0}.md\:shrink{flex-shrink:1}.md\:grow-0{flex-grow:0}.md\:basis-1\/2{flex-basis:50%}.md\:basis-\[449px\]{flex-basis:449px}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:items-center{align-items:center}.md\:justify-center{justify-content:center}.md\:justify-between{justify-content:space-between}.md\:gap-6{gap:1.5rem}.md\:gap-x-\[21px\]{-moz-column-gap:21px;column-gap:21px}.md\:gap-y-6{row-gap:1.5rem}.md\:border-r{border-right-width:1px}.md\:border-\[\#E5E7EB\]{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.md\:p-24{padding:6rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:px-\[40px\]{padding-left:40px;padding-right:40px}.md\:pb-\[40px\]{padding-bottom:40px}.md\:pl-4{padding-left:1rem}.md\:pl-\[52px\]{padding-left:52px}.md\:pl-\[61px\]{padding-left:61px}.md\:pr-\[20px\]{padding-right:20px}.md\:pr-\[48px\]{padding-right:48px}.md\:pt-0{padding-top:0}.md\:pt-\[58px\]{padding-top:58px}.md\:text-left{text-align:left}.md\:text-center{text-align:center}.md\:text-2xl{font-size:1.5rem;line-height:2rem}.md\:text-\[30px\]{font-size:30px}.md\:text-\[32px\]{font-size:32px}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 1024px){.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-6{grid-column:span 6 / span 6}.lg\:col-span-7{grid-column:span 7 / span 7}.lg\:col-span-8{grid-column:span 8 / span 8}.lg\:col-start-3{grid-column-start:3}.lg\:col-start-4{grid-column-start:4}.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:-mb-1{margin-bottom:-.25rem}.lg\:-ml-5{margin-left:-1.25rem}.lg\:mt-24{margin-top:6rem}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:hidden{display:none}.lg\:h-screen{height:100vh}.lg\:w-1\/2{width:50%}.lg\:w-1\/3{width:33.333333%}.lg\:w-1\/4{width:25%}.lg\:w-1\/5{width:20%}.lg\:max-w-\[80\%\]{max-width:80%}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:items-center{align-items:center}.lg\:gap-4{gap:1rem}.lg\:rounded-lg{border-radius:.5rem}.lg\:px-16{padding-left:4rem;padding-right:4rem}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:px-4{padding-left:1rem;padding-right:1rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-2{padding-top:.5rem;padding-bottom:.5rem}}@media (min-width: 1280px){.xl\:col-span-4{grid-column:span 4 / span 4}.xl\:col-span-6{grid-column:span 6 / span 6}.xl\:col-span-8{grid-column:span 8 / span 8}.xl\:col-span-9{grid-column:span 9 / span 9}.xl\:col-start-4{grid-column-start:4}.xl\:ml-5{margin-left:1.25rem}.xl\:mt-0{margin-top:0}.xl\:mt-32{margin-top:8rem}.xl\:flex{display:flex}.xl\:w-auto{width:auto}.xl\:basis-auto{flex-basis:auto}.xl\:flex-row{flex-direction:row}.xl\:flex-nowrap{flex-wrap:nowrap}.xl\:justify-center{justify-content:center}.xl\:border-r{border-right-width:1px}.xl\:border-\[\#E5E7EB\]{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.xl\:px-16{padding-left:4rem;padding-right:4rem}.xl\:px-20{padding-left:5rem;padding-right:5rem}.xl\:px-5{padding-left:1.25rem;padding-right:1.25rem}.xl\:pr-20{padding-right:5rem}}@media (prefers-color-scheme: dark){.dark\:border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.dark\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\:placeholder-gray-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity))}.dark\:placeholder-gray-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity))}.dark\:focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.dark\:focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity))}} diff --git a/public/build/assets/app-aa93be80.js b/public/build/assets/app-aa93be80.js deleted file mode 100644 index 4809e86044..0000000000 --- a/public/build/assets/app-aa93be80.js +++ /dev/null @@ -1,109 +0,0 @@ -import{A as Ll}from"./index-08e160a7.js";import{c as zt,g as Il}from"./_commonjsHelpers-725317a4.js";var Dl={visa:{niceType:"Visa",type:"visa",patterns:[4],gaps:[4,8,12],lengths:[16,18,19],code:{name:"CVV",size:3}},mastercard:{niceType:"Mastercard",type:"mastercard",patterns:[[51,55],[2221,2229],[223,229],[23,26],[270,271],2720],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}},"american-express":{niceType:"American Express",type:"american-express",patterns:[34,37],gaps:[4,10],lengths:[15],code:{name:"CID",size:4}},"diners-club":{niceType:"Diners Club",type:"diners-club",patterns:[[300,305],36,38,39],gaps:[4,10],lengths:[14,16,19],code:{name:"CVV",size:3}},discover:{niceType:"Discover",type:"discover",patterns:[6011,[644,649],65],gaps:[4,8,12],lengths:[16,19],code:{name:"CID",size:3}},jcb:{niceType:"JCB",type:"jcb",patterns:[2131,1800,[3528,3589]],gaps:[4,8,12],lengths:[16,17,18,19],code:{name:"CVV",size:3}},unionpay:{niceType:"UnionPay",type:"unionpay",patterns:[620,[624,626],[62100,62182],[62184,62187],[62185,62197],[62200,62205],[622010,622999],622018,[622019,622999],[62207,62209],[622126,622925],[623,626],6270,6272,6276,[627700,627779],[627781,627799],[6282,6289],6291,6292,810,[8110,8131],[8132,8151],[8152,8163],[8164,8171]],gaps:[4,8,12],lengths:[14,15,16,17,18,19],code:{name:"CVN",size:3}},maestro:{niceType:"Maestro",type:"maestro",patterns:[493698,[5e5,504174],[504176,506698],[506779,508999],[56,59],63,67,6],gaps:[4,8,12],lengths:[12,13,14,15,16,17,18,19],code:{name:"CVC",size:3}},elo:{niceType:"Elo",type:"elo",patterns:[401178,401179,438935,457631,457632,431274,451416,457393,504175,[506699,506778],[509e3,509999],627780,636297,636368,[650031,650033],[650035,650051],[650405,650439],[650485,650538],[650541,650598],[650700,650718],[650720,650727],[650901,650978],[651652,651679],[655e3,655019],[655021,655058]],gaps:[4,8,12],lengths:[16],code:{name:"CVE",size:3}},mir:{niceType:"Mir",type:"mir",patterns:[[2200,2204]],gaps:[4,8,12],lengths:[16,17,18,19],code:{name:"CVP2",size:3}},hiper:{niceType:"Hiper",type:"hiper",patterns:[637095,63737423,63743358,637568,637599,637609,637612],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}},hipercard:{niceType:"Hipercard",type:"hipercard",patterns:[606282],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}}},$l=Dl,ii={},Sn={};Object.defineProperty(Sn,"__esModule",{value:!0});Sn.clone=void 0;function Fl(e){return e?JSON.parse(JSON.stringify(e)):null}Sn.clone=Fl;var ai={};Object.defineProperty(ai,"__esModule",{value:!0});ai.matches=void 0;function Bl(e,r,n){var a=String(r).length,s=e.substr(0,a),l=parseInt(s,10);return r=parseInt(String(r).substr(0,s.length),10),n=parseInt(String(n).substr(0,s.length),10),l>=r&&l<=n}function Ul(e,r){return r=String(r),r.substring(0,e.length)===e.substring(0,r.length)}function Hl(e,r){return Array.isArray(r)?Bl(e,r[0],r[1]):Ul(e,r)}ai.matches=Hl;Object.defineProperty(ii,"__esModule",{value:!0});ii.addMatchingCardsToResults=void 0;var ql=Sn,Vl=ai;function zl(e,r,n){var a,s;for(a=0;a=s&&(m.matchStrength=s),n.push(m);break}}}ii.addMatchingCardsToResults=zl;var oi={};Object.defineProperty(oi,"__esModule",{value:!0});oi.isValidInputType=void 0;function Wl(e){return typeof e=="string"||e instanceof String}oi.isValidInputType=Wl;var si={};Object.defineProperty(si,"__esModule",{value:!0});si.findBestMatch=void 0;function Kl(e){var r=e.filter(function(n){return n.matchStrength}).length;return r>0&&r===e.length}function Jl(e){return Kl(e)?e.reduce(function(r,n){return!r||Number(r.matchStrength)eu?mn(!1,!1):Zl.test(e)?mn(!1,!0):mn(!0,!0)}li.cardholderName=tu;var ui={};function ru(e){for(var r=0,n=!1,a=e.length-1,s;a>=0;)s=parseInt(e.charAt(a),10),n&&(s*=2,s>9&&(s=s%10+1)),n=!n,r+=s,a--;return r%10===0}var nu=ru;Object.defineProperty(ui,"__esModule",{value:!0});ui.cardNumber=void 0;var iu=nu,ho=Yo;function yr(e,r,n){return{card:e,isPotentiallyValid:r,isValid:n}}function au(e,r){r===void 0&&(r={});var n,a,s;if(typeof e!="string"&&typeof e!="number")return yr(null,!1,!1);var l=String(e).replace(/-|\s/g,"");if(!/^\d*$/.test(l))return yr(null,!1,!1);var m=ho(l);if(m.length===0)return yr(null,!1,!1);if(m.length!==1)return yr(null,!0,!1);var h=m[0];if(r.maxLength&&l.length>r.maxLength)return yr(h,!1,!1);h.type===ho.types.UNIONPAY&&r.luhnValidateUnionPay!==!0?a=!0:a=iu(l),s=Math.max.apply(null,h.lengths),r.maxLength&&(s=Math.min(r.maxLength,s));for(var P=0;P4)return sr(!1,!1);var h=parseInt(e,10),P=Number(String(s).substr(2,2)),I=!1;if(a===2){if(String(s).substr(0,2)===e)return sr(!1,!0);n=P===h,I=h>=P&&h<=P+r}else a===4&&(n=s===h,I=h>=s&&h<=s+r);return sr(I,I,n)}Qr.expirationYear=su;var di={};Object.defineProperty(di,"__esModule",{value:!0});di.isArray=void 0;di.isArray=Array.isArray||function(e){return Object.prototype.toString.call(e)==="[object Array]"};Object.defineProperty(fi,"__esModule",{value:!0});fi.parseDate=void 0;var lu=Qr,uu=di;function cu(e){var r=Number(e[0]),n;return r===0?2:r>1||r===1&&Number(e[1])>2?1:r===1?(n=e.substr(1),lu.expirationYear(n).isPotentiallyValid?1:2):e.length===5?1:e.length>5?2:1}function fu(e){var r;if(/^\d{4}-\d{1,2}$/.test(e)?r=e.split("-").reverse():/\//.test(e)?r=e.split(/\s*\/\s*/g):/\s/.test(e)&&(r=e.split(/ +/g)),uu.isArray(r))return{month:r[0]||"",year:r.slice(1).join()};var n=cu(e),a=e.substr(0,n);return{month:a,year:e.substr(a.length)}}fi.parseDate=fu;var On={};Object.defineProperty(On,"__esModule",{value:!0});On.expirationMonth=void 0;function vn(e,r,n){return{isValid:e,isPotentiallyValid:r,isValidForThisYear:n||!1}}function du(e){var r=new Date().getMonth()+1;if(typeof e!="string")return vn(!1,!1);if(e.replace(/\s/g,"")===""||e==="0")return vn(!1,!0);if(!/^\d*$/.test(e))return vn(!1,!1);var n=parseInt(e,10);if(isNaN(Number(e)))return vn(!1,!1);var a=n>0&&n<13;return vn(a,a,a&&n>=r)}On.expirationMonth=du;var la=zt&&zt.__assign||function(){return la=Object.assign||function(e){for(var r,n=1,a=arguments.length;nr?e[n]:r;return r}function qr(e,r){return{isValid:e,isPotentiallyValid:r}}function bu(e,r){return r===void 0&&(r=Xo),r=r instanceof Array?r:[r],typeof e!="string"||!/^\d*$/.test(e)?qr(!1,!1):vu(r,e.length)?qr(!0,!0):e.lengthyu(r)?qr(!1,!1):qr(!0,!0)}pi.cvv=bu;var hi={};Object.defineProperty(hi,"__esModule",{value:!0});hi.postalCode=void 0;var _u=3;function Xi(e,r){return{isValid:e,isPotentiallyValid:r}}function wu(e,r){r===void 0&&(r={});var n=r.minLength||_u;return typeof e!="string"?Xi(!1,!1):e.lengthfunction(){return r||(0,e[Zo(e)[0]])((r={exports:{}}).exports,r),r.exports},Bu=(e,r,n,a)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of Zo(r))!Fu.call(e,s)&&s!==n&&Qo(e,s,{get:()=>r[s],enumerable:!(a=Du(r,s))||a.enumerable});return e},Ke=(e,r,n)=>(n=e!=null?Iu($u(e)):{},Bu(r||!e||!e.__esModule?Qo(n,"default",{value:e,enumerable:!0}):n,e)),ft=Wt({"../alpine/packages/alpinejs/dist/module.cjs.js"(e,r){var n=Object.create,a=Object.defineProperty,s=Object.getOwnPropertyDescriptor,l=Object.getOwnPropertyNames,m=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty,P=(t,i)=>function(){return i||(0,t[l(t)[0]])((i={exports:{}}).exports,i),i.exports},I=(t,i)=>{for(var o in i)a(t,o,{get:i[o],enumerable:!0})},Z=(t,i,o,c)=>{if(i&&typeof i=="object"||typeof i=="function")for(let d of l(i))!h.call(t,d)&&d!==o&&a(t,d,{get:()=>i[d],enumerable:!(c=s(i,d))||c.enumerable});return t},re=(t,i,o)=>(o=t!=null?n(m(t)):{},Z(i||!t||!t.__esModule?a(o,"default",{value:t,enumerable:!0}):o,t)),q=t=>Z(a({},"__esModule",{value:!0}),t),K=P({"node_modules/@vue/shared/dist/shared.cjs.js"(t){Object.defineProperty(t,"__esModule",{value:!0});function i(b,z){const te=Object.create(null),pe=b.split(",");for(let ze=0;ze!!te[ze.toLowerCase()]:ze=>!!te[ze]}var o={1:"TEXT",2:"CLASS",4:"STYLE",8:"PROPS",16:"FULL_PROPS",32:"HYDRATE_EVENTS",64:"STABLE_FRAGMENT",128:"KEYED_FRAGMENT",256:"UNKEYED_FRAGMENT",512:"NEED_PATCH",1024:"DYNAMIC_SLOTS",2048:"DEV_ROOT_FRAGMENT",[-1]:"HOISTED",[-2]:"BAIL"},c={1:"STABLE",2:"DYNAMIC",3:"FORWARDED"},d="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt",p=i(d),v=2;function E(b,z=0,te=b.length){let pe=b.split(/(\r?\n)/);const ze=pe.filter((xt,dt)=>dt%2===1);pe=pe.filter((xt,dt)=>dt%2===0);let rt=0;const wt=[];for(let xt=0;xt=z){for(let dt=xt-v;dt<=xt+v||te>rt;dt++){if(dt<0||dt>=pe.length)continue;const hn=dt+1;wt.push(`${hn}${" ".repeat(Math.max(3-String(hn).length,0))}| ${pe[dt]}`);const Ur=pe[dt].length,ei=ze[dt]&&ze[dt].length||0;if(dt===xt){const Hr=z-(rt-(Ur+ei)),Gi=Math.max(1,te>rt?Ur-Hr:te-z);wt.push(" | "+" ".repeat(Hr)+"^".repeat(Gi))}else if(dt>xt){if(te>rt){const Hr=Math.max(Math.min(te-rt,Ur),1);wt.push(" | "+"^".repeat(Hr))}rt+=Ur+ei}}break}return wt.join(` -`)}var L="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ee=i(L),ke=i(L+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected"),Ze=/[>/="'\u0009\u000a\u000c\u0020]/,De={};function Ge(b){if(De.hasOwnProperty(b))return De[b];const z=Ze.test(b);return z&&console.error(`unsafe attribute name: ${b}`),De[b]=!z}var Tt={acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv"},Ut=i("animation-iteration-count,border-image-outset,border-image-slice,border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width"),we=i("accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap");function We(b){if(Dt(b)){const z={};for(let te=0;te{if(te){const pe=te.split(Ve);pe.length>1&&(z[pe[0].trim()]=pe[1].trim())}}),z}function It(b){let z="";if(!b)return z;for(const te in b){const pe=b[te],ze=te.startsWith("--")?te:Qn(te);(hr(pe)||typeof pe=="number"&&Ut(ze))&&(z+=`${ze}:${pe};`)}return z}function Ht(b){let z="";if(hr(b))z=b;else if(Dt(b))for(let te=0;te]/;function Fi(b){const z=""+b,te=$i.exec(z);if(!te)return z;let pe="",ze,rt,wt=0;for(rt=te.index;rt||--!>|Mr(te,z))}var Un=b=>b==null?"":qt(b)?JSON.stringify(b,Hi,2):String(b),Hi=(b,z)=>pr(z)?{[`Map(${z.size})`]:[...z.entries()].reduce((te,[pe,ze])=>(te[`${pe} =>`]=ze,te),{})}:$t(z)?{[`Set(${z.size})`]:[...z.values()]}:qt(z)&&!Dt(z)&&!Kn(z)?String(z):z,qi=["bigInt","optionalChaining","nullishCoalescingOperator"],ln=Object.freeze({}),un=Object.freeze([]),cn=()=>{},Nr=()=>!1,jr=/^on[^a-z]/,Lr=b=>jr.test(b),Ir=b=>b.startsWith("onUpdate:"),Hn=Object.assign,qn=(b,z)=>{const te=b.indexOf(z);te>-1&&b.splice(te,1)},Vn=Object.prototype.hasOwnProperty,zn=(b,z)=>Vn.call(b,z),Dt=Array.isArray,pr=b=>gr(b)==="[object Map]",$t=b=>gr(b)==="[object Set]",fn=b=>b instanceof Date,dn=b=>typeof b=="function",hr=b=>typeof b=="string",Vi=b=>typeof b=="symbol",qt=b=>b!==null&&typeof b=="object",Dr=b=>qt(b)&&dn(b.then)&&dn(b.catch),Wn=Object.prototype.toString,gr=b=>Wn.call(b),zi=b=>gr(b).slice(8,-1),Kn=b=>gr(b)==="[object Object]",Jn=b=>hr(b)&&b!=="NaN"&&b[0]!=="-"&&""+parseInt(b,10)===b,Gn=i(",key,ref,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),mr=b=>{const z=Object.create(null);return te=>z[te]||(z[te]=b(te))},Yn=/-(\w)/g,Xn=mr(b=>b.replace(Yn,(z,te)=>te?te.toUpperCase():"")),Wi=/\B([A-Z])/g,Qn=mr(b=>b.replace(Wi,"-$1").toLowerCase()),vr=mr(b=>b.charAt(0).toUpperCase()+b.slice(1)),Ki=mr(b=>b?`on${vr(b)}`:""),pn=(b,z)=>b!==z&&(b===b||z===z),Ji=(b,z)=>{for(let te=0;te{Object.defineProperty(b,z,{configurable:!0,enumerable:!1,value:te})},Fr=b=>{const z=parseFloat(b);return isNaN(z)?b:z},Br,Zn=()=>Br||(Br=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});t.EMPTY_ARR=un,t.EMPTY_OBJ=ln,t.NO=Nr,t.NOOP=cn,t.PatchFlagNames=o,t.babelParserDefaultPlugins=qi,t.camelize=Xn,t.capitalize=vr,t.def=$r,t.escapeHtml=Fi,t.escapeHtmlComment=Bi,t.extend=Hn,t.generateCodeFrame=E,t.getGlobalThis=Zn,t.hasChanged=pn,t.hasOwn=zn,t.hyphenate=Qn,t.invokeArrayFns=Ji,t.isArray=Dt,t.isBooleanAttr=ke,t.isDate=fn,t.isFunction=dn,t.isGloballyWhitelisted=p,t.isHTMLTag=Rr,t.isIntegerKey=Jn,t.isKnownAttr=we,t.isMap=pr,t.isModelListener=Ir,t.isNoUnitNumericStyleProp=Ut,t.isObject=qt,t.isOn=Lr,t.isPlainObject=Kn,t.isPromise=Dr,t.isReservedProp=Gn,t.isSSRSafeAttrName=Ge,t.isSVGTag=Di,t.isSet=$t,t.isSpecialBooleanAttr=ee,t.isString=hr,t.isSymbol=Vi,t.isVoidTag=kr,t.looseEqual=Mr,t.looseIndexOf=Bn,t.makeMap=i,t.normalizeClass=Ht,t.normalizeStyle=We,t.objectToString=Wn,t.parseStringStyle=_t,t.propsToAttrMap=Tt,t.remove=qn,t.slotFlagsText=c,t.stringifyStyle=It,t.toDisplayString=Un,t.toHandlerKey=Ki,t.toNumber=Fr,t.toRawType=zi,t.toTypeString=gr}}),T=P({"node_modules/@vue/shared/index.js"(t,i){i.exports=K()}}),y=P({"node_modules/@vue/reactivity/dist/reactivity.cjs.js"(t){Object.defineProperty(t,"__esModule",{value:!0});var i=T(),o=new WeakMap,c=[],d,p=Symbol("iterate"),v=Symbol("Map key iterate");function E(u){return u&&u._isEffect===!0}function L(u,R=i.EMPTY_OBJ){E(u)&&(u=u.raw);const j=Ze(u,R);return R.lazy||j(),j}function ee(u){u.active&&(De(u),u.options.onStop&&u.options.onStop(),u.active=!1)}var ke=0;function Ze(u,R){const j=function(){if(!j.active)return u();if(!c.includes(j)){De(j);try{return we(),c.push(j),d=j,u()}finally{c.pop(),We(),d=c[c.length-1]}}};return j.id=ke++,j.allowRecurse=!!R.allowRecurse,j._isEffect=!0,j.active=!0,j.raw=u,j.deps=[],j.options=R,j}function De(u){const{deps:R}=u;if(R.length){for(let j=0;j{ht&&ht.forEach(Ft=>{(Ft!==d||Ft.allowRecurse)&&nt.add(Ft)})};if(R==="clear")Le.forEach(St);else if(j==="length"&&i.isArray(u))Le.forEach((ht,Ft)=>{(Ft==="length"||Ft>=oe)&&St(ht)});else switch(j!==void 0&&St(Le.get(j)),R){case"add":i.isArray(u)?i.isIntegerKey(j)&&St(Le.get("length")):(St(Le.get(p)),i.isMap(u)&&St(Le.get(v)));break;case"delete":i.isArray(u)||(St(Le.get(p)),i.isMap(u)&&St(Le.get(v)));break;case"set":i.isMap(u)&&St(Le.get(p));break}const gn=ht=>{ht.options.onTrigger&&ht.options.onTrigger({effect:ht,target:u,key:j,type:R,newValue:oe,oldValue:J,oldTarget:me}),ht.options.scheduler?ht.options.scheduler(ht):ht()};nt.forEach(gn)}var _t=i.makeMap("__proto__,__v_isRef,__isVue"),It=new Set(Object.getOwnPropertyNames(Symbol).map(u=>Symbol[u]).filter(i.isSymbol)),Ht=kr(),Pr=kr(!1,!0),on=kr(!0),sn=kr(!0,!0),Rr=Di();function Di(){const u={};return["includes","indexOf","lastIndexOf"].forEach(R=>{u[R]=function(...j){const oe=b(this);for(let me=0,Le=this.length;me{u[R]=function(...j){Ut();const oe=b(this)[R].apply(this,j);return We(),oe}}),u}function kr(u=!1,R=!1){return function(oe,J,me){if(J==="__v_isReactive")return!u;if(J==="__v_isReadonly")return u;if(J==="__v_raw"&&me===(u?R?Xn:Yn:R?mr:Gn).get(oe))return oe;const Le=i.isArray(oe);if(!u&&Le&&i.hasOwn(Rr,J))return Reflect.get(Rr,J,me);const nt=Reflect.get(oe,J,me);return(i.isSymbol(J)?It.has(J):_t(J))||(u||je(oe,"get",J),R)?nt:pe(nt)?!Le||!i.isIntegerKey(J)?nt.value:nt:i.isObject(nt)?u?pn(nt):vr(nt):nt}}var $i=Fn(),Fi=Fn(!0);function Fn(u=!1){return function(j,oe,J,me){let Le=j[oe];if(!u&&(J=b(J),Le=b(Le),!i.isArray(j)&&pe(Le)&&!pe(J)))return Le.value=J,!0;const nt=i.isArray(j)&&i.isIntegerKey(oe)?Number(oe)i.isObject(u)?vr(u):u,un=u=>i.isObject(u)?pn(u):u,cn=u=>u,Nr=u=>Reflect.getPrototypeOf(u);function jr(u,R,j=!1,oe=!1){u=u.__v_raw;const J=b(u),me=b(R);R!==me&&!j&&je(J,"get",R),!j&&je(J,"get",me);const{has:Le}=Nr(J),nt=oe?cn:j?un:ln;if(Le.call(J,R))return nt(u.get(R));if(Le.call(J,me))return nt(u.get(me));u!==J&&u.get(R)}function Lr(u,R=!1){const j=this.__v_raw,oe=b(j),J=b(u);return u!==J&&!R&&je(oe,"has",u),!R&&je(oe,"has",J),u===J?j.has(u):j.has(u)||j.has(J)}function Ir(u,R=!1){return u=u.__v_raw,!R&&je(b(u),"iterate",p),Reflect.get(u,"size",u)}function Hn(u){u=b(u);const R=b(this);return Nr(R).has.call(R,u)||(R.add(u),Ve(R,"add",u,u)),this}function qn(u,R){R=b(R);const j=b(this),{has:oe,get:J}=Nr(j);let me=oe.call(j,u);me?Jn(j,oe,u):(u=b(u),me=oe.call(j,u));const Le=J.call(j,u);return j.set(u,R),me?i.hasChanged(R,Le)&&Ve(j,"set",u,R,Le):Ve(j,"add",u,R),this}function Vn(u){const R=b(this),{has:j,get:oe}=Nr(R);let J=j.call(R,u);J?Jn(R,j,u):(u=b(u),J=j.call(R,u));const me=oe?oe.call(R,u):void 0,Le=R.delete(u);return J&&Ve(R,"delete",u,void 0,me),Le}function zn(){const u=b(this),R=u.size!==0,j=i.isMap(u)?new Map(u):new Set(u),oe=u.clear();return R&&Ve(u,"clear",void 0,void 0,j),oe}function Dt(u,R){return function(oe,J){const me=this,Le=me.__v_raw,nt=b(Le),St=R?cn:u?un:ln;return!u&&je(nt,"iterate",p),Le.forEach((gn,ht)=>oe.call(J,St(gn),St(ht),me))}}function pr(u,R,j){return function(...oe){const J=this.__v_raw,me=b(J),Le=i.isMap(me),nt=u==="entries"||u===Symbol.iterator&&Le,St=u==="keys"&&Le,gn=J[u](...oe),ht=j?cn:R?un:ln;return!R&&je(me,"iterate",St?v:p),{next(){const{value:Ft,done:Yi}=gn.next();return Yi?{value:Ft,done:Yi}:{value:nt?[ht(Ft[0]),ht(Ft[1])]:ht(Ft),done:Yi}},[Symbol.iterator](){return this}}}}function $t(u){return function(...R){{const j=R[0]?`on key "${R[0]}" `:"";console.warn(`${i.capitalize(u)} operation ${j}failed: target is readonly.`,b(this))}return u==="delete"?!1:this}}function fn(){const u={get(me){return jr(this,me)},get size(){return Ir(this)},has:Lr,add:Hn,set:qn,delete:Vn,clear:zn,forEach:Dt(!1,!1)},R={get(me){return jr(this,me,!1,!0)},get size(){return Ir(this)},has:Lr,add:Hn,set:qn,delete:Vn,clear:zn,forEach:Dt(!1,!0)},j={get(me){return jr(this,me,!0)},get size(){return Ir(this,!0)},has(me){return Lr.call(this,me,!0)},add:$t("add"),set:$t("set"),delete:$t("delete"),clear:$t("clear"),forEach:Dt(!0,!1)},oe={get(me){return jr(this,me,!0,!0)},get size(){return Ir(this,!0)},has(me){return Lr.call(this,me,!0)},add:$t("add"),set:$t("set"),delete:$t("delete"),clear:$t("clear"),forEach:Dt(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(me=>{u[me]=pr(me,!1,!1),j[me]=pr(me,!0,!1),R[me]=pr(me,!1,!0),oe[me]=pr(me,!0,!0)}),[u,j,R,oe]}var[dn,hr,Vi,qt]=fn();function Dr(u,R){const j=R?u?qt:Vi:u?hr:dn;return(oe,J,me)=>J==="__v_isReactive"?!u:J==="__v_isReadonly"?u:J==="__v_raw"?oe:Reflect.get(i.hasOwn(j,J)&&J in oe?j:oe,J,me)}var Wn={get:Dr(!1,!1)},gr={get:Dr(!1,!0)},zi={get:Dr(!0,!1)},Kn={get:Dr(!0,!0)};function Jn(u,R,j){const oe=b(j);if(oe!==j&&R.call(u,oe)){const J=i.toRawType(u);console.warn(`Reactive ${J} contains both the raw and reactive versions of the same object${J==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var Gn=new WeakMap,mr=new WeakMap,Yn=new WeakMap,Xn=new WeakMap;function Wi(u){switch(u){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Qn(u){return u.__v_skip||!Object.isExtensible(u)?0:Wi(i.toRawType(u))}function vr(u){return u&&u.__v_isReadonly?u:$r(u,!1,Bn,Wn,Gn)}function Ki(u){return $r(u,!1,Hi,gr,mr)}function pn(u){return $r(u,!0,Un,zi,Yn)}function Ji(u){return $r(u,!0,qi,Kn,Xn)}function $r(u,R,j,oe,J){if(!i.isObject(u))return console.warn(`value cannot be made reactive: ${String(u)}`),u;if(u.__v_raw&&!(R&&u.__v_isReactive))return u;const me=J.get(u);if(me)return me;const Le=Qn(u);if(Le===0)return u;const nt=new Proxy(u,Le===2?oe:j);return J.set(u,nt),nt}function Fr(u){return Br(u)?Fr(u.__v_raw):!!(u&&u.__v_isReactive)}function Br(u){return!!(u&&u.__v_isReadonly)}function Zn(u){return Fr(u)||Br(u)}function b(u){return u&&b(u.__v_raw)||u}function z(u){return i.def(u,"__v_skip",!0),u}var te=u=>i.isObject(u)?vr(u):u;function pe(u){return!!(u&&u.__v_isRef===!0)}function ze(u){return xt(u)}function rt(u){return xt(u,!0)}var wt=class{constructor(u,R=!1){this._shallow=R,this.__v_isRef=!0,this._rawValue=R?u:b(u),this._value=R?u:te(u)}get value(){return je(b(this),"get","value"),this._value}set value(u){u=this._shallow?u:b(u),i.hasChanged(u,this._rawValue)&&(this._rawValue=u,this._value=this._shallow?u:te(u),Ve(b(this),"set","value",u))}};function xt(u,R=!1){return pe(u)?u:new wt(u,R)}function dt(u){Ve(b(u),"set","value",u.value)}function hn(u){return pe(u)?u.value:u}var Ur={get:(u,R,j)=>hn(Reflect.get(u,R,j)),set:(u,R,j,oe)=>{const J=u[R];return pe(J)&&!pe(j)?(J.value=j,!0):Reflect.set(u,R,j,oe)}};function ei(u){return Fr(u)?u:new Proxy(u,Ur)}var Hr=class{constructor(u){this.__v_isRef=!0;const{get:R,set:j}=u(()=>je(this,"get","value"),()=>Ve(this,"set","value"));this._get=R,this._set=j}get value(){return this._get()}set value(u){this._set(u)}};function Gi(u){return new Hr(u)}function kl(u){Zn(u)||console.warn("toRefs() expects a reactive object but received a plain one.");const R=i.isArray(u)?new Array(u.length):{};for(const j in u)R[j]=po(u,j);return R}var Ml=class{constructor(u,R){this._object=u,this._key=R,this.__v_isRef=!0}get value(){return this._object[this._key]}set value(u){this._object[this._key]=u}};function po(u,R){return pe(u[R])?u[R]:new Ml(u,R)}var Nl=class{constructor(u,R,j){this._setter=R,this._dirty=!0,this.__v_isRef=!0,this.effect=L(u,{lazy:!0,scheduler:()=>{this._dirty||(this._dirty=!0,Ve(b(this),"set","value"))}}),this.__v_isReadonly=j}get value(){const u=b(this);return u._dirty&&(u._value=this.effect(),u._dirty=!1),je(u,"get","value"),u._value}set value(u){this._setter(u)}};function jl(u){let R,j;return i.isFunction(u)?(R=u,j=()=>{console.warn("Write operation failed: computed value is readonly")}):(R=u.get,j=u.set),new Nl(R,j,i.isFunction(u)||!u.set)}t.ITERATE_KEY=p,t.computed=jl,t.customRef=Gi,t.effect=L,t.enableTracking=we,t.isProxy=Zn,t.isReactive=Fr,t.isReadonly=Br,t.isRef=pe,t.markRaw=z,t.pauseTracking=Ut,t.proxyRefs=ei,t.reactive=vr,t.readonly=pn,t.ref=ze,t.resetTracking=We,t.shallowReactive=Ki,t.shallowReadonly=Ji,t.shallowRef=rt,t.stop=ee,t.toRaw=b,t.toRef=po,t.toRefs=kl,t.track=je,t.trigger=Ve,t.triggerRef=dt,t.unref=hn}}),x=P({"node_modules/@vue/reactivity/index.js"(t,i){i.exports=y()}}),_={};I(_,{Alpine:()=>fo,default:()=>Rl}),r.exports=q(_);var w=!1,k=!1,D=[],he=-1;function ve(t){A(t)}function A(t){D.includes(t)||D.push(t),G()}function O(t){let i=D.indexOf(t);i!==-1&&i>he&&D.splice(i,1)}function G(){!k&&!w&&(w=!0,queueMicrotask(le))}function le(){w=!1,k=!0;for(let t=0;tt.effect(i,{scheduler:o=>{Xe?ve(o):o()}}),Ye=t.raw}function lt(t){Y=t}function Et(t){let i=()=>{};return[c=>{let d=Y(c);return t._x_effects||(t._x_effects=new Set,t._x_runEffects=()=>{t._x_effects.forEach(p=>p())}),t._x_effects.add(d),i=()=>{d!==void 0&&(t._x_effects.delete(d),Re(d))},d},()=>{i()}]}function mt(t,i){let o=!0,c,d=Y(()=>{let p=t();JSON.stringify(p),o?c=p:queueMicrotask(()=>{i(p,c),c=p}),o=!1});return()=>Re(d)}var Ee=[],be=[],Oe=[];function Ae(t){Oe.push(t)}function ye(t,i){typeof i=="function"?(t._x_cleanups||(t._x_cleanups=[]),t._x_cleanups.push(i)):(i=t,be.push(i))}function W(t){Ee.push(t)}function Je(t,i,o){t._x_attributeCleanups||(t._x_attributeCleanups={}),t._x_attributeCleanups[i]||(t._x_attributeCleanups[i]=[]),t._x_attributeCleanups[i].push(o)}function ct(t,i){t._x_attributeCleanups&&Object.entries(t._x_attributeCleanups).forEach(([o,c])=>{(i===void 0||i.includes(o))&&(c.forEach(d=>d()),delete t._x_attributeCleanups[o])})}function V(t){var i,o;for((i=t._x_effects)==null||i.forEach(O);(o=t._x_cleanups)!=null&&o.length;)t._x_cleanups.pop()()}var ne=new MutationObserver(ue),Ce=!1;function Me(){ne.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),Ce=!0}function xe(){vt(),ne.disconnect(),Ce=!1}var ie=[];function vt(){let t=ne.takeRecords();ie.push(()=>t.length>0&&ue(t));let i=ie.length;queueMicrotask(()=>{if(ie.length===i)for(;ie.length>0;)ie.shift()()})}function X(t){if(!Ce)return t();xe();let i=t();return Me(),i}var M=!1,$=[];function fe(){M=!0}function _e(){M=!1,ue($),$=[]}function ue(t){if(M){$=$.concat(t);return}let i=[],o=new Set,c=new Map,d=new Map;for(let p=0;p{v.nodeType===1&&v._x_marker&&o.add(v)}),t[p].addedNodes.forEach(v=>{if(v.nodeType===1){if(o.has(v)){o.delete(v);return}v._x_marker||i.push(v)}})),t[p].type==="attributes")){let v=t[p].target,E=t[p].attributeName,L=t[p].oldValue,ee=()=>{c.has(v)||c.set(v,[]),c.get(v).push({name:E,value:v.getAttribute(E)})},ke=()=>{d.has(v)||d.set(v,[]),d.get(v).push(E)};v.hasAttribute(E)&&L===null?ee():v.hasAttribute(E)?(ke(),ee()):ke()}d.forEach((p,v)=>{ct(v,p)}),c.forEach((p,v)=>{Ee.forEach(E=>E(v,p))});for(let p of o)i.some(v=>v.contains(p))||be.forEach(v=>v(p));for(let p of i)p.isConnected&&Oe.forEach(v=>v(p));i=null,o=null,c=null,d=null}function ce(t){return de(H(t))}function B(t,i,o){return t._x_dataStack=[i,...H(o||t)],()=>{t._x_dataStack=t._x_dataStack.filter(c=>c!==i)}}function H(t){return t._x_dataStack?t._x_dataStack:typeof ShadowRoot=="function"&&t instanceof ShadowRoot?H(t.host):t.parentNode?H(t.parentNode):[]}function de(t){return new Proxy({objects:t},He)}var He={ownKeys({objects:t}){return Array.from(new Set(t.flatMap(i=>Object.keys(i))))},has({objects:t},i){return i==Symbol.unscopables?!1:t.some(o=>Object.prototype.hasOwnProperty.call(o,i)||Reflect.has(o,i))},get({objects:t},i,o){return i=="toJSON"?$e:Reflect.get(t.find(c=>Reflect.has(c,i))||{},i,o)},set({objects:t},i,o,c){const d=t.find(v=>Object.prototype.hasOwnProperty.call(v,i))||t[t.length-1],p=Object.getOwnPropertyDescriptor(d,i);return p!=null&&p.set&&(p!=null&&p.get)?p.set.call(c,o)||!0:Reflect.set(d,i,o)}};function $e(){return Reflect.ownKeys(this).reduce((i,o)=>(i[o]=Reflect.get(this,o),i),{})}function Fe(t){let i=c=>typeof c=="object"&&!Array.isArray(c)&&c!==null,o=(c,d="")=>{Object.entries(Object.getOwnPropertyDescriptors(c)).forEach(([p,{value:v,enumerable:E}])=>{if(E===!1||v===void 0||typeof v=="object"&&v!==null&&v.__v_skip)return;let L=d===""?p:`${d}.${p}`;typeof v=="object"&&v!==null&&v._x_interceptor?c[p]=v.initialize(t,L,p):i(v)&&v!==c&&!(v instanceof Element)&&o(v,L)})};return o(t)}function at(t,i=()=>{}){let o={initialValue:void 0,_x_interceptor:!0,initialize(c,d,p){return t(this.initialValue,()=>Rt(c,d),v=>jt(c,d,v),d,p)}};return i(o),c=>{if(typeof c=="object"&&c!==null&&c._x_interceptor){let d=o.initialize.bind(o);o.initialize=(p,v,E)=>{let L=c.initialize(p,v,E);return o.initialValue=L,d(p,v,E)}}else o.initialValue=c;return o}}function Rt(t,i){return i.split(".").reduce((o,c)=>o[c],t)}function jt(t,i,o){if(typeof i=="string"&&(i=i.split(".")),i.length===1)t[i[0]]=o;else{if(i.length===0)throw error;return t[i[0]]||(t[i[0]]={}),jt(t[i[0]],i.slice(1),o)}}var cr={};function Ot(t,i){cr[t]=i}function Kt(t,i){let o=fr(i);return Object.entries(cr).forEach(([c,d])=>{Object.defineProperty(t,`$${c}`,{get(){return d(i,o)},enumerable:!1})}),t}function fr(t){let[i,o]=ae(t),c={interceptor:at,...i};return ye(t,o),c}function Cn(t,i,o,...c){try{return o(...c)}catch(d){rr(d,t,i)}}function rr(t,i,o=void 0){t=Object.assign(t??{message:"No error message given."},{el:i,expression:o}),console.warn(`Alpine Expression Error: ${t.message} - -${o?'Expression: "'+o+`" - -`:""}`,i),setTimeout(()=>{throw t},0)}var Sr=!0;function Tn(t){let i=Sr;Sr=!1;let o=t();return Sr=i,o}function Jt(t,i,o={}){let c;return yt(t,i)(d=>c=d,o),c}function yt(...t){return Zr(...t)}var Zr=Rn;function Pn(t){Zr=t}function Rn(t,i){let o={};Kt(o,t);let c=[o,...H(t)],d=typeof i=="function"?bi(c,i):wi(c,i,t);return Cn.bind(null,t,i,d)}function bi(t,i){return(o=()=>{},{scope:c={},params:d=[]}={})=>{let p=i.apply(de([c,...t]),d);Er(o,p)}}var en={};function _i(t,i){if(en[t])return en[t];let o=Object.getPrototypeOf(async function(){}).constructor,c=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t,p=(()=>{try{let v=new o(["__self","scope"],`with (scope) { __self.result = ${c} }; __self.finished = true; return __self.result;`);return Object.defineProperty(v,"name",{value:`[Alpine] ${t}`}),v}catch(v){return rr(v,i,t),Promise.resolve()}})();return en[t]=p,p}function wi(t,i,o){let c=_i(i,o);return(d=()=>{},{scope:p={},params:v=[]}={})=>{c.result=void 0,c.finished=!1;let E=de([p,...t]);if(typeof c=="function"){let L=c(c,E).catch(ee=>rr(ee,o,i));c.finished?(Er(d,c.result,E,v,o),c.result=void 0):L.then(ee=>{Er(d,ee,E,v,o)}).catch(ee=>rr(ee,o,i)).finally(()=>c.result=void 0)}}}function Er(t,i,o,c,d){if(Sr&&typeof i=="function"){let p=i.apply(o,c);p instanceof Promise?p.then(v=>Er(t,v,o,c)).catch(v=>rr(v,d,i)):t(p)}else typeof i=="object"&&i instanceof Promise?i.then(p=>t(p)):t(i)}var Or="x-";function Gt(t=""){return Or+t}function kn(t){Or=t}var Ar={};function f(t,i){return Ar[t]=i,{before(o){if(!Ar[o]){console.warn(String.raw`Cannot find directive \`${o}\`. \`${t}\` will use the default order of execution`);return}const c=Qe.indexOf(o);Qe.splice(c>=0?c:Qe.indexOf("DEFAULT"),0,t)}}}function g(t){return Object.keys(Ar).includes(t)}function S(t,i,o){if(i=Array.from(i),t._x_virtualDirectives){let p=Object.entries(t._x_virtualDirectives).map(([E,L])=>({name:E,value:L})),v=C(p);p=p.map(E=>v.find(L=>L.name===E.name)?{name:`x-bind:${E.name}`,value:`"${E.value}"`}:E),i=i.concat(p)}let c={};return i.map(Be((p,v)=>c[p]=v)).filter(Ie).map(Ue(c,o)).sort(Ct).map(p=>se(t,p))}function C(t){return Array.from(t).map(Be()).filter(i=>!Ie(i))}var N=!1,F=new Map,U=Symbol();function Q(t){N=!0;let i=Symbol();U=i,F.set(i,[]);let o=()=>{for(;F.get(i).length;)F.get(i).shift()();F.delete(i)},c=()=>{N=!1,o()};t(o),c()}function ae(t){let i=[],o=E=>i.push(E),[c,d]=Et(t);return i.push(d),[{Alpine:an,effect:c,cleanup:o,evaluateLater:yt.bind(yt,t),evaluate:Jt.bind(Jt,t)},()=>i.forEach(E=>E())]}function se(t,i){let o=()=>{},c=Ar[i.type]||o,[d,p]=ae(t);Je(t,i.original,p);let v=()=>{t._x_ignore||t._x_ignoreSelf||(c.inline&&c.inline(t,i,d),c=c.bind(c,t,i,d),N?F.get(U).push(c):c())};return v.runCleanups=p,v}var Ne=(t,i)=>({name:o,value:c})=>(o.startsWith(t)&&(o=o.replace(t,i)),{name:o,value:c}),Te=t=>t;function Be(t=()=>{}){return({name:i,value:o})=>{let{name:c,value:d}=Se.reduce((p,v)=>v(p),{name:i,value:o});return c!==i&&t(c,i),{name:c,value:d}}}var Se=[];function Pe(t){Se.push(t)}function Ie({name:t}){return et().test(t)}var et=()=>new RegExp(`^${Or}([^:^.]+)\\b`);function Ue(t,i){return({name:o,value:c})=>{let d=o.match(et()),p=o.match(/:([a-zA-Z0-9\-_:]+)/),v=o.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],E=i||t[o]||o;return{type:d?d[1]:null,value:p?p[1]:null,modifiers:v.map(L=>L.replace(".","")),expression:c,original:E}}}var tt="DEFAULT",Qe=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",tt,"teleport"];function Ct(t,i){let o=Qe.indexOf(t.type)===-1?tt:t.type,c=Qe.indexOf(i.type)===-1?tt:i.type;return Qe.indexOf(o)-Qe.indexOf(c)}function ot(t,i,o={}){t.dispatchEvent(new CustomEvent(i,{detail:o,bubbles:!0,composed:!0,cancelable:!0}))}function At(t,i){if(typeof ShadowRoot=="function"&&t instanceof ShadowRoot){Array.from(t.children).forEach(d=>At(d,i));return}let o=!1;if(i(t,()=>o=!0),o)return;let c=t.firstElementChild;for(;c;)At(c,i),c=c.nextElementSibling}function bt(t,...i){console.warn(`Alpine Warning: ${t}`,...i)}var nr=!1;function Mn(){nr&&bt("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),nr=!0,document.body||bt("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` diff --git a/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php b/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php index ba3a8dc67d..0cfa66ff82 100644 --- a/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php +++ b/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php @@ -56,7 +56,7 @@ input:checked ~ .dot { Pro Plan

- $12 + $14

monthly @@ -95,7 +95,7 @@ input:checked ~ .dot { Enterprise Plan

- $14 + $18

monthly @@ -139,7 +139,7 @@ input:checked ~ .dot { Pro Plan

- $120 + $140

yearly @@ -275,15 +275,15 @@ document.getElementById('handleProYearlyClick').addEventListener('click', functi }); const price_map = new Map(); //monthly -price_map.set('7LDdwRb1YK', '$14'); -price_map.set('MVyb8mdvAZ', '$26'); -price_map.set('WpmbkR5azJ', '$36'); -price_map.set('k8mepY2aMy', '$44'); +price_map.set('7LDdwRb1YK', '$16'); +price_map.set('MVyb8mdvAZ', '$32'); +price_map.set('WpmbkR5azJ', '$54'); +price_map.set('k8mepY2aMy', '$84'); //yearly -price_map.set('LYqaQWldnj', '$140'); -price_map.set('kQBeX6mbyK', '$260'); -price_map.set('GELe32Qd69', '$360'); -price_map.set('MVyb86oevA', '$440'); +price_map.set('LYqaQWldnj', '$160'); +price_map.set('kQBeX6mbyK', '$320'); +price_map.set('GELe32Qd69', '$540'); +price_map.set('MVyb86oevA', '$840'); From 8f82039f98ffa487b1713f1fc53d2ae9a457fa1a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 2 Jan 2026 08:25:07 +1100 Subject: [PATCH 052/177] Fixes for validation --- .../EDocument/Standards/Validation/Peppol/EntityLevel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php b/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php index 9184a9c809..d2bce3fd29 100644 --- a/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php +++ b/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php @@ -271,7 +271,7 @@ class EntityLevel implements EntityLevelInterface } //test legal entity id present - if (!is_int($company->legal_entity_id)) { + if(isset($company->legal_entity_id) && intval($company->legal_entity_id) > 0){ $errors[] = ['field' => "You have not registered a legal entity id as yet."]; } From 2214acaedd6f7b42bf9ecb4d92e75a5ed37f6e47 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 2 Jan 2026 08:25:29 +1100 Subject: [PATCH 053/177] v5.12.42 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 80cc4e2e94..79df7cd1d7 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.12.41 \ No newline at end of file +5.12.42 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index ba3a9f5871..2377b0dd05 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.12.41'), - 'app_tag' => env('APP_TAG', '5.12.41'), + 'app_version' => env('APP_VERSION', '5.12.42'), + 'app_tag' => env('APP_TAG', '5.12.42'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), From 30e14b82a6aabad1c222a32be230fcf287701fcb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 2 Jan 2026 08:34:20 +1100 Subject: [PATCH 054/177] Fixes for validation --- .../EDocument/Standards/Validation/Peppol/EntityLevel.php | 2 +- tests/Integration/Einvoice/Storecove/EInvoiceValidationTest.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php b/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php index d2bce3fd29..9e0added3b 100644 --- a/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php +++ b/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php @@ -271,7 +271,7 @@ class EntityLevel implements EntityLevelInterface } //test legal entity id present - if(isset($company->legal_entity_id) && intval($company->legal_entity_id) > 0){ + if(intval($company->legal_entity_id) == 0){ $errors[] = ['field' => "You have not registered a legal entity id as yet."]; } diff --git a/tests/Integration/Einvoice/Storecove/EInvoiceValidationTest.php b/tests/Integration/Einvoice/Storecove/EInvoiceValidationTest.php index 9a70f69dee..1aff0f8691 100644 --- a/tests/Integration/Einvoice/Storecove/EInvoiceValidationTest.php +++ b/tests/Integration/Einvoice/Storecove/EInvoiceValidationTest.php @@ -84,6 +84,8 @@ class EInvoiceValidationTest extends TestCase $el = new EntityLevel(); $validation = $el->checkCompany($company); + $this->assertTrue(isset($company->legal_entity_id)); + $this->assertTrue(intval($company->legal_entity_id) > 0); $this->assertTrue($validation['passes']); } From be5f3b80220a5625c27cd3cd3bd055cd902df04d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 2 Jan 2026 08:34:35 +1100 Subject: [PATCH 055/177] v5.12.43 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 79df7cd1d7..58d2e07038 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.12.42 \ No newline at end of file +5.12.43 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 2377b0dd05..cc5cfa9375 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.12.42'), - 'app_tag' => env('APP_TAG', '5.12.42'), + 'app_version' => env('APP_VERSION', '5.12.43'), + 'app_tag' => env('APP_TAG', '5.12.43'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), From 0ce55baf089612a39bff9902ad4e90521018783c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 2 Jan 2026 08:45:44 +1100 Subject: [PATCH 056/177] Fixes for resolving Hermes from LEI validator --- .../EDocument/Gateway/Storecove/StorecoveRouter.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php b/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php index ea93e22185..376817ecbb 100644 --- a/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php +++ b/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php @@ -214,6 +214,12 @@ class StorecoveRouter $parts = explode(":", $identifier); $country = $parts[0]; + /** When using HERMES, the country does not resolve, we cast back to BE here. */ + if($country == 'LEI'){ + $country = 'BE'; + $identifier = 'BE:VAT'; + } + $rules = $this->routing_rules[$country]; if (is_array($rules) && !is_array($rules[0])) { From da7190774479dfecf969cce2edeb553766ee654f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 2 Jan 2026 08:46:01 +1100 Subject: [PATCH 057/177] Fixes for validating Hermes --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 58d2e07038..4c190b1e3e 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.12.43 \ No newline at end of file +5.12.44 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index cc5cfa9375..293261a528 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.12.43'), - 'app_tag' => env('APP_TAG', '5.12.43'), + 'app_version' => env('APP_VERSION', '5.12.44'), + 'app_tag' => env('APP_TAG', '5.12.44'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), From ba268136c173d9f0367182d77abe078c54bbf26b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 2 Jan 2026 10:47:59 +1100 Subject: [PATCH 058/177] Fixes for no late invoice notifications --- app/Console/Kernel.php | 6 +- app/Jobs/Invoice/InvoiceCheckOverdue.php | 191 +++++++++++++++++++++++ app/Mail/Admin/InvoiceOverdueObject.php | 102 ++++++++++++ lang/en/texts.php | 2 + 4 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 app/Jobs/Invoice/InvoiceCheckOverdue.php create mode 100644 app/Mail/Admin/InvoiceOverdueObject.php diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index f0069c9155..02c45a310f 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -37,6 +37,7 @@ use App\Jobs\EDocument\EInvoicePullDocs; use App\Jobs\Cron\InvoiceTaxSummary; use Illuminate\Console\Scheduling\Schedule; use App\Jobs\Invoice\InvoiceCheckLateWebhook; +use App\Jobs\Invoice\InvoiceCheckOverdue; use App\Jobs\Subscription\CleanStaleInvoiceOrder; use App\PaymentDrivers\Rotessa\Jobs\TransactionReport; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -130,7 +131,10 @@ class Kernel extends ConsoleKernel $schedule->job(new AutoBillCron())->dailyAt('06:20')->withoutOverlapping()->name('auto-bill-job')->onOneServer(); /* Fires webhooks for overdue Invoice */ - $schedule->job(new InvoiceCheckLateWebhook())->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-job')->onOneServer(); + $schedule->job(new InvoiceCheckLateWebhook())->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-webhook-job')->onOneServer(); + + /* Fires notifications for overdue Invoice (respects company timezone) */ + $schedule->job(new InvoiceCheckOverdue())->hourly()->withoutOverlapping()->name('invoice-overdue-notification-job')->onOneServer(); /* Pulls in bank transactions from third party services */ $schedule->job(new BankTransactionSync())->twiceDaily(1, 13)->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer(); diff --git a/app/Jobs/Invoice/InvoiceCheckOverdue.php b/app/Jobs/Invoice/InvoiceCheckOverdue.php new file mode 100644 index 0000000000..f0b1079d55 --- /dev/null +++ b/app/Jobs/Invoice/InvoiceCheckOverdue.php @@ -0,0 +1,191 @@ +processOverdueInvoices(); + } else { + foreach (MultiDB::$dbs as $db) { + MultiDB::setDB($db); + $this->processOverdueInvoices(); + } + } + } + + /** + * Process overdue invoices for the current database connection. + * We check each company's timezone to ensure the invoice is truly overdue + * based on the company's local time. + */ + private function processOverdueInvoices(): void + { + // Get all companies that are not disabled + Company::query() + ->where('is_disabled', false) + ->when(Ninja::isHosted(), function ($query) { + $query->whereHas('account', function ($q) { + $q->where('is_flagged', false) + ->whereIn('plan', ['enterprise', 'pro']) + ->where('plan_expires', '>', now()->subHours(12)); + }); + }) + ->cursor() + ->each(function (Company $company) { + $this->checkCompanyOverdueInvoices($company); + }); + } + + /** + * Check for overdue invoices for a specific company, + * using the company's timezone to determine if the invoice is overdue. + * + * Two scenarios trigger an overdue notification: + * 1. partial > 0 && partial_due_date was yesterday (partial payment is overdue) + * 2. partial == 0 && balance > 0 && due_date was yesterday (full invoice is overdue) + * + * To prevent duplicate notifications when running hourly, we only process + * a company when it's currently between midnight and 1am in their timezone. + * This ensures each company is only checked once per day. + */ + private function checkCompanyOverdueInvoices(Company $company): void + { + // Get the company's timezone + $timezone = $company->timezone(); + $timezone_name = $timezone ? $timezone->name : 'UTC'; + + // Get the current hour in the company's timezone + $now_in_company_tz = Carbon::now($timezone_name); + + // Only process this company if it's currently between midnight and 1am in their timezone + // This prevents duplicate notifications when running hourly across all timezones + if ($now_in_company_tz->hour !== 0) { + return; + } + + // Calculate the date range for "just became overdue" in the company's timezone + // We check for invoices whose due date was yesterday in the company's timezone + $yesterday_start = $now_in_company_tz->copy()->subDay()->startOfDay()->format('Y-m-d'); + $yesterday_end = $now_in_company_tz->copy()->startOfDay()->subSecond()->format('Y-m-d'); + + Invoice::query() + ->where('company_id', $company->id) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('is_deleted', false) + ->whereNull('deleted_at') + ->where('balance', '>', 0) + ->whereHas('client', function ($query) { + $query->where('is_deleted', 0) + ->whereNull('deleted_at'); + }) + // Check for overdue conditions based on partial or full invoice + ->where(function ($query) use ($yesterday_start, $yesterday_end) { + // Case 1: Partial payment is overdue (partial > 0 and partial_due_date was yesterday) + $query->where(function ($q) use ($yesterday_start, $yesterday_end) { + $q->where('partial', '>', 0) + ->whereNotNull('partial_due_date') + ->whereBetween('partial_due_date', [$yesterday_start, $yesterday_end]); + }) + // Case 2: Full invoice is overdue (partial == 0 and due_date was yesterday) + ->orWhere(function ($q) use ($yesterday_start, $yesterday_end) { + $q->where(function ($subq) { + $subq->where('partial', '=', 0) + ->orWhereNull('partial'); + }) + ->whereNotNull('due_date') + ->whereBetween('due_date', [$yesterday_start, $yesterday_end]); + }); + }) + ->cursor() + ->each(function ($invoice) { + $this->notifyOverdueInvoice($invoice); + }); + } + + /** + * Send notifications for an overdue invoice to all relevant company users. + */ + private function notifyOverdueInvoice(Invoice $invoice): void + { + $nmo = new NinjaMailerObject(); + $nmo->company = $invoice->company; + $nmo->settings = $invoice->company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($invoice->company->company_users as $company_user) { + /* The User */ + $user = $company_user->user; + + if (! $user) { + continue; + } + + $nmo->mailable = new NinjaMailer((new InvoiceOverdueObject($invoice, $invoice->company, $company_user->portalType()))->build()); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes( + $invoice->invitations()->first(), + $company_user, + 'invoice', + ['all_notifications', 'invoice_late', 'invoice_late_all', 'invoice_late_user'] + ); + + /* If one of the methods is email then we fire the mailer */ + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + } + } + } +} + diff --git a/app/Mail/Admin/InvoiceOverdueObject.php b/app/Mail/Admin/InvoiceOverdueObject.php new file mode 100644 index 0000000000..d42a4071a7 --- /dev/null +++ b/app/Mail/Admin/InvoiceOverdueObject.php @@ -0,0 +1,102 @@ +company->db); + + if (! $this->invoice) { + return; + } + + App::forgetInstance('translator'); + /* Init a new copy of the translator */ + $t = app('translator'); + /* Set the locale */ + App::setLocale($this->company->getLocale()); + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->company->settings)); + + $mail_obj = new stdClass(); + $mail_obj->amount = $this->getAmount(); + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + $mail_obj->text_view = 'email.template.text'; + + return $mail_obj; + } + + private function getAmount() + { + return Number::formatMoney($this->invoice->balance, $this->invoice->client); + } + + private function getSubject() + { + return + ctrans( + 'texts.notification_invoice_overdue_subject', + [ + 'client' => $this->invoice->client->present()->name(), + 'invoice' => $this->invoice->number, + ] + ); + } + + private function getData() + { + $settings = $this->invoice->client->getMergedSettings(); + $content = ctrans( + 'texts.notification_invoice_overdue', + [ + 'amount' => $this->getAmount(), + 'client' => $this->invoice->client->present()->name(), + 'invoice' => $this->invoice->number, + ] + ); + + $data = [ + 'title' => $this->getSubject(), + 'content' => $content, + 'url' => $this->invoice->invitations->first()->getAdminLink($this->use_react_url), + 'button' => $this->use_react_url ? ctrans('texts.view_invoice') : ctrans('texts.login'), + 'signature' => $settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + 'text_body' => $content, + 'template' => $this->company->account->isPremium() ? 'email.template.admin_premium' : 'email.template.admin', + + ]; + + return $data; + } +} + diff --git a/lang/en/texts.php b/lang/en/texts.php index e632ddfd0e..e9576dd192 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4691,6 +4691,8 @@ $lang = array( 'show_tasks_in_client_portal' => 'Show Tasks in Client Portal', 'notification_quote_expired_subject' => 'Quote :invoice has expired for :client', 'notification_quote_expired' => 'The following Quote :invoice for client :client and :amount has now expired.', + 'notification_invoice_overdue_subject' => 'Invoice :invoice is overdue for :client', + 'notification_invoice_overdue' => 'The following Invoice :invoice for client :client and :amount is now overdue.', 'auto_sync' => 'Auto Sync', 'refresh_accounts' => 'Refresh Accounts', 'upgrade_to_connect_bank_account' => 'Upgrade to Enterprise to connect your bank account', From 400319d09667ce9a93d9462279a4a8f26edeab15 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 2 Jan 2026 10:52:15 +1100 Subject: [PATCH 059/177] Fixes for no late invoice notifications --- app/Jobs/Invoice/InvoiceCheckOverdue.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/Jobs/Invoice/InvoiceCheckOverdue.php b/app/Jobs/Invoice/InvoiceCheckOverdue.php index f0b1079d55..575c2afbfe 100644 --- a/app/Jobs/Invoice/InvoiceCheckOverdue.php +++ b/app/Jobs/Invoice/InvoiceCheckOverdue.php @@ -110,10 +110,8 @@ class InvoiceCheckOverdue implements ShouldQueue return; } - // Calculate the date range for "just became overdue" in the company's timezone - // We check for invoices whose due date was yesterday in the company's timezone - $yesterday_start = $now_in_company_tz->copy()->subDay()->startOfDay()->format('Y-m-d'); - $yesterday_end = $now_in_company_tz->copy()->startOfDay()->subSecond()->format('Y-m-d'); + // Yesterday's date in the company's timezone (Y-m-d format) + $yesterday = $now_in_company_tz->copy()->subDay()->format('Y-m-d'); Invoice::query() ->where('company_id', $company->id) @@ -126,21 +124,19 @@ class InvoiceCheckOverdue implements ShouldQueue ->whereNull('deleted_at'); }) // Check for overdue conditions based on partial or full invoice - ->where(function ($query) use ($yesterday_start, $yesterday_end) { + ->where(function ($query) use ($yesterday) { // Case 1: Partial payment is overdue (partial > 0 and partial_due_date was yesterday) - $query->where(function ($q) use ($yesterday_start, $yesterday_end) { + $query->where(function ($q) use ($yesterday) { $q->where('partial', '>', 0) - ->whereNotNull('partial_due_date') - ->whereBetween('partial_due_date', [$yesterday_start, $yesterday_end]); + ->where('partial_due_date', $yesterday); }) // Case 2: Full invoice is overdue (partial == 0 and due_date was yesterday) - ->orWhere(function ($q) use ($yesterday_start, $yesterday_end) { + ->orWhere(function ($q) use ($yesterday) { $q->where(function ($subq) { $subq->where('partial', '=', 0) ->orWhereNull('partial'); }) - ->whereNotNull('due_date') - ->whereBetween('due_date', [$yesterday_start, $yesterday_end]); + ->where('due_date', $yesterday); }); }) ->cursor() From 1282cec6842b6b0fc17584911b557d9bfe20e503 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 3 Jan 2026 15:12:09 +1100 Subject: [PATCH 060/177] Cast correctly - include_drafts query param --- app/Http/Requests/Chart/ShowChartRequest.php | 4 +++- app/Http/ValidationRules/Account/BlackListRule.php | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Http/Requests/Chart/ShowChartRequest.php b/app/Http/Requests/Chart/ShowChartRequest.php index 62b84ac3c6..f813648f93 100644 --- a/app/Http/Requests/Chart/ShowChartRequest.php +++ b/app/Http/Requests/Chart/ShowChartRequest.php @@ -38,7 +38,7 @@ class ShowChartRequest extends Request 'date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom', 'start_date' => 'bail|sometimes|date', 'end_date' => 'bail|sometimes|date', - 'include_drafts' => 'bail|sometimes|in:true,false', + 'include_drafts' => 'bail|sometimes|boolean', ]; } @@ -50,6 +50,8 @@ class ShowChartRequest extends Request $input = $this->all(); + $input['include_drafts'] = filter_var($input['include_drafts'] ?? false, FILTER_VALIDATE_BOOLEAN); + if (isset($input['date_range'])) { $dates = $this->calculateStartAndEndDates($input, $user->company()); $input['start_date'] = $dates[0]; diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 331037da3e..0041063908 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -22,6 +22,7 @@ class BlackListRule implements ValidationRule { /** Bad domains +/- disposable email domains */ private array $blacklist = [ + "usdtbeta.com", "asurad.com", "isb.nu.edu.pk", "edux3.us", From 147ac6c8c32f6937b4b608d2324f73e9f7910c34 Mon Sep 17 00:00:00 2001 From: ayaz <1739113+ayarse@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:24:45 +0500 Subject: [PATCH 061/177] fix: add maldives currency code --- database/seeders/CurrenciesSeeder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seeders/CurrenciesSeeder.php b/database/seeders/CurrenciesSeeder.php index 3555106099..6eb50c9309 100644 --- a/database/seeders/CurrenciesSeeder.php +++ b/database/seeders/CurrenciesSeeder.php @@ -68,7 +68,7 @@ class CurrenciesSeeder extends Seeder ['id' => 43, 'name' => 'Croatian Kuna', 'code' => 'HRK', 'symbol' => 'kn', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','], ['id' => 44, 'name' => 'Saudi Riyal', 'code' => 'SAR', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 45, 'name' => 'Japanese Yen', 'code' => 'JPY', 'symbol' => '¥', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'], - ['id' => 46, 'name' => 'Maldivian Rufiyaa', 'code' => 'MVR', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], + ['id' => 46, 'name' => 'Maldivian Rufiyaa', 'code' => 'MVR', 'symbol' => 'MVR ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 47, 'name' => 'Costa Rican Colón', 'code' => 'CRC', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 48, 'name' => 'Pakistani Rupee', 'code' => 'PKR', 'symbol' => 'Rs ', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 49, 'name' => 'Polish Zloty', 'code' => 'PLN', 'symbol' => 'zł', 'precision' => '2', 'thousand_separator' => ' ', 'decimal_separator' => ',', 'swap_currency_symbol' => true], From f012c8e18a831d8fba857f79d9f73af4a2510a9d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 5 Jan 2026 18:24:53 +1100 Subject: [PATCH 062/177] Ensure location ID is passed through when creating recurring invoices --- app/Factory/RecurringInvoiceToInvoiceFactory.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Factory/RecurringInvoiceToInvoiceFactory.php b/app/Factory/RecurringInvoiceToInvoiceFactory.php index 835afeaa79..957040ce2e 100644 --- a/app/Factory/RecurringInvoiceToInvoiceFactory.php +++ b/app/Factory/RecurringInvoiceToInvoiceFactory.php @@ -69,7 +69,8 @@ class RecurringInvoiceToInvoiceFactory $invoice->design_id = $recurring_invoice->design_id; $invoice->e_invoice = self::transformEInvoice($recurring_invoice); $invoice->vendor_id = $recurring_invoice->vendor_id; - + $invoice->location_id = $recurring_invoice->location_id; + return $invoice; } From 89c7feab5dacfb5bd3d51ccf64f8222dbfdaa834 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 6 Jan 2026 08:45:32 +1100 Subject: [PATCH 063/177] Test routing ID if deliverable --- .../EDocument/Gateway/Storecove/Mutator.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/Services/EDocument/Gateway/Storecove/Mutator.php b/app/Services/EDocument/Gateway/Storecove/Mutator.php index f60119df68..db5a69ce68 100644 --- a/app/Services/EDocument/Gateway/Storecove/Mutator.php +++ b/app/Services/EDocument/Gateway/Storecove/Mutator.php @@ -613,6 +613,26 @@ class Mutator implements MutatorInterface $this->setEmailRouting($client_email); } + + if(stripos($this->invoice->client->routing_id ?? '', ":") !== false){ + + $parts = explode(":", $this->invoice->client->routing_id); + + if(count($parts) == 2){ + $scheme = $parts[0]; + $id = $parts[1]; + + if($this->storecove->discovery($id, $scheme)){ + $this->setStorecoveMeta($this->buildRouting([ + ["scheme" => $scheme, "id" => $id] + ])); + + return $this; + } + } + + } + $code = $this->getClientRoutingCode(); $identifier = false; From d14c7be9b85956c63c46b16c64c4905890a74844 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 7 Jan 2026 11:08:44 +1100 Subject: [PATCH 064/177] embed expense documents where add to invoice is selected in settings --- app/Services/Invoice/InvoiceService.php | 4 ++-- app/Utils/HtmlEngine.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 7d80d5b1b8..d4d13c664b 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -566,8 +566,8 @@ class InvoiceService return $item; }); - Task::query()->withTrashed()->whereIn('id', $tasks->pluck('task_id'))->update(['invoice_id' => $this->invoice->id]); - Expense::query()->withTrashed()->whereIn('id', $tasks->pluck('expense_id'))->update(['invoice_id' => $this->invoice->id]); + Task::query()->withTrashed()->where('company_id', $this->invoice->company_id)->whereIn('id', $tasks->pluck('task_id'))->update(['invoice_id' => $this->invoice->id]); + Expense::query()->withTrashed()->where('company_id', $this->invoice->company_id)->whereIn('id', $tasks->pluck('expense_id'))->update(['invoice_id' => $this->invoice->id]); return $this; } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 66f736210e..901aca29cf 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -1108,6 +1108,24 @@ Código seguro de verificación (CSV): {$verifactu_log->status}"; $container->appendChild($image); } + if($this->entity_string == 'invoice') { + + foreach($this->entity->expense_documents() as $expense){ + foreach($expense->documents()->where('is_public', true)->get() as $document){ + if (!$document->isImage()) { + continue; + } + + $image = $dom->createElement('img'); + + $image->setAttribute('src', "data:image/png;base64,".base64_encode($document->compress())); + $image->setAttribute('style', 'max-width: 50%; margin-top: 20px;'); + + $container->appendChild($image); + } + } + } + $dom->appendChild($container); $html = $dom->saveHTML(); From f882d0c0c2360afd89ddc06bb95946c7bef4c6d5 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 7 Jan 2026 13:08:54 +1100 Subject: [PATCH 065/177] Handle changes to PEPPOL rounding for line taxes --- app/Helpers/Invoice/InvoiceItemSum.php | 3 +++ app/Helpers/Invoice/Taxer.php | 6 ++++++ app/Http/Controllers/EInvoicePeppolController.php | 1 - app/Services/EDocument/Gateway/Storecove/StorecoveProxy.php | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 2164482b15..36f0ebe6a9 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -146,6 +146,8 @@ class InvoiceItemSum private RuleInterface $rule; + public bool $peppol_enabled = false; + public function __construct(RecurringInvoice | Invoice | Quote | Credit | PurchaseOrder | RecurringQuote $invoice) { $this->tax_collection = collect([]); @@ -157,6 +159,7 @@ class InvoiceItemSum if ($this->client) { $this->currency = $this->client->currency(); $this->shouldCalculateTax(); + $this->peppol_enabled = $this->client->getSetting('e_invoice_type') == 'PEPPOL'; } else { $this->currency = $this->invoice->vendor->currency(); } diff --git a/app/Helpers/Invoice/Taxer.php b/app/Helpers/Invoice/Taxer.php index c9e3629b3a..e1fba705a4 100644 --- a/app/Helpers/Invoice/Taxer.php +++ b/app/Helpers/Invoice/Taxer.php @@ -29,6 +29,12 @@ trait Taxer public function calcAmountLineTax($tax_rate, $amount) { + $tax_amount = ($amount * $tax_rate / 100); + + if($this->peppol_enabled) { + return $tax_amount; + } + return $this->formatValue(($amount * $tax_rate / 100), 2); } diff --git a/app/Http/Controllers/EInvoicePeppolController.php b/app/Http/Controllers/EInvoicePeppolController.php index 0c0d409611..b975987590 100644 --- a/app/Http/Controllers/EInvoicePeppolController.php +++ b/app/Http/Controllers/EInvoicePeppolController.php @@ -52,7 +52,6 @@ class EInvoicePeppolController extends BaseController */ public function setup(StoreEntityRequest $request, Storecove $storecove): Response|JsonResponse { - /** * @var \App\Models\Company */ diff --git a/app/Services/EDocument/Gateway/Storecove/StorecoveProxy.php b/app/Services/EDocument/Gateway/Storecove/StorecoveProxy.php index 9d849dc022..648c1ecdf7 100644 --- a/app/Services/EDocument/Gateway/Storecove/StorecoveProxy.php +++ b/app/Services/EDocument/Gateway/Storecove/StorecoveProxy.php @@ -229,7 +229,7 @@ class StorecoveProxy private function remoteRequest(string $uri, array $payload = []): array { - // nlog(config('ninja.hosted_ninja_url')); + $response = Http::baseUrl(config('ninja.hosted_ninja_url')) ->withHeaders($this->getHeaders()) ->post($uri, $payload); From 653a1b6c803e4d922121c21e3640c4d5991562bf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 7 Jan 2026 13:10:49 +1100 Subject: [PATCH 066/177] v5.12.45 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 4c190b1e3e..e9a24b696d 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.12.44 \ No newline at end of file +5.12.45 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 293261a528..6858635e12 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.12.44'), - 'app_tag' => env('APP_TAG', '5.12.44'), + 'app_version' => env('APP_VERSION', '5.12.45'), + 'app_tag' => env('APP_TAG', '5.12.45'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), From 2adcc4cd5bbed7d47c6a21c7cc4908cdb50b2b84 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 9 Jan 2026 08:56:31 +1100 Subject: [PATCH 067/177] Add created between filter --- app/Filters/QueryFilters.php | 27 +++++++++++++++++++++++++++ config/services.php | 4 +++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/Filters/QueryFilters.php b/app/Filters/QueryFilters.php index 8c2d6f5428..e66993d5b3 100644 --- a/app/Filters/QueryFilters.php +++ b/app/Filters/QueryFilters.php @@ -338,6 +338,33 @@ abstract class QueryFilters } + + /** + * Filter by created at date range + * + * @param string $date_range + * @return Builder + */ + public function created_between(string $date_range = ''): Builder + { + $parts = explode(",", $date_range); + + if (count($parts) != 2 || !in_array('created_at', \Illuminate\Support\Facades\Schema::getColumnListing($this->builder->getModel()->getTable()))) { + return $this->builder; + } + + try { + + $start_date = Carbon::parse($parts[0]); + $end_date = Carbon::parse($parts[1]); + + return $this->builder->whereBetween('created_at', [$start_date, $end_date]); + } catch (\Exception $e) { + return $this->builder; + } + + } + /** * Filter by date range * diff --git a/config/services.php b/config/services.php index 398b1b5e24..95ec894333 100644 --- a/config/services.php +++ b/config/services.php @@ -145,7 +145,9 @@ return [ 'gocardless' => [ 'client_id' => env('GOCARDLESS_CLIENT_ID', null), 'client_secret' => env('GOCARDLESS_CLIENT_SECRET', null), - 'debug' => env('APP_DEBUG',false) + 'debug' => env('APP_DEBUG',false), + 'redirect_uri' => env('GOCARDLESS_REDIRECT_URI', null), + 'environment' => env('GOCARDLESS_ENVIRONMENT', 'production'), ], 'quickbooks_webhook' => [ 'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false), From f2a4dcd0f33e9af83e6f06462647cfcd90e49d9e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 9 Jan 2026 16:32:04 +1100 Subject: [PATCH 068/177] Add term_days variable for use in templates / designs --- app/Utils/HtmlEngine.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 901aca29cf..a8172a05e2 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -208,6 +208,8 @@ class HtmlEngine $data['$location.custom3'] = &$data['$location3']; $data['$location.custom4'] = &$data['$location4']; + $data['$term_days'] = ['value' => '', 'label' => ctrans('texts.payment_terms')]; + if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') { if($this->client->peppolSendingEnabled() && $this->entity->amount < 0) { @@ -217,6 +219,9 @@ class HtmlEngine $data['$entity'] = ['value' => ctrans('texts.invoice'), 'label' => ctrans('texts.invoice')]; } + + $data['$term_days'] = ['value' => $this->client->getSetting('payment_terms'), 'label' => ctrans('texts.payment_terms')]; + $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$invoice'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$number_short'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number_short')]; @@ -275,6 +280,10 @@ class HtmlEngine } if ($this->entity_string == 'quote') { + + + $data['$term_days'] = ['value' => $this->client->getSetting('valid_until'), 'label' => ctrans('texts.valid_until')]; + $data['$entity'] = ['value' => ctrans('texts.quote'), 'label' => ctrans('texts.quote')]; $data['$number'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.quote_number')]; $data['$number_short'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.quote_number_short')]; @@ -315,6 +324,10 @@ class HtmlEngine } if ($this->entity_string == 'credit') { + + + $data['$term_days'] = ['value' => $this->client->getSetting('payment_terms'), 'label' => ctrans('texts.payment_terms')]; + $data['$entity'] = ['value' => ctrans('texts.credit'), 'label' => ctrans('texts.credit')]; $data['$number'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.credit_number')]; $data['$number_short'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.credit_number_short')]; From 268cba2b3324de00e834e44e95ad87bf5778c7ab Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 11 Jan 2026 14:43:47 +1100 Subject: [PATCH 069/177] Improvements for PEPPOL payment means --- app/Http/Controllers/EInvoiceController.php | 19 ++++++++++++++++++- app/Utils/HtmlEngine.php | 9 ++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/EInvoiceController.php b/app/Http/Controllers/EInvoiceController.php index c6c8044f9a..c2737957ec 100644 --- a/app/Http/Controllers/EInvoiceController.php +++ b/app/Http/Controllers/EInvoiceController.php @@ -87,7 +87,24 @@ class EInvoiceController extends BaseController $pm->CardAccount = $card_account; } - if (isset($payment_means['iban'])) { + if (isset($payment_means['code']) && $payment_means['code'] == '58') { + $fib = new FinancialInstitutionBranch(); + $fi = new FinancialInstitution(); + $bic_id = new ID(); + $bic_id->value = $payment_means['bic_swift']; + $fi->ID = $bic_id; + $fib->FinancialInstitution = $fi; + $pfa = new PayeeFinancialAccount(); + $iban_id = new ID(); + $iban_id->value = $payment_means['iban']; + $pfa->ID = $iban_id; + $pfa->Name = $payment_means['account_holder'] ?? 'SEPA_CREDIT_TRANSFER'; + $pfa->FinancialInstitutionBranch = $fib; + + $pm->PayeeFinancialAccount = $pfa; + + } + else if (isset($payment_means['iban'])) { $fib = new FinancialInstitutionBranch(); $fi = new FinancialInstitution(); $bic_id = new ID(); diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index a8172a05e2..cf813e7edc 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -391,7 +391,14 @@ class HtmlEngine $data['$balance_due_dec'] = ['value' => sprintf("%01.2f", $this->entity->amount), 'label' => ctrans('texts.balance_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.balance_due')]; $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; - } else { + } + elseif($this->entity->status_id == 4 && $this->entity_string == 'invoice') { + $data['$balance_due'] = ['value' => Number::formatMoney(0, $this->client) ?: ' ', 'label' => ctrans('texts.balance_due')]; + $data['$balance_due_dec'] = ['value' => sprintf("%01.2f", 0), 'label' => ctrans('texts.balance_due')]; + $data['$balance_due_raw'] = ['value' => 0, 'label' => ctrans('texts.balance_due')]; + $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; + } + else { $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: ' ', 'label' => ctrans('texts.balance_due')]; $data['$balance_due_dec'] = ['value' => sprintf("%01.2f", $this->entity->balance), 'label' => ctrans('texts.balance_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')]; From c4809314e24f9e7b7a2d1e19005d0cba892d3166 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 11 Jan 2026 16:14:25 +1100 Subject: [PATCH 070/177] Set invoice late emails as a summary --- app/Jobs/Invoice/InvoiceCheckOverdue.php | 125 ++++++++++++++++-- .../Admin/InvoiceOverdueSummaryObject.php | 103 +++++++++++++++ app/Repositories/UserRepository.php | 2 +- lang/en/texts.php | 2 + .../views/email/admin/generic_table.blade.php | 80 +++++++++++ .../email/admin/generic_table_text.blade.php | 22 +++ 6 files changed, 319 insertions(+), 15 deletions(-) create mode 100644 app/Mail/Admin/InvoiceOverdueSummaryObject.php create mode 100644 resources/views/email/admin/generic_table.blade.php create mode 100644 resources/views/email/admin/generic_table_text.blade.php diff --git a/app/Jobs/Invoice/InvoiceCheckOverdue.php b/app/Jobs/Invoice/InvoiceCheckOverdue.php index 575c2afbfe..240422d481 100644 --- a/app/Jobs/Invoice/InvoiceCheckOverdue.php +++ b/app/Jobs/Invoice/InvoiceCheckOverdue.php @@ -12,21 +12,24 @@ namespace App\Jobs\Invoice; -use App\Jobs\Mail\NinjaMailer; -use App\Jobs\Mail\NinjaMailerJob; -use App\Jobs\Mail\NinjaMailerObject; -use App\Libraries\MultiDB; -use App\Mail\Admin\InvoiceOverdueObject; +use App\Utils\Ninja; +use App\Utils\Number; use App\Models\Company; use App\Models\Invoice; -use App\Utils\Traits\Notifications\UserNotifies; +use App\Libraries\MultiDB; use Illuminate\Bus\Queueable; +use App\Jobs\Mail\NinjaMailer; +use Illuminate\Support\Carbon; +use App\Utils\Traits\MakesDates; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; +use Illuminate\Queue\SerializesModels; +use App\Mail\Admin\InvoiceOverdueObject; +use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Carbon; -use App\Utils\Ninja; +use App\Mail\Admin\InvoiceOverdueSummaryObject; +use App\Utils\Traits\Notifications\UserNotifies; class InvoiceCheckOverdue implements ShouldQueue { @@ -35,6 +38,7 @@ class InvoiceCheckOverdue implements ShouldQueue use Queueable; use SerializesModels; use UserNotifies; + use MakesDates; /** * Create a new job instance. @@ -113,7 +117,7 @@ class InvoiceCheckOverdue implements ShouldQueue // Yesterday's date in the company's timezone (Y-m-d format) $yesterday = $now_in_company_tz->copy()->subDay()->format('Y-m-d'); - Invoice::query() + $overdue_invoices = Invoice::query() ->where('company_id', $company->id) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('is_deleted', false) @@ -140,14 +144,107 @@ class InvoiceCheckOverdue implements ShouldQueue }); }) ->cursor() - ->each(function ($invoice) { - $this->notifyOverdueInvoice($invoice); - }); + ->map(function ($invoice){ + + return [ + 'id' => $invoice->id, + 'client' => $invoice->client->present()->name(), + 'number' => $invoice->number, + 'amount' => max($invoice->partial, $invoice->balance), + 'due_date' => $invoice->due_date, + 'formatted_amount' => Number::formatMoney($invoice->balance, $invoice->client), + 'formatted_due_date' => $this->translateDate($invoice->due_date, $invoice->company->date_format(), $invoice->company->locale()), + ]; + + }) + ->toArray(); + + $this->sendOverdueNotifications($overdue_invoices, $company); + + // ->each(function ($invoice) { + // $this->notifyOverdueInvoice($invoice); + // }); + } + + private function sendOverdueNotifications(array $overdue_invoices, Company $company): void + { + + if(empty($overdue_invoices)){ + return; + } + + $nmo = new NinjaMailerObject(); + $nmo->company = $company; + $nmo->settings = $company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($company->company_users as $company_user) { + /* The User */ + $user = $company_user->user; + + if (! $user) { + continue; + } + + nlog($company_user->permissions); + + $overdue_invoices_collection = $overdue_invoices; + + $invoice = Invoice::withTrashed()->find($overdue_invoices[0]['id']); + + $table_headers = [ + 'client' => ctrans('texts.client'), + 'number' => ctrans('texts.invoice_number'), + 'formatted_due_date' => ctrans('texts.due_date'), + 'formatted_amount' => ctrans('texts.amount'), + ]; + + /** filter down the set if the user only has notifications for their own invoices */ + if(isset($company_user->notifications->email) && is_array($company_user->notifications->email) && in_array('invoice_late_user', $company_user->notifications->email)){ + + $overdue_invoices_collection = collect($overdue_invoices) + ->filter(function ($overdue_invoice) use ($user) { + $invoice = Invoice::withTrashed()->find($overdue_invoice['id']); + nlog([$invoice->user_id, $user->id, $invoice->assigned_user_id, $user->id]); + return $invoice->user_id == $user->id || $invoice->assigned_user_id == $user->id; + }) + ->toArray(); + + if(count($overdue_invoices_collection) === 0){ + continue; + } + + $invoice = Invoice::withTrashed()->find(end($overdue_invoices_collection)['id']); + + } + + $nmo->mailable = new NinjaMailer((new InvoiceOverdueSummaryObject($overdue_invoices_collection, $table_headers, $company, $company_user->portalType()))->build()); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes( + $invoice->invitations()->first(), + $company_user, + 'invoice', + ['all_notifications', 'invoice_late', 'invoice_late_all', 'invoice_late_user'] + ); + + + /* If one of the methods is email then we fire the mailer */ + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + } + } + } /** * Send notifications for an overdue invoice to all relevant company users. */ + /** @phpstan-ignore-next-line */ private function notifyOverdueInvoice(Invoice $invoice): void { $nmo = new NinjaMailerObject(); diff --git a/app/Mail/Admin/InvoiceOverdueSummaryObject.php b/app/Mail/Admin/InvoiceOverdueSummaryObject.php new file mode 100644 index 0000000000..db778ac273 --- /dev/null +++ b/app/Mail/Admin/InvoiceOverdueSummaryObject.php @@ -0,0 +1,103 @@ +company->db); + + App::forgetInstance('translator'); + /* Init a new copy of the translator */ + $t = app('translator'); + /* Set the locale */ + App::setLocale($this->company->getLocale()); + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->company->settings)); + + $mail_obj = new stdClass(); + $mail_obj->amount = 0; + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic_table'; + $mail_obj->tag = $this->company->company_key; + $mail_obj->text_view = 'email.admin.generic_table_text'; + + return $mail_obj; + } + + private function getSubject() + { + + $timezone = $this->company->timezone(); + $timezone_name = $timezone ? $timezone->name : 'UTC'; + + // Get the current hour in the company's timezone + $now_in_company_tz = Carbon::now($timezone_name); + $date = $this->translateDate($now_in_company_tz->format('Y-m-d'), $this->company->date_format(), $this->company->locale()); + + return + ctrans( + 'texts.notification_invoice_overdue_summary_subject', + [ + 'date' => $date + ] + ); + } + + private function getData() + { + + $invoice = Invoice::withTrashed()->find(reset($this->overdue_invoices)['id']); + + $overdue_invoices_collection = array_map( + fn($row) => \Illuminate\Support\Arr::except($row, ['id', 'amount', 'due_date']), + $this->overdue_invoices + ); + + $data = [ + 'title' => $this->getSubject(), + 'content' => ctrans('texts.notification_invoice_overdue_summary'), + 'url' => $invoice->invitations->first()->getAdminLink($this->use_react_url), + 'button' => $this->use_react_url ? ctrans('texts.view_invoice') : ctrans('texts.login'), + 'signature' => $this->company->settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $this->company->settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + 'text_body' => ctrans('texts.notification_invoice_overdue_summary'), + 'template' => $this->company->account->isPremium() ? 'email.template.admin_premium' : 'email.template.admin', + 'table' => $overdue_invoices_collection, + 'table_headers' => $this->table_headers, + ]; + + return $data; + } +} + diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index 288c62577b..9b4b442e4d 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -80,7 +80,7 @@ class UserRepository extends BaseRepository $user->account_id = $account->id;//@todo we should never change the account_id if it is set at this point. - if (strlen($user->password) >= 1) { + if (strlen($user->password ?? '') >= 1) { $user->has_password = true; } diff --git a/lang/en/texts.php b/lang/en/texts.php index e9576dd192..2d5b6c1ac7 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5684,6 +5684,8 @@ $lang = array( 'reject_quote_confirmation' => 'Are you sure you want to reject this quote?', 'reason' => 'Reason', 'enter_reason' => 'Enter a reason...', + 'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date', + 'notification_invoice_overdue_summary' => 'The following invoices are overdue:', ); return $lang; diff --git a/resources/views/email/admin/generic_table.blade.php b/resources/views/email/admin/generic_table.blade.php new file mode 100644 index 0000000000..0002d86f9d --- /dev/null +++ b/resources/views/email/admin/generic_table.blade.php @@ -0,0 +1,80 @@ +@component('email.template.admin', ['design' => 'light', 'settings' => $settings, 'logo' => $logo, 'url' => $url]) +

+ @isset($greeting) +

{{ $greeting }}

+ @endisset + + @isset($title) +

{{ $title }}

+ @endisset + + @isset($h2) +

{{ $title }}

+ @endisset + +
+ @isset($content) + {!! nl2br($content, true) !!} + @endisset + + @isset($table) + + + + @foreach($table_headers as $key => $value) + + @endforeach + + + + @foreach($table as $index => $row) + + @foreach($row as $key => $value) + + @endforeach + + @endforeach + +
{{ $value }}
{{ $value }}
+ @endisset + + @isset($slot) + {{ $slot }} + @endisset +
+ + @isset($additional_info) +

{{ $additional_info }}

+ @endisset + + @if($url) + + + + + + + +
+ + {{ ctrans($button) }} + +
+ + + + @endif + + @isset($signature) +

{!! nl2br($signature) !!}

+ @endisset +
+@endcomponent diff --git a/resources/views/email/admin/generic_table_text.blade.php b/resources/views/email/admin/generic_table_text.blade.php new file mode 100644 index 0000000000..ed7a273e41 --- /dev/null +++ b/resources/views/email/admin/generic_table_text.blade.php @@ -0,0 +1,22 @@ +{{ $title }} + +@isset($body) +{{ strip_tags(str_replace("
", "\r\n", $body)) }} +@endisset + +@isset($content) +{{ strip_tags(str_replace("
", "\r\n", $content)) }} +@endisset + +@isset($table) + + @foreach($table as $row) + {{ implode("\t", array_values($row)) }} + @endforeach +@endisset + +@isset($whitelabel) +@if(!$whitelabel) +{{ ctrans('texts.ninja_email_footer', ['site' => 'https://invoiceninja.com']) }} +@endif +@endisset \ No newline at end of file From 2b5577f9daefcca16fcb8da0cdd9ffeb34862726 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Jan 2026 07:31:53 +1100 Subject: [PATCH 071/177] Disable Calculate Taxes for VERIFACTU --- app/Http/Requests/Company/UpdateCompanyRequest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index f09ecf3425..22baaf0043 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -194,6 +194,10 @@ class UpdateCompanyRequest extends Request $input['session_timeout'] = 0; } + if($company->settings->e_invoice_type == 'VERIFACTU') { + $input['calculate_taxes'] = false; + } + $this->replace($input); } From a9a127f8a86a63cf98302ee7b69237833b9f470f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Jan 2026 08:52:43 +1100 Subject: [PATCH 072/177] Fixes for invoice period in PEPPOL --- app/Factory/RecurringInvoiceToInvoiceFactory.php | 16 +++++++++++----- app/Http/Controllers/EInvoiceController.php | 1 + .../EInvoice/ValidateEInvoiceRequest.php | 6 ++++-- app/Services/EDocument/Standards/Peppol.php | 8 ++++++-- .../Validation/EntityLevelInterface.php | 3 +++ .../Standards/Validation/Peppol/EntityLevel.php | 16 +++++++++++----- .../Validation/Verifactu/EntityLevel.php | 6 ++++++ 7 files changed, 42 insertions(+), 14 deletions(-) diff --git a/app/Factory/RecurringInvoiceToInvoiceFactory.php b/app/Factory/RecurringInvoiceToInvoiceFactory.php index 957040ce2e..c3024f1cfe 100644 --- a/app/Factory/RecurringInvoiceToInvoiceFactory.php +++ b/app/Factory/RecurringInvoiceToInvoiceFactory.php @@ -114,13 +114,19 @@ class RecurringInvoiceToInvoiceFactory $end_date = $end_date->format('Y-m-d'); - $einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + // $einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + // $ip = new \InvoiceNinja\EInvoice\Models\Peppol\PeriodType\InvoicePeriod(); + // $ip->StartDate = new \DateTime($start_date); + // $ip->EndDate = new \DateTime($end_date); + // $einvoice->InvoicePeriod = [$ip]; - $ip = new \InvoiceNinja\EInvoice\Models\Peppol\PeriodType\InvoicePeriod(); - $ip->StartDate = new \DateTime($start_date); - $ip->EndDate = new \DateTime($end_date); - $einvoice->InvoicePeriod = [$ip]; + // 2026-01-12 - To prevent storing datetime objects in the database, we manually build the InvoicePeriod object + $einvoice = new \stdClass(); + $invoice_period = new \stdClass(); + $invoice_period->StartDate = $start_date; + $invoice_period->EndDate = $end_date; + $einvoice->InvoicePeriod = [$invoice_period]; $stub = new \stdClass(); $stub->Invoice = $einvoice; diff --git a/app/Http/Controllers/EInvoiceController.php b/app/Http/Controllers/EInvoiceController.php index c2737957ec..79c47b1a55 100644 --- a/app/Http/Controllers/EInvoiceController.php +++ b/app/Http/Controllers/EInvoiceController.php @@ -48,6 +48,7 @@ class EInvoiceController extends BaseController match ($request->entity) { 'invoices' => $data = $el->checkInvoice($request->getEntity()), + 'recurring_invoices' => $data = $el->checkRecurringInvoice($request->getEntity()), 'clients' => $data = $el->checkClient($request->getEntity()), 'companies' => $data = $el->checkCompany($request->getEntity()), default => $data['passes'] = false, diff --git a/app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php b/app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php index 7e194936e8..1c9d9ee4d2 100644 --- a/app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php +++ b/app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php @@ -17,8 +17,9 @@ use App\Models\Client; use App\Models\Company; use App\Models\Invoice; use App\Http\Requests\Request; -use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel; use Illuminate\Validation\Rule; +use App\Models\RecurringInvoice; +use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel; class ValidateEInvoiceRequest extends Request { @@ -50,7 +51,7 @@ class ValidateEInvoiceRequest extends Request $user = auth()->user(); return [ - 'entity' => 'required|bail|in:invoices,clients,companies', + 'entity' => 'required|bail|in:invoices,recurring_invoices,clients,companies', 'entity_id' => ['required','bail', Rule::exists($this->entity, 'id') ->when($this->entity != 'companies', function ($q) use ($user) { $q->where('company_id', $user->company()->id); @@ -81,6 +82,7 @@ class ValidateEInvoiceRequest extends Request match ($this->entity) { 'invoices' => $class = Invoice::class, + 'recurring_invoices' => $class = RecurringInvoice::class, 'clients' => $class = Client::class, 'companies' => $class = Company::class, default => $class = Invoice::class, diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 8d857740dd..19c04e168a 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -1349,9 +1349,13 @@ class Peppol extends AbstractService if (isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]) && isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate) && isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate)) { + + $start_date = isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate->date) ? $this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate->date :$this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate; + $end_date = isset($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate->date) ? $this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate->date : $this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate; + $ip = new \InvoiceNinja\EInvoice\Models\Peppol\PeriodType\InvoicePeriod(); - $ip->StartDate = new \DateTime($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate); - $ip->EndDate = new \DateTime($this->invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate); + $ip->StartDate = new \DateTime($start_date); + $ip->EndDate = new \DateTime($end_date); $this->p_invoice->InvoicePeriod = [$ip]; } diff --git a/app/Services/EDocument/Standards/Validation/EntityLevelInterface.php b/app/Services/EDocument/Standards/Validation/EntityLevelInterface.php index 65f281ee7f..bfd7f96a05 100644 --- a/app/Services/EDocument/Standards/Validation/EntityLevelInterface.php +++ b/app/Services/EDocument/Standards/Validation/EntityLevelInterface.php @@ -15,6 +15,7 @@ namespace App\Services\EDocument\Standards\Validation; use App\Models\Client; use App\Models\Company; use App\Models\Invoice; +use App\Models\RecurringInvoice; interface EntityLevelInterface { @@ -24,4 +25,6 @@ interface EntityLevelInterface public function checkInvoice(Invoice $invoice): array; + public function checkRecurringInvoice(RecurringInvoice $recurring_invoice): array; + } diff --git a/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php b/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php index 9e0added3b..10f329da28 100644 --- a/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php +++ b/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php @@ -12,7 +12,7 @@ namespace App\Services\EDocument\Standards\Validation\Peppol; -use App\Exceptions\PeppolValidationException; +use XSLTProcessor; use App\Models\Quote; use App\Models\Client; use App\Models\Credit; @@ -20,11 +20,12 @@ use App\Models\Vendor; use App\Models\Company; use App\Models\Invoice; use App\Models\PurchaseOrder; -use App\Services\EDocument\Standards\Peppol; -use App\Services\EDocument\Standards\Validation\XsltDocumentValidator; -use App\Services\EDocument\Standards\Validation\EntityLevelInterface; +use App\Models\RecurringInvoice; use Illuminate\Support\Facades\App; -use XSLTProcessor; +use App\Services\EDocument\Standards\Peppol; +use App\Exceptions\PeppolValidationException; +use App\Services\EDocument\Standards\Validation\EntityLevelInterface; +use App\Services\EDocument\Standards\Validation\XsltDocumentValidator; class EntityLevel implements EntityLevelInterface { @@ -124,6 +125,11 @@ class EntityLevel implements EntityLevelInterface } + public function checkRecurringInvoice(RecurringInvoice $recurring_invoice): array + { + return ['passes' => true]; + } + public function checkInvoice(Invoice $invoice): array { $this->init($invoice->client->locale()); diff --git a/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php b/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php index ee8460852d..52599630a3 100644 --- a/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php +++ b/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php @@ -19,6 +19,7 @@ use App\Models\Vendor; use App\Models\Company; use App\Models\Invoice; use App\Models\PurchaseOrder; +use App\Models\RecurringInvoice; use Illuminate\Support\Facades\App; use App\Services\EDocument\Standards\Validation\EntityLevelInterface; @@ -84,6 +85,11 @@ class EntityLevel implements EntityLevelInterface } + public function checkRecurringInvoice(RecurringInvoice $recurring_invoice): array + { + return ['passes' => true]; + } + public function checkInvoice(Invoice $invoice): array { From 3bd5d04316200f366ba1be1cc23c7b4bb06852cc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Jan 2026 09:07:09 +1100 Subject: [PATCH 073/177] Fixes for Invoice Period Test --- .../EInvoice/RequestValidation/InvoicePeriodTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Feature/EInvoice/RequestValidation/InvoicePeriodTest.php b/tests/Feature/EInvoice/RequestValidation/InvoicePeriodTest.php index 39b4b7e98c..adf77f56a0 100644 --- a/tests/Feature/EInvoice/RequestValidation/InvoicePeriodTest.php +++ b/tests/Feature/EInvoice/RequestValidation/InvoicePeriodTest.php @@ -3,10 +3,11 @@ namespace Tests\Feature\EInvoice\RequestValidation; use Tests\TestCase; +use Tests\MockAccountData; use Illuminate\Support\Facades\Validator; +use App\Factory\RecurringInvoiceToInvoiceFactory; use App\Http\Requests\Invoice\UpdateInvoiceRequest; use Illuminate\Routing\Middleware\ThrottleRequests; -use Tests\MockAccountData; class InvoicePeriodTest extends TestCase { @@ -85,10 +86,10 @@ class InvoicePeriodTest extends TestCase $this->recurring_invoice = $this->recurring_invoice->fresh(); - $invoice = \App\Factory\RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); + $invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); - $this->assertEquals($invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate->date, now()->setTimezone($this->recurring_invoice->client->timezone()->name)->startOfMonth()->startOfDay()->format('Y-m-d H:i:s.u')); - $this->assertEquals($invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate->date, now()->setTimezone($this->recurring_invoice->client->timezone()->name)->endOfMonth()->startOfDay()->format('Y-m-d H:i:s.u')); + $this->assertEquals($invoice->e_invoice->Invoice->InvoicePeriod[0]->StartDate, now()->setTimezone($this->recurring_invoice->client->timezone()->name)->startOfMonth()->startOfDay()->format('Y-m-d')); + $this->assertEquals($invoice->e_invoice->Invoice->InvoicePeriod[0]->EndDate, now()->setTimezone($this->recurring_invoice->client->timezone()->name)->endOfMonth()->startOfDay()->format('Y-m-d')); } From ea071f290432e7ac61dc80d0d427463de2659172 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Jan 2026 10:37:35 +1100 Subject: [PATCH 074/177] Fixes for paths for search expenses --- app/Http/Controllers/SearchController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 0277e787c0..2a52644431 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -232,7 +232,7 @@ class SearchController extends Controller 'name' => $result['_source']['name'], 'type' => '/expense', 'id' => $result['_source']['hashed_id'], - 'path' => "/expenses/{$result['_source']['hashed_id']}" + 'path' => "/expenses/{$result['_source']['hashed_id']}/edit" ]; break; From 4697ab08c9abecf164084199dea64ffba2587532 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Jan 2026 16:04:49 +1100 Subject: [PATCH 075/177] Adjustments for Nordigen, requisition email notifications changed to once per account, not company --- app/Helpers/Bank/Nordigen/Nordigen.php | 6 +++--- .../Controllers/BankIntegrationController.php | 2 +- .../Bank/ProcessBankTransactionsNordigen.php | 19 ++++++++++++++----- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/Helpers/Bank/Nordigen/Nordigen.php b/app/Helpers/Bank/Nordigen/Nordigen.php index 77e5756309..026ef58533 100644 --- a/app/Helpers/Bank/Nordigen/Nordigen.php +++ b/app/Helpers/Bank/Nordigen/Nordigen.php @@ -297,10 +297,10 @@ class Nordigen * getTransactions * * @param string $accountId - * @param string $dateFrom + * @param ?string $dateFrom * @return array */ - public function getTransactions(Company $company, string $accountId, string $dateFrom = null): array + public function getTransactions(Company $company, string $accountId, ?string $dateFrom = null): array { $transactionResponse = $this->client->account($accountId)->getAccountTransactions($dateFrom); @@ -310,7 +310,7 @@ class Nordigen public function disabledAccountEmail(BankIntegration $bank_integration): void { - $cache_key = "email_quota:{$bank_integration->company->company_key}:{$bank_integration->id}"; + $cache_key = "email_quota:{$bank_integration->account->key}:bank_integration_notified"; if (Cache::has($cache_key)) { return; diff --git a/app/Http/Controllers/BankIntegrationController.php b/app/Http/Controllers/BankIntegrationController.php index f8fe5c431e..8567b88b12 100644 --- a/app/Http/Controllers/BankIntegrationController.php +++ b/app/Http/Controllers/BankIntegrationController.php @@ -286,7 +286,7 @@ class BankIntegrationController extends BaseController $bank_integration->bank_account_status = "429 Rate limit reached, check back later...."; $bank_integration->save(); return; - } elseif (is_array($account) && isset($account['account_status']) && !in_array($account['account_status'], ['READY', 'PROCESSING','DISCOVERED'])) { + } elseif (is_array($account) && isset($account['account_status']) && !in_array($account['account_status'], ['READY', 'PROCESSING', 'DISCOVERED'])) { $bank_integration->disabled_upstream = true; $bank_integration->save(); diff --git a/app/Jobs/Bank/ProcessBankTransactionsNordigen.php b/app/Jobs/Bank/ProcessBankTransactionsNordigen.php index dcc4880a1c..cc8566e76f 100644 --- a/app/Jobs/Bank/ProcessBankTransactionsNordigen.php +++ b/app/Jobs/Bank/ProcessBankTransactionsNordigen.php @@ -98,6 +98,10 @@ class ProcessBankTransactionsNordigen implements ShouldQueue // UPDATE TRANSACTIONS try { $this->processTransactions(); + + // Perform Matching + BankMatchingService::dispatch($this->company->id, $this->company->db); + } catch (\Exception $e) { nlog("Nordigen: {$this->bank_integration->nordigen_account_id} - exited abnormally => " . $e->getMessage()); @@ -109,11 +113,9 @@ class ProcessBankTransactionsNordigen implements ShouldQueue $this->bank_integration->company->notification(new GenericNinjaAdminNotification($content))->ninja(); - throw $e; + // throw $e; } - // Perform Matching - BankMatchingService::dispatch($this->company->id, $this->company->db); } // const DISCOVERED = 'DISCOVERED'; // Account was discovered but not yet processed @@ -163,8 +165,10 @@ class ProcessBankTransactionsNordigen implements ShouldQueue private function processTransactions() { //Get transaction count object + $transactions = []; + $transactions = $this->nordigen->getTransactions($this->company, $this->bank_integration->nordigen_account_id, $this->from_date); - + //if no transactions, update the from_date and move on if (count($transactions) == 0) { @@ -189,7 +193,12 @@ class ProcessBankTransactionsNordigen implements ShouldQueue foreach ($transactions as $transaction) { - if (BankTransaction::where('nordigen_transaction_id', $transaction['nordigen_transaction_id'])->where('company_id', $this->company->id)->where('bank_integration_id', $this->bank_integration->id)->where('is_deleted', 0)->withTrashed()->exists()) { + if (BankTransaction::where('nordigen_transaction_id', $transaction['nordigen_transaction_id']) + ->where('company_id', $this->company->id) + ->where('bank_integration_id', $this->bank_integration->id) + ->where('is_deleted', 0) + ->withTrashed() + ->exists()) { continue; } From 86af7367b2d6cd0a15e768f412602790d094e4ee Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Jan 2026 16:11:44 +1100 Subject: [PATCH 076/177] Ensure archived/deleted yodlee accounts are not resync'd incorrectly: --- app/Http/Controllers/Bank/YodleeController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Http/Controllers/Bank/YodleeController.php b/app/Http/Controllers/Bank/YodleeController.php index 40297128a7..26b08667e7 100644 --- a/app/Http/Controllers/Bank/YodleeController.php +++ b/app/Http/Controllers/Bank/YodleeController.php @@ -78,6 +78,11 @@ class YodleeController extends BaseController foreach ($accounts as $account) { if ($bi = BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $company->id)->first()) { + + if($bi->deleted_at){ + continue; + } + $bi->disabled_upstream = false; $bi->balance = $account['current_balance']; $bi->currency = $account['account_currency']; From 2b525ad2797470a49407ecd9c408461cb1285f48 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Jan 2026 16:21:45 +1100 Subject: [PATCH 077/177] Adjustments for syncing Yodlee accounts + transactions --- .../Bank/ProcessBankTransactionsYodlee.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/Jobs/Bank/ProcessBankTransactionsYodlee.php b/app/Jobs/Bank/ProcessBankTransactionsYodlee.php index c70e5cc472..b8d4f1daf7 100644 --- a/app/Jobs/Bank/ProcessBankTransactionsYodlee.php +++ b/app/Jobs/Bank/ProcessBankTransactionsYodlee.php @@ -163,7 +163,25 @@ class ProcessBankTransactionsYodlee implements ShouldQueue $now = now(); foreach ($transactions as $transaction) { - if (BankTransaction::query()->where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->where('bank_integration_id', $this->bank_integration->id)->withTrashed()->exists()) { + if (BankTransaction::query() //ensure we don't duplicate transactions with the same ID + ->where('transaction_id', $transaction['transaction_id']) + ->where('company_id', $this->company->id) + ->where('bank_integration_id', $this->bank_integration->id) + ->withTrashed() + ->exists()) { + continue; + } + elseif (BankTransaction::query() //ensure we don't duplicate transactions that have the same amount, currency, account type, category type, date, and description + ->where('company_id', $this->company->id) + ->where('bank_integration_id', $this->bank_integration->id) + ->where('amount', $transaction['amount']) + ->where('currency_id', $transaction['currency_id']) + ->where('account_type', $transaction['account_type']) + ->where('category_type', $transaction['category_type']) + ->where('date', $transaction['date']) + ->where('description', $transaction['description']) + ->withTrashed() + ->exists()) { continue; } From 250549f78dddd94a5adc1c99e13c470c122ec757 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Jan 2026 17:01:11 +1100 Subject: [PATCH 078/177] Purge user functionality --- app/Http/Controllers/UserController.php | 47 ++-- app/Http/Requests/User/PurgeUserRequest.php | 28 +++ app/Models/User.php | 190 +++++++++++++++ app/Repositories/UserRepository.php | 77 +++++- routes/api.php | 1 + tests/Feature/UserTest.php | 256 ++++++++++++++++++++ 6 files changed, 573 insertions(+), 26 deletions(-) create mode 100644 app/Http/Requests/User/PurgeUserRequest.php diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index c66b253e5c..61345be2cc 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -12,31 +12,32 @@ namespace App\Http\Controllers; +use App\Models\User; +use App\Utils\Ninja; +use App\Models\CompanyUser; +use App\Factory\UserFactory; +use App\Filters\UserFilters; +use Illuminate\Http\Response; +use App\Utils\Traits\MakesHash; use App\Events\User\UserWasCreated; use App\Events\User\UserWasDeleted; use App\Events\User\UserWasUpdated; -use App\Factory\UserFactory; -use App\Filters\UserFilters; -use App\Http\Controllers\Traits\VerifiesUserEmail; -use App\Http\Requests\User\BulkUserRequest; -use App\Http\Requests\User\CreateUserRequest; -use App\Http\Requests\User\DestroyUserRequest; -use App\Http\Requests\User\DetachCompanyUserRequest; -use App\Http\Requests\User\DisconnectUserMailerRequest; -use App\Http\Requests\User\EditUserRequest; -use App\Http\Requests\User\ReconfirmUserRequest; -use App\Http\Requests\User\ShowUserRequest; -use App\Http\Requests\User\StoreUserRequest; -use App\Http\Requests\User\UpdateUserRequest; -use App\Jobs\Company\CreateCompanyToken; use App\Jobs\User\UserEmailChanged; -use App\Models\CompanyUser; -use App\Models\User; use App\Repositories\UserRepository; use App\Transformers\UserTransformer; -use App\Utils\Ninja; -use App\Utils\Traits\MakesHash; -use Illuminate\Http\Response; +use App\Jobs\Company\CreateCompanyToken; +use App\Http\Requests\User\BulkUserRequest; +use App\Http\Requests\User\EditUserRequest; +use App\Http\Requests\User\ShowUserRequest; +use App\Http\Requests\User\PurgeUserRequest; +use App\Http\Requests\User\StoreUserRequest; +use App\Http\Requests\User\CreateUserRequest; +use App\Http\Requests\User\UpdateUserRequest; +use App\Http\Requests\User\DestroyUserRequest; +use App\Http\Requests\User\ReconfirmUserRequest; +use App\Http\Controllers\Traits\VerifiesUserEmail; +use App\Http\Requests\User\DetachCompanyUserRequest; +use App\Http\Requests\User\DisconnectUserMailerRequest; /** * Class UserController. @@ -350,4 +351,12 @@ class UserController extends BaseController } + public function purge(PurgeUserRequest $request, User $user) + { + $this->user_repo->purge($user, auth()->user()); + + return response()->noContent(); + + } + } diff --git a/app/Http/Requests/User/PurgeUserRequest.php b/app/Http/Requests/User/PurgeUserRequest.php new file mode 100644 index 0000000000..452ebf33ed --- /dev/null +++ b/app/Http/Requests/User/PurgeUserRequest.php @@ -0,0 +1,28 @@ +user()->isOwner() && auth()->user()->id !== $this->user->id; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 29ac538e3b..00fe0a2a1e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -351,6 +351,196 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasMany(Client::class); } + public function activities(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Activity::class); + } + + public function bank_integrations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(BankIntegration::class)->withTrashed(); + } + + public function bank_transaction_rules(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(BankTransactionRule::class)->withTrashed(); + } + + public function bank_transactions(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(BankTransaction::class)->withTrashed(); + } + + public function client_contacts(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(ClientContact::class)->withTrashed(); + } + + public function company_gateways(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(CompanyGateway::class)->withTrashed(); + } + + public function company_ledgers(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(CompanyLedger::class); + } + + public function company_tokens(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(CompanyToken::class)->withTrashed(); + } + + public function credit_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(CreditInvitation::class)->withTrashed(); + } + + public function credits(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Credit::class)->withTrashed(); + } + + public function designs(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Design::class)->withTrashed(); + } + + public function expense_categories(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(ExpenseCategory::class)->withTrashed(); + } + + public function expenses(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Expense::class)->withTrashed(); + } + + public function group_settings(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(GroupSetting::class)->withTrashed(); + } + + public function invoice_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(InvoiceInvitation::class)->withTrashed(); + } + + public function invoices(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Invoice::class)->withTrashed(); + } + + public function locations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Location::class)->withTrashed(); + } + + public function payment_terms(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(PaymentTerm::class)->withTrashed(); + } + + public function payments(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Payment::class)->withTrashed(); + } + + public function products(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Product::class)->withTrashed(); + } + + public function projects(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Project::class)->withTrashed(); + } + + public function purchase_order_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(PurchaseOrderInvitation::class)->withTrashed(); + } + + public function purchase_orders(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(PurchaseOrder::class)->withTrashed(); + } + + public function quote_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(QuoteInvitation::class)->withTrashed(); + } + + public function quotes(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Quote::class)->withTrashed(); + } + + public function recurring_expenses(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(RecurringExpense::class)->withTrashed(); + } + + public function recurring_invoice_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(RecurringInvoiceInvitation::class)->withTrashed(); + } + + public function recurring_invoices(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(RecurringInvoice::class)->withTrashed(); + } + + public function recurring_quotes(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(RecurringQuote::class)->withTrashed(); + } + + public function recurring_quote_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(RecurringQuoteInvitation::class)->withTrashed(); + } + + public function schedules(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Scheduler::class)->withTrashed(); + } + + public function system_logs(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(SystemLog::class)->withTrashed(); + } + + public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Task::class)->withTrashed(); + } + + public function task_statuses(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(TaskStatus::class)->withTrashed(); + } + + public function tax_rates(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(TaxRate::class)->withTrashed(); + } + + public function vendor_contacts(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(VendorContact::class)->withTrashed(); + } + + public function vendors(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Vendor::class)->withTrashed(); + } + + public function webhooks(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Webhook::class)->withTrashed(); + } + /** * Returns a comma separated list of user permissions. * diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index 9b4b442e4d..a7c79c54e6 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -12,17 +12,33 @@ namespace App\Repositories; -use App\DataMapper\CompanySettings; -use App\Events\User\UserWasArchived; -use App\Events\User\UserWasDeleted; -use App\Events\User\UserWasRestored; -use App\Jobs\Company\CreateCompanyToken; -use App\Models\CompanyUser; +use App\Models\Task; use App\Models\User; use App\Utils\Ninja; -use App\Utils\Traits\MakesHash; +use App\Models\Quote; +use App\Models\Client; +use App\Models\Credit; +use App\Models\Vendor; +use App\Models\Expense; +use App\Models\Invoice; +use App\Models\Payment; +use App\Models\Product; +use App\Models\Project; +use App\Models\CompanyUser; use Illuminate\Http\Request; +use App\Models\PurchaseOrder; +use App\Models\RecurringQuote; +use App\Utils\Traits\MakesHash; +use App\Models\RecurringExpense; +use App\Models\RecurringInvoice; +use Illuminate\Support\Facades\DB; +use App\DataMapper\CompanySettings; +use App\Events\User\UserWasDeleted; +use App\Events\User\UserWasArchived; +use App\Events\User\UserWasRestored; +use App\Repositories\BaseRepository; use Illuminate\Support\Facades\Hash; +use App\Jobs\Company\CreateCompanyToken; /** * UserRepository. @@ -242,4 +258,51 @@ class UserRepository extends BaseRepository }); } } + + public function purge(User $user, User $new_owner_user): void + { + + \DB::transaction(function () use ($user, $new_owner_user) { + + // Relations to transfer user_id to new owner + $allRelations = [ + 'activities', 'bank_integrations', 'bank_transaction_rules', + 'bank_transactions', 'client_contacts', 'company_gateways', + 'company_ledgers', 'company_tokens', 'credit_invitations', + 'designs', 'expense_categories', 'group_settings', + 'invoice_invitations', 'locations', 'payment_terms', + 'quote_invitations', 'purchase_order_invitations', + 'recurring_invoice_invitations', 'recurring_quote_invitations', + 'schedules', 'system_logs', 'task_statuses', 'tax_rates', + 'vendor_contacts', 'webhooks', + // Models that also have assigned_user_id + 'clients', 'invoices', 'credits', 'quotes', 'payments', + 'expenses', 'tasks', 'projects', 'vendors', 'products', + 'purchase_orders', 'recurring_invoices', 'recurring_expenses', + 'recurring_quotes', + ]; + + foreach ($allRelations as $relation) { + $user->{$relation}()->update(['user_id' => $new_owner_user->id]); + } + + // Models with both user_id and assigned_user_id + $modelsWithAssignedUser = [ + Client::class, Invoice::class, Credit::class, Quote::class, + Payment::class, Expense::class, Task::class, Project::class, + Vendor::class, Product::class, PurchaseOrder::class, + RecurringInvoice::class, RecurringExpense::class, RecurringQuote::class, + ]; + + foreach ($modelsWithAssignedUser as $model) { + // Null out assigned_user_id + $model::withTrashed() + ->where('assigned_user_id', $user->id) + ->update(['assigned_user_id' => null]); + } + + $user->forceDelete(); + }); + + } } diff --git a/routes/api.php b/routes/api.php index a2204d6375..f55db58e6d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -436,6 +436,7 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local Route::post('/users/{user}/disconnect_mailer', [UserController::class, 'disconnectOauthMailer']); Route::post('/users/{user}/disconnect_oauth', [UserController::class, 'disconnectOauth']); Route::post('/user/{user}/reconfirm', [UserController::class, 'reconfirm']); + Route::post('/user/{user}/purge', [UserController::class, 'purge'])->middleware('password_protected'); Route::resource('webhooks', WebhookController::class); Route::post('webhooks/bulk', [WebhookController::class, 'bulk'])->name('webhooks.bulk'); diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 168ba19ea8..59f5711762 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -755,4 +755,260 @@ class UserTest extends TestCase $this->assertFalse($arr['data']['company_user']['is_owner']); $this->assertEquals($arr['data']['company_user']['permissions'], 'create_invoice,create_invoice'); } + + public function testPurgeUserTransfersEntities() + { + // Create account and owner user + $account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000, + ]); + + $account->num_users = 3; + $account->save(); + + $owner_user = User::factory()->create([ + 'account_id' => $account->id, + 'email' => \Illuminate\Support\Str::random(32)."@example.com", + ]); + + $settings = CompanySettings::defaults(); + + $company = Company::factory()->create([ + 'account_id' => $account->id, + 'settings' => $settings, + ]); + + $owner_user->companies()->attach($company->id, [ + 'account_id' => $account->id, + 'is_owner' => 1, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); + + // Create secondary user to be purged + $secondary_user = User::factory()->create([ + 'account_id' => $account->id, + 'email' => \Illuminate\Support\Str::random(32)."@example.com", + ]); + + $secondary_user->companies()->attach($company->id, [ + 'account_id' => $account->id, + 'is_owner' => 0, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); + + // Create a client owned by secondary user + $client = \App\Models\Client::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'assigned_user_id' => $secondary_user->id, + ]); + + // Create client contact + $client_contact = \App\Models\ClientContact::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'client_id' => $client->id, + 'is_primary' => true, + ]); + + // Create invoice owned by secondary user + $invoice = \App\Models\Invoice::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'client_id' => $client->id, + 'assigned_user_id' => $secondary_user->id, + 'status_id' => \App\Models\Invoice::STATUS_DRAFT, + ]); + $invoice = $invoice->service()->createInvitations()->markSent()->save(); + + // Create credit owned by secondary user + $credit = \App\Models\Credit::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'client_id' => $client->id, + 'assigned_user_id' => $secondary_user->id, + 'status_id' => \App\Models\Credit::STATUS_DRAFT, + ]); + + $credit = $credit->service()->createInvitations()->markSent()->save(); + + // Create quote owned by secondary user + $quote = \App\Models\Quote::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'client_id' => $client->id, + 'assigned_user_id' => $secondary_user->id, + 'status_id' => \App\Models\Quote::STATUS_DRAFT, + ]); + $quote = $quote->service()->createInvitations()->markSent()->save(); + + // Create recurring invoice owned by secondary user + $recurring_invoice = \App\Models\RecurringInvoice::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'client_id' => $client->id, + 'assigned_user_id' => $secondary_user->id, + 'status_id' => \App\Models\RecurringInvoice::STATUS_DRAFT, + ]); + + $recurring_invoice = $recurring_invoice->service()->createInvitations()->start()->save(); + // Create expense owned by secondary user + $expense = \App\Models\Expense::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'assigned_user_id' => $secondary_user->id, + ]); + + // Create task owned by secondary user + $task = \App\Models\Task::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'client_id' => $client->id, + 'assigned_user_id' => $secondary_user->id, + ]); + + // Create vendor owned by secondary user + $vendor = \App\Models\Vendor::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'assigned_user_id' => $secondary_user->id, + ]); + + // Create vendor contact + $vendor_contact = \App\Models\VendorContact::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'vendor_id' => $vendor->id, + 'is_primary' => true, + ]); + + // Create product owned by secondary user + $product = \App\Models\Product::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'assigned_user_id' => $secondary_user->id, + ]); + + // Create project owned by secondary user + $project = \App\Models\Project::factory()->create([ + 'user_id' => $secondary_user->id, + 'company_id' => $company->id, + 'client_id' => $client->id, + 'assigned_user_id' => $secondary_user->id, + ]); + + // Create an entity owned by owner but assigned to secondary user + $invoice_assigned_only = \App\Models\Invoice::factory()->create([ + 'user_id' => $owner_user->id, + 'company_id' => $company->id, + 'client_id' => $client->id, + 'assigned_user_id' => $secondary_user->id, + ]); + + + $invoice = $invoice->load('invitations'); + + $this->assertCount(1, $invoice->invitations); + $this->assertCount(1, $recurring_invoice->invitations); + // Store IDs for later assertions + $secondary_user_id = $secondary_user->id; + $client_id = $client->id; + $client_contact_id = $client_contact->id; + $invoice_id = $invoice->id; + $invoice_invitation_id = $invoice->invitations()->first()->id; + $credit_id = $credit->id; + $credit_invitation_id = $credit->invitations()->first()->id; + $quote_id = $quote->id; + $quote_invitation_id = $quote->invitations()->first()->id; + $recurring_invoice_id = $recurring_invoice->id; + $expense_id = $expense->id; + $task_id = $task->id; + $vendor_id = $vendor->id; + $vendor_contact_id = $vendor_contact->id; + $product_id = $product->id; + $project_id = $project->id; + $invoice_assigned_only_id = $invoice_assigned_only->id; + + // Perform the purge + $user_repo = new UserRepository(); + $user_repo->purge($secondary_user, $owner_user); + + // Assert secondary user is deleted + $this->assertNull(User::find($secondary_user_id)); + + // Assert all entities are now owned by owner user + $client = \App\Models\Client::find($client_id); + $this->assertEquals($owner_user->id, $client->user_id); + $this->assertNull($client->assigned_user_id); + + // Assert client contact user_id updated + $client_contact = \App\Models\ClientContact::find($client_contact_id); + $this->assertEquals($owner_user->id, $client_contact->user_id); + + $invoice = \App\Models\Invoice::find($invoice_id); + $this->assertEquals($owner_user->id, $invoice->user_id); + $this->assertNull($invoice->assigned_user_id); + + // Assert invoice invitation user_id updated + $invoice_invitation = \App\Models\InvoiceInvitation::find($invoice_invitation_id); + $this->assertEquals($owner_user->id, $invoice_invitation->user_id); + + $credit = \App\Models\Credit::find($credit_id); + $this->assertEquals($owner_user->id, $credit->user_id); + $this->assertNull($credit->assigned_user_id); + + // Assert credit invitation user_id updated + $credit_invitation = \App\Models\CreditInvitation::find($credit_invitation_id); + $this->assertEquals($owner_user->id, $credit_invitation->user_id); + + $quote = \App\Models\Quote::find($quote_id); + $this->assertEquals($owner_user->id, $quote->user_id); + $this->assertNull($quote->assigned_user_id); + + // Assert quote invitation user_id updated + $quote_invitation = \App\Models\QuoteInvitation::find($quote_invitation_id); + $this->assertEquals($owner_user->id, $quote_invitation->user_id); + + $recurring_invoice = \App\Models\RecurringInvoice::find($recurring_invoice_id); + $this->assertEquals($owner_user->id, $recurring_invoice->user_id); + $this->assertNull($recurring_invoice->assigned_user_id); + + $expense = \App\Models\Expense::find($expense_id); + $this->assertEquals($owner_user->id, $expense->user_id); + $this->assertNull($expense->assigned_user_id); + + $task = \App\Models\Task::find($task_id); + $this->assertEquals($owner_user->id, $task->user_id); + $this->assertNull($task->assigned_user_id); + + $vendor = \App\Models\Vendor::find($vendor_id); + $this->assertEquals($owner_user->id, $vendor->user_id); + $this->assertNull($vendor->assigned_user_id); + + // Assert vendor contact user_id updated + $vendor_contact = \App\Models\VendorContact::find($vendor_contact_id); + $this->assertEquals($owner_user->id, $vendor_contact->user_id); + + $product = \App\Models\Product::find($product_id); + $this->assertEquals($owner_user->id, $product->user_id); + $this->assertNull($product->assigned_user_id); + + $project = \App\Models\Project::find($project_id); + $this->assertEquals($owner_user->id, $project->user_id); + $this->assertNull($project->assigned_user_id); + + // Assert entity owned by owner but assigned to secondary now has null assigned_user_id + $invoice_assigned_only = \App\Models\Invoice::find($invoice_assigned_only_id); + $this->assertEquals($owner_user->id, $invoice_assigned_only->user_id); + $this->assertNull($invoice_assigned_only->assigned_user_id); + } } From d94ea48bbff31e71852c6e78029af27997e20733 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 13 Jan 2026 10:40:28 +1100 Subject: [PATCH 079/177] Translation for purge user --- app/Repositories/UserRepository.php | 10 +++++++++- lang/en/texts.php | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index a7c79c54e6..90f50e20b7 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -258,7 +258,15 @@ class UserRepository extends BaseRepository }); } } - + + /** + * purge a user and all of their data across + * all companies and accounts. + * + * @param User $user + * @param User $new_owner_user + * @return void + */ public function purge(User $user, User $new_owner_user): void { diff --git a/lang/en/texts.php b/lang/en/texts.php index 2d5b6c1ac7..5b578e9dd5 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5686,6 +5686,7 @@ $lang = array( 'enter_reason' => 'Enter a reason...', 'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date', 'notification_invoice_overdue_summary' => 'The following invoices are overdue:', + 'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?', ); return $lang; From 89c79daabddd29c83ce3530c9520ce979be7a643 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 13 Jan 2026 10:49:54 +1100 Subject: [PATCH 080/177] Updates for Swiss QR and moving to structured address --- app/Helpers/SwissQr/SwissQrGenerator.php | 12 +- composer.json | 2 +- composer.lock | 899 +++++++++++++---------- 3 files changed, 531 insertions(+), 382 deletions(-) diff --git a/app/Helpers/SwissQr/SwissQrGenerator.php b/app/Helpers/SwissQr/SwissQrGenerator.php index 6f896e16a4..0859f7271a 100644 --- a/app/Helpers/SwissQr/SwissQrGenerator.php +++ b/app/Helpers/SwissQr/SwissQrGenerator.php @@ -68,10 +68,12 @@ class SwissQrGenerator // Add creditor information // Who will receive the payment and to which bank account? $qrBill->setCreditor( - QrBill\DataGroup\Element\CombinedAddress::create( - $this->company->present()->name(), - $this->company->present()->address1(), - $this->company->present()->getCompanyCityState(), + QrBill\DataGroup\Element\StructuredAddress::createWithStreet( + substr($this->company->present()->name(), 0, 70), + $this->company->settings->address1 ? substr($this->company->settings->address1, 0, 70) : ' ', + $this->company->settings->address2 ? substr($this->company->settings->address2, 0, 16) : ' ', + $this->company->settings->postal_code ? substr($this->company->settings->postal_code, 0, 16) : ' ', + $this->company->settings->city ? substr($this->company->settings->city, 0, 35) : ' ', 'CH' ) ); @@ -170,7 +172,7 @@ class SwissQrGenerator $output = new QrBill\PaymentPart\Output\HtmlOutput\HtmlOutput($qrBill, $this->resolveLanguage()); $html = $output - ->setPrintable(false) + // ->setPrintable(false) ->getPaymentPart(); // return $html; diff --git a/composer.json b/composer.json index 121bad8ecf..9686b51f48 100644 --- a/composer.json +++ b/composer.json @@ -102,7 +102,7 @@ "setasign/fpdi": "^2.6", "socialiteproviders/apple": "dev-master", "socialiteproviders/microsoft": "^4.1", - "sprain/swiss-qr-bill": "^4.3", + "sprain/swiss-qr-bill": "^5.2", "square/square": "30.0.0.*", "stripe/stripe-php": "^17", "symfony/brevo-mailer": "^7.1", diff --git a/composer.lock b/composer.lock index b279821627..9100dc38a9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bbc9c82c8904f23fad218ab0485d2de5", + "content-hash": "007bf7c6e51e1a5429cf7fc81904a14e", "packages": [ { "name": "afosto/yaac", @@ -381,16 +381,16 @@ }, { "name": "awobaz/compoships", - "version": "2.5.3", + "version": "2.5.4", "source": { "type": "git", "url": "https://github.com/topclaudy/compoships.git", - "reference": "61beaab6dfbeb9a1c253e880804b63d45a6ba973" + "reference": "dcae8012a8704fc2acd8dce2d8a1b35ce292adbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/topclaudy/compoships/zipball/61beaab6dfbeb9a1c253e880804b63d45a6ba973", - "reference": "61beaab6dfbeb9a1c253e880804b63d45a6ba973", + "url": "https://api.github.com/repos/topclaudy/compoships/zipball/dcae8012a8704fc2acd8dce2d8a1b35ce292adbe", + "reference": "dcae8012a8704fc2acd8dce2d8a1b35ce292adbe", "shasum": "" }, "require": { @@ -431,7 +431,7 @@ ], "support": { "issues": "https://github.com/topclaudy/compoships/issues", - "source": "https://github.com/topclaudy/compoships/tree/2.5.3" + "source": "https://github.com/topclaudy/compoships/tree/2.5.4" }, "funding": [ { @@ -439,7 +439,7 @@ "type": "custom" } ], - "time": "2025-12-17T13:24:30+00:00" + "time": "2025-12-23T18:33:46+00:00" }, { "name": "aws/aws-crt-php", @@ -497,16 +497,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.0", + "version": "3.369.11", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "2bbe45aaaaa23a863a5daadcda326cf1c8b4a15b" + "reference": "014c521eae8eeb1f9562cb512523b17cd6c1bec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2bbe45aaaaa23a863a5daadcda326cf1c8b4a15b", - "reference": "2bbe45aaaaa23a863a5daadcda326cf1c8b4a15b", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/014c521eae8eeb1f9562cb512523b17cd6c1bec7", + "reference": "014c521eae8eeb1f9562cb512523b17cd6c1bec7", "shasum": "" }, "require": { @@ -520,7 +520,7 @@ "mtdowling/jmespath.php": "^2.8.0", "php": ">=8.1", "psr/http-message": "^1.0 || ^2.0", - "symfony/filesystem": "^v6.4.3 || ^v7.1.0 || ^v8.0.0" + "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -588,9 +588,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.369.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.11" }, - "time": "2025-12-19T19:08:40+00:00" + "time": "2026-01-12T19:14:19+00:00" }, { "name": "babenkoivan/elastic-adapter", @@ -2169,25 +2169,25 @@ }, { "name": "dompdf/php-svg-lib", - "version": "1.0.0", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af" + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af", - "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^7.1 || ^8.0", - "sabberworm/php-css-parser": "^8.4" + "sabberworm/php-css-parser": "^8.4 || ^9.0" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" }, "type": "library", "autoload": { @@ -2209,9 +2209,9 @@ "homepage": "https://github.com/dompdf/php-svg-lib", "support": { "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0" + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" }, - "time": "2024-04-29T13:26:35+00:00" + "time": "2026-01-02T16:01:13+00:00" }, { "name": "dragonmantank/cron-expression", @@ -2967,20 +2967,20 @@ }, { "name": "google/apiclient", - "version": "v2.18.4", + "version": "v2.19.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client.git", - "reference": "5b51fdb2cbd2a96088e3dfc6f565bdf6fb0af94b" + "reference": "b18fa8aed7b2b2dd4bcce74e2c7d267e16007ea9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/5b51fdb2cbd2a96088e3dfc6f565bdf6fb0af94b", - "reference": "5b51fdb2cbd2a96088e3dfc6f565bdf6fb0af94b", + "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/b18fa8aed7b2b2dd4bcce74e2c7d267e16007ea9", + "reference": "b18fa8aed7b2b2dd4bcce74e2c7d267e16007ea9", "shasum": "" }, "require": { - "firebase/php-jwt": "^6.0", + "firebase/php-jwt": "^6.0||^7.0", "google/apiclient-services": "~0.350", "google/auth": "^1.37", "guzzlehttp/guzzle": "^7.4.5", @@ -3030,22 +3030,22 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client/issues", - "source": "https://github.com/googleapis/google-api-php-client/tree/v2.18.4" + "source": "https://github.com/googleapis/google-api-php-client/tree/v2.19.0" }, - "time": "2025-09-30T04:23:07+00:00" + "time": "2026-01-09T19:59:47+00:00" }, { "name": "google/apiclient-services", - "version": "v0.424.0", + "version": "v0.428.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client-services.git", - "reference": "b368dcc5dce8043fed1b248a66020747d5cec353" + "reference": "94a3c50a80a36cafb76e32fb76b8007e9f572deb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/b368dcc5dce8043fed1b248a66020747d5cec353", - "reference": "b368dcc5dce8043fed1b248a66020747d5cec353", + "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/94a3c50a80a36cafb76e32fb76b8007e9f572deb", + "reference": "94a3c50a80a36cafb76e32fb76b8007e9f572deb", "shasum": "" }, "require": { @@ -3074,26 +3074,26 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client-services/issues", - "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.424.0" + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.428.0" }, - "time": "2025-12-15T00:58:22+00:00" + "time": "2026-01-12T00:58:26+00:00" }, { "name": "google/auth", - "version": "v1.49.0", + "version": "v1.50.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-auth-library-php.git", - "reference": "68e3d88cb59a49f713e3db25d4f6bb3cc0b70764" + "reference": "e1c26a718198e16d8a3c69b1cae136b73f959b0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/68e3d88cb59a49f713e3db25d4f6bb3cc0b70764", - "reference": "68e3d88cb59a49f713e3db25d4f6bb3cc0b70764", + "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/e1c26a718198e16d8a3c69b1cae136b73f959b0f", + "reference": "e1c26a718198e16d8a3c69b1cae136b73f959b0f", "shasum": "" }, "require": { - "firebase/php-jwt": "^6.0", + "firebase/php-jwt": "^6.0||^7.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.4.5", "php": "^8.1", @@ -3103,7 +3103,7 @@ }, "require-dev": { "guzzlehttp/promises": "^2.0", - "kelvinmo/simplejwt": "0.7.1", + "kelvinmo/simplejwt": "^1.1.0", "phpseclib/phpseclib": "^3.0.35", "phpspec/prophecy-phpunit": "^2.1", "phpunit/phpunit": "^9.6", @@ -3111,7 +3111,7 @@ "squizlabs/php_codesniffer": "^4.0", "symfony/filesystem": "^6.3||^7.3", "symfony/process": "^6.0||^7.0", - "webmozart/assert": "^1.11" + "webmozart/assert": "^1.11||^2.0" }, "suggest": { "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2." @@ -3136,30 +3136,30 @@ "support": { "docs": "https://cloud.google.com/php/docs/reference/auth/latest", "issues": "https://github.com/googleapis/google-auth-library-php/issues", - "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.49.0" + "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.50.0" }, - "time": "2025-11-06T21:27:55+00:00" + "time": "2026-01-08T21:33:57+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "type": "library", "autoload": { @@ -3188,7 +3188,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -3200,7 +3200,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "graylog2/gelf-php", @@ -4034,16 +4034,16 @@ }, { "name": "horstoeko/zugferd", - "version": "v1.0.118", + "version": "v1.0.120", "source": { "type": "git", "url": "https://github.com/horstoeko/zugferd.git", - "reference": "5259a34da6a5e5e92a764e7dd39cd7330335a579" + "reference": "c143ec75a7ffc62e2b64a87d175db9f13ab843c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/5259a34da6a5e5e92a764e7dd39cd7330335a579", - "reference": "5259a34da6a5e5e92a764e7dd39cd7330335a579", + "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/c143ec75a7ffc62e2b64a87d175db9f13ab843c1", + "reference": "c143ec75a7ffc62e2b64a87d175db9f13ab843c1", "shasum": "" }, "require": { @@ -4102,9 +4102,9 @@ ], "support": { "issues": "https://github.com/horstoeko/zugferd/issues", - "source": "https://github.com/horstoeko/zugferd/tree/v1.0.118" + "source": "https://github.com/horstoeko/zugferd/tree/v1.0.120" }, - "time": "2025-12-10T06:08:16+00:00" + "time": "2026-01-07T09:45:19+00:00" }, { "name": "horstoeko/zugferdvisualizer", @@ -5300,16 +5300,16 @@ }, { "name": "laravel/octane", - "version": "v2.13.3", + "version": "v2.13.4", "source": { "type": "git", "url": "https://github.com/laravel/octane.git", - "reference": "aae775360fceae422651042d73137fff092ba800" + "reference": "ae618600cb54826a21f67d130a39446f68be1a9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/octane/zipball/aae775360fceae422651042d73137fff092ba800", - "reference": "aae775360fceae422651042d73137fff092ba800", + "url": "https://api.github.com/repos/laravel/octane/zipball/ae618600cb54826a21f67d130a39446f68be1a9a", + "reference": "ae618600cb54826a21f67d130a39446f68be1a9a", "shasum": "" }, "require": { @@ -5386,7 +5386,7 @@ "issues": "https://github.com/laravel/octane/issues", "source": "https://github.com/laravel/octane" }, - "time": "2025-12-10T15:24:24+00:00" + "time": "2025-12-21T23:59:50+00:00" }, { "name": "laravel/prompts", @@ -5655,21 +5655,21 @@ }, { "name": "laravel/socialite", - "version": "v5.24.0", + "version": "v5.24.1", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd" + "reference": "25e28c14d55404886777af1d77cf030e0f633142" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/1d19358c28e8951dde6e36603b89d8f09e6cfbfd", - "reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd", + "url": "https://api.github.com/repos/laravel/socialite/zipball/25e28c14d55404886777af1d77cf030e0f633142", + "reference": "25e28c14d55404886777af1d77cf030e0f633142", "shasum": "" }, "require": { "ext-json": "*", - "firebase/php-jwt": "^6.4", + "firebase/php-jwt": "^6.4|^7.0", "guzzlehttp/guzzle": "^6.0|^7.0", "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", @@ -5723,20 +5723,20 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-12-09T15:37:06+00:00" + "time": "2026-01-01T02:57:21+00:00" }, { "name": "laravel/tinker", - "version": "v2.10.2", + "version": "v2.11.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c" + "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/3bcb5f62d6f837e0f093a601e26badafb127bd4c", - "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c", + "url": "https://api.github.com/repos/laravel/tinker/zipball/3d34b97c9a1747a81a3fde90482c092bd8b66468", + "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468", "shasum": "" }, "require": { @@ -5745,7 +5745,7 @@ "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "php": "^7.2.5|^8.0", "psy/psysh": "^0.11.1|^0.12.0", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", @@ -5787,9 +5787,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.2" + "source": "https://github.com/laravel/tinker/tree/v2.11.0" }, - "time": "2025-11-20T16:29:12+00:00" + "time": "2025-12-19T19:16:45+00:00" }, { "name": "laravel/ui", @@ -6182,16 +6182,16 @@ }, { "name": "league/csv", - "version": "9.27.1", + "version": "9.28.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797" + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/26de738b8fccf785397d05ee2fc07b6cd8749797", - "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/6582ace29ae09ba5b07049d40ea13eb19c8b5073", + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073", "shasum": "" }, "require": { @@ -6201,14 +6201,14 @@ "require-dev": { "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^3.75.0", - "phpbench/phpbench": "^1.4.1", - "phpstan/phpstan": "^1.12.27", + "friendsofphp/php-cs-fixer": "^3.92.3", + "phpbench/phpbench": "^1.4.3", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-phpunit": "^1.4.2", "phpstan/phpstan-strict-rules": "^1.6.2", - "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.3.6", - "symfony/var-dumper": "^6.4.8 || ^7.3.0" + "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.5.4", + "symfony/var-dumper": "^6.4.8 || ^7.4.0 || ^8.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", @@ -6269,7 +6269,7 @@ "type": "github" } ], - "time": "2025-10-25T08:35:20+00:00" + "time": "2025-12-27T15:18:42+00:00" }, { "name": "league/flysystem", @@ -7432,16 +7432,16 @@ }, { "name": "monolog/monolog", - "version": "3.9.0", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -7459,7 +7459,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -7519,7 +7519,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -7531,7 +7531,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "mpdf/mpdf", @@ -8077,16 +8077,16 @@ }, { "name": "nette/utils", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", - "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", "shasum": "" }, "require": { @@ -8160,9 +8160,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.0" + "source": "https://github.com/nette/utils/tree/v4.1.1" }, - "time": "2025-12-01T17:49:23+00:00" + "time": "2025-12-22T12:14:32+00:00" }, { "name": "nikic/php-parser", @@ -8781,16 +8781,16 @@ }, { "name": "paragonie/sodium_compat", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "547e2dc4d45107440e76c17ab5a46e4252460158" + "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/547e2dc4d45107440e76c17ab5a46e4252460158", - "reference": "547e2dc4d45107440e76c17ab5a46e4252460158", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4714da6efdc782c06690bc72ce34fae7941c2d9f", + "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f", "shasum": "" }, "require": { @@ -8871,9 +8871,9 @@ ], "support": { "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v2.4.0" + "source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0" }, - "time": "2025-10-06T08:47:40+00:00" + "time": "2025-12-30T16:12:18+00:00" }, { "name": "payfast/payfast-php-sdk", @@ -9419,16 +9419,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.5", + "version": "5.6.6", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", "shasum": "" }, "require": { @@ -9438,7 +9438,7 @@ "phpdocumentor/reflection-common": "^2.2", "phpdocumentor/type-resolver": "^1.7", "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1" + "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { "mockery/mockery": "~1.3.5 || ~1.6.0", @@ -9477,9 +9477,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" }, - "time": "2025-11-27T19:50:05+00:00" + "time": "2025-12-22T21:13:58+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -9541,16 +9541,16 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "2.4.2", + "version": "2.4.3", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "931ad61fb2c229063fc4e7e665fb52b87249cc56" + "reference": "3b204d00c19f9d809f8d2374f408b197f37ad0bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/931ad61fb2c229063fc4e7e665fb52b87249cc56", - "reference": "931ad61fb2c229063fc4e7e665fb52b87249cc56", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3b204d00c19f9d809f8d2374f408b197f37ad0bd", + "reference": "3b204d00c19f9d809f8d2374f408b197f37ad0bd", "shasum": "" }, "require": { @@ -9572,8 +9572,6 @@ "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", "php": ">=8.1.0 <8.6.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { @@ -9624,6 +9622,9 @@ }, { "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", @@ -9640,22 +9641,22 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.2" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.3" }, - "time": "2025-11-24T15:59:19+00:00" + "time": "2026-01-11T06:08:40+00:00" }, { "name": "phpoption/phpoption", - "version": "1.9.4", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -9705,7 +9706,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -9717,7 +9718,7 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "phpseclib/phpseclib", @@ -9831,16 +9832,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374", + "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374", "shasum": "" }, "require": { @@ -9872,9 +9873,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1" }, - "time": "2025-08-30T15:50:23+00:00" + "time": "2026-01-12T11:33:04+00:00" }, { "name": "pragmarx/google2fa", @@ -11044,25 +11045,33 @@ }, { "name": "sabberworm/php-css-parser", - "version": "v8.9.0", + "version": "v9.1.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" + "reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", - "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb", + "reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb", "shasum": "" }, "require": { "ext-iconv": "*", - "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3" }, "require-dev": { - "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", - "rawr/cross-data-providers": "^2.0.0" + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.28 || 2.1.25", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.7", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6", + "phpunit/phpunit": "8.5.46", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.1.7", + "rector/type-perfect": "1.0.0 || 2.1.0" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -11070,7 +11079,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.0.x-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -11104,9 +11113,9 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.1.0" }, - "time": "2025-07-11T13:20:48+00:00" + "time": "2025-09-14T07:37:21+00:00" }, { "name": "sabre/uri", @@ -11327,16 +11336,16 @@ }, { "name": "sentry/sentry-laravel", - "version": "4.20.0", + "version": "4.20.1", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "95f2542ee1ebc993529b63f5c8543184abd00650" + "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/95f2542ee1ebc993529b63f5c8543184abd00650", - "reference": "95f2542ee1ebc993529b63f5c8543184abd00650", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/503853fa7ee74b34b64e76f1373db86cd11afe72", + "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72", "shasum": "" }, "require": { @@ -11344,7 +11353,7 @@ "nyholm/psr7": "^1.0", "php": "^7.2 | ^8.0", "sentry/sentry": "^4.19.0", - "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0" + "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0 | ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.11", @@ -11401,7 +11410,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.20.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.20.1" }, "funding": [ { @@ -11413,7 +11422,7 @@ "type": "custom" } ], - "time": "2025-12-02T10:37:40+00:00" + "time": "2026-01-07T08:53:19+00:00" }, { "name": "setasign/fpdf", @@ -11535,16 +11544,16 @@ }, { "name": "smalot/pdfparser", - "version": "v2.12.2", + "version": "v2.12.3", "source": { "type": "git", "url": "https://github.com/smalot/pdfparser.git", - "reference": "370b7e983fafecb787a9bcfd73baab8038212ad1" + "reference": "61c9bcafcb92899b76d8ebda6508267bae77e264" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smalot/pdfparser/zipball/370b7e983fafecb787a9bcfd73baab8038212ad1", - "reference": "370b7e983fafecb787a9bcfd73baab8038212ad1", + "url": "https://api.github.com/repos/smalot/pdfparser/zipball/61c9bcafcb92899b76d8ebda6508267bae77e264", + "reference": "61c9bcafcb92899b76d8ebda6508267bae77e264", "shasum": "" }, "require": { @@ -11580,9 +11589,9 @@ ], "support": { "issues": "https://github.com/smalot/pdfparser/issues", - "source": "https://github.com/smalot/pdfparser/tree/v2.12.2" + "source": "https://github.com/smalot/pdfparser/tree/v2.12.3" }, - "time": "2025-09-04T08:49:09+00:00" + "time": "2026-01-08T08:04:04+00:00" }, { "name": "socialiteproviders/apple", @@ -11783,16 +11792,16 @@ }, { "name": "sprain/swiss-qr-bill", - "version": "v4.20", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/sprain/php-swiss-qr-bill.git", - "reference": "8f709ab60426c9b6cbc43fb03dee364071eda044" + "reference": "94df4479b9b92f2f37217b7860e01a1d5e5cefcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sprain/php-swiss-qr-bill/zipball/8f709ab60426c9b6cbc43fb03dee364071eda044", - "reference": "8f709ab60426c9b6cbc43fb03dee364071eda044", + "url": "https://api.github.com/repos/sprain/php-swiss-qr-bill/zipball/94df4479b9b92f2f37217b7860e01a1d5e5cefcf", + "reference": "94df4479b9b92f2f37217b7860e01a1d5e5cefcf", "shasum": "" }, "require": { @@ -11800,14 +11809,11 @@ "ext-bcmath": "*", "ext-dom": "*", "kmukku/php-iso11649": "^1.5", - "php": "~8.1.0|~8.2.0|~8.3.0|~8.4.0", - "symfony/intl": "^4.4|^5.0|^6.0|^7.0", + "php": "~8.1.0|~8.2.0|~8.3.0|~8.4.0|~8.5.0", + "symfony/intl": "^6.3|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.23", "symfony/polyfill-mbstring": "^1.30", - "symfony/validator": "^4.4|^5.0|^6.0|^7.0" - }, - "conflict": { - "khanamiryan/qrcode-detector-decoder": "1.0.6" + "symfony/validator": "^6.3|^7.0|^8.0" }, "require-dev": { "dg/bypass-finals": "^1.8", @@ -11842,7 +11848,7 @@ "description": "A PHP library to create Swiss QR bills", "support": { "issues": "https://github.com/sprain/php-swiss-qr-bill/issues", - "source": "https://github.com/sprain/php-swiss-qr-bill/tree/v4.20" + "source": "https://github.com/sprain/php-swiss-qr-bill/tree/v5.2.1" }, "funding": [ { @@ -11850,7 +11856,7 @@ "type": "github" } ], - "time": "2025-02-25T08:57:29+00:00" + "time": "2025-12-05T10:28:28+00:00" }, { "name": "square/square", @@ -12038,16 +12044,16 @@ }, { "name": "symfony/cache", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366" + "reference": "642117d18bc56832e74b68235359ccefab03dd11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/21e0755783bbbab58f2bb6a7a57896d21d27a366", - "reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366", + "url": "https://api.github.com/repos/symfony/cache/zipball/642117d18bc56832e74b68235359ccefab03dd11", + "reference": "642117d18bc56832e74b68235359ccefab03dd11", "shasum": "" }, "require": { @@ -12118,7 +12124,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.4.1" + "source": "https://github.com/symfony/cache/tree/v7.4.3" }, "funding": [ { @@ -12138,7 +12144,7 @@ "type": "tidelift" } ], - "time": "2025-12-04T18:11:45+00:00" + "time": "2025-12-28T10:45:24+00:00" }, { "name": "symfony/cache-contracts", @@ -12296,16 +12302,16 @@ }, { "name": "symfony/config", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495" + "reference": "800ce889e358a53a9678b3212b0c8cecd8c6aace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/2c323304c354a43a48b61c5fa760fc4ed60ce495", - "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495", + "url": "https://api.github.com/repos/symfony/config/zipball/800ce889e358a53a9678b3212b0c8cecd8c6aace", + "reference": "800ce889e358a53a9678b3212b0c8cecd8c6aace", "shasum": "" }, "require": { @@ -12351,7 +12357,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.1" + "source": "https://github.com/symfony/config/tree/v7.4.3" }, "funding": [ { @@ -12371,20 +12377,20 @@ "type": "tidelift" } ], - "time": "2025-12-05T07:52:08+00:00" + "time": "2025-12-23T14:24:27+00:00" }, { "name": "symfony/console", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", - "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", "shasum": "" }, "require": { @@ -12449,7 +12455,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.1" + "source": "https://github.com/symfony/console/tree/v7.4.3" }, "funding": [ { @@ -12469,7 +12475,7 @@ "type": "tidelift" } ], - "time": "2025-12-05T15:23:39+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/css-selector", @@ -12542,16 +12548,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v7.4.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b" + "reference": "54122901b6d772e94f1e71a75e0533bc16563499" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", - "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54122901b6d772e94f1e71a75e0533bc16563499", + "reference": "54122901b6d772e94f1e71a75e0533bc16563499", "shasum": "" }, "require": { @@ -12602,7 +12608,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.2" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.3" }, "funding": [ { @@ -12622,7 +12628,7 @@ "type": "tidelift" } ], - "time": "2025-12-08T06:57:04+00:00" + "time": "2025-12-28T10:55:46+00:00" }, { "name": "symfony/deprecation-contracts", @@ -13006,16 +13012,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" + "reference": "fffe05569336549b20a1be64250b40516d6e8d06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", + "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06", + "reference": "fffe05569336549b20a1be64250b40516d6e8d06", "shasum": "" }, "require": { @@ -13050,7 +13056,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.0" + "source": "https://github.com/symfony/finder/tree/v7.4.3" }, "funding": [ { @@ -13070,20 +13076,20 @@ "type": "tidelift" } ], - "time": "2025-11-05T05:42:40+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/framework-bundle", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3" + "reference": "df908e8f9e5f6cc3c9e0d0172e030a5c1c280582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3", - "reference": "2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/df908e8f9e5f6cc3c9e0d0172e030a5c1c280582", + "reference": "df908e8f9e5f6cc3c9e0d0172e030a5c1c280582", "shasum": "" }, "require": { @@ -13091,7 +13097,7 @@ "ext-xml": "*", "php": ">=8.2", "symfony/cache": "^6.4.12|^7.0|^8.0", - "symfony/config": "^7.4|^8.0", + "symfony/config": "^7.4.3|^8.0.3", "symfony/dependency-injection": "^7.4|^8.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^7.3|^8.0", @@ -13208,7 +13214,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v7.4.1" + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.3" }, "funding": [ { @@ -13228,20 +13234,20 @@ "type": "tidelift" } ], - "time": "2025-12-05T14:04:53+00:00" + "time": "2025-12-29T09:31:36+00:00" }, { "name": "symfony/http-client", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "26cc224ea7103dda90e9694d9e139a389092d007" + "reference": "d01dfac1e0dc99f18da48b18101c23ce57929616" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/26cc224ea7103dda90e9694d9e139a389092d007", - "reference": "26cc224ea7103dda90e9694d9e139a389092d007", + "url": "https://api.github.com/repos/symfony/http-client/zipball/d01dfac1e0dc99f18da48b18101c23ce57929616", + "reference": "d01dfac1e0dc99f18da48b18101c23ce57929616", "shasum": "" }, "require": { @@ -13309,7 +13315,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.1" + "source": "https://github.com/symfony/http-client/tree/v7.4.3" }, "funding": [ { @@ -13329,7 +13335,7 @@ "type": "tidelift" } ], - "time": "2025-12-04T21:12:57+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/http-client-contracts", @@ -13411,16 +13417,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" + "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", - "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a70c745d4cea48dbd609f4075e5f5cbce453bd52", + "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52", "shasum": "" }, "require": { @@ -13469,7 +13475,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.1" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.3" }, "funding": [ { @@ -13489,20 +13495,20 @@ "type": "tidelift" } ], - "time": "2025-12-07T11:13:10+00:00" + "time": "2025-12-23T14:23:49+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f" + "reference": "885211d4bed3f857b8c964011923528a55702aa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f", - "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/885211d4bed3f857b8c964011923528a55702aa5", + "reference": "885211d4bed3f857b8c964011923528a55702aa5", "shasum": "" }, "require": { @@ -13588,7 +13594,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.2" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.3" }, "funding": [ { @@ -13608,7 +13614,7 @@ "type": "tidelift" } ], - "time": "2025-12-08T07:43:37+00:00" + "time": "2025-12-31T08:43:57+00:00" }, { "name": "symfony/intl", @@ -15078,16 +15084,16 @@ }, { "name": "symfony/process", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f", + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f", "shasum": "" }, "require": { @@ -15119,7 +15125,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.0" + "source": "https://github.com/symfony/process/tree/v7.4.3" }, "funding": [ { @@ -15139,25 +15145,25 @@ "type": "tidelift" } ], - "time": "2025-10-16T11:21:06+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/property-access", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "537626149d2910ca43eb9ce465654366bf4442f4" + "reference": "30aff8455647be949fc2e8fcef2847d5a6743c98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/537626149d2910ca43eb9ce465654366bf4442f4", - "reference": "537626149d2910ca43eb9ce465654366bf4442f4", + "url": "https://api.github.com/repos/symfony/property-access/zipball/30aff8455647be949fc2e8fcef2847d5a6743c98", + "reference": "30aff8455647be949fc2e8fcef2847d5a6743c98", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/property-info": "^6.4|^7.0|^8.0" + "symfony/property-info": "^6.4.31|~7.3.9|^7.4.2|^8.0.3" }, "require-dev": { "symfony/cache": "^6.4|^7.0|^8.0", @@ -15200,7 +15206,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.4.0" + "source": "https://github.com/symfony/property-access/tree/v7.4.3" }, "funding": [ { @@ -15220,20 +15226,20 @@ "type": "tidelift" } ], - "time": "2025-09-08T21:14:32+00:00" + "time": "2025-12-18T10:35:58+00:00" }, { "name": "symfony/property-info", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "912aafe70bee5cfd09fec5916fe35b83f04ae6ae" + "reference": "ea62b28cd68fb36e252abd77de61e505a0f2a7b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/912aafe70bee5cfd09fec5916fe35b83f04ae6ae", - "reference": "912aafe70bee5cfd09fec5916fe35b83f04ae6ae", + "url": "https://api.github.com/repos/symfony/property-info/zipball/ea62b28cd68fb36e252abd77de61e505a0f2a7b1", + "reference": "ea62b28cd68fb36e252abd77de61e505a0f2a7b1", "shasum": "" }, "require": { @@ -15290,7 +15296,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.4.1" + "source": "https://github.com/symfony/property-info/tree/v7.4.3" }, "funding": [ { @@ -15310,7 +15316,7 @@ "type": "tidelift" } ], - "time": "2025-12-05T14:04:53+00:00" + "time": "2025-12-18T08:28:41+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -15402,16 +15408,16 @@ }, { "name": "symfony/routing", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "4720254cb2644a0b876233d258a32bf017330db7" + "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7", - "reference": "4720254cb2644a0b876233d258a32bf017330db7", + "url": "https://api.github.com/repos/symfony/routing/zipball/5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", + "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", "shasum": "" }, "require": { @@ -15463,7 +15469,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.0" + "source": "https://github.com/symfony/routing/tree/v7.4.3" }, "funding": [ { @@ -15483,20 +15489,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/serializer", - "version": "v7.4.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "1a957acb613b520e443c2c659a67c782b67794bc" + "reference": "af01e99d6fc63549063fb9e849ce1240cfef5c4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/1a957acb613b520e443c2c659a67c782b67794bc", - "reference": "1a957acb613b520e443c2c659a67c782b67794bc", + "url": "https://api.github.com/repos/symfony/serializer/zipball/af01e99d6fc63549063fb9e849ce1240cfef5c4a", + "reference": "af01e99d6fc63549063fb9e849ce1240cfef5c4a", "shasum": "" }, "require": { @@ -15566,7 +15572,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.4.2" + "source": "https://github.com/symfony/serializer/tree/v7.4.3" }, "funding": [ { @@ -15586,7 +15592,7 @@ "type": "tidelift" } ], - "time": "2025-12-07T17:35:40+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/service-contracts", @@ -15768,16 +15774,16 @@ }, { "name": "symfony/translation", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" + "reference": "7ef27c65d78886f7599fdd5c93d12c9243ecf44d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", - "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "url": "https://api.github.com/repos/symfony/translation/zipball/7ef27c65d78886f7599fdd5c93d12c9243ecf44d", + "reference": "7ef27c65d78886f7599fdd5c93d12c9243ecf44d", "shasum": "" }, "require": { @@ -15844,7 +15850,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.0" + "source": "https://github.com/symfony/translation/tree/v7.4.3" }, "funding": [ { @@ -15864,7 +15870,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-29T09:31:36+00:00" }, { "name": "symfony/translation-contracts", @@ -15950,16 +15956,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "9103559ef3e9f06708d8bff6810f6335b8f1eee8" + "reference": "43c922fce020060c65b0fd54bfd8def3b38949b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/9103559ef3e9f06708d8bff6810f6335b8f1eee8", - "reference": "9103559ef3e9f06708d8bff6810f6335b8f1eee8", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/43c922fce020060c65b0fd54bfd8def3b38949b6", + "reference": "43c922fce020060c65b0fd54bfd8def3b38949b6", "shasum": "" }, "require": { @@ -16041,7 +16047,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v7.4.1" + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.3" }, "funding": [ { @@ -16061,20 +16067,20 @@ "type": "tidelift" } ], - "time": "2025-12-05T14:04:53+00:00" + "time": "2025-12-16T08:02:06+00:00" }, { "name": "symfony/twig-bundle", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "f83f530d00d1bbc6f7fafeb433077887c83326ef" + "reference": "9e1f5fd2668ed26c60d17d63f15fe270ed8da5e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/f83f530d00d1bbc6f7fafeb433077887c83326ef", - "reference": "f83f530d00d1bbc6f7fafeb433077887c83326ef", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/9e1f5fd2668ed26c60d17d63f15fe270ed8da5e6", + "reference": "9e1f5fd2668ed26c60d17d63f15fe270ed8da5e6", "shasum": "" }, "require": { @@ -16082,6 +16088,7 @@ "php": ">=8.2", "symfony/config": "^7.4|^8.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", "symfony/twig-bridge": "^7.3|^8.0", @@ -16130,7 +16137,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v7.4.0" + "source": "https://github.com/symfony/twig-bundle/tree/v7.4.3" }, "funding": [ { @@ -16150,7 +16157,7 @@ "type": "tidelift" } ], - "time": "2025-10-02T07:41:02+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/type-info", @@ -16315,16 +16322,16 @@ }, { "name": "symfony/validator", - "version": "v7.4.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "569b71d1243ccc58e8f1d21e279669239e78f60d" + "reference": "9670bedf4c454b21d1e04606b6c227990da8bebe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/569b71d1243ccc58e8f1d21e279669239e78f60d", - "reference": "569b71d1243ccc58e8f1d21e279669239e78f60d", + "url": "https://api.github.com/repos/symfony/validator/zipball/9670bedf4c454b21d1e04606b6c227990da8bebe", + "reference": "9670bedf4c454b21d1e04606b6c227990da8bebe", "shasum": "" }, "require": { @@ -16395,7 +16402,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.4.2" + "source": "https://github.com/symfony/validator/tree/v7.4.3" }, "funding": [ { @@ -16415,20 +16422,20 @@ "type": "tidelift" } ], - "time": "2025-12-07T17:35:40+00:00" + "time": "2025-12-27T17:05:22+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" + "reference": "7e99bebcb3f90d8721890f2963463280848cba92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", - "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92", + "reference": "7e99bebcb3f90d8721890f2963463280848cba92", "shasum": "" }, "require": { @@ -16482,7 +16489,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.3" }, "funding": [ { @@ -16502,7 +16509,7 @@ "type": "tidelift" } ], - "time": "2025-10-27T20:36:44+00:00" + "time": "2025-12-18T07:04:31+00:00" }, { "name": "symfony/var-exporter", @@ -16661,6 +16668,145 @@ ], "time": "2025-12-04T18:11:45+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2025-05-14T06:15:44+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.4.0", @@ -17174,26 +17320,26 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -17242,7 +17388,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -17254,7 +17400,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -17486,16 +17632,16 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.16.2", + "version": "v3.16.3", "source": { "type": "git", - "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3" + "url": "https://github.com/fruitcake/laravel-debugbar.git", + "reference": "c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/730dbf8bf41f5691e026dd771e64dd54ad1b10b3", - "reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3", + "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e", + "reference": "c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e", "shasum": "" }, "require": { @@ -17504,7 +17650,7 @@ "illuminate/support": "^10|^11|^12", "php": "^8.1", "php-debugbar/php-debugbar": "^2.2.4", - "symfony/finder": "^6|^7" + "symfony/finder": "^6|^7|^8" }, "require-dev": { "mockery/mockery": "^1.3.3", @@ -17554,8 +17700,8 @@ "webprofiler" ], "support": { - "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.2" + "issues": "https://github.com/fruitcake/laravel-debugbar/issues", + "source": "https://github.com/fruitcake/laravel-debugbar/tree/v3.16.3" }, "funding": [ { @@ -17567,7 +17713,7 @@ "type": "github" } ], - "time": "2025-12-03T14:52:46+00:00" + "time": "2025-12-23T17:37:00+00:00" }, { "name": "barryvdh/laravel-ide-helper", @@ -17717,16 +17863,16 @@ }, { "name": "brianium/paratest", - "version": "v7.8.4", + "version": "v7.8.5", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4" + "reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/130a9bf0e269ee5f5b320108f794ad03e275cad4", - "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/9b324c8fc319cf9728b581c7a90e1c8f6361c5e5", + "reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5", "shasum": "" }, "require": { @@ -17734,27 +17880,27 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", - "fidry/cpu-core-counter": "^1.2.0", + "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "phpunit/php-code-coverage": "^11.0.10", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^11.0.12", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-timer": "^7.0.1", - "phpunit/phpunit": "^11.5.24", + "phpunit/phpunit": "^11.5.46", "sebastian/environment": "^7.2.1", - "symfony/console": "^6.4.22 || ^7.3.0", - "symfony/process": "^6.4.20 || ^7.3.0" + "symfony/console": "^6.4.22 || ^7.3.4 || ^8.0.3", + "symfony/process": "^6.4.20 || ^7.3.4 || ^8.0.3" }, "require-dev": { "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.17", + "phpstan/phpstan": "^2.1.33", "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.6", - "phpstan/phpstan-strict-rules": "^2.0.4", - "squizlabs/php_codesniffer": "^3.13.2", - "symfony/filesystem": "^6.4.13 || ^7.3.0" + "phpstan/phpstan-phpunit": "^2.0.11", + "phpstan/phpstan-strict-rules": "^2.0.7", + "squizlabs/php_codesniffer": "^3.13.5", + "symfony/filesystem": "^6.4.13 || ^7.3.2 || ^8.0.1" }, "bin": [ "bin/paratest", @@ -17794,7 +17940,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.8.4" + "source": "https://github.com/paratestphp/paratest/tree/v7.8.5" }, "funding": [ { @@ -17806,7 +17952,7 @@ "type": "paypal" } ], - "time": "2025-06-23T06:07:21+00:00" + "time": "2026-01-08T08:02:38+00:00" }, { "name": "clue/ndjson-react", @@ -17874,16 +18020,16 @@ }, { "name": "composer/class-map-generator", - "version": "1.7.0", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "2373419b7709815ed323ebf18c3c72d03ff4a8a6" + "reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/2373419b7709815ed323ebf18c3c72d03ff4a8a6", - "reference": "2373419b7709815ed323ebf18c3c72d03ff4a8a6", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8f5fa3cc214230e71f54924bd0197a3bcc705eb1", + "reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1", "shasum": "" }, "require": { @@ -17927,7 +18073,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.7.0" + "source": "https://github.com/composer/class-map-generator/tree/1.7.1" }, "funding": [ { @@ -17939,7 +18085,7 @@ "type": "github" } ], - "time": "2025-11-19T10:41:15+00:00" + "time": "2025-12-29T13:15:25+00:00" }, { "name": "composer/semver", @@ -18265,16 +18411,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.92.3", + "version": "v3.92.5", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2ba8f5a60f6f42fb65758cfb3768434fa2d1c7e8" + "reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2ba8f5a60f6f42fb65758cfb3768434fa2d1c7e8", - "reference": "2ba8f5a60f6f42fb65758cfb3768434fa2d1c7e8", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58", + "reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58", "shasum": "" }, "require": { @@ -18306,17 +18452,17 @@ }, "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", + "infection/infection": "^0.31", + "justinrainbow/json-schema": "^6.6", + "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", + "phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.46", "symfony/polyfill-php85": "^1.33", - "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" + "symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -18357,7 +18503,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.92.3" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.5" }, "funding": [ { @@ -18365,7 +18511,7 @@ "type": "github" } ], - "time": "2025-12-18T10:45:02+00:00" + "time": "2026-01-08T21:57:37+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -18461,7 +18607,7 @@ }, { "name": "illuminate/json-schema", - "version": "v12.43.1", + "version": "v12.46.0", "source": { "type": "git", "url": "https://github.com/illuminate/json-schema.git", @@ -18656,16 +18802,16 @@ }, { "name": "laravel/boost", - "version": "v1.8.7", + "version": "v1.8.9", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "7a5709a8134ed59d3e7f34fccbd74689830e296c" + "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/7a5709a8134ed59d3e7f34fccbd74689830e296c", - "reference": "7a5709a8134ed59d3e7f34fccbd74689830e296c", + "url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd", + "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd", "shasum": "" }, "require": { @@ -18718,20 +18864,20 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-12-19T15:04:12+00:00" + "time": "2026-01-07T18:43:11+00:00" }, { "name": "laravel/mcp", - "version": "v0.5.1", + "version": "v0.5.2", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4" + "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4", - "reference": "10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4", + "url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c", + "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c", "shasum": "" }, "require": { @@ -18791,7 +18937,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-12-17T06:14:23+00:00" + "time": "2025-12-19T19:32:34+00:00" }, { "name": "laravel/roster", @@ -19156,31 +19302,32 @@ }, { "name": "php-debugbar/php-debugbar", - "version": "v2.2.4", + "version": "v2.2.6", "source": { "type": "git", "url": "https://github.com/php-debugbar/php-debugbar.git", - "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35" + "reference": "abb9fa3c5c8dbe7efe03ddba56782917481de3e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/3146d04671f51f69ffec2a4207ac3bdcf13a9f35", - "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35", + "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/abb9fa3c5c8dbe7efe03ddba56782917481de3e8", + "reference": "abb9fa3c5c8dbe7efe03ddba56782917481de3e8", "shasum": "" }, "require": { - "php": "^8", + "php": "^8.1", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^4|^5|^6|^7" + "symfony/var-dumper": "^5.4|^6.4|^7.3|^8.0" }, "replace": { "maximebf/debugbar": "self.version" }, "require-dev": { "dbrekelmans/bdi": "^1", - "phpunit/phpunit": "^8|^9", + "phpunit/phpunit": "^10", + "symfony/browser-kit": "^6.0|7.0", "symfony/panther": "^1|^2.1", - "twig/twig": "^1.38|^2.7|^3.0" + "twig/twig": "^3.11.2" }, "suggest": { "kriswallsmith/assetic": "The best way to manage assets", @@ -19190,7 +19337,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" } }, "autoload": { @@ -19223,9 +19370,9 @@ ], "support": { "issues": "https://github.com/php-debugbar/php-debugbar/issues", - "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.4" + "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.6" }, - "time": "2025-07-22T14:01:30+00:00" + "time": "2025-12-22T13:21:32+00:00" }, { "name": "phpstan/phpstan", @@ -19282,35 +19429,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", @@ -19348,7 +19495,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": [ { @@ -19368,7 +19515,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T14:37:49+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -19798,16 +19945,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": { @@ -19861,7 +20008,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": [ { @@ -19869,7 +20016,7 @@ "type": "open_collective" } ], - "time": "2025-01-01T16:37:48+00:00" + "time": "2025-12-23T15:25:20+00:00" }, { "name": "react/dns", From 1fabf6f023e26de4ede3c62cad06c3340ee03445 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 13 Jan 2026 11:36:11 +1100 Subject: [PATCH 081/177] Updates for task request filters --- app/Http/Requests/Task/StoreTaskRequest.php | 4 ++++ app/Http/Requests/Task/UpdateTaskRequest.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/Http/Requests/Task/StoreTaskRequest.php b/app/Http/Requests/Task/StoreTaskRequest.php index 4ced70470b..0ce51a8860 100644 --- a/app/Http/Requests/Task/StoreTaskRequest.php +++ b/app/Http/Requests/Task/StoreTaskRequest.php @@ -161,6 +161,10 @@ class StoreTaskRequest extends Request } + if(isset($input['description']) && is_string($input['description'])) { + $input['description'] = str_ireplace(['', '/etc/'], "", $input['description']); + } + /* Ensure the project is related */ if (array_key_exists('project_id', $input) && isset($input['project_id'])) { $project = Project::withTrashed()->where('id', $input['project_id'])->company()->first(); diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index f880013a5f..08245c187d 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -136,6 +136,10 @@ class UpdateTaskRequest extends Request $input['status_id'] = $this->decodePrimaryKey($input['status_id']); } + if(isset($input['description']) && is_string($input['description'])) { + $input['description'] = str_ireplace(['', '/etc/'], "", $input['description']); + } + if (isset($input['documents'])) { unset($input['documents']); } From 5dbfcf70973f976d2a526d180fa5bcf36aedf900 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 13 Jan 2026 15:49:06 +1100 Subject: [PATCH 082/177] Updated translations --- app/Jobs/PostMark/ProcessPostmarkWebhook.php | 2 +- lang/en/texts.php | 2 + lang/fr_CA/texts.php | 31 +++++-- lang/nl/texts.php | 86 ++++++++++++++++++-- lang/vi/texts.php | 31 +++++-- 5 files changed, 134 insertions(+), 18 deletions(-) diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php index 38865d0e41..d1199a57f5 100644 --- a/app/Jobs/PostMark/ProcessPostmarkWebhook.php +++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php @@ -166,7 +166,7 @@ class ProcessPostmarkWebhook implements ShouldQueue private function processOpen() { - $this->invitation->opened_date = now(); + $this->invitation->opened_date = now()->setTimezone($this->invitation->company->timezone()->name); $this->invitation->saveQuietly(); $data = array_merge($this->request, ['history' => $this->fetchMessage()]); diff --git a/lang/en/texts.php b/lang/en/texts.php index 5b578e9dd5..a6e03ee7de 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5687,6 +5687,8 @@ $lang = array( 'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date', 'notification_invoice_overdue_summary' => 'The following invoices are overdue:', 'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?', + 'peppol_sending_failed' => 'E-Invoice sending failed!', + 'peppol_sending_success' => 'E-Invoice sent successfully!', ); return $lang; diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index 5aaa02e677..b54eb2e620 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -4688,6 +4688,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'show_tasks_in_client_portal' => 'Afficher les tâches sur le portail du client', 'notification_quote_expired_subject' => 'La soumission :invoice a expiré pour :client', 'notification_quote_expired' => 'La soumission :invoice pour le client :client au montant de :amount est expirée', + 'notification_invoice_overdue_subject' => 'La facture :invoice est échue pour :client', + 'notification_invoice_overdue' => 'La facture :invoice pour le client :client de :amount est échue.', 'auto_sync' => 'Synchronisation automatique', 'refresh_accounts' => 'Rafraîchir les comptes', 'upgrade_to_connect_bank_account' => 'Passer au plan Entreprise pour connecter votre compte bancaire', @@ -5634,7 +5636,6 @@ Développe automatiquement la section des notes dans le tableau de produits pour 'einvoice_received_subject' => 'E-facture(s) reçues', 'einvoice_received_body' => 'Vous avez reçu :count nouvelle(s) E-facture(s).

Connectez-vous pour les consulter.', 'download_files_too_large' => 'La taille de certains fichiers dépassait la limite pour être joints directement au message courriel.', - 'restore_disabled_verifactu' => 'Vous ne pouvez pas restaurer une facture une fois qu\'elle a été supprimée.', 'delete_disabled_verifactu' => 'Vous ne pouvez pas supprimer une facture une fois qu\'elle a été annulée ou modifiée.', 'rectify' => 'Rectificar', @@ -5643,10 +5644,6 @@ Développe automatiquement la section des notes dans le tableau de produits pour 'verifactu_cancellation_send_success' => 'Annulation de facture pour :invoice envoyée à AEAT', 'verifactu_cancellation_send_failure' => 'Annulation de facture pour :invoice n\'a pas été envoyée AEAT :notes', 'verifactu' => 'Verifactu', - 'activity_150' => 'Compte supprimé :notes', - 'activity_151' => 'Le client :notes a été fusionné avec :client par :user', - 'activity_152' => 'Le fournisseur :notes a été fusionné avec :vendor par :user', - 'activity_153' => 'Le client :notes a été purgé par :user', 'justify' => 'Justifier', 'outdent' => 'Désindenter', 'indent' => 'Indenter', @@ -5665,6 +5662,30 @@ Développe automatiquement la section des notes dans le tableau de produits pour 'thank_you_for_feedback' => 'Merci pour vos commentaires !', 'use_legacy_editor' => 'Utiliser l\'éditeur classique Wysiwyg', 'use_legacy_editor_help' => 'Utiliser l\'éditeur TinyMCE.', + 'enable_e_invoice_received_notification' => 'Activer la notification de réception de facture électronique', + 'enable_e_invoice_received_notification_help' => 'Recevoir une notification par courriel lorsqu\'une nouvelle facture électronique est reçue.', + 'price_changes' => 'Changements de prix des forfaits à partir du 1er janvier 2026', + 'notification_quote_rejected_subject' => 'La soumission :quote n\'a pas été acceptée par :client', + 'notification_quote_rejected' => 'Le client :client n\'a pas accepté la soumission :quote pour :amount :notes.', + 'activity_150' => 'Compte supprimé :notes', + 'activity_151' => 'Le client :notes a été fusionné avec :client par :user', + 'activity_152' => 'Le fournisseur :notes a été fusionné avec :vendor par :user', + 'activity_153' => 'Le client :notes a été purgé par :user', + 'activity_154' => 'La facture électronique :invoice pour :client a été envoyer à AEAT.', + 'activity_155' => 'L\'envoi de la facture électronique :invoice pour :client à l\'AEAT a échoué :notes', + 'activity_156' => 'L\'annulation de la facture pour :invoice a été envoyée', + 'activity_157' => 'L\'envoi de l\'annulation de la facture pour :invoice a échoué pour AEAT :notes', + 'activity_158' => 'La soumission :quote n\'a pas été accepté par :client :notes', + 'quotes_with_status_sent_can_be_rejected' => 'Seules les soumission avec le statut "Envoyée" peuvent être rejetées.', + 'reject' => 'Rejeter', + 'rejected' => 'Rejeté', + 'reject_quote' => 'Rejeter la soumission', + 'reject_quote_confirmation' => 'Êtes-vous sûr de vouloir rejeter cette soumission ?', + 'reason' => 'Raison', + 'enter_reason' => 'Préciser la raison...', + 'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date', + 'notification_invoice_overdue_summary' => 'The following invoices are overdue:', + 'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?', ); return $lang; diff --git a/lang/nl/texts.php b/lang/nl/texts.php index 781d412297..bc0446b653 100644 --- a/lang/nl/texts.php +++ b/lang/nl/texts.php @@ -3831,9 +3831,9 @@ Kom terug naar deze betaalmethode pagina zodra u de bedragen heeft ontvangen en 'to_view_entity_password' => 'Om de :entity te bekijken moet u een wachtwoord invoeren.', 'showing_x_of' => 'Toont de :first tot :last van de :total resultaten', 'no_results' => 'Geen resultaten gevonden.', - 'payment_failed_subject' => 'Betaling mislukt voor klant :klant', + 'payment_failed_subject' => 'Betaling mislukt voor klant :client', 'payment_failed_body' => 'Een betaling gedaan door de klant :client is mislukt met bericht :bericht', - 'register' => 'Registreer', + 'register' => 'Registreren', 'register_label' => 'Maak binnen enkele seconden uw account aan', 'password_confirmation' => 'Bevestig uw wachtwoord', 'verification' => 'Verificatie', @@ -3925,8 +3925,8 @@ Kom terug naar deze betaalmethode pagina zodra u de bedragen heeft ontvangen en 'invoice_number_taken' => 'Factuurnummer reeds in gebruik', 'payment_id_required' => 'Betalings-id verplicht', 'unable_to_retrieve_payment' => 'Niet in staat om gevraagde betaling op te halen', - 'invoice_not_related_to_payment' => 'Factuur ID :invoice is niet herleidbaar naar deze betaling', - 'credit_not_related_to_payment' => 'Creditfactuur ID :credit is niet verwant aan deze betaling', + 'invoice_not_related_to_payment' => 'Factuur # :invoice is niet gerelateerd aan deze betaling.', + 'credit_not_related_to_payment' => 'Krediet # :credit is niet gerelateerd aan deze betaling.', 'max_refundable_invoice' => 'Poging tot terugbetaling is groter dan toegestaan voor invoice id :invoice, maximum terug te betalen bedrag is :amount', 'refund_without_invoices' => 'Wanneer u een betaling met bijgevoegde facturen wilt terugbetalen, geef dan aan welke geldige factuur/facturen u wilt terugbetalen.', 'refund_without_credits' => 'Wanneer u een betaling met bijgevoegde tegoeden wilt terugbetalen, geef dan aan welke tegoeden geldig zijn en u deze wilt terugbetalen.', @@ -4691,6 +4691,8 @@ E-mail: :email
', 'show_tasks_in_client_portal' => 'Toon taken in klantenportaal', 'notification_quote_expired_subject' => 'Offerte :invoice is verlopen voor :client', 'notification_quote_expired' => 'De volgende Offerte :invoice voor klant :client en :amount is nu verlopen.', + 'notification_invoice_overdue_subject' => 'Invoice :invoice is overdue for :client', + 'notification_invoice_overdue' => 'The following Invoice :invoice for client :client and :amount is now overdue.', 'auto_sync' => 'Automatisch synchroniseren', 'refresh_accounts' => 'Ververs accounts', 'upgrade_to_connect_bank_account' => 'Upgrade naar Enterprise om uw bankrekening te koppelen', @@ -4946,7 +4948,7 @@ E-mail: :email
', 'here' => 'hier', 'industry_Restaurant & Catering' => 'Restaurant & Horeca', 'show_credits_table' => 'Credittabel tonen', - 'manual_payment' => 'Betalingshandleiding', + 'manual_payment' => 'Handmatige betaling', 'tax_summary_report' => 'Fiscaal overzichtsrapport', 'tax_category' => 'Belastingcategorie', 'physical_goods' => 'Fysieke goederen', @@ -5186,7 +5188,7 @@ E-mail: :email
', 'step_authentication_fail' => 'U moet ten minste één van de authenticatiemethoden opnemen.', 'auth.login' => 'Log in', 'auth.login-or-register' => 'Log in of registreer', - 'auth.register' => 'Register', + 'auth.register' => 'Registreren', 'cart' => 'Winkelwagen', 'methods' => 'Methoden', 'rff' => 'Verplichte velden formulier', @@ -5366,7 +5368,7 @@ E-mail: :email
', 'step' => 'Stap', 'peppol_whitelabel_warning' => 'Voor het gebruik van e-facturatie via het PEPPOL-netwerk is een whitelabellicentie vereist.', 'peppol_plan_warning' => 'Voor het gebruik van e-facturatie via het PEPPOL-netwerk is een Enterprise-abonnement vereist.', - 'peppol_credits_info' => 'Ecredits zijn vereist om e-facturen te versturen en ontvangen. Deze worden per document in rekening gebracht.', + 'peppol_credits_info' => 'Ecredits are required to send and receive einvoices. These are charged on a per document basis. If you already have credits, click Continue.', 'buy_credits' => 'Koop E-credits', 'peppol_successfully_configured' => 'PEPPOL succesvol geconfigureerd.', 'peppol_not_paid_message' => 'Enterprise-abonnement vereist voor PEPPOL. Upgrade uw abonnement.', @@ -5615,6 +5617,76 @@ E-mail: :email
', 'tax_nexus' => 'Belastingnexus', 'tax_period_report' => 'Belastingperioderapport', 'creator' => 'Gemaakt door', + 'ses_topic_arn_help' => 'The SES topic (optional, only for webhook tracking)', + 'ses_region_help' => 'The AWS region, ie us-east-1', + 'ses_secret_key' => 'SES Secret Key', + 'ses_access_key' => 'SES Access Key ID', + 'activity_151' => 'Klant :notes is samengevoegd met :client door :user', + 'activity_152' => 'Leverancier :notes is samengevoegd met :vendor door :user', + 'activity_153' => 'Klant :notes verwijderd door :user', + 'lifecycle' => 'Lifecycle', + 'order_columns' => 'Sorteer kolommen', + 'topic_arn' => 'Topic ARN', + 'lang_Catalan' => 'Catalan', + 'lang_Afrikaans' => 'Afrikaans', + 'lang_Indonesian' => 'Indonesian', + 'replaced' => 'Replaced', + 'ses_from_address' => 'SES From Address', + 'ses_from_address_help' => 'The Sending Email Address, must be verified in AWS', + 'unauthorized_action' => 'U bent niet gemachtigd om deze actie uit te voeren.', + 'einvoice_received_subject' => 'E-Invoice/s Received', + 'einvoice_received_body' => 'You have received :count new E-Invoice/s.

Login to view.', + 'download_files_too_large' => 'Some files were too large to attach directly to the email. Please use the links below to download these individually.', + 'restore_disabled_verifactu' => 'You cannot restore an invoice once it has been deleted', + 'delete_disabled_verifactu' => 'You cannot delete an invoice once it has been cancelled or modified', + 'rectify' => 'Rectificar', + 'verifactu_invoice_send_success' => 'Invoice :invoice for :client sent to AEAT successfully', + 'verifactu_invoice_sent_failure' => 'Invoice :invoice for :client failed to send to AEAT :notes', + 'verifactu_cancellation_send_success' => 'Invoice cancellation for :invoice sent to AEAT successfully', + 'verifactu_cancellation_send_failure' => 'Invoice cancellation for :invoice failed to send to AEAT :notes', + 'verifactu' => 'Verifactu', + 'justify' => 'Justify', + 'outdent' => 'Outdent', + 'indent' => 'Indent', + 'clear_filters' => 'Clear Filters', + 'feedback' => 'Feedback', + 'feedback_modal_description' => 'We would love to hear your feedback!', + 'do_not_ask_again' => 'Do not ask again', + 'not_likely' => 'Not likely', + 'extremely_likely' => 'Extremely likely', + 'feedback_slider_title' => 'How likely are you to recommend Invoice Ninja to a friend or colleague?', + 'actual_delivery_date' => 'Actual Delivery Date', + 'actual_delivery_date_help' => 'Sometimes required when billing across borders. Defines the EXACT date of delivery of goods.', + 'invoice_period' => 'Invoice Period', + 'invoice_period_help' => 'Defines the time period for which the services were provided.', + 'paused_recurring_invoice_helper' => 'Caution! When restarting a recurring invoice, ensure the next send date is in the future.', + 'thank_you_for_feedback' => 'Thank you for your feedback!', + 'use_legacy_editor' => 'Use Legacy Wysiwyg Editor', + 'use_legacy_editor_help' => 'Use the TinyMCE editor.', + 'enable_e_invoice_received_notification' => 'Enable E-Invoice Received Notification', + 'enable_e_invoice_received_notification_help' => 'Receive an email notification when a new E-Invoice is received.', + 'price_changes' => 'Plan Price Changes from January 1st 2026', + 'notification_quote_rejected_subject' => 'Offerte :quote werd afgewezen door :client', + 'notification_quote_rejected' => 'The following client :client rejected Quote :quote for :amount :notes.', + 'activity_150' => 'Account verwijderd :notes', + 'activity_151' => 'Klant :notes is samengevoegd met :client door :user', + 'activity_152' => 'Leverancier :notes is samengevoegd met :vendor door :user', + 'activity_153' => 'Klant :notes verwijderd door :user', + 'activity_154' => 'E-Invoice :invoice for :client sent to AEAT successfully', + 'activity_155' => 'E-Invoice :invoice for :client failed to send to AEAT :notes', + 'activity_156' => 'Invoice cancellation for :invoice sent to AEAT successfully', + 'activity_157' => 'Invoice cancellation for :invoice failed to send to AEAT :notes', + 'activity_158' => 'Quote :quote was rejected by :client :notes', + 'quotes_with_status_sent_can_be_rejected' => 'Only quotes with "Sent" status can be rejected.', + 'reject' => 'Reject', + 'rejected' => 'Rejected', + 'reject_quote' => 'Reject Quote', + 'reject_quote_confirmation' => 'Are you sure you want to reject this quote?', + 'reason' => 'Reden', + 'enter_reason' => 'Geef een reden op...', + 'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date', + 'notification_invoice_overdue_summary' => 'The following invoices are overdue:', + 'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?', ); return $lang; diff --git a/lang/vi/texts.php b/lang/vi/texts.php index c8282c06b5..76a02d18a8 100644 --- a/lang/vi/texts.php +++ b/lang/vi/texts.php @@ -4691,6 +4691,8 @@ $lang = array( 'show_tasks_in_client_portal' => 'Hiển thị nhiệm vụ trong Cổng thông tin khách hàng', 'notification_quote_expired_subject' => 'Báo giá :invoice đã hết hạn cho :client', 'notification_quote_expired' => 'Báo giá :invoice sau đây dành cho khách hàng :client và :amount hiện đã hết hạn.', + 'notification_invoice_overdue_subject' => 'Hóa đơn :invoice quá hạn cho :client', + 'notification_invoice_overdue' => 'Hóa đơn :invoice cho khách hàng :client và :amount hiện đã quá hạn.', 'auto_sync' => 'Tự động đồng bộ', 'refresh_accounts' => 'Làm mới tài khoản', 'upgrade_to_connect_bank_account' => 'Nâng cấp lên Enterprise để kết nối tài khoản ngân hàng của bạn', @@ -5635,7 +5637,6 @@ $lang = array( 'einvoice_received_subject' => 'E- Hóa đơn /s Received', 'einvoice_received_body' => 'Bạn đã nhận được :count mới E- Hóa đơn /s.

Đăng nhập đến Xem .', 'download_files_too_large' => 'Một số tệp quá lớn đến không thể đính kèm trực tiếp đến email . Vui lòng sử dụng các liên kết bên dưới đến tải xuống từng tệp riêng lẻ.', - 'restore_disabled_verifactu' => 'Bạn không thể Khôi phục Hóa đơn một khi nó đã bị đã xóa', 'delete_disabled_verifactu' => 'Bạn không thể Xóa một Hóa đơn sau khi nó đã bị hủy hoặc sửa đổi', 'rectify' => 'Rectificar', @@ -5644,10 +5645,6 @@ $lang = array( 'verifactu_cancellation_send_success' => 'Hóa đơn hủy :invoice đã gửi đến AEAT Thành công', 'verifactu_cancellation_send_failure' => 'Hóa đơn hủy cho :invoice không gửi đến đến AEAT :notes', 'verifactu' => 'Verifactu', - 'activity_150' => 'tài khoản đã xóa :notes', - 'activity_151' => 'Khách hàng :notes merged into :client by :user', - 'activity_152' => 'Người bán :notes merged into :vendor by :user', - 'activity_153' => 'Khách hàng :notes bị :user thanh lọc', 'justify' => 'Căn chỉnh', 'outdent' => 'Lồi ra ngoài', 'indent' => 'thụt lề', @@ -5666,6 +5663,30 @@ $lang = array( 'thank_you_for_feedback' => 'Cảm ơn phản hồi của bạn!', 'use_legacy_editor' => 'Sử dụng Legacy Wysiwyg Editor', 'use_legacy_editor_help' => 'Sử dụng trình soạn thảo TinyMCE.', + 'enable_e_invoice_received_notification' => 'Enable E- Hóa đơn Đã nhận Thông báo', + 'enable_e_invoice_received_notification_help' => 'Nhận thông báo email khi nhận được Hóa đơn mới .', + 'price_changes' => 'Thay đổi giá gói dịch vụ từ ngày 1 tháng 1 năm 2026', + 'notification_quote_rejected_subject' => 'báo giá :quote đã bị từ chối bởi :client', + 'notification_quote_rejected' => 'khách hàng sau đây :client đã từ chối báo giá :quote cho :amount :notes .', + 'activity_150' => 'tài khoản đã xóa :notes', + 'activity_151' => 'Khách hàng :notes merged into :client by :user', + 'activity_152' => 'Người bán :notes merged into :vendor by :user', + 'activity_153' => 'Khách hàng :notes bị :user thanh lọc', + 'activity_154' => 'E- Hóa đơn :invoice for :client TXEND gửi đến AEAT Thành công', + 'activity_155' => 'E- Hóa đơn :invoice cho :client không đến được đến AEAT :notes', + 'activity_156' => 'Hóa đơn hủy :invoice đã gửi đến AEAT Thành công', + 'activity_157' => 'Hóa đơn hủy cho :invoice không đến được đến AEAT :notes', + 'activity_158' => 'báo giá :quote đã bị từ chối bởi :client :notes', + 'quotes_with_status_sent_can_be_rejected' => 'Chỉ Báo giá có trạng thái "Đã gửi" mới bị từ chối.', + 'reject' => 'Từ chối', + 'rejected' => 'Vật bị loại bỏ', + 'reject_quote' => 'Từ chối báo giá', + 'reject_quote_confirmation' => 'Bạn có chắc chắn muốn đến chối báo giá này không?', + 'reason' => 'Lý do', + 'enter_reason' => 'Nhập một lý do...', + 'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date', + 'notification_invoice_overdue_summary' => 'The following invoices are overdue:', + 'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?', ); return $lang; From 48b07f4af781e67a5a56a0f6df6f38035dcba570 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 13 Jan 2026 17:55:39 +1100 Subject: [PATCH 083/177] Improvements for Gateways that are preferentially sorted --- .../Controllers/CompanyGatewayController.php | 10 +- app/Models/CompanyGateway.php | 2 +- app/Repositories/CompanyGatewayRepository.php | 94 +++++++++++++++++++ app/Services/Invoice/AutoBillInvoice.php | 30 ++++-- tests/Feature/CompanyGatewayApiTest.php | 38 ++++++++ tests/Feature/CompanyGatewayTest.php | 2 - 6 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 app/Repositories/CompanyGatewayRepository.php diff --git a/app/Http/Controllers/CompanyGatewayController.php b/app/Http/Controllers/CompanyGatewayController.php index fe8c6e84b3..43f38cc37a 100644 --- a/app/Http/Controllers/CompanyGatewayController.php +++ b/app/Http/Controllers/CompanyGatewayController.php @@ -22,8 +22,8 @@ use App\Jobs\Util\ApplePayDomain; use Illuminate\Support\Facades\Cache; use App\Factory\CompanyGatewayFactory; use App\Filters\CompanyGatewayFilters; -use App\Repositories\CompanyRepository; use Illuminate\Foundation\Bus\DispatchesJobs; +use App\Repositories\CompanyGatewayRepository; use App\Transformers\CompanyGatewayTransformer; use App\PaymentDrivers\Stripe\Jobs\StripeWebhook; use App\PaymentDrivers\CheckoutCom\CheckoutSetupWebhook; @@ -63,9 +63,9 @@ class CompanyGatewayController extends BaseController /** * CompanyGatewayController constructor. - * @param CompanyRepository $company_repo + * @param CompanyGatewayRepository $company_repo */ - public function __construct(CompanyRepository $company_repo) + public function __construct(CompanyGatewayRepository $company_repo) { parent::__construct(); @@ -210,10 +210,14 @@ class CompanyGatewayController extends BaseController /** @var \App\Models\User $user */ $user = auth()->user(); + $company = $user->company(); + $company_gateway = CompanyGatewayFactory::create($user->company()->id, $user->id); $company_gateway->fill($request->all()); $company_gateway->save(); + $this->company_repo->addGatewayToCompanyGatewayIds($company_gateway); + /*Always ensure at least one fees and limits object is set per gateway*/ $gateway_types = $company_gateway->driver(new Client())->getAvailableMethods(); diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index 12125bcbd0..a218576444 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -30,7 +30,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property bool $is_deleted * @property string $config * @property object $settings - * @property mixed $fees_and_limits + * @property array|object|mixed $fees_and_limits * @property string|null $custom_value1 * @property string|null $custom_value2 * @property string|null $custom_value3 diff --git a/app/Repositories/CompanyGatewayRepository.php b/app/Repositories/CompanyGatewayRepository.php new file mode 100644 index 0000000000..32bb3f0424 --- /dev/null +++ b/app/Repositories/CompanyGatewayRepository.php @@ -0,0 +1,94 @@ +removeGatewayFromCompanyGatewayIds($company_gateway); + + return $company_gateway; + } + + public function delete($company_gateway): CompanyGateway + { + parent::delete($company_gateway); + + $this->removeGatewayFromCompanyGatewayIds($company_gateway); + + return $company_gateway; + } + + public function restore($company_gateway): CompanyGateway + { + parent::restore($company_gateway); + + $this->addGatewayToCompanyGatewayIds($company_gateway); + + return $company_gateway; + } + + public function addGatewayToCompanyGatewayIds(CompanyGateway $company_gateway) + { + $company_gateway_ids = $company_gateway->company->getSetting('company_gateway_ids'); + + if(strlen($company_gateway_ids ?? '') > 2){ + $transformed_ids = collect($this->transformKeys(explode(',', $company_gateway_ids))) + ->push($company_gateway->hashed_id) + ->implode(","); + + $company = $company_gateway->company; + $settings = $company->settings; + $settings->company_gateway_ids = $transformed_ids; + $company->settings = $settings; + $company->save(); + } + + } + + public function removeGatewayFromCompanyGatewayIds(CompanyGateway $company_gateway) + { + $company_gateway_ids = $company_gateway->company->getSetting('company_gateway_ids'); + + if(strpos($company_gateway_ids, $company_gateway->hashed_id) !== false){ + $transformed_ids = collect($this->transformKeys(explode(',', $company_gateway_ids))) + ->filter(function ($id) use ($company_gateway){ + return $id !== $company_gateway->hashed_id; + }) + ->implode(","); + + $company = $company_gateway->company; + $settings = $company->settings; + $settings->company_gateway_ids = $transformed_ids; + $company->settings = $settings; + $company->save(); + } + + } +} \ No newline at end of file diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 3fdd830571..709707fb09 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -33,9 +33,12 @@ use App\Events\Invoice\InvoiceWasPaid; use App\Repositories\CreditRepository; use App\Repositories\PaymentRepository; use App\Events\Payment\PaymentWasCreated; +use App\Utils\Traits\MakesHash; class AutoBillInvoice extends AbstractService { + use MakesHash; + private Client $client; private array $used_credit = []; @@ -45,9 +48,7 @@ class AutoBillInvoice extends AbstractService public function __construct(private Invoice $invoice, protected string $db) { - $this->client = $this->invoice->client; - } public function run() @@ -55,7 +56,6 @@ class AutoBillInvoice extends AbstractService MultiDB::setDb($this->db); /* @var \App\Modesl\Client $client */ - $is_partial = false; /* Is the invoice payable? */ @@ -444,14 +444,32 @@ class AutoBillInvoice extends AbstractService */ public function getGateway($amount) { + $company_gateway_ids = $this->client->getSetting('company_gateway_ids'); + + $transformed_ids = false; + + //gateways are disabled! + if($company_gateway_ids == "0") { + return false; + } + elseif(strlen($company_gateway_ids ?? '') > 2){ + + // If the client has a special gateway configuration, we need to ensure we only use the ones that are enabled! + $transformed_ids = $this->transformKeys(explode(',', $company_gateway_ids)); + } + //get all client gateway tokens and set the is_default one to the first record $gateway_tokens = \App\Models\ClientGatewayToken::query() ->where('client_id', $this->client->id) ->where('is_deleted', 0) - ->whereHas('gateway', function ($query) { + ->whereHas('gateway', function ($query) use ($transformed_ids) { $query->where('is_deleted', 0) - ->where('deleted_at', null); - })->orderBy('is_default', 'DESC') + ->where('deleted_at', null) + ->when($transformed_ids, function ($q) use ($transformed_ids) { + $q->whereIn('id', $transformed_ids); + }); + }) + ->orderBy('is_default', 'DESC') ->get(); $filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use ($amount) { diff --git a/tests/Feature/CompanyGatewayApiTest.php b/tests/Feature/CompanyGatewayApiTest.php index 8f0db3bcd3..9e835e51fa 100644 --- a/tests/Feature/CompanyGatewayApiTest.php +++ b/tests/Feature/CompanyGatewayApiTest.php @@ -45,6 +45,44 @@ class CompanyGatewayApiTest extends TestCase Model::reguard(); } + public function testCompanyGatewayIdsUpdateWhenAddingNewGateway() + { + $settings = $this->company->settings; + $settings->company_gateway_ids = "Xe0Vjm5ybx,Xe00Aw9Lex,Xe0RpmK3Gb"; + $this->company->settings = $settings; + $this->company->save(); + + $this->assertEquals("Xe0Vjm5ybx,Xe00Aw9Lex,Xe0RpmK3Gb", $this->company->getSetting('company_gateway_ids')); + + $data = [ + 'config' => 'random config', + 'gateway_key' => '3b6621f970ab18887c4f6dca78d3f8bb', + ]; + + /* POST */ + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/company_gateways', $data); + + $cg = $response->json(); + + $cg_id = $cg['data']['id']; + + $this->assertNotNull($cg_id); + + $response->assertStatus(200); + + $company = $this->company->fresh(); + + $settings = $company->settings; + + $this->assertCount(4, explode(',', $company->getSetting('company_gateway_ids'))); + + $this->assertStringContainsString($cg_id, $company->getSetting('company_gateway_ids')); + + } + public function testBulkActions() { $cg = CompanyGatewayFactory::create($this->company->id, $this->user->id); diff --git a/tests/Feature/CompanyGatewayTest.php b/tests/Feature/CompanyGatewayTest.php index d194d4106a..36b2393550 100644 --- a/tests/Feature/CompanyGatewayTest.php +++ b/tests/Feature/CompanyGatewayTest.php @@ -12,12 +12,10 @@ namespace Tests\Feature; -use App\Jobs\Invoice\CheckGatewayFee; use App\Models\CompanyGateway; use App\Models\GatewayType; use App\Models\Invoice; use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\MockAccountData; use Tests\TestCase; From faa67f7cc0f936b13683939f1ce9c10ad3cc5f28 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 13 Jan 2026 23:32:02 +1100 Subject: [PATCH 084/177] Minor fixes for payment intent processing --- .../Stripe/Jobs/PaymentIntentProcessingWebhook.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentProcessingWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentProcessingWebhook.php index 36f976d627..d1e9bdc9e5 100644 --- a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentProcessingWebhook.php +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentProcessingWebhook.php @@ -83,7 +83,8 @@ class PaymentIntentProcessingWebhook implements ShouldQueue /** @var \App\Models\ClientGatewayToken $cgt **/ $cgt = ClientGatewayToken::where('token', $transaction['payment_method'])->first(); - if ($cgt && $cgt->meta?->state == 'unauthorized') { + if ($cgt && isset($cgt->meta)) { + // if ($cgt && $cgt->meta?->state == 'unauthorized') { $meta = $cgt->meta; $meta->state = 'authorized'; $cgt->meta = $meta; From 7351494447a4e83181fa293d468cbd4a6a982e58 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 14 Jan 2026 12:32:22 +1100 Subject: [PATCH 085/177] Updates for PEPPOL invoices --- app/Helpers/SwissQr/SwissQrGenerator.php | 4 ++-- app/Models/BaseModel.php | 2 +- app/Models/Invoice.php | 2 +- app/Services/EDocument/Standards/Peppol.php | 19 +++++++++++-------- phpstan.neon | 1 + 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/Helpers/SwissQr/SwissQrGenerator.php b/app/Helpers/SwissQr/SwissQrGenerator.php index 0859f7271a..33b627d22a 100644 --- a/app/Helpers/SwissQr/SwissQrGenerator.php +++ b/app/Helpers/SwissQr/SwissQrGenerator.php @@ -112,11 +112,11 @@ class SwissQrGenerator // Add payment reference // This is what you will need to identify incoming payments. - if (stripos($this->invoice->number, "Live") === 0) { + if (stripos($this->invoice->number ?? '', "Live") === 0) { // we're currently in preview status. Let's give a dummy reference for now $invoice_number = "123456789"; } else { - $tempInvoiceNumber = $this->invoice->number; + $tempInvoiceNumber = $this->invoice->number ?? ''; $tempInvoiceNumber = preg_replace('/[^A-Za-z0-9]/', '', $tempInvoiceNumber); // $tempInvoiceNumber = substr($tempInvoiceNumber, 1); diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index e9bae19646..4356e00253 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -279,7 +279,7 @@ class BaseModel extends Model public function numberFormatter() { - $number = strlen($this->number) >= 1 ? $this->translate_entity() . "_" . $this->number : class_basename($this) . "_" . Str::random(5); + $number = strlen($this->number ?? '') >= 1 ? $this->translate_entity() . "_" . $this->number : class_basename($this) . "_" . Str::random(5); $formatted_number = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $number); diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index d6d8124b1d..ea84fe7cf2 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -38,7 +38,7 @@ use App\Utils\Number; * App\Models\Invoice * * @property int $id - * @property object|null $e_invoice + * @property object|array|null $e_invoice * @property int $client_id * @property int $user_id * @property int|null $location_id diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 19c04e168a..b7fabbc46b 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -65,7 +65,7 @@ class Peppol extends AbstractService * */ - private ?string $override_vat_number; + private string $override_vat_number = ''; /** @var array $InvoiceTypeCodes */ private array $InvoiceTypeCodes = [ @@ -657,7 +657,8 @@ class Peppol extends AbstractService $tax_type = 'S'; break; case Product::PRODUCT_TYPE_REDUCED_TAX: - $tax_type = 'AA'; + // $tax_type = 'AA'; + $tax_type = 'S'; //2026-01-14 - using AA breaks PEPPOL VALIDATION!! break; case Product::PRODUCT_TYPE_EXEMPT: $tax_type = 'E'; @@ -759,10 +760,12 @@ class Peppol extends AbstractService $this->globalTaxCategories = [$taxCategory]; if ($this->tax_category_id == 'O' && isset($this->p_invoice->AccountingSupplierParty->Party->PartyTaxScheme)) { + nlog("unset 1"); unset($this->p_invoice->AccountingSupplierParty->Party->PartyTaxScheme); } if ($this->tax_category_id == 'O' && isset($this->p_invoice->AccountingCustomerParty->Party->PartyTaxScheme)) { + nlog("unset 2"); unset($this->p_invoice->AccountingCustomerParty->Party->PartyTaxScheme); } @@ -1032,18 +1035,18 @@ class Peppol extends AbstractService $party->PartyName[] = $party_name; if (strlen($this->company->settings->vat_number ?? '') > 1) { - + $pi = new PartyIdentification(); $vatID = new ID(); $vatID->schemeID = $this->resolveScheme(); - $vatID->value = $this->override_vat_number ?? preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->company->settings->vat_number); //todo if we are cross border - switch to the supplier local vat number + $vatID->value = strlen($this->override_vat_number ?? '') > 1 ? $this->override_vat_number : preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->company->settings->vat_number); //todo if we are cross border - switch to the supplier local vat number $pi->ID = $vatID; $party->PartyIdentification[] = $pi; $pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme(); $companyID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CompanyID(); - $companyID->value = $this->override_vat_number ?? preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->company->settings->vat_number); + $companyID->value = strlen($this->override_vat_number ?? '') > 1 ? $this->override_vat_number : preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->company->settings->vat_number); //todo if we are cross border - switch to the supplier local vat number $pts->CompanyID = $companyID; $ts = new TaxScheme(); @@ -1498,9 +1501,9 @@ class Peppol extends AbstractService $category_id->value = $this->getTaxType($grouped_tax['tax_id']); // Standard rate // Temp fix for reduced tax rate categorization. - if($grouped_tax['tax_rate'] < 15 && $grouped_tax['tax_rate'] >= 0) { - $category_id->value = 'AA'; - } + // if($grouped_tax['tax_rate'] < 15 && $grouped_tax['tax_rate'] >= 0) { + // $category_id->value = 'AA'; + // } $tax_category->ID = $category_id; diff --git a/phpstan.neon b/phpstan.neon index 0b2d3fd189..20d5a7cbdd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -30,6 +30,7 @@ parameters: - \Stripe\Collection reportUnmatchedIgnoredErrors: false ignoreErrors: + - '#\Saxon\SaxonProcessor#' - '#Array has 2 duplicate keys with value#' - '#Call to an undefined method#' - '#makeHidden#' From 6ac17bc4c6093bbde3747327308073c55ff9c77f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 14 Jan 2026 12:34:42 +1100 Subject: [PATCH 086/177] Cleanup --- app/Services/EDocument/Standards/Peppol.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index b7fabbc46b..c253e41277 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -760,12 +760,10 @@ class Peppol extends AbstractService $this->globalTaxCategories = [$taxCategory]; if ($this->tax_category_id == 'O' && isset($this->p_invoice->AccountingSupplierParty->Party->PartyTaxScheme)) { - nlog("unset 1"); unset($this->p_invoice->AccountingSupplierParty->Party->PartyTaxScheme); } if ($this->tax_category_id == 'O' && isset($this->p_invoice->AccountingCustomerParty->Party->PartyTaxScheme)) { - nlog("unset 2"); unset($this->p_invoice->AccountingCustomerParty->Party->PartyTaxScheme); } @@ -1293,7 +1291,6 @@ class Peppol extends AbstractService ///////////////// Helper Methods ///////////////////////// - /** * setInvoiceDefaults * From dfd4fd051eeaa19aad22f28555ba8af379b5407f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 14 Jan 2026 13:25:29 +1100 Subject: [PATCH 087/177] Handle null tax details --- app/Services/Report/TaxPeriodReport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/Report/TaxPeriodReport.php b/app/Services/Report/TaxPeriodReport.php index 189a6c2d47..0318a5c18f 100644 --- a/app/Services/Report/TaxPeriodReport.php +++ b/app/Services/Report/TaxPeriodReport.php @@ -450,7 +450,7 @@ class TaxPeriodReport extends BaseExport $this->data['invoices'][] = $invoice_row_builder->build(); // Build and add invoice item rows for each tax detail - foreach ($event->metadata->tax_report->tax_details as $tax_detail_data) { + foreach ($event->metadata->tax_report->tax_details ?? [] as $tax_detail_data) { $tax_detail = TaxDetail::fromMetadata($tax_detail_data); $item_row_builder = new InvoiceItemReportRow( From ba3d19246b5d0cef66f967cfa1ac9f70e565fc72 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 14 Jan 2026 16:28:35 +1100 Subject: [PATCH 088/177] Handle additional identifiers when processing inbound documents --- .../EDocument/Gateway/Storecove/StorecoveRouter.php | 3 +++ .../EDocument/Gateway/Transformers/StorecoveExpense.php | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php b/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php index 376817ecbb..6a600074e5 100644 --- a/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php +++ b/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php @@ -219,6 +219,9 @@ class StorecoveRouter $country = 'BE'; $identifier = 'BE:VAT'; } + elseif($country == 'GLN'){ + return 'routing_id'; + } $rules = $this->routing_rules[$country]; diff --git a/app/Services/EDocument/Gateway/Transformers/StorecoveExpense.php b/app/Services/EDocument/Gateway/Transformers/StorecoveExpense.php index 4bb317226b..effe08d045 100644 --- a/app/Services/EDocument/Gateway/Transformers/StorecoveExpense.php +++ b/app/Services/EDocument/Gateway/Transformers/StorecoveExpense.php @@ -189,13 +189,12 @@ class StorecoveExpense $tax_totals[] = (array)$tdf; } - $totals = collect($tax_totals); - $party = $storecove_invoice->getAccountingSupplierParty()->getParty(); $pis = $storecove_invoice->getAccountingSupplierParty()->getPublicIdentifiers(); $vat_number = ''; $id_number = ''; + $routing_id = ''; foreach ($pis as $pi) { if ($ident = $this->storecove->router->resolveIdentifierTypeByValue($pi->getScheme())) { @@ -203,6 +202,8 @@ class StorecoveExpense $vat_number = $pi->getId(); } elseif ($ident == 'id_number') { $id_number = $pi->getId(); + } elseif ($ident == 'routing_id') { + $routing_id = $pi->getId(); } } } @@ -271,6 +272,7 @@ class StorecoveExpense 'currency_id' => $currency, 'id_number' => $id_number, 'vat_number' => $vat_number, + 'routing_id' => $routing_id, 'address1' => $party->getAddress()->getStreet1() ?? '', 'address2' => $party->getAddress()->getStreet2() ?? '', 'city' => $party->getAddress()->getCity() ?? '', From 33d300f4e6cb92acfcae112cdf12b561405814e2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 14 Jan 2026 16:58:02 +1100 Subject: [PATCH 089/177] Minor fixes --- app/Http/Controllers/ImportController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 00110b8d13..c2661b64ca 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -456,7 +456,7 @@ class ImportController extends Controller $headers = $data[0]; // Remove Invoice Ninja headers - if (count($headers) && count($data) > 4) { + if (is_array($headers) && count($headers) > 0 && count($data) > 4) { $firstCell = $headers[0]; if (strstr($firstCell, (string) config('ninja.app_name'))) { From f6f888decc33b541c6ab096294d954b3cf7d420e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 14 Jan 2026 18:31:50 +1100 Subject: [PATCH 090/177] Working on additional helper to debug bad imports --- .../Sources/PayPalBalanceAffecting.php | 2 +- app/Helpers/Cache/Atomic.php | 23 +++-- app/Http/Controllers/ImportController.php | 1 - app/Import/Providers/BaseImport.php | 94 +++++++++++++++---- app/Import/Providers/Csv.php | 1 - app/Utils/HtmlEngine.php | 7 +- .../subscriptions/ninja_plan.blade.php | 6 +- .../Export/ReportCsvGenerationTest.php | 6 +- 8 files changed, 98 insertions(+), 42 deletions(-) diff --git a/app/DataMapper/Sources/PayPalBalanceAffecting.php b/app/DataMapper/Sources/PayPalBalanceAffecting.php index d898f4d651..11a4389e80 100644 --- a/app/DataMapper/Sources/PayPalBalanceAffecting.php +++ b/app/DataMapper/Sources/PayPalBalanceAffecting.php @@ -236,7 +236,7 @@ class PayPalBalanceAffecting -// $csv = Reader::createFromString($csvFile); +// $csv = Reader::fromString($csvFile); // // $csvdelimiter = self::detectDelimiter($csvfile); // $csv->setDelimiter(","); // $stmt = new Statement(); diff --git a/app/Helpers/Cache/Atomic.php b/app/Helpers/Cache/Atomic.php index 6f794290be..e3c38bf854 100644 --- a/app/Helpers/Cache/Atomic.php +++ b/app/Helpers/Cache/Atomic.php @@ -11,37 +11,42 @@ namespace App\Helpers\Cache; +use Illuminate\Contracts\Redis\Factory as RedisFactory; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Redis; class Atomic { - public static function set($key, $value = true, $ttl = 1): bool + public static function set(string $key, mixed $value = true, int $ttl = 1): bool { $new_ttl = now()->addSeconds($ttl); try { - return Redis::connection('sentinel-cache')->set($key, $value, 'EX', $ttl, 'NX') ? true : false; + /** @var RedisFactory $redis */ + $redis = app('redis'); + $result = $redis->connection('sentinel-cache')->command('set', [$key, $value, 'EX', $ttl, 'NX']); + return (bool) $result; } catch (\Throwable) { return Cache::add($key, $value, $new_ttl) ? true : false; } - } - public static function get($key) + public static function get(string $key): mixed { try { - return Redis::connection('sentinel-cache')->get($key); + /** @var RedisFactory $redis */ + $redis = app('redis'); + return $redis->connection('sentinel-cache')->command('get', [$key]); } catch (\Throwable) { return Cache::get($key); } - } - public static function del($key) + public static function del(string $key): mixed { try { - return Redis::connection('sentinel-cache')->del($key); + /** @var RedisFactory $redis */ + $redis = app('redis'); + return $redis->connection('sentinel-cache')->command('del', [$key]); } catch (\Throwable) { return Cache::forget($key); } diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index c2661b64ca..cbbd3101f2 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -446,7 +446,6 @@ class ImportController extends Controller $csv = Reader::fromString($csvfile); - // $csv = Reader::createFromString($csvfile); $csvdelimiter = self::detectDelimiter($csvfile); $csv->setDelimiter($csvdelimiter); $stmt = new Statement(); diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index 913ac1fa3a..b2096e3a31 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -12,33 +12,35 @@ namespace App\Import\Providers; +use App\Models\User; +use App\Utils\Ninja; +use App\Models\Quote; +use League\Csv\Reader; +use App\Models\Company; +use App\Models\Invoice; +use League\Csv\Statement; +use App\Factory\TaskFactory; +use App\Factory\QuoteFactory; use App\Factory\ClientFactory; +use Illuminate\Support\Carbon; use App\Factory\InvoiceFactory; use App\Factory\PaymentFactory; -use App\Factory\QuoteFactory; -use App\Factory\RecurringInvoiceFactory; -use App\Factory\TaskFactory; -use App\Http\Requests\Quote\StoreQuoteRequest; use App\Import\ImportException; use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerObject; -use App\Mail\Import\CsvImportCompleted; -use App\Models\Company; -use App\Models\Invoice; -use App\Models\Quote; -use App\Models\User; -use App\Repositories\ClientRepository; -use App\Repositories\InvoiceRepository; -use App\Repositories\PaymentRepository; -use App\Repositories\QuoteRepository; -use App\Repositories\RecurringInvoiceRepository; use App\Repositories\TaskRepository; use App\Utils\Traits\CleanLineItems; -use Illuminate\Support\Carbon; +use App\Repositories\QuoteRepository; use Illuminate\Support\Facades\Cache; +use App\Repositories\ClientRepository; +use App\Mail\Import\CsvImportCompleted; +use App\Repositories\InvoiceRepository; +use App\Repositories\PaymentRepository; +use App\Factory\RecurringInvoiceFactory; use Illuminate\Support\Facades\Validator; -use League\Csv\Reader; -use League\Csv\Statement; +use App\Http\Requests\Quote\StoreQuoteRequest; +use App\Repositories\RecurringInvoiceRepository; +use App\Notifications\Ninja\GenericNinjaAdminNotification; class BaseImport { @@ -70,6 +72,8 @@ class BaseImport public array $entity_count = []; + public bool $store_import_for_research = false; + public function __construct(array $request, Company $company) { $this->company = $company; @@ -107,7 +111,7 @@ class BaseImport $csv = base64_decode($base64_encoded_csv); // $csv = mb_convert_encoding($csv, 'UTF-8', 'UTF-8'); - $csv = Reader::createFromString($csv); + $csv = Reader::fromString($csv); $csvdelimiter = self::detectDelimiter($csv); $csv->setDelimiter($csvdelimiter); @@ -119,7 +123,8 @@ class BaseImport // Remove Invoice Ninja headers if ( - count($headers) && + is_array($headers) && + count($headers) > 0 && count($data) > 4 && $this->import_type === 'csv' ) { @@ -320,7 +325,8 @@ class BaseImport $entity->saveQuietly(); $count++; } - } catch (\Exception $ex) { + } + catch (\Exception $ex) { if (\DB::connection(config('database.default'))->transactionLevel() > 0) { \DB::connection(config('database.default'))->rollBack(); } @@ -339,6 +345,20 @@ class BaseImport nlog("Ingest {$ex->getMessage()}"); nlog($record); + + $this->store_import_for_research = true; + + } + catch(\Throwable $ex){ + if (\DB::connection(config('database.default'))->transactionLevel() > 0) { + \DB::connection(config('database.default'))->rollBack(); + } + + nlog("Throwable:: Ingest {$ex->getMessage()}"); + nlog($record); + + $this->store_import_for_research = true; + } } @@ -945,6 +965,40 @@ class BaseImport $nmo->to_user = $this->company->owner(); NinjaMailerJob::dispatch($nmo, true); + + if (Ninja::isHosted() && $this->store_import_for_research) { + + $content = [ + 'company_key' => $this->company->company_key, + 'class_name' => class_basename($this), + 'hash' => $this->hash, + ]; + + $potential_imports = [ + 'client', + 'product', + 'invoice', + 'payment', + 'vendor', + 'expense', + 'quote', + 'bank_transaction', + 'task', + 'recurring_invoice', + ]; + + foreach ($potential_imports as $import) { + + if(Cache::has($this->hash.'-'.$import)) { + // store on s3 + // Cache::put($this->hash.'-'.$import, Cache::get($this->hash.'-'.$import), 60*60*24*2); + } + } + + $this->company->notification(new GenericNinjaAdminNotification($content))->ninja(); + + + } } public function preTransform(array $data, $entity_type) diff --git a/app/Import/Providers/Csv.php b/app/Import/Providers/Csv.php index c935c28166..f3ebc1be3e 100644 --- a/app/Import/Providers/Csv.php +++ b/app/Import/Providers/Csv.php @@ -382,7 +382,6 @@ class Csv extends BaseImport implements ImportInterface $this->entity_count['tasks'] = $task_count; - } public function transform(array $data) diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index cf813e7edc..94bfa1a0f2 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -937,11 +937,10 @@ Código seguro de verificación (CSV): {$verifactu_log->status}"; $tax_label = ''; if (collect($this->entity->line_items)->contains('tax_id', \App\Models\Product::PRODUCT_TYPE_REVERSE_TAX)) { - $tax_label .= ctrans('texts.reverse_tax_info') . "
"; + $tax_label .= ctrans('texts.reverse_tax_info') . "
"; } - - if ((int)$this->client->country_id !== (int)$this->company->settings->country_id) { - $tax_label .= ctrans('texts.intracommunity_tax_info') . "
"; + else if ((int)$this->client->country_id !== (int)$this->company->settings->country_id) { + $tax_label .= ctrans('texts.intracommunity_tax_info') . "
"; if ($this->entity_calc->getTotalTaxes() > 0) { $tax_label = ''; diff --git a/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php b/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php index 0cfa66ff82..cc9ea996d8 100644 --- a/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php +++ b/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php @@ -178,7 +178,7 @@ input:checked ~ .dot { Enterprise Plan

- $140 + $180

yearly @@ -275,12 +275,12 @@ document.getElementById('handleProYearlyClick').addEventListener('click', functi }); const price_map = new Map(); //monthly -price_map.set('7LDdwRb1YK', '$16'); +price_map.set('7LDdwRb1YK', '$18'); price_map.set('MVyb8mdvAZ', '$32'); price_map.set('WpmbkR5azJ', '$54'); price_map.set('k8mepY2aMy', '$84'); //yearly -price_map.set('LYqaQWldnj', '$160'); +price_map.set('LYqaQWldnj', '$180'); price_map.set('kQBeX6mbyK', '$320'); price_map.set('GELe32Qd69', '$540'); price_map.set('MVyb86oevA', '$840'); diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index abf25cf410..687744749a 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -1134,7 +1134,7 @@ class ReportCsvGenerationTest extends TestCase $csv = $response->body(); - $reader = Reader::createFromString($csv); + $reader = Reader::fromString($csv); $reader->setHeaderOffset(0); $res = $reader->fetchColumnByName('Street'); @@ -1983,7 +1983,7 @@ class ReportCsvGenerationTest extends TestCase $csv = $response->body(); - $reader = Reader::createFromString($csv); + $reader = Reader::fromString($csv); $reader->setHeaderOffset(0); $res = $reader->fetchColumnByName('Contact First Name'); @@ -2014,7 +2014,7 @@ class ReportCsvGenerationTest extends TestCase private function getFirstValueByColumn($csv, $column) { - $reader = Reader::createFromString($csv); + $reader = Reader::fromString($csv); $reader->setHeaderOffset(0); $res = $reader->fetchColumnByName($column); From c068f38f51350c0632b907dce8b7f8f52db9546d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 14 Jan 2026 18:37:03 +1100 Subject: [PATCH 091/177] Working on additional helper to debug bad imports --- app/Import/Providers/BaseImport.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index b2096e3a31..759268a2bf 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -966,12 +966,13 @@ class BaseImport NinjaMailerJob::dispatch($nmo, true); + /** Debug for import failures */ if (Ninja::isHosted() && $this->store_import_for_research) { $content = [ - 'company_key' => $this->company->company_key, - 'class_name' => class_basename($this), - 'hash' => $this->hash, + 'company_key - '. $this->company->company_key, + 'class_name - ' . class_basename($this), + 'hash - ' => $this->hash, ]; $potential_imports = [ @@ -990,14 +991,12 @@ class BaseImport foreach ($potential_imports as $import) { if(Cache::has($this->hash.'-'.$import)) { - // store on s3 - // Cache::put($this->hash.'-'.$import, Cache::get($this->hash.'-'.$import), 60*60*24*2); + Cache::put($this->hash.'-'.$import, Cache::get($this->hash.'-'.$import), 60*60*24*2); } } $this->company->notification(new GenericNinjaAdminNotification($content))->ninja(); - } } From c21893b0a8ba5431d639c9649c1cd66775f18aac Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 15 Jan 2026 07:00:59 +1100 Subject: [PATCH 092/177] V5.12.46 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index e9a24b696d..331e550baf 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.12.45 \ No newline at end of file +5.12.46 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 6858635e12..69426c2278 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.12.45'), - 'app_tag' => env('APP_TAG', '5.12.45'), + 'app_version' => env('APP_VERSION', '5.12.46'), + 'app_tag' => env('APP_TAG', '5.12.46'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), From 23d258bcb697e7bafcfa36e47d15d546b2ffeb0d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 15 Jan 2026 08:40:37 +1100 Subject: [PATCH 093/177] Improve output for double encoded entities --- app/Models/BaseModel.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 4356e00253..7334c6158b 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -391,8 +391,9 @@ class BaseModel extends Model $section = strtr($this->{$field}, $variables['labels']); - return strtr($section, $variables['values']); + $parsed = strtr($section, $variables['values']); + return \App\Services\Pdf\Purify::clean(html_entity_decode($parsed)); } /** From ea2bf3c505b257359610efc125d23e11ca6090e1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 15 Jan 2026 09:06:55 +1100 Subject: [PATCH 094/177] Change for exception handling in postmark webhooks --- app/Jobs/PostMark/ProcessPostmarkWebhook.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php index d1199a57f5..4b69386933 100644 --- a/app/Jobs/PostMark/ProcessPostmarkWebhook.php +++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php @@ -362,7 +362,7 @@ class ProcessPostmarkWebhook implements ShouldQueue try { $messageDetail = $postmark->getOutboundMessageDetails($message_id); - } catch (\Exception $e) { + } catch (\Throwable $e) { $postmark_secret = config('services.postmark-outlook.token'); $postmark = new PostmarkClient($postmark_secret); @@ -380,7 +380,6 @@ class ProcessPostmarkWebhook implements ShouldQueue $messageDetail = $this->getRawMessage($message_id); - $event = collect($messageDetail->messageevents)->first(function ($event) { return $event?->Details?->BounceID ?? false; From 6c7eb28b2a2e3f8bf9a9b6dc6560cde0ae335141 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 16 Jan 2026 08:52:58 +1100 Subject: [PATCH 095/177] Set stable version of einvoice --- app/Services/EDocument/Standards/Peppol.php | 4 ++-- composer.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index c253e41277..68b3c9a67d 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -217,9 +217,9 @@ class Peppol extends AbstractService /** Auto switch between Invoice / Credit based on the amount value */ - // $this->p_invoice->InvoiceTypeCode = ($this->invoice->amount >= 0) ? 380 : 381; + $this->p_invoice->InvoiceTypeCode = ($this->invoice->amount >= 0) ? 380 : 381; - $this->p_invoice->InvoiceTypeCode = 380; + // $this->p_invoice->InvoiceTypeCode = 380; $this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty(); $this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty(); diff --git a/composer.json b/composer.json index 9686b51f48..bd39bc4bc4 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "hyvor/php-json-exporter": "^0.0.3", "imdhemy/laravel-purchases": "^1.7", "intervention/image": "^2.5", - "invoiceninja/einvoice": "dev-main", + "invoiceninja/einvoice": "v1.0.0", "invoiceninja/ubl_invoice": "^3", "josemmo/facturae-php": "^1.7", "laracasts/presenter": "^0.2.1", From 3c7d954613ea6fe77c230a362d975d4cd4aa7bb9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 16 Jan 2026 09:40:54 +1100 Subject: [PATCH 096/177] Updated dependencieS --- app/Services/EDocument/Standards/Peppol.php | 314 +++++++++++++++++--- composer.json | 2 +- composer.lock | 90 +++--- 3 files changed, 314 insertions(+), 92 deletions(-) diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 68b3c9a67d..7f0fa82c00 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -12,6 +12,7 @@ namespace App\Services\EDocument\Standards; +use App\Models\Credit; use App\Models\Company; use App\Models\Invoice; use App\Models\Product; @@ -41,6 +42,7 @@ use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxableAmount; use InvoiceNinja\EInvoice\Models\Peppol\PeriodType\InvoicePeriod; use InvoiceNinja\EInvoice\Models\Peppol\CodeType\IdentificationCode; use InvoiceNinja\EInvoice\Models\Peppol\InvoiceLineType\InvoiceLine; +use InvoiceNinja\EInvoice\Models\Peppol\CreditNoteLineType\CreditNoteLine; use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory; use InvoiceNinja\EInvoice\Models\Peppol\TaxSubtotalType\TaxSubtotal; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount; @@ -131,7 +133,8 @@ class Peppol extends AbstractService private InvoiceSum | InvoiceSumInclusive $calc; - private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice; + /** @var \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote */ + private \InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $p_invoice; private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_client_settings; @@ -139,6 +142,9 @@ class Peppol extends AbstractService private EInvoice $e; + /** @var bool Flag to indicate if document is a Credit Note */ + private bool $isCreditNote = false; + private string $api_network = Storecove::class; // Storecove::class; public Storecove $gateway; @@ -157,16 +163,40 @@ class Peppol extends AbstractService private array $errors = []; - public function __construct(public Invoice $invoice) + public function __construct(public Invoice | Credit $invoice) { - $this->company = $invoice->company; $this->calc = $this->invoice->calc(); $this->e = new EInvoice(); $this->gateway = new $this->api_network(); + $this->isCreditNote = $this->shouldBeCreditNote(); $this->setSettings()->setInvoice(); } + /** + * Determine if the document should be a Credit Note + * + * Credit Note is used when: + * - The entity is a Credit model + * - The entity is an Invoice with a negative amount + * + * @return bool + */ + private function shouldBeCreditNote(): bool + { + // Credit model = always credit note + if ($this->invoice instanceof Credit) { + return true; + } + + // Negative invoice = credit note + if ($this->invoice instanceof Invoice && $this->invoice->amount < 0) { + return true; + } + + return false; + } + /** * Entry point for building document * @@ -200,13 +230,6 @@ class Peppol extends AbstractService $this->p_invoice->DocumentCurrencyCode = $this->invoice->client->currency()->code; - // if ($this->invoice->date && $this->invoice->due_date) { - // $ip = new InvoicePeriod(); - // $ip->StartDate = new \DateTime($this->invoice->date); - // $ip->EndDate = new \DateTime($this->invoice->due_date); - // $this->p_invoice->InvoicePeriod = [$ip]; - // } - if ($this->invoice->project_id) { $pr = new \InvoiceNinja\EInvoice\Models\Peppol\ProjectReferenceType\ProjectReference(); $id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID(); @@ -215,15 +238,17 @@ class Peppol extends AbstractService $this->p_invoice->ProjectReference = [$pr]; } - /** Auto switch between Invoice / Credit based on the amount value */ - - $this->p_invoice->InvoiceTypeCode = ($this->invoice->amount >= 0) ? 380 : 381; - - // $this->p_invoice->InvoiceTypeCode = 380; + /** Set type code and line items based on document type */ + if ($this->isCreditNote) { + $this->p_invoice->CreditNoteTypeCode = 381; + $this->p_invoice->CreditNoteLine = $this->getCreditNoteLines(); + } else { + $this->p_invoice->InvoiceTypeCode = 380; + $this->p_invoice->InvoiceLine = $this->getInvoiceLines(); + } $this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty(); $this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty(); - $this->p_invoice->InvoiceLine = $this->getInvoiceLines(); $this->p_invoice->AllowanceCharge = $this->getAllowanceCharges(); $this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal(); $this->p_invoice->Delivery = $this->getDelivery(); @@ -234,6 +259,7 @@ class Peppol extends AbstractService ->addAttachments() ->standardPeppolRules(); + //isolate this class to only peppol changes $this->p_invoice = $this->gateway ->mutator @@ -251,16 +277,16 @@ class Peppol extends AbstractService } /** - * Transforms a stdClass Invoice - * to Peppol\Invoice::class + * Transforms a stdClass document to Peppol\Invoice or Peppol\CreditNote * - * @param mixed $invoice + * @param mixed $document + * @param string $type 'Invoice' or 'CreditNote' * @return self */ - public function decode(mixed $invoice): self + public function decode(mixed $document, string $type = 'Invoice'): self { - - $this->p_invoice = $this->e->decode('Peppol', json_encode($invoice), 'json'); + $peppolType = $type === 'CreditNote' ? 'Peppol_CreditNote' : 'Peppol'; + $this->p_invoice = $this->e->decode($peppolType, json_encode($document), 'json'); return $this; } @@ -272,10 +298,10 @@ class Peppol extends AbstractService */ private function setInvoice(): self { - /** Handle Existing Document */ - if ($this->invoice->e_invoice && isset($this->invoice->e_invoice->Invoice) && isset($this->invoice->e_invoice->Invoice->ID)) { + /** Handle Existing CreditNote Document */ + if ($this->isCreditNote && $this->invoice->e_invoice && isset($this->invoice->e_invoice->CreditNote) && isset($this->invoice->e_invoice->CreditNote->ID)) { - $this->decode($this->invoice->e_invoice->Invoice); + $this->decode($this->invoice->e_invoice->CreditNote, 'CreditNote'); $this->gateway ->mutator @@ -285,11 +311,29 @@ class Peppol extends AbstractService ->setCompanySettings($this->_company_settings); return $this; - } - /** Scaffold new document */ - $this->p_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + /** Handle Existing Invoice Document */ + if (!$this->isCreditNote && $this->invoice->e_invoice && isset($this->invoice->e_invoice->Invoice) && isset($this->invoice->e_invoice->Invoice->ID)) { + + $this->decode($this->invoice->e_invoice->Invoice, 'Invoice'); + + $this->gateway + ->mutator + ->setInvoice($this->invoice) + ->setPeppol($this->p_invoice) + ->setClientSettings($this->_client_settings) + ->setCompanySettings($this->_company_settings); + + return $this; + } + + /** Scaffold new document based on type */ + if ($this->isCreditNote) { + $this->p_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\CreditNote(); + } else { + $this->p_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + } /** Set Props */ $this->gateway @@ -319,15 +363,36 @@ class Peppol extends AbstractService } /** - * getInvoice + * getDocument * - * @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice + * @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote */ - public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice + public function getDocument(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote { return $this->p_invoice; } + /** + * getInvoice + * + * @deprecated Use getDocument() instead + * @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote + */ + public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote + { + return $this->p_invoice; + } + + /** + * Check if the document is a Credit Note + * + * @return bool + */ + public function isCreditNote(): bool + { + return $this->isCreditNote; + } + /** * toXml * @@ -340,20 +405,24 @@ class Peppol extends AbstractService $e = new EInvoice(); $xml = $e->encode($this->p_invoice, 'xml'); - $prefix = ' + if ($this->isCreditNote) { + $prefix = ' +'; + $suffix = ''; + } else { + $prefix = ' '; - - $suffix = ''; + $suffix = ''; + } $xml = str_ireplace(['\n',''], ['', $prefix], $xml); - $xml .= $suffix; - // nlog($xml); return $xml; - } /** @@ -381,11 +450,15 @@ class Peppol extends AbstractService */ public function toObject(): mixed { - $invoice = new \stdClass(); + $document = new \stdClass(); - $invoice->Invoice = json_decode($this->toJson()); + if ($this->isCreditNote) { + $document->CreditNote = json_decode($this->toJson()); + } else { + $document->Invoice = json_decode($this->toJson()); + } - return $invoice; + return $document; } /** @@ -397,7 +470,8 @@ class Peppol extends AbstractService */ public function toArray(): array { - return ['Invoice' => json_decode($this->toJson(), true)]; + $key = $this->isCreditNote ? 'CreditNote' : 'Invoice'; + return [$key => json_decode($this->toJson(), true)]; } @@ -601,32 +675,37 @@ class Peppol extends AbstractService { $taxable = $this->getTaxable(); + // For credit notes, ensure all amounts are positive + $amount = $this->isCreditNote ? abs($this->invoice->amount) : $this->invoice->amount; + $totalTaxes = $this->isCreditNote ? abs($this->invoice->total_taxes) : $this->invoice->total_taxes; + $subtotal = $this->isCreditNote ? abs($this->calc->getSubtotal()) : $this->calc->getSubtotal(); + $lmt = new LegalMonetaryTotal(); $lea = new LineExtensionAmount(); $lea->currencyID = $this->invoice->client->currency()->code; - $lea->amount = $this->invoice->uses_inclusive_taxes ? round($this->invoice->amount - $this->invoice->total_taxes, 2) : $this->calc->getSubtotal(); + $lea->amount = $this->invoice->uses_inclusive_taxes ? round($amount - $totalTaxes, 2) : $subtotal; $lmt->LineExtensionAmount = $lea; $tea = new TaxExclusiveAmount(); $tea->currencyID = $this->invoice->client->currency()->code; - $tea->amount = round($this->invoice->amount - $this->invoice->total_taxes, 2); + $tea->amount = round($amount - $totalTaxes, 2); $lmt->TaxExclusiveAmount = $tea; $tia = new TaxInclusiveAmount(); $tia->currencyID = $this->invoice->client->currency()->code; - $tia->amount = $this->invoice->amount; + $tia->amount = $amount; $lmt->TaxInclusiveAmount = $tia; $pa = new PayableAmount(); $pa->currencyID = $this->invoice->client->currency()->code; - $pa->amount = $this->invoice->amount; + $pa->amount = $amount; $lmt->PayableAmount = $pa; $am = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\AllowanceTotalAmount(); $am->currencyID = $this->invoice->client->currency()->code; - $am->amount = number_format($this->calc->getTotalDiscount(), 2, '.', ''); + $am->amount = number_format(abs($this->calc->getTotalDiscount()), 2, '.', ''); $lmt->AllowanceTotalAmount = $am; $cta = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\ChargeTotalAmount(); @@ -936,6 +1015,149 @@ class Peppol extends AbstractService return $lines; } + /** + * getCreditNoteLines + * + * Compiles the credit note line items of the document + * + * @return array + */ + private function getCreditNoteLines(): array + { + $lines = []; + + foreach ($this->invoice->line_items as $key => $item) { + + $_item = new Item(); + $_item->Name = strlen($item->product_key ?? '') >= 1 ? $item->product_key : ctrans('texts.item'); + $_item->Description = $item->notes; + + $ctc = new ClassifiedTaxCategory(); + $ctc->ID = new ID(); + $ctc->ID->value = $this->getTaxType($item->tax_id); + + if ($item->tax_rate1 > 0) { + $ctc->Percent = (string)$item->tax_rate1; + } + + $ts = new TaxScheme(); + $id = new ID(); + $id->value = $this->standardizeTaxSchemeId($item->tax_name1); + $ts->ID = $id; + $ctc->TaxScheme = $ts; + + if (floatval($item->tax_rate1) === 0.0) { + $ctc = $this->resolveTaxExemptReason($item, $ctc); + + if ($this->tax_category_id == 'O') { + unset($ctc->Percent); + } + } + + $_item->ClassifiedTaxCategory[] = $ctc; + + if ($item->tax_rate2 > 0) { + $ctc = new ClassifiedTaxCategory(); + $ctc->ID = new ID(); + $ctc->ID->value = $this->getTaxType($item->tax_id); + $ctc->Percent = (string)$item->tax_rate2; + + $ts = new TaxScheme(); + $id = new ID(); + $id->value = $this->standardizeTaxSchemeId($item->tax_name2); + $ts->ID = $id; + $ctc->TaxScheme = $ts; + + $_item->ClassifiedTaxCategory[] = $ctc; + } + + if ($item->tax_rate3 > 0) { + $ctc = new ClassifiedTaxCategory(); + $ctc->ID = new ID(); + $ctc->ID->value = $this->getTaxType($item->tax_id); + $ctc->Percent = (string)$item->tax_rate3; + + $ts = new TaxScheme(); + $id = new ID(); + $id->value = $this->standardizeTaxSchemeId($item->tax_name3); + $ts->ID = $id; + $ctc->TaxScheme = $ts; + + $_item->ClassifiedTaxCategory[] = $ctc; + } + + $line = new CreditNoteLine(); + + $id = new ID(); + $id->value = (string) ($key + 1); + $line->ID = $id; + + // Use CreditedQuantity instead of InvoicedQuantity + $cq = new \InvoiceNinja\EInvoice\Models\Peppol\QuantityType\CreditedQuantity(); + $cq->amount = abs($item->quantity); // Ensure positive quantity + $cq->unitCode = $item->unit_code ?? 'C62'; + $line->CreditedQuantity = $cq; + + $lea = new LineExtensionAmount(); + $lea->currencyID = $this->invoice->client->currency()->code; + $lineTotal = $this->invoice->uses_inclusive_taxes + ? round($item->line_total - $this->calcInclusiveLineTax($item->tax_rate1, $item->line_total), 2) + : round($item->line_total, 2); + $lea->amount = abs($lineTotal); // Ensure positive amount + $line->LineExtensionAmount = $lea; + $line->Item = $_item; + + // Handle Price and Discounts + if ($item->discount > 0) { + + // Base Price (before discount) + $basePrice = new Price(); + $basePriceAmount = new PriceAmount(); + $basePriceAmount->currencyID = $this->invoice->client->currency()->code; + $basePriceAmount->amount = (string)abs($item->cost); + $basePrice->PriceAmount = $basePriceAmount; + + // Add Allowance Charge to Price + $allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge(); + $allowanceCharge->ChargeIndicator = 'false'; // false = discount + $allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount(); + $allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code; + $allowanceCharge->Amount->amount = number_format($this->calculateTotalItemDiscountAmount($item), 2, '.', ''); + $this->allowance_total += $this->calculateTotalItemDiscountAmount($item); + + // Add percentage if available + if ($item->discount > 0 && !$item->is_amount_discount) { + + $allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount(); + $allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code; + $allowanceCharge->BaseAmount->amount = (string)round(abs($item->cost * $item->quantity), 2); + + $mfn = new \InvoiceNinja\EInvoice\Models\Peppol\NumericType\MultiplierFactorNumeric(); + $mfn->value = (string) round($item->discount, 2); + $allowanceCharge->MultiplierFactorNumeric = $mfn; + } + + $allowanceCharge->AllowanceChargeReason = ctrans('texts.discount'); + + $line->Price = $basePrice; + $line->AllowanceCharge[] = $allowanceCharge; + + } else { + // No discount case + $price = new Price(); + $pa = new PriceAmount(); + $pa->currencyID = $this->invoice->client->currency()->code; + $pa->amount = (string)abs($item->cost); + $price->PriceAmount = $pa; + $line->Price = $price; + } + + $lines[] = $line; + } + + return $lines; + } + private function calculateTotalItemDiscountAmount($item): float { diff --git a/composer.json b/composer.json index bd39bc4bc4..9686b51f48 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "hyvor/php-json-exporter": "^0.0.3", "imdhemy/laravel-purchases": "^1.7", "intervention/image": "^2.5", - "invoiceninja/einvoice": "v1.0.0", + "invoiceninja/einvoice": "dev-main", "invoiceninja/ubl_invoice": "^3", "josemmo/facturae-php": "^1.7", "laracasts/presenter": "^0.2.1", diff --git a/composer.lock b/composer.lock index 9100dc38a9..8f4dd78ed8 100644 --- a/composer.lock +++ b/composer.lock @@ -497,16 +497,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.11", + "version": "3.369.14", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "014c521eae8eeb1f9562cb512523b17cd6c1bec7" + "reference": "b40eb1177d2e621c18cd797ca6cc9efb5a0e99d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/014c521eae8eeb1f9562cb512523b17cd6c1bec7", - "reference": "014c521eae8eeb1f9562cb512523b17cd6c1bec7", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b40eb1177d2e621c18cd797ca6cc9efb5a0e99d9", + "reference": "b40eb1177d2e621c18cd797ca6cc9efb5a0e99d9", "shasum": "" }, "require": { @@ -588,9 +588,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.369.11" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.14" }, - "time": "2026-01-12T19:14:19+00:00" + "time": "2026-01-15T19:10:54+00:00" }, { "name": "babenkoivan/elastic-adapter", @@ -4488,12 +4488,12 @@ "source": { "type": "git", "url": "https://github.com/invoiceninja/einvoice.git", - "reference": "811eed276e2de35e513a9b03ff14c50fbffcedf3" + "reference": "4812a7ed9db0c5710a371618e696da23f19b0ed5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/811eed276e2de35e513a9b03ff14c50fbffcedf3", - "reference": "811eed276e2de35e513a9b03ff14c50fbffcedf3", + "url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/4812a7ed9db0c5710a371618e696da23f19b0ed5", + "reference": "4812a7ed9db0c5710a371618e696da23f19b0ed5", "shasum": "" }, "require": { @@ -4535,7 +4535,7 @@ "source": "https://github.com/invoiceninja/einvoice/tree/main", "issues": "https://github.com/invoiceninja/einvoice/issues" }, - "time": "2025-11-27T01:49:29+00:00" + "time": "2026-01-15T22:25:34+00:00" }, { "name": "invoiceninja/ubl_invoice", @@ -5390,16 +5390,16 @@ }, { "name": "laravel/prompts", - "version": "v0.3.8", + "version": "v0.3.10", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" + "reference": "360ba095ef9f51017473505191fbd4ab73e1cab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", - "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", + "url": "https://api.github.com/repos/laravel/prompts/zipball/360ba095ef9f51017473505191fbd4ab73e1cab3", + "reference": "360ba095ef9f51017473505191fbd4ab73e1cab3", "shasum": "" }, "require": { @@ -5443,9 +5443,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.8" + "source": "https://github.com/laravel/prompts/tree/v0.3.10" }, - "time": "2025-11-21T20:52:52+00:00" + "time": "2026-01-13T20:29:29+00:00" }, { "name": "laravel/scout", @@ -5529,16 +5529,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.7", + "version": "v2.0.8", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", - "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", "shasum": "" }, "require": { @@ -5586,7 +5586,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-11-21T20:52:36+00:00" + "time": "2026-01-08T16:22:46+00:00" }, { "name": "laravel/slack-notification-channel", @@ -5655,16 +5655,16 @@ }, { "name": "laravel/socialite", - "version": "v5.24.1", + "version": "v5.24.2", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "25e28c14d55404886777af1d77cf030e0f633142" + "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/25e28c14d55404886777af1d77cf030e0f633142", - "reference": "25e28c14d55404886777af1d77cf030e0f633142", + "url": "https://api.github.com/repos/laravel/socialite/zipball/5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613", + "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613", "shasum": "" }, "require": { @@ -5723,7 +5723,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2026-01-01T02:57:21+00:00" + "time": "2026-01-10T16:07:28+00:00" }, { "name": "laravel/tinker", @@ -6844,16 +6844,16 @@ }, { "name": "livewire/livewire", - "version": "v3.7.3", + "version": "v3.7.4", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c" + "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", - "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", + "url": "https://api.github.com/repos/livewire/livewire/zipball/5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0", + "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0", "shasum": "" }, "require": { @@ -6908,7 +6908,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.7.3" + "source": "https://github.com/livewire/livewire/tree/v3.7.4" }, "funding": [ { @@ -6916,7 +6916,7 @@ "type": "github" } ], - "time": "2025-12-19T02:00:29+00:00" + "time": "2026-01-13T09:37:21+00:00" }, { "name": "maennchen/zipstream-php", @@ -18607,7 +18607,7 @@ }, { "name": "illuminate/json-schema", - "version": "v12.46.0", + "version": "v12.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/json-schema.git", @@ -18802,16 +18802,16 @@ }, { "name": "laravel/boost", - "version": "v1.8.9", + "version": "v1.8.10", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd" + "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd", - "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd", + "url": "https://api.github.com/repos/laravel/boost/zipball/aad8b2a423b0a886c2ce7ee92abbfde69992ff32", + "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32", "shasum": "" }, "require": { @@ -18864,7 +18864,7 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-01-07T18:43:11+00:00" + "time": "2026-01-14T14:51:16+00:00" }, { "name": "laravel/mcp", @@ -19764,16 +19764,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.46", + "version": "11.5.47", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "75dfe79a2aa30085b7132bb84377c24062193f33" + "reference": "a8c3c540923f8a3d499659b927228059bb3809d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/75dfe79a2aa30085b7132bb84377c24062193f33", - "reference": "75dfe79a2aa30085b7132bb84377c24062193f33", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a8c3c540923f8a3d499659b927228059bb3809d8", + "reference": "a8c3c540923f8a3d499659b927228059bb3809d8", "shasum": "" }, "require": { @@ -19787,7 +19787,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.11", + "phpunit/php-code-coverage": "^11.0.12", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", @@ -19845,7 +19845,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.46" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.47" }, "funding": [ { @@ -19869,7 +19869,7 @@ "type": "tidelift" } ], - "time": "2025-12-06T08:01:15+00:00" + "time": "2026-01-15T12:00:46+00:00" }, { "name": "react/cache", From 1cb34acd65b26d587e98a7736124f5625061e169 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 16 Jan 2026 11:35:02 +1100 Subject: [PATCH 097/177] Updates for peppol --- .../EDocument/Gateway/MutatorInterface.php | 6 + .../EDocument/Gateway/Qvalia/Mutator.php | 9 +- .../EDocument/Gateway/Storecove/Mutator.php | 7 +- app/Services/EDocument/Imports/Ubl2Pdf.php | 6 +- .../EDocument/Imports/UblEDocument.php | 77 +- app/Services/EDocument/Standards/Peppol.php | 88 +- .../Validation/Peppol/EntityLevel.php | 2 +- .../Stylesheets/UBL2.1/UBL-CreditNote-2.1.xsd | 951 ++++++++++++++++++ .../Validation/XsltDocumentValidator.php | 51 +- app/Services/Report/ARDetailReport.php | 3 +- app/Services/Report/ARSummaryReport.php | 3 +- 11 files changed, 1145 insertions(+), 58 deletions(-) create mode 100644 app/Services/EDocument/Standards/Validation/Peppol/Stylesheets/UBL2.1/UBL-CreditNote-2.1.xsd diff --git a/app/Services/EDocument/Gateway/MutatorInterface.php b/app/Services/EDocument/Gateway/MutatorInterface.php index 70c9c27841..409ef93110 100644 --- a/app/Services/EDocument/Gateway/MutatorInterface.php +++ b/app/Services/EDocument/Gateway/MutatorInterface.php @@ -20,8 +20,14 @@ interface MutatorInterface public function setInvoice($invoice): self; + /** + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $p_invoice + */ public function setPeppol($p_invoice): self; + /** + * @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote + */ public function getPeppol(): mixed; public function setClientSettings($client_settings): self; diff --git a/app/Services/EDocument/Gateway/Qvalia/Mutator.php b/app/Services/EDocument/Gateway/Qvalia/Mutator.php index 0484615039..3d16fe22ae 100644 --- a/app/Services/EDocument/Gateway/Qvalia/Mutator.php +++ b/app/Services/EDocument/Gateway/Qvalia/Mutator.php @@ -17,7 +17,8 @@ use App\Services\EDocument\Gateway\MutatorInterface; class Mutator implements MutatorInterface { - private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice; + /** @var \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote */ + private \InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $p_invoice; private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_client_settings; @@ -38,12 +39,18 @@ class Mutator implements MutatorInterface return $this; } + /** + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $p_invoice + */ public function setPeppol($p_invoice): self { $this->p_invoice = $p_invoice; return $this; } + /** + * @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote + */ public function getPeppol(): mixed { return $this->p_invoice; diff --git a/app/Services/EDocument/Gateway/Storecove/Mutator.php b/app/Services/EDocument/Gateway/Storecove/Mutator.php index db5a69ce68..8ac34358e8 100644 --- a/app/Services/EDocument/Gateway/Storecove/Mutator.php +++ b/app/Services/EDocument/Gateway/Storecove/Mutator.php @@ -19,7 +19,8 @@ use App\Services\EDocument\Gateway\Storecove\StorecoveRouter; class Mutator implements MutatorInterface { - private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice; + /** @var \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote */ + private \InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $p_invoice; private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_client_settings; @@ -51,7 +52,7 @@ class Mutator implements MutatorInterface /** * setPeppol * - * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $p_invoice * @return self */ public function setPeppol($p_invoice): self @@ -63,7 +64,7 @@ class Mutator implements MutatorInterface /** * getPeppol * - * @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice + * @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote */ public function getPeppol(): mixed { diff --git a/app/Services/EDocument/Imports/Ubl2Pdf.php b/app/Services/EDocument/Imports/Ubl2Pdf.php index b861709796..fbb240b4f3 100644 --- a/app/Services/EDocument/Imports/Ubl2Pdf.php +++ b/app/Services/EDocument/Imports/Ubl2Pdf.php @@ -32,11 +32,15 @@ use App\Services\Template\TemplateService; class Ubl2Pdf extends AbstractService { + /** @var \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote */ + public \InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice; + /** * @throws \Throwable */ - public function __construct(public \InvoiceNinja\EInvoice\Models\Peppol\Invoice $invoice, public Company $company) + public function __construct(\InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice, public Company $company) { + $this->invoice = $invoice; } public function run() diff --git a/app/Services/EDocument/Imports/UblEDocument.php b/app/Services/EDocument/Imports/UblEDocument.php index 928ea65053..de2e58fd04 100644 --- a/app/Services/EDocument/Imports/UblEDocument.php +++ b/app/Services/EDocument/Imports/UblEDocument.php @@ -54,17 +54,15 @@ class UblEDocument extends AbstractService /** * extractInvoiceUbl * - * If the document. + * If the ', '', $xml); - // nlog($xml); $dom = new \DOMDocument(); $dom->loadXML($xml); @@ -73,28 +71,43 @@ class UblEDocument extends AbstractService // Register the namespaces $xpath->registerNamespace('sh', 'http://www.unece.org/cefact/namespaces/StandardBusinessDocumentHeader'); - $xpath->registerNamespace('ubl', 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'); + $xpath->registerNamespace('ubl-inv', 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'); + $xpath->registerNamespace('ubl-cn', 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2'); - // Search for Invoice with default namespace - $invoiceNodes = $xpath->query('//ubl:Invoice'); + // Try to find Invoice first + $invoiceNodes = $xpath->query('//ubl-inv:Invoice'); - if ($invoiceNodes === false || $invoiceNodes->length === 0) { - throw new \Exception('No Invoice tag found in XML'); + if ($invoiceNodes !== false && $invoiceNodes->length > 0) { + $invoiceNode = $invoiceNodes->item(0); + $newDom = new \DOMDocument(); + $newNode = $newDom->importNode($invoiceNode, true); + $newDom->appendChild($newNode); + return $newDom->saveXML($newDom->documentElement); } - $invoiceNode = $invoiceNodes->item(0); + // Try to find CreditNote + $creditNoteNodes = $xpath->query('//ubl-cn:CreditNote'); - // Create new document with just the Invoice - $newDom = new \DOMDocument(); - $newNode = $newDom->importNode($invoiceNode, true); - $newDom->appendChild($newNode); - - return $newDom->saveXML($newDom->documentElement); + if ($creditNoteNodes !== false && $creditNoteNodes->length > 0) { + $creditNoteNode = $creditNoteNodes->item(0); + $newDom = new \DOMDocument(); + $newNode = $newDom->importNode($creditNoteNode, true); + $newDom->appendChild($newNode); + return $newDom->saveXML($newDom->documentElement); + } + throw new \Exception('No Invoice or CreditNote tag found in XML'); } - private function buildAndSaveExpense(\InvoiceNinja\EInvoice\Models\Peppol\Invoice $invoice): Expense + /** + * Build and save expense from Peppol Invoice or CreditNote + * + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice + * @return Expense + */ + private function buildAndSaveExpense(\InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice): Expense { + $isCreditNote = $invoice instanceof \InvoiceNinja\EInvoice\Models\Peppol\CreditNote; $vendor = $this->findOrCreateVendor($invoice); @@ -124,7 +137,10 @@ class UblEDocument extends AbstractService return $means === false; })->implode("\n"); - $invoice_items = data_get($invoice, 'InvoiceLine', []); + // Handle both InvoiceLine and CreditNoteLine + $invoice_items = $isCreditNote + ? data_get($invoice, 'CreditNoteLine', []) + : data_get($invoice, 'InvoiceLine', []); $items = []; @@ -225,7 +241,10 @@ class UblEDocument extends AbstractService })?->id ?? (int) $this->company->settings->currency_id; } - private function findOrCreateVendor(\InvoiceNinja\EInvoice\Models\Peppol\Invoice $invoice): Vendor + /** + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice + */ + private function findOrCreateVendor(\InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice): Vendor { $asp = $invoice->AccountingSupplierParty; @@ -256,7 +275,10 @@ class UblEDocument extends AbstractService return $vendor ?? $this->newVendor($invoice); } - private function resolveSupplierName(\InvoiceNinja\EInvoice\Models\Peppol\Invoice $invoice): string + /** + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice + */ + private function resolveSupplierName(\InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice): string { if (data_get($invoice, 'AccountingSupplierParty.Party.PartyName', false)) { $party_name = data_get($invoice, 'AccountingSupplierParty.Party.PartyName', false); @@ -271,7 +293,10 @@ class UblEDocument extends AbstractService return ''; } - private function resolveVendorIdNumber(\InvoiceNinja\EInvoice\Models\Peppol\Invoice $invoice): string + /** + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice + */ + private function resolveVendorIdNumber(\InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice): string { $pts = data_get($invoice, 'AccountingSupplierParty.Party.PartyIdentification', false); @@ -280,7 +305,10 @@ class UblEDocument extends AbstractService } - private function resolveVendorVat(\InvoiceNinja\EInvoice\Models\Peppol\Invoice $invoice): string + /** + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice + */ + private function resolveVendorVat(\InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice): string { $pts = data_get($invoice, 'AccountingSupplierParty.Party.PartyTaxScheme', false); @@ -289,7 +317,10 @@ class UblEDocument extends AbstractService } - private function newVendor(\InvoiceNinja\EInvoice\Models\Peppol\Invoice $invoice): Vendor + /** + * @param \InvoiceNinja\EInvoice\Models\Peppol\Invoice|\InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice + */ + private function newVendor(\InvoiceNinja\EInvoice\Models\Peppol\Invoice | \InvoiceNinja\EInvoice\Models\Peppol\CreditNote $invoice): Vendor { $vendor = VendorFactory::create($this->company->id, $this->company->owner()->id); diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 7f0fa82c00..f8c277ba89 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -197,6 +197,22 @@ class Peppol extends AbstractService return false; } + /** + * Normalize amount for credit notes + * + * Credit notes must have positive values - the document type + * itself indicates it's a credit. This method ensures all + * amounts are positive when building a credit note. + * + * @param float|int|string $amount + * @return float + */ + private function normalizeAmount(float|int|string $amount): float + { + $value = (float) $amount; + return $this->isCreditNote ? abs($value) : $value; + } + /** * Entry point for building document * @@ -217,7 +233,13 @@ class Peppol extends AbstractService $id->value = $this->profileID; $this->p_invoice->ProfileID = $id; - $this->p_invoice->ID = $this->invoice->number; + // Set ID - for CreditNote it expects an ID object + $docId = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID(); + $docId->value = $this->invoice->number; + $this->p_invoice->ID = $docId; + + // $this->p_invoice->ID = $this->invoice->number; + $this->p_invoice->IssueDate = new \DateTime($this->invoice->date); if ($this->invoice->due_date) { @@ -556,14 +578,14 @@ class Peppol extends AbstractService $allowanceCharge->ChargeIndicator = 'false'; // false = discount $allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount(); $allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code; - $allowanceCharge->Amount->amount = number_format($this->calc->getTotalDiscount(), 2, '.', ''); + $allowanceCharge->Amount->amount = number_format($this->normalizeAmount($this->calc->getTotalDiscount()), 2, '.', ''); // Add percentage if available if ($this->invoice->discount > 0 && !$this->invoice->is_amount_discount) { $allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount(); $allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code; - $allowanceCharge->BaseAmount->amount = number_format($this->calc->getSubtotalWithSurcharges(), 2, '.', ''); + $allowanceCharge->BaseAmount->amount = number_format($this->normalizeAmount($this->calc->getSubtotalWithSurcharges()), 2, '.', ''); $mfn = new \InvoiceNinja\EInvoice\Models\Peppol\NumericType\MultiplierFactorNumeric(); $mfn->value = number_format(round(($this->invoice->discount), 2), 2, '.', ''); // Format to always show 2 decimals @@ -675,10 +697,10 @@ class Peppol extends AbstractService { $taxable = $this->getTaxable(); - // For credit notes, ensure all amounts are positive - $amount = $this->isCreditNote ? abs($this->invoice->amount) : $this->invoice->amount; - $totalTaxes = $this->isCreditNote ? abs($this->invoice->total_taxes) : $this->invoice->total_taxes; - $subtotal = $this->isCreditNote ? abs($this->calc->getSubtotal()) : $this->calc->getSubtotal(); + // Normalize amounts for credit notes (ensure positive values) + $amount = $this->normalizeAmount($this->invoice->amount); + $totalTaxes = $this->normalizeAmount($this->invoice->total_taxes); + $subtotal = $this->normalizeAmount($this->calc->getSubtotal()); $lmt = new LegalMonetaryTotal(); @@ -705,12 +727,12 @@ class Peppol extends AbstractService $am = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\AllowanceTotalAmount(); $am->currencyID = $this->invoice->client->currency()->code; - $am->amount = number_format(abs($this->calc->getTotalDiscount()), 2, '.', ''); + $am->amount = number_format($this->normalizeAmount($this->calc->getTotalDiscount()), 2, '.', ''); $lmt->AllowanceTotalAmount = $am; $cta = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\ChargeTotalAmount(); $cta->currencyID = $this->invoice->client->currency()->code; - $cta->amount = number_format($this->calc->getTotalSurcharges(), 2, '.', ''); + $cta->amount = number_format($this->normalizeAmount($this->calc->getTotalSurcharges()), 2, '.', ''); $lmt->ChargeTotalAmount = $cta; return $lmt; @@ -1094,7 +1116,7 @@ class Peppol extends AbstractService // Use CreditedQuantity instead of InvoicedQuantity $cq = new \InvoiceNinja\EInvoice\Models\Peppol\QuantityType\CreditedQuantity(); - $cq->amount = abs($item->quantity); // Ensure positive quantity + $cq->amount = (string) $this->isCreditNote ? abs($item->quantity) : $item->quantity; // Ensure positive quantity $cq->unitCode = $item->unit_code ?? 'C62'; $line->CreditedQuantity = $cq; @@ -1103,7 +1125,7 @@ class Peppol extends AbstractService $lineTotal = $this->invoice->uses_inclusive_taxes ? round($item->line_total - $this->calcInclusiveLineTax($item->tax_rate1, $item->line_total), 2) : round($item->line_total, 2); - $lea->amount = abs($lineTotal); // Ensure positive amount + $lea->amount = (string) abs($lineTotal); // Ensure positive amount $line->LineExtensionAmount = $lea; $line->Item = $_item; @@ -1508,7 +1530,7 @@ class Peppol extends AbstractService $total += $this->invoice->custom_surcharge4; } - return round($total, 2); + return round($this->normalizeAmount($total), 2); } ///////////////// Helper Methods ///////////////////////// @@ -1521,10 +1543,22 @@ class Peppol extends AbstractService */ public function setInvoiceDefaults(): self { + // Properties that are Invoice-specific and should not be assigned to CreditNote + $invoiceOnlyProps = ['InvoiceTypeCode', 'InvoiceLine', 'InvoicePeriod']; + // Properties that are CreditNote-specific and should not be assigned to Invoice + $creditNoteOnlyProps = ['CreditNoteTypeCode', 'CreditNoteLine']; // Stub new invoice with company settings. if ($this->_company_settings) { foreach (get_object_vars($this->_company_settings) as $prop => $value) { + // Skip Invoice-specific properties when building CreditNote + if ($this->isCreditNote && in_array($prop, $invoiceOnlyProps)) { + continue; + } + // Skip CreditNote-specific properties when building Invoice + if (!$this->isCreditNote && in_array($prop, $creditNoteOnlyProps)) { + continue; + } $this->p_invoice->{$prop} = $value; } } @@ -1532,12 +1566,28 @@ class Peppol extends AbstractService // Overwrite with any client level settings if ($this->_client_settings) { foreach (get_object_vars($this->_client_settings) as $prop => $value) { + // Skip Invoice-specific properties when building CreditNote + if ($this->isCreditNote && in_array($prop, $invoiceOnlyProps)) { + continue; + } + // Skip CreditNote-specific properties when building Invoice + if (!$this->isCreditNote && in_array($prop, $creditNoteOnlyProps)) { + continue; + } $this->p_invoice->{$prop} = $value; } } - if (isset($this->invoice->e_invoice->Invoice)) { - foreach (get_object_vars($this->invoice->e_invoice->Invoice) as $prop => $value) { + // Handle existing e_invoice data + $existingData = null; + if ($this->isCreditNote && isset($this->invoice->e_invoice->CreditNote)) { + $existingData = $this->invoice->e_invoice->CreditNote; + } elseif (!$this->isCreditNote && isset($this->invoice->e_invoice->Invoice)) { + $existingData = $this->invoice->e_invoice->Invoice; + } + + if ($existingData) { + foreach (get_object_vars($existingData) as $prop => $value) { $this->p_invoice->{$prop} = $value; } } @@ -1645,7 +1695,7 @@ class Peppol extends AbstractService // Required: TaxableAmount (BT-116) $taxable_amount = new TaxableAmount(); $taxable_amount->currencyID = $this->invoice->client->currency()->code; - $taxable_amount->amount = (string)round($this->invoice->amount, 2); + $taxable_amount->amount = (string)round($this->normalizeAmount($this->invoice->amount), 2); $tax_subtotal->TaxableAmount = $taxable_amount; @@ -1687,7 +1737,7 @@ class Peppol extends AbstractService $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; // $tax_amount->amount = (string)$grouped_tax['total']; - $tax_amount->amount = (string)round($this->invoice->total_taxes, 2); + $tax_amount->amount = (string)round($this->normalizeAmount($this->invoice->total_taxes), 2); $tax_total->TaxAmount = $tax_amount; // Required: TaxSubtotal (BG-23) @@ -1698,9 +1748,9 @@ class Peppol extends AbstractService $taxable_amount->currencyID = $this->invoice->client->currency()->code; if (floatval($grouped_tax['total']) === 0.0) { - $taxable_amount->amount = (string)round($this->invoice->amount, 2); + $taxable_amount->amount = (string)round($this->normalizeAmount($this->invoice->amount), 2); } else { - $taxable_amount->amount = (string)round($grouped_tax['base_amount'], 2); + $taxable_amount->amount = (string)round($this->normalizeAmount($grouped_tax['base_amount']), 2); } $tax_subtotal->TaxableAmount = $taxable_amount; @@ -1708,7 +1758,7 @@ class Peppol extends AbstractService $subtotal_tax_amount = new TaxAmount(); $subtotal_tax_amount->currencyID = $this->invoice->client->currency()->code; - $subtotal_tax_amount->amount = (string)round($grouped_tax['total'], 2); + $subtotal_tax_amount->amount = (string)round($this->normalizeAmount($grouped_tax['total']), 2); $tax_subtotal->TaxAmount = $subtotal_tax_amount; diff --git a/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php b/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php index 10f329da28..66a8c65da6 100644 --- a/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php +++ b/app/Services/EDocument/Standards/Validation/Peppol/EntityLevel.php @@ -130,7 +130,7 @@ class EntityLevel implements EntityLevelInterface return ['passes' => true]; } - public function checkInvoice(Invoice $invoice): array + public function checkInvoice(Invoice | Credit $invoice): array { $this->init($invoice->client->locale()); diff --git a/app/Services/EDocument/Standards/Validation/Peppol/Stylesheets/UBL2.1/UBL-CreditNote-2.1.xsd b/app/Services/EDocument/Standards/Validation/Peppol/Stylesheets/UBL2.1/UBL-CreditNote-2.1.xsd new file mode 100644 index 0000000000..499ee49a35 --- /dev/null +++ b/app/Services/EDocument/Standards/Validation/Peppol/Stylesheets/UBL2.1/UBL-CreditNote-2.1.xsd @@ -0,0 +1,951 @@ + + + + + + + + + + + This element MUST be conveyed as the root element in any instance document based on this Schema expression + + + + + + + + + ABIE + Credit Note. Details + A document used to specify credits due to the Debtor from the Creditor. + Credit Note + + + + + + + A container for all extensions present in the document. + + + + + + + BBIE + Credit Note. UBL Version Identifier. Identifier + Identifies the earliest version of the UBL 2 schema for this document type that defines all of the elements that might be encountered in the current instance. + 0..1 + Credit Note + UBL Version Identifier + Identifier + Identifier. Type + 2.0.5 + + + + + + + + + BBIE + Credit Note. Customization Identifier. Identifier + Identifies a user-defined customization of UBL for a specific use. + 0..1 + Credit Note + Customization Identifier + Identifier + Identifier. Type + NES + + + + + + + + + BBIE + Credit Note. Profile Identifier. Identifier + Identifies a user-defined profile of the customization of UBL being used. + 0..1 + Credit Note + Profile Identifier + Identifier + Identifier. Type + BasicProcurementProcess + + + + + + + + + BBIE + Credit Note. Profile Execution Identifier. Identifier + Identifies an instance of executing a profile, to associate all transactions in a collaboration. + 0..1 + Credit Note + Profile Execution Identifier + Identifier + Identifier. Type + BPP-1001 + + + + + + + + + BBIE + Credit Note. Identifier + An identifier for this document, assigned by the sender. + 1 + Credit Note + Identifier + Identifier + Identifier. Type + + + + + + + + + BBIE + Credit Note. Copy_ Indicator. Indicator + Indicates whether this document is a copy (true) or not (false). + 0..1 + Credit Note + Copy + Indicator + Indicator + Indicator. Type + + + + + + + + + BBIE + Credit Note. UUID. Identifier + A universally unique identifier for an instance of this document. + 0..1 + Credit Note + UUID + Identifier + Identifier. Type + + + + + + + + + BBIE + Credit Note. Issue Date. Date + The date, assigned by the sender, on which this document was issued. + 1 + Credit Note + Issue Date + Date + Date. Type + + + + + + + + + BBIE + Credit Note. Issue Time. Time + The time, assigned by the sender, at which this document was issued. + 0..1 + Credit Note + Issue Time + Time + Time. Type + + + + + + + + + BBIE + Credit Note. Tax Point Date. Date + The date of the Credit Note, used to indicate the point at which tax becomes applicable. + 0..1 + Credit Note + Tax Point Date + Date + Date. Type + + + + + + + + + BBIE + Credit Note. Credit Note Type Code. Code + A code signifying the type of the Credit Note. + 0..1 + Credit Note + Credit Note Type Code + Code + Code. Type + + + + + + + + + BBIE + Credit Note. Note. Text + Free-form text pertinent to this document, conveying information that is not contained explicitly in other structures. + 0..n + Credit Note + Note + Text + Text. Type + + + + + + + + + BBIE + Credit Note. Document_ Currency Code. Code + A code signifying the default currency for this document. + 0..1 + Credit Note + Document + Currency Code + Code + Currency + Currency_ Code. Type + + + + + + + + + BBIE + Credit Note. Tax_ Currency Code. Code + A code signifying the currency used for tax amounts in the Credit Note. + 0..1 + Credit Note + Tax + Currency Code + Code + Currency + Currency_ Code. Type + + + + + + + + + BBIE + Credit Note. Pricing_ Currency Code. Code + A code signifying the currency used for prices in the Credit Note. + 0..1 + Credit Note + Pricing + Currency Code + Code + Currency + Currency_ Code. Type + + + + + + + + + BBIE + Credit Note. Payment_ Currency Code. Code + A code signifying the currency used for payment in the Credit Note. + 0..1 + Credit Note + Payment + Currency Code + Code + Currency + Currency_ Code. Type + + + + + + + + + BBIE + Credit Note. Payment Alternative_ Currency Code. Code + A code signifying the alternative currency used for payment in the Credit Note. + 0..1 + Credit Note + Payment Alternative + Currency Code + Code + Currency + Currency_ Code. Type + + + + + + + + + BBIE + Credit Note. Accounting Cost Code. Code + The buyer's accounting code, applied to the Credit Note as a whole. + 0..1 + Credit Note + Accounting Cost Code + Code + Code. Type + + + + + + + + + BBIE + Credit Note. Accounting Cost. Text + The buyer's accounting code, applied to the Credit Note as a whole, expressed as text. + 0..1 + Credit Note + Accounting Cost + Text + Text. Type + + + + + + + + + BBIE + Credit Note. Line Count. Numeric + The number of Credit Note Lines in the document. + 0..1 + Credit Note + Line Count + Numeric + Numeric. Type + + + + + + + + + BBIE + Credit Note. Buyer_ Reference. Text + A reference provided by the buyer used for internal routing of the document. + 0..1 + Credit Note + Buyer + Reference + Text + Text. Type + + + + + + + + + ASBIE + Credit Note. Invoice_ Period. Period + Associates the Credit Note with Invoicing Periods rather than with a specific Invoice. + 0..n + Credit Note + Invoice + Period + Period + Period + + + + + + + + + ASBIE + Credit Note. Discrepancy_ Response. Response + A reason for the Credit Note as a whole. + 0..n + Credit Note + Discrepancy + Response + Response + Response + + + + + + + + + ASBIE + Credit Note. Order Reference + The Order associated with this Credit Note. + 0..1 + Credit Note + Order Reference + Order Reference + Order Reference + + + + + + + + + ASBIE + Credit Note. Billing Reference + A reference to a billing document associated with this document. + 0..n + Credit Note + Billing Reference + Billing Reference + Billing Reference + + + + + + + + + ASBIE + Credit Note. Despatch_ Document Reference. Document Reference + A reference to a Despatch Advice associated with this document. + 0..n + Credit Note + Despatch + Document Reference + Document Reference + Document Reference + + + + + + + + + ASBIE + Credit Note. Receipt_ Document Reference. Document Reference + A reference to a Receipt Advice associated with this document. + 0..n + Credit Note + Receipt + Document Reference + Document Reference + Document Reference + + + + + + + + + ASBIE + Credit Note. Contract_ Document Reference. Document Reference + A reference to a contract associated with this document. + 0..n + Credit Note + Contract + Document Reference + Document Reference + Document Reference + + + + + + + + + ASBIE + Credit Note. Additional_ Document Reference. Document Reference + A reference to an additional document associated with this document. + 0..n + Credit Note + Additional + Document Reference + Document Reference + Document Reference + + + + + + + + + ASBIE + Credit Note. Statement_ Document Reference. Document Reference + A reference to a Statement associated with this document. + 0..n + Credit Note + Statement + Document Reference + Document Reference + Document Reference + + + + + + + + + ASBIE + Credit Note. Originator_ Document Reference. Document Reference + A reference to an originator document associated with this document. + 0..n + Credit Note + Originator + Document Reference + Document Reference + Document Reference + + + + + + + + + ASBIE + Credit Note. Signature + A signature applied to this document. + 0..n + Credit Note + Signature + Signature + Signature + + + + + + + + + ASBIE + Credit Note. Accounting_ Supplier Party. Supplier Party + The accounting supplier party. + 1 + Credit Note + Accounting + Supplier Party + Supplier Party + Supplier Party + + + + + + + + + ASBIE + Credit Note. Accounting_ Customer Party. Customer Party + The accounting customer party. + 1 + Credit Note + Accounting + Customer Party + Customer Party + Customer Party + + + + + + + + + ASBIE + Credit Note. Payee_ Party. Party + The payee. + 0..1 + Credit Note + Payee + Party + Party + Party + + + + + + + + + ASBIE + Credit Note. Buyer_ Customer Party. Customer Party + The buyer. + 0..1 + Credit Note + Buyer + Customer Party + Customer Party + Customer Party + + + + + + + + + ASBIE + Credit Note. Seller_ Supplier Party. Supplier Party + The seller. + 0..1 + Credit Note + Seller + Supplier Party + Supplier Party + Supplier Party + + + + + + + + + ASBIE + Credit Note. Tax Representative_ Party. Party + The tax representative. + 0..1 + Credit Note + Tax Representative + Party + Party + Party + + + + + + + + + ASBIE + Credit Note. Delivery + A delivery associated with this document. + 0..n + Credit Note + Delivery + Delivery + Delivery + + + + + + + + + ASBIE + Credit Note. Delivery Terms + A set of delivery terms associated with this document. + 0..n + Credit Note + Delivery Terms + Delivery Terms + Delivery Terms + + + + + + + + + ASBIE + Credit Note. Payment Means + Expected means of payment. + 0..n + Credit Note + Payment Means + Payment Means + Payment Means + + + + + + + + + ASBIE + Credit Note. Payment Terms + A set of payment terms associated with this document. + 0..n + Credit Note + Payment Terms + Payment Terms + Payment Terms + + + + + + + + + ASBIE + Credit Note. Tax_ Exchange Rate. Exchange Rate + The exchange rate between the document currency and the tax currency. + 0..1 + Credit Note + Tax + Exchange Rate + Exchange Rate + Exchange Rate + + + + + + + + + ASBIE + Credit Note. Pricing_ Exchange Rate. Exchange Rate + The exchange rate between the document currency and the pricing currency. + 0..1 + Credit Note + Pricing + Exchange Rate + Exchange Rate + Exchange Rate + + + + + + + + + ASBIE + Credit Note. Payment_ Exchange Rate. Exchange Rate + The exchange rate between the document currency and the payment currency. + 0..1 + Credit Note + Payment + Exchange Rate + Exchange Rate + Exchange Rate + + + + + + + + + ASBIE + Credit Note. Payment Alternative_ Exchange Rate. Exchange Rate + The exchange rate between the document currency and the payment alternative currency. + 0..1 + Credit Note + Payment Alternative + Exchange Rate + Exchange Rate + Exchange Rate + + + + + + + + + ASBIE + Credit Note. Allowance Charge + A discount or charge that applies to a price component. + 0..n + Credit Note + Allowance Charge + Allowance Charge + Allowance Charge + + + + + + + + + ASBIE + Credit Note. Tax Total + The total amount of a specific type of tax. + 0..n + Credit Note + Tax Total + Tax Total + Tax Total + + + + + + + + + ASBIE + Credit Note. Legal_ Monetary Total. Monetary Total + The total amount payable on the Credit Note, including allowances, charges, and taxes. + 1 + Credit Note + Legal + Monetary Total + Monetary Total + Monetary Total + + + + + + + + + ASBIE + Credit Note. Credit Note Line + A Credit Note line. + 1..n + Credit Note + Credit Note Line + Credit Note Line + Credit Note Line + + + + + + + + \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Validation/XsltDocumentValidator.php b/app/Services/EDocument/Standards/Validation/XsltDocumentValidator.php index e8777b8ef1..5bf55acdc4 100644 --- a/app/Services/EDocument/Standards/Validation/XsltDocumentValidator.php +++ b/app/Services/EDocument/Standards/Validation/XsltDocumentValidator.php @@ -21,16 +21,53 @@ class XsltDocumentValidator private string $ubl_xsd = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/UBL2.1/UBL-Invoice-2.1.xsd'; - private string $peppol_stylesheet = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/generic_stylesheet.xslt'; - // private string $peppol_stylesheet = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/xrechung.xslt'; + private string $ubl_credit_note_xsd = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/UBL2.1/UBL-CreditNote-2.1.xsd'; - // private string $peppol_stylesheetx = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/ubl_stylesheet.xslt'; - // private string $peppol_stylesheet = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/ci_to_ubl_stylesheet.xslt'; + private string $peppol_stylesheet = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/generic_stylesheet.xslt'; public array $errors = []; + private bool $isCreditNote = false; + public function __construct(public string $xml_document) { + $this->isCreditNote = $this->detectDocumentType() === 'creditnote'; + } + + /** + * Detect the document type from XML content + * + * @return string 'invoice' or 'creditnote' + */ + private function detectDocumentType(): string + { + // Check for CreditNote root element (with or without namespace prefix) + if (preg_match('/]*>/i', $this->xml_document) || + preg_match('/<[a-z0-9]+:CreditNote[^>]*>/i', $this->xml_document)) { + return 'creditnote'; + } + + return 'invoice'; + } + + /** + * Get the appropriate XSD path based on document type + * + * @return string + */ + private function getXsdPath(): string + { + return $this->isCreditNote ? $this->ubl_credit_note_xsd : $this->ubl_xsd; + } + + /** + * Check if the document is a Credit Note + * + * @return bool + */ + public function isCreditNote(): bool + { + return $this->isCreditNote; } /** @@ -87,17 +124,15 @@ class XsltDocumentValidator private function validateXsd(): self { - libxml_use_internal_errors(true); $xml = new \DOMDocument(); $xml->loadXML($this->xml_document); - if (!$xml->schemaValidate(app_path($this->ubl_xsd))) { + if (!$xml->schemaValidate(app_path($this->getXsdPath()))) { $errors = libxml_get_errors(); libxml_clear_errors(); - $errorMessages = []; foreach ($errors as $error) { $this->errors['xsd'][] = sprintf( 'Line %d: %s', @@ -105,7 +140,6 @@ class XsltDocumentValidator trim($error->message) ); } - } return $this; @@ -154,6 +188,7 @@ class XsltDocumentValidator $xml_doc = $processor->parseXmlFromString($xml); // Compile and apply stylesheet + /** @var \Saxon\XsltExecutable $stylesheet */ $stylesheet = $xslt->compileFromFile(app_path($this->peppol_stylesheet)); //@phpstan-ignore-line // Transform to HTML diff --git a/app/Services/Report/ARDetailReport.php b/app/Services/Report/ARDetailReport.php index 4ec81344bc..fa42b9d9af 100644 --- a/app/Services/Report/ARDetailReport.php +++ b/app/Services/Report/ARDetailReport.php @@ -32,7 +32,8 @@ class ARDetailReport extends BaseExport public Writer $csv; - public string $date_key = 'created_at'; + // 2026-01-16: Changed from created_at to date to match the invoice date + public string $date_key = 'date'; private string $template = '/views/templates/reports/ar_detail_report.html'; diff --git a/app/Services/Report/ARSummaryReport.php b/app/Services/Report/ARSummaryReport.php index b65fc98815..dca8dc7b65 100644 --- a/app/Services/Report/ARSummaryReport.php +++ b/app/Services/Report/ARSummaryReport.php @@ -33,7 +33,8 @@ class ARSummaryReport extends BaseExport public Writer $csv; - public string $date_key = 'created_at'; + // 2026-01-16: Changed from created_at to date to match the invoice date + public string $date_key = 'date'; public Client $client; From 59eca1abc68e6aa65c49e2ebfc726addbc72c2ae Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 16 Jan 2026 19:25:55 +1100 Subject: [PATCH 098/177] Expose credit backup prop --- app/Jobs/PostMark/ProcessPostmarkWebhook.php | 4 ++-- app/Models/Credit.php | 5 +++-- app/Transformers/CreditTransformer.php | 2 +- app/Utils/HtmlEngine.php | 4 ++++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php index 4b69386933..816b7f9c9a 100644 --- a/app/Jobs/PostMark/ProcessPostmarkWebhook.php +++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php @@ -166,7 +166,7 @@ class ProcessPostmarkWebhook implements ShouldQueue private function processOpen() { - $this->invitation->opened_date = now()->setTimezone($this->invitation->company->timezone()->name); + $this->invitation->opened_date = now(); $this->invitation->saveQuietly(); $data = array_merge($this->request, ['history' => $this->fetchMessage()]); @@ -422,7 +422,7 @@ class ProcessPostmarkWebhook implements ShouldQueue 'delivery_message' => $event->Details->DeliveryMessage ?? $event->Details->Summary ?? '', 'server' => $event->Details->DestinationServer ?? '', 'server_ip' => $event->Details->DestinationIP ?? '', - 'date' => \Carbon\Carbon::parse($event->ReceivedAt)->format('Y-m-d H:i:s') ?? '', + 'date' => \Carbon\Carbon::parse($event->ReceivedAt)->setTimezone($this->invitation->company->timezone()->name)->format('Y-m-d H:i:s') ?? '', ]; })->toArray(); diff --git a/app/Models/Credit.php b/app/Models/Credit.php index 6890d4b296..5a902bfdd8 100644 --- a/app/Models/Credit.php +++ b/app/Models/Credit.php @@ -12,6 +12,7 @@ namespace App\Models; +use App\DataMapper\InvoiceBackup; use App\Utils\Ninja; use App\Utils\Number; use Elastic\ScoutDriverPlus\Searchable; @@ -54,7 +55,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property string|null $due_date * @property bool $is_deleted * @property array|null $line_items - * @property object|null $backup + * @property InvoiceBackup $backup * @property string|null $footer * @property string|null $public_notes * @property string|null $private_notes @@ -200,7 +201,7 @@ class Credit extends BaseModel protected $casts = [ 'line_items' => 'object', - 'backup' => 'object', + 'backup' => InvoiceBackup::class, 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', diff --git a/app/Transformers/CreditTransformer.php b/app/Transformers/CreditTransformer.php index e97eb27d55..c824b99f01 100644 --- a/app/Transformers/CreditTransformer.php +++ b/app/Transformers/CreditTransformer.php @@ -148,7 +148,7 @@ class CreditTransformer extends EntityTransformer 'tax_info' => $credit->tax_data ?: new \stdClass(), 'e_invoice' => $credit->e_invoice ?: new \stdClass(), 'location_id' => $this->encodePrimaryKey($credit->location_id), - + 'backup' => $credit->backup ]; } } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 94bfa1a0f2..ae72006081 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -321,6 +321,10 @@ class HtmlEngine $data['$quote.project'] = &$data['$project.name']; $data['$invoice.vendor'] = ['value' => $this->entity->vendor?->present()->name() ?: '', 'label' => ctrans('texts.vendor_name')]; + + $data['$payment_qrcode'] = ['value' => '', 'label' => ctrans('texts.pay_now')]; + $data['$payment_qrcode_raw'] = ['value' => '', 'label' => ctrans('texts.pay_now')]; + } if ($this->entity_string == 'credit') { From 48e78d6160b1a6b001d3a57a9a8b208c51e48d0b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 16 Jan 2026 21:28:52 +1100 Subject: [PATCH 099/177] Fixes for try/catch logic with Postmark webhook sync --- app/Jobs/Ninja/MailWebhookSync.php | 8 +++++++- .../EDocument/Gateway/Storecove/Storecove.php | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/Jobs/Ninja/MailWebhookSync.php b/app/Jobs/Ninja/MailWebhookSync.php index 8884641009..8e963f4f7b 100644 --- a/app/Jobs/Ninja/MailWebhookSync.php +++ b/app/Jobs/Ninja/MailWebhookSync.php @@ -131,7 +131,13 @@ class MailWebhookSync implements ShouldQueue } catch (\Throwable $th) { $token = config('services.postmark-outlook.token'); $postmark = new \Postmark\PostmarkClient($token); - $messageDetail = $postmark->getOutboundMessageDetails($invite->message_id); + + try { + $messageDetail = $postmark->getOutboundMessageDetails($invite->message_id); + } catch (\Throwable $th){ + + } + } try { diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php index 672cdc6fab..882a6d1433 100644 --- a/app/Services/EDocument/Gateway/Storecove/Storecove.php +++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php @@ -326,6 +326,24 @@ class Storecove ]; } + + public function removePeppolIdentifier(int $legal_entity_id, string $identifier, string $scheme, string $superscheme = "iso6523-actorid-upis"): array|\Illuminate\Http\Client\Response + { + + $uri = "/legal_entities/{$legal_entity_id}/peppol_identifiers/{$superscheme}/{$scheme}/{$identifier}"; + + $r = $this->httpClient($uri, (HttpVerb::DELETE)->value, []); + + if ($r->successful()) { + $data = $r->json(); + + return $data; + } + + return $r; + + } + /** * CreateLegalEntity * From dc686faca93eb604ab6ad84558b20ded085ea6d4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 16 Jan 2026 21:31:09 +1100 Subject: [PATCH 100/177] Fixes for missing backups --- app/Http/Controllers/ActivityController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Http/Controllers/ActivityController.php b/app/Http/Controllers/ActivityController.php index a67f818d68..d689b97039 100644 --- a/app/Http/Controllers/ActivityController.php +++ b/app/Http/Controllers/ActivityController.php @@ -132,6 +132,11 @@ class ActivityController extends BaseController $backup = $activity->backup; $html_backup = ''; + + if (!$activity->backup) { + return response()->json(['message' => ctrans('texts.no_backup_exists'), 'errors' => new stdClass()], 404); + } + $file = $backup->getFile(); $html_backup = $file; From f158004907b4d108fd38114be1c90895f2e1423d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 16 Jan 2026 21:31:55 +1100 Subject: [PATCH 101/177] Fixes for types --- app/Services/EDocument/Standards/Peppol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index f8c277ba89..881a6d3feb 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -706,7 +706,7 @@ class Peppol extends AbstractService $lea = new LineExtensionAmount(); $lea->currencyID = $this->invoice->client->currency()->code; - $lea->amount = $this->invoice->uses_inclusive_taxes ? round($amount - $totalTaxes, 2) : $subtotal; + $lea->amount = $this->invoice->uses_inclusive_taxes ? (string) round($amount - $totalTaxes, 2) : (string) $subtotal; $lmt->LineExtensionAmount = $lea; $tea = new TaxExclusiveAmount(); From 0a9fed42aa370da4ef0b3aea0a483040813c4121 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 17 Jan 2026 19:05:26 +1100 Subject: [PATCH 102/177] Fixes for verifactu --- .../EDocument/Standards/Verifactu/Models/IDFactura.php | 4 ++-- app/Services/Invoice/InvoiceService.php | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/Services/EDocument/Standards/Verifactu/Models/IDFactura.php b/app/Services/EDocument/Standards/Verifactu/Models/IDFactura.php index 4757be6b6c..1a7df0f9e5 100644 --- a/app/Services/EDocument/Standards/Verifactu/Models/IDFactura.php +++ b/app/Services/EDocument/Standards/Verifactu/Models/IDFactura.php @@ -35,9 +35,9 @@ class IDFactura extends BaseXmlModel return $this->numSerieFactura; } - public function setNumSerieFactura(string $numSerieFactura): self + public function setNumSerieFactura(?string $numSerieFactura = ''): self { - $this->numSerieFactura = $numSerieFactura; + $this->numSerieFactura = $numSerieFactura ?? '&'; return $this; } diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index d4d13c664b..2643647f89 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -724,9 +724,14 @@ class InvoiceService if ($new_model && $this->invoice->amount >= 0) { $this->invoice->backup->document_type = 'F1'; $this->invoice->backup->adjustable_amount = (new \App\Services\EDocument\Standards\Verifactu($this->invoice))->run()->registro_alta->calc->getTotal(); - $this->invoice->backup->parent_invoice_number = $this->invoice->number; + $this->invoice->backup->parent_invoice_number = $this->invoice->number ?? '&'; $this->invoice->saveQuietly(); - } elseif (isset($invoice_array['modified_invoice_id'])) { + } + elseif($this->invoice->backup->parent_invoice_number == '&') { // ensure we ALWAYS have a parent invoice number - handles cases where the invoice number is only set when SENT not when SAVED. + $this->invoice->backup->parent_invoice_number = $this->invoice->number ?? '&'; + $this->invoice->saveQuietly(); + } + elseif (isset($invoice_array['modified_invoice_id'])) { $document_type = 'R2'; // <- Default to R2 type /** Was it a partial or FULL rectification? */ From ba7b9040811004e81db26b446644360ffb3d31b3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 18 Jan 2026 09:20:31 +1100 Subject: [PATCH 103/177] Updated translations --- lang/en/texts.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lang/en/texts.php b/lang/en/texts.php index a6e03ee7de..0729f17ebd 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5689,6 +5689,7 @@ $lang = array( 'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?', 'peppol_sending_failed' => 'E-Invoice sending failed!', 'peppol_sending_success' => 'E-Invoice sent successfully!', + 'auto_generate' => 'Auto Generate', ); return $lang; From d659337863cd2495754dfa1d861e12e0aba86708 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 18 Jan 2026 11:22:26 +1100 Subject: [PATCH 104/177] Send E Doc --- app/Models/BaseModel.php | 8 ++++---- app/Services/EDocument/Jobs/SendEDocument.php | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 7334c6158b..7fab98f2dc 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -314,13 +314,13 @@ class BaseModel extends Model } // special catch here for einvoicing eventing - if ($event_id == Webhook::EVENT_SENT_INVOICE && ($this instanceof Invoice) && $this->backup->guid == "") { + if (in_array($event_id, [Webhook::EVENT_SENT_INVOICE, Webhook::EVENT_SENT_CREDIT]) && ($this instanceof Invoice || $this instanceof Credit) && $this->backup->guid == "") { if($this->client->peppolSendingEnabled()) { \App\Services\EDocument\Jobs\SendEDocument::dispatch(get_class($this), $this->id, $this->company->db); } - elseif($this->company->verifactuEnabled()) { - $this->service()->sendVerifactu(); - } + } + elseif(in_array($event_id, [Webhook::EVENT_SENT_INVOICE]) && $this->company->verifactuEnabled() && ($this instanceof Invoice) && $this->backup->guid == "") { + $this->service()->sendVerifactu(); } } diff --git a/app/Services/EDocument/Jobs/SendEDocument.php b/app/Services/EDocument/Jobs/SendEDocument.php index 4e4dc318a8..09fc67e48f 100644 --- a/app/Services/EDocument/Jobs/SendEDocument.php +++ b/app/Services/EDocument/Jobs/SendEDocument.php @@ -15,6 +15,7 @@ namespace App\Services\EDocument\Jobs; use Mail; use App\Utils\Ninja; use App\Models\Invoice; +use App\Models\Credit; use App\Models\Activity; use App\Models\SystemLog; use App\Libraries\MultiDB; @@ -100,7 +101,7 @@ class SendEDocument implements ShouldQueue ]; //Self Hosted Sending Code Path - if (Ninja::isSelfHost() && ($model instanceof Invoice) && $model->company->peppolSendingEnabled()) { + if (Ninja::isSelfHost() && ($model instanceof Invoice || $model instanceof Credit) && $model->company->peppolSendingEnabled()) { $r = Http::withHeaders([...$this->getHeaders(), 'X-EInvoice-Token' => $model->company->account->e_invoicing_token]) ->post(config('ninja.hosted_ninja_url')."/api/einvoice/submission", $payload); @@ -161,7 +162,7 @@ class SendEDocument implements ShouldQueue } //Hosted Sending Code Path. - if (($model instanceof Invoice) && $model->company->peppolSendingEnabled()) { + if (($model instanceof Invoice || $model instanceof Credit) && $model->company->peppolSendingEnabled()) { if ($model->company->account->e_invoice_quota <= config('ninja.e_invoice_quota_warning')) { $key = "e_invoice_quota_low_{$model->company->account->key}"; @@ -224,16 +225,14 @@ class SendEDocument implements ShouldQueue $activity->company_id = $model->company_id; $activity->account_id = $model->company->account_id; $activity->activity_type_id = $activity_id; - $activity->invoice_id = $model->id; + $activity->invoice_id = ($model instanceof Invoice) ? $model->id : null; + $activity->credit_id = ($model instanceof Credit) ? $model->id : null; $activity->notes = str_replace('"', '', $notes); $activity->is_system = true; $activity->save(); if ($activity_id == Activity::EINVOICE_DELIVERY_SUCCESS) { - - // $backup = ($model->backup && is_object($model->backup)) ? $model->backup : new \stdClass(); - // $backup->guid = str_replace('"', '', $notes); $model->backup->guid = str_replace('"', '', $notes); $model->saveQuietly(); From 0349a5a2ae0945e6c3615cc697aae118377c2faf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 18 Jan 2026 13:15:41 +1100 Subject: [PATCH 105/177] Working on dedicated credits for PEPPOL --- app/Http/Requests/Credit/UpdateCreditRequest.php | 2 ++ .../EInvoice/UpdateEInvoiceConfiguration.php | 3 ++- app/Services/EDocument/Standards/Peppol.php | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index b22a9e9615..33c9e6265d 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -81,6 +81,8 @@ class UpdateCreditRequest extends Request $rules['location_id'] = ['nullable', 'sometimes','bail', Rule::exists('locations', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->credit->client_id)]; + $rules['e_invoice'] = ['sometimes', 'nullable', new ValidInvoiceScheme()]; + return $rules; } diff --git a/app/Http/Requests/EInvoice/UpdateEInvoiceConfiguration.php b/app/Http/Requests/EInvoice/UpdateEInvoiceConfiguration.php index 6a923a442e..0f8f6525b1 100644 --- a/app/Http/Requests/EInvoice/UpdateEInvoiceConfiguration.php +++ b/app/Http/Requests/EInvoice/UpdateEInvoiceConfiguration.php @@ -39,7 +39,7 @@ class UpdateEInvoiceConfiguration extends Request public function rules() { return [ - 'entity' => 'required|bail|in:invoice,client,company', + 'entity' => 'required|bail|in:credit,invoice,client,company', 'payment_means' => 'sometimes|bail|array', 'payment_means.*.code' => ['required_with:payment_means', 'bail', Rule::in(PaymentMeans::getPaymentMeansCodelist())], 'payment_means.*.bic_swift' => Rule::forEach(function (string|null $value, string $attribute) { @@ -115,6 +115,7 @@ class UpdateEInvoiceConfiguration extends Request return [...$rules, 'nullable']; }), + 'document_reference' => ['sometimes', 'bail', 'array'], ]; } diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 881a6d3feb..e050b9962b 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -276,6 +276,7 @@ class Peppol extends AbstractService $this->p_invoice->Delivery = $this->getDelivery(); $this->setOrderReference() + ->setDocumentReference() ->setTaxBreakdown() ->setPaymentTerms() ->addAttachments() @@ -496,6 +497,19 @@ class Peppol extends AbstractService return [$key => json_decode($this->toJson(), true)]; } + /** + * Set the reference for this document, + * ie: for a credit note, this reference would be the invoice it is referencing. Will always be stored on the e_invoice object. + * + * @return self + */ + private function setDocumentReference(): self + { + // InvoiceNinja\EInvoice\Models\Peppol\DocumentReferenceType + + // We should only need to pull this in from the already stored object. + return $this; + } /** * setOrderReference From 9e21590ee92698c8180b37cd4e04462890763234 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 18 Jan 2026 14:09:26 +1100 Subject: [PATCH 106/177] Validation for credits --- .../Requests/Credit/UpdateCreditRequest.php | 8 +- .../EInvoice/UpdateEInvoiceConfiguration.php | 1 - .../Requests/Invoice/UpdateInvoiceRequest.php | 1 - lang/fr_CA/texts.php | 8 +- lang/vi/texts.php | 8 +- tests/Feature/CreditTest.php | 123 ++++++++++++++++++ 6 files changed, 138 insertions(+), 11 deletions(-) diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index 33c9e6265d..c63c2db1fa 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -13,10 +13,11 @@ namespace App\Http\Requests\Credit; use App\Http\Requests\Request; -use App\Utils\Traits\ChecksEntityStatus; -use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\MakesHash; use Illuminate\Validation\Rule; +use App\Utils\Traits\CleanLineItems; +use App\Utils\Traits\ChecksEntityStatus; +use App\Http\ValidationRules\EInvoice\ValidCreditScheme; class UpdateCreditRequest extends Request { @@ -81,7 +82,7 @@ class UpdateCreditRequest extends Request $rules['location_id'] = ['nullable', 'sometimes','bail', Rule::exists('locations', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->credit->client_id)]; - $rules['e_invoice'] = ['sometimes', 'nullable', new ValidInvoiceScheme()]; + $rules['e_invoice'] = ['sometimes', 'nullable', new ValidCreditScheme()]; return $rules; } @@ -96,6 +97,7 @@ class UpdateCreditRequest extends Request { $input = $this->all(); + nlog($input); $input = $this->decodePrimaryKeys($input); if (isset($input['documents'])) { diff --git a/app/Http/Requests/EInvoice/UpdateEInvoiceConfiguration.php b/app/Http/Requests/EInvoice/UpdateEInvoiceConfiguration.php index 0f8f6525b1..6ced45c966 100644 --- a/app/Http/Requests/EInvoice/UpdateEInvoiceConfiguration.php +++ b/app/Http/Requests/EInvoice/UpdateEInvoiceConfiguration.php @@ -115,7 +115,6 @@ class UpdateEInvoiceConfiguration extends Request return [...$rules, 'nullable']; }), - 'document_reference' => ['sometimes', 'bail', 'array'], ]; } diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 758bb08bcb..2aa636effe 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -17,7 +17,6 @@ use App\Utils\Traits\MakesHash; use Illuminate\Validation\Rule; use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\ChecksEntityStatus; -use App\Http\ValidationRules\Invoice\LockedInvoiceRule; use App\Http\ValidationRules\EInvoice\ValidInvoiceScheme; use App\Http\ValidationRules\Project\ValidProjectForClient; diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index b54eb2e620..058692dc13 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -5683,9 +5683,11 @@ Développe automatiquement la section des notes dans le tableau de produits pour 'reject_quote_confirmation' => 'Êtes-vous sûr de vouloir rejeter cette soumission ?', 'reason' => 'Raison', 'enter_reason' => 'Préciser la raison...', - 'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date', - 'notification_invoice_overdue_summary' => 'The following invoices are overdue:', - 'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?', + 'notification_invoice_overdue_summary_subject' => 'Récapitulatif des factures impayées: :date', + 'notification_invoice_overdue_summary' => 'Les factures suivantes sont impayées:', + 'purge_user_confirmation' => 'Attention ! Cette action réattribuera toutes les entités au propriétaire du compte et supprimera définitivement l\'utilisateur de l\'ensemble des entreprises et des comptes. Voulez-vous vraiment continuer ?', + 'peppol_sending_failed' => 'Problème technique de livraison. Réessai impossible.', + 'peppol_sending_success' => 'La facture électronique a été envoyée!', ); return $lang; diff --git a/lang/vi/texts.php b/lang/vi/texts.php index 76a02d18a8..4ca91d2bf1 100644 --- a/lang/vi/texts.php +++ b/lang/vi/texts.php @@ -5684,9 +5684,11 @@ $lang = array( 'reject_quote_confirmation' => 'Bạn có chắc chắn muốn đến chối báo giá này không?', 'reason' => 'Lý do', 'enter_reason' => 'Nhập một lý do...', - 'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date', - 'notification_invoice_overdue_summary' => 'The following invoices are overdue:', - 'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?', + 'notification_invoice_overdue_summary_subject' => 'Hóa đơn Tóm tắt quá hạn: :date', + 'notification_invoice_overdue_summary' => 'Các Hóa đơn sau đây đã quá hạn:', + 'purge_user_confirmation' => 'Cảnh báo! Thao tác này sẽ gán lại tất cả các thực thể đến chủ sở hữu tài khoản và Xóa vĩnh viễn Người dùng trên tất cả các công ty và tài khoản. Bạn có chắc chắn muốn đến tục không?', + 'peppol_sending_failed' => 'Sự cố giao hàng kỹ thuật. Không thể thử lại', + 'peppol_sending_success' => 'E- Hóa đơn gửi Thành công !', ); return $lang; diff --git a/tests/Feature/CreditTest.php b/tests/Feature/CreditTest.php index b68e67daa4..3793e591a3 100644 --- a/tests/Feature/CreditTest.php +++ b/tests/Feature/CreditTest.php @@ -30,6 +30,7 @@ class CreditTest extends TestCase use MakesHash; use DatabaseTransactions; use MockAccountData; + protected function setUp(): void { parent::setUp(); @@ -38,6 +39,128 @@ class CreditTest extends TestCase Model::reguard(); $this->makeTestData(); + // $this->withoutExceptionHandling(); + } + + public function testCreditEInvoiceValidation() + { + + $credit_update = [ + 'e_invoice' => [ + 'CreditNote' => [ + 'InvoiceDocumentReference' => [ + 'ID' => '', + 'IssueDate' => '', + ], + ], + ], + ]; + + $data = array_merge($this->credit->toArray(), $credit_update); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/credits/'.$this->encodePrimaryKey($this->credit->id), $data); + + $response->assertStatus(422); + } + + public function testCreditEInvoiceValidationWithProperNumber() + { + + $credit_update = [ + 'e_invoice' => [ + 'CreditNote' => [ + 'InvoiceDocumentReference' => [ + 'ID' => 'INV-123456S', + 'IssueDate' => '', + ], + ], + ], + ]; + + $data = array_merge($this->credit->toArray(), $credit_update); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/credits/'.$this->encodePrimaryKey($this->credit->id), $data); + + $response->assertStatus(200); + } + + public function testCreditEInvoiceValidationWithProperDate() + { + + $credit_update = [ + 'e_invoice' => [ + 'CreditNote' => [ + 'InvoiceDocumentReference' => [ + 'ID' => 'INV-123456S', + 'IssueDate' => '2026-01-18', + ], + ], + ], + ]; + + $data = array_merge($this->credit->toArray(), $credit_update); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/credits/'.$this->encodePrimaryKey($this->credit->id), $data); + + $response->assertStatus(200); + } + + + public function testCreditEInvoiceValidationWithIncorrectDate() + { + + $credit_update = [ + 'e_invoice' => [ + 'CreditNote' => [ + 'InvoiceDocumentReference' => [ + 'ID' => 'INV-123456S', + 'IssueDate' => '203326-01-118', + ], + ], + ], + ]; + + $data = array_merge($this->credit->toArray(), $credit_update); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/credits/'.$this->encodePrimaryKey($this->credit->id), $data); + + $response->assertStatus(422); + } + + public function testCreditEInvoiceValidationWithIncorrectDateButPassesValidation() + { + + $credit_update = [ + 'e_invoice' => [ + 'CreditNote' => [ + 'InvoiceDocumentReference' => [ + 'ID' => 'INV-123456S', + 'IssueDate' => '3000-01-11', + ], + ], + ], + ]; + + $data = array_merge($this->credit->toArray(), $credit_update); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/credits/'.$this->encodePrimaryKey($this->credit->id), $data); + + $response->assertStatus(200); } From 56ffabbfdd42e7685a672ccd775743359167660f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 18 Jan 2026 16:30:13 +1100 Subject: [PATCH 107/177] Credit validation --- .../EInvoice/ValidCreditScheme.php | 105 ++++++++++++++++++ .../Validation/Peppol/CreditLevel.php | 24 ++++ 2 files changed, 129 insertions(+) create mode 100644 app/Http/ValidationRules/EInvoice/ValidCreditScheme.php create mode 100644 app/Services/EDocument/Standards/Validation/Peppol/CreditLevel.php diff --git a/app/Http/ValidationRules/EInvoice/ValidCreditScheme.php b/app/Http/ValidationRules/EInvoice/ValidCreditScheme.php new file mode 100644 index 0000000000..5accd70912 --- /dev/null +++ b/app/Http/ValidationRules/EInvoice/ValidCreditScheme.php @@ -0,0 +1,105 @@ +validateRequest($value['CreditNote'], CreditLevel::class); + + foreach ($errors as $key => $msg) { + + $this->validator->errors()->add( + "e_invoice.{$key}", + "{$key} - {$msg}" + ); + + } + + + if (data_get($value, 'CreditNote.InvoiceDocumentReference.ID') === null || + data_get($value, 'CreditNote.InvoiceDocumentReference.ID') === '') { + + $this->validator->errors()->add( + "e_invoice.InvoiceDocumentReference.ID", + "Invoice Reference/Number is required" + ); + + } + + if (isset($value['CreditNote']['InvoiceDocumentReference']['IssueDate']) && strlen($value['CreditNote']['InvoiceDocumentReference']['IssueDate']) > 1 && !$this->isValidDateSyntax($value['CreditNote']['InvoiceDocumentReference']['IssueDate'])) { + + $this->validator->errors()->add( + "e_invoice.InvoiceDocumentReference.IssueDate", + "Invoice Issue Date is required" + ); + + } + + + } + + } + + private function isValidDateSyntax(string $date_string): bool + { + try { + $date = date_create($date_string); + return $date !== false && $date instanceof \DateTime; + } catch (\Exception $e) { + return false; + } + } + + /** + * Set the current validator. + */ + public function setValidator(Validator $validator): static + { + $this->validator = $validator; + + return $this; + } + + +} diff --git a/app/Services/EDocument/Standards/Validation/Peppol/CreditLevel.php b/app/Services/EDocument/Standards/Validation/Peppol/CreditLevel.php new file mode 100644 index 0000000000..5ba4ab604e --- /dev/null +++ b/app/Services/EDocument/Standards/Validation/Peppol/CreditLevel.php @@ -0,0 +1,24 @@ + Date: Sun, 18 Jan 2026 20:00:50 +1100 Subject: [PATCH 108/177] Peppol support for credit + invoice document reference # --- app/Services/EDocument/Standards/Peppol.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index e050b9962b..f597726f17 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -507,6 +507,26 @@ class Peppol extends AbstractService { // InvoiceNinja\EInvoice\Models\Peppol\DocumentReferenceType + if($this->isCreditNote() && $this->invoice->e_invoice->CreditNote->InvoiceDocumentReference ?? false) { + $document_reference = new \InvoiceNinja\EInvoice\Models\Peppol\DocumentReferenceType\InvoiceDocumentReference(); + + $d_id = new ID(); + $d_id->value = $this->invoice->e_invoice->CreditNote->InvoiceDocumentReference->ID; + + $document_reference->ID = $d_id; + + if(isset($this->invoice->e_invoice->CreditNote->InvoiceDocumentReference->IssueDate)) { + $issue_date = new \DateTime($this->invoice->e_invoice->CreditNote->InvoiceDocumentReference->IssueDate); + $document_reference->IssueDate = $issue_date; + } + + $this->p_invoice->InvoiceDocumentReference = $document_reference; + + // $this->p_invoice->InvoiceDocumentReference = $this->invoice->e_invoice->CreditNote->InvoiceDocumentReference; + return $this; + } + + // We should only need to pull this in from the already stored object. return $this; } From 0800575e7e8c16044813adcc37dc653b0440531b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 19 Jan 2026 08:29:08 +1100 Subject: [PATCH 109/177] Fixes for activity display for lesser permissioned users --- app/Http/Controllers/ActivityController.php | 4 +++- app/Http/Requests/Activity/ShowActivityRequest.php | 13 +++++++++++++ app/Http/Requests/Activity/StoreNoteRequest.php | 2 ++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/ActivityController.php b/app/Http/Controllers/ActivityController.php index d689b97039..4e1f84d36c 100644 --- a/app/Http/Controllers/ActivityController.php +++ b/app/Http/Controllers/ActivityController.php @@ -102,7 +102,9 @@ class ActivityController extends BaseController /** @var \App\Models\User auth()->user() */ $user = auth()->user(); - if (!$user->isAdmin()) { + $entity = $request->getEntity(); + + if ($user->cannot('view', $entity)) { $activities->where('user_id', auth()->user()->id); } diff --git a/app/Http/Requests/Activity/ShowActivityRequest.php b/app/Http/Requests/Activity/ShowActivityRequest.php index 58407d0370..8baaa356b0 100644 --- a/app/Http/Requests/Activity/ShowActivityRequest.php +++ b/app/Http/Requests/Activity/ShowActivityRequest.php @@ -12,6 +12,7 @@ namespace App\Http\Requests\Activity; +use Illuminate\Support\Str; use App\Http\Requests\Request; use App\Utils\Traits\MakesHash; @@ -48,4 +49,16 @@ class ShowActivityRequest extends Request $this->replace($input); } + + public function getEntity() + { + if (!$this->entity) { + return false; + } + + $class = "\\App\\Models\\".ucfirst(Str::camel(rtrim($this->entity, 's'))); + return $class::withTrashed()->company()->where('id', is_string($this->entity_id) ? $this->decodePrimaryKey($this->entity_id) : $this->entity_id)->first(); + + } + } diff --git a/app/Http/Requests/Activity/StoreNoteRequest.php b/app/Http/Requests/Activity/StoreNoteRequest.php index 08b3539cdb..29ddba205c 100644 --- a/app/Http/Requests/Activity/StoreNoteRequest.php +++ b/app/Http/Requests/Activity/StoreNoteRequest.php @@ -18,6 +18,8 @@ use Illuminate\Validation\Rule; class StoreNoteRequest extends Request { + public $error_message; + /** * Determine if the user is authorized to make this request. * From 154b74f9cc727263f23efaa61331f1d82ec75619 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 19 Jan 2026 09:16:40 +1100 Subject: [PATCH 110/177] Updates for Stripe ACH --- README.md | 2 +- app/Models/Gateway.php | 2 +- app/PaymentDrivers/Stripe/ACH.php | 203 +++++++++++++++--- app/PaymentDrivers/StripePaymentDriver.php | 6 + .../{app-8ade2cf8.js => app-03a29a9d.js} | 12 +- public/build/assets/stripe-ach-1f0bff4a.js | 9 + public/build/assets/stripe-ach-fe366ca7.js | 9 - public/build/manifest.json | 4 +- resources/js/clients/payments/stripe-ach.js | 104 ++++++--- .../gateways/stripe/ach/authorize.blade.php | 40 +--- 10 files changed, 274 insertions(+), 117 deletions(-) rename public/build/assets/{app-8ade2cf8.js => app-03a29a9d.js} (76%) create mode 100644 public/build/assets/stripe-ach-1f0bff4a.js delete mode 100644 public/build/assets/stripe-ach-fe366ca7.js diff --git a/README.md b/README.md index 48882a1a46..8def0bfb99 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Invoice Ninja Version 5 is here! We've taken the best parts of version 4 and add - [Hosted](https://www.invoiceninja.com): Our hosted version is a Software as a Service (SaaS) solution. You're up and running in under 5 minutes, with no need to worry about hosting or server infrastructure. - [Self-Hosted](https://www.invoiceninja.org): For those who prefer to manage their own hosting and server infrastructure. This version gives you full control and flexibility. -All Pro and Enterprise features from the hosted app are included in the source-available code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app. +All Pro and Enterprise features from the hosted app are included in the source-available code. We offer a $40 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app. #### Get social with us diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 6963714437..2d47070832 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -147,7 +147,7 @@ class Gateway extends StaticModel case 56: return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'charge.refunded', 'payment_intent.payment_failed']], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.refunded','charge.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.refunded', 'charge.succeeded', 'customer.source.updated', 'setup_intent.succeeded', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']], GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing', 'charge.refunded', 'payment_intent.succeeded', 'payment_intent.partially_funded', 'payment_intent.payment_failed']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], diff --git a/app/PaymentDrivers/Stripe/ACH.php b/app/PaymentDrivers/Stripe/ACH.php index df3f22eaaa..e4589ca3e5 100644 --- a/app/PaymentDrivers/Stripe/ACH.php +++ b/app/PaymentDrivers/Stripe/ACH.php @@ -51,10 +51,46 @@ class ACH implements LivewireMethodInterface /** * Authorize a bank account - requires microdeposit verification */ + // public function authorizeView(array $data) + // { + // $data['gateway'] = $this->stripe; + + // return render('gateways.stripe.ach.authorize', array_merge($data)); + // } + + + /** + * Instant Verification methods with fall back to microdeposits. + * + * @param array $data + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ public function authorizeView(array $data) { $data['gateway'] = $this->stripe; - + + $customer = $this->stripe->findOrCreateCustomer(); + + // Create SetupIntent with Financial Connections for instant verification + $intent = \Stripe\SetupIntent::create([ + 'customer' => $customer->id, + 'usage' => 'off_session', + 'payment_method_types' => ['us_bank_account'], + 'payment_method_options' => [ + 'us_bank_account' => [ + 'financial_connections' => [ + 'permissions' => ['payment_method'], + // Optional: add 'balances', 'ownership' for additional data + ], + 'verification_method' => 'automatic', // instant with microdeposit fallback + // Or use 'instant' to require instant only (no fallback) + ], + ], + ], $this->stripe->stripe_connect_auth); + + $data['client_secret'] = $intent->client_secret; + $data['customer'] = $customer; + return render('gateways.stripe.ach.authorize', array_merge($data)); } @@ -62,36 +98,96 @@ class ACH implements LivewireMethodInterface { $this->stripe->init(); - $stripe_response = json_decode($request->input('gateway_response')); + $setup_intent = json_decode($request->input('gateway_response')); + + if (!$setup_intent || !isset($setup_intent->payment_method)) { + throw new PaymentFailed('Invalid response from payment gateway.'); + } $customer = $this->stripe->findOrCreateCustomer(); try { - $source = Customer::createSource($customer->id, ['source' => $stripe_response->token->id], array_merge($this->stripe->stripe_connect_auth, ['idempotency_key' => uniqid("st", true)])); + // Retrieve the payment method to get bank account details + $payment_method = $this->stripe->getStripePaymentMethod($setup_intent->payment_method); + + if (!$payment_method || !isset($payment_method->us_bank_account)) { + throw new PaymentFailed('Unable to retrieve bank account details.'); + } + + $bank_account = $payment_method->us_bank_account; + + // Determine verification state based on SetupIntent status + /** @var string $status */ + $status = $setup_intent->status ?? 'unauthorized'; //@phpstan-ignore-line + $state = match ($status) { + 'succeeded' => 'authorized', + 'requires_action' => 'unauthorized', // Microdeposit verification pending + default => 'unauthorized', + }; + + // Build a new stdClass object for storage (Stripe objects are immutable) + $method = new \stdClass(); + $method->id = $setup_intent->payment_method; //@phpstan-ignore-line + $method->bank_name = $bank_account->bank_name; + $method->last4 = $bank_account->last4; + $method->state = $state; + + // If microdeposit verification is required, store the verification URL + if ($status === 'requires_action' && + isset($setup_intent->next_action) && + ($setup_intent->next_action->type ?? null) === 'verify_with_microdeposits') { //@phpstan-ignore-line + $method->next_action = $setup_intent->next_action->verify_with_microdeposits->hosted_verification_url ?? null; //@phpstan-ignore-line + } + + // Note: We don't attach the payment method here - it's already linked to the + // customer via the SetupIntent. For us_bank_account, the payment method must be + // verified before it can be used. Verification happens via: + // - Instant verification (Financial Connections) - already verified + // - Microdeposits - verified via webhook (setup_intent.succeeded) + + $client_gateway_token = $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer); + + // If instant verification succeeded, redirect to payment methods + if ($state === 'authorized') { + return redirect()->route('client.payment_methods.show', ['payment_method' => $client_gateway_token->hashed_id]) + ->with('message', ctrans('texts.payment_method_added')); + } + + // If microdeposit verification required, send notification and redirect + $verification = route('client.payment_methods.verification', [ + 'payment_method' => $client_gateway_token->hashed_id, + 'method' => GatewayType::BANK_TRANSFER + ], false); + + $mailer = new NinjaMailerObject(); + + $mailer->mailable = new ACHVerificationNotification( + auth()->guard('contact')->user()->client->company, + route('client.contact_login', [ + 'contact_key' => auth()->guard('contact')->user()->contact_key, + 'next' => $verification + ]) + ); + + $mailer->company = auth()->guard('contact')->user()->client->company; + $mailer->settings = auth()->guard('contact')->user()->client->company->settings; + $mailer->to_user = auth()->guard('contact')->user(); + + NinjaMailerJob::dispatch($mailer); + + return redirect()->route('client.payment_methods.verification', [ + 'payment_method' => $client_gateway_token->hashed_id, + 'method' => GatewayType::BANK_TRANSFER + ]); + } catch (InvalidRequestException $e) { throw new PaymentFailed($e->getMessage(), $e->getCode()); } - - $client_gateway_token = $this->storePaymentMethod($source, $request->input('method'), $customer); - - $verification = route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::BANK_TRANSFER], false); - - $mailer = new NinjaMailerObject(); - - $mailer->mailable = new ACHVerificationNotification( - auth()->guard('contact')->user()->client->company, - route('client.contact_login', ['contact_key' => auth()->guard('contact')->user()->contact_key, 'next' => $verification]) - ); - - $mailer->company = auth()->guard('contact')->user()->client->company; - $mailer->settings = auth()->guard('contact')->user()->client->company->settings; - $mailer->to_user = auth()->guard('contact')->user(); - - NinjaMailerJob::dispatch($mailer); - - return redirect()->route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]); } + /** + * Handle customer.source.updated webhook (legacy Sources API) + */ public function updateBankAccount(array $event) { $stripe_event = $event['data']['object']; @@ -108,6 +204,57 @@ class ACH implements LivewireMethodInterface } } + /** + * Handle setup_intent.succeeded webhook (new SetupIntent/Financial Connections flow) + * + * This is called when microdeposit verification is completed for us_bank_account payment methods. + */ + public function handleSetupIntentSucceeded(array $event): void + { + $setup_intent = $event['data']['object']; + + // Only handle us_bank_account payment methods + if (!isset($setup_intent['payment_method']) || !isset($setup_intent['payment_method_types'])) { + return; + } + + if (!in_array('us_bank_account', $setup_intent['payment_method_types'])) { + return; + } + + $payment_method_id = $setup_intent['payment_method']; + $customer_id = $setup_intent['customer'] ?? null; + + if (!$payment_method_id || !$customer_id) { + return; + } + + // Find the token by payment method ID + $token = ClientGatewayToken::query() + ->where('token', $payment_method_id) + ->where('gateway_customer_reference', $customer_id) + ->first(); + + if (!$token) { + nlog("ACH SetupIntent succeeded but no matching token found for payment_method: {$payment_method_id}"); + return; + } + + // Update the token state to authorized + $meta = $token->meta; + $meta->state = 'authorized'; + + // Clear the next_action since verification is complete + if (isset($meta->next_action)) { + unset($meta->next_action); + } + + $token->meta = $meta; + $token->save(); + + nlog("ACH bank account verified via SetupIntent webhook: {$payment_method_id}"); + } + public function verificationView(ClientGatewayToken $token) { @@ -379,12 +526,16 @@ class ACH implements LivewireMethodInterface $response = json_decode($request->gateway_response); $bank_account_response = json_decode($request->bank_account_response); - if ($response->status == 'requires_source_action' && $response->next_action->type == 'verify_with_microdeposits') { - $method = $bank_account_response->payment_method->us_bank_account; - $method = $bank_account_response->payment_method->us_bank_account; + if (in_array($response->status,['requires_action','requires_source_action']) && ($response->next_action->type ?? null) == 'verify_with_microdeposits') { + $method = $bank_account_response->payment_method->us_bank_account ?? null; + + if (!$method) { + throw new PaymentFailed('Unable to retrieve bank account details'); + } + $method->id = $response->payment_method; $method->state = 'unauthorized'; - $method->next_action = $response->next_action->verify_with_microdeposits->hosted_verification_url; + $method->next_action = $response->next_action->verify_with_microdeposits->hosted_verification_url ?? null; $customer = $this->stripe->getCustomer($request->customer); $cgt = $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer); diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 5adea2e4c0..acb9e4f35d 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -727,6 +727,12 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac $ach->updateBankAccount($request->all()); } + // Handle SetupIntent succeeded for ACH microdeposit verification + if ($request->type === 'setup_intent.succeeded') { + $ach = new ACH($this); + $ach->handleSetupIntentSucceeded($request->all()); + } + if ($request->type === 'payment_intent.processing') { PaymentIntentProcessingWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5)); return response()->json([], 200); diff --git a/public/build/assets/app-8ade2cf8.js b/public/build/assets/app-03a29a9d.js similarity index 76% rename from public/build/assets/app-8ade2cf8.js rename to public/build/assets/app-03a29a9d.js index 46aecdbccc..54ab279e00 100644 --- a/public/build/assets/app-8ade2cf8.js +++ b/public/build/assets/app-03a29a9d.js @@ -1,10 +1,10 @@ -import{A as Kl}from"./index-08e160a7.js";import{c as Jt,g as Jl}from"./_commonjsHelpers-725317a4.js";var Gl={visa:{niceType:"Visa",type:"visa",patterns:[4],gaps:[4,8,12],lengths:[16,18,19],code:{name:"CVV",size:3}},mastercard:{niceType:"Mastercard",type:"mastercard",patterns:[[51,55],[2221,2229],[223,229],[23,26],[270,271],2720],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}},"american-express":{niceType:"American Express",type:"american-express",patterns:[34,37],gaps:[4,10],lengths:[15],code:{name:"CID",size:4}},"diners-club":{niceType:"Diners Club",type:"diners-club",patterns:[[300,305],36,38,39],gaps:[4,10],lengths:[14,16,19],code:{name:"CVV",size:3}},discover:{niceType:"Discover",type:"discover",patterns:[6011,[644,649],65],gaps:[4,8,12],lengths:[16,19],code:{name:"CID",size:3}},jcb:{niceType:"JCB",type:"jcb",patterns:[2131,1800,[3528,3589]],gaps:[4,8,12],lengths:[16,17,18,19],code:{name:"CVV",size:3}},unionpay:{niceType:"UnionPay",type:"unionpay",patterns:[620,[624,626],[62100,62182],[62184,62187],[62185,62197],[62200,62205],[622010,622999],622018,[622019,622999],[62207,62209],[622126,622925],[623,626],6270,6272,6276,[627700,627779],[627781,627799],[6282,6289],6291,6292,810,[8110,8131],[8132,8151],[8152,8163],[8164,8171]],gaps:[4,8,12],lengths:[14,15,16,17,18,19],code:{name:"CVN",size:3}},maestro:{niceType:"Maestro",type:"maestro",patterns:[493698,[5e5,504174],[504176,506698],[506779,508999],[56,59],63,67,6],gaps:[4,8,12],lengths:[12,13,14,15,16,17,18,19],code:{name:"CVC",size:3}},elo:{niceType:"Elo",type:"elo",patterns:[401178,401179,438935,457631,457632,431274,451416,457393,504175,[506699,506778],[509e3,509999],627780,636297,636368,[650031,650033],[650035,650051],[650405,650439],[650485,650538],[650541,650598],[650700,650718],[650720,650727],[650901,650978],[651652,651679],[655e3,655019],[655021,655058]],gaps:[4,8,12],lengths:[16],code:{name:"CVE",size:3}},mir:{niceType:"Mir",type:"mir",patterns:[[2200,2204]],gaps:[4,8,12],lengths:[16,17,18,19],code:{name:"CVP2",size:3}},hiper:{niceType:"Hiper",type:"hiper",patterns:[637095,63737423,63743358,637568,637599,637609,637612],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}},hipercard:{niceType:"Hipercard",type:"hipercard",patterns:[606282],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}}},Yl=Gl,si={},_n={};Object.defineProperty(_n,"__esModule",{value:!0});_n.clone=void 0;function Xl(e){return e?JSON.parse(JSON.stringify(e)):null}_n.clone=Xl;var li={};Object.defineProperty(li,"__esModule",{value:!0});li.matches=void 0;function Ql(e,r,n){var a=String(r).length,o=e.substr(0,a),l=parseInt(o,10);return r=parseInt(String(r).substr(0,o.length),10),n=parseInt(String(n).substr(0,o.length),10),l>=r&&l<=n}function Zl(e,r){return r=String(r),r.substring(0,e.length)===e.substring(0,r.length)}function eu(e,r){return Array.isArray(r)?Ql(e,r[0],r[1]):Zl(e,r)}li.matches=eu;Object.defineProperty(si,"__esModule",{value:!0});si.addMatchingCardsToResults=void 0;var tu=_n,ru=li;function nu(e,r,n){var a,o;for(a=0;a=o&&(h.matchStrength=o),n.push(h);break}}}si.addMatchingCardsToResults=nu;var ui={};Object.defineProperty(ui,"__esModule",{value:!0});ui.isValidInputType=void 0;function iu(e){return typeof e=="string"||e instanceof String}ui.isValidInputType=iu;var ci={};Object.defineProperty(ci,"__esModule",{value:!0});ci.findBestMatch=void 0;function au(e){var r=e.filter(function(n){return n.matchStrength}).length;return r>0&&r===e.length}function ou(e){return au(e)?e.reduce(function(r,n){return!r||Number(r.matchStrength)du?hn(!1,!1):fu.test(e)?hn(!1,!0):hn(!0,!0)}fi.cardholderName=pu;var di={};function hu(e){for(var r=0,n=!1,a=e.length-1,o;a>=0;)o=parseInt(e.charAt(a),10),n&&(o*=2,o>9&&(o=o%10+1)),n=!n,r+=o,a--;return r%10===0}var gu=hu;Object.defineProperty(di,"__esModule",{value:!0});di.cardNumber=void 0;var mu=gu,wo=as;function xr(e,r,n){return{card:e,isPotentiallyValid:r,isValid:n}}function vu(e,r){r===void 0&&(r={});var n,a,o;if(typeof e!="string"&&typeof e!="number")return xr(null,!1,!1);var l=String(e).replace(/-|\s/g,"");if(!/^\d*$/.test(l))return xr(null,!1,!1);var h=wo(l);if(h.length===0)return xr(null,!1,!1);if(h.length!==1)return xr(null,!0,!1);var v=h[0];if(r.maxLength&&l.length>r.maxLength)return xr(v,!1,!1);v.type===wo.types.UNIONPAY&&r.luhnValidateUnionPay!==!0?a=!0:a=mu(l),o=Math.max.apply(null,v.lengths),r.maxLength&&(o=Math.min(r.maxLength,o));for(var _=0;_4)return lr(!1,!1);var v=parseInt(e,10),_=Number(String(o).substr(2,2)),N=!1;if(a===2){if(String(o).substr(0,2)===e)return lr(!1,!0);n=_===v,N=v>=_&&v<=_+r}else a===4&&(n=o===v,N=v>=o&&v<=o+r);return lr(N,N,n)}en.expirationYear=bu;var gi={};Object.defineProperty(gi,"__esModule",{value:!0});gi.isArray=void 0;gi.isArray=Array.isArray||function(e){return Object.prototype.toString.call(e)==="[object Array]"};Object.defineProperty(hi,"__esModule",{value:!0});hi.parseDate=void 0;var _u=en,wu=gi;function xu(e){var r=Number(e[0]),n;return r===0?2:r>1||r===1&&Number(e[1])>2?1:r===1?(n=e.substr(1),_u.expirationYear(n).isPotentiallyValid?1:2):e.length===5?1:e.length>5?2:1}function Su(e){var r;if(/^\d{4}-\d{1,2}$/.test(e)?r=e.split("-").reverse():/\//.test(e)?r=e.split(/\s*\/\s*/g):/\s/.test(e)&&(r=e.split(/ +/g)),wu.isArray(r))return{month:r[0]||"",year:r.slice(1).join()};var n=xu(e),a=e.substr(0,n);return{month:a,year:e.substr(a.length)}}hi.parseDate=Su;var xn={};Object.defineProperty(xn,"__esModule",{value:!0});xn.expirationMonth=void 0;function gn(e,r,n){return{isValid:e,isPotentiallyValid:r,isValidForThisYear:n||!1}}function Eu(e){var r=new Date().getMonth()+1;if(typeof e!="string")return gn(!1,!1);if(e.replace(/\s/g,"")===""||e==="0")return gn(!1,!0);if(!/^\d*$/.test(e))return gn(!1,!1);var n=parseInt(e,10);if(isNaN(Number(e)))return gn(!1,!1);var a=n>0&&n<13;return gn(a,a,a&&n>=r)}xn.expirationMonth=Eu;var da=Jt&&Jt.__assign||function(){return da=Object.assign||function(e){for(var r,n=1,a=arguments.length;nr?e[n]:r;return r}function Wr(e,r){return{isValid:e,isPotentiallyValid:r}}function Mu(e,r){return r===void 0&&(r=os),r=r instanceof Array?r:[r],typeof e!="string"||!/^\d*$/.test(e)?Wr(!1,!1):Pu(r,e.length)?Wr(!0,!0):e.lengthRu(r)?Wr(!1,!1):Wr(!0,!0)}mi.cvv=Mu;var vi={};Object.defineProperty(vi,"__esModule",{value:!0});vi.postalCode=void 0;var ku=3;function ta(e,r){return{isValid:e,isPotentiallyValid:r}}function Nu(e,r){r===void 0&&(r={});var n=r.minLength||ku;return typeof e!="string"?ta(!1,!1):e.lengthfunction(){return r||(0,e[ls(e)[0]])((r={exports:{}}).exports,r),r.exports},Qu=(e,r,n,a)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of ls(r))!Xu.call(e,o)&&o!==n&&ss(e,o,{get:()=>r[o],enumerable:!(a=Gu(r,o))||a.enumerable});return e},Je=(e,r,n)=>(n=e!=null?Ju(Yu(e)):{},Qu(r||!e||!e.__esModule?ss(n,"default",{value:e,enumerable:!0}):n,e)),ct=rr({"node_modules/alpinejs/dist/module.cjs.js"(e,r){var n=Object.create,a=Object.defineProperty,o=Object.getOwnPropertyDescriptor,l=Object.getOwnPropertyNames,h=Object.getPrototypeOf,v=Object.prototype.hasOwnProperty,_=(t,i)=>function(){return i||(0,t[l(t)[0]])((i={exports:{}}).exports,i),i.exports},N=(t,i)=>{for(var s in i)a(t,s,{get:i[s],enumerable:!0})},L=(t,i,s,c)=>{if(i&&typeof i=="object"||typeof i=="function")for(let d of l(i))!v.call(t,d)&&d!==s&&a(t,d,{get:()=>i[d],enumerable:!(c=o(i,d))||c.enumerable});return t},ie=(t,i,s)=>(s=t!=null?n(h(t)):{},L(i||!t||!t.__esModule?a(s,"default",{value:t,enumerable:!0}):s,t)),ne=t=>L(a({},"__esModule",{value:!0}),t),B=_({"node_modules/@vue/shared/dist/shared.cjs.js"(t){Object.defineProperty(t,"__esModule",{value:!0});function i(y,Q){const oe=Object.create(null),he=y.split(",");for(let Ue=0;Ue!!oe[Ue.toLowerCase()]:Ue=>!!oe[Ue]}var s={1:"TEXT",2:"CLASS",4:"STYLE",8:"PROPS",16:"FULL_PROPS",32:"HYDRATE_EVENTS",64:"STABLE_FRAGMENT",128:"KEYED_FRAGMENT",256:"UNKEYED_FRAGMENT",512:"NEED_PATCH",1024:"DYNAMIC_SLOTS",2048:"DEV_ROOT_FRAGMENT",[-1]:"HOISTED",[-2]:"BAIL"},c={1:"STABLE",2:"DYNAMIC",3:"FORWARDED"},d="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt",p=i(d),m=2;function x(y,Q=0,oe=y.length){let he=y.split(/(\r?\n)/);const Ue=he.filter((bt,ft)=>ft%2===1);he=he.filter((bt,ft)=>ft%2===0);let Xe=0;const yt=[];for(let bt=0;bt=Q){for(let ft=bt-m;ft<=bt+m||oe>Xe;ft++){if(ft<0||ft>=he.length)continue;const dn=ft+1;yt.push(`${dn}${" ".repeat(Math.max(3-String(dn).length,0))}| ${he[ft]}`);const Vr=he[ft].length,ti=Ue[ft]&&Ue[ft].length||0;if(ft===bt){const zr=Q-(Xe-(Vr+ti)),Zi=Math.max(1,oe>Xe?Vr-zr:oe-Q);yt.push(" | "+" ".repeat(zr)+"^".repeat(Zi))}else if(ft>bt){if(oe>Xe){const zr=Math.max(Math.min(oe-Xe,Vr),1);yt.push(" | "+"^".repeat(zr))}Xe+=Vr+ti}}break}return yt.join(` -`)}var j="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ae=i(j),_e=i(j+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected"),qe=/[>/="'\u0009\u000a\u000c\u0020]/,Ie={};function Ge(y){if(Ie.hasOwnProperty(y))return Ie[y];const Q=qe.test(y);return Q&&console.error(`unsafe attribute name: ${y}`),Ie[y]=!Q}var At={acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv"},Vt=i("animation-iteration-count,border-image-outset,border-image-slice,border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width"),xe=i("accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap");function Ve(y){if(It(y)){const Q={};for(let oe=0;oe{if(oe){const he=oe.split(Be);he.length>1&&(Q[he[0].trim()]=he[1].trim())}}),Q}function Lt(y){let Q="";if(!y)return Q;for(const oe in y){const he=y[oe],Ue=oe.startsWith("--")?oe:Zn(oe);(yr(he)||typeof he=="number"&&Vt(Ue))&&(Q+=`${Ue}:${he};`)}return Q}function zt(y){let Q="";if(yr(y))Q=y;else if(It(y))for(let oe=0;oe]/;function qi(y){const Q=""+y,oe=Hi.exec(Q);if(!oe)return Q;let he="",Ue,Xe,yt=0;for(Xe=oe.index;Xe||--!>|Lr(oe,Q))}var Hn=y=>y==null?"":Wt(y)?JSON.stringify(y,Wi,2):String(y),Wi=(y,Q)=>vr(Q)?{[`Map(${Q.size})`]:[...Q.entries()].reduce((oe,[he,Ue])=>(oe[`${he} =>`]=Ue,oe),{})}:Dt(Q)?{[`Set(${Q.size})`]:[...Q.values()]}:Wt(Q)&&!It(Q)&&!Jn(Q)?String(Q):Q,Ki=["bigInt","optionalChaining","nullishCoalescingOperator"],on=Object.freeze({}),sn=Object.freeze([]),ln=()=>{},Ir=()=>!1,Dr=/^on[^a-z]/,$r=y=>Dr.test(y),Fr=y=>y.startsWith("onUpdate:"),qn=Object.assign,Vn=(y,Q)=>{const oe=y.indexOf(Q);oe>-1&&y.splice(oe,1)},zn=Object.prototype.hasOwnProperty,Wn=(y,Q)=>zn.call(y,Q),It=Array.isArray,vr=y=>br(y)==="[object Map]",Dt=y=>br(y)==="[object Set]",un=y=>y instanceof Date,cn=y=>typeof y=="function",yr=y=>typeof y=="string",Ji=y=>typeof y=="symbol",Wt=y=>y!==null&&typeof y=="object",Br=y=>Wt(y)&&cn(y.then)&&cn(y.catch),Kn=Object.prototype.toString,br=y=>Kn.call(y),Gi=y=>br(y).slice(8,-1),Jn=y=>br(y)==="[object Object]",Gn=y=>yr(y)&&y!=="NaN"&&y[0]!=="-"&&""+parseInt(y,10)===y,Yn=i(",key,ref,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),_r=y=>{const Q=Object.create(null);return oe=>Q[oe]||(Q[oe]=y(oe))},Xn=/-(\w)/g,Qn=_r(y=>y.replace(Xn,(Q,oe)=>oe?oe.toUpperCase():"")),Yi=/\B([A-Z])/g,Zn=_r(y=>y.replace(Yi,"-$1").toLowerCase()),wr=_r(y=>y.charAt(0).toUpperCase()+y.slice(1)),Xi=_r(y=>y?`on${wr(y)}`:""),fn=(y,Q)=>y!==Q&&(y===y||Q===Q),Qi=(y,Q)=>{for(let oe=0;oe{Object.defineProperty(y,Q,{configurable:!0,enumerable:!1,value:oe})},Hr=y=>{const Q=parseFloat(y);return isNaN(Q)?y:Q},qr,ei=()=>qr||(qr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});t.EMPTY_ARR=sn,t.EMPTY_OBJ=on,t.NO=Ir,t.NOOP=ln,t.PatchFlagNames=s,t.babelParserDefaultPlugins=Ki,t.camelize=Qn,t.capitalize=wr,t.def=Ur,t.escapeHtml=qi,t.escapeHtmlComment=Vi,t.extend=qn,t.generateCodeFrame=x,t.getGlobalThis=ei,t.hasChanged=fn,t.hasOwn=Wn,t.hyphenate=Zn,t.invokeArrayFns=Qi,t.isArray=It,t.isBooleanAttr=_e,t.isDate=un,t.isFunction=cn,t.isGloballyWhitelisted=p,t.isHTMLTag=Nr,t.isIntegerKey=Gn,t.isKnownAttr=xe,t.isMap=vr,t.isModelListener=Fr,t.isNoUnitNumericStyleProp=Vt,t.isObject=Wt,t.isOn=$r,t.isPlainObject=Jn,t.isPromise=Br,t.isReservedProp=Yn,t.isSSRSafeAttrName=Ge,t.isSVGTag=Ui,t.isSet=Dt,t.isSpecialBooleanAttr=ae,t.isString=yr,t.isSymbol=Ji,t.isVoidTag=jr,t.looseEqual=Lr,t.looseIndexOf=Un,t.makeMap=i,t.normalizeClass=zt,t.normalizeStyle=Ve,t.objectToString=Kn,t.parseStringStyle=vt,t.propsToAttrMap=At,t.remove=Vn,t.slotFlagsText=c,t.stringifyStyle=Lt,t.toDisplayString=Hn,t.toHandlerKey=Xi,t.toNumber=Hr,t.toRawType=Gi,t.toTypeString=br}}),C=_({"node_modules/@vue/shared/index.js"(t,i){i.exports=B()}}),b=_({"node_modules/@vue/reactivity/dist/reactivity.cjs.js"(t){Object.defineProperty(t,"__esModule",{value:!0});var i=C(),s=new WeakMap,c=[],d,p=Symbol("iterate"),m=Symbol("Map key iterate");function x(u){return u&&u._isEffect===!0}function j(u,P=i.EMPTY_OBJ){x(u)&&(u=u.raw);const I=qe(u,P);return P.lazy||I(),I}function ae(u){u.active&&(Ie(u),u.options.onStop&&u.options.onStop(),u.active=!1)}var _e=0;function qe(u,P){const I=function(){if(!I.active)return u();if(!c.includes(I)){Ie(I);try{return xe(),c.push(I),d=I,u()}finally{c.pop(),Ve(),d=c[c.length-1]}}};return I.id=_e++,I.allowRecurse=!!P.allowRecurse,I._isEffect=!0,I.active=!0,I.raw=u,I.deps=[],I.options=P,I}function Ie(u){const{deps:P}=u;if(P.length){for(let I=0;I{gt&>.forEach($t=>{($t!==d||$t.allowRecurse)&&rt.add($t)})};if(P==="clear")Ne.forEach(_t);else if(I==="length"&&i.isArray(u))Ne.forEach((gt,$t)=>{($t==="length"||$t>=le)&&_t(gt)});else switch(I!==void 0&&_t(Ne.get(I)),P){case"add":i.isArray(u)?i.isIntegerKey(I)&&_t(Ne.get("length")):(_t(Ne.get(p)),i.isMap(u)&&_t(Ne.get(m)));break;case"delete":i.isArray(u)||(_t(Ne.get(p)),i.isMap(u)&&_t(Ne.get(m)));break;case"set":i.isMap(u)&&_t(Ne.get(p));break}const pn=gt=>{gt.options.onTrigger&>.options.onTrigger({effect:gt,target:u,key:I,type:P,newValue:le,oldValue:te,oldTarget:ge}),gt.options.scheduler?gt.options.scheduler(gt):gt()};rt.forEach(pn)}var vt=i.makeMap("__proto__,__v_isRef,__isVue"),Lt=new Set(Object.getOwnPropertyNames(Symbol).map(u=>Symbol[u]).filter(i.isSymbol)),zt=jr(),kr=jr(!1,!0),nn=jr(!0),an=jr(!0,!0),Nr=Ui();function Ui(){const u={};return["includes","indexOf","lastIndexOf"].forEach(P=>{u[P]=function(...I){const le=y(this);for(let ge=0,Ne=this.length;ge{u[P]=function(...I){Vt();const le=y(this)[P].apply(this,I);return Ve(),le}}),u}function jr(u=!1,P=!1){return function(le,te,ge){if(te==="__v_isReactive")return!u;if(te==="__v_isReadonly")return u;if(te==="__v_raw"&&ge===(u?P?Qn:Xn:P?_r:Yn).get(le))return le;const Ne=i.isArray(le);if(!u&&Ne&&i.hasOwn(Nr,te))return Reflect.get(Nr,te,ge);const rt=Reflect.get(le,te,ge);return(i.isSymbol(te)?Lt.has(te):vt(te))||(u||ke(le,"get",te),P)?rt:he(rt)?!Ne||!i.isIntegerKey(te)?rt.value:rt:i.isObject(rt)?u?fn(rt):wr(rt):rt}}var Hi=Bn(),qi=Bn(!0);function Bn(u=!1){return function(I,le,te,ge){let Ne=I[le];if(!u&&(te=y(te),Ne=y(Ne),!i.isArray(I)&&he(Ne)&&!he(te)))return Ne.value=te,!0;const rt=i.isArray(I)&&i.isIntegerKey(le)?Number(le)i.isObject(u)?wr(u):u,sn=u=>i.isObject(u)?fn(u):u,ln=u=>u,Ir=u=>Reflect.getPrototypeOf(u);function Dr(u,P,I=!1,le=!1){u=u.__v_raw;const te=y(u),ge=y(P);P!==ge&&!I&&ke(te,"get",P),!I&&ke(te,"get",ge);const{has:Ne}=Ir(te),rt=le?ln:I?sn:on;if(Ne.call(te,P))return rt(u.get(P));if(Ne.call(te,ge))return rt(u.get(ge));u!==te&&u.get(P)}function $r(u,P=!1){const I=this.__v_raw,le=y(I),te=y(u);return u!==te&&!P&&ke(le,"has",u),!P&&ke(le,"has",te),u===te?I.has(u):I.has(u)||I.has(te)}function Fr(u,P=!1){return u=u.__v_raw,!P&&ke(y(u),"iterate",p),Reflect.get(u,"size",u)}function qn(u){u=y(u);const P=y(this);return Ir(P).has.call(P,u)||(P.add(u),Be(P,"add",u,u)),this}function Vn(u,P){P=y(P);const I=y(this),{has:le,get:te}=Ir(I);let ge=le.call(I,u);ge?Gn(I,le,u):(u=y(u),ge=le.call(I,u));const Ne=te.call(I,u);return I.set(u,P),ge?i.hasChanged(P,Ne)&&Be(I,"set",u,P,Ne):Be(I,"add",u,P),this}function zn(u){const P=y(this),{has:I,get:le}=Ir(P);let te=I.call(P,u);te?Gn(P,I,u):(u=y(u),te=I.call(P,u));const ge=le?le.call(P,u):void 0,Ne=P.delete(u);return te&&Be(P,"delete",u,void 0,ge),Ne}function Wn(){const u=y(this),P=u.size!==0,I=i.isMap(u)?new Map(u):new Set(u),le=u.clear();return P&&Be(u,"clear",void 0,void 0,I),le}function It(u,P){return function(le,te){const ge=this,Ne=ge.__v_raw,rt=y(Ne),_t=P?ln:u?sn:on;return!u&&ke(rt,"iterate",p),Ne.forEach((pn,gt)=>le.call(te,_t(pn),_t(gt),ge))}}function vr(u,P,I){return function(...le){const te=this.__v_raw,ge=y(te),Ne=i.isMap(ge),rt=u==="entries"||u===Symbol.iterator&&Ne,_t=u==="keys"&&Ne,pn=te[u](...le),gt=I?ln:P?sn:on;return!P&&ke(ge,"iterate",_t?m:p),{next(){const{value:$t,done:ea}=pn.next();return ea?{value:$t,done:ea}:{value:rt?[gt($t[0]),gt($t[1])]:gt($t),done:ea}},[Symbol.iterator](){return this}}}}function Dt(u){return function(...P){{const I=P[0]?`on key "${P[0]}" `:"";console.warn(`${i.capitalize(u)} operation ${I}failed: target is readonly.`,y(this))}return u==="delete"?!1:this}}function un(){const u={get(ge){return Dr(this,ge)},get size(){return Fr(this)},has:$r,add:qn,set:Vn,delete:zn,clear:Wn,forEach:It(!1,!1)},P={get(ge){return Dr(this,ge,!1,!0)},get size(){return Fr(this)},has:$r,add:qn,set:Vn,delete:zn,clear:Wn,forEach:It(!1,!0)},I={get(ge){return Dr(this,ge,!0)},get size(){return Fr(this,!0)},has(ge){return $r.call(this,ge,!0)},add:Dt("add"),set:Dt("set"),delete:Dt("delete"),clear:Dt("clear"),forEach:It(!0,!1)},le={get(ge){return Dr(this,ge,!0,!0)},get size(){return Fr(this,!0)},has(ge){return $r.call(this,ge,!0)},add:Dt("add"),set:Dt("set"),delete:Dt("delete"),clear:Dt("clear"),forEach:It(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(ge=>{u[ge]=vr(ge,!1,!1),I[ge]=vr(ge,!0,!1),P[ge]=vr(ge,!1,!0),le[ge]=vr(ge,!0,!0)}),[u,I,P,le]}var[cn,yr,Ji,Wt]=un();function Br(u,P){const I=P?u?Wt:Ji:u?yr:cn;return(le,te,ge)=>te==="__v_isReactive"?!u:te==="__v_isReadonly"?u:te==="__v_raw"?le:Reflect.get(i.hasOwn(I,te)&&te in le?I:le,te,ge)}var Kn={get:Br(!1,!1)},br={get:Br(!1,!0)},Gi={get:Br(!0,!1)},Jn={get:Br(!0,!0)};function Gn(u,P,I){const le=y(I);if(le!==I&&P.call(u,le)){const te=i.toRawType(u);console.warn(`Reactive ${te} contains both the raw and reactive versions of the same object${te==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var Yn=new WeakMap,_r=new WeakMap,Xn=new WeakMap,Qn=new WeakMap;function Yi(u){switch(u){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Zn(u){return u.__v_skip||!Object.isExtensible(u)?0:Yi(i.toRawType(u))}function wr(u){return u&&u.__v_isReadonly?u:Ur(u,!1,Un,Kn,Yn)}function Xi(u){return Ur(u,!1,Wi,br,_r)}function fn(u){return Ur(u,!0,Hn,Gi,Xn)}function Qi(u){return Ur(u,!0,Ki,Jn,Qn)}function Ur(u,P,I,le,te){if(!i.isObject(u))return console.warn(`value cannot be made reactive: ${String(u)}`),u;if(u.__v_raw&&!(P&&u.__v_isReactive))return u;const ge=te.get(u);if(ge)return ge;const Ne=Zn(u);if(Ne===0)return u;const rt=new Proxy(u,Ne===2?le:I);return te.set(u,rt),rt}function Hr(u){return qr(u)?Hr(u.__v_raw):!!(u&&u.__v_isReactive)}function qr(u){return!!(u&&u.__v_isReadonly)}function ei(u){return Hr(u)||qr(u)}function y(u){return u&&y(u.__v_raw)||u}function Q(u){return i.def(u,"__v_skip",!0),u}var oe=u=>i.isObject(u)?wr(u):u;function he(u){return!!(u&&u.__v_isRef===!0)}function Ue(u){return bt(u)}function Xe(u){return bt(u,!0)}var yt=class{constructor(u,P=!1){this._shallow=P,this.__v_isRef=!0,this._rawValue=P?u:y(u),this._value=P?u:oe(u)}get value(){return ke(y(this),"get","value"),this._value}set value(u){u=this._shallow?u:y(u),i.hasChanged(u,this._rawValue)&&(this._rawValue=u,this._value=this._shallow?u:oe(u),Be(y(this),"set","value",u))}};function bt(u,P=!1){return he(u)?u:new yt(u,P)}function ft(u){Be(y(u),"set","value",u.value)}function dn(u){return he(u)?u.value:u}var Vr={get:(u,P,I)=>dn(Reflect.get(u,P,I)),set:(u,P,I,le)=>{const te=u[P];return he(te)&&!he(I)?(te.value=I,!0):Reflect.set(u,P,I,le)}};function ti(u){return Hr(u)?u:new Proxy(u,Vr)}var zr=class{constructor(u){this.__v_isRef=!0;const{get:P,set:I}=u(()=>ke(this,"get","value"),()=>Be(this,"set","value"));this._get=P,this._set=I}get value(){return this._get()}set value(u){this._set(u)}};function Zi(u){return new zr(u)}function ql(u){ei(u)||console.warn("toRefs() expects a reactive object but received a plain one.");const P=i.isArray(u)?new Array(u.length):{};for(const I in u)P[I]=_o(u,I);return P}var Vl=class{constructor(u,P){this._object=u,this._key=P,this.__v_isRef=!0}get value(){return this._object[this._key]}set value(u){this._object[this._key]=u}};function _o(u,P){return he(u[P])?u[P]:new Vl(u,P)}var zl=class{constructor(u,P,I){this._setter=P,this._dirty=!0,this.__v_isRef=!0,this.effect=j(u,{lazy:!0,scheduler:()=>{this._dirty||(this._dirty=!0,Be(y(this),"set","value"))}}),this.__v_isReadonly=I}get value(){const u=y(this);return u._dirty&&(u._value=this.effect(),u._dirty=!1),ke(u,"get","value"),u._value}set value(u){this._setter(u)}};function Wl(u){let P,I;return i.isFunction(u)?(P=u,I=()=>{console.warn("Write operation failed: computed value is readonly")}):(P=u.get,I=u.set),new zl(P,I,i.isFunction(u)||!u.set)}t.ITERATE_KEY=p,t.computed=Wl,t.customRef=Zi,t.effect=j,t.enableTracking=xe,t.isProxy=ei,t.isReactive=Hr,t.isReadonly=qr,t.isRef=he,t.markRaw=Q,t.pauseTracking=Vt,t.proxyRefs=ti,t.reactive=wr,t.readonly=fn,t.ref=Ue,t.resetTracking=Ve,t.shallowReactive=Xi,t.shallowReadonly=Qi,t.shallowRef=Xe,t.stop=ae,t.toRaw=y,t.toRef=_o,t.toRefs=ql,t.track=ke,t.trigger=Be,t.triggerRef=ft,t.unref=dn}}),T=_({"node_modules/@vue/reactivity/index.js"(t,i){i.exports=b()}}),E={};N(E,{Alpine:()=>bo,default:()=>Hl}),r.exports=ne(E);var O=!1,D=!1,F=[],be=-1;function Se(t){G(t)}function G(t){F.includes(t)||F.push(t),R()}function W(t){let i=F.indexOf(t);i!==-1&&i>be&&F.splice(i,1)}function R(){!D&&!O&&(O=!0,queueMicrotask(K))}function K(){O=!1,D=!0;for(let t=0;tt.effect(i,{scheduler:s=>{Ce?Se(s):s()}}),re=t.raw}function X(t){S=t}function nt(t){let i=()=>{};return[c=>{let d=S(c);return t._x_effects||(t._x_effects=new Set,t._x_runEffects=()=>{t._x_effects.forEach(p=>p())}),t._x_effects.add(d),i=()=>{d!==void 0&&(t._x_effects.delete(d),M(d))},d},()=>{i()}]}function ze(t,i){let s=!0,c,d=S(()=>{let p=t();JSON.stringify(p),s?c=p:queueMicrotask(()=>{i(p,c),c=p}),s=!1});return()=>M(d)}var fe=[],me=[],we=[];function Oe(t){we.push(t)}function J(t,i){typeof i=="function"?(t._x_cleanups||(t._x_cleanups=[]),t._x_cleanups.push(i)):(i=t,me.push(i))}function ee(t){fe.push(t)}function We(t,i,s){t._x_attributeCleanups||(t._x_attributeCleanups={}),t._x_attributeCleanups[i]||(t._x_attributeCleanups[i]=[]),t._x_attributeCleanups[i].push(s)}function pt(t,i){t._x_attributeCleanups&&Object.entries(t._x_attributeCleanups).forEach(([s,c])=>{(i===void 0||i.includes(s))&&(c.forEach(d=>d()),delete t._x_attributeCleanups[s])})}function st(t){var i,s;for((i=t._x_effects)==null||i.forEach(W);(s=t._x_cleanups)!=null&&s.length;)t._x_cleanups.pop()()}var Ze=new MutationObserver(Ye),wt=!1;function Ke(){Ze.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),wt=!0}function Pt(){Ft(),Ze.disconnect(),wt=!1}var Et=[];function Ft(){let t=Ze.takeRecords();Et.push(()=>t.length>0&&Ye(t));let i=Et.length;queueMicrotask(()=>{if(Et.length===i)for(;Et.length>0;)Et.shift()()})}function de(t){if(!wt)return t();Pt();let i=t();return Ke(),i}var $=!1,U=[];function ye(){$=!0}function Ae(){$=!1,Ye(U),U=[]}function Ye(t){if($){U=U.concat(t);return}let i=[],s=new Set,c=new Map,d=new Map;for(let p=0;p{m.nodeType===1&&m._x_marker&&s.add(m)}),t[p].addedNodes.forEach(m=>{if(m.nodeType===1){if(s.has(m)){s.delete(m);return}m._x_marker||i.push(m)}})),t[p].type==="attributes")){let m=t[p].target,x=t[p].attributeName,j=t[p].oldValue,ae=()=>{c.has(m)||c.set(m,[]),c.get(m).push({name:x,value:m.getAttribute(x)})},_e=()=>{d.has(m)||d.set(m,[]),d.get(m).push(x)};m.hasAttribute(x)&&j===null?ae():m.hasAttribute(x)?(_e(),ae()):_e()}d.forEach((p,m)=>{pt(m,p)}),c.forEach((p,m)=>{fe.forEach(x=>x(m,p))});for(let p of s)i.some(m=>m.contains(p))||me.forEach(m=>m(p));for(let p of i)p.isConnected&&we.forEach(m=>m(p));i=null,s=null,c=null,d=null}function ve(t){return pe(Y(t))}function V(t,i,s){return t._x_dataStack=[i,...Y(s||t)],()=>{t._x_dataStack=t._x_dataStack.filter(c=>c!==i)}}function Y(t){return t._x_dataStack?t._x_dataStack:typeof ShadowRoot=="function"&&t instanceof ShadowRoot?Y(t.host):t.parentNode?Y(t.parentNode):[]}function pe(t){return new Proxy({objects:t},He)}var He={ownKeys({objects:t}){return Array.from(new Set(t.flatMap(i=>Object.keys(i))))},has({objects:t},i){return i==Symbol.unscopables?!1:t.some(s=>Object.prototype.hasOwnProperty.call(s,i)||Reflect.has(s,i))},get({objects:t},i,s){return i=="toJSON"?Fe:Reflect.get(t.find(c=>Reflect.has(c,i))||{},i,s)},set({objects:t},i,s,c){const d=t.find(m=>Object.prototype.hasOwnProperty.call(m,i))||t[t.length-1],p=Object.getOwnPropertyDescriptor(d,i);return p!=null&&p.set&&(p!=null&&p.get)?p.set.call(c,s)||!0:Reflect.set(d,i,s)}};function Fe(){return Reflect.ownKeys(this).reduce((i,s)=>(i[s]=Reflect.get(this,s),i),{})}function it(t){let i=c=>typeof c=="object"&&!Array.isArray(c)&&c!==null,s=(c,d="")=>{Object.entries(Object.getOwnPropertyDescriptors(c)).forEach(([p,{value:m,enumerable:x}])=>{if(x===!1||m===void 0||typeof m=="object"&&m!==null&&m.__v_skip)return;let j=d===""?p:`${d}.${p}`;typeof m=="object"&&m!==null&&m._x_interceptor?c[p]=m.initialize(t,j,p):i(m)&&m!==c&&!(m instanceof Element)&&s(m,j)})};return s(t)}function at(t,i=()=>{}){let s={initialValue:void 0,_x_interceptor:!0,initialize(c,d,p){return t(this.initialValue,()=>Rt(c,d),m=>Nt(c,d,m),d,p)}};return i(s),c=>{if(typeof c=="object"&&c!==null&&c._x_interceptor){let d=s.initialize.bind(s);s.initialize=(p,m,x)=>{let j=c.initialize(p,m,x);return s.initialValue=j,d(p,m,x)}}else s.initialValue=c;return s}}function Rt(t,i){return i.split(".").reduce((s,c)=>s[c],t)}function Nt(t,i,s){if(typeof i=="string"&&(i=i.split(".")),i.length===1)t[i[0]]=s;else{if(i.length===0)throw error;return t[i[0]]||(t[i[0]]={}),Nt(t[i[0]],i.slice(1),s)}}var fr={};function xt(t,i){fr[t]=i}function jt(t,i){let s=dr(i);return Object.entries(fr).forEach(([c,d])=>{Object.defineProperty(t,`$${c}`,{get(){return d(i,s)},enumerable:!1})}),t}function dr(t){let[i,s]=Le(t),c={interceptor:at,...i};return J(t,s),c}function En(t,i,s,...c){try{return s(...c)}catch(d){nr(d,t,i)}}function nr(...t){return An(...t)}var An=Si;function xi(t){An=t}function Si(t,i,s=void 0){t=Object.assign(t??{message:"No error message given."},{el:i,expression:s}),console.warn(`Alpine Expression Error: ${t.message} +import{A as Kl}from"./index-08e160a7.js";import{c as Jt,g as Jl}from"./_commonjsHelpers-725317a4.js";var Gl={visa:{niceType:"Visa",type:"visa",patterns:[4],gaps:[4,8,12],lengths:[16,18,19],code:{name:"CVV",size:3}},mastercard:{niceType:"Mastercard",type:"mastercard",patterns:[[51,55],[2221,2229],[223,229],[23,26],[270,271],2720],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}},"american-express":{niceType:"American Express",type:"american-express",patterns:[34,37],gaps:[4,10],lengths:[15],code:{name:"CID",size:4}},"diners-club":{niceType:"Diners Club",type:"diners-club",patterns:[[300,305],36,38,39],gaps:[4,10],lengths:[14,16,19],code:{name:"CVV",size:3}},discover:{niceType:"Discover",type:"discover",patterns:[6011,[644,649],65],gaps:[4,8,12],lengths:[16,19],code:{name:"CID",size:3}},jcb:{niceType:"JCB",type:"jcb",patterns:[2131,1800,[3528,3589]],gaps:[4,8,12],lengths:[16,17,18,19],code:{name:"CVV",size:3}},unionpay:{niceType:"UnionPay",type:"unionpay",patterns:[620,[624,626],[62100,62182],[62184,62187],[62185,62197],[62200,62205],[622010,622999],622018,[622019,622999],[62207,62209],[622126,622925],[623,626],6270,6272,6276,[627700,627779],[627781,627799],[6282,6289],6291,6292,810,[8110,8131],[8132,8151],[8152,8163],[8164,8171]],gaps:[4,8,12],lengths:[14,15,16,17,18,19],code:{name:"CVN",size:3}},maestro:{niceType:"Maestro",type:"maestro",patterns:[493698,[5e5,504174],[504176,506698],[506779,508999],[56,59],63,67,6],gaps:[4,8,12],lengths:[12,13,14,15,16,17,18,19],code:{name:"CVC",size:3}},elo:{niceType:"Elo",type:"elo",patterns:[401178,401179,438935,457631,457632,431274,451416,457393,504175,[506699,506778],[509e3,509999],627780,636297,636368,[650031,650033],[650035,650051],[650405,650439],[650485,650538],[650541,650598],[650700,650718],[650720,650727],[650901,650978],[651652,651679],[655e3,655019],[655021,655058]],gaps:[4,8,12],lengths:[16],code:{name:"CVE",size:3}},mir:{niceType:"Mir",type:"mir",patterns:[[2200,2204]],gaps:[4,8,12],lengths:[16,17,18,19],code:{name:"CVP2",size:3}},hiper:{niceType:"Hiper",type:"hiper",patterns:[637095,63737423,63743358,637568,637599,637609,637612],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}},hipercard:{niceType:"Hipercard",type:"hipercard",patterns:[606282],gaps:[4,8,12],lengths:[16],code:{name:"CVC",size:3}}},Yl=Gl,si={},_n={};Object.defineProperty(_n,"__esModule",{value:!0});_n.clone=void 0;function Xl(e){return e?JSON.parse(JSON.stringify(e)):null}_n.clone=Xl;var li={};Object.defineProperty(li,"__esModule",{value:!0});li.matches=void 0;function Ql(e,r,n){var a=String(r).length,o=e.substr(0,a),l=parseInt(o,10);return r=parseInt(String(r).substr(0,o.length),10),n=parseInt(String(n).substr(0,o.length),10),l>=r&&l<=n}function Zl(e,r){return r=String(r),r.substring(0,e.length)===e.substring(0,r.length)}function eu(e,r){return Array.isArray(r)?Ql(e,r[0],r[1]):Zl(e,r)}li.matches=eu;Object.defineProperty(si,"__esModule",{value:!0});si.addMatchingCardsToResults=void 0;var tu=_n,ru=li;function nu(e,r,n){var a,o;for(a=0;a=o&&(h.matchStrength=o),n.push(h);break}}}si.addMatchingCardsToResults=nu;var ui={};Object.defineProperty(ui,"__esModule",{value:!0});ui.isValidInputType=void 0;function iu(e){return typeof e=="string"||e instanceof String}ui.isValidInputType=iu;var ci={};Object.defineProperty(ci,"__esModule",{value:!0});ci.findBestMatch=void 0;function au(e){var r=e.filter(function(n){return n.matchStrength}).length;return r>0&&r===e.length}function ou(e){return au(e)?e.reduce(function(r,n){return!r||Number(r.matchStrength)du?hn(!1,!1):fu.test(e)?hn(!1,!0):hn(!0,!0)}fi.cardholderName=pu;var di={};function hu(e){for(var r=0,n=!1,a=e.length-1,o;a>=0;)o=parseInt(e.charAt(a),10),n&&(o*=2,o>9&&(o=o%10+1)),n=!n,r+=o,a--;return r%10===0}var gu=hu;Object.defineProperty(di,"__esModule",{value:!0});di.cardNumber=void 0;var mu=gu,wo=as;function xr(e,r,n){return{card:e,isPotentiallyValid:r,isValid:n}}function vu(e,r){r===void 0&&(r={});var n,a,o;if(typeof e!="string"&&typeof e!="number")return xr(null,!1,!1);var l=String(e).replace(/-|\s/g,"");if(!/^\d*$/.test(l))return xr(null,!1,!1);var h=wo(l);if(h.length===0)return xr(null,!1,!1);if(h.length!==1)return xr(null,!0,!1);var v=h[0];if(r.maxLength&&l.length>r.maxLength)return xr(v,!1,!1);v.type===wo.types.UNIONPAY&&r.luhnValidateUnionPay!==!0?a=!0:a=mu(l),o=Math.max.apply(null,v.lengths),r.maxLength&&(o=Math.min(r.maxLength,o));for(var _=0;_4)return lr(!1,!1);var v=parseInt(e,10),_=Number(String(o).substr(2,2)),j=!1;if(a===2){if(String(o).substr(0,2)===e)return lr(!1,!0);n=_===v,j=v>=_&&v<=_+r}else a===4&&(n=o===v,j=v>=o&&v<=o+r);return lr(j,j,n)}en.expirationYear=bu;var gi={};Object.defineProperty(gi,"__esModule",{value:!0});gi.isArray=void 0;gi.isArray=Array.isArray||function(e){return Object.prototype.toString.call(e)==="[object Array]"};Object.defineProperty(hi,"__esModule",{value:!0});hi.parseDate=void 0;var _u=en,wu=gi;function xu(e){var r=Number(e[0]),n;return r===0?2:r>1||r===1&&Number(e[1])>2?1:r===1?(n=e.substr(1),_u.expirationYear(n).isPotentiallyValid?1:2):e.length===5?1:e.length>5?2:1}function Su(e){var r;if(/^\d{4}-\d{1,2}$/.test(e)?r=e.split("-").reverse():/\//.test(e)?r=e.split(/\s*\/\s*/g):/\s/.test(e)&&(r=e.split(/ +/g)),wu.isArray(r))return{month:r[0]||"",year:r.slice(1).join()};var n=xu(e),a=e.substr(0,n);return{month:a,year:e.substr(a.length)}}hi.parseDate=Su;var xn={};Object.defineProperty(xn,"__esModule",{value:!0});xn.expirationMonth=void 0;function gn(e,r,n){return{isValid:e,isPotentiallyValid:r,isValidForThisYear:n||!1}}function Eu(e){var r=new Date().getMonth()+1;if(typeof e!="string")return gn(!1,!1);if(e.replace(/\s/g,"")===""||e==="0")return gn(!1,!0);if(!/^\d*$/.test(e))return gn(!1,!1);var n=parseInt(e,10);if(isNaN(Number(e)))return gn(!1,!1);var a=n>0&&n<13;return gn(a,a,a&&n>=r)}xn.expirationMonth=Eu;var da=Jt&&Jt.__assign||function(){return da=Object.assign||function(e){for(var r,n=1,a=arguments.length;nr?e[n]:r;return r}function Wr(e,r){return{isValid:e,isPotentiallyValid:r}}function Mu(e,r){return r===void 0&&(r=os),r=r instanceof Array?r:[r],typeof e!="string"||!/^\d*$/.test(e)?Wr(!1,!1):Pu(r,e.length)?Wr(!0,!0):e.lengthRu(r)?Wr(!1,!1):Wr(!0,!0)}mi.cvv=Mu;var vi={};Object.defineProperty(vi,"__esModule",{value:!0});vi.postalCode=void 0;var ku=3;function ta(e,r){return{isValid:e,isPotentiallyValid:r}}function Nu(e,r){r===void 0&&(r={});var n=r.minLength||ku;return typeof e!="string"?ta(!1,!1):e.lengthfunction(){return r||(0,e[ls(e)[0]])((r={exports:{}}).exports,r),r.exports},Qu=(e,r,n,a)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of ls(r))!Xu.call(e,o)&&o!==n&&ss(e,o,{get:()=>r[o],enumerable:!(a=Gu(r,o))||a.enumerable});return e},Je=(e,r,n)=>(n=e!=null?Ju(Yu(e)):{},Qu(r||!e||!e.__esModule?ss(n,"default",{value:e,enumerable:!0}):n,e)),ct=rr({"node_modules/alpinejs/dist/module.cjs.js"(e,r){var n=Object.create,a=Object.defineProperty,o=Object.getOwnPropertyDescriptor,l=Object.getOwnPropertyNames,h=Object.getPrototypeOf,v=Object.prototype.hasOwnProperty,_=(t,i)=>function(){return i||(0,t[l(t)[0]])((i={exports:{}}).exports,i),i.exports},j=(t,i)=>{for(var s in i)a(t,s,{get:i[s],enumerable:!0})},R=(t,i,s,c)=>{if(i&&typeof i=="object"||typeof i=="function")for(let d of l(i))!v.call(t,d)&&d!==s&&a(t,d,{get:()=>i[d],enumerable:!(c=o(i,d))||c.enumerable});return t},re=(t,i,s)=>(s=t!=null?n(h(t)):{},R(i||!t||!t.__esModule?a(s,"default",{value:t,enumerable:!0}):s,t)),ie=t=>R(a({},"__esModule",{value:!0}),t),B=_({"node_modules/@vue/shared/dist/shared.cjs.js"(t){Object.defineProperty(t,"__esModule",{value:!0});function i(y,Q){const oe=Object.create(null),he=y.split(",");for(let Ue=0;Ue!!oe[Ue.toLowerCase()]:Ue=>!!oe[Ue]}var s={1:"TEXT",2:"CLASS",4:"STYLE",8:"PROPS",16:"FULL_PROPS",32:"HYDRATE_EVENTS",64:"STABLE_FRAGMENT",128:"KEYED_FRAGMENT",256:"UNKEYED_FRAGMENT",512:"NEED_PATCH",1024:"DYNAMIC_SLOTS",2048:"DEV_ROOT_FRAGMENT",[-1]:"HOISTED",[-2]:"BAIL"},c={1:"STABLE",2:"DYNAMIC",3:"FORWARDED"},d="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt",p=i(d),m=2;function x(y,Q=0,oe=y.length){let he=y.split(/(\r?\n)/);const Ue=he.filter((bt,ft)=>ft%2===1);he=he.filter((bt,ft)=>ft%2===0);let Xe=0;const yt=[];for(let bt=0;bt=Q){for(let ft=bt-m;ft<=bt+m||oe>Xe;ft++){if(ft<0||ft>=he.length)continue;const dn=ft+1;yt.push(`${dn}${" ".repeat(Math.max(3-String(dn).length,0))}| ${he[ft]}`);const Vr=he[ft].length,ti=Ue[ft]&&Ue[ft].length||0;if(ft===bt){const zr=Q-(Xe-(Vr+ti)),Zi=Math.max(1,oe>Xe?Vr-zr:oe-Q);yt.push(" | "+" ".repeat(zr)+"^".repeat(Zi))}else if(ft>bt){if(oe>Xe){const zr=Math.max(Math.min(oe-Xe,Vr),1);yt.push(" | "+"^".repeat(zr))}Xe+=Vr+ti}}break}return yt.join(` +`)}var L="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ae=i(L),_e=i(L+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected"),qe=/[>/="'\u0009\u000a\u000c\u0020]/,Ie={};function Ge(y){if(Ie.hasOwnProperty(y))return Ie[y];const Q=qe.test(y);return Q&&console.error(`unsafe attribute name: ${y}`),Ie[y]=!Q}var At={acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv"},Vt=i("animation-iteration-count,border-image-outset,border-image-slice,border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width"),xe=i("accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap");function Ve(y){if(It(y)){const Q={};for(let oe=0;oe{if(oe){const he=oe.split(Be);he.length>1&&(Q[he[0].trim()]=he[1].trim())}}),Q}function Lt(y){let Q="";if(!y)return Q;for(const oe in y){const he=y[oe],Ue=oe.startsWith("--")?oe:Zn(oe);(yr(he)||typeof he=="number"&&Vt(Ue))&&(Q+=`${Ue}:${he};`)}return Q}function zt(y){let Q="";if(yr(y))Q=y;else if(It(y))for(let oe=0;oe]/;function qi(y){const Q=""+y,oe=Hi.exec(Q);if(!oe)return Q;let he="",Ue,Xe,yt=0;for(Xe=oe.index;Xe||--!>|Lr(oe,Q))}var Hn=y=>y==null?"":Wt(y)?JSON.stringify(y,Wi,2):String(y),Wi=(y,Q)=>vr(Q)?{[`Map(${Q.size})`]:[...Q.entries()].reduce((oe,[he,Ue])=>(oe[`${he} =>`]=Ue,oe),{})}:Dt(Q)?{[`Set(${Q.size})`]:[...Q.values()]}:Wt(Q)&&!It(Q)&&!Jn(Q)?String(Q):Q,Ki=["bigInt","optionalChaining","nullishCoalescingOperator"],on=Object.freeze({}),sn=Object.freeze([]),ln=()=>{},Ir=()=>!1,Dr=/^on[^a-z]/,$r=y=>Dr.test(y),Fr=y=>y.startsWith("onUpdate:"),qn=Object.assign,Vn=(y,Q)=>{const oe=y.indexOf(Q);oe>-1&&y.splice(oe,1)},zn=Object.prototype.hasOwnProperty,Wn=(y,Q)=>zn.call(y,Q),It=Array.isArray,vr=y=>br(y)==="[object Map]",Dt=y=>br(y)==="[object Set]",un=y=>y instanceof Date,cn=y=>typeof y=="function",yr=y=>typeof y=="string",Ji=y=>typeof y=="symbol",Wt=y=>y!==null&&typeof y=="object",Br=y=>Wt(y)&&cn(y.then)&&cn(y.catch),Kn=Object.prototype.toString,br=y=>Kn.call(y),Gi=y=>br(y).slice(8,-1),Jn=y=>br(y)==="[object Object]",Gn=y=>yr(y)&&y!=="NaN"&&y[0]!=="-"&&""+parseInt(y,10)===y,Yn=i(",key,ref,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),_r=y=>{const Q=Object.create(null);return oe=>Q[oe]||(Q[oe]=y(oe))},Xn=/-(\w)/g,Qn=_r(y=>y.replace(Xn,(Q,oe)=>oe?oe.toUpperCase():"")),Yi=/\B([A-Z])/g,Zn=_r(y=>y.replace(Yi,"-$1").toLowerCase()),wr=_r(y=>y.charAt(0).toUpperCase()+y.slice(1)),Xi=_r(y=>y?`on${wr(y)}`:""),fn=(y,Q)=>y!==Q&&(y===y||Q===Q),Qi=(y,Q)=>{for(let oe=0;oe{Object.defineProperty(y,Q,{configurable:!0,enumerable:!1,value:oe})},Hr=y=>{const Q=parseFloat(y);return isNaN(Q)?y:Q},qr,ei=()=>qr||(qr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});t.EMPTY_ARR=sn,t.EMPTY_OBJ=on,t.NO=Ir,t.NOOP=ln,t.PatchFlagNames=s,t.babelParserDefaultPlugins=Ki,t.camelize=Qn,t.capitalize=wr,t.def=Ur,t.escapeHtml=qi,t.escapeHtmlComment=Vi,t.extend=qn,t.generateCodeFrame=x,t.getGlobalThis=ei,t.hasChanged=fn,t.hasOwn=Wn,t.hyphenate=Zn,t.invokeArrayFns=Qi,t.isArray=It,t.isBooleanAttr=_e,t.isDate=un,t.isFunction=cn,t.isGloballyWhitelisted=p,t.isHTMLTag=Nr,t.isIntegerKey=Gn,t.isKnownAttr=xe,t.isMap=vr,t.isModelListener=Fr,t.isNoUnitNumericStyleProp=Vt,t.isObject=Wt,t.isOn=$r,t.isPlainObject=Jn,t.isPromise=Br,t.isReservedProp=Yn,t.isSSRSafeAttrName=Ge,t.isSVGTag=Ui,t.isSet=Dt,t.isSpecialBooleanAttr=ae,t.isString=yr,t.isSymbol=Ji,t.isVoidTag=jr,t.looseEqual=Lr,t.looseIndexOf=Un,t.makeMap=i,t.normalizeClass=zt,t.normalizeStyle=Ve,t.objectToString=Kn,t.parseStringStyle=vt,t.propsToAttrMap=At,t.remove=Vn,t.slotFlagsText=c,t.stringifyStyle=Lt,t.toDisplayString=Hn,t.toHandlerKey=Xi,t.toNumber=Hr,t.toRawType=Gi,t.toTypeString=br}}),C=_({"node_modules/@vue/shared/index.js"(t,i){i.exports=B()}}),b=_({"node_modules/@vue/reactivity/dist/reactivity.cjs.js"(t){Object.defineProperty(t,"__esModule",{value:!0});var i=C(),s=new WeakMap,c=[],d,p=Symbol("iterate"),m=Symbol("Map key iterate");function x(u){return u&&u._isEffect===!0}function L(u,P=i.EMPTY_OBJ){x(u)&&(u=u.raw);const I=qe(u,P);return P.lazy||I(),I}function ae(u){u.active&&(Ie(u),u.options.onStop&&u.options.onStop(),u.active=!1)}var _e=0;function qe(u,P){const I=function(){if(!I.active)return u();if(!c.includes(I)){Ie(I);try{return xe(),c.push(I),d=I,u()}finally{c.pop(),Ve(),d=c[c.length-1]}}};return I.id=_e++,I.allowRecurse=!!P.allowRecurse,I._isEffect=!0,I.active=!0,I.raw=u,I.deps=[],I.options=P,I}function Ie(u){const{deps:P}=u;if(P.length){for(let I=0;I{gt&>.forEach($t=>{($t!==d||$t.allowRecurse)&&rt.add($t)})};if(P==="clear")Ne.forEach(_t);else if(I==="length"&&i.isArray(u))Ne.forEach((gt,$t)=>{($t==="length"||$t>=le)&&_t(gt)});else switch(I!==void 0&&_t(Ne.get(I)),P){case"add":i.isArray(u)?i.isIntegerKey(I)&&_t(Ne.get("length")):(_t(Ne.get(p)),i.isMap(u)&&_t(Ne.get(m)));break;case"delete":i.isArray(u)||(_t(Ne.get(p)),i.isMap(u)&&_t(Ne.get(m)));break;case"set":i.isMap(u)&&_t(Ne.get(p));break}const pn=gt=>{gt.options.onTrigger&>.options.onTrigger({effect:gt,target:u,key:I,type:P,newValue:le,oldValue:te,oldTarget:ge}),gt.options.scheduler?gt.options.scheduler(gt):gt()};rt.forEach(pn)}var vt=i.makeMap("__proto__,__v_isRef,__isVue"),Lt=new Set(Object.getOwnPropertyNames(Symbol).map(u=>Symbol[u]).filter(i.isSymbol)),zt=jr(),kr=jr(!1,!0),nn=jr(!0),an=jr(!0,!0),Nr=Ui();function Ui(){const u={};return["includes","indexOf","lastIndexOf"].forEach(P=>{u[P]=function(...I){const le=y(this);for(let ge=0,Ne=this.length;ge{u[P]=function(...I){Vt();const le=y(this)[P].apply(this,I);return Ve(),le}}),u}function jr(u=!1,P=!1){return function(le,te,ge){if(te==="__v_isReactive")return!u;if(te==="__v_isReadonly")return u;if(te==="__v_raw"&&ge===(u?P?Qn:Xn:P?_r:Yn).get(le))return le;const Ne=i.isArray(le);if(!u&&Ne&&i.hasOwn(Nr,te))return Reflect.get(Nr,te,ge);const rt=Reflect.get(le,te,ge);return(i.isSymbol(te)?Lt.has(te):vt(te))||(u||ke(le,"get",te),P)?rt:he(rt)?!Ne||!i.isIntegerKey(te)?rt.value:rt:i.isObject(rt)?u?fn(rt):wr(rt):rt}}var Hi=Bn(),qi=Bn(!0);function Bn(u=!1){return function(I,le,te,ge){let Ne=I[le];if(!u&&(te=y(te),Ne=y(Ne),!i.isArray(I)&&he(Ne)&&!he(te)))return Ne.value=te,!0;const rt=i.isArray(I)&&i.isIntegerKey(le)?Number(le)i.isObject(u)?wr(u):u,sn=u=>i.isObject(u)?fn(u):u,ln=u=>u,Ir=u=>Reflect.getPrototypeOf(u);function Dr(u,P,I=!1,le=!1){u=u.__v_raw;const te=y(u),ge=y(P);P!==ge&&!I&&ke(te,"get",P),!I&&ke(te,"get",ge);const{has:Ne}=Ir(te),rt=le?ln:I?sn:on;if(Ne.call(te,P))return rt(u.get(P));if(Ne.call(te,ge))return rt(u.get(ge));u!==te&&u.get(P)}function $r(u,P=!1){const I=this.__v_raw,le=y(I),te=y(u);return u!==te&&!P&&ke(le,"has",u),!P&&ke(le,"has",te),u===te?I.has(u):I.has(u)||I.has(te)}function Fr(u,P=!1){return u=u.__v_raw,!P&&ke(y(u),"iterate",p),Reflect.get(u,"size",u)}function qn(u){u=y(u);const P=y(this);return Ir(P).has.call(P,u)||(P.add(u),Be(P,"add",u,u)),this}function Vn(u,P){P=y(P);const I=y(this),{has:le,get:te}=Ir(I);let ge=le.call(I,u);ge?Gn(I,le,u):(u=y(u),ge=le.call(I,u));const Ne=te.call(I,u);return I.set(u,P),ge?i.hasChanged(P,Ne)&&Be(I,"set",u,P,Ne):Be(I,"add",u,P),this}function zn(u){const P=y(this),{has:I,get:le}=Ir(P);let te=I.call(P,u);te?Gn(P,I,u):(u=y(u),te=I.call(P,u));const ge=le?le.call(P,u):void 0,Ne=P.delete(u);return te&&Be(P,"delete",u,void 0,ge),Ne}function Wn(){const u=y(this),P=u.size!==0,I=i.isMap(u)?new Map(u):new Set(u),le=u.clear();return P&&Be(u,"clear",void 0,void 0,I),le}function It(u,P){return function(le,te){const ge=this,Ne=ge.__v_raw,rt=y(Ne),_t=P?ln:u?sn:on;return!u&&ke(rt,"iterate",p),Ne.forEach((pn,gt)=>le.call(te,_t(pn),_t(gt),ge))}}function vr(u,P,I){return function(...le){const te=this.__v_raw,ge=y(te),Ne=i.isMap(ge),rt=u==="entries"||u===Symbol.iterator&&Ne,_t=u==="keys"&&Ne,pn=te[u](...le),gt=I?ln:P?sn:on;return!P&&ke(ge,"iterate",_t?m:p),{next(){const{value:$t,done:ea}=pn.next();return ea?{value:$t,done:ea}:{value:rt?[gt($t[0]),gt($t[1])]:gt($t),done:ea}},[Symbol.iterator](){return this}}}}function Dt(u){return function(...P){{const I=P[0]?`on key "${P[0]}" `:"";console.warn(`${i.capitalize(u)} operation ${I}failed: target is readonly.`,y(this))}return u==="delete"?!1:this}}function un(){const u={get(ge){return Dr(this,ge)},get size(){return Fr(this)},has:$r,add:qn,set:Vn,delete:zn,clear:Wn,forEach:It(!1,!1)},P={get(ge){return Dr(this,ge,!1,!0)},get size(){return Fr(this)},has:$r,add:qn,set:Vn,delete:zn,clear:Wn,forEach:It(!1,!0)},I={get(ge){return Dr(this,ge,!0)},get size(){return Fr(this,!0)},has(ge){return $r.call(this,ge,!0)},add:Dt("add"),set:Dt("set"),delete:Dt("delete"),clear:Dt("clear"),forEach:It(!0,!1)},le={get(ge){return Dr(this,ge,!0,!0)},get size(){return Fr(this,!0)},has(ge){return $r.call(this,ge,!0)},add:Dt("add"),set:Dt("set"),delete:Dt("delete"),clear:Dt("clear"),forEach:It(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(ge=>{u[ge]=vr(ge,!1,!1),I[ge]=vr(ge,!0,!1),P[ge]=vr(ge,!1,!0),le[ge]=vr(ge,!0,!0)}),[u,I,P,le]}var[cn,yr,Ji,Wt]=un();function Br(u,P){const I=P?u?Wt:Ji:u?yr:cn;return(le,te,ge)=>te==="__v_isReactive"?!u:te==="__v_isReadonly"?u:te==="__v_raw"?le:Reflect.get(i.hasOwn(I,te)&&te in le?I:le,te,ge)}var Kn={get:Br(!1,!1)},br={get:Br(!1,!0)},Gi={get:Br(!0,!1)},Jn={get:Br(!0,!0)};function Gn(u,P,I){const le=y(I);if(le!==I&&P.call(u,le)){const te=i.toRawType(u);console.warn(`Reactive ${te} contains both the raw and reactive versions of the same object${te==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var Yn=new WeakMap,_r=new WeakMap,Xn=new WeakMap,Qn=new WeakMap;function Yi(u){switch(u){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Zn(u){return u.__v_skip||!Object.isExtensible(u)?0:Yi(i.toRawType(u))}function wr(u){return u&&u.__v_isReadonly?u:Ur(u,!1,Un,Kn,Yn)}function Xi(u){return Ur(u,!1,Wi,br,_r)}function fn(u){return Ur(u,!0,Hn,Gi,Xn)}function Qi(u){return Ur(u,!0,Ki,Jn,Qn)}function Ur(u,P,I,le,te){if(!i.isObject(u))return console.warn(`value cannot be made reactive: ${String(u)}`),u;if(u.__v_raw&&!(P&&u.__v_isReactive))return u;const ge=te.get(u);if(ge)return ge;const Ne=Zn(u);if(Ne===0)return u;const rt=new Proxy(u,Ne===2?le:I);return te.set(u,rt),rt}function Hr(u){return qr(u)?Hr(u.__v_raw):!!(u&&u.__v_isReactive)}function qr(u){return!!(u&&u.__v_isReadonly)}function ei(u){return Hr(u)||qr(u)}function y(u){return u&&y(u.__v_raw)||u}function Q(u){return i.def(u,"__v_skip",!0),u}var oe=u=>i.isObject(u)?wr(u):u;function he(u){return!!(u&&u.__v_isRef===!0)}function Ue(u){return bt(u)}function Xe(u){return bt(u,!0)}var yt=class{constructor(u,P=!1){this._shallow=P,this.__v_isRef=!0,this._rawValue=P?u:y(u),this._value=P?u:oe(u)}get value(){return ke(y(this),"get","value"),this._value}set value(u){u=this._shallow?u:y(u),i.hasChanged(u,this._rawValue)&&(this._rawValue=u,this._value=this._shallow?u:oe(u),Be(y(this),"set","value",u))}};function bt(u,P=!1){return he(u)?u:new yt(u,P)}function ft(u){Be(y(u),"set","value",u.value)}function dn(u){return he(u)?u.value:u}var Vr={get:(u,P,I)=>dn(Reflect.get(u,P,I)),set:(u,P,I,le)=>{const te=u[P];return he(te)&&!he(I)?(te.value=I,!0):Reflect.set(u,P,I,le)}};function ti(u){return Hr(u)?u:new Proxy(u,Vr)}var zr=class{constructor(u){this.__v_isRef=!0;const{get:P,set:I}=u(()=>ke(this,"get","value"),()=>Be(this,"set","value"));this._get=P,this._set=I}get value(){return this._get()}set value(u){this._set(u)}};function Zi(u){return new zr(u)}function ql(u){ei(u)||console.warn("toRefs() expects a reactive object but received a plain one.");const P=i.isArray(u)?new Array(u.length):{};for(const I in u)P[I]=_o(u,I);return P}var Vl=class{constructor(u,P){this._object=u,this._key=P,this.__v_isRef=!0}get value(){return this._object[this._key]}set value(u){this._object[this._key]=u}};function _o(u,P){return he(u[P])?u[P]:new Vl(u,P)}var zl=class{constructor(u,P,I){this._setter=P,this._dirty=!0,this.__v_isRef=!0,this.effect=L(u,{lazy:!0,scheduler:()=>{this._dirty||(this._dirty=!0,Be(y(this),"set","value"))}}),this.__v_isReadonly=I}get value(){const u=y(this);return u._dirty&&(u._value=this.effect(),u._dirty=!1),ke(u,"get","value"),u._value}set value(u){this._setter(u)}};function Wl(u){let P,I;return i.isFunction(u)?(P=u,I=()=>{console.warn("Write operation failed: computed value is readonly")}):(P=u.get,I=u.set),new zl(P,I,i.isFunction(u)||!u.set)}t.ITERATE_KEY=p,t.computed=Wl,t.customRef=Zi,t.effect=L,t.enableTracking=xe,t.isProxy=ei,t.isReactive=Hr,t.isReadonly=qr,t.isRef=he,t.markRaw=Q,t.pauseTracking=Vt,t.proxyRefs=ti,t.reactive=wr,t.readonly=fn,t.ref=Ue,t.resetTracking=Ve,t.shallowReactive=Xi,t.shallowReadonly=Qi,t.shallowRef=Xe,t.stop=ae,t.toRaw=y,t.toRef=_o,t.toRefs=ql,t.track=ke,t.trigger=Be,t.triggerRef=ft,t.unref=dn}}),T=_({"node_modules/@vue/reactivity/index.js"(t,i){i.exports=b()}}),E={};j(E,{Alpine:()=>bo,default:()=>Hl}),r.exports=ie(E);var O=!1,D=!1,F=[],be=-1;function Se(t){G(t)}function G(t){F.includes(t)||F.push(t),M()}function W(t){let i=F.indexOf(t);i!==-1&&i>be&&F.splice(i,1)}function M(){!D&&!O&&(O=!0,queueMicrotask(K))}function K(){O=!1,D=!0;for(let t=0;tt.effect(i,{scheduler:s=>{Ce?Se(s):s()}}),ne=t.raw}function X(t){S=t}function nt(t){let i=()=>{};return[c=>{let d=S(c);return t._x_effects||(t._x_effects=new Set,t._x_runEffects=()=>{t._x_effects.forEach(p=>p())}),t._x_effects.add(d),i=()=>{d!==void 0&&(t._x_effects.delete(d),k(d))},d},()=>{i()}]}function ze(t,i){let s=!0,c,d=S(()=>{let p=t();JSON.stringify(p),s?c=p:queueMicrotask(()=>{i(p,c),c=p}),s=!1});return()=>k(d)}var fe=[],me=[],we=[];function Oe(t){we.push(t)}function J(t,i){typeof i=="function"?(t._x_cleanups||(t._x_cleanups=[]),t._x_cleanups.push(i)):(i=t,me.push(i))}function ee(t){fe.push(t)}function We(t,i,s){t._x_attributeCleanups||(t._x_attributeCleanups={}),t._x_attributeCleanups[i]||(t._x_attributeCleanups[i]=[]),t._x_attributeCleanups[i].push(s)}function pt(t,i){t._x_attributeCleanups&&Object.entries(t._x_attributeCleanups).forEach(([s,c])=>{(i===void 0||i.includes(s))&&(c.forEach(d=>d()),delete t._x_attributeCleanups[s])})}function st(t){var i,s;for((i=t._x_effects)==null||i.forEach(W);(s=t._x_cleanups)!=null&&s.length;)t._x_cleanups.pop()()}var Ze=new MutationObserver(Ye),wt=!1;function Ke(){Ze.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),wt=!0}function Pt(){Ft(),Ze.disconnect(),wt=!1}var Et=[];function Ft(){let t=Ze.takeRecords();Et.push(()=>t.length>0&&Ye(t));let i=Et.length;queueMicrotask(()=>{if(Et.length===i)for(;Et.length>0;)Et.shift()()})}function de(t){if(!wt)return t();Pt();let i=t();return Ke(),i}var $=!1,U=[];function ye(){$=!0}function Ae(){$=!1,Ye(U),U=[]}function Ye(t){if($){U=U.concat(t);return}let i=[],s=new Set,c=new Map,d=new Map;for(let p=0;p{m.nodeType===1&&m._x_marker&&s.add(m)}),t[p].addedNodes.forEach(m=>{if(m.nodeType===1){if(s.has(m)){s.delete(m);return}m._x_marker||i.push(m)}})),t[p].type==="attributes")){let m=t[p].target,x=t[p].attributeName,L=t[p].oldValue,ae=()=>{c.has(m)||c.set(m,[]),c.get(m).push({name:x,value:m.getAttribute(x)})},_e=()=>{d.has(m)||d.set(m,[]),d.get(m).push(x)};m.hasAttribute(x)&&L===null?ae():m.hasAttribute(x)?(_e(),ae()):_e()}d.forEach((p,m)=>{pt(m,p)}),c.forEach((p,m)=>{fe.forEach(x=>x(m,p))});for(let p of s)i.some(m=>m.contains(p))||me.forEach(m=>m(p));for(let p of i)p.isConnected&&we.forEach(m=>m(p));i=null,s=null,c=null,d=null}function ve(t){return pe(Y(t))}function V(t,i,s){return t._x_dataStack=[i,...Y(s||t)],()=>{t._x_dataStack=t._x_dataStack.filter(c=>c!==i)}}function Y(t){return t._x_dataStack?t._x_dataStack:typeof ShadowRoot=="function"&&t instanceof ShadowRoot?Y(t.host):t.parentNode?Y(t.parentNode):[]}function pe(t){return new Proxy({objects:t},He)}var He={ownKeys({objects:t}){return Array.from(new Set(t.flatMap(i=>Object.keys(i))))},has({objects:t},i){return i==Symbol.unscopables?!1:t.some(s=>Object.prototype.hasOwnProperty.call(s,i)||Reflect.has(s,i))},get({objects:t},i,s){return i=="toJSON"?Fe:Reflect.get(t.find(c=>Reflect.has(c,i))||{},i,s)},set({objects:t},i,s,c){const d=t.find(m=>Object.prototype.hasOwnProperty.call(m,i))||t[t.length-1],p=Object.getOwnPropertyDescriptor(d,i);return p!=null&&p.set&&(p!=null&&p.get)?p.set.call(c,s)||!0:Reflect.set(d,i,s)}};function Fe(){return Reflect.ownKeys(this).reduce((i,s)=>(i[s]=Reflect.get(this,s),i),{})}function it(t){let i=c=>typeof c=="object"&&!Array.isArray(c)&&c!==null,s=(c,d="")=>{Object.entries(Object.getOwnPropertyDescriptors(c)).forEach(([p,{value:m,enumerable:x}])=>{if(x===!1||m===void 0||typeof m=="object"&&m!==null&&m.__v_skip)return;let L=d===""?p:`${d}.${p}`;typeof m=="object"&&m!==null&&m._x_interceptor?c[p]=m.initialize(t,L,p):i(m)&&m!==c&&!(m instanceof Element)&&s(m,L)})};return s(t)}function at(t,i=()=>{}){let s={initialValue:void 0,_x_interceptor:!0,initialize(c,d,p){return t(this.initialValue,()=>Rt(c,d),m=>Nt(c,d,m),d,p)}};return i(s),c=>{if(typeof c=="object"&&c!==null&&c._x_interceptor){let d=s.initialize.bind(s);s.initialize=(p,m,x)=>{let L=c.initialize(p,m,x);return s.initialValue=L,d(p,m,x)}}else s.initialValue=c;return s}}function Rt(t,i){return i.split(".").reduce((s,c)=>s[c],t)}function Nt(t,i,s){if(typeof i=="string"&&(i=i.split(".")),i.length===1)t[i[0]]=s;else{if(i.length===0)throw error;return t[i[0]]||(t[i[0]]={}),Nt(t[i[0]],i.slice(1),s)}}var fr={};function xt(t,i){fr[t]=i}function jt(t,i){let s=dr(i);return Object.entries(fr).forEach(([c,d])=>{Object.defineProperty(t,`$${c}`,{get(){return d(i,s)},enumerable:!1})}),t}function dr(t){let[i,s]=Le(t),c={interceptor:at,...i};return J(t,s),c}function En(t,i,s,...c){try{return s(...c)}catch(d){nr(d,t,i)}}function nr(...t){return An(...t)}var An=Si;function xi(t){An=t}function Si(t,i,s=void 0){t=Object.assign(t??{message:"No error message given."},{el:i,expression:s}),console.warn(`Alpine Expression Error: ${t.message} ${s?'Expression: "'+s+`" -`:""}`,i),setTimeout(()=>{throw t},0)}var ir=!0;function tn(t){let i=ir;ir=!1;let s=t();return ir=i,s}function Bt(t,i,s={}){let c;return mt(t,i)(d=>c=d,s),c}function mt(...t){return Cn(...t)}var Cn=Tn;function Ei(t){Cn=t}var On;function Ai(t){On=t}function Tn(t,i){let s={};jt(s,t);let c=[s,...Y(t)],d=typeof i=="function"?Pn(c,i):Ci(c,i,t);return En.bind(null,t,i,d)}function Pn(t,i){return(s=()=>{},{scope:c={},params:d=[],context:p}={})=>{if(!ir){f(s,i,pe([c,...t]),d);return}let m=i.apply(pe([c,...t]),d);f(s,m)}}var Tr={};function Rn(t,i){if(Tr[t])return Tr[t];let s=Object.getPrototypeOf(async function(){}).constructor,c=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t,p=(()=>{try{let m=new s(["__self","scope"],`with (scope) { __self.result = ${c} }; __self.finished = true; return __self.result;`);return Object.defineProperty(m,"name",{value:`[Alpine] ${t}`}),m}catch(m){return nr(m,i,t),Promise.resolve()}})();return Tr[t]=p,p}function Ci(t,i,s){let c=Rn(i,s);return(d=()=>{},{scope:p={},params:m=[],context:x}={})=>{c.result=void 0,c.finished=!1;let j=pe([p,...t]);if(typeof c=="function"){let ae=c.call(x,c,j).catch(_e=>nr(_e,s,i));c.finished?(f(d,c.result,j,m,s),c.result=void 0):ae.then(_e=>{f(d,_e,j,m,s)}).catch(_e=>nr(_e,s,i)).finally(()=>c.result=void 0)}}}function f(t,i,s,c,d){if(ir&&typeof i=="function"){let p=i.apply(s,c);p instanceof Promise?p.then(m=>f(t,m,s,c)).catch(m=>nr(m,d,i)):t(p)}else typeof i=="object"&&i instanceof Promise?i.then(p=>t(p)):t(i)}function g(...t){return On(...t)}function w(t,i,s={}){var c,d;let p={};jt(p,t);let m=[p,...Y(t)],x=pe([(c=s.scope)!=null?c:{},...m]),j=(d=s.params)!=null?d:[];if(i.includes("await")){let ae=Object.getPrototypeOf(async function(){}).constructor,_e=/^[\n\s]*if.*\(.*\)/.test(i.trim())||/^(let|const)\s/.test(i.trim())?`(async()=>{ ${i} })()`:i;return new ae(["scope"],`with (scope) { let __result = ${_e}; return __result }`).call(s.context,x)}else{let ae=/^[\n\s]*if.*\(.*\)/.test(i.trim())||/^(let|const)\s/.test(i.trim())?`(()=>{ ${i} })()`:i,qe=new Function(["scope"],`with (scope) { let __result = ${ae}; return __result }`).call(s.context,x);return typeof qe=="function"&&ir?qe.apply(x,j):qe}}var A="x-";function k(t=""){return A+t}function q(t){A=t}var H={};function z(t,i){return H[t]=i,{before(s){if(!H[s]){console.warn(String.raw`Cannot find directive \`${s}\`. \`${t}\` will use the default order of execution`);return}const c=Ht.indexOf(s);Ht.splice(c>=0?c:Ht.indexOf("DEFAULT"),0,t)}}}function ue(t){return Object.keys(H).includes(t)}function ce(t,i,s){if(i=Array.from(i),t._x_virtualDirectives){let p=Object.entries(t._x_virtualDirectives).map(([x,j])=>({name:x,value:j})),m=Me(p);p=p.map(x=>m.find(j=>j.name===x.name)?{name:`x-bind:${x.name}`,value:`"${x.value}"`}:x),i=i.concat(p)}let c={};return i.map(lt((p,m)=>c[p]=m)).filter(Ut).map(hr(c,s)).sort(Mn).map(p=>et(t,p))}function Me(t){return Array.from(t).map(lt()).filter(i=>!Ut(i))}var Te=!1,je=new Map,Ee=Symbol();function Re(t){Te=!0;let i=Symbol();Ee=i,je.set(i,[]);let s=()=>{for(;je.get(i).length;)je.get(i).shift()();je.delete(i)},c=()=>{Te=!1,s()};t(s),c()}function Le(t){let i=[],s=x=>i.push(x),[c,d]=nt(t);return i.push(d),[{Alpine:Mr,effect:c,cleanup:s,evaluateLater:mt.bind(mt,t),evaluate:Bt.bind(Bt,t)},()=>i.forEach(x=>x())]}function et(t,i){let s=()=>{},c=H[i.type]||s,[d,p]=Le(t);We(t,i.original,p);let m=()=>{t._x_ignore||t._x_ignoreSelf||(c.inline&&c.inline(t,i,d),c=c.bind(c,t,i,d),Te?je.get(Ee).push(c):c())};return m.runCleanups=p,m}var De=(t,i)=>({name:s,value:c})=>(s.startsWith(t)&&(s=s.replace(t,i)),{name:s,value:c}),tt=t=>t;function lt(t=()=>{}){return({name:i,value:s})=>{let{name:c,value:d}=St.reduce((p,m)=>m(p),{name:i,value:s});return c!==i&&t(c,i),{name:c,value:d}}}var St=[];function ut(t){St.push(t)}function Ut({name:t}){return pr().test(t)}var pr=()=>new RegExp(`^${A}([^:^.]+)\\b`);function hr(t,i){return({name:s,value:c})=>{let d=s.match(pr()),p=s.match(/:([a-zA-Z0-9\-_:]+)/),m=s.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],x=i||t[s]||s;return{type:d?d[1]:null,value:p?p[1]:null,modifiers:m.map(j=>j.replace(".","")),expression:c,original:x}}}var Pr="DEFAULT",Ht=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",Pr,"teleport"];function Mn(t,i){let s=Ht.indexOf(t.type)===-1?Pr:t.type,c=Ht.indexOf(i.type)===-1?Pr:i.type;return Ht.indexOf(s)-Ht.indexOf(c)}function Ct(t,i,s={}){t.dispatchEvent(new CustomEvent(i,{detail:s,bubbles:!0,composed:!0,cancelable:!0}))}function qt(t,i){if(typeof ShadowRoot=="function"&&t instanceof ShadowRoot){Array.from(t.children).forEach(d=>qt(d,i));return}let s=!1;if(i(t,()=>s=!0),s)return;let c=t.firstElementChild;for(;c;)qt(c,i),c=c.nextElementSibling}function ht(t,...i){console.warn(`Alpine Warning: ${t}`,...i)}var Yt=!1;function ar(){Yt&&ht("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),Yt=!0,document.body||ht("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` +@endscript diff --git a/resources/views/portal/ninja2020/flow2/required-fields.blade.php b/resources/views/portal/ninja2020/flow2/required-fields.blade.php index 244c6de856..43b822b98b 100644 --- a/resources/views/portal/ninja2020/flow2/required-fields.blade.php +++ b/resources/views/portal/ninja2020/flow2/required-fields.blade.php @@ -1,58 +1,4 @@ -

+

{{ ctrans('texts.required_fields') }} @@ -72,7 +18,7 @@ @foreach($fields as $field) @component('portal.ninja2020.components.general.card-element', ['title' => $field['label']]) @if($field['name'] == 'client_country_id' || $field['name'] == 'client_shipping_country_id') -