diff --git a/README.md b/README.md
index 77c166068b..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
@@ -58,6 +58,9 @@ All Pro and Enterprise features from the hosted app are included in the source-a
* [Stripe](https://stripe.com/)
* [Postmark](https://postmarkapp.com/)
+### SDKs available for easier API consumption
+* [Go SDK](https://github.com/AshkanYarmoradi/go-invoice-ninja)
+
## [Advanced] Quick Hosting Setup
In addition to the official [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) we have a few commands for you.
diff --git a/VERSION.txt b/VERSION.txt
index 63e89ae548..ef425f93f1 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1 +1 @@
-5.12.38
\ No newline at end of file
+5.12.50
\ No newline at end of file
diff --git a/app/Casts/QuickbooksSettingsCast.php b/app/Casts/QuickbooksSettingsCast.php
index 904b5bafaa..6dfece5960 100644
--- a/app/Casts/QuickbooksSettingsCast.php
+++ b/app/Casts/QuickbooksSettingsCast.php
@@ -30,10 +30,9 @@ class QuickbooksSettingsCast implements CastsAttributes
public function set($model, string $key, $value, array $attributes)
{
if ($value instanceof QuickbooksSettings) {
- return json_encode(get_object_vars($value));
+ return json_encode($value->toArray());
}
return null;
- // return json_encode($value);
}
}
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/DataMapper/IncomeAccountMap.php b/app/DataMapper/IncomeAccountMap.php
new file mode 100644
index 0000000000..c1cb100424
--- /dev/null
+++ b/app/DataMapper/IncomeAccountMap.php
@@ -0,0 +1,125 @@
+physical = $attributes['physical'] ?? null;
+ $this->service = $attributes['service'] ?? null;
+ $this->digital = $attributes['digital'] ?? null;
+ $this->shipping = $attributes['shipping'] ?? null;
+ $this->exempt = $attributes['exempt'] ?? null;
+ $this->reduced_tax = $attributes['reduced_tax'] ?? null;
+ $this->override_tax = $attributes['override_tax'] ?? null;
+ $this->zero_rated = $attributes['zero_rated'] ?? null;
+ $this->reverse_tax = $attributes['reverse_tax'] ?? null;
+ $this->intra_community = $attributes['intra_community'] ?? null;
+ }
+
+
+ /**
+ * getAccountId
+ *
+ * Gets the Quickbooks Income Account ID for a given product tax_id.
+ * @param string $product_tax_id
+ * @return string|null
+ */
+ public function getAccountId(?string $product_tax_id): ?string
+ {
+ /**
+ * @var string|null $prop
+ *
+ * Translates "2" => "service"
+ *
+ * */
+ $prop = $this->getPropertyName($product_tax_id);
+
+ return $prop ? $this->{$prop} : null;
+ }
+
+ /**
+ * getPropertyName
+ *
+ * Tranlates the $item->tax_id => property name.
+ *
+ * Gets the property name for a given product tax_id.
+ * @param int|string $key
+ * @return string|null
+ */
+ private function getPropertyName(int|string $key): ?string
+ {
+ return match ((string)$key) {
+ (string)Product::PRODUCT_TYPE_PHYSICAL => 'physical',
+ (string)Product::PRODUCT_TYPE_SERVICE => 'service',
+ (string)Product::PRODUCT_TYPE_DIGITAL => 'digital',
+ (string)Product::PRODUCT_TYPE_SHIPPING => 'shipping',
+ (string)Product::PRODUCT_TYPE_EXEMPT => 'exempt',
+ (string)Product::PRODUCT_TYPE_REDUCED_TAX => 'reduced_tax',
+ (string)Product::PRODUCT_TYPE_OVERRIDE_TAX => 'override_tax',
+ (string)Product::PRODUCT_TYPE_ZERO_RATED => 'zero_rated',
+ (string)Product::PRODUCT_TYPE_REVERSE_TAX => 'reverse_tax',
+ (string)Product::PRODUCT_INTRA_COMMUNITY => 'intra_community',
+ default => null,
+ };
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'physical' => $this->physical,
+ 'service' => $this->service,
+ 'digital' => $this->digital,
+ 'shipping' => $this->shipping,
+ 'exempt' => $this->exempt,
+ 'reduced_tax' => $this->reduced_tax,
+ 'override_tax' => $this->override_tax,
+ 'zero_rated' => $this->zero_rated,
+ 'reverse_tax' => $this->reverse_tax,
+ 'intra_community' => $this->intra_community,
+ ];
+ }
+}
diff --git a/app/DataMapper/QuickbooksSettings.php b/app/DataMapper/QuickbooksSettings.php
index 4b31efa630..b5ca2a6976 100644
--- a/app/DataMapper/QuickbooksSettings.php
+++ b/app/DataMapper/QuickbooksSettings.php
@@ -32,6 +32,8 @@ class QuickbooksSettings implements Castable
public string $baseURL;
+ public string $companyName;
+
public QuickbooksSync $settings;
public function __construct(array $attributes = [])
@@ -42,6 +44,7 @@ class QuickbooksSettings implements Castable
$this->accessTokenExpiresAt = $attributes['accessTokenExpiresAt'] ?? 0;
$this->refreshTokenExpiresAt = $attributes['refreshTokenExpiresAt'] ?? 0;
$this->baseURL = $attributes['baseURL'] ?? '';
+ $this->companyName = $attributes['companyName'] ?? '';
$this->settings = new QuickbooksSync($attributes['settings'] ?? []);
}
@@ -55,4 +58,43 @@ class QuickbooksSettings implements Castable
return new self($data);
}
+ public function toArray(): array
+ {
+ return [
+ 'accessTokenKey' => $this->accessTokenKey,
+ 'refresh_token' => $this->refresh_token,
+ 'realmID' => $this->realmID,
+ 'accessTokenExpiresAt' => $this->accessTokenExpiresAt,
+ 'refreshTokenExpiresAt' => $this->refreshTokenExpiresAt,
+ 'baseURL' => $this->baseURL,
+ 'companyName' => $this->companyName,
+ 'settings' => $this->settings->toArray(),
+ ];
+ }
+
+ /**
+ * Check if this QuickbooksSettings instance represents actual data or is just a default empty object.
+ *
+ * @return bool True if this has actual QuickBooks connection data, false if it's just defaults
+ */
+ public function isConfigured(): bool
+ {
+ // If accessTokenKey is set, we have a connection
+ return !empty($this->accessTokenKey);
+ }
+
+ /**
+ * Check if this QuickbooksSettings instance is empty (default values only).
+ *
+ * @return bool True if this is an empty/default instance
+ */
+ public function isEmpty(): bool
+ {
+ return empty($this->accessTokenKey)
+ && empty($this->refresh_token)
+ && empty($this->realmID)
+ && $this->accessTokenExpiresAt === 0
+ && $this->refreshTokenExpiresAt === 0
+ && empty($this->baseURL);
+ }
}
diff --git a/app/DataMapper/QuickbooksSync.php b/app/DataMapper/QuickbooksSync.php
index ec8666dbc3..ed9bc8424c 100644
--- a/app/DataMapper/QuickbooksSync.php
+++ b/app/DataMapper/QuickbooksSync.php
@@ -12,8 +12,15 @@
namespace App\DataMapper;
+use App\Models\Product;
+
/**
* QuickbooksSync.
+ *
+ * Product type to income account mapping:
+ * Keys are Product::PRODUCT_TYPE_* constants (int). Values are QuickBooks account IDs (string|null).
+ * Example: [Product::PRODUCT_TYPE_SERVICE => '123', Product::PRODUCT_TYPE_PHYSICAL => '456']
+ * Null values indicate the account has not been configured for that product type.
*/
class QuickbooksSync
{
@@ -35,9 +42,11 @@ class QuickbooksSync
public QuickbooksSyncMap $expense;
- public string $default_income_account = '';
-
- public string $default_expense_account = '';
+ /**
+ * QuickBooks income account ID per product type.
+ * Use getAccountId(int $productTypeId) or the typed properties (physical, service, etc.).
+ */
+ public IncomeAccountMap $income_account_map;
public function __construct(array $attributes = [])
{
@@ -50,7 +59,23 @@ class QuickbooksSync
$this->product = new QuickbooksSyncMap($attributes['product'] ?? []);
$this->payment = new QuickbooksSyncMap($attributes['payment'] ?? []);
$this->expense = new QuickbooksSyncMap($attributes['expense'] ?? []);
- $this->default_income_account = $attributes['default_income_account'] ?? '';
- $this->default_expense_account = $attributes['default_expense_account'] ?? '';
+ $this->income_account_map = new IncomeAccountMap($attributes['income_account_map'] ?? []);
+
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'client' => $this->client->toArray(),
+ 'vendor' => $this->vendor->toArray(),
+ 'invoice' => $this->invoice->toArray(),
+ 'sales' => $this->sales->toArray(),
+ 'quote' => $this->quote->toArray(),
+ 'purchase_order' => $this->purchase_order->toArray(),
+ 'product' => $this->product->toArray(),
+ 'payment' => $this->payment->toArray(),
+ 'expense' => $this->expense->toArray(),
+ 'income_account_map' => $this->income_account_map->toArray(),
+ ];
}
}
diff --git a/app/DataMapper/QuickbooksSyncMap.php b/app/DataMapper/QuickbooksSyncMap.php
index 4ad3613eef..897f6e2bf2 100644
--- a/app/DataMapper/QuickbooksSyncMap.php
+++ b/app/DataMapper/QuickbooksSyncMap.php
@@ -19,13 +19,24 @@ use App\Enum\SyncDirection;
*/
class QuickbooksSyncMap
{
- public SyncDirection $direction = SyncDirection::BIDIRECTIONAL;
+ public SyncDirection $direction = SyncDirection::NONE;
public function __construct(array $attributes = [])
{
$this->direction = isset($attributes['direction'])
? SyncDirection::from($attributes['direction'])
- : SyncDirection::BIDIRECTIONAL;
+ : SyncDirection::NONE;
+ }
+ public function toArray(): array
+ {
+ // Ensure direction is always returned as a string value, not the enum object
+ $directionValue = $this->direction instanceof \App\Enum\SyncDirection
+ ? $this->direction->value
+ : (string) $this->direction;
+
+ return [
+ 'direction' => $directionValue,
+ ];
}
}
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/Enum/SyncDirection.php b/app/Enum/SyncDirection.php
index a36d92bf5a..4d02aa0a3b 100644
--- a/app/Enum/SyncDirection.php
+++ b/app/Enum/SyncDirection.php
@@ -17,4 +17,5 @@ enum SyncDirection: string
case PUSH = 'push'; // only creates and updates records created by Invoice Ninja.
case PULL = 'pull'; // creates and updates record from QB.
case BIDIRECTIONAL = 'bidirectional'; // creates and updates records created by Invoice Ninja and from QB.
+ case NONE = 'none'; // no sync.
}
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/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/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;
}
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;
}
diff --git a/app/Export/CSV/RecurringInvoiceItemExport.php b/app/Export/CSV/RecurringInvoiceItemExport.php
index a397a81cea..9030a57244 100644
--- a/app/Export/CSV/RecurringInvoiceItemExport.php
+++ b/app/Export/CSV/RecurringInvoiceItemExport.php
@@ -65,7 +65,7 @@ class RecurringInvoiceItemExport extends BaseExport
if (count($this->input['report_keys']) == 0) {
$this->force_keys = true;
$this->input['report_keys'] = array_values($this->mergeItemsKeys('recurring_invoice_report_keys'));
- nlog($this->input['report_keys']);
+ // nlog($this->input['report_keys']);
}
$this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys']));
diff --git a/app/Factory/RecurringInvoiceToInvoiceFactory.php b/app/Factory/RecurringInvoiceToInvoiceFactory.php
index 835afeaa79..c3024f1cfe 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;
}
@@ -113,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/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php
index 8c9d5c11e1..935234fcd4 100644
--- a/app/Filters/InvoiceFilters.php
+++ b/app/Filters/InvoiceFilters.php
@@ -287,10 +287,50 @@ class InvoiceFilters extends QueryFilters
if ($sort_col[0] == 'client_id') {
- return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
- ->orderBy(\App\Models\Client::select('name')
- ->whereColumn('clients.id', 'invoices.client_id'), $dir);
+ // 2026-01-21: Original sort by client name that is not optimal when clients.name is empty.
+ // return $this->builder->orderByRaw('client_id IS NULL')
+ // ->orderBy(\App\Models\Client::select('name')
+ // ->whereColumn('clients.id', 'recurring_invoices.client_id')
+ // ->limit(1), $dir);
+
+
+/**
+ * future options for order by raw if this is not performant:
+ *
+ COALESCE(
+ NULLIF((SELECT name FROM clients WHERE clients.id = invoices.client_id LIMIT 1), ''),
+ (SELECT email FROM client_contacts
+ WHERE client_contacts.client_id = invoices.client_id
+ AND client_contacts.email IS NOT NULL
+ ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC
+ LIMIT 1),
+ 'No Contact Set'
+ ) " . $dir
+
+ */
+
+ return $this->builder
+ ->orderByRaw("
+ CASE
+ WHEN CHAR_LENGTH((SELECT name FROM clients WHERE clients.id = invoices.client_id LIMIT 1)) > 1
+ THEN (SELECT name FROM clients WHERE clients.id = invoices.client_id LIMIT 1)
+ WHEN CHAR_LENGTH(CONCAT(
+ COALESCE((SELECT first_name FROM client_contacts WHERE client_contacts.client_id = invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1), ''),
+ COALESCE((SELECT last_name FROM client_contacts WHERE client_contacts.client_id = invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1), '')
+ )) >= 1
+ THEN TRIM(CONCAT(
+ COALESCE((SELECT first_name FROM client_contacts WHERE client_contacts.client_id = invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1), ''),
+ ' ',
+ COALESCE((SELECT last_name FROM client_contacts WHERE client_contacts.client_id = invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1), '')
+ ))
+ WHEN CHAR_LENGTH((SELECT email FROM client_contacts WHERE client_contacts.client_id = invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1)) > 0
+ THEN (SELECT email FROM client_contacts WHERE client_contacts.client_id = invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1)
+ ELSE 'No Contact Set'
+ END " . $dir
+ );
+
+
}
if ($sort_col[0] == 'project_id') {
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/app/Filters/RecurringInvoiceFilters.php b/app/Filters/RecurringInvoiceFilters.php
index 9cbfe8231a..bb2de7921f 100644
--- a/app/Filters/RecurringInvoiceFilters.php
+++ b/app/Filters/RecurringInvoiceFilters.php
@@ -141,9 +141,35 @@ class RecurringInvoiceFilters extends QueryFilters
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'client_id') {
- return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
- ->orderBy(\App\Models\Client::select('name')
- ->whereColumn('clients.id', 'recurring_invoices.client_id'), $dir);
+
+
+ return $this->builder
+ ->orderByRaw("
+ CASE
+ WHEN CHAR_LENGTH((SELECT name FROM clients WHERE clients.id = recurring_invoices.client_id LIMIT 1)) > 1
+ THEN (SELECT name FROM clients WHERE clients.id = recurring_invoices.client_id LIMIT 1)
+ WHEN CHAR_LENGTH(CONCAT(
+ COALESCE((SELECT first_name FROM client_contacts WHERE client_contacts.client_id = recurring_invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1), ''),
+ COALESCE((SELECT last_name FROM client_contacts WHERE client_contacts.client_id = recurring_invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1), '')
+ )) >= 1
+ THEN TRIM(CONCAT(
+ COALESCE((SELECT first_name FROM client_contacts WHERE client_contacts.client_id = recurring_invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1), ''),
+ ' ',
+ COALESCE((SELECT last_name FROM client_contacts WHERE client_contacts.client_id = recurring_invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1), '')
+ ))
+ WHEN CHAR_LENGTH((SELECT email FROM client_contacts WHERE client_contacts.client_id = recurring_invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1)) > 0
+ THEN (SELECT email FROM client_contacts WHERE client_contacts.client_id = recurring_invoices.client_id AND client_contacts.email IS NOT NULL ORDER BY client_contacts.is_primary DESC, client_contacts.id ASC LIMIT 1)
+ ELSE 'No Contact Set'
+ END " . $dir
+ );
+
+
+
+ // return $this->builder->orderByRaw('client_id IS NULL')
+ // ->orderBy(\App\Models\Client::select('name')
+ // ->whereColumn('clients.id', 'recurring_invoices.client_id')
+ // ->limit(1), $dir);
+
}
if ($sort_col[0] == 'number') {
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/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/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/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..e1fba705a4 100644
--- a/app/Helpers/Invoice/Taxer.php
+++ b/app/Helpers/Invoice/Taxer.php
@@ -19,11 +19,22 @@ 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)
{
+ $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/Helpers/SwissQr/SwissQrGenerator.php b/app/Helpers/SwissQr/SwissQrGenerator.php
index 6f896e16a4..33b627d22a 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'
)
);
@@ -110,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);
@@ -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/app/Http/Controllers/ActivityController.php b/app/Http/Controllers/ActivityController.php
index a67f818d68..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);
}
@@ -132,6 +134,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;
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
index 4fca7fdfe6..442e123e6c 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -200,6 +200,40 @@ class LoginController extends BaseController
}
}
+ public function refreshReact(Request $request)
+ {
+ $truth = app()->make(TruthSource::class);
+
+ if ($truth->getCompanyToken()) {
+ $company_token = $truth->getCompanyToken();
+ } else {
+ $company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first();
+ }
+
+ $cu = CompanyUser::query()
+ ->where('user_id', $company_token->user_id);
+
+ if ($cu->count() == 0) {
+ return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400);
+ }
+
+ $cu->first()->account->companies->each(function ($company) use ($cu, $request) {
+ if ($company->tokens()->where('is_system', true)->count() == 0) {
+ (new CreateCompanyToken($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')))->handle();
+ }
+ });
+
+ if ($request->has('current_company') && $request->input('current_company') == 'true') {
+ $cu->where('company_id', $company_token->company_id);
+ }
+
+ if (Ninja::isHosted() && !$cu->first()->is_owner && !$cu->first()->user->account->isEnterprisePaidClient()) {
+ return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
+ }
+
+ return $this->refreshReactResponse($cu);
+ }
+
/**
* Refreshes the data feed with the current Company User.
*
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'];
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/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php
index 3f71ca67fe..0cf9c3cf77 100644
--- a/app/Http/Controllers/BaseController.php
+++ b/app/Http/Controllers/BaseController.php
@@ -297,6 +297,44 @@ class BaseController extends Controller
return response()->make($error, $httpErrorCode, $headers);
}
+ /**
+ * Heavily reduced refresh query to reduce DB burden
+ *
+ * @param Builder $query
+ * @return Response| \Illuminate\Http\JsonResponse
+ */
+ protected function refreshReactResponse($query)
+ {
+ $this->manager->parseIncludes([
+ 'account',
+ 'user.company_user',
+ 'token',
+ 'company.tax_rates',
+ ]);
+
+ $this->serializer = request()->input('serializer') ?: EntityTransformer::API_SERIALIZER_ARRAY;
+
+ if ($this->serializer === EntityTransformer::API_SERIALIZER_JSON) {
+ $this->manager->setSerializer(new JsonApiSerializer());
+ } else {
+ $this->manager->setSerializer(new ArraySerializer());
+ }
+
+ $transformer = new $this->entity_transformer($this->serializer);
+
+ $limit = $this->resolveQueryLimit();
+
+ $paginator = $query->paginate($limit);
+
+ /** @phpstan-ignore-next-line */
+ $query = $paginator->getCollection(); // @phpstan-ignore-line
+
+ $resource = new Collection($query, $transformer, $this->entity_type);
+
+ $resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
+
+ return $this->response($this->manager->createData($resource)->toArray());
+ }
/**
* Refresh API response with latest cahnges
*
@@ -681,7 +719,10 @@ class BaseController extends Controller
// Set created_at to current time to filter out all existing related records
// (designs, documents, groups, etc.) for a minimal response payload
request()->merge(['created_at' => time()]);
- return $this->miniLoadResponse($query);
+
+ //2026-01-23: Improve Login Performance for react.
+ return $this->refreshReactResponse($query);
+ // return $this->miniLoadResponse($query);
}
elseif ($user->getCompany()->is_large) {
$this->manager->parseIncludes($this->mini_load);
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/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/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/Http/Controllers/EInvoiceController.php b/app/Http/Controllers/EInvoiceController.php
index c6c8044f9a..a94be10ad0 100644
--- a/app/Http/Controllers/EInvoiceController.php
+++ b/app/Http/Controllers/EInvoiceController.php
@@ -42,12 +42,29 @@ class EInvoiceController extends BaseController
*/
public function validateEntity(ValidateEInvoiceRequest $request)
{
+
+ $user = auth()->user();
+
+ if(!in_array($user->company()->settings->e_invoice_type, ['VERIFACTU', 'PEPPOL'])) {
+
+ $data = [
+ 'passes' => true,
+ 'invoices' => [],
+ 'recurring_invoices' => [],
+ 'clients' => [],
+ 'companies' => [],
+ ];
+
+ return response()->json($data, 200);
+ }
+
$el = $request->getValidatorClass();
$data = [];
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,
@@ -87,7 +104,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/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/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php
index 00110b8d13..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();
@@ -456,7 +455,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'))) {
diff --git a/app/Http/Controllers/ImportQuickbooksController.php b/app/Http/Controllers/ImportQuickbooksController.php
index 295e5d6a3b..17048d351b 100644
--- a/app/Http/Controllers/ImportQuickbooksController.php
+++ b/app/Http/Controllers/ImportQuickbooksController.php
@@ -20,39 +20,72 @@ use App\Services\Quickbooks\QuickbooksService;
class ImportQuickbooksController extends BaseController
{
+
/**
- * Determine if the user is authorized to make this request.
+ * authorizeQuickbooks
*
+ * Starts the Quickbooks authorization process.
+ *
+ * @param AuthQuickbooksRequest $request
+ * @param string $token
+ * @return \Illuminate\Http\RedirectResponse
*/
public function authorizeQuickbooks(AuthQuickbooksRequest $request, string $token)
{
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
+
$company = $request->getCompany();
+
$qb = new QuickbooksService($company);
$authorizationUrl = $qb->sdk()->getAuthorizationUrl();
- nlog($authorizationUrl);
-
$state = $qb->sdk()->getState();
Cache::put($state, $token, 190);
return redirect()->to($authorizationUrl);
}
-
+
+ /**
+ * onAuthorized
+ *
+ * Handles the callback from Quickbooks after authorization.
+ *
+ * @param AuthorizedQuickbooksRequest $request
+ * @return \Illuminate\Http\RedirectResponse
+ */
public function onAuthorized(AuthorizedQuickbooksRequest $request)
{
+ nlog($request->all());
+
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = $request->getCompany();
+
$qb = new QuickbooksService($company);
$realm = $request->query('realmId');
+
+ nlog($realm);
+
$access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm);
+
+ nlog($access_token_object);
+
$qb->sdk()->saveOAuthToken($access_token_object);
+ // Refresh the service to initialize SDK with the new access token
+ $qb->refresh();
+
+ $companyInfo = $qb->sdk()->company();
+
+ $company->quickbooks->companyName = $companyInfo->CompanyName;
+ $company->save();
+
+ nlog($companyInfo);
+
return redirect(config('ninja.react_url'));
}
diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php
index 6277825cae..25a069e96c 100644
--- a/app/Http/Controllers/ProjectController.php
+++ b/app/Http/Controllers/ProjectController.php
@@ -607,7 +607,7 @@ class ProjectController extends BaseController
$this->entity_transformer = InvoiceTransformer::class;
$this->entity_type = Invoice::class;
- $invoice = $this->project_repo->invoice($project);
+ $invoice = $this->project_repo->invoice(collect([$project]));
return $this->itemResponse($invoice);
}
diff --git a/app/Http/Controllers/QuickbooksController.php b/app/Http/Controllers/QuickbooksController.php
new file mode 100644
index 0000000000..5ffffba2e6
--- /dev/null
+++ b/app/Http/Controllers/QuickbooksController.php
@@ -0,0 +1,66 @@
+noContent();
+ }
+
+ public function configuration(ConfigQuickbooksRequest $request)
+ {
+
+ $user = auth()->user();
+ $company = $user->company();
+
+ $quickbooks = $company->quickbooks;
+ $quickbooks->settings->client->direction = $request->clients ? SyncDirection::PUSH : SyncDirection::NONE;
+ $quickbooks->settings->vendor->direction = $request->vendors ? SyncDirection::PUSH : SyncDirection::NONE;
+ $quickbooks->settings->product->direction = $request->products ? SyncDirection::PUSH : SyncDirection::NONE;
+ $quickbooks->settings->invoice->direction = $request->invoices ? SyncDirection::PUSH : SyncDirection::NONE;
+ $quickbooks->settings->quote->direction = $request->quotes ? SyncDirection::PUSH : SyncDirection::NONE;
+ $quickbooks->settings->payment->direction = $request->payments ? SyncDirection::PUSH : SyncDirection::NONE;
+ $company->quickbooks = $quickbooks;
+ $company->save();
+
+ return response()->noContent();
+ }
+
+ public function disconnect(DisconnectQuickbooksRequest $request)
+ {
+
+ $user = auth()->user();
+ $company = $user->company();
+
+ $qb = new QuickbooksService($company);
+ $rs = $qb->sdk()->revokeAccessToken();
+
+ nlog($rs);
+
+ $company->quickbooks = null;
+ $company->save();
+
+ return response()->noContent();
+ }
+}
\ No newline at end of file
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;
diff --git a/app/Http/Controllers/SetupController.php b/app/Http/Controllers/SetupController.php
index 10c3a258e9..7d1423236b 100644
--- a/app/Http/Controllers/SetupController.php
+++ b/app/Http/Controllers/SetupController.php
@@ -185,7 +185,7 @@ class SetupController extends Controller
try {
$status = SystemHealth::dbCheck($request);
- if (is_array($status) && $status['success'] === true) {
+ if (is_array($status) && $status['success'] === true && \App\Models\Account::count() > 0) {
return response([], 200);
}
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/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php
index e9e5fa9a1f..b411398653 100644
--- a/app/Http/Controllers/WebhookController.php
+++ b/app/Http/Controllers/WebhookController.php
@@ -303,7 +303,6 @@ class WebhookController extends BaseController
$webhook = WebhookFactory::create($user->company()->id, $user->id);
$webhook->fill($request->all());
- $webhook->save();
return $this->itemResponse($webhook);
}
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.
*
diff --git a/app/Http/Requests/Chart/ShowChartRequest.php b/app/Http/Requests/Chart/ShowChartRequest.php
index 7b62f4ae88..f813648f93 100644
--- a/app/Http/Requests/Chart/ShowChartRequest.php
+++ b/app/Http/Requests/Chart/ShowChartRequest.php
@@ -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/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/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php
index 3e69e01d78..22baaf0043 100644
--- a/app/Http/Requests/Company/UpdateCompanyRequest.php
+++ b/app/Http/Requests/Company/UpdateCompanyRequest.php
@@ -130,15 +130,26 @@ class UpdateCompanyRequest extends Request
$input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/");
}
- // /** Disabled on the hosted platform */
- // if (isset($input['expense_mailbox']) && Ninja::isHosted() && !($this->company->account->isPaid() && $this->company->account->plan == 'enterprise')) {
- // unset($input['expense_mailbox']);
- // }
+
if (isset($input['settings'])) {
$input['settings'] = (array) $this->filterSaveableSettings($input['settings']);
}
+ /**
+ * @var \App\Models\User $user
+ */
+ $user = auth()->user();
+
+ /**
+ * @var \App\Models\Company $company
+ */
+ $company = $user->company();
+
+ if(isset($company->legal_entity_id) && intval($company->legal_entity_id) > 0){
+ $input['settings']['e_invoice_type'] = 'PEPPOL';
+ }
+
if (isset($input['subdomain']) && $this->company->subdomain == $input['subdomain']) {
unset($input['subdomain']);
}
@@ -183,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);
}
diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php
index b22a9e9615..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,6 +82,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 ValidCreditScheme()];
+
return $rules;
}
@@ -94,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 6a923a442e..6ced45c966 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) {
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/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/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/app/Http/Requests/Preview/PreviewInvoiceRequest.php b/app/Http/Requests/Preview/PreviewInvoiceRequest.php
index bd0b0e3f0f..b260cfa335 100644
--- a/app/Http/Requests/Preview/PreviewInvoiceRequest.php
+++ b/app/Http/Requests/Preview/PreviewInvoiceRequest.php
@@ -118,7 +118,7 @@ class PreviewInvoiceRequest extends Request
};
if ($invitation) {
- nlog($invitation->toArray());
+ // nlog($invitation->toArray());
return $invitation;
}
diff --git a/app/Http/Requests/Quickbooks/ConfigQuickbooksRequest.php b/app/Http/Requests/Quickbooks/ConfigQuickbooksRequest.php
new file mode 100644
index 0000000000..fa6a95c582
--- /dev/null
+++ b/app/Http/Requests/Quickbooks/ConfigQuickbooksRequest.php
@@ -0,0 +1,49 @@
+user()->isAdmin();
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ *
+ * @return array
+ */
+ public function rules(): array
+ {
+ return [
+ 'invoices' => 'required|boolean|bail',
+ 'quotes' => 'required|boolean|bail',
+ 'payments' => 'required|boolean|bail',
+ 'products' => 'required|boolean|bail',
+ 'vendors' => 'required|boolean|bail',
+ 'clients' => 'required|boolean|bail',
+ ];
+ }
+
+}
diff --git a/app/Http/Requests/Quickbooks/DisconnectQuickbooksRequest.php b/app/Http/Requests/Quickbooks/DisconnectQuickbooksRequest.php
new file mode 100644
index 0000000000..f5381b10d3
--- /dev/null
+++ b/app/Http/Requests/Quickbooks/DisconnectQuickbooksRequest.php
@@ -0,0 +1,44 @@
+user()->isAdmin();
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ *
+ * @return array
+ */
+ public function rules(): array
+ {
+ return [
+ //
+ ];
+ }
+
+}
diff --git a/app/Http/Requests/Quickbooks/SyncQuickbooksRequest.php b/app/Http/Requests/Quickbooks/SyncQuickbooksRequest.php
new file mode 100644
index 0000000000..2ed705a57b
--- /dev/null
+++ b/app/Http/Requests/Quickbooks/SyncQuickbooksRequest.php
@@ -0,0 +1,49 @@
+user()->isAdmin();
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ *
+ * @return array
+ */
+ public function rules(): array
+ {
+ return [
+ 'clients' => 'required_with:invoices,quotes,payments|in:email,name,always_create',
+ 'products' => 'sometimes|in:product_key,always_create',
+ 'invoices' => 'sometimes|in:number,always_create',
+ 'quotes' => 'sometimes|in:number,always_create',
+ 'payments' => 'sometimes|in:always_create',
+ 'vendors' => 'sometimes|in:email,name,always_create',
+ ];
+ }
+
+}
diff --git a/app/Http/Requests/Setup/CheckDatabaseRequest.php b/app/Http/Requests/Setup/CheckDatabaseRequest.php
index b79bae20ed..b733a651d0 100644
--- a/app/Http/Requests/Setup/CheckDatabaseRequest.php
+++ b/app/Http/Requests/Setup/CheckDatabaseRequest.php
@@ -13,6 +13,7 @@
namespace App\Http\Requests\Setup;
use App\Http\Requests\Request;
+use Illuminate\Support\Facades\Schema;
class CheckDatabaseRequest extends Request
{
@@ -23,7 +24,16 @@ class CheckDatabaseRequest extends Request
*/
public function authorize()
{
- return true; /* Return something that will check if setup has been completed, like Ninja::hasCompletedSetup() */
+ if (!\App\Utils\Ninja::isSelfHost()) {
+ return false;
+ }
+
+ try {
+ return !Schema::hasTable('accounts') || \App\Models\Account::count() == 0;
+ } catch (\Throwable $e) {
+ // If database connection fails, allow the request (we're checking the DB)
+ return true;
+ }
}
/**
diff --git a/app/Http/Requests/Setup/CheckMailRequest.php b/app/Http/Requests/Setup/CheckMailRequest.php
index 603e331132..384711eaf7 100644
--- a/app/Http/Requests/Setup/CheckMailRequest.php
+++ b/app/Http/Requests/Setup/CheckMailRequest.php
@@ -13,6 +13,7 @@
namespace App\Http\Requests\Setup;
use App\Http\Requests\Request;
+use Illuminate\Support\Facades\Schema;
class CheckMailRequest extends Request
{
@@ -23,7 +24,16 @@ class CheckMailRequest extends Request
*/
public function authorize()
{
- return true; /* Return something that will check if setup has been completed, like Ninja::hasCompletedSetup() */
+ if (!\App\Utils\Ninja::isSelfHost()) {
+ return false;
+ }
+
+ try {
+ return !Schema::hasTable('accounts') || \App\Models\Account::count() == 0;
+ } catch (\Throwable $e) {
+ // If database connection fails, allow the request (we're checking the DB)
+ return true;
+ }
}
/**
diff --git a/app/Http/Requests/Subscription/StoreSubscriptionRequest.php b/app/Http/Requests/Subscription/StoreSubscriptionRequest.php
index 0e5f6b4950..ea5cc2af6a 100644
--- a/app/Http/Requests/Subscription/StoreSubscriptionRequest.php
+++ b/app/Http/Requests/Subscription/StoreSubscriptionRequest.php
@@ -60,7 +60,7 @@ class StoreSubscriptionRequest extends Request
'refund_period' => 'bail|sometimes|numeric',
'webhook_configuration' => 'bail|array',
'webhook_configuration.post_purchase_url' => 'bail|sometimes|nullable|string',
- 'webhook_configuration.post_purchase_rest_method' => 'bail|sometimes|nullable|string',
+ 'webhook_configuration.post_purchase_rest_method' => 'bail|sometimes|nullable|in:post,put',
'webhook_configuration.post_purchase_headers' => 'bail|sometimes|array',
'registration_required' => 'bail|sometimes|bool',
'optional_recurring_product_ids' => 'bail|sometimes|nullable|string',
@@ -72,6 +72,68 @@ class StoreSubscriptionRequest extends Request
return $this->globalRules($rules);
}
+ /**
+ * @param \Illuminate\Validation\Validator $validator
+ * @return void
+ */
+ public function withValidator(\Illuminate\Validation\Validator $validator): void
+ {
+ $validator->after(function ($validator) {
+ $this->validateWebhookUrl($validator, 'webhook_configuration.post_purchase_url');
+ });
+ }
+
+ /**
+ * Validate that a URL doesn't point to internal/private IP addresses.
+ *
+ * @param \Illuminate\Validation\Validator $validator
+ * @param string $field
+ * @return void
+ */
+ private function validateWebhookUrl(\Illuminate\Validation\Validator $validator, string $field): void
+ {
+ $url = $this->input($field);
+
+ if (empty($url)) {
+ return;
+ }
+
+ // Validate URL format
+ if (!filter_var($url, FILTER_VALIDATE_URL)) {
+ $validator->errors()->add($field, ctrans('texts.invalid_url'));
+ return;
+ }
+
+ $parsed = parse_url($url);
+
+ // Only allow http/https protocols
+ $scheme = $parsed['scheme'] ?? '';
+ if (!in_array(strtolower($scheme), ['http', 'https'])) {
+ $validator->errors()->add($field, ctrans('texts.invalid_url'));
+ return;
+ }
+
+ $host = $parsed['host'] ?? '';
+ if (empty($host)) {
+ $validator->errors()->add($field, ctrans('texts.invalid_url'));
+ return;
+ }
+
+ // Resolve hostname to IP and check for private/reserved ranges
+ $ip = gethostbyname($host);
+
+ // gethostbyname returns the hostname if resolution fails
+ if ($ip === $host && !filter_var($host, FILTER_VALIDATE_IP)) {
+ // DNS resolution failed - allow it (external DNS might resolve differently)
+ return;
+ }
+
+ // Block private and reserved IP ranges (SSRF protection)
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
+ $validator->errors()->add($field, ctrans('texts.invalid_url'));
+ }
+ }
+
public function prepareForValidation()
{
$input = $this->all();
diff --git a/app/Http/Requests/Subscription/UpdateSubscriptionRequest.php b/app/Http/Requests/Subscription/UpdateSubscriptionRequest.php
index df03ec7701..5c7a21d901 100644
--- a/app/Http/Requests/Subscription/UpdateSubscriptionRequest.php
+++ b/app/Http/Requests/Subscription/UpdateSubscriptionRequest.php
@@ -61,7 +61,7 @@ class UpdateSubscriptionRequest extends Request
'refund_period' => 'bail|sometimes|numeric',
'webhook_configuration' => 'bail|array',
'webhook_configuration.post_purchase_url' => 'bail|sometimes|nullable|string',
- 'webhook_configuration.post_purchase_rest_method' => 'bail|sometimes|nullable|string',
+ 'webhook_configuration.post_purchase_rest_method' => 'bail|sometimes|nullable|in:post,put',
'webhook_configuration.post_purchase_headers' => 'bail|sometimes|array',
'registration_required' => 'bail|sometimes|bool',
'optional_recurring_product_ids' => 'bail|sometimes|nullable|string',
@@ -73,6 +73,69 @@ class UpdateSubscriptionRequest extends Request
return $this->globalRules($rules);
}
+
+ /**
+ * @param \Illuminate\Validation\Validator $validator
+ * @return void
+ */
+ public function withValidator(\Illuminate\Validation\Validator $validator): void
+ {
+ $validator->after(function ($validator) {
+ $this->validateWebhookUrl($validator, 'webhook_configuration.post_purchase_url');
+ });
+ }
+
+ /**
+ * Validate that a URL doesn't point to internal/private IP addresses.
+ *
+ * @param \Illuminate\Validation\Validator $validator
+ * @param string $field
+ * @return void
+ */
+ private function validateWebhookUrl(\Illuminate\Validation\Validator $validator, string $field): void
+ {
+ $url = $this->input($field);
+
+ if (empty($url)) {
+ return;
+ }
+
+ // Validate URL format
+ if (!filter_var($url, FILTER_VALIDATE_URL)) {
+ $validator->errors()->add($field, ctrans('texts.invalid_url'));
+ return;
+ }
+
+ $parsed = parse_url($url);
+
+ // Only allow http/https protocols
+ $scheme = $parsed['scheme'] ?? '';
+ if (!in_array(strtolower($scheme), ['http', 'https'])) {
+ $validator->errors()->add($field, ctrans('texts.invalid_url'));
+ return;
+ }
+
+ $host = $parsed['host'] ?? '';
+ if (empty($host)) {
+ $validator->errors()->add($field, ctrans('texts.invalid_url'));
+ return;
+ }
+
+ // Resolve hostname to IP and check for private/reserved ranges
+ $ip = gethostbyname($host);
+
+ // gethostbyname returns the hostname if resolution fails
+ if ($ip === $host && !filter_var($host, FILTER_VALIDATE_IP)) {
+ // DNS resolution failed - allow it (external DNS might resolve differently)
+ return;
+ }
+
+ // Block private and reserved IP ranges (SSRF protection)
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
+ $validator->errors()->add($field, ctrans('texts.invalid_url'));
+ }
+ }
+
public function prepareForValidation()
{
$input = $this->all();
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']);
}
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/Http/Requests/User/UpdateUserRequest.php b/app/Http/Requests/User/UpdateUserRequest.php
index 2f89230c9b..ec049fe1c9 100644
--- a/app/Http/Requests/User/UpdateUserRequest.php
+++ b/app/Http/Requests/User/UpdateUserRequest.php
@@ -56,6 +56,8 @@ class UpdateUserRequest extends Request
$input['email'] = trim($input['email']);
} elseif (isset($input['email'])) {
$input['email'] = false;
+ } else {
+ $input['email'] = $this->user->email;
}
if (array_key_exists('first_name', $input)) {
diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php
index 88a16cc8f4..0041063908 100644
--- a/app/Http/ValidationRules/Account/BlackListRule.php
+++ b/app/Http/ValidationRules/Account/BlackListRule.php
@@ -22,6 +22,13 @@ class BlackListRule implements ValidationRule
{
/** Bad domains +/- disposable email domains */
private array $blacklist = [
+ "usdtbeta.com",
+ "asurad.com",
+ "isb.nu.edu.pk",
+ "edux3.us",
+ "bwmyga.com",
+ "asurad.com",
+ "comfythings.com",
"edu.pk",
"bablace.com",
"moonfee.com",
diff --git a/app/Http/ValidationRules/EInvoice/ValidCreditScheme.php b/app/Http/ValidationRules/EInvoice/ValidCreditScheme.php
new file mode 100644
index 0000000000..fa87b6e130
--- /dev/null
+++ b/app/Http/ValidationRules/EInvoice/ValidCreditScheme.php
@@ -0,0 +1,106 @@
+validateRequest($value['CreditNote'], CreditLevel::class);
+
+ foreach ($errors as $key => $msg) {
+
+ $this->validator->errors()->add(
+ "e_invoice.{$key}",
+ "{$key} - {$msg}"
+ );
+
+ }
+
+ if (data_get($value, 'CreditNote.BillingReference.0.InvoiceDocumentReference.ID') === null ||
+ data_get($value, 'CreditNote.BillingReference.0.InvoiceDocumentReference.ID') === '') {
+
+ $this->validator->errors()->add(
+ "e_invoice.BillingReference.0.InvoiceDocumentReference.ID",
+ "Invoice Reference/Number is required"
+ );
+
+ }
+
+ if (isset($value['CreditNote']['BillingReference'][0]['InvoiceDocumentReference']['IssueDate']) && strlen($value['CreditNote']['BillingReference'][0]['InvoiceDocumentReference']['IssueDate']) > 1 && !$this->isValidDateSyntax($value['CreditNote']['BillingReference'][0]['InvoiceDocumentReference']['IssueDate'])) {
+
+ $this->validator->errors()->add(
+ "e_invoice.BillingReference.0.InvoiceDocumentReference.IssueDate",
+ "Invoice Issue Date is required"
+ );
+
+ }
+
+
+ }
+
+ }
+
+ private function isValidDateSyntax(string $date_string): bool
+ {
+ // Strict format validation: must be exactly Y-m-d
+ $date = \DateTime::createFromFormat('Y-m-d', $date_string);
+
+ if ($date === false) {
+ return false;
+ }
+
+ // Ensure the formatted date matches the input (catches overflow)
+ return $date->format('Y-m-d') === $date_string;
+ }
+ /**
+ * Set the current validator.
+ */
+ public function setValidator(Validator $validator): static
+ {
+ $this->validator = $validator;
+
+ return $this;
+ }
+
+
+}
diff --git a/app/Http/ValidationRules/Payment/ValidInvoicesRules.php b/app/Http/ValidationRules/Payment/ValidInvoicesRules.php
index a2920af1cf..498ebdd4d8 100644
--- a/app/Http/ValidationRules/Payment/ValidInvoicesRules.php
+++ b/app/Http/ValidationRules/Payment/ValidInvoicesRules.php
@@ -88,6 +88,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;
diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php
index 913ac1fa3a..759268a2bf 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,39 @@ class BaseImport
$nmo->to_user = $this->company->owner();
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,
+ ];
+
+ $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)) {
+ 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/Import/Providers/QBBackup.php b/app/Import/Providers/QBBackup.php
index d35f5b7cf5..d75b202294 100644
--- a/app/Import/Providers/QBBackup.php
+++ b/app/Import/Providers/QBBackup.php
@@ -4,6 +4,7 @@ namespace App\Import\Providers;
use App\Models\Company;
use App\Models\Invoice;
+use App\Import\Providers\BaseImport;
use Illuminate\Support\Facades\Cache;
use App\Services\Quickbooks\QuickbooksService;
use App\Services\Quickbooks\Transformers\ClientTransformer;
diff --git a/app/Import/Transformer/Freshbooks/ClientTransformer.php b/app/Import/Transformer/Freshbooks/ClientTransformer.php
index 9af37aac55..db41af8c32 100644
--- a/app/Import/Transformer/Freshbooks/ClientTransformer.php
+++ b/app/Import/Transformer/Freshbooks/ClientTransformer.php
@@ -32,11 +32,15 @@ class ClientTransformer extends BaseTransformer
throw new ImportException('Client already exists');
}
+ $address1 = data_get($data, 'Street', data_get($data, 'Address Line 1', ''));
+ $address2 = data_get($data, 'Address Line 2', '');
+
return [
'company_id' => $this->company->id,
'name' => $this->getString($data, 'Organization'),
'phone' => $this->getString($data, 'Phone'),
- 'address1' => $this->getString($data, 'Street'),
+ 'address1' => $address1,
+ 'address2' => $address2,
'city' => $this->getString($data, 'City'),
'state' => $this->getString($data, 'Province/State'),
'postal_code' => $this->getString($data, 'Postal Code'),
diff --git a/app/Import/Transformer/Freshbooks/InvoiceTransformer.php b/app/Import/Transformer/Freshbooks/InvoiceTransformer.php
index c58cf8271b..ab33159b62 100644
--- a/app/Import/Transformer/Freshbooks/InvoiceTransformer.php
+++ b/app/Import/Transformer/Freshbooks/InvoiceTransformer.php
@@ -85,11 +85,11 @@ class InvoiceTransformer extends BaseTransformer
return ($record[$field] / $record['Line Subtotal']) * 100;
}
- $tax_amount1 = isset($record['Tax 1 Amount']) ? $record['Tax 1 Amount'] : 0;
+ $tax_amount1 = isset($record['Tax 1 Amount']) ? floatval($record['Tax 1 Amount']) : 0;
- $tax_amount2 = isset($record['Tax 2 Amount']) ? $record['Tax 2 Amount'] : 0;
+ $tax_amount2 = isset($record['Tax 2 Amount']) ? floatval($record['Tax 2 Amount']) : 0;
- $line_total = isset($record['Line Total']) ? $record['Line Total'] : 0;
+ $line_total = isset($record['Line Total']) ? floatval($record['Line Total']) : 0;
$subtotal = $line_total - $tax_amount2 - $tax_amount1;
diff --git a/app/Import/Transformer/Wave/InvoiceTransformer.php b/app/Import/Transformer/Wave/InvoiceTransformer.php
index 63c2b88636..e625ef2d8f 100644
--- a/app/Import/Transformer/Wave/InvoiceTransformer.php
+++ b/app/Import/Transformer/Wave/InvoiceTransformer.php
@@ -28,7 +28,15 @@ class InvoiceTransformer extends BaseTransformer
*/
public function transform($line_items_data)
{
- $invoice_data = reset($line_items_data);
+ // Handle both array of arrays and single array scenarios
+ if (is_array($line_items_data) && isset($line_items_data[0]) && is_array($line_items_data[0])) {
+ // Array of arrays - take the first invoice
+ $invoice_data = reset($line_items_data);
+ } else {
+ // Single array - use as-is
+ $invoice_data = $line_items_data;
+ $line_items_data = [$line_items_data];
+ }
if ($this->hasInvoice($invoice_data['Invoice Number'])) {
throw new ImportException('Invoice number already exists');
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;
}
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;
}
diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php
index 816db622a8..6f02e392a7 100644
--- a/app/Jobs/Company/CompanyImport.php
+++ b/app/Jobs/Company/CompanyImport.php
@@ -1272,7 +1272,6 @@ class CompanyImport implements ShouldQueue
{
$activities = [];
-
$this->genericNewClassImport(
Activity::class,
[
@@ -1556,9 +1555,23 @@ class CompanyImport implements ShouldQueue
{
// foreach($this->backup_file->payments as $payment)
foreach ((object)$this->getObject("payments") as $payment) {
+
+
foreach ($payment->paymentables as $paymentable_obj) {
+
+
+try {
+ $ppid = $this->transformId('payments', $paymentable_obj->payment_id);
+
+} catch (\Exception $e) {
+ // nlog($e->getMessage());
+ nlog($paymentable_obj);
+ continue;
+}
+
+
$paymentable = new Paymentable();
- $paymentable->payment_id = $this->transformId('payments', $paymentable_obj->payment_id);
+ $paymentable->payment_id = $ppid;
$paymentable->paymentable_type = $paymentable_obj->paymentable_type;
$paymentable->amount = $paymentable_obj->amount;
$paymentable->refunded = $paymentable_obj->refunded;
@@ -1675,7 +1688,15 @@ class CompanyImport implements ShouldQueue
$key = $activity_invitation_key;
}
- $obj_array["{$value}"] = $this->transformId($key, $obj->{$value});
+ if($class == 'App\Models\Activity'){
+
+ if(isset($this->ids[$key][$obj->{$value}]))
+ $obj_array["{$value}"] = $this->ids[$key][$obj->{$value}];
+ }
+ else {
+ $obj_array["{$value}"] = $this->transformId($key, $obj->{$value});
+ }
+
}
}
@@ -1978,7 +1999,11 @@ class CompanyImport implements ShouldQueue
private function transformId(string $resource, ?string $old): ?int
{
- if (empty($old) || $old == 'WjnegYbwZ1') {
+ if (empty($old) || in_array($old, ['WjnegYbwZ1'])) {
+ return null;
+ }
+
+ if($resource == 'tasks' && in_array($old, ['WjnegnldwZ','kQBeX5layK','MVyb895dvA','OpnelpJeKB'])) {
return null;
}
diff --git a/app/Jobs/Cron/InvoiceTaxSummary.php b/app/Jobs/Cron/InvoiceTaxSummary.php
index e8eadf47de..58500424a4 100644
--- a/app/Jobs/Cron/InvoiceTaxSummary.php
+++ b/app/Jobs/Cron/InvoiceTaxSummary.php
@@ -44,6 +44,9 @@ class InvoiceTaxSummary implements ShouldQueue
public function handle()
{
+ nlog("InvoiceTaxSummary:: Starting job @ " . now()->toDateTimeString());
+ $start = now();
+
$currentUtcHour = now()->hour;
$transitioningTimezones = $this->getTransitioningTimezones($currentUtcHour);
@@ -56,6 +59,8 @@ class InvoiceTaxSummary implements ShouldQueue
$this->processCompanyTaxSummary($company);
}
}
+
+ nlog("InvoiceTaxSummary:: Job completed in " . now()->diffInSeconds($start) . " seconds");
}
private function getTransitioningTimezones($utcHour)
@@ -117,7 +122,11 @@ class InvoiceTaxSummary implements ShouldQueue
}
// Get companies that have timezone_id in their JSON settings matching the transitioning timezones
- return Company::whereRaw("JSON_EXTRACT(settings, '$.timezone_id') IN (" . implode(',', $timezoneIds) . ")")->get();
+ $companies = Company::whereRaw("JSON_EXTRACT(settings, '$.timezone_id') IN (" . implode(',', $timezoneIds) . ")")->get();
+
+ nlog("InvoiceTaxSummary:: Found " . $companies->count() . " companies in timezones: " . implode(',', $timezoneIds));
+
+ return $companies;
}
private function processCompanyTaxSummary($company)
diff --git a/app/Jobs/Invoice/InvoiceCheckOverdue.php b/app/Jobs/Invoice/InvoiceCheckOverdue.php
new file mode 100644
index 0000000000..695110c35b
--- /dev/null
+++ b/app/Jobs/Invoice/InvoiceCheckOverdue.php
@@ -0,0 +1,283 @@
+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;
+ }
+
+ // Yesterday's date in the company's timezone (Y-m-d format)
+ $yesterday = $now_in_company_tz->copy()->subDay()->format('Y-m-d');
+
+ $overdue_invoices = 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) {
+ // Case 1: Partial payment is overdue (partial > 0 and partial_due_date was yesterday)
+ $query->where(function ($q) use ($yesterday) {
+ $q->where('partial', '>', 0)
+ ->where('partial_due_date', $yesterday);
+ })
+ // Case 2: Full invoice is overdue (partial == 0 and due_date was yesterday)
+ ->orWhere(function ($q) use ($yesterday) {
+ $q->where(function ($subq) {
+ $subq->where('partial', '=', 0)
+ ->orWhereNull('partial');
+ })
+ ->where('due_date', $yesterday);
+ });
+ })
+ ->cursor()
+ ->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;
+ }
+
+ $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.
+ * @deprecated in favour of sendOverdueNotifications to send a summary email to all users
+ */
+ /** @phpstan-ignore-next-line */
+ 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/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php
index 4d93d3f298..d3ea65cc61 100644
--- a/app/Jobs/Mail/NinjaMailerJob.php
+++ b/app/Jobs/Mail/NinjaMailerJob.php
@@ -825,7 +825,7 @@ class NinjaMailerJob implements ShouldQueue
}
/* Ensure the user has a valid email address */
- if (!str_contains($this->nmo->to_user->email, "@")) {
+ if (!str_contains($this->nmo->to_user->email ?? '', "@")) {
return true;
}
diff --git a/app/Jobs/Ninja/MailWebhookSync.php b/app/Jobs/Ninja/MailWebhookSync.php
index 8884641009..e197127aa6 100644
--- a/app/Jobs/Ninja/MailWebhookSync.php
+++ b/app/Jobs/Ninja/MailWebhookSync.php
@@ -54,6 +54,7 @@ class MailWebhookSync implements ShouldQueue
*/
public function handle()
{
+
if (! Ninja::isHosted()) {
return;
}
@@ -131,7 +132,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/Jobs/Ninja/TaskScheduler.php b/app/Jobs/Ninja/TaskScheduler.php
index 960b531978..37a9908d9e 100644
--- a/app/Jobs/Ninja/TaskScheduler.php
+++ b/app/Jobs/Ninja/TaskScheduler.php
@@ -64,7 +64,12 @@ class TaskScheduler implements ShouldQueue
//@var \App\Models\Schedule $scheduler
$scheduler->service()->runTask();
} catch (\Throwable $e) {
+
nlog("Exception:: TaskScheduler:: Doing job :: {$scheduler->id} :: {$scheduler->name}" . $e->getMessage());
+
+ if (app()->bound('sentry')) {
+ app('sentry')->captureException($e);
+ }
}
});
diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php
index 38865d0e41..a404d03c29 100644
--- a/app/Jobs/PostMark/ProcessPostmarkWebhook.php
+++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php
@@ -62,6 +62,9 @@ class ProcessPostmarkWebhook implements ShouldQueue
*/
public function __construct(private array $request, private string $security_token)
{
+ if(\App\Utils\Ninja::isHosted()){
+ $this->onQueue('postmark');
+ }
}
private function getSystemLog(string $message_id): ?SystemLog
@@ -362,7 +365,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 +383,6 @@ class ProcessPostmarkWebhook implements ShouldQueue
$messageDetail = $this->getRawMessage($message_id);
-
$event = collect($messageDetail->messageevents)->first(function ($event) {
return $event?->Details?->BounceID ?? false;
@@ -423,7 +425,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();
@@ -443,12 +445,6 @@ class ProcessPostmarkWebhook implements ShouldQueue
}
}
- // public function middleware()
- // {
- // $key = $this->request['MessageID'] ?? '' . $this->request['Tag'] ?? '';
- // return [(new \Illuminate\Queue\Middleware\WithoutOverlapping($key))->releaseAfter(60)];
- // }
-
public function failed($exception = null)
{
diff --git a/app/Jobs/Quickbooks/PushToQuickbooks.php b/app/Jobs/Quickbooks/PushToQuickbooks.php
new file mode 100644
index 0000000000..46edfa47f4
--- /dev/null
+++ b/app/Jobs/Quickbooks/PushToQuickbooks.php
@@ -0,0 +1,176 @@
+db);
+
+ $company = Company::find($this->company_id);
+
+ if (!$company) {
+ return;
+ }
+
+ // Resolve the entity based on type
+ $entity = $this->resolveEntity($this->entity_type, $this->entity_id);
+
+ if (!$entity) {
+ return;
+ }
+
+ // Double-check push is still enabled (settings might have changed)
+ if (!$this->shouldPush($company, $this->entity_type, $this->action, $this->status)) {
+ return;
+ }
+
+ $qbService = new QuickbooksService($company);
+
+ // Dispatch to appropriate handler based on entity type
+ match($this->entity_type) {
+ 'client' => $this->pushClient($qbService, $entity),
+ 'invoice' => $this->pushInvoice($qbService, $entity),
+ default => nlog("QuickBooks: Unsupported entity type: {$this->entity_type}"),
+ };
+ }
+
+ /**
+ * Resolve the entity model based on type.
+ *
+ * @param string $entity_type
+ * @param int $entity_id
+ * @return Client|Invoice|null
+ */
+ private function resolveEntity(string $entity_type, int $entity_id): Client|Invoice|null
+ {
+ return match($entity_type) {
+ 'client' => Client::find($entity_id),
+ 'invoice' => Invoice::find($entity_id),
+ default => null,
+ };
+ }
+
+ /**
+ * Check if push should still occur (settings might have changed since job was queued).
+ *
+ * @param Company $company
+ * @param string $entity_type
+ * @param string $action
+ * @param string|null $status
+ * @return bool
+ */
+ private function shouldPush(Company $company, string $entity_type, string $action, ?string $status): bool
+ {
+ return $company->shouldPushToQuickbooks($entity_type, $action, $status);
+ }
+
+ /**
+ * Push a client to QuickBooks.
+ *
+ * @param QuickbooksService $qbService
+ * @param Client $client
+ * @return void
+ */
+ private function pushClient(QuickbooksService $qbService, Client $client): void
+ {
+ // TODO: Implement actual push logic
+ // $qbService->client->push($client, $this->action);
+
+ nlog("QuickBooks: Pushing client {$client->id} to QuickBooks ({$this->action})");
+ }
+
+ /**
+ * Push an invoice to QuickBooks.
+ *
+ * @param QuickbooksService $qbService
+ * @param Invoice $invoice
+ * @return void
+ */
+ private function pushInvoice(QuickbooksService $qbService, Invoice $invoice): void
+ {
+ // Use syncToForeign to push the invoice
+ $qbService->invoice->syncToForeign([$invoice]);
+ }
+
+ /**
+ * Map invoice status_id and is_deleted to status string.
+ *
+ * @param int $statusId
+ * @param bool $isDeleted
+ * @return string
+ */
+ private function mapInvoiceStatusToString(int $statusId, bool $isDeleted): string
+ {
+ if ($isDeleted) {
+ return 'deleted';
+ }
+
+ return match($statusId) {
+ \App\Models\Invoice::STATUS_DRAFT => 'draft',
+ \App\Models\Invoice::STATUS_SENT => 'sent',
+ \App\Models\Invoice::STATUS_PAID => 'paid',
+ default => 'unknown',
+ };
+ }
+}
diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php
index 8cba5dbe40..6013753932 100644
--- a/app/Jobs/Util/Import.php
+++ b/app/Jobs/Util/Import.php
@@ -472,16 +472,56 @@ class Import implements ShouldQueue
$company_repository->save($data, $this->company);
if (isset($data['settings']->company_logo) && strlen($data['settings']->company_logo) > 0) {
+
+
try {
- $tempImage = tempnam(sys_get_temp_dir(), basename($data['settings']->company_logo));
- copy($data['settings']->company_logo, $tempImage);
- $this->uploadLogo($tempImage, $this->company, $this->company);
+ $logoUrl = $data['settings']->company_logo;
+
+ // 1. Validate URL format
+ if (!filter_var($logoUrl, FILTER_VALIDATE_URL)) {
+ throw new \Exception('Invalid URL format');
+ }
+
+ // 2. Restrict protocols
+ $parsed = parse_url($logoUrl);
+ if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
+ throw new \Exception('Only HTTP/HTTPS allowed');
+ }
+
+ // 3. Block internal/private IPs (SSRF protection)
+ $host = $parsed['host'] ?? '';
+ $ip = gethostbyname($host);
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
+ throw new \Exception('Internal hosts not allowed');
+ }
+
+ // 4. Use HTTP client with timeout and size limits instead of copy()
+ $response = \Illuminate\Support\Facades\Http::timeout(20)->get($logoUrl);
+
+ if ($response->successful() && strlen($response->body()) < 20 * 1024 * 1024) { // 5MB limit
+ $tempImage = tempnam(sys_get_temp_dir(), 'logo_');
+ file_put_contents($tempImage, $response->body());
+ $this->uploadLogo($tempImage, $this->company, $this->company);
+ @unlink($tempImage); // Cleanup
+ }
} catch (\Exception $e) {
$settings = $this->company->settings;
$settings->company_logo = '';
$this->company->settings = $settings;
$this->company->save();
+ nlog("Logo import failed: " . $e->getMessage());
}
+
+ // try {
+ // $tempImage = tempnam(sys_get_temp_dir(), basename($data['settings']->company_logo));
+ // copy($data['settings']->company_logo, $tempImage);
+ // $this->uploadLogo($tempImage, $this->company, $this->company);
+ // } catch (\Exception $e) {
+ // $settings = $this->company->settings;
+ // $settings->company_logo = '';
+ // $this->company->settings = $settings;
+ // $this->company->save();
+ // }
}
Company::reguard();
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/Livewire/Flow2/InvoicePay.php b/app/Livewire/Flow2/InvoicePay.php
index a05018e0cf..556c1e7a6b 100644
--- a/app/Livewire/Flow2/InvoicePay.php
+++ b/app/Livewire/Flow2/InvoicePay.php
@@ -235,8 +235,6 @@ class InvoicePay extends Component
public function mount()
{
- // $this->resetContext();
-
MultiDB::setDb($this->db);
// @phpstan-ignore-next-line
@@ -302,6 +300,8 @@ class InvoicePay extends Component
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
+ MultiDB::setDb($this->db);
+
//@phpstan-ignore-next-line
$invite = \App\Models\InvoiceInvitation::with('contact.client', 'company')->withTrashed()->find($this->invitation_id);
diff --git a/app/Livewire/Flow2/RequiredFields.php b/app/Livewire/Flow2/RequiredFields.php
index 6a606e00bb..48fe782e6d 100644
--- a/app/Livewire/Flow2/RequiredFields.php
+++ b/app/Livewire/Flow2/RequiredFields.php
@@ -15,7 +15,6 @@ namespace App\Livewire\Flow2;
use Livewire\Component;
use App\Libraries\MultiDB;
use App\Models\CompanyGateway;
-use Livewire\Attributes\Computed;
use App\Services\Client\RFFService;
use App\Utils\Traits\WithSecureContext;
diff --git a/app/Livewire/QuotesTable.php b/app/Livewire/QuotesTable.php
index ef05c58c23..a603ad3f2c 100644
--- a/app/Livewire/QuotesTable.php
+++ b/app/Livewire/QuotesTable.php
@@ -88,6 +88,10 @@ class QuotesTable extends Component
if (in_array('3', $this->status)) {
$query->whereIn('status_id', [Quote::STATUS_APPROVED, Quote::STATUS_CONVERTED]);
}
+
+ if (in_array('5', $this->status)) {
+ $query->where('status_id', Quote::STATUS_REJECTED);
+ }
}
$query = $query
diff --git a/app/Mail/Admin/EntityFailedSendObject.php b/app/Mail/Admin/EntityFailedSendObject.php
index 124263f3ab..cc06af31cc 100644
--- a/app/Mail/Admin/EntityFailedSendObject.php
+++ b/app/Mail/Admin/EntityFailedSendObject.php
@@ -46,6 +46,31 @@ class EntityFailedSendObject
{
$this->invitation = $invitation;
$this->entity_type = $entity_type;
+
+ // Load relationships if they're not already loaded (e.g., when withoutRelations() was called)
+ if (!$invitation->relationLoaded('contact')) {
+ $invitation->load('contact');
+ }
+ if (!$invitation->relationLoaded('company')) {
+ $invitation->load('company.account');
+ } else {
+ // If company is loaded, ensure account is also loaded
+ if ($invitation->company && !$invitation->company->relationLoaded('account')) {
+ $invitation->company->load('account');
+ }
+ }
+ if (!$invitation->relationLoaded($entity_type)) {
+ $invitation->load([$entity_type => function ($query) {
+ $query->with('client');
+ }]);
+ } else {
+ // If entity is loaded, ensure client is also loaded
+ $entity = $invitation->{$entity_type};
+ if ($entity && !$entity->relationLoaded('client')) {
+ $entity->load('client');
+ }
+ }
+
$this->entity = $invitation->{$entity_type};
$this->contact = $invitation->contact;
$this->company = $invitation->company;
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/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/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/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) {
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/BaseModel.php b/app/Models/BaseModel.php
index e9bae19646..7fab98f2dc 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);
@@ -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();
}
}
@@ -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));
}
/**
diff --git a/app/Models/Company.php b/app/Models/Company.php
index e882b4712c..4fab2e6164 100644
--- a/app/Models/Company.php
+++ b/app/Models/Company.php
@@ -1062,4 +1062,49 @@ class Company extends BaseModel
return $this->getSetting('e_invoice_type') == 'VERIFACTU';
});
}
+
+ /**
+ * Check if QuickBooks push should be triggered for an entity/action.
+ *
+ * Uses efficient checks to avoid overhead for companies not using QuickBooks.
+ * Uses once() to cache the result for the request lifecycle.
+ *
+ * This method is designed to be called from model observers to efficiently
+ * determine if a push job should be dispatched, with zero overhead for
+ * companies that don't use QuickBooks.
+ *
+ * @param string $entity Entity type: 'client', 'invoice', etc.
+ * @param string $action Action type: 'create', 'update', 'status'
+ * @param string|null $status Optional status for status-based pushes (e.g., invoice status: 'draft', 'sent', 'paid', 'deleted')
+ * @return bool
+ */
+ public function shouldPushToQuickbooks(string $entity, string $action, ?string $status = null): bool
+ {
+ // FASTEST CHECK: Raw database column (no object instantiation, no JSON decode)
+ // This is the cheapest possible check - just a null comparison
+ // For companies without QuickBooks, this returns immediately with ~0.001ms overhead
+ if (is_null($this->getRawOriginal('quickbooks'))) {
+ return false;
+ }
+
+ // Cache the detailed check for this request lifecycle
+ // This prevents re-checking if called multiple times in the same request
+ return once(function () use ($entity) {
+ // Check if QuickBooks is actually configured (has token)
+ if (!$this->quickbooks->isConfigured()) {
+ return false;
+ }
+
+ // Verify entity exists in settings
+ if (!isset($this->quickbooks->settings->{$entity})) {
+ return false;
+ }
+
+ $entitySettings = $this->quickbooks->settings->{$entity};
+ $direction = $entitySettings->direction->value;
+
+ // Check if sync direction allows push
+ return $direction === 'push' || $direction === 'bidirectional';
+ });
+ }
}
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/Models/Credit.php b/app/Models/Credit.php
index 6890d4b296..25f3d1558d 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
@@ -196,11 +197,12 @@ class Credit extends BaseModel
'subscription_id',
'vendor_id',
'location_id',
+ 'e_invoice',
];
protected $casts = [
'line_items' => 'object',
- 'backup' => 'object',
+ 'backup' => InvoiceBackup::class,
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
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/Models/Invoice.php b/app/Models/Invoice.php
index d6d8124b1d..93aff72600 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
@@ -279,7 +279,7 @@ class Invoice extends BaseModel
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
'po_number' => (string) $this->po_number,
- 'line_items' => (array) $this->line_items,
+ //'line_items' => (array) $this->line_items, //@todo - reinstate this when elastic indexes have been rebuilt
];
}
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/Models/Quote.php b/app/Models/Quote.php
index a2a10aed3f..94ee5126fa 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()
@@ -382,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').' ';
}
@@ -400,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');
@@ -420,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/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/Observers/ClientObserver.php b/app/Observers/ClientObserver.php
index f5f86c0f74..c6ee0e3bad 100644
--- a/app/Observers/ClientObserver.php
+++ b/app/Observers/ClientObserver.php
@@ -77,6 +77,17 @@ class ClientObserver
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_CREATE_CLIENT, $client, $client->company)->delay(0);
}
+
+ // QuickBooks push - efficient check in observer (zero overhead if not configured)
+ if ($client->company->shouldPushToQuickbooks('client', 'create')) {
+ \App\Jobs\Quickbooks\PushToQuickbooks::dispatch(
+ 'client',
+ $client->id,
+ $client->company->id,
+ $client->company->db,
+ 'create'
+ );
+ }
}
/**
@@ -115,6 +126,17 @@ class ClientObserver
if ($subscriptions) {
WebhookHandler::dispatch($event, $client, $client->company, 'client')->delay(0);
}
+
+ // QuickBooks push - efficient check in observer (zero overhead if not configured)
+ if ($client->company->shouldPushToQuickbooks('client', 'update')) {
+ \App\Jobs\Quickbooks\PushToQuickbooks::dispatch(
+ 'client',
+ $client->id,
+ $client->company->id,
+ $client->company->db,
+ 'update'
+ );
+ }
}
/**
@@ -137,4 +159,5 @@ class ClientObserver
WebhookHandler::dispatch(Webhook::EVENT_ARCHIVE_CLIENT, $client, $client->company)->delay(0);
}
}
+
}
diff --git a/app/Observers/InvoiceObserver.php b/app/Observers/InvoiceObserver.php
index 083d375885..6d394e0e0b 100644
--- a/app/Observers/InvoiceObserver.php
+++ b/app/Observers/InvoiceObserver.php
@@ -35,6 +35,21 @@ class InvoiceObserver
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_CREATE_INVOICE, $invoice, $invoice->company, 'client')->delay(0);
}
+
+ // QuickBooks push - check if invoice status matches push_invoice_statuses
+ // Map invoice status to string for status-based push check
+ $invoiceStatus = $this->mapInvoiceStatusToString($invoice->status_id, $invoice->is_deleted);
+
+ if ($invoice->company->shouldPushToQuickbooks('invoice', 'status', $invoiceStatus)) {
+ \App\Jobs\Quickbooks\PushToQuickbooks::dispatch(
+ 'invoice',
+ $invoice->id,
+ $invoice->company->id,
+ $invoice->company->db,
+ 'create',
+ $invoiceStatus
+ );
+ }
}
/**
@@ -63,6 +78,42 @@ class InvoiceObserver
if ($subscriptions) {
WebhookHandler::dispatch($event, $invoice, $invoice->company, 'client')->delay(0);
}
+
+ // QuickBooks push - check if invoice status matches push_invoice_statuses
+ // Map invoice status to string for status-based push check
+ $invoiceStatus = $this->mapInvoiceStatusToString($invoice->status_id, $invoice->is_deleted);
+
+ if ($invoice->company->shouldPushToQuickbooks('invoice', 'status', $invoiceStatus)) {
+ \App\Jobs\Quickbooks\PushToQuickbooks::dispatch(
+ 'invoice',
+ $invoice->id,
+ $invoice->company->id,
+ $invoice->company->db,
+ 'update',
+ $invoiceStatus
+ );
+ }
+ }
+
+ /**
+ * Map invoice status_id and is_deleted to status string for QuickBooks push.
+ *
+ * @param int $statusId
+ * @param bool $isDeleted
+ * @return string
+ */
+ private function mapInvoiceStatusToString(int $statusId, bool $isDeleted): string
+ {
+ if ($isDeleted) {
+ return 'deleted';
+ }
+
+ return match($statusId) {
+ \App\Models\Invoice::STATUS_DRAFT => 'draft',
+ \App\Models\Invoice::STATUS_SENT => 'sent',
+ \App\Models\Invoice::STATUS_PAID => 'paid',
+ default => 'unknown',
+ };
}
/**
diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php
index 58b844bf78..a51c4d780c 100644
--- a/app/PaymentDrivers/Mollie/CreditCard.php
+++ b/app/PaymentDrivers/Mollie/CreditCard.php
@@ -2,19 +2,19 @@
namespace App\PaymentDrivers\Mollie;
-use App\Exceptions\PaymentFailed;
-use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
-use App\Jobs\Util\SystemLogger;
-use App\Models\ClientGatewayToken;
-use App\Models\GatewayType;
use App\Models\Payment;
-use App\Models\PaymentType;
use App\Models\SystemLog;
-use App\PaymentDrivers\Common\LivewireMethodInterface;
-use App\PaymentDrivers\MolliePaymentDriver;
-use Illuminate\Contracts\View\Factory;
-use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
+use App\Models\GatewayType;
+use App\Models\PaymentType;
+use App\Jobs\Util\SystemLogger;
+use App\Exceptions\PaymentFailed;
+use App\Models\ClientGatewayToken;
+use Illuminate\Http\RedirectResponse;
+use Illuminate\Contracts\View\Factory;
+use App\PaymentDrivers\MolliePaymentDriver;
+use App\PaymentDrivers\Common\LivewireMethodInterface;
+use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
class CreditCard implements LivewireMethodInterface
{
@@ -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);
}
}
@@ -150,6 +151,7 @@ class CreditCard implements LivewireMethodInterface
$payment = $this->mollie->gateway->payments->create($data);
if ($payment->status === 'paid') {
+
$this->mollie->logSuccessfulGatewayResponse(
['response' => $payment, 'data' => $this->mollie->payment_hash->data],
SystemLog::TYPE_MOLLIE
@@ -161,9 +163,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 {
@@ -179,15 +178,22 @@ class CreditCard implements LivewireMethodInterface
public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment)
{
+
$payment_hash = $this->mollie->payment_hash;
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);
}
+ 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) '';
@@ -208,7 +214,7 @@ class CreditCard implements LivewireMethodInterface
'payment_type' => PaymentType::CREDIT_CARD_OTHER,
'transaction_reference' => $payment->id,
];
-
+
$payment_record = $this->mollie->createPayment($data, $payment->status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING);
SystemLogger::dispatch(
@@ -223,7 +229,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 035bc3c0aa..3776ab0166 100644
--- a/app/PaymentDrivers/MolliePaymentDriver.php
+++ b/app/PaymentDrivers/MolliePaymentDriver.php
@@ -12,27 +12,30 @@
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\Exceptions\PaymentFailed;
+use App\Models\ClientGatewayToken;
+use App\PaymentDrivers\BaseDriver;
+use App\PaymentDrivers\Mollie\KBC;
+use App\PaymentDrivers\Mollie\IDEAL;
+use App\Exceptions\PaymentOpenMollie;
+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 +290,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'],
@@ -308,8 +313,6 @@ class MolliePaymentDriver extends BaseDriver
'paid' => Payment::STATUS_COMPLETED,
];
- nlog($request->id);
-
try {
$payment = $this->gateway->payments->get($request->id);
$record = Payment::withTrashed()->where('transaction_reference', $request->id)->first();
@@ -317,7 +320,28 @@ class MolliePaymentDriver extends BaseDriver
if ($record) {
$client = $record->client;
$this->client = $client;
- } else {
+ }
+ elseif($payment->status == 'failed' && $payment->metadata->gateway_type_id === GatewayType::CREDIT_CARD){
+ //no payment, and it failed? return early!
+ $client = Client::withTrashed()->find($this->decodePrimaryKey($payment->metadata->client_id));
+
+ $message = [
+ 'server_response' => $payment,
+ 'data' => $request->all(),
+ ];
+
+ SystemLogger::dispatch(
+ $message,
+ SystemLog::CATEGORY_GATEWAY_RESPONSE,
+ SystemLog::EVENT_GATEWAY_FAILURE,
+ SystemLog::TYPE_MOLLIE,
+ $client,
+ $client->company
+ );
+
+ return response()->json([], 200);
+ }
+ else {
$client = Client::withTrashed()->find($this->decodePrimaryKey($payment->metadata->client_id));
$this->client = $client;
// sometimes if the user is not returned to the site with a response from Mollie
@@ -365,11 +389,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 +413,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);
}
}
@@ -416,11 +448,20 @@ class MolliePaymentDriver extends BaseDriver
try {
$payment = $this->gateway->payments->get($request->getPaymentId());
+ // if($payment->status == 'open'){
+ // nlog("open furfy");
+ // return render('gateways.mollie.mollie_pending_payment_placeholder');
+ // }
+ // else
+
+ if($payment->status == 'failed'){
+ return (new CreditCard($this))->processUnsuccessfulPayment(new PaymentFailed($payment->details->failureMessage, 400));
+ }
return (new CreditCard($this))->processSuccessfulPayment($payment);
} catch (\Mollie\Api\Exceptions\ApiException $e) {
return (new CreditCard($this))->processUnsuccessfulPayment($e);
- }
+ }
}
public function detach(ClientGatewayToken $token)
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/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;
diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php
index 6f156f524f..acb9e4f35d 100644
--- a/app/PaymentDrivers/StripePaymentDriver.php
+++ b/app/PaymentDrivers/StripePaymentDriver.php
@@ -122,6 +122,7 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac
throw new StripeConnectFailure('Stripe Connect has not been configured');
}
} else {
+
$this->stripe = new StripeClient(
$this->company_gateway->getConfigField('apiKey')
);
@@ -700,13 +701,38 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac
public function processWebhookRequest(PaymentWebhookRequest $request)
{
- nlog($request->all());
-
+ // 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,
+ $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());
}
+ // 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/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php
index 803a4a0b56..e25a4461a5 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;
@@ -221,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;
@@ -570,6 +573,10 @@ class EventServiceProvider extends ServiceProvider
CreatedQuoteActivity::class,
QuoteCreatedNotification::class,
],
+ QuoteWasRejected::class => [
+ QuoteRejectedActivity::class,
+ QuoteRejectedNotification::class,
+ ],
QuoteWasUpdated::class => [
QuoteUpdatedActivity::class,
],
diff --git a/app/Repositories/ClientRepository.php b/app/Repositories/ClientRepository.php
index 6dc4a19020..18437b633c 100644
--- a/app/Repositories/ClientRepository.php
+++ b/app/Repositories/ClientRepository.php
@@ -179,6 +179,9 @@ class ClientRepository extends BaseRepository
$client->system_logs()->forceDelete();
// $client->documents()->forceDelete();
$client->payments()->forceDelete();
+
+ $client->unsearchable();
+
$client->forceDelete();
}
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/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/app/Repositories/ProjectRepository.php b/app/Repositories/ProjectRepository.php
index b11c65b5b5..fdc6fb459d 100644
--- a/app/Repositories/ProjectRepository.php
+++ b/app/Repositories/ProjectRepository.php
@@ -21,13 +21,23 @@ use App\Models\Project;
* Class for project repository.
*/
class ProjectRepository extends BaseRepository
-{
+{
+ /**
+ * Invoices a collection of projects into a single invoice.
+ *
+ * @param mixed $projects
+ * @return App\Models\Invoice
+ */
public function invoice(mixed $projects)
{
$_project = $projects->first();
$invoice = InvoiceFactory::create($_project->company_id, $_project->user_id);
$invoice->client_id = $_project->client_id;
+
+ if(count($projects) == 1) {
+ $invoice->project_id = $_project->id;
+ }
// $invoice->project_id = $project->id;
$lines = [];
diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php
index 288c62577b..90f50e20b7 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.
@@ -80,7 +96,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;
}
@@ -242,4 +258,59 @@ 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
+ {
+
+ \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/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php
index 7903205af7..5dbd159884 100644
--- a/app/Services/Client/ClientService.php
+++ b/app/Services/Client/ClientService.php
@@ -178,7 +178,7 @@ class ClientService
$credits = Credit::withTrashed()->where('client_id', $this->client->id)
->where('is_deleted', false)
->where(function ($query) {
- $query->whereDate('due_date', '<=', now()->format('Y-m-d'))
+ $query->where('due_date', '>=', now()->format('Y-m-d'))
->orWhereNull('due_date');
})
->orderBy('created_at', 'ASC');
@@ -192,7 +192,7 @@ class ClientService
->where('is_deleted', false)
->where('balance', '>', 0)
->where(function ($query) {
- $query->whereDate('due_date', '<=', now()->format('Y-m-d'))
+ $query->where('due_date', '>=', now()->format('Y-m-d'))
->orWhereNull('due_date');
})
->orderBy('created_at', 'ASC')->get();
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,
];
}
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/Models/AccountingCustomerParty.php b/app/Services/EDocument/Gateway/Storecove/Models/AccountingCustomerParty.php
index ca10b55d5f..19f849db49 100644
--- a/app/Services/EDocument/Gateway/Storecove/Models/AccountingCustomerParty.php
+++ b/app/Services/EDocument/Gateway/Storecove/Models/AccountingCustomerParty.php
@@ -1,4 +1,13 @@
'Y-m-d'])]
+ public $issue_date;
+
+ #[SerializedPath('[cac:AccountingCustomerParty]')]
+ /** @var ?AccountingCustomerParty */
+ public $accounting_customer_party;
+
+ #[SerializedName('cac:CreditNoteLine')]
+ /** @var CreditLines[] */
+ public ?array $invoice_lines = [];
+
+ #[SerializedPath('[cbc:AccountingCost]')]
+ public $accounting_cost;
+
+ public ?string $accounting_currency_exchange_rate;
+ public ?string $accounting_currency_taxable_amount;
+ public ?string $accounting_currency_tax_amount;
+ public ?string $accounting_currency_tax_amount_currency;
+
+ #[SerializedPath('[cac:AccountingSupplierParty]')]
+ /** @var ?AccountingSupplierParty */
+ public $accounting_supplier_party;
+
+ #[SerializedPath('[cac:AllowanceCharge]')]
+ /** @var AllowanceCharges[] */
+ public ?array $allowance_charges = [];
+
+ //this is an experimental prop
+ // #[SerializedPath('[cac:LegalMonetaryTotal][cbc:TaxInclusiveAmount][#]')]
+ // #[Context(['path_type' => 'tax'])]
+ public $amount_including_tax;
+
+ #[SerializedPath('[cac:LegalMonetaryTotal][cbc:TaxInclusiveAmount][#]')]
+ public $amount_including_vat;
+
+ #[SerializedPath('[cac:AdditionalDocumentReference]')]
+ /** @var Attachments[] */
+ public ?array $attachments;
+
+ public ?bool $consumer_tax_mode; //toggle this to TRUE if we are using a secondary identifier ie. when German company is taxing French company and therefore using the additional Vat identifier
+
+ #[SerializedName('[cac:Delivery][0]')]
+ public ?Delivery $delivery;
+
+ //no mapping
+ public ?DeliveryTerms $delivery_terms;
+
+ #[SerializedPath('[cbc:DocumentCurrencyCode][#]')]
+ public $document_currency_code;
+
+ // /** @var ?\DateTime */
+ #[SerializedPath('[cbc:DueDate]')]
+ // #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
+ public ?string $due_date;
+
+ //may need something custom for this
+ public ?string $invoice_period;
+
+ //no mapping
+ public ?array $issue_reasons;
+
+ //no mapping
+ public ?string $issue_time;
+
+ #[SerializedPath('[cbc:Note]')]
+ public $note;
+
+ //no mapping
+ public ?string $payable_rounding_amount;
+
+ #[SerializedPath('[cac:PaymentMeans]')]
+ /** @var PaymentMeansArray[] */
+ public array $payment_means_array;
+
+ #[SerializedPath('[cac:PaymentTerms][0]')]
+ public ?PaymentTerms $payment_terms;
+
+ // no mapping
+ public ?string $preferred_invoice_type;
+
+ #[SerializedPath('[cac:LegalMonetaryTotal][cbc:PrepaidAmount]')]
+ public ?string $prepaid_amount;
+
+ //no mapping
+ // 'price_mode_gross' can only be used for sender countries ES, IT and PT
+ public ?string $price_mode;
+
+ #[SerializedPath('[cac:BillingReference]')]
+ /** @var References[] */
+ public ?array $references;
+
+ //AU/NZ and JP. - only available intra country ie AU - AU
+ public ?bool $self_billing_mode;
+
+ //readonly prop for received documents
+ public ?string $sub_type;
+
+ //proxy of issue date
+ public ?string $tax_point_date;
+
+ #[SerializedPath('[cac:TaxTotal][0][cac:TaxSubtotal]')]
+ /** @var CreditTaxSubtotals[] */
+ public $tax_subtotals;
+
+ //storecove - no mappings - tax_line_percentages
+ public ?string $tax_system;
+
+ //italy only - invoice level
+ /** @var TaxesDutiesFees[] */
+ public ?array $taxes_duties_fees;
+
+ //no mapping
+ public ?string $time_zone;
+
+ // india only enum (b2b, sezwp, sezwop, expwp, expwop, dexp)
+ public ?string $transaction_type;
+
+ //no mapping
+ public ?array $ubl_extensions;
+
+ //no mapping
+ public ?string $x2y;
+
+ //not found in schema
+ public ?bool $vat_reverse_charge;
+ public ?string $tax_exempt_reason;
+ public ?string $invoice_type;
+ public ?string $payment_means_iban;
+ public ?string $payment_means_bic;
+ public ?string $payment_means_code;
+ public ?string $payment_means_payment_id;
+ //not found in schema
+
+ #[SerializedPath('[cbc:BuyerReference]')]
+ public ?string $buyer_reference;
+
+ #[SerializedPath('[cac:OrderReference][cbc:ID][#]')]
+ public ?string $order_reference;
+
+ #[SerializedPath('[cac:OrderReference][cbc:SalesOrderID][#]')]
+ public ?string $sales_order_id;
+
+ #[SerializedPath('[cac:BillingReference][cac:InvoiceDocumentReference][cbc:ID][#]')]
+ public ?string $billing_reference;
+
+ #[SerializedPath('[cac:ContractDocumentReference][cbc:ID][#]')]
+ public ?string $contract_document_reference;
+
+ #[SerializedPath('[cac:ProjectReference][cbc:ID][#]')]
+ public ?string $project_reference;
+
+ public function __construct(
+ ?array $references,
+ ?string $invoice_number,
+ ?string $issue_date,
+ ?AccountingCustomerParty $accounting_customer_party,
+ ?array $invoice_lines,
+ ?string $accounting_cost,
+ ?string $accounting_currency_exchange_rate,
+ ?string $accounting_currency_taxable_amount,
+ ?string $accounting_currency_tax_amount,
+ ?string $accounting_currency_tax_amount_currency,
+ ?AccountingSupplierParty $accounting_supplier_party,
+ ?array $allowance_charges,
+ ?float $amount_including_tax,
+ ?float $amount_including_vat,
+ ?array $attachments,
+ ?bool $consumer_tax_mode,
+ ?Delivery $delivery,
+ ?DeliveryTerms $delivery_terms,
+ ?string $document_currency_code,
+ ?string $due_date,
+ ?string $invoice_period,
+ ?array $issue_reasons,
+ ?string $issue_time,
+ ?string $note,
+ ?string $payable_rounding_amount,
+ ?array $payment_means_array,
+ ?PaymentTerms $payment_terms,
+ ?string $preferred_invoice_type,
+ ?string $prepaid_amount,
+ ?string $price_mode,
+ ?bool $self_billing_mode,
+ ?string $sub_type,
+ ?string $tax_point_date,
+ ?array $tax_subtotals,
+ ?string $tax_system,
+ ?array $taxes_duties_fees,
+ ?string $time_zone,
+ ?string $transaction_type,
+ ?array $ubl_extensions,
+ ?string $x2y,
+ ?bool $vat_reverse_charge,
+ ?string $tax_exempt_reason,
+ ?string $invoice_type,
+ ?string $buyer_reference,
+ ?string $order_reference,
+ ?string $sales_order_id,
+ ?string $billing_reference,
+ ?string $contract_document_reference,
+ ?string $project_reference,
+ ?string $payment_means_iban,
+ ?string $payment_means_bic,
+ ?string $payment_means_code,
+ ?string $payment_means_payment_id
+ ) {
+ $this->invoice_number = $invoice_number;
+ $this->issue_date = $issue_date;
+ $this->accounting_customer_party = $accounting_customer_party;
+ $this->invoice_lines = $invoice_lines;
+ $this->accounting_cost = $accounting_cost;
+ $this->accounting_currency_exchange_rate = $accounting_currency_exchange_rate;
+ $this->accounting_currency_taxable_amount = $accounting_currency_taxable_amount;
+ $this->accounting_currency_tax_amount = $accounting_currency_tax_amount;
+ $this->accounting_currency_tax_amount_currency = $accounting_currency_tax_amount_currency;
+ $this->accounting_supplier_party = $accounting_supplier_party;
+ $this->allowance_charges = $allowance_charges;
+ $this->amount_including_tax = $amount_including_tax;
+ $this->amount_including_vat = $amount_including_vat * -1;
+ $this->attachments = $attachments;
+ $this->consumer_tax_mode = $consumer_tax_mode;
+ $this->delivery = $delivery;
+ $this->delivery_terms = $delivery_terms;
+ $this->document_currency_code = $document_currency_code;
+ $this->due_date = $due_date;
+ $this->invoice_period = $invoice_period;
+ $this->issue_reasons = $issue_reasons;
+ $this->issue_time = $issue_time;
+ $this->note = $note;
+ $this->payable_rounding_amount = $payable_rounding_amount;
+ $this->payment_means_array = $payment_means_array;
+ $this->payment_terms = $payment_terms;
+ $this->preferred_invoice_type = $preferred_invoice_type;
+ $this->prepaid_amount = $prepaid_amount;
+ $this->price_mode = $price_mode;
+ $this->references = $references;
+ $this->self_billing_mode = $self_billing_mode;
+ $this->sub_type = $sub_type;
+ $this->tax_point_date = $tax_point_date;
+ $this->tax_subtotals = $tax_subtotals;
+ $this->tax_system = $tax_system;
+ $this->taxes_duties_fees = $taxes_duties_fees;
+ $this->time_zone = $time_zone;
+ $this->transaction_type = $transaction_type;
+ $this->ubl_extensions = $ubl_extensions;
+ $this->x2y = $x2y;
+ $this->vat_reverse_charge = $vat_reverse_charge;
+ $this->tax_exempt_reason = $tax_exempt_reason;
+ $this->invoice_type = $invoice_type;
+ $this->buyer_reference = $buyer_reference;
+ $this->order_reference = $order_reference;
+ $this->sales_order_id = $sales_order_id;
+ $this->billing_reference = $billing_reference;
+ $this->contract_document_reference = $contract_document_reference;
+ $this->project_reference = $project_reference;
+ $this->payment_means_iban = $payment_means_iban;
+ $this->payment_means_bic = $payment_means_bic;
+ $this->payment_means_code = $payment_means_code;
+ $this->payment_means_payment_id = $payment_means_payment_id;
+ }
+
+ public function getInvoiceNumber(): ?string
+ {
+ return $this->invoice_number;
+ }
+
+ public function getIssueDate(): ?string
+ {
+ return $this->issue_date;
+ }
+
+ public function getAccountingCustomerParty(): ?AccountingCustomerParty
+ {
+ return $this->accounting_customer_party;
+ }
+
+ /**
+ * @return CreditLines[]
+ */
+ public function getInvoiceLines(): ?array
+ {
+ return $this->invoice_lines;
+ }
+
+ public function getAccountingCost(): ?string
+ {
+ return $this->accounting_cost;
+ }
+
+ public function getAccountingCurrencyExchangeRate(): ?string
+ {
+ return $this->accounting_currency_exchange_rate;
+ }
+
+ public function getAccountingCurrencyTaxableAmount(): ?string
+ {
+ return $this->accounting_currency_taxable_amount;
+ }
+
+ public function getAccountingCurrencyTaxAmount(): ?string
+ {
+ return $this->accounting_currency_tax_amount;
+ }
+
+ public function getAccountingCurrencyTaxAmountCurrency(): ?string
+ {
+ return $this->accounting_currency_tax_amount_currency;
+ }
+
+ public function getAccountingSupplierParty(): ?AccountingSupplierParty
+ {
+ return $this->accounting_supplier_party;
+ }
+
+ /**
+ * @return AllowanceCharges[]
+ */
+ public function getAllowanceCharges(): ?array
+ {
+ return $this->allowance_charges;
+ }
+
+ public function getAmountIncludingTax(): ?float
+ {
+ return $this->amount_including_tax;
+ }
+
+ public function getAmountIncludingVat(): ?float
+ {
+ return $this->amount_including_vat;
+ }
+
+ /**
+ * @return Attachments[]
+ */
+ public function getAttachments(): ?array
+ {
+ return $this->attachments;
+ }
+
+ public function getConsumerTaxMode(): ?bool
+ {
+ return $this->consumer_tax_mode;
+ }
+
+ public function getDelivery(): ?Delivery
+ {
+ return $this->delivery;
+ }
+
+ public function getDeliveryTerms(): ?DeliveryTerms
+ {
+ return $this->delivery_terms;
+ }
+
+ public function getDocumentCurrencyCode(): ?string
+ {
+ return $this->document_currency_code;
+ }
+
+ public function getDueDate()
+ {
+ return $this->due_date;
+ }
+
+ public function getInvoicePeriod(): ?string
+ {
+ return $this->invoice_period;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getIssueReasons(): ?array
+ {
+ return $this->issue_reasons;
+ }
+
+ public function getIssueTime(): ?string
+ {
+ return $this->issue_time;
+ }
+
+ public function getNote(): ?string
+ {
+ return $this->note;
+ }
+
+ public function getPayableRoundingAmount(): ?string
+ {
+ return $this->payable_rounding_amount;
+ }
+
+ /**
+ * @return PaymentMeansArray[]
+ */
+ public function getPaymentMeansArray(): ?array
+ {
+ return $this->payment_means_array;
+ }
+
+ public function getPaymentTerms(): ?PaymentTerms
+ {
+ return $this->payment_terms;
+ }
+
+ public function getPreferredInvoiceType(): ?string
+ {
+ return $this->preferred_invoice_type;
+ }
+
+ public function getPrepaidAmount(): ?string
+ {
+ return $this->prepaid_amount;
+ }
+
+ public function getPriceMode(): ?string
+ {
+ return $this->price_mode;
+ }
+
+ /**
+ * @return References[]
+ */
+ public function getReferences(): ?array
+ {
+ return $this->references;
+ }
+
+ public function getSelfBillingMode(): ?bool
+ {
+ return $this->self_billing_mode;
+ }
+
+ public function getSubType(): ?string
+ {
+ return $this->sub_type;
+ }
+
+ public function getTaxPointDate(): ?string
+ {
+ return $this->tax_point_date;
+ }
+
+ /**
+ * @return CreditTaxSubtotals[]
+ */
+ public function getTaxSubtotals(): ?array
+ {
+ return $this->tax_subtotals;
+ }
+
+ public function getTaxSystem(): ?string
+ {
+ return $this->tax_system;
+ }
+
+ /**
+ * @return TaxesDutiesFees[]
+ */
+ public function getTaxesDutiesFees(): ?array
+ {
+ return $this->taxes_duties_fees;
+ }
+
+ public function getTimeZone(): ?string
+ {
+ return $this->time_zone;
+ }
+
+ public function getTransactionType(): ?string
+ {
+ return $this->transaction_type;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getUblExtensions(): ?array
+ {
+ return $this->ubl_extensions;
+ }
+
+ public function getX2y(): ?string
+ {
+ return $this->x2y;
+ }
+
+ public function getVatReverseCharge(): ?bool
+ {
+ return $this->vat_reverse_charge;
+ }
+
+ public function getTaxExemptReason(): ?string
+ {
+ return $this->tax_exempt_reason;
+ }
+
+ public function getInvoiceType(): ?string
+ {
+ return $this->invoice_type;
+ }
+
+ public function getBuyerReference(): ?string
+ {
+ return $this->buyer_reference;
+ }
+
+ public function getOrderReference(): ?string
+ {
+ return $this->order_reference;
+ }
+
+ public function getSalesOrderId(): ?string
+ {
+ return $this->sales_order_id;
+ }
+
+ public function getBillingReference(): ?string
+ {
+ return $this->billing_reference;
+ }
+
+ public function getContractDocumentReference(): ?string
+ {
+ return $this->contract_document_reference;
+ }
+
+ public function getProjectReference(): ?string
+ {
+ return $this->project_reference;
+ }
+
+ public function getPaymentMeansIban(): ?string
+ {
+ return $this->payment_means_iban;
+ }
+
+ public function getPaymentMeansBic(): ?string
+ {
+ return $this->payment_means_bic;
+ }
+
+ public function getPaymentMeansCode(): ?string
+ {
+ return $this->payment_means_code;
+ }
+
+ public function getPaymentMeansPaymentId(): ?string
+ {
+ return $this->payment_means_payment_id;
+ }
+
+ public function setInvoiceNumber(?string $invoice_number): self
+ {
+ $this->invoice_number = $invoice_number;
+ return $this;
+ }
+
+ public function setIssueDate($issue_date): self
+ {
+ $this->issue_date = $issue_date;
+ return $this;
+ }
+
+ public function setAccountingCustomerParty(?AccountingCustomerParty $accounting_customer_party): self
+ {
+ $this->accounting_customer_party = $accounting_customer_party;
+ return $this;
+ }
+
+ /**
+ * @param CreditLines[] $invoice_lines
+ */
+ public function setInvoiceLines(?array $invoice_lines): self
+ {
+ $this->invoice_lines = $invoice_lines ?? [];
+ return $this;
+ }
+
+ public function setAccountingCost(?string $accounting_cost): self
+ {
+ $this->accounting_cost = $accounting_cost;
+ return $this;
+ }
+
+ public function setAccountingCurrencyExchangeRate(?string $accounting_currency_exchange_rate): self
+ {
+ $this->accounting_currency_exchange_rate = $accounting_currency_exchange_rate;
+ return $this;
+ }
+
+ public function setAccountingCurrencyTaxableAmount(?string $accounting_currency_taxable_amount): self
+ {
+ $this->accounting_currency_taxable_amount = $accounting_currency_taxable_amount;
+ return $this;
+ }
+
+ public function setAccountingCurrencyTaxAmount(?string $accounting_currency_tax_amount): self
+ {
+ $this->accounting_currency_tax_amount = $accounting_currency_tax_amount;
+ return $this;
+ }
+
+ public function setAccountingCurrencyTaxAmountCurrency(?string $accounting_currency_tax_amount_currency): self
+ {
+ $this->accounting_currency_tax_amount_currency = $accounting_currency_tax_amount_currency;
+ return $this;
+ }
+
+ public function setAccountingSupplierParty(?AccountingSupplierParty $accounting_supplier_party): self
+ {
+ $this->accounting_supplier_party = $accounting_supplier_party;
+ return $this;
+ }
+
+ /**
+ * @param AllowanceCharges[] $allowance_charges
+ */
+ public function setAllowanceCharges(?array $allowance_charges): self
+ {
+ $this->allowance_charges = $allowance_charges;
+ return $this;
+ }
+
+ public function setAmountIncludingTax(?float $amount_including_tax): self
+ {
+ $this->amount_including_tax = $amount_including_tax;
+ return $this;
+ }
+
+ public function setAmountIncludingVat(?float $amount_including_vat): self
+ {
+ $this->amount_including_vat = $amount_including_vat;
+ return $this;
+ }
+
+ /**
+ * @param Attachments[] $attachments
+ */
+ public function setAttachments(?array $attachments): self
+ {
+ $this->attachments = $attachments;
+ return $this;
+ }
+
+ public function setConsumerTaxMode(?bool $consumer_tax_mode): self
+ {
+ $this->consumer_tax_mode = $consumer_tax_mode;
+ return $this;
+ }
+
+ public function setDelivery(?Delivery $delivery): self
+ {
+ $this->delivery = $delivery;
+ return $this;
+ }
+
+ public function setDeliveryTerms(?DeliveryTerms $delivery_terms): self
+ {
+ $this->delivery_terms = $delivery_terms;
+ return $this;
+ }
+
+ public function setDocumentCurrencyCode(?string $document_currency_code): self
+ {
+ $this->document_currency_code = $document_currency_code;
+ return $this;
+ }
+
+ public function setDueDate(?string $due_date): self
+ {
+ $this->due_date = $due_date;
+ return $this;
+ }
+
+ public function setInvoicePeriod(?string $invoice_period): self
+ {
+ $this->invoice_period = $invoice_period;
+ return $this;
+ }
+
+ /**
+ * @param string[] $issue_reasons
+ */
+ public function setIssueReasons(?array $issue_reasons): self
+ {
+ $this->issue_reasons = $issue_reasons;
+ return $this;
+ }
+
+ public function setIssueTime(?string $issue_time): self
+ {
+ $this->issue_time = $issue_time;
+ return $this;
+ }
+
+ public function setNote(?string $note): self
+ {
+ $this->note = $note;
+ return $this;
+ }
+
+ public function setPayableRoundingAmount(?string $payable_rounding_amount): self
+ {
+ $this->payable_rounding_amount = $payable_rounding_amount;
+ return $this;
+ }
+
+ /**
+ * @param PaymentMeansArray[] $payment_means_array
+ */
+ public function setPaymentMeansArray(?array $payment_means_array): self
+ {
+ $this->payment_means_array = $payment_means_array;
+ return $this;
+ }
+
+ public function setPaymentTerms(?PaymentTerms $payment_terms): self
+ {
+ $this->payment_terms = $payment_terms;
+ return $this;
+ }
+
+ public function setPreferredInvoiceType(?string $preferred_invoice_type): self
+ {
+ $this->preferred_invoice_type = $preferred_invoice_type;
+ return $this;
+ }
+
+ public function setPrepaidAmount(?string $prepaid_amount): self
+ {
+ $this->prepaid_amount = $prepaid_amount;
+ return $this;
+ }
+
+ public function setPriceMode(?string $price_mode): self
+ {
+ $this->price_mode = $price_mode;
+ return $this;
+ }
+
+ /**
+ * @param References[] $references
+ */
+ public function setReferences(?array $references): self
+ {
+ $this->references = $references;
+ return $this;
+ }
+
+ public function setSelfBillingMode(?bool $self_billing_mode): self
+ {
+ $this->self_billing_mode = $self_billing_mode;
+ return $this;
+ }
+
+ public function setSubType(?string $sub_type): self
+ {
+ $this->sub_type = $sub_type;
+ return $this;
+ }
+
+ public function setTaxPointDate(?string $tax_point_date): self
+ {
+ $this->tax_point_date = $tax_point_date;
+ return $this;
+ }
+
+ /**
+ * @param CreditTaxSubtotals[] $tax_subtotals
+ */
+ public function setTaxSubtotals(?array $tax_subtotals): self
+ {
+ $this->tax_subtotals = $tax_subtotals;
+ return $this;
+ }
+
+ public function setTaxSystem(?string $tax_system): self
+ {
+ $this->tax_system = $tax_system;
+ return $this;
+ }
+
+ /**
+ * @param TaxesDutiesFees[] $taxes_duties_fees
+ */
+ public function setTaxesDutiesFees(?array $taxes_duties_fees): self
+ {
+ $this->taxes_duties_fees = $taxes_duties_fees;
+ return $this;
+ }
+
+ public function setTimeZone(?string $time_zone): self
+ {
+ $this->time_zone = $time_zone;
+ return $this;
+ }
+
+ public function setTransactionType(?string $transaction_type): self
+ {
+ $this->transaction_type = $transaction_type;
+ return $this;
+ }
+
+ /**
+ * @param string[] $ubl_extensions
+ */
+ public function setUblExtensions(?array $ubl_extensions): self
+ {
+ $this->ubl_extensions = $ubl_extensions;
+ return $this;
+ }
+
+ public function setX2y(?string $x2y): self
+ {
+ $this->x2y = $x2y;
+ return $this;
+ }
+
+ public function setVatReverseCharge(?bool $vat_reverse_charge): self
+ {
+ $this->vat_reverse_charge = $vat_reverse_charge;
+ return $this;
+ }
+
+ public function setTaxExemptReason(?string $tax_exempt_reason): self
+ {
+ $this->tax_exempt_reason = $tax_exempt_reason;
+ return $this;
+ }
+
+ public function setInvoiceType(?string $invoice_type): self
+ {
+ $this->invoice_type = $invoice_type;
+ return $this;
+ }
+
+ public function setBuyerReference(?string $buyer_reference): self
+ {
+ $this->buyer_reference = $buyer_reference;
+ return $this;
+ }
+
+ public function setOrderReference(?string $order_reference): self
+ {
+ $this->order_reference = $order_reference;
+ return $this;
+ }
+
+ public function setSalesOrderId(?string $sales_order_id): self
+ {
+ $this->sales_order_id = $sales_order_id;
+ return $this;
+ }
+
+ public function setBillingReference(?string $billing_reference): self
+ {
+ $this->billing_reference = $billing_reference;
+ return $this;
+ }
+
+ public function setContractDocumentReference(?string $contract_document_reference): self
+ {
+ $this->contract_document_reference = $contract_document_reference;
+ return $this;
+ }
+
+ public function setProjectReference(?string $project_reference): self
+ {
+ $this->project_reference = $project_reference;
+ return $this;
+ }
+
+ public function setPaymentMeansIban(?string $payment_means_iban): self
+ {
+ $this->payment_means_iban = $payment_means_iban;
+ return $this;
+ }
+
+ public function setPaymentMeansBic(?string $payment_means_bic): self
+ {
+ $this->payment_means_bic = $payment_means_bic;
+ return $this;
+ }
+
+ public function setPaymentMeansCode(?string $payment_means_code): self
+ {
+ $this->payment_means_code = $payment_means_code;
+ return $this;
+ }
+
+ public function setPaymentMeansPaymentId(?string $payment_means_payment_id): self
+ {
+ $this->payment_means_payment_id = $payment_means_payment_id;
+ return $this;
+ }
+}
diff --git a/app/Services/EDocument/Gateway/Storecove/Models/CreditLines.php b/app/Services/EDocument/Gateway/Storecove/Models/CreditLines.php
new file mode 100644
index 0000000000..0e179fa9f1
--- /dev/null
+++ b/app/Services/EDocument/Gateway/Storecove/Models/CreditLines.php
@@ -0,0 +1,416 @@
+line_id = $line_id;
+ $this->description = $description;
+ $this->name = $name;
+ $this->order_line_reference_line_id = $order_line_reference_line_id;
+ $this->invoice_period = $invoice_period;
+ $this->item_price = $item_price;
+ $this->quantity = $quantity * -1;
+ $this->base_quantity = $base_quantity;
+ $this->quantity_unit_code = $quantity_unit_code;
+ $this->allowance_charges = $allowance_charges;
+ $this->amount_excluding_vat = $amount_excluding_vat * -1;
+ $this->amount_excluding_tax = $amount_excluding_tax;
+ $this->amount_including_tax = $amount_including_tax;
+ $this->taxes_duties_fees = $taxes_duties_fees;
+ $this->accounting_cost = $accounting_cost;
+ $this->references = $references;
+ $this->additional_item_properties = $additional_item_properties;
+ $this->sellers_item_identification = $sellers_item_identification;
+ $this->buyers_item_identification = $buyers_item_identification;
+ $this->standard_item_identification = $standard_item_identification;
+ $this->standard_item_identification_scheme_id = $standard_item_identification_scheme_id;
+ $this->standard_item_identification_scheme_agency_id = $standard_item_identification_scheme_agency_id;
+ $this->note = $note;
+ }
+
+ public function getLineId(): ?string
+ {
+ return $this->line_id;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function getOrderLineReferenceLineId(): ?string
+ {
+ return $this->order_line_reference_line_id;
+ }
+
+ public function getInvoicePeriod(): ?string
+ {
+ return $this->invoice_period;
+ }
+
+ public function getItemPrice(): ?float
+ {
+ return $this->item_price;
+ }
+
+ public function getQuantity(): ?float
+ {
+ return $this->quantity * -1;
+ }
+
+ public function getBaseQuantity(): ?float
+ {
+ return $this->base_quantity;
+ }
+
+ public function getQuantityUnitCode(): ?string
+ {
+ return $this->quantity_unit_code;
+ }
+
+ /**
+ * @return AllowanceCharges[]
+ */
+ public function getAllowanceCharges(): ?array
+ {
+ return $this->allowance_charges;
+ }
+
+ public function getAmountExcludingVat(): ?float
+ {
+ return $this->amount_excluding_vat;
+ }
+
+ public function getAmountExcludingTax(): ?float
+ {
+ return $this->amount_excluding_tax;
+ }
+
+ public function getAmountIncludingTax(): ?float
+ {
+ return $this->amount_including_tax;
+ }
+
+ /**
+ * @return TaxesDutiesFees[]
+ */
+ public function getTaxesDutiesFees(): ?array
+ {
+ return $this->taxes_duties_fees;
+ }
+
+ public function getAccountingCost(): ?string
+ {
+ return $this->accounting_cost;
+ }
+
+ /**
+ * @return References[]
+ */
+ public function getReferences(): ?array
+ {
+ return $this->references;
+ }
+
+ /**
+ * @return AdditionalItemProperties[]
+ */
+ public function getAdditionalItemProperties(): ?array
+ {
+ return $this->additional_item_properties;
+ }
+
+ public function getSellersItemIdentification(): ?string
+ {
+ return $this->sellers_item_identification;
+ }
+
+ public function getBuyersItemIdentification(): ?string
+ {
+ return $this->buyers_item_identification;
+ }
+
+ public function getStandardItemIdentification(): ?string
+ {
+ return $this->standard_item_identification;
+ }
+
+ public function getStandardItemIdentificationSchemeId(): ?string
+ {
+ return $this->standard_item_identification_scheme_id;
+ }
+
+ public function getStandardItemIdentificationSchemeAgencyId(): ?string
+ {
+ return $this->standard_item_identification_scheme_agency_id;
+ }
+
+ public function getNote(): ?string
+ {
+ return $this->note;
+ }
+
+ public function setLineId(?string $line_id): self
+ {
+ $this->line_id = $line_id;
+ return $this;
+ }
+
+ public function setDescription(?string $description): self
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function setName(?string $name): self
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function setOrderLineReferenceLineId(?string $order_line_reference_line_id): self
+ {
+ $this->order_line_reference_line_id = $order_line_reference_line_id;
+ return $this;
+ }
+
+ public function setInvoicePeriod(?string $invoice_period): self
+ {
+ $this->invoice_period = $invoice_period;
+ return $this;
+ }
+
+ public function setItemPrice(?float $item_price): self
+ {
+ $this->item_price = $item_price;
+ return $this;
+ }
+
+ public function setQuantity(?float $quantity): self
+ {
+ $this->quantity = $quantity;
+ return $this;
+ }
+
+ public function setBaseQuantity(?float $base_quantity): self
+ {
+ $this->base_quantity = $base_quantity;
+ return $this;
+ }
+
+ public function setQuantityUnitCode(?string $quantity_unit_code): self
+ {
+ $this->quantity_unit_code = $quantity_unit_code;
+ return $this;
+ }
+
+ /**
+ * @param AllowanceCharges[] $allowance_charges
+ */
+ public function setAllowanceCharges(?array $allowance_charges): self
+ {
+ $this->allowance_charges = $allowance_charges;
+ return $this;
+ }
+
+ public function setAmountExcludingVat(?float $amount_excluding_vat): self
+ {
+ $this->amount_excluding_vat = $amount_excluding_vat;
+ return $this;
+ }
+
+ public function setAmountExcludingTax(?float $amount_excluding_tax): self
+ {
+ $this->amount_excluding_tax = $amount_excluding_tax;
+ return $this;
+ }
+
+ public function setAmountIncludingTax(?float $amount_including_tax): self
+ {
+ $this->amount_including_tax = $amount_including_tax;
+ return $this;
+ }
+
+
+ /**
+ * @param TaxesDutiesFees[] $taxes_duties_fees
+ */
+ public function setTaxesDutiesFees(?array $taxes_duties_fees): self
+ {
+ $this->taxes_duties_fees = $taxes_duties_fees;
+ return $this;
+ }
+
+ public function setAccountingCost(?string $accounting_cost): self
+ {
+ $this->accounting_cost = $accounting_cost;
+ return $this;
+ }
+
+ /**
+ * @param References[] $references
+ */
+ public function setReferences(?array $references): self
+ {
+ $this->references = $references;
+ return $this;
+ }
+
+ /**
+ * @param AdditionalItemProperties[] $additional_item_properties
+ */
+ public function setAdditionalItemProperties(?array $additional_item_properties): self
+ {
+ $this->additional_item_properties = $additional_item_properties;
+ return $this;
+ }
+
+ public function setSellersItemIdentification(?string $sellers_item_identification): self
+ {
+ $this->sellers_item_identification = $sellers_item_identification;
+ return $this;
+ }
+
+ public function setBuyersItemIdentification(?string $buyers_item_identification): self
+ {
+ $this->buyers_item_identification = $buyers_item_identification;
+ return $this;
+ }
+
+ public function setStandardItemIdentification(?string $standard_item_identification): self
+ {
+ $this->standard_item_identification = $standard_item_identification;
+ return $this;
+ }
+
+ public function setStandardItemIdentificationSchemeId(?string $standard_item_identification_scheme_id): self
+ {
+ $this->standard_item_identification_scheme_id = $standard_item_identification_scheme_id;
+ return $this;
+ }
+
+ public function setStandardItemIdentificationSchemeAgencyId(?string $standard_item_identification_scheme_agency_id): self
+ {
+ $this->standard_item_identification_scheme_agency_id = $standard_item_identification_scheme_agency_id;
+ return $this;
+ }
+
+ public function setNote(?string $note): self
+ {
+ $this->note = $note;
+ return $this;
+ }
+}
diff --git a/app/Services/EDocument/Gateway/Storecove/Models/CreditTaxSubtotals.php b/app/Services/EDocument/Gateway/Storecove/Models/CreditTaxSubtotals.php
new file mode 100644
index 0000000000..152ccf2358
--- /dev/null
+++ b/app/Services/EDocument/Gateway/Storecove/Models/CreditTaxSubtotals.php
@@ -0,0 +1,113 @@
+tax_amount = $tax_amount * -1;
+ $this->country = $country;
+ $this->taxable_amount = $taxable_amount * -1;
+ $this->percentage = $percentage;
+ $this->category = $category;
+ $this->type = $type;
+ }
+
+ public function getType(): ?string
+ {
+ return $this->type;
+ }
+
+ public function setType(?string $type): self
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function getTaxAmount(): ?float
+ {
+ return $this->tax_amount;
+ }
+
+ public function getCountry(): ?string
+ {
+ return $this->country;
+ }
+
+ public function getTaxableAmount(): ?float
+ {
+ return $this->taxable_amount;
+ }
+
+ public function getPercentage(): ?float
+ {
+ return $this->percentage;
+ }
+
+ public function getCategory(): ?string
+ {
+ return $this->category;
+ }
+
+ public function setTaxAmount(?float $tax_amount): self
+ {
+ $this->tax_amount = $tax_amount;
+ return $this;
+ }
+
+ public function setCountry(?string $country): self
+ {
+ $this->country = $country;
+ return $this;
+ }
+
+ public function setTaxableAmount(?float $taxable_amount): self
+ {
+ $this->taxable_amount = $taxable_amount;
+ return $this;
+ }
+
+ public function setPercentage(?float $percentage): self
+ {
+ $this->percentage = $percentage;
+ return $this;
+ }
+
+ public function setCategory(?string $category): self
+ {
+ $this->category = $category;
+ return $this;
+ }
+
+ public function toArray(): array
+ {
+ return (array)$this;
+ }
+}
diff --git a/app/Services/EDocument/Gateway/Storecove/Models/References.php b/app/Services/EDocument/Gateway/Storecove/Models/References.php
index 37b2232901..360af31cda 100644
--- a/app/Services/EDocument/Gateway/Storecove/Models/References.php
+++ b/app/Services/EDocument/Gateway/Storecove/Models/References.php
@@ -2,10 +2,16 @@
namespace App\Services\EDocument\Gateway\Storecove\Models;
+use Symfony\Component\Serializer\Attribute\SerializedName;
+use Symfony\Component\Serializer\Attribute\SerializedPath;
+
class References
{
public ?string $document_type;
+
public ?string $document_type_code;
+
+ #[SerializedPath('[cac:InvoiceDocumentReference][cbc:ID][#]')]
public ?string $document_id;
public ?string $document_uuid;
public ?string $document_id_scheme_id;
@@ -15,6 +21,8 @@ class References
public ?string $document_id_list_agency_id;
public ?string $document_id_list_version_id;
public ?string $line_id;
+
+ #[SerializedPath('[cac:InvoiceDocumentReference][cbc:IssueDate]')]
public ?string $issue_date;
public ?string $document_description;
@@ -33,7 +41,7 @@ class References
?string $issue_date,
?string $document_description
) {
- $this->document_type = $document_type;
+ $this->document_type = $document_type ?? 'billing';
$this->document_type_code = $document_type_code;
$this->document_id = $document_id;
$this->document_uuid = $document_uuid;
@@ -50,7 +58,7 @@ class References
public function getDocumentType(): ?string
{
- return $this->document_type;
+ return $this->document_type ?? "billing";
}
public function getDocumentTypeCode(): ?string
diff --git a/app/Services/EDocument/Gateway/Storecove/Mutator.php b/app/Services/EDocument/Gateway/Storecove/Mutator.php
index f60119df68..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
{
@@ -613,6 +614,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;
diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php
index 672cdc6fab..411f21f132 100644
--- a/app/Services/EDocument/Gateway/Storecove/Storecove.php
+++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php
@@ -76,7 +76,7 @@ class Storecove
/**
* build
*
- * @param \App\Models\Invoice $model
+ * @param \App\Models\Invoice|\App\Models\Credit $model
* @return self
*/
public function build($model): self
@@ -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
*
diff --git a/app/Services/EDocument/Gateway/Storecove/StorecoveAdapter.php b/app/Services/EDocument/Gateway/Storecove/StorecoveAdapter.php
index b005c91f5b..054266bbcf 100644
--- a/app/Services/EDocument/Gateway/Storecove/StorecoveAdapter.php
+++ b/app/Services/EDocument/Gateway/Storecove/StorecoveAdapter.php
@@ -23,6 +23,7 @@ use App\Services\EDocument\Gateway\Storecove\Storecove;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use App\Services\EDocument\Gateway\Storecove\Models\Invoice;
+use App\Services\EDocument\Gateway\Storecove\Models\Credit;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
@@ -45,7 +46,7 @@ class StorecoveAdapter
{
}
- private Invoice $storecove_invoice;
+ private Invoice | Credit $storecove_invoice;
private array $errors = [];
@@ -113,10 +114,10 @@ class StorecoveAdapter
/**
* transform
*
- * @param \App\Models\Invoice $invoice
+ * @param \App\Models\Invoice |\App\Models\Credit $invoice
* @return self
*/
- public function transform($invoice): self
+ public function transform(\App\Models\Invoice|\App\Models\Credit $invoice): self
{
try {
$this->ninja_invoice = $invoice;
@@ -134,7 +135,8 @@ class StorecoveAdapter
$e = new \InvoiceNinja\EInvoice\EInvoice();
$peppolInvoice = $e->decode('Peppol', $p, 'xml');
- $parent = \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
+
+ $parent = $invoice instanceof \App\Models\Credit ? \App\Services\EDocument\Gateway\Storecove\Models\Credit::class : \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
$peppolInvoice = $e->encode($peppolInvoice, 'json');
$this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'json', $context);
@@ -160,6 +162,12 @@ class StorecoveAdapter
return $this;
}
+ // if($this->ninja_invoice instanceof \App\Models\Credit) {
+ // $lines = $this->storecove_invoice->getCreditLines();
+ // $this->storecove_invoice->setCreditLines([]);
+ // $this->storecove_invoice->setInvoiceLines($lines);
+ // }
+
//set all taxmap countries - resolve the taxing country
$lines = $this->storecove_invoice->getInvoiceLines();
@@ -254,7 +262,7 @@ class StorecoveAdapter
//resolve and set the public identifier for the customer
$accounting_customer_party = $this->storecove_invoice->getAccountingCustomerParty();
- if (strlen($this->ninja_invoice->client->vat_number) > 2) {
+ if (strlen($this->ninja_invoice->client->vat_number ?? '') > 2) {
$id = preg_replace("/[^a-zA-Z0-9]/", "", $this->ninja_invoice->client->vat_number ?? '');
$scheme = $this->storecove->router->setInvoice($this->ninja_invoice)->resolveTaxScheme($this->ninja_invoice->client->country->iso_3166_2, $this->ninja_invoice->client->classification ?? 'individual');
$pi = new \App\Services\EDocument\Gateway\Storecove\Models\PublicIdentifiers($scheme, $id);
@@ -374,13 +382,13 @@ class StorecoveAdapter
$this->ninja_invoice->company->tax_data->regions->EU->has_sales_above_threshold;
// Is this B2B or B2C?
- $is_b2c = strlen($this->ninja_invoice->client->vat_number) < 2 ||
+ $is_b2c = strlen($this->ninja_invoice->client->vat_number ?? '') < 2 ||
!($this->ninja_invoice->client->has_valid_vat_number ?? false) ||
$this->ninja_invoice->client->classification == 'individual';
// B2C, under threshold, no Company VAT Registerd - must charge origin country VAT
- if ($is_b2c && !$is_over_threshold && strlen($this->ninja_invoice->company->settings->vat_number) < 2) {
+ if ($is_b2c && !$is_over_threshold && strlen($this->ninja_invoice->company->settings->vat_number ?? '') < 2) {
nlog("no company vat");
$this->nexus = $company_country_code;
} elseif ($is_b2c) {
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);
diff --git a/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php b/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php
index ea93e22185..6a600074e5 100644
--- a/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php
+++ b/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php
@@ -214,6 +214,15 @@ 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';
+ }
+ elseif($country == 'GLN'){
+ return 'routing_id';
+ }
+
$rules = $this->routing_rules[$country];
if (is_array($rules) && !is_array($rules[0])) {
diff --git a/app/Services/EDocument/Gateway/Transformers/StorecoveExpense.php b/app/Services/EDocument/Gateway/Transformers/StorecoveExpense.php
index 4bb317226b..c18cbb6503 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,12 @@ class StorecoveExpense
$vat_number = $pi->getId();
} elseif ($ident == 'id_number') {
$id_number = $pi->getId();
+ } elseif ($ident == 'routing_id') {
+ $routing_id = $pi->getId();
+ } else{
+ //Sometimes some very unusual identifiers are returned, we should always skip these.
+ // ie. IBAN, etc.
+ continue;
}
}
}
@@ -271,6 +276,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() ?? '',
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/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();
diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php
index 1c6997d2da..1aaaf89730 100644
--- a/app/Services/EDocument/Standards/Peppol.php
+++ b/app/Services/EDocument/Standards/Peppol.php
@@ -12,11 +12,12 @@
namespace App\Services\EDocument\Standards;
-use App\DataMapper\Tax\BaseRule;
+use App\Models\Credit;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Product;
use App\Helpers\Invoice\Taxer;
+use App\DataMapper\Tax\BaseRule;
use App\Services\AbstractService;
use App\Helpers\Invoice\InvoiceSum;
use InvoiceNinja\EInvoice\EInvoice;
@@ -46,8 +47,10 @@ use InvoiceNinja\EInvoice\Models\Peppol\TaxSubtotalType\TaxSubtotal;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\LineExtensionAmount;
+use InvoiceNinja\EInvoice\Models\Peppol\CreditNoteLineType\CreditNoteLine;
use InvoiceNinja\EInvoice\Models\Peppol\OrderReferenceType\OrderReference;
use InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal;
+use InvoiceNinja\EInvoice\Models\Peppol\BillingReferenceType\BillingReference;
use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\ClassifiedTaxCategory;
use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty;
use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty;
@@ -65,7 +68,7 @@ class Peppol extends AbstractService
*
*/
- private ?string $override_vat_number;
+ private string $override_vat_number = '';
/** @var array $InvoiceTypeCodes */
private array $InvoiceTypeCodes = [
@@ -131,7 +134,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 +143,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 +164,56 @@ 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;
+ }
+
+ /**
+ * 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
*
@@ -187,7 +234,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) {
@@ -200,13 +253,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,24 +261,29 @@ 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();
$this->setOrderReference()
+
->setTaxBreakdown()
->setPaymentTerms()
->addAttachments()
- ->standardPeppolRules();
+ ->standardPeppolRules()
+ ->setDocumentReference();
+
//isolate this class to only peppol changes
$this->p_invoice = $this->gateway
@@ -251,16 +302,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 +323,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 +336,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 +388,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 +430,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 +475,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,9 +495,48 @@ 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)];
}
+ /**
+ * 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
+
+ if($this->isCreditNote() && isset($this->invoice->e_invoice->CreditNote->BillingReference) && isset($this->invoice->e_invoice->CreditNote->BillingReference[0]->InvoiceDocumentReference)) {
+
+ $document_reference = new \InvoiceNinja\EInvoice\Models\Peppol\DocumentReferenceType\InvoiceDocumentReference();
+
+ $_idr = reset($this->invoice->e_invoice->CreditNote->BillingReference);
+
+ $d_id = new ID();
+ $d_id->value = $_idr->InvoiceDocumentReference->ID;
+
+ $document_reference->ID = $d_id;
+
+ if(isset($_idr->InvoiceDocumentReference->IssueDate)) {
+ $issue_date = new \DateTime($_idr->InvoiceDocumentReference->IssueDate);
+ $document_reference->IssueDate = $issue_date;
+ }
+
+ $billing_reference = new BillingReference();
+ $billing_reference->InvoiceDocumentReference = $document_reference;
+
+ $this->p_invoice->BillingReference = [$billing_reference];
+
+ return $this;
+ }
+
+
+ // We should only need to pull this in from the already stored object.
+ return $this;
+ }
/**
* setOrderReference
@@ -428,7 +565,7 @@ class Peppol extends AbstractService
// Only the invoice itself to start with:
$filename = $this->invoice->getFileName();
- $pdf = $this->invoice->service()->getInvoicePdf();
+ $pdf = $this->invoice instanceof \App\Models\Credit ? $this->invoice->service()->getCreditPdf($this->invoice->invitations->first()) : $this->invoice->service()->getInvoicePdf();
$mime_code = 'application/pdf';
$adr = new \InvoiceNinja\EInvoice\Models\Peppol\DocumentReferenceType\AdditionalDocumentReference();
@@ -482,14 +619,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
@@ -601,37 +738,42 @@ class Peppol extends AbstractService
{
$taxable = $this->getTaxable();
+ // 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();
$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 ? (string) round($amount - $totalTaxes, 2) : (string) $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($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;
@@ -654,9 +796,12 @@ class Peppol extends AbstractService
case Product::PRODUCT_TYPE_DIGITAL:
case Product::PRODUCT_TYPE_PHYSICAL:
case Product::PRODUCT_TYPE_SHIPPING:
- case Product::PRODUCT_TYPE_REDUCED_TAX:
$tax_type = 'S';
break;
+ case Product::PRODUCT_TYPE_REDUCED_TAX:
+ // $tax_type = 'AA';
+ $tax_type = 'S'; //2026-01-14 - using AA breaks PEPPOL VALIDATION!!
+ break;
case Product::PRODUCT_TYPE_EXEMPT:
$tax_type = 'E';
break;
@@ -933,6 +1078,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 = (string) $this->isCreditNote ? abs($item->quantity) : $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 = (string) 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
{
@@ -1030,18 +1318,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();
@@ -1119,11 +1407,13 @@ class Peppol extends AbstractService
$party->PartyIdentification[] = $pi;
- $pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme();
+ //// If this is intracommunity supply, ensure that the country prefix is on the party tax scheme
+ $pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme();
$companyID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CompanyID();
- $companyID->value = preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->client->vat_number);
+ $companyID->value = $this->ensureVatNumberPrefix($this->invoice->client->vat_number, $this->invoice->client->country->iso_3166_2);
$pts->CompanyID = $companyID;
+ //// If this is intracommunity supply, ensure that the country prefix is on the party tax scheme
$ts = new TaxScheme();
$id = new ID();
@@ -1142,8 +1432,8 @@ class Peppol extends AbstractService
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\EndpointID();
$id->value = $this->invoice->client->routing_id
- ?? preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->client->vat_number)
- ?? preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->client->id_number)
+ ?? preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->client->vat_number ?? '')
+ ?? preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->client->id_number ?? '')
?? 'fallback1234';
$id->schemeID = $this->resolveScheme(true);
@@ -1224,8 +1514,13 @@ class Peppol extends AbstractService
$location->Address = $address;
$delivery->DeliveryLocation = $location;
- if (isset($this->invoice->e_invoice->Invoice->Delivery[0]->ActualDeliveryDate->date)) {
- $delivery->ActualDeliveryDate = new \DateTime($this->invoice->e_invoice->Invoice->Delivery[0]->ActualDeliveryDate->date);
+ // Safely extract delivery date using data_get to handle missing properties
+ $delivery_date = data_get($this->invoice->e_invoice, 'Invoice.Delivery.0.ActualDeliveryDate.date')
+ ?? data_get($this->invoice->e_invoice, 'Invoice.Delivery.0.ActualDeliveryDate')
+ ?? null;
+
+ if ($delivery_date) {
+ $delivery->ActualDeliveryDate = new \DateTime($delivery_date);
}
return [$delivery];
@@ -1283,12 +1578,11 @@ class Peppol extends AbstractService
$total += $this->invoice->custom_surcharge4;
}
- return round($total, 2);
+ return round($this->normalizeAmount($total), 2);
}
///////////////// Helper Methods /////////////////////////
-
/**
* setInvoiceDefaults
*
@@ -1297,10 +1591,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;
}
}
@@ -1308,12 +1614,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;
}
}
@@ -1347,9 +1669,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];
}
@@ -1405,7 +1731,7 @@ class Peppol extends AbstractService
$tax_total = new TaxTotal();
$taxes = $this->calc->getTaxMap();
- if (count($taxes) < 1) {
+ if (count($taxes) < 1 || (count($taxes) == 1 && $this->invoice->total_taxes == 0)) {
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
@@ -1417,7 +1743,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;
@@ -1458,7 +1784,8 @@ class Peppol extends AbstractService
// Required: TaxAmount (BT-110)
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
- $tax_amount->amount = (string)$grouped_tax['total'];
+ // $tax_amount->amount = (string)$grouped_tax['total'];
+ $tax_amount->amount = (string)round($this->normalizeAmount($this->invoice->total_taxes), 2);
$tax_total->TaxAmount = $tax_amount;
// Required: TaxSubtotal (BG-23)
@@ -1469,9 +1796,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;
@@ -1479,7 +1806,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;
@@ -1490,6 +1817,11 @@ class Peppol extends AbstractService
$category_id = new ID();
$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';
+ // }
+
$tax_category->ID = $category_id;
// Required: TaxCategory Rate (BT-119)
@@ -1504,7 +1836,8 @@ class Peppol extends AbstractService
$tax_scheme->ID = $scheme_id;
$tax_category->TaxScheme = $tax_scheme;
- $tax_subtotal->TaxCategory = $this->globalTaxCategories[0];
+ $tax_subtotal->TaxCategory = $tax_category;
+ // $tax_subtotal->TaxCategory = $this->globalTaxCategories[0];
$tax_total->TaxSubtotal[] = $tax_subtotal;
@@ -1533,8 +1866,8 @@ class Peppol extends AbstractService
$country_code = $this->invoice->client->country->iso_3166_2;
- if (isset($this->ninja_invoice->company->tax_data->regions->EU->subregions->{$country_code}->vat_number)) {
- $this->override_vat_number = $this->ninja_invoice->company->tax_data->regions->EU->subregions->{$country_code}->vat_number;
+ if (isset($this->company->tax_data->regions->EU->subregions->{$country_code}->vat_number)) {
+ $this->override_vat_number = $this->company->tax_data->regions->EU->subregions->{$country_code}->vat_number;
}
}
}
@@ -1589,6 +1922,31 @@ class Peppol extends AbstractService
return '0037';
}
+ /**
+ * Ensures the VAT number has the correct country code prefix.
+ *
+ * @param string $vatNumber The raw VAT number.
+ * @param string $countryCode The 2-letter ISO country code.
+ * @return string The formatted VAT number with prefix.
+ */
+ private function ensureVatNumberPrefix(string $vatNumber, string $countryCode): string
+ {
+ // Handle Greece special case
+ $prefix = ($countryCode === 'GR') ? 'EL' : $countryCode;
+
+ // Clean the VAT number by removing non-alphanumeric characters
+ $cleanedVat = preg_replace("/[^a-zA-Z0-9]/", "", $vatNumber);
+
+ // Check if the VAT number already starts with the country prefix
+ // If it does, return it as-is (preserving any check digits like "AA" in "FRAA123456789")
+ if (str_starts_with(strtoupper($cleanedVat), strtoupper($prefix))) {
+ return $cleanedVat;
+ }
+
+ // If the prefix is missing, clean and prepend it
+ return $prefix . $cleanedVat;
+ }
+
public function getErrors(): array
{
return $this->errors;
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/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 @@
+ true];
+ }
+
+ public function checkInvoice(Invoice | Credit $invoice): array
{
$this->init($invoice->client->locale());
@@ -271,7 +277,7 @@ class EntityLevel implements EntityLevelInterface
}
//test legal entity id present
- if (!is_int($company->legal_entity_id)) {
+ if(intval($company->legal_entity_id) == 0){
$errors[] = ['field' => "You have not registered a legal entity id as yet."];
}
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/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
{
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/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/Email/EmailMailable.php b/app/Services/Email/EmailMailable.php
index 529355e0f9..127f6541e3 100644
--- a/app/Services/Email/EmailMailable.php
+++ b/app/Services/Email/EmailMailable.php
@@ -119,13 +119,19 @@ class EmailMailable extends Mailable
$file = $document->getFile();
+ if (empty($file)) {
+ nlog("EmailMailable: Document file is empty: {$document->url}");
+ return null;
+ }
+
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo, $file);
$mime = $mime ?: 'application/octet-stream';
finfo_close($finfo);
return Attachment::fromData(fn () => $file, $document->name)->withMime($mime);
- });
+ })
+ ->filter();
return $attachments->merge($documents)->toArray();
}
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 {
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/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php
index 7d80d5b1b8..2643647f89 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;
}
@@ -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? */
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/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/app/Services/Quickbooks/Models/QbInvoice.php b/app/Services/Quickbooks/Models/QbInvoice.php
index 5688948c7e..26b467b6b9 100644
--- a/app/Services/Quickbooks/Models/QbInvoice.php
+++ b/app/Services/Quickbooks/Models/QbInvoice.php
@@ -101,7 +101,53 @@ class QbInvoice implements SyncInterface
public function syncToForeign(array $records): void
{
+ foreach ($records as $invoice) {
+ if (!$invoice instanceof Invoice) {
+ continue;
+ }
+ // Check if sync direction allows push
+ if (!$this->service->syncable('invoice', \App\Enum\SyncDirection::PUSH)) {
+ continue;
+ }
+
+ try {
+ // Transform invoice to QuickBooks format
+ $qb_invoice_data = $this->invoice_transformer->ninjaToQb($invoice, $this->service);
+
+ // If updating, fetch SyncToken using existing find() method
+ if (isset($invoice->sync->qb_id) && !empty($invoice->sync->qb_id)) {
+ $existing_qb_invoice = $this->find($invoice->sync->qb_id);
+ if ($existing_qb_invoice) {
+ $qb_invoice_data['SyncToken'] = $existing_qb_invoice->SyncToken ?? '0';
+ }
+ }
+
+ // Create or update invoice in QuickBooks
+ $qb_invoice = \QuickBooksOnline\API\Facades\Invoice::create($qb_invoice_data);
+
+ if (isset($invoice->sync->qb_id) && !empty($invoice->sync->qb_id)) {
+ // Update existing invoice
+ $result = $this->service->sdk->Update($qb_invoice);
+ nlog("QuickBooks: Updated invoice {$invoice->id} (QB ID: {$invoice->sync->qb_id})");
+ } else {
+ // Create new invoice
+ $result = $this->service->sdk->Add($qb_invoice);
+
+ // Store QB ID in invoice sync
+ $sync = new InvoiceSync();
+ $sync->qb_id = data_get($result, 'Id') ?? data_get($result, 'Id.value');
+ $invoice->sync = $sync;
+ $invoice->saveQuietly();
+
+ nlog("QuickBooks: Created invoice {$invoice->id} (QB ID: {$sync->qb_id})");
+ }
+ } catch (\Exception $e) {
+ nlog("QuickBooks: Error pushing invoice {$invoice->id} to QuickBooks: {$e->getMessage()}");
+ // Continue with next invoice instead of failing completely
+ continue;
+ }
+ }
}
private function qbInvoiceUpdate(array $ninja_invoice_data, Invoice $invoice): void
diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php
index 65846cdcf0..793aabbe3c 100644
--- a/app/Services/Quickbooks/QuickbooksService.php
+++ b/app/Services/Quickbooks/QuickbooksService.php
@@ -62,25 +62,28 @@ class QuickbooksService
private function init(): self
{
- $config = [
- 'ClientID' => config('services.quickbooks.client_id'),
- 'ClientSecret' => config('services.quickbooks.client_secret'),
- 'auth_mode' => 'oauth2',
- 'scope' => "com.intuit.quickbooks.accounting",
- 'RedirectURI' => $this->testMode ? 'https://grok.romulus.com.au/quickbooks/authorized' : 'https://invoicing.co/quickbooks/authorized',
- 'baseUrl' => $this->testMode ? CoreConstants::SANDBOX_DEVELOPMENT : CoreConstants::QBO_BASEURL,
- ];
+ if(config('services.quickbooks.client_id'))
+ {
+ $config = [
+ 'ClientID' => config('services.quickbooks.client_id'),
+ 'ClientSecret' => config('services.quickbooks.client_secret'),
+ 'auth_mode' => 'oauth2',
+ 'scope' => "com.intuit.quickbooks.accounting",
+ 'RedirectURI' => config('services.quickbooks.redirect'),
+ 'baseUrl' => $this->testMode ? CoreConstants::SANDBOX_DEVELOPMENT : CoreConstants::QBO_BASEURL,
+ ];
- $merged = array_merge($config, $this->ninjaAccessToken());
+ $merged = array_merge($config, $this->ninjaAccessToken());
- $this->sdk = DataService::Configure($merged);
-
- $this->sdk->enableLog();
- $this->sdk->setMinorVersion("75");
- $this->sdk->throwExceptionOnError(true);
-
- $this->checkToken();
+ $this->sdk = DataService::Configure($merged);
+ $this->sdk->enableLog();
+ $this->sdk->setMinorVersion("75");
+ $this->sdk->throwExceptionOnError(true);
+
+ $this->checkToken();
+ }
+
$this->invoice = new QbInvoice($this);
$this->quote = new QbQuote($this);
@@ -131,6 +134,24 @@ class QuickbooksService
// return $this;
// }
+ /**
+ * Refresh the service after OAuth token has been updated.
+ * This reloads the company from the database and reinitializes the SDK
+ * with the new access token.
+ *
+ * @return self
+ */
+ public function refresh(): self
+ {
+ // Reload company from database to get fresh token data
+ $this->company = $this->company->fresh();
+
+ // Reinitialize the SDK with the updated token
+ $this->init();
+
+ return $this;
+ }
+
private function checkToken(): self
{
@@ -148,6 +169,7 @@ class QuickbooksService
}
nlog('Quickbooks token expired and could not be refreshed => ' .$this->company->company_key);
+
throw new \Exception('Quickbooks token expired and could not be refreshed');
}
@@ -198,4 +220,173 @@ class QuickbooksService
return isset($this->settings->{$entity}->direction) && ($this->settings->{$entity}->direction === $direction || $this->settings->{$entity}->direction === \App\Enum\SyncDirection::BIDIRECTIONAL);
}
+
+// [
+// QuickBooksOnline\API\Data\IPPAccount {#7706
+// +Id: "30",
+// +SyncToken: "0",
+// +MetaData: QuickBooksOnline\API\Data\IPPModificationMetaData {#7707
+// +CreatedByRef: null,
+// +CreateTime: "2024-05-22T14:46:30-07:00",
+// +LastModifiedByRef: null,
+// +LastUpdatedTime: "2024-05-22T14:46:30-07:00",
+// +LastChangedInQB: null,
+// +Synchronized: null,
+// },
+// +CustomField: null,
+// +AttachableRef: null,
+// +domain: null,
+// +status: null,
+// +sparse: null,
+// +Name: "Uncategorized Income",
+// +SubAccount: "false",
+// +ParentRef: null,
+// +Description: null,
+// +FullyQualifiedName: "Uncategorized Income",
+// +AccountAlias: null,
+// +TxnLocationType: null,
+// +Active: "true",
+// +Classification: "Revenue",
+// +AccountType: "Income",
+// +AccountSubType: "ServiceFeeIncome",
+// +AccountPurposes: null,
+// +AcctNum: null,
+// +AcctNumExtn: null,
+// +BankNum: null,
+// +OpeningBalance: null,
+// +OpeningBalanceDate: null,
+// +CurrentBalance: "0",
+// +CurrentBalanceWithSubAccounts: "0",
+// +CurrencyRef: "USD",
+// +TaxAccount: null,
+// +TaxCodeRef: null,
+// +OnlineBankingEnabled: null,
+// +FIName: null,
+// +JournalCodeRef: null,
+// +AccountEx: null,
+// },
+// ]
+ /**
+ * Fetch income accounts from QuickBooks.
+ *
+ * @return array Array of account objects with 'Id', 'Name', 'AccountType', etc.
+ */
+ public function fetchIncomeAccounts(): array
+ {
+ try {
+ if (!$this->sdk) {
+ return [];
+ }
+
+ $query = "SELECT * FROM Account WHERE AccountType = 'Income' AND Active = true";
+ $accounts = $this->sdk->Query($query);
+
+ return is_array($accounts) ? $accounts : [];
+ } catch (\Exception $e) {
+ nlog("Error fetching income accounts: {$e->getMessage()}");
+ return [];
+ }
+ }
+
+
+// [
+// QuickBooksOnline\API\Data\IPPAccount {#7709
+// +Id: "57",
+// +SyncToken: "0",
+// +MetaData: QuickBooksOnline\API\Data\IPPModificationMetaData {#7698
+// +CreatedByRef: null,
+// +CreateTime: "2024-05-27T10:17:24-07:00",
+// +LastModifiedByRef: null,
+// +LastUpdatedTime: "2024-05-27T10:17:24-07:00",
+// +LastChangedInQB: null,
+// +Synchronized: null,
+// },
+// +CustomField: null,
+// +AttachableRef: null,
+// +domain: null,
+// +status: null,
+// +sparse: null,
+// +Name: "Workers Compensation",
+// +SubAccount: "true",
+// +ParentRef: "11",
+// +Description: null,
+// +FullyQualifiedName: "Insurance:Workers Compensation",
+// +AccountAlias: null,
+// +TxnLocationType: null,
+// +Active: "true",
+// +Classification: "Expense",
+// +AccountType: "Expense",
+// +AccountSubType: "Insurance",
+// +AccountPurposes: null,
+// +AcctNum: null,
+// +AcctNumExtn: null,
+// +BankNum: null,
+// +OpeningBalance: null,
+// +OpeningBalanceDate: null,
+// +CurrentBalance: "0",
+// +CurrentBalanceWithSubAccounts: "0",
+// +CurrencyRef: "USD",
+// +TaxAccount: null,
+// +TaxCodeRef: null,
+// +OnlineBankingEnabled: null,
+// +FIName: null,
+// +JournalCodeRef: null,
+// +AccountEx: null,
+// },
+// ]
+ /**
+ * Fetch expense accounts from QuickBooks.
+ *
+ * @return array Array of account objects with 'Id', 'Name', 'AccountType', etc.
+ */
+ public function fetchExpenseAccounts(): array
+ {
+ try {
+ if (!$this->sdk) {
+ return [];
+ }
+
+ $query = "SELECT * FROM Account WHERE AccountType IN ('Expense', 'Cost of Goods Sold') AND Active = true";
+ $accounts = $this->sdk->Query($query);
+
+ return is_array($accounts) ? $accounts : [];
+ } catch (\Exception $e) {
+ nlog("Error fetching expense accounts: {$e->getMessage()}");
+ return [];
+ }
+ }
+
+ /**
+ * Format accounts for UI dropdown consumption.
+ *
+ * @param array $accounts Raw account objects from QuickBooks API
+ * @return array Formatted array with 'value' (ID) and 'label' (Name) for each account
+ */
+ public function formatAccountsForDropdown(array $accounts): array
+ {
+ $formatted = [];
+
+ foreach ($accounts as $account) {
+ $id = is_object($account) && isset($account->Id)
+ ? (string) $account->Id
+ : (is_array($account) && isset($account['Id']) ? (string) $account['Id'] : null);
+
+ $name = is_object($account) && isset($account->Name)
+ ? (string) $account->Name
+ : (is_array($account) && isset($account['Name']) ? (string) $account['Name'] : '');
+
+ if ($id && $name) {
+ $formatted[] = [
+ 'value' => $id,
+ 'label' => $name,
+ 'account_type' => is_object($account) && isset($account->AccountType)
+ ? (string) $account->AccountType
+ : (is_array($account) && isset($account['AccountType']) ? (string) $account['AccountType'] : ''),
+ ];
+ }
+ }
+
+ return $formatted;
+ }
+
}
diff --git a/app/Services/Quickbooks/SdkWrapper.php b/app/Services/Quickbooks/SdkWrapper.php
index 26a14d615a..50871eb996 100644
--- a/app/Services/Quickbooks/SdkWrapper.php
+++ b/app/Services/Quickbooks/SdkWrapper.php
@@ -55,6 +55,11 @@ class SdkWrapper
return $this->accessToken()->getRefreshToken();
}
+ public function revokeAccessToken()
+ {
+ return $this->sdk->getOAuth2LoginHelper()->revokeToken($this->accessToken()->getAccessToken());
+ }
+
public function company()
{
return $this->sdk->getCompanyInfo();
diff --git a/app/Services/Quickbooks/Transformers/BaseTransformer.php b/app/Services/Quickbooks/Transformers/BaseTransformer.php
index 52d4ae39ac..408ddd25ba 100644
--- a/app/Services/Quickbooks/Transformers/BaseTransformer.php
+++ b/app/Services/Quickbooks/Transformers/BaseTransformer.php
@@ -50,6 +50,21 @@ class BaseTransformer
return $currency ? (string) $currency->id : $this->company->settings->currency_id;
}
+ public function resolveTimezone(?string $timezone_name): string
+ {
+ if (empty($timezone_name)) {
+ return (string) $this->company->settings->timezone_id;
+ }
+
+ /** @var \App\Models\Timezone $timezone */
+ $timezone = app('timezones')->first(function ($t) use ($timezone_name) {
+ /** @var \App\Models\Timezone $t */
+ return $t->name === $timezone_name;
+ });
+
+ return $timezone ? (string) $timezone->id : (string) $this->company->settings->timezone_id;
+ }
+
public function getShipAddrCountry($data, $field)
{
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
diff --git a/app/Services/Quickbooks/Transformers/CompanyTransformer.php b/app/Services/Quickbooks/Transformers/CompanyTransformer.php
new file mode 100644
index 0000000000..b9d210715b
--- /dev/null
+++ b/app/Services/Quickbooks/Transformers/CompanyTransformer.php
@@ -0,0 +1,99 @@
+transform($qb_data);
+ }
+
+ public function ninjaToQb(): void
+ {
+ // Reserved for Ninja → QB sync when needed.
+ }
+
+ /**
+ * @param mixed $data IPPCompanyInfo object or array
+ * @return array{quickbooks: array, settings: array}
+ */
+ public function transform(mixed $data): array
+ {
+ $addr = $this->pickAddress($data);
+ $country_raw = data_get($addr, 'Country') ?? data_get($addr, 'CountryCode') ?? data_get($data, 'Country');
+ $country_id = $this->resolveCountry($country_raw);
+
+ $quickbooks = [
+ 'companyName' => data_get($data, 'CompanyName', '') ?: data_get($data, 'LegalName', ''),
+ ];
+
+ $settings = [
+ 'address1' => data_get($addr, 'Line1', ''),
+ 'address2' => data_get($addr, 'Line2', ''),
+ 'city' => data_get($addr, 'City', ''),
+ 'state' => data_get($addr, 'CountrySubDivisionCode', ''),
+ 'postal_code' => data_get($addr, 'PostalCode', ''),
+ 'country_id' => $country_id,
+ 'phone' => $this->pickPhone($data),
+ 'email' => $this->pickEmail($data),
+ 'website' => data_get($data, 'WebAddr', '') ?: data_get($data, 'CompanyURL', ''),
+ 'timezone_id' => $this->resolveTimezone(data_get($data, 'DefaultTimeZone')),
+ ];
+
+ return [
+ 'quickbooks' => $quickbooks,
+ 'settings' => $settings,
+ ];
+ }
+
+ /**
+ * Prefer CompanyAddr, then LegalAddr, then CustomerCommunicationAddr.
+ *
+ * @param mixed $data
+ * @return object|array|null
+ */
+ private function pickAddress(mixed $data)
+ {
+ $addr = data_get($data, 'CompanyAddr') ?? data_get($data, 'LegalAddr') ?? data_get($data, 'CustomerCommunicationAddr');
+
+ return is_object($addr) ? $addr : (is_array($addr) ? $addr : []);
+ }
+
+ private function pickPhone(mixed $data): string
+ {
+ $phone = data_get($data, 'PrimaryPhone.FreeFormNumber');
+
+ return is_string($phone) ? $phone : '';
+ }
+
+ private function pickEmail(mixed $data): string
+ {
+ $email = data_get($data, 'Email.Address') ?? data_get($data, 'CustomerCommunicationEmailAddr.Address') ?? data_get($data, 'CompanyEmailAddr');
+
+ return is_string($email) ? $email : '';
+ }
+}
diff --git a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php
index 3e2c4e2f8c..bdc300c14d 100644
--- a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php
+++ b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php
@@ -29,8 +29,171 @@ class InvoiceTransformer extends BaseTransformer
return $this->transform($qb_data);
}
- public function ninjaToQb()
+ public function ninjaToQb(Invoice $invoice, \App\Services\Quickbooks\QuickbooksService $qb_service): array
{
+ // Get client's QuickBooks ID
+ $client_qb_id = $invoice->client->sync->qb_id ?? null;
+
+ // If client doesn't have QB ID, create it first
+ if (!$client_qb_id) {
+ $client_qb_id = $this->createClientInQuickbooks($invoice->client, $qb_service);
+ }
+
+ // Build line items
+ $line_items = [];
+ $line_num = 1;
+
+ foreach ($invoice->line_items as $line_item) {
+ // Get product's QuickBooks ID if it exists
+ $product = \App\Models\Product::where('company_id', $this->company->id)
+ ->where('product_key', $line_item->product_key)
+ ->first();
+
+ if (!$product || !isset($product->sync->qb_id)) {
+ // If product doesn't exist in QB, we'll need to create it or use a default item
+ // For now, skip items without QB product mapping
+ continue;
+ }
+
+ $tax_code = 'TAX';
+ if (isset($line_item->tax_id)) {
+ // Check if tax exempt (similar to test pattern)
+ if (in_array($line_item->tax_id, [5, 8])) {
+ $tax_code = 'NON';
+ }
+ }
+
+ $line_payload = [
+ 'LineNum' => $line_num,
+ 'DetailType' => 'SalesItemLineDetail',
+ 'SalesItemLineDetail' => [
+ 'ItemRef' => [
+ 'value' => $product->sync->qb_id,
+ ],
+ 'Qty' => $line_item->quantity ?? 1,
+ 'UnitPrice' => $line_item->cost ?? 0,
+ 'TaxCodeRef' => [
+ 'value' => $tax_code,
+ ],
+ ],
+ 'Description' => $line_item->notes ?? '',
+ 'Amount' => $line_item->line_total ?? ($line_item->cost * ($line_item->quantity ?? 1)),
+ ];
+
+
+ //check here if we need to inject the income account reference
+ // $line_payload['AccountRef'] = ['value' => $income_account_qb_id];
+
+ $line_items[] = $line_payload;
+
+ $line_num++;
+ }
+
+ // Get primary contact email
+ $primary_contact = $invoice->client->contacts()->orderBy('is_primary', 'desc')->first();
+ $email = $primary_contact?->email ?? $invoice->client->contacts()->first()?->email ?? '';
+
+ // Build invoice data
+ $invoice_data = [
+ 'Line' => $line_items,
+ 'CustomerRef' => [
+ 'value' => $client_qb_id,
+ ],
+ 'BillEmail' => [
+ 'Address' => $email,
+ ],
+ 'TxnDate' => $invoice->date,
+ 'DueDate' => $invoice->due_date,
+ 'TotalAmt' => $invoice->amount,
+ 'DocNumber' => $invoice->number,
+ 'ApplyTaxAfterDiscount' => true,
+ 'PrintStatus' => 'NeedToPrint',
+ 'EmailStatus' => 'NotSet',
+ 'GlobalTaxCalculation' => 'TaxExcluded',
+ ];
+
+ // Add optional fields
+ if ($invoice->public_notes) {
+ $invoice_data['CustomerMemo'] = [
+ 'value' => $invoice->public_notes,
+ ];
+ }
+
+ if ($invoice->private_notes) {
+ $invoice_data['PrivateNote'] = $invoice->private_notes;
+ }
+
+ if ($invoice->po_number) {
+ $invoice_data['PONumber'] = $invoice->po_number;
+ }
+
+ // If invoice already has a QB ID, include it for updates
+ // Note: SyncToken will be fetched in QbInvoice::syncToForeign using the existing find() method
+ if (isset($invoice->sync->qb_id) && !empty($invoice->sync->qb_id)) {
+ $invoice_data['Id'] = $invoice->sync->qb_id;
+ }
+
+ return $invoice_data;
+ }
+
+ /**
+ * Create a client in QuickBooks if it doesn't exist.
+ *
+ * @param \App\Models\Client $client
+ * @param \App\Services\Quickbooks\QuickbooksService $qb_service
+ * @return string The QuickBooks customer ID
+ */
+ private function createClientInQuickbooks(\App\Models\Client $client, \App\Services\Quickbooks\QuickbooksService $qb_service): string
+ {
+ $primary_contact = $client->contacts()->orderBy('is_primary', 'desc')->first();
+
+ $customer_data = [
+ 'DisplayName' => $client->present()->name(),
+ 'PrimaryEmailAddr' => [
+ 'Address' => $primary_contact?->email ?? '',
+ ],
+ 'PrimaryPhone' => [
+ 'FreeFormNumber' => $primary_contact?->phone ?? '',
+ ],
+ 'CompanyName' => $client->present()->name(),
+ 'BillAddr' => [
+ 'Line1' => $client->address1 ?? '',
+ 'City' => $client->city ?? '',
+ 'CountrySubDivisionCode' => $client->state ?? '',
+ 'PostalCode' => $client->postal_code ?? '',
+ 'Country' => $client->country?->iso_3166_3 ?? '',
+ ],
+ 'ShipAddr' => [
+ 'Line1' => $client->shipping_address1 ?? '',
+ 'City' => $client->shipping_city ?? '',
+ 'CountrySubDivisionCode' => $client->shipping_state ?? '',
+ 'PostalCode' => $client->shipping_postal_code ?? '',
+ 'Country' => $client->shipping_country?->iso_3166_3 ?? '',
+ ],
+ 'GivenName' => $primary_contact?->first_name ?? '',
+ 'FamilyName' => $primary_contact?->last_name ?? '',
+ 'PrintOnCheckName' => $client->present()->primary_contact_name(),
+ 'Notes' => $client->public_notes ?? '',
+ 'BusinessNumber' => $client->id_number ?? '',
+ 'Active' => $client->deleted_at ? false : true,
+ 'V4IDPseudonym' => $client->client_hash ?? \Illuminate\Support\Str::random(32),
+ 'WebAddr' => $client->website ?? '',
+ ];
+
+ $customer = \QuickBooksOnline\API\Facades\Customer::create($customer_data);
+ $resulting_customer = $qb_service->sdk->Add($customer);
+
+ $qb_id = data_get($resulting_customer, 'Id') ?? data_get($resulting_customer, 'Id.value');
+
+ // Store QB ID in client sync
+ $sync = new \App\DataMapper\ClientSync();
+ $sync->qb_id = $qb_id;
+ $client->sync = $sync;
+ $client->saveQuietly();
+
+ nlog("QuickBooks: Auto-created client {$client->id} in QuickBooks (QB ID: {$qb_id})");
+
+ return $qb_id;
}
public function transform($qb_data)
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/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;
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(
diff --git a/app/Services/Scheduler/EmailReport.php b/app/Services/Scheduler/EmailReport.php
index eded156c03..3aa60a3fc3 100644
--- a/app/Services/Scheduler/EmailReport.php
+++ b/app/Services/Scheduler/EmailReport.php
@@ -58,6 +58,10 @@ class EmailReport
$start_end_dates = $this->calculateStartAndEndDates($this->scheduler->parameters, $this->scheduler->company);
$data = $this->scheduler->parameters;
+
+ if(!isset($data['user_id'])) {
+ $data['user_id'] = $this->scheduler->user_id;
+ }
$data['start_date'] = $start_end_dates[0];
$data['end_date'] = $start_end_dates[1];
diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php
index 6391d59e7c..1b84d64ca0 100644
--- a/app/Services/Template/TemplateService.php
+++ b/app/Services/Template/TemplateService.php
@@ -161,7 +161,7 @@ class TemplateService
$this->twig->addFilter($filter);
$allowedTags = ['if', 'for', 'set', 'filter'];
- $allowedFilters = ['default', 'groupBy','capitalize', 'abs', 'date_modify', 'keys', 'join', 'reduce', 'format_date','json_decode','date_modify','trim','round','format_spellout_number','split', 'reduce','replace', 'escape', 'e', 'reverse', 'shuffle', 'slice', 'batch', 'title', 'sort', 'split', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br','striptags','markdown_to_html'];
+ $allowedFilters = ['url_encode','default', 'groupBy','capitalize', 'abs', 'date_modify', 'keys', 'join', 'reduce', 'format_date','json_decode','date_modify','trim','round','format_spellout_number','split', 'reduce','replace', 'escape', 'e', 'reverse', 'shuffle', 'slice', 'batch', 'title', 'sort', 'split', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br','striptags','markdown_to_html'];
$allowedFunctions = ['range', 'cycle', 'constant', 'date','img','t'];
$allowedProperties = ['type_id'];
// $allowedMethods = ['img','t'];
@@ -828,6 +828,11 @@ class TemplateService
foreach ($refund['invoices'] as $refunded_invoice) {
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
+
+ if (!$invoice) {
+ continue;
+ }
+
$amount = Number::formatMoney($refunded_invoice['amount'], $payment->client);
$notes = ctrans('texts.status_partially_refunded_amount', ['amount' => $amount]);
diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php
index 7de04e15db..6795573d76 100644
--- a/app/Transformers/CompanyTransformer.php
+++ b/app/Transformers/CompanyTransformer.php
@@ -223,10 +223,11 @@ class CompanyTransformer extends EntityTransformer
'smtp_local_domain' => (string) $company->smtp_local_domain ?? '',
'smtp_verify_peer' => (bool) $company->smtp_verify_peer,
'e_invoice' => $company->e_invoice ?: new \stdClass(),
- 'has_quickbooks_token' => $company->quickbooks ? true : false,
- 'is_quickbooks_token_active' => $company->quickbooks?->accessTokenKey ?? false,
- 'legal_entity_id' => $company->legal_entity_id ?? null,
+ 'legal_entity_id' => $company->legal_entity_id ? (int) $company->legal_entity_id : null,
+ 'quickbooks' => $company->getRawOriginal('quickbooks') ? $company->quickbooks->toArray() : null,
];
+
+ return $data;
}
private function isLarge(Company $company): bool
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/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
diff --git a/app/Utils/Helpers.php b/app/Utils/Helpers.php
index f6fa5d8eec..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'] = '';
@@ -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/HtmlEngine.php b/app/Utils/HtmlEngine.php
index 47977b273e..948a16c1f7 100644
--- a/app/Utils/HtmlEngine.php
+++ b/app/Utils/HtmlEngine.php
@@ -194,6 +194,8 @@ class HtmlEngine
$data['$payment_schedule'] = ['value' => '', 'label' => ctrans('texts.payment_schedule')];
$data['$payment_schedule_interval'] = ['value' => '', 'label' => ctrans('texts.payment_schedule')];
+ $data['$days_overdue'] = ['value' => $this->daysOverdue(), 'label' => ctrans('texts.overdue')];
+
if(method_exists($this->entity, 'paymentSchedule')) {
$data['$payment_schedule'] = ['value' => $this->entity->paymentSchedule(true), 'label' => ctrans('texts.payment_schedule')];
$data['$payment_schedule_interval'] = ['value' => $this->entity->paymentScheduleInterval(), 'label' => ctrans('texts.payment_schedule')];
@@ -208,15 +210,13 @@ 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) {
- $data['$entity'] = ['value' => ctrans('texts.credit'), 'label' => ctrans('texts.credit')];
- }
- else {
- $data['$entity'] = ['value' => ctrans('texts.invoice'), 'label' => ctrans('texts.invoice')];
- }
-
+ $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 +275,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')];
@@ -312,9 +316,16 @@ 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') {
+ if ($this->entity_string == 'credit' || ($this->entity_string == 'invoice' && $this->client->peppolSendingEnabled() && $this->entity->amount < 0)) {
+
+ $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')];
@@ -347,6 +358,9 @@ class HtmlEngine
$data['$invoice.custom2'] = &$data['$credit.custom2'];
$data['$invoice.custom3'] = &$data['$credit.custom3'];
$data['$invoice.custom4'] = &$data['$credit.custom4'];
+
+ $data['$invoice.number'] = &$data['$number'];
+ $data['$invoice.total'] = &$data['$credit_total'];
}
$data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' => ''];
@@ -378,7 +392,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')];
@@ -475,6 +496,12 @@ class HtmlEngine
$data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: ' ', 'label' => ctrans('texts.credit_balance')];
+ if($this->entity_string == 'invoice' && $this->client->peppolSendingEnabled() && $this->entity->amount < 0) {
+ $data['$invoice.total'] = &$data['$credit_amount'];
+ $data['$invoice_total_raw'] = ['value' => $this->entity_calc->getTotal(), 'label' => ctrans('texts.credit_total')];
+ $data['$invoice.amount'] = &$data['$credit_amount'];
+ }
+
$data['$credit_number'] = &$data['$number'];
$data['$credit_no'] = &$data['$number'];
$data['$credit.credit_no'] = &$data['$number'];
@@ -667,6 +694,7 @@ class HtmlEngine
$data['$task.rate'] = ['value' => '', 'label' => ctrans('texts.rate')];
$data['$task.cost'] = ['value' => '', 'label' => ctrans('texts.rate')];
$data['$task.hours'] = ['value' => '', 'label' => ctrans('texts.hours')];
+ $data['$task.total_hours'] = ['value' => $this->totalTaskHours(), 'label' => ctrans('texts.total_hours')];
$data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$task.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')];
@@ -816,7 +844,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' => ''];
}
@@ -874,6 +902,48 @@ Código seguro de verificación (CSV): {$verifactu_log->status}";
return "{$text} ";
}
+
+ /**
+ * totalTaskHours
+ *
+ * calculates the total hours of all tasks in the invoice
+ *
+ * @return int
+ */
+ private function totalTaskHours()
+ {
+ return collect($this->entity->line_items)
+ ->filter(function ($item) {
+ return $item->type_id == '2';
+ })
+ ->sum('quantity');
+ }
+
+ /**
+ * daysOverdue
+ *
+ * calculates the number of days overdue the entity is
+ *
+ * @return int
+ */
+ private function daysOverdue()
+ {
+ if($this->entity->partial > 0 && !empty($this->entity->partial_due_date)) {
+
+ $days_overdue = \Carbon\Carbon::parse($this->entity->partial_due_date)->diffInDays(now()->startOfDay()->setTimezone($this->entity->company->timezone()->name));
+
+ return max($days_overdue, 0);
+ }
+
+ if(!empty($this->entity->due_date)) {
+
+ $days_overdue = \Carbon\Carbon::parse($this->entity->due_date)->diffInDays(now()->startOfDay()->setTimezone($this->entity->company->timezone()->name));
+
+ return max($days_overdue, 0);
+ }
+
+ return 0;
+ }
private function getPaymentMeta(\App\Models\Payment $payment)
{
@@ -917,11 +987,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 = '';
@@ -1108,6 +1177,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();
diff --git a/app/Utils/TemplateEngine.php b/app/Utils/TemplateEngine.php
index 0e72820588..f30730df69 100644
--- a/app/Utils/TemplateEngine.php
+++ b/app/Utils/TemplateEngine.php
@@ -233,7 +233,7 @@ class TemplateEngine
$data['title'] = '';
$data['body'] = '$body';
$data['footer'] = '';
- $data['logo'] = $user->company()->present()->logo();
+ $data['logo'] = $user->company()->present()->logo($this->settings);
if ($this->entity_obj->client()->exists()) {
$data = array_merge($data, Helpers::sharedEmailVariables($this->entity_obj->client));
diff --git a/app/Utils/Traits/Inviteable.php b/app/Utils/Traits/Inviteable.php
index 59163053e2..3f74e28abe 100644
--- a/app/Utils/Traits/Inviteable.php
+++ b/app/Utils/Traits/Inviteable.php
@@ -83,6 +83,27 @@ trait Inviteable
}
+ /**
+ * stubbed for future when we want to change from svg to png qrcodes.
+ */
+ // public function getPaymentQrCodeRawPng()
+ // {
+
+ // $result = \Endroid\QrCode\Builder\Builder::create()
+ // ->writer(new \Endroid\QrCode\Writer\PngWriter())
+ // ->data($this->getPaymentLink())
+ // ->encoding(new \Endroid\QrCode\Encoding\Encoding('UTF-8'))
+ // ->errorCorrectionLevel(\Endroid\QrCode\ErrorCorrectionLevel::Medium)
+ // ->size(150)
+ // ->margin(0)
+ // ->build();
+
+ // $png = base64_encode($result->getString());
+
+ // return ' ';
+
+ // }
+
public function getUnsubscribeLink()
{
if (Ninja::isHosted()) {
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
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;
diff --git a/app/Utils/Traits/WithSecureContext.php b/app/Utils/Traits/WithSecureContext.php
index 77aae32e15..e49a60d6a0 100644
--- a/app/Utils/Traits/WithSecureContext.php
+++ b/app/Utils/Traits/WithSecureContext.php
@@ -23,7 +23,7 @@ trait WithSecureContext
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
- public function getContext(string $key): mixed
+ public function getContext(?string $key): mixed
{
$context = \Illuminate\Support\Facades\Cache::get($key) ?? [];
diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php
index c0868d5f4a..6c10132ce7 100644
--- a/app/Utils/VendorHtmlEngine.php
+++ b/app/Utils/VendorHtmlEngine.php
@@ -12,19 +12,20 @@
namespace App\Utils;
+use Exception;
+use App\Utils\Ninja;
use App\Models\Account;
use App\Models\Country;
-use App\Models\CreditInvitation;
-use App\Models\InvoiceInvitation;
-use App\Models\PurchaseOrderInvitation;
-use App\Models\QuoteInvitation;
-use App\Models\RecurringInvoiceInvitation;
use App\Utils\Traits\AppSetup;
-use App\Utils\Traits\DesignCalculator;
+use App\Models\QuoteInvitation;
+use App\Models\CreditInvitation;
use App\Utils\Traits\MakesDates;
-use Exception;
+use App\Models\InvoiceInvitation;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
+use App\Utils\Traits\DesignCalculator;
+use App\Models\PurchaseOrderInvitation;
+use App\Models\RecurringInvoiceInvitation;
/**
* Note the premise used here is that any currencies will be formatted back to the company currency and not
@@ -350,8 +351,13 @@ class VendorHtmlEngine
$data['$signature'] = ['value' => $this->settings->email_signature ?: ' ', 'label' => ''];
$data['$emailSignature'] = &$data['$signature'];
- $logo = $this->company->present()->logo_base64($this->settings);
+ if (Ninja::isHosted()) {
+ $logo = $this->company->present()->logo($this->settings);
+ } else {
+ $logo = $this->company->present()->logo_base64($this->settings);
+ }
+
$data['$company.logo'] = ['value' => $logo ?: ' ', 'label' => ctrans('texts.logo')];
$data['$company_logo'] = &$data['$company.logo'];
$data['$company1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
diff --git a/composer.json b/composer.json
index cad686c04e..b0e65f64ab 100644
--- a/composer.json
+++ b/composer.json
@@ -32,12 +32,12 @@
"type": "project",
"require": {
"php": ">=8.2",
+ "ext-bcmath": "*",
"ext-curl": "*",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
- "ext-bcmath": "*",
"afosto/yaac": "^1.5",
"asm/php-ansible": "dev-main",
"authorizenet/authorizenet": "^2.0",
@@ -51,6 +51,7 @@
"braintree/braintree_php": "^6.28",
"btcpayserver/btcpayserver-greenfield-php": "^2.6",
"checkout/checkout-sdk-php": "^3.0",
+ "endroid/qr-code": "^5",
"eway/eway-rapid-php": "^1.3",
"fakerphp/faker": "^1.14",
"getbrevo/brevo-php": "^1.0",
@@ -102,7 +103,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",
@@ -219,6 +220,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..435bca4d33 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": "d044985779d84769736a51fd6217b050",
"packages": [
{
"name": "afosto/yaac",
@@ -61,16 +61,16 @@
},
{
"name": "apimatic/core",
- "version": "0.3.16",
+ "version": "0.3.17",
"source": {
"type": "git",
"url": "https://github.com/apimatic/core-lib-php.git",
- "reference": "ae4ab4ca26a41be41718f33c703d67b7a767c07b"
+ "reference": "a48a583f686ee3786432b976c795a2817ec095b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/apimatic/core-lib-php/zipball/ae4ab4ca26a41be41718f33c703d67b7a767c07b",
- "reference": "ae4ab4ca26a41be41718f33c703d67b7a767c07b",
+ "url": "https://api.github.com/repos/apimatic/core-lib-php/zipball/a48a583f686ee3786432b976c795a2817ec095b3",
+ "reference": "a48a583f686ee3786432b976c795a2817ec095b3",
"shasum": ""
},
"require": {
@@ -110,9 +110,9 @@
],
"support": {
"issues": "https://github.com/apimatic/core-lib-php/issues",
- "source": "https://github.com/apimatic/core-lib-php/tree/0.3.16"
+ "source": "https://github.com/apimatic/core-lib-php/tree/0.3.17"
},
- "time": "2025-11-25T04:42:27+00:00"
+ "time": "2026-01-27T05:14:10+00:00"
},
{
"name": "apimatic/core-interfaces",
@@ -381,16 +381,16 @@
},
{
"name": "awobaz/compoships",
- "version": "2.5.1",
+ "version": "2.5.4",
"source": {
"type": "git",
"url": "https://github.com/topclaudy/compoships.git",
- "reference": "d8de30b57949d6021bb0312105f6d9d0920266b6"
+ "reference": "dcae8012a8704fc2acd8dce2d8a1b35ce292adbe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/topclaudy/compoships/zipball/d8de30b57949d6021bb0312105f6d9d0920266b6",
- "reference": "d8de30b57949d6021bb0312105f6d9d0920266b6",
+ "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.1"
+ "source": "https://github.com/topclaudy/compoships/tree/2.5.4"
},
"funding": [
{
@@ -439,7 +439,7 @@
"type": "custom"
}
],
- "time": "2025-10-10T14:07:12+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.363.3",
+ "version": "3.369.22",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "0ec2218d32e291b988b1602583032ca5d11f8e8d"
+ "reference": "fe83cbc3adb5ed384179ac6d63531aadde0198e3"
},
"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/fe83cbc3adb5ed384179ac6d63531aadde0198e3",
+ "reference": "fe83cbc3adb5ed384179ac6d63531aadde0198e3",
"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": "^v5.4.45 || ^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.369.22"
},
- "time": "2025-11-26T19:05:22+00:00"
+ "time": "2026-01-28T19:19:00+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",
@@ -1171,16 +1179,16 @@
},
{
"name": "btcpayserver/btcpayserver-greenfield-php",
- "version": "v2.8.1",
+ "version": "v2.9.0",
"source": {
"type": "git",
"url": "https://github.com/btcpayserver/btcpayserver-greenfield-php.git",
- "reference": "3118f9e4e04590f53b2560866238af463153b2cf"
+ "reference": "60e6be57f9cd08dbe8f851d056358d1df9f07968"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/3118f9e4e04590f53b2560866238af463153b2cf",
- "reference": "3118f9e4e04590f53b2560866238af463153b2cf",
+ "url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/60e6be57f9cd08dbe8f851d056358d1df9f07968",
+ "reference": "60e6be57f9cd08dbe8f851d056358d1df9f07968",
"shasum": ""
},
"require": {
@@ -1219,9 +1227,9 @@
"description": "BTCPay Server Greenfield API PHP client library.",
"support": {
"issues": "https://github.com/btcpayserver/btcpayserver-greenfield-php/issues",
- "source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v2.8.1"
+ "source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v2.9.0"
},
- "time": "2024-11-22T16:34:09+00:00"
+ "time": "2026-01-21T11:31:48+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
@@ -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",
@@ -2116,16 +2124,16 @@
},
{
"name": "dompdf/php-font-lib",
- "version": "1.0.1",
+ "version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
- "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d"
+ "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
- "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
+ "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
+ "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"shasum": ""
},
"require": {
@@ -2133,7 +2141,7 @@
"php": "^7.1 || ^8.0"
},
"require-dev": {
- "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
+ "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
},
"type": "library",
"autoload": {
@@ -2155,31 +2163,31 @@
"homepage": "https://github.com/dompdf/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
- "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1"
+ "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
},
- "time": "2024-12-02T14:37:59+00:00"
+ "time": "2026-01-20T14:10:26+00:00"
},
{
"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": {
@@ -2201,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",
@@ -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",
@@ -2899,22 +2907,22 @@
},
{
"name": "goetas-webservices/xsd2php-runtime",
- "version": "v0.2.17",
+ "version": "0.2.18",
"source": {
"type": "git",
"url": "https://github.com/goetas-webservices/xsd2php-runtime.git",
- "reference": "be15c48cda6adfab82e180a69dfa1937e208cfe1"
+ "reference": "1215ce8504f2726b1a0c5025478da86a2791a0ba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/goetas-webservices/xsd2php-runtime/zipball/be15c48cda6adfab82e180a69dfa1937e208cfe1",
- "reference": "be15c48cda6adfab82e180a69dfa1937e208cfe1",
+ "url": "https://api.github.com/repos/goetas-webservices/xsd2php-runtime/zipball/1215ce8504f2726b1a0c5025478da86a2791a0ba",
+ "reference": "1215ce8504f2726b1a0c5025478da86a2791a0ba",
"shasum": ""
},
"require": {
"jms/serializer": "^1.2|^2.0|^3.0",
"php": ">=7.1",
- "symfony/yaml": "^2.2|^3.0|^4.0|^5.0|^6.0|^7.0"
+ "symfony/yaml": "^2.2|^3.0|^4.0|^5.0|^6.0|^7.0|^8.0"
},
"conflict": {
"jms/serializer": "1.4.1|1.6.1|1.6.2"
@@ -2953,26 +2961,26 @@
],
"support": {
"issues": "https://github.com/goetas-webservices/xsd2php-runtime/issues",
- "source": "https://github.com/goetas-webservices/xsd2php-runtime/tree/v0.2.17"
+ "source": "https://github.com/goetas-webservices/xsd2php-runtime/tree/0.2.18"
},
- "time": "2024-04-12T22:55:31+00:00"
+ "time": "2026-01-16T19:24:00+00:00"
},
{
"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",
@@ -3022,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.421.0",
+ "version": "v0.430.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
- "reference": "d84e7301a52405677807564dab6b1a112dfd03bd"
+ "reference": "b936a8bb717af0367c6ebd45d3dcb69642fce340"
},
"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/b936a8bb717af0367c6ebd45d3dcb69642fce340",
+ "reference": "b936a8bb717af0367c6ebd45d3dcb69642fce340",
"shasum": ""
},
"require": {
@@ -3066,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.421.0"
+ "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.430.0"
},
- "time": "2025-11-23T01:06:22+00:00"
+ "time": "2026-01-26T00:56:27+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",
@@ -3095,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",
@@ -3103,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."
@@ -3128,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": {
@@ -3180,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": [
{
@@ -3192,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",
@@ -4026,16 +4034,16 @@
},
{
"name": "horstoeko/zugferd",
- "version": "v1.0.117",
+ "version": "v1.0.120",
"source": {
"type": "git",
"url": "https://github.com/horstoeko/zugferd.git",
- "reference": "7c2fdb58e0910e199b1fd2162ae5d33a1e62e933"
+ "reference": "c143ec75a7ffc62e2b64a87d175db9f13ab843c1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/7c2fdb58e0910e199b1fd2162ae5d33a1e62e933",
- "reference": "7c2fdb58e0910e199b1fd2162ae5d33a1e62e933",
+ "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/c143ec75a7ffc62e2b64a87d175db9f13ab843c1",
+ "reference": "c143ec75a7ffc62e2b64a87d175db9f13ab843c1",
"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.120"
},
- "time": "2025-11-17T14:09:15+00:00"
+ "time": "2026-01-07T09:45:19+00:00"
},
{
"name": "horstoeko/zugferdvisualizer",
@@ -4480,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": {
@@ -4527,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",
@@ -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.48.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "d6b16e72a98c2ad3257ec6b3f1f00532c3b1c2fc"
+ "reference": "5b23ab29087dbcb13077e5c049c431ec4b82f236"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/d6b16e72a98c2ad3257ec6b3f1f00532c3b1c2fc",
- "reference": "d6b16e72a98c2ad3257ec6b3f1f00532c3b1c2fc",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/5b23ab29087dbcb13077e5c049c431ec4b82f236",
+ "reference": "5b23ab29087dbcb13077e5c049c431ec4b82f236",
"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": "2026-01-20T15:26:20+00:00"
},
{
"name": "laravel/octane",
- "version": "v2.13.1",
+ "version": "v2.13.5",
"source": {
"type": "git",
"url": "https://github.com/laravel/octane.git",
- "reference": "20b741badaa22cae73b87ffc4d979f3a7f06db25"
+ "reference": "c343716659c280a7613a0c10d3241215512355ee"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/octane/zipball/20b741badaa22cae73b87ffc4d979f3a7f06db25",
- "reference": "20b741badaa22cae73b87ffc4d979f3a7f06db25",
+ "url": "https://api.github.com/repos/laravel/octane/zipball/c343716659c280a7613a0c10d3241215512355ee",
+ "reference": "c343716659c280a7613a0c10d3241215512355ee",
"shasum": ""
},
"require": {
@@ -5379,20 +5386,20 @@
"issues": "https://github.com/laravel/octane/issues",
"source": "https://github.com/laravel/octane"
},
- "time": "2025-10-27T12:05:17+00:00"
+ "time": "2026-01-22T17:24:46+00:00"
},
{
"name": "laravel/prompts",
- "version": "v0.3.8",
+ "version": "v0.3.11",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "096748cdfb81988f60090bbb839ce3205ace0d35"
+ "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35",
- "reference": "096748cdfb81988f60090bbb839ce3205ace0d35",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/dd2a2ed95acacbcccd32fd98dee4c946ae7a7217",
+ "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217",
"shasum": ""
},
"require": {
@@ -5436,22 +5443,22 @@
"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.11"
},
- "time": "2025-11-21T20:52:52+00:00"
+ "time": "2026-01-27T02:55:06+00:00"
},
{
"name": "laravel/scout",
- "version": "v10.22.1",
+ "version": "v10.23.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/scout.git",
- "reference": "13ed8e0eeaddd894bf360b85cb873980de19dbaf"
+ "reference": "ef12fcef311421fe9f2e2425416daa17c45b8ae1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/scout/zipball/13ed8e0eeaddd894bf360b85cb873980de19dbaf",
- "reference": "13ed8e0eeaddd894bf360b85cb873980de19dbaf",
+ "url": "https://api.github.com/repos/laravel/scout/zipball/ef12fcef311421fe9f2e2425416daa17c45b8ae1",
+ "reference": "ef12fcef311421fe9f2e2425416daa17c45b8ae1",
"shasum": ""
},
"require": {
@@ -5518,20 +5525,20 @@
"issues": "https://github.com/laravel/scout/issues",
"source": "https://github.com/laravel/scout"
},
- "time": "2025-11-25T15:19:35+00:00"
+ "time": "2026-01-28T03:55:17+00:00"
},
{
"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": {
@@ -5579,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",
@@ -5648,21 +5655,21 @@
},
{
"name": "laravel/socialite",
- "version": "v5.23.2",
+ "version": "v5.24.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "41e65d53762d33d617bf0253330d672cb95e624b"
+ "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/41e65d53762d33d617bf0253330d672cb95e624b",
- "reference": "41e65d53762d33d617bf0253330d672cb95e624b",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613",
+ "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613",
"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",
@@ -5716,20 +5723,20 @@
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
- "time": "2025-11-21T14:00:38+00:00"
+ "time": "2026-01-10T16:07:28+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": {
@@ -5738,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",
@@ -5780,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",
@@ -6175,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": {
@@ -6194,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",
@@ -6262,20 +6269,20 @@
"type": "github"
}
],
- "time": "2025-10-25T08:35:20+00:00"
+ "time": "2025-12-27T15:18:42+00:00"
},
{
"name": "league/flysystem",
- "version": "3.30.2",
+ "version": "3.31.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277"
+ "reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277",
- "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1717e0b3642b0df65ecb0cc89cdd99fa840672ff",
+ "reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff",
"shasum": ""
},
"require": {
@@ -6343,22 +6350,22 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
- "source": "https://github.com/thephpleague/flysystem/tree/3.30.2"
+ "source": "https://github.com/thephpleague/flysystem/tree/3.31.0"
},
- "time": "2025-11-10T17:13:11+00:00"
+ "time": "2026-01-23T15:38:47+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
- "version": "3.30.1",
+ "version": "3.31.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
- "reference": "d286e896083bed3190574b8b088b557b59eb66f5"
+ "reference": "e36a2bc60b06332c92e4435047797ded352b446f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d286e896083bed3190574b8b088b557b59eb66f5",
- "reference": "d286e896083bed3190574b8b088b557b59eb66f5",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/e36a2bc60b06332c92e4435047797ded352b446f",
+ "reference": "e36a2bc60b06332c92e4435047797ded352b446f",
"shasum": ""
},
"require": {
@@ -6398,22 +6405,22 @@
"storage"
],
"support": {
- "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.30.1"
+ "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.31.0"
},
- "time": "2025-10-20T15:27:33+00:00"
+ "time": "2026-01-23T15:30:45+00:00"
},
{
"name": "league/flysystem-local",
- "version": "3.30.2",
+ "version": "3.31.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git",
- "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d"
+ "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d",
- "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079",
+ "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079",
"shasum": ""
},
"require": {
@@ -6447,9 +6454,9 @@
"local"
],
"support": {
- "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2"
+ "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0"
},
- "time": "2025-11-10T11:23:37+00:00"
+ "time": "2026-01-23T15:30:45+00:00"
},
{
"name": "league/fractal",
@@ -6655,20 +6662,20 @@
},
{
"name": "league/uri",
- "version": "7.6.0",
+ "version": "7.8.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
- "reference": "f625804987a0a9112d954f9209d91fec52182344"
+ "reference": "4436c6ec8d458e4244448b069cc572d088230b76"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344",
- "reference": "f625804987a0a9112d954f9209d91fec52182344",
+ "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76",
+ "reference": "4436c6ec8d458e4244448b069cc572d088230b76",
"shasum": ""
},
"require": {
- "league/uri-interfaces": "^7.6",
+ "league/uri-interfaces": "^7.8",
"php": "^8.1",
"psr/http-factory": "^1"
},
@@ -6682,11 +6689,11 @@
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"ext-uri": "to use the PHP native URI class",
- "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain",
- "league/uri-components": "Needed to easily manipulate URI objects components",
- "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP",
+ "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain",
+ "league/uri-components": "to provide additional tools to manipulate URI objects components",
+ "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP",
"php-64bit": "to improve IPV4 host parsing",
- "rowbot/url": "to handle WHATWG URL",
+ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"type": "library",
@@ -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.8.0"
},
"funding": [
{
@@ -6749,20 +6756,20 @@
"type": "github"
}
],
- "time": "2025-11-18T12:17:23+00:00"
+ "time": "2026-01-14T17:24:56+00:00"
},
{
"name": "league/uri-interfaces",
- "version": "7.6.0",
+ "version": "7.8.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
- "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368"
+ "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4"
},
"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/c5c5cd056110fc8afaba29fa6b72a43ced42acd4",
+ "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4",
"shasum": ""
},
"require": {
@@ -6775,7 +6782,7 @@
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"php-64bit": "to improve IPV4 host parsing",
- "rowbot/url": "to handle WHATWG URL",
+ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"type": "library",
@@ -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.8.0"
},
"funding": [
{
@@ -6833,20 +6840,20 @@
"type": "github"
}
],
- "time": "2025-11-18T12:17:23+00:00"
+ "time": "2026-01-15T06:54:53+00:00"
},
{
"name": "livewire/livewire",
- "version": "v3.7.0",
+ "version": "v3.7.6",
"source": {
"type": "git",
"url": "https://github.com/livewire/livewire.git",
- "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb"
+ "reference": "276ac156f6ae414990784854a2673e3d23c68b24"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/livewire/livewire/zipball/f5f9efe6d5a7059116bd695a89d95ceedf33f3cb",
- "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb",
+ "url": "https://api.github.com/repos/livewire/livewire/zipball/276ac156f6ae414990784854a2673e3d23c68b24",
+ "reference": "276ac156f6ae414990784854a2673e3d23c68b24",
"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.6"
},
"funding": [
{
@@ -6909,7 +6916,7 @@
"type": "github"
}
],
- "time": "2025-11-12T17:58:16+00:00"
+ "time": "2026-01-23T05:41:38+00:00"
},
{
"name": "maennchen/zipstream-php",
@@ -7425,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": {
@@ -7452,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",
@@ -7512,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": [
{
@@ -7524,20 +7531,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-24T10:02:05+00:00"
+ "time": "2026-01-02T08:56:05+00:00"
},
{
"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.1",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
- "reference": "505a30ad386daa5211f08a318e47015b501cad30"
+ "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/505a30ad386daa5211f08a318e47015b501cad30",
- "reference": "505a30ad386daa5211f08a318e47015b501cad30",
+ "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72",
+ "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72",
"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.1"
},
- "time": "2025-10-31T00:45:47+00:00"
+ "time": "2025-12-22T12:14:32+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",
@@ -8526,16 +8533,16 @@
},
{
"name": "open-telemetry/api",
- "version": "1.7.1",
+ "version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/api.git",
- "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4"
+ "reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4",
- "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4",
+ "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/df5197c6fd0ddd8e9883b87de042d9341300e2ad",
+ "reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad",
"shasum": ""
},
"require": {
@@ -8545,7 +8552,7 @@
"symfony/polyfill-php82": "^1.26"
},
"conflict": {
- "open-telemetry/sdk": "<=1.0.8"
+ "open-telemetry/sdk": "<=1.11"
},
"type": "library",
"extra": {
@@ -8592,7 +8599,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
- "time": "2025-10-19T10:49:48+00:00"
+ "time": "2026-01-21T04:14:03+00:00"
},
{
"name": "open-telemetry/context",
@@ -8774,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": {
@@ -8864,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",
@@ -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.6",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "90a04bcbf03784066f16038e87e23a0a83cee3c2"
+ "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90a04bcbf03784066f16038e87e23a0a83cee3c2",
- "reference": "90a04bcbf03784066f16038e87e23a0a83cee3c2",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8",
+ "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8",
"shasum": ""
},
"require": {
@@ -9433,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",
@@ -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.6"
},
- "time": "2025-11-17T21:13:10+00:00"
+ "time": "2025-12-22T21:13:58+00:00"
},
{
"name": "phpdocumentor/type-resolver",
@@ -9536,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": {
@@ -9567,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": {
@@ -9619,6 +9622,9 @@
},
{
"name": "Adrien Crivelli"
+ },
+ {
+ "name": "Owen Leibman"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
@@ -9635,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": {
@@ -9700,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": [
{
@@ -9712,20 +9718,20 @@
"type": "tidelift"
}
],
- "time": "2025-08-21T11:53:16+00:00"
+ "time": "2025-12-27T19:41:33+00:00"
},
{
"name": "phpseclib/phpseclib",
- "version": "3.0.47",
+ "version": "3.0.49",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
- "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d"
+ "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d",
- "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d",
+ "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9",
+ "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9",
"shasum": ""
},
"require": {
@@ -9806,7 +9812,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
- "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47"
+ "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49"
},
"funding": [
{
@@ -9822,20 +9828,20 @@
"type": "tidelift"
}
],
- "time": "2025-10-06T01:07:24+00:00"
+ "time": "2026-01-27T09:17:28+00:00"
},
{
"name": "phpstan/phpdoc-parser",
- "version": "2.3.0",
+ "version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495"
+ "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a"
},
"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/a004701b11273a26cd7955a61d67a7f1e525a45a",
+ "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a",
"shasum": ""
},
"require": {
@@ -9867,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.2"
},
- "time": "2025-08-30T15:50:23+00:00"
+ "time": "2026-01-25T14:56:51+00:00"
},
{
"name": "pragmarx/google2fa",
@@ -10448,16 +10454,16 @@
},
{
"name": "psy/psysh",
- "version": "v0.12.14",
+ "version": "v0.12.18",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
- "reference": "95c29b3756a23855a30566b745d218bee690bef2"
+ "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2",
- "reference": "95c29b3756a23855a30566b745d218bee690bef2",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196",
+ "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196",
"shasum": ""
},
"require": {
@@ -10465,8 +10471,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 +10527,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.18"
},
- "time": "2025-10-27T17:15:31+00:00"
+ "time": "2025-12-17T14:35:46+00:00"
},
{
"name": "pusher/pusher-php-server",
@@ -10768,20 +10774,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 +10846,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 +10917,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 +10999,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,31 +11039,39 @@
],
"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",
- "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"
@@ -11065,7 +11079,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "9.0.x-dev"
+ "dev-main": "9.2.x-dev"
}
},
"autoload": {
@@ -11099,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",
@@ -11234,16 +11248,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 +11320,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,28 +11332,28 @@
"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.1",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-laravel.git",
- "reference": "7fdffd57e8fff0a6f9a18d9a83f32e960af63e3f"
+ "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72"
},
"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/503853fa7ee74b34b64e76f1373db86cd11afe72",
+ "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72",
"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",
- "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0"
+ "sentry/sentry": "^4.19.0",
+ "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0 | ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.11",
@@ -11396,7 +11410,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.1"
},
"funding": [
{
@@ -11408,7 +11422,7 @@
"type": "custom"
}
],
- "time": "2025-11-11T09:01:14+00:00"
+ "time": "2026-01-07T08:53:19+00:00"
},
{
"name": "setasign/fpdf",
@@ -11530,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": {
@@ -11575,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",
@@ -11778,16 +11792,16 @@
},
{
"name": "sprain/swiss-qr-bill",
- "version": "v4.20",
+ "version": "v5.3",
"source": {
"type": "git",
"url": "https://github.com/sprain/php-swiss-qr-bill.git",
- "reference": "8f709ab60426c9b6cbc43fb03dee364071eda044"
+ "reference": "2e5e4f16ae1832d2594ab288e401ede6a76eff2a"
},
"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/2e5e4f16ae1832d2594ab288e401ede6a76eff2a",
+ "reference": "2e5e4f16ae1832d2594ab288e401ede6a76eff2a",
"shasum": ""
},
"require": {
@@ -11795,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",
@@ -11837,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.3"
},
"funding": [
{
@@ -11845,7 +11856,7 @@
"type": "github"
}
],
- "time": "2025-02-25T08:57:29+00:00"
+ "time": "2026-01-28T11:45:53+00:00"
},
{
"name": "square/square",
@@ -12033,16 +12044,16 @@
},
{
"name": "symfony/cache",
- "version": "v7.3.6",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
- "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701"
+ "reference": "8dde98d5a4123b53877aca493f9be57b333f14bd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/cache/zipball/1277a1ec61c8d93ea61b2a59738f1deb9bfb6701",
- "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/8dde98d5a4123b53877aca493f9be57b333f14bd",
+ "reference": "8dde98d5a4123b53877aca493f9be57b333f14bd",
"shasum": ""
},
"require": {
@@ -12050,12 +12061,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 +12083,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 +12124,7 @@
"psr6"
],
"support": {
- "source": "https://github.com/symfony/cache/tree/v7.3.6"
+ "source": "https://github.com/symfony/cache/tree/v7.4.5"
},
"funding": [
{
@@ -12131,7 +12144,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-30T13:22:58+00:00"
+ "time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/cache-contracts",
@@ -12211,16 +12224,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 +12278,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 +12289,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.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "9d18eba95655a3152ae4c1d53c6cc34eb4d4a0b7"
+ "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/9d18eba95655a3152ae4c1d53c6cc34eb4d4a0b7",
- "reference": "9d18eba95655a3152ae4c1d53c6cc34eb4d4a0b7",
+ "url": "https://api.github.com/repos/symfony/config/zipball/4275b53b8ab0cf37f48bf273dc2285c8178efdfb",
+ "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb",
"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 +12325,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 +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.3.6"
+ "source": "https://github.com/symfony/config/tree/v7.4.4"
},
"funding": [
{
@@ -12360,20 +12377,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-02T08:04:43+00:00"
+ "time": "2026-01-13T11:36:38+00:00"
},
{
"name": "symfony/console",
- "version": "v7.3.6",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a"
+ "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a",
- "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a",
+ "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
+ "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
"shasum": ""
},
"require": {
@@ -12381,7 +12398,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 +12412,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 +12455,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.3.6"
+ "source": "https://github.com/symfony/console/tree/v7.4.4"
},
"funding": [
{
@@ -12458,20 +12475,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-04T01:21:42+00:00"
+ "time": "2026-01-13T11:36:38+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 +12524,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 +12544,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.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
- "reference": "98af8bb46c56aedd9dd5a7f0414fc72bf2dcfe69"
+ "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2"
},
"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/76a02cddca45a5254479ad68f9fa274ead0a7ef2",
+ "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2",
"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 +12578,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 +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.3.6"
+ "source": "https://github.com/symfony/dependency-injection/tree/v7.4.5"
},
"funding": [
{
@@ -12611,7 +12628,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-31T10:11:11+00:00"
+ "time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -12682,32 +12699,33 @@
},
{
"name": "symfony/error-handler",
- "version": "v7.3.6",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "bbe40bfab84323d99dab491b716ff142410a92a8"
+ "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8"
},
"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/8da531f364ddfee53e36092a7eebbbd0b775f6b8",
+ "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8",
"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 +12757,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.4"
},
"funding": [
{
@@ -12759,20 +12777,20 @@
"type": "tidelift"
}
],
- "time": "2025-10-31T19:12:50+00:00"
+ "time": "2026-01-20T16:42:42+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v7.3.3",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191"
+ "reference": "dc2c0eba1af673e736bb851d747d266108aea746"
},
"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/dc2c0eba1af673e736bb851d747d266108aea746",
+ "reference": "dc2c0eba1af673e736bb851d747d266108aea746",
"shasum": ""
},
"require": {
@@ -12789,13 +12807,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 +12842,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.4"
},
"funding": [
{
@@ -12843,7 +12862,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-13T11:49:31+00:00"
+ "time": "2026-01-05T11:45:34+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -12923,16 +12942,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 +12960,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 +12988,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 +13008,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.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "9f696d2f1e340484b4683f7853b273abff94421f"
+ "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f",
- "reference": "9f696d2f1e340484b4683f7853b273abff94421f",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
+ "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
"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 +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.3.5"
+ "source": "https://github.com/symfony/finder/tree/v7.4.5"
},
"funding": [
{
@@ -13057,57 +13076,56 @@
"type": "tidelift"
}
],
- "time": "2025-10-15T18:45:57+00:00"
+ "time": "2026-01-26T15:07:59+00:00"
},
{
"name": "symfony/framework-bundle",
- "version": "v7.3.6",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/framework-bundle.git",
- "reference": "cabfdfa82bc4f75d693a329fe263d96937636b77"
+ "reference": "dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd"
},
"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/dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd",
+ "reference": "dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd",
"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.4|^8.0.4",
+ "symfony/dependency-injection": "^7.4.4|^8.0.4",
"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",
- "phpdocumentor/reflection-docblock": "<3.2.2",
- "phpdocumentor/type-resolver": "<1.4.0",
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1",
"symfony/asset": "<6.4",
"symfony/asset-mapper": "<6.4",
"symfony/clock": "<6.4",
"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 +13140,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",
+ "phpdocumentor/reflection-docblock": "^5.2",
"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 +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.3.6"
+ "source": "https://github.com/symfony/framework-bundle/tree/v7.4.5"
},
"funding": [
{
@@ -13215,20 +13234,20 @@
"type": "tidelift"
}
],
- "time": "2025-10-30T09:42:24+00:00"
+ "time": "2026-01-27T08:59:58+00:00"
},
{
"name": "symfony/http-client",
- "version": "v7.3.6",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de"
+ "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
},
"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/84bb634857a893cc146cceb467e31b3f02c5fe9f",
+ "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
"shasum": ""
},
"require": {
@@ -13259,12 +13278,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 +13315,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v7.3.6"
+ "source": "https://github.com/symfony/http-client/tree/v7.4.5"
},
"funding": [
{
@@ -13315,7 +13335,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-05T17:41:46+00:00"
+ "time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -13397,23 +13417,22 @@
},
{
"name": "symfony/http-foundation",
- "version": "v7.3.7",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4"
+ "reference": "446d0db2b1f21575f1284b74533e425096abdfb6"
},
"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/446d0db2b1f21575f1284b74533e425096abdfb6",
+ "reference": "446d0db2b1f21575f1284b74533e425096abdfb6",
"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 +13441,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 +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.3.7"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.4.5"
},
"funding": [
{
@@ -13476,29 +13495,29 @@
"type": "tidelift"
}
],
- "time": "2025-11-08T16:41:12+00:00"
+ "time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v7.3.7",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce"
+ "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a"
},
"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/229eda477017f92bd2ce7615d06222ec0c19e82a",
+ "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a",
"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 +13527,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 +13545,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 +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.3.7"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.4.5"
},
"funding": [
{
@@ -13594,20 +13614,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-12T11:38:40+00:00"
+ "time": "2026-01-28T10:33:42+00:00"
},
{
"name": "symfony/intl",
- "version": "v7.3.5",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/intl.git",
- "reference": "9eccaaa94ac6f9deb3620c9d47a057d965baeabf"
+ "reference": "7fa2d46174166bcd7829abc8717949f8a0b21fb7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/intl/zipball/9eccaaa94ac6f9deb3620c9d47a057d965baeabf",
- "reference": "9eccaaa94ac6f9deb3620c9d47a057d965baeabf",
+ "url": "https://api.github.com/repos/symfony/intl/zipball/7fa2d46174166bcd7829abc8717949f8a0b21fb7",
+ "reference": "7fa2d46174166bcd7829abc8717949f8a0b21fb7",
"shasum": ""
},
"require": {
@@ -13618,8 +13638,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 +13684,7 @@
"localization"
],
"support": {
- "source": "https://github.com/symfony/intl/tree/v7.3.5"
+ "source": "https://github.com/symfony/intl/tree/v7.4.4"
},
"funding": [
{
@@ -13684,7 +13704,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-01T06:11:17+00:00"
+ "time": "2026-01-12T12:19:02+00:00"
},
{
"name": "symfony/mailer",
@@ -13837,39 +13857,40 @@
},
{
"name": "symfony/mime",
- "version": "v7.3.4",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35"
+ "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35",
- "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148",
+ "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148",
"shasum": ""
},
"require": {
"php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
- "phpdocumentor/reflection-docblock": "<3.2.2",
- "phpdocumentor/type-resolver": "<1.4.0",
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1",
"symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
},
"require-dev": {
"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"
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "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 +13922,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.3.4"
+ "source": "https://github.com/symfony/mime/tree/v7.4.5"
},
"funding": [
{
@@ -13921,20 +13942,20 @@
"type": "tidelift"
}
],
- "time": "2025-09-16T08:38:17+00:00"
+ "time": "2026-01-27T08:59:58+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 +13993,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 +14013,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 +14849,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 +15084,16 @@
},
{
"name": "symfony/process",
- "version": "v7.3.4",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
+ "reference": "608476f4604102976d687c483ac63a79ba18cc97"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
- "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
+ "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97",
+ "reference": "608476f4604102976d687c483ac63a79ba18cc97",
"shasum": ""
},
"require": {
@@ -15024,7 +15125,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.5"
},
"funding": [
{
@@ -15044,28 +15145,29 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T10:12:26+00:00"
+ "time": "2026-01-26T15:07:59+00:00"
},
{
"name": "symfony/property-access",
- "version": "v7.3.3",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-access.git",
- "reference": "4a4389e5c8bd1d0320d80a23caa6a1ac71cb81a7"
+ "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1"
},
"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/fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1",
+ "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1",
"shasum": ""
},
"require": {
"php": ">=8.2",
- "symfony/property-info": "^6.4|^7.0"
+ "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4"
},
"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 +15206,7 @@
"reflection"
],
"support": {
- "source": "https://github.com/symfony/property-access/tree/v7.3.3"
+ "source": "https://github.com/symfony/property-access/tree/v7.4.4"
},
"funding": [
{
@@ -15124,30 +15226,30 @@
"type": "tidelift"
}
],
- "time": "2025-08-04T15:15:28+00:00"
+ "time": "2026-01-05T08:47:25+00:00"
},
{
"name": "symfony/property-info",
- "version": "v7.3.5",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-info.git",
- "reference": "0b346ed259dc5da43535caf243005fe7d4b0f051"
+ "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21"
},
"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/1c9d326bd69602561e2ea467a16c09b5972eee21",
+ "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21",
"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.10|^7.4.4|^8.0.4"
},
"conflict": {
- "phpdocumentor/reflection-docblock": "<5.2",
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
"phpdocumentor/type-resolver": "<1.5.1",
"symfony/cache": "<6.4",
"symfony/dependency-injection": "<6.4",
@@ -15156,9 +15258,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 +15296,7 @@
"validator"
],
"support": {
- "source": "https://github.com/symfony/property-info/tree/v7.3.5"
+ "source": "https://github.com/symfony/property-info/tree/v7.4.5"
},
"funding": [
{
@@ -15214,26 +15316,26 @@
"type": "tidelift"
}
],
- "time": "2025-10-05T22:12:41+00:00"
+ "time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
- "version": "v7.3.0",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/psr-http-message-bridge.git",
- "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f"
+ "reference": "929ffe10bbfbb92e711ac3818d416f9daffee067"
},
"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/929ffe10bbfbb92e711ac3818d416f9daffee067",
+ "reference": "929ffe10bbfbb92e711ac3818d416f9daffee067",
"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 +15345,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 +15384,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.4"
},
"funding": [
{
@@ -15292,25 +15395,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": "2026-01-03T23:30:35+00:00"
},
{
"name": "symfony/routing",
- "version": "v7.3.6",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "c97abe725f2a1a858deca629a6488c8fc20c3091"
+ "reference": "0798827fe2c79caeed41d70b680c2c3507d10147"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/c97abe725f2a1a858deca629a6488c8fc20c3091",
- "reference": "c97abe725f2a1a858deca629a6488c8fc20c3091",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147",
+ "reference": "0798827fe2c79caeed41d70b680c2c3507d10147",
"shasum": ""
},
"require": {
@@ -15324,11 +15431,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 +15469,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v7.3.6"
+ "source": "https://github.com/symfony/routing/tree/v7.4.4"
},
"funding": [
{
@@ -15382,20 +15489,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-05T07:57:47+00:00"
+ "time": "2026-01-12T12:19:02+00:00"
},
{
"name": "symfony/serializer",
- "version": "v7.3.5",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
- "reference": "ba2e50a5f2870c93f0f47ca1a4e56e4bbe274035"
+ "reference": "480cd1237c98ab1219c20945b92c9d4480a44f47"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/serializer/zipball/ba2e50a5f2870c93f0f47ca1a4e56e4bbe274035",
- "reference": "ba2e50a5f2870c93f0f47ca1a4e56e4bbe274035",
+ "url": "https://api.github.com/repos/symfony/serializer/zipball/480cd1237c98ab1219c20945b92c9d4480a44f47",
+ "reference": "480cd1237c98ab1219c20945b92c9d4480a44f47",
"shasum": ""
},
"require": {
@@ -15405,8 +15512,8 @@
"symfony/polyfill-php84": "^1.30"
},
"conflict": {
- "phpdocumentor/reflection-docblock": "<3.2.2",
- "phpdocumentor/type-resolver": "<1.4.0",
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1",
"symfony/dependency-injection": "<6.4",
"symfony/property-access": "<6.4",
"symfony/property-info": "<6.4",
@@ -15415,29 +15522,29 @@
"symfony/yaml": "<6.4"
},
"require-dev": {
- "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0",
+ "phpdocumentor/reflection-docblock": "^5.2",
"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 +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.3.5"
+ "source": "https://github.com/symfony/serializer/tree/v7.4.5"
},
"funding": [
{
@@ -15485,7 +15592,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-08T11:26:21+00:00"
+ "time": "2026-01-27T08:59:58+00:00"
},
{
"name": "symfony/service-contracts",
@@ -15576,22 +15683,23 @@
},
{
"name": "symfony/string",
- "version": "v7.3.4",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "f96476035142921000338bad71e5247fbc138872"
+ "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
- "reference": "f96476035142921000338bad71e5247fbc138872",
+ "url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f",
+ "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f",
"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 +15707,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 +15750,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.3.4"
+ "source": "https://github.com/symfony/string/tree/v7.4.4"
},
"funding": [
{
@@ -15662,27 +15770,27 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T14:36:48+00:00"
+ "time": "2026-01-12T10:54:30+00:00"
},
{
"name": "symfony/translation",
- "version": "v7.3.4",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "ec25870502d0c7072d086e8ffba1420c85965174"
+ "reference": "bfde13711f53f549e73b06d27b35a55207528877"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174",
- "reference": "ec25870502d0c7072d086e8ffba1420c85965174",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/bfde13711f53f549e73b06d27b35a55207528877",
+ "reference": "bfde13711f53f549e73b06d27b35a55207528877",
"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 +15809,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 +15850,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.4"
},
"funding": [
{
@@ -15762,7 +15870,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-07T11:39:36+00:00"
+ "time": "2026-01-13T10:40:19+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -15848,16 +15956,16 @@
},
{
"name": "symfony/twig-bridge",
- "version": "v7.3.6",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/twig-bridge.git",
- "reference": "d1aaec8eee1f5591f56b9efe00194d73a8e38319"
+ "reference": "f2dd26b604e856476ef7e0efa4568bc07eb7ddc8"
},
"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/f2dd26b604e856476ef7e0efa4568bc07eb7ddc8",
+ "reference": "f2dd26b604e856476ef7e0efa4568bc07eb7ddc8",
"shasum": ""
},
"require": {
@@ -15867,10 +15975,10 @@
"twig/twig": "^3.21"
},
"conflict": {
- "phpdocumentor/reflection-docblock": "<3.2.2",
- "phpdocumentor/type-resolver": "<1.4.0",
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1",
"symfony/console": "<6.4",
- "symfony/form": "<6.4",
+ "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4",
"symfony/http-foundation": "<6.4",
"symfony/http-kernel": "<6.4",
"symfony/mime": "<6.4",
@@ -15881,34 +15989,34 @@
"require-dev": {
"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",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "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.32|~7.3.10|^7.4.4|^8.0.4",
+ "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 +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.3.6"
+ "source": "https://github.com/symfony/twig-bridge/tree/v7.4.5"
},
"funding": [
{
@@ -15959,30 +16067,31 @@
"type": "tidelift"
}
],
- "time": "2025-11-04T15:37:51+00:00"
+ "time": "2026-01-27T08:59:58+00:00"
},
{
"name": "symfony/twig-bundle",
- "version": "v7.3.4",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/twig-bundle.git",
- "reference": "da5c778a8416fcce5318737c4d944f6fa2bb3f81"
+ "reference": "e8829e02ff96a391ed0703bac9e7ff0537480b6b"
},
"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/e8829e02ff96a391ed0703bac9e7ff0537480b6b",
+ "reference": "e8829e02ff96a391ed0703bac9e7ff0537480b6b",
"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/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",
"twig/twig": "^3.12"
},
"conflict": {
@@ -15990,16 +16099,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 +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.3.4"
+ "source": "https://github.com/symfony/twig-bundle/tree/v7.4.4"
},
"funding": [
{
@@ -16047,20 +16157,20 @@
"type": "tidelift"
}
],
- "time": "2025-09-10T12:00:31+00:00"
+ "time": "2026-01-06T12:34:24+00:00"
},
{
"name": "symfony/type-info",
- "version": "v7.3.5",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/type-info.git",
- "reference": "8b36f41421160db56914f897b57eaa6a830758b3"
+ "reference": "f83c725e72b39b2704b9d6fc85070ad6ac7a5889"
},
"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/f83c725e72b39b2704b9d6fc85070ad6ac7a5889",
+ "reference": "f83c725e72b39b2704b9d6fc85070ad6ac7a5889",
"shasum": ""
},
"require": {
@@ -16110,7 +16220,7 @@
"type"
],
"support": {
- "source": "https://github.com/symfony/type-info/tree/v7.3.5"
+ "source": "https://github.com/symfony/type-info/tree/v7.4.4"
},
"funding": [
{
@@ -16130,20 +16240,20 @@
"type": "tidelift"
}
],
- "time": "2025-10-16T12:30:12+00:00"
+ "time": "2026-01-09T12:14:21+00:00"
},
{
"name": "symfony/uid",
- "version": "v7.3.1",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
- "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb"
+ "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb",
- "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36",
+ "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36",
"shasum": ""
},
"require": {
@@ -16151,7 +16261,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 +16298,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/uid/tree/v7.3.1"
+ "source": "https://github.com/symfony/uid/tree/v7.4.4"
},
"funding": [
{
@@ -16199,25 +16309,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": "2026-01-03T23:30:35+00:00"
},
{
"name": "symfony/validator",
- "version": "v7.3.7",
+ "version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/validator.git",
- "reference": "8290a095497c3fe5046db21888d1f75b54ddf39d"
+ "reference": "fcec92c40df1c93507857da08226005573b655c6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/validator/zipball/8290a095497c3fe5046db21888d1f75b54ddf39d",
- "reference": "8290a095497c3fe5046db21888d1f75b54ddf39d",
+ "url": "https://api.github.com/repos/symfony/validator/zipball/fcec92c40df1c93507857da08226005573b655c6",
+ "reference": "fcec92c40df1c93507857da08226005573b655c6",
"shasum": ""
},
"require": {
@@ -16237,27 +16351,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 +16402,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.5"
},
"funding": [
{
@@ -16306,20 +16422,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-08T16:29:29+00:00"
+ "time": "2026-01-27T08:59:58+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v7.3.5",
+ "version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d"
+ "reference": "0e4769b46a0c3c62390d124635ce59f66874b282"
},
"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/0e4769b46a0c3c62390d124635ce59f66874b282",
+ "reference": "0e4769b46a0c3c62390d124635ce59f66874b282",
"shasum": ""
},
"require": {
@@ -16331,10 +16447,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 +16489,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.3.5"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.4.4"
},
"funding": [
{
@@ -16393,20 +16509,20 @@
"type": "tidelift"
}
],
- "time": "2025-09-27T09:00:46+00:00"
+ "time": "2026-01-01T22:13:48+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 +16530,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 +16570,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 +16590,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 +16646,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 +16666,166 @@
"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",
+ "name": "thecodingmachine/safe",
+ "version": "v3.3.0",
"source": {
"type": "git",
- "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
- "reference": "0d72ac1c00084279c1816675284073c5a337c20d"
+ "url": "https://github.com/thecodingmachine/safe.git",
+ "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d",
- "reference": "0d72ac1c00084279c1816675284073c5a337c20d",
+ "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",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
+ "reference": "f0292ccf0ec75843d65027214426b6b163b48b41"
+ },
+ "dist": {
+ "type": "zip",
+ "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 +16858,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 +16978,16 @@
},
{
"name": "twig/extra-bundle",
- "version": "v3.22.1",
+ "version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/twig-extra-bundle.git",
- "reference": "b6534bc925bec930004facca92fccebd0c809247"
+ "reference": "7a27e784dc56eddfef5e9295829b290ce06f1682"
},
"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/7a27e784dc56eddfef5e9295829b290ce06f1682",
+ "reference": "7a27e784dc56eddfef5e9295829b290ce06f1682",
"shasum": ""
},
"require": {
@@ -16781,7 +17036,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.23.0"
},
"funding": [
{
@@ -16793,20 +17048,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-02T11:00:49+00:00"
+ "time": "2025-12-18T20:46:15+00:00"
},
{
"name": "twig/intl-extra",
- "version": "v3.22.1",
+ "version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/intl-extra.git",
- "reference": "93ac31e53cdd3f2e541f42690cd0c54ca8138ab1"
+ "reference": "32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/93ac31e53cdd3f2e541f42690cd0c54ca8138ab1",
- "reference": "93ac31e53cdd3f2e541f42690cd0c54ca8138ab1",
+ "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f",
+ "reference": "32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f",
"shasum": ""
},
"require": {
@@ -16845,7 +17100,7 @@
"twig"
],
"support": {
- "source": "https://github.com/twigphp/intl-extra/tree/v3.22.1"
+ "source": "https://github.com/twigphp/intl-extra/tree/v3.23.0"
},
"funding": [
{
@@ -16857,20 +17112,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-02T11:00:49+00:00"
+ "time": "2026-01-17T13:57:47+00:00"
},
{
"name": "twig/markdown-extra",
- "version": "v3.22.0",
+ "version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/markdown-extra.git",
- "reference": "fb6f952082e3a7d62a75c8be2c8c47242d3925fb"
+ "reference": "faf069b259e2d3930c73c2f53e2dec8440bd90a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/fb6f952082e3a7d62a75c8be2c8c47242d3925fb",
- "reference": "fb6f952082e3a7d62a75c8be2c8c47242d3925fb",
+ "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/faf069b259e2d3930c73c2f53e2dec8440bd90a2",
+ "reference": "faf069b259e2d3930c73c2f53e2dec8440bd90a2",
"shasum": ""
},
"require": {
@@ -16917,7 +17172,7 @@
"twig"
],
"support": {
- "source": "https://github.com/twigphp/markdown-extra/tree/v3.22.0"
+ "source": "https://github.com/twigphp/markdown-extra/tree/v3.23.0"
},
"funding": [
{
@@ -16929,20 +17184,20 @@
"type": "tidelift"
}
],
- "time": "2025-09-15T05:57:37+00:00"
+ "time": "2025-12-02T14:45:16+00:00"
},
{
"name": "twig/twig",
- "version": "v3.22.0",
+ "version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "4509984193026de413baf4ba80f68590a7f2c51d"
+ "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/4509984193026de413baf4ba80f68590a7f2c51d",
- "reference": "4509984193026de413baf4ba80f68590a7f2c51d",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
+ "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
"shasum": ""
},
"require": {
@@ -16996,7 +17251,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.23.0"
},
"funding": [
{
@@ -17008,7 +17263,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-29T15:56:47+00:00"
+ "time": "2026-01-23T21:00:41+00:00"
},
{
"name": "twilio/sdk",
@@ -17065,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",
@@ -17133,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": [
{
@@ -17145,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",
@@ -17377,16 +17632,16 @@
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
- "version": "v3.16.1",
+ "version": "v3.16.5",
"source": {
"type": "git",
- "url": "https://github.com/barryvdh/laravel-debugbar.git",
- "reference": "21b2c6fce05453efd4bceb34f9fddaa1cdb44090"
+ "url": "https://github.com/fruitcake/laravel-debugbar.git",
+ "reference": "e85c0a8464da67e5b4a53a42796d46a43fc06c9a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/21b2c6fce05453efd4bceb34f9fddaa1cdb44090",
- "reference": "21b2c6fce05453efd4bceb34f9fddaa1cdb44090",
+ "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/e85c0a8464da67e5b4a53a42796d46a43fc06c9a",
+ "reference": "e85c0a8464da67e5b4a53a42796d46a43fc06c9a",
"shasum": ""
},
"require": {
@@ -17395,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",
@@ -17445,8 +17700,8 @@
"webprofiler"
],
"support": {
- "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
- "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.1"
+ "issues": "https://github.com/fruitcake/laravel-debugbar/issues",
+ "source": "https://github.com/fruitcake/laravel-debugbar/tree/v3.16.5"
},
"funding": [
{
@@ -17458,20 +17713,20 @@
"type": "github"
}
],
- "time": "2025-11-19T08:31:25+00:00"
+ "time": "2026-01-23T15:03:22+00:00"
},
{
"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 +17795,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 +17807,7 @@
"type": "github"
}
],
- "time": "2025-07-17T20:11:57+00:00"
+ "time": "2025-12-10T09:11:07+00:00"
},
{
"name": "barryvdh/reflection-docblock",
@@ -17608,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": {
@@ -17625,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",
@@ -17685,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": [
{
@@ -17697,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",
@@ -17765,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": {
@@ -17818,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": [
{
@@ -17830,7 +18085,7 @@
"type": "github"
}
],
- "time": "2025-11-19T10:41:15+00:00"
+ "time": "2025-12-29T13:15:25+00:00"
},
{
"name": "composer/semver",
@@ -18156,16 +18411,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
- "version": "v3.90.0",
+ "version": "v3.93.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
- "reference": "ad732c2e9299c9743f9c55ae53cc0e7642ab1155"
+ "reference": "50895a07cface1385082e4caa6a6786c4e033468"
},
"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/50895a07cface1385082e4caa6a6786c4e033468",
+ "reference": "50895a07cface1385082e4caa6a6786c4e033468",
"shasum": ""
},
"require": {
@@ -18197,16 +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.32",
+ "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",
- "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"
+ "phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.48",
+ "symfony/polyfill-php85": "^1.33",
+ "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",
@@ -18221,7 +18477,7 @@
"PhpCsFixer\\": "src/"
},
"exclude-from-classmap": [
- "src/Fixer/Internal/*"
+ "src/**/Internal/"
]
},
"notification-url": "https://packagist.org/downloads/",
@@ -18247,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.90.0"
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.0"
},
"funding": [
{
@@ -18255,7 +18511,7 @@
"type": "github"
}
],
- "time": "2025-11-20T15:15:16+00:00"
+ "time": "2026-01-23T17:33:21+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@@ -18351,19 +18607,20 @@
},
{
"name": "illuminate/json-schema",
- "version": "v12.40.2",
+ "version": "v12.49.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 +18650,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,34 +18802,34 @@
},
{
"name": "laravel/boost",
- "version": "v1.8.3",
+ "version": "v1.8.10",
"source": {
"type": "git",
"url": "https://github.com/laravel/boost.git",
- "reference": "26572e858e67334952779c0110ca4c378a44d28d"
+ "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/boost/zipball/26572e858e67334952779c0110ca4c378a44d28d",
- "reference": "26572e858e67334952779c0110ca4c378a44d28d",
+ "url": "https://api.github.com/repos/laravel/boost/zipball/aad8b2a423b0a886c2ce7ee92abbfde69992ff32",
+ "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^7.9",
- "illuminate/console": "^10.49.0|^11.45.3|^12.28.1",
- "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",
+ "illuminate/console": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/contracts": "^10.49.0|^11.45.3|^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",
+ "laravel/mcp": "^0.5.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 +18864,38 @@
"issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost"
},
- "time": "2025-11-26T14:12:52+00:00"
+ "time": "2026-01-14T14:51:16+00:00"
},
{
"name": "laravel/mcp",
- "version": "v0.3.4",
+ "version": "v0.5.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/mcp.git",
- "reference": "0b86fb613a0df971cec89271c674a677c2cb4f77"
+ "reference": "39b9791b989927642137dd5b55dde0529f1614f9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/mcp/zipball/0b86fb613a0df971cec89271c674a677c2cb4f77",
- "reference": "0b86fb613a0df971cec89271c674a677c2cb4f77",
+ "url": "https://api.github.com/repos/laravel/mcp/zipball/39b9791b989927642137dd5b55dde0529f1614f9",
+ "reference": "39b9791b989927642137dd5b55dde0529f1614f9",
"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 +18937,7 @@
"issues": "https://github.com/laravel/mcp/issues",
"source": "https://github.com/laravel/mcp"
},
- "time": "2025-11-18T14:41:05+00:00"
+ "time": "2026-01-26T10:25:21+00:00"
},
{
"name": "laravel/roster",
@@ -19045,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",
@@ -19079,7 +19337,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.1-dev"
+ "dev-master": "2.2-dev"
}
},
"autoload": {
@@ -19112,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",
@@ -19171,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",
@@ -19237,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": [
{
@@ -19257,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",
@@ -19506,16 +19764,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "11.5.44",
+ "version": "11.5.50",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "c346885c95423eda3f65d85a194aaa24873cda82"
+ "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82",
- "reference": "c346885c95423eda3f65d85a194aaa24873cda82",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3",
+ "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3",
"shasum": ""
},
"require": {
@@ -19529,14 +19787,14 @@
"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",
"phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.3",
- "sebastian/comparator": "^6.3.2",
+ "sebastian/comparator": "^6.3.3",
"sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.1",
"sebastian/exporter": "^6.3.2",
@@ -19587,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.44"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50"
},
"funding": [
{
@@ -19611,7 +19869,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-13T07:17:35+00:00"
+ "time": "2026-01-27T05:59:18+00:00"
},
{
"name": "react/cache",
@@ -19687,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": {
@@ -19750,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": [
{
@@ -19758,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",
@@ -20311,16 +20569,16 @@
},
{
"name": "sebastian/comparator",
- "version": "6.3.2",
+ "version": "6.3.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8"
+ "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8",
- "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
+ "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
"shasum": ""
},
"require": {
@@ -20379,7 +20637,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3"
},
"funding": [
{
@@ -20399,7 +20657,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-10T08:07:46+00:00"
+ "time": "2026-01-24T09:26:40+00:00"
},
{
"name": "sebastian/complexity",
@@ -21417,38 +21675,39 @@
},
{
"name": "spatie/laravel-ignition",
- "version": "2.9.1",
+ "version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ignition.git",
- "reference": "1baee07216d6748ebd3a65ba97381b051838707a"
+ "reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/1baee07216d6748ebd3a65ba97381b051838707a",
- "reference": "1baee07216d6748ebd3a65ba97381b051838707a",
+ "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/2abefdcca6074a9155f90b4ccb3345af8889d5f5",
+ "reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
- "illuminate/support": "^10.0|^11.0|^12.0",
- "php": "^8.1",
- "spatie/ignition": "^1.15",
- "symfony/console": "^6.2.3|^7.0",
- "symfony/var-dumper": "^6.2.3|^7.0"
+ "illuminate/support": "^11.0|^12.0",
+ "nesbot/carbon": "^2.72|^3.0",
+ "php": "^8.2",
+ "spatie/ignition": "^1.15.1",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0"
},
"require-dev": {
- "livewire/livewire": "^2.11|^3.3.5",
- "mockery/mockery": "^1.5.1",
- "openai-php/client": "^0.8.1|^0.10",
- "orchestra/testbench": "8.22.3|^9.0|^10.0",
- "pestphp/pest": "^2.34|^3.7",
- "phpstan/extension-installer": "^1.3.1",
- "phpstan/phpstan-deprecation-rules": "^1.1.1|^2.0",
- "phpstan/phpstan-phpunit": "^1.3.16|^2.0",
- "vlucas/phpdotenv": "^5.5"
+ "livewire/livewire": "^3.7.0|^4.0",
+ "mockery/mockery": "^1.6.12",
+ "openai-php/client": "^0.10.3",
+ "orchestra/testbench": "^v9.16.0|^10.6",
+ "pestphp/pest": "^3.7|^4.0",
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan-deprecation-rules": "^2.0.3",
+ "phpstan/phpstan-phpunit": "^2.0.8",
+ "vlucas/phpdotenv": "^5.6.2"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
@@ -21504,7 +21763,7 @@
"type": "github"
}
],
- "time": "2025-02-20T13:13:55+00:00"
+ "time": "2026-01-20T13:16:11+00:00"
},
{
"name": "spaze/phpstan-stripe",
@@ -21702,16 +21961,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 +22003,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 +22014,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",
@@ -21825,12 +22088,12 @@
"prefer-lowest": false,
"platform": {
"php": ">=8.2",
+ "ext-bcmath": "*",
"ext-curl": "*",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
- "ext-simplexml": "*",
- "ext-bcmath": "*"
+ "ext-simplexml": "*"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
diff --git a/config/ninja.php b/config/ninja.php
index 2b7ba5a361..87ca61bc35 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.50'),
+ 'app_tag' => env('APP_TAG', '5.12.50'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),
diff --git a/config/services.php b/config/services.php
index 398b1b5e24..7e762ee7f3 100644
--- a/config/services.php
+++ b/config/services.php
@@ -145,11 +145,14 @@ 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),
],
+
'verifactu' => [
'sender_nif' => env('VERIFACTU_SENDER_NIF', ''),
'certificate' => env('VERIFACTU_CERTIFICATE', ''),
@@ -157,6 +160,14 @@ return [
'sender_name' => env('VERIFACTU_SENDER_NAME', 'CERTIFICADO FISICA PRUEBAS'),
'test_mode' => env('VERIFACTU_TEST_MODE', false),
],
+ 'quickbooks' => [
+ 'client_id' => env('QUICKBOOKS_CLIENT_ID', false),
+ 'client_secret' => env('QUICKBOOKS_CLIENT_SECRET', false),
+ 'redirect' => env('QUICKBOOKS_REDIRECT_URI'),
+ 'test_redirect' => env('QUICKBOOKS_TEST_REDIRECT_URI'),
+ 'env' => env('QUICKBOOKS_ENV', 'sandbox'),
+ 'debug' => env('APP_DEBUG',false)
+ ],
'cloudflare' => [
'zone_id' => env('CLOUDFLARE_SAAS_ZONE_ID', false),
'api_token' => env('CLOUDFLARE_SAAS_API_TOKEN', false),
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],
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":""}'],
diff --git a/lang/en/texts.php b/lang/en/texts.php
index 65acdb85dd..44c52f0bd0 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',
@@ -5635,7 +5637,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',
@@ -5644,10 +5645,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',
@@ -5668,6 +5665,34 @@ $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',
+ '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',
+ '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...',
+ '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!',
+ 'auto_generate' => 'Auto Generate',
+ 'mollie_payment_pending' => 'Your payment is pending. Please wait for it to be processed. We will email you when it is completed.',
+ 'over_payment_helper' => 'Optional: you can pay more than the amount shown here. (ie tip, round up)',
+ 'new_resource' => 'New Resource',
);
return $lang;
diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php
index 5aaa02e677..80fe92c669 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,34 @@ 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' => '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!',
+ 'auto_generate' => 'Auto générer',
+ 'mollie_payment_pending' => 'Votre paiement est en attente. Veuillez patienter pendant son traitement. Nous vous enverrons un e-mail une fois qu\'il sera terminé.',
);
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..dec5f6ec97 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,34 @@ $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' => '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 !',
+ 'auto_generate' => 'Tự động tạo',
+ 'mollie_payment_pending' => 'Sự chi trả của bạn đang chờ xử lý. Vui lòng chờ đến xử lý. Chúng tôi sẽ email cho bạn khi hoàn tất.',
);
return $lang;
diff --git a/lang/zh_TW/texts.php b/lang/zh_TW/texts.php
index 77cb0b961b..f791e973d3 100644
--- a/lang/zh_TW/texts.php
+++ b/lang/zh_TW/texts.php
@@ -60,8 +60,8 @@ $lang = array(
'download_pdf' => '下載 PDF',
'pay_now' => '立即付款',
'save_invoice' => '儲存發票',
- 'clone_invoice' => '克隆至發票',
- 'archive_invoice' => '存檔發票',
+ 'clone_invoice' => '複製至發票',
+ 'archive_invoice' => '歸檔發票',
'delete_invoice' => '刪除發票',
'email_invoice' => '電郵發票',
'enter_payment' => '輸入付款',
@@ -75,7 +75,7 @@ $lang = array(
'clients' => '客戶',
'invoices' => '發票',
'payments' => '付款',
- 'credits' => '致謝',
+ 'credits' => '信用',
'history' => '歷史',
'search' => '搜尋',
'sign_up' => '報名',
@@ -85,17 +85,17 @@ $lang = array(
'notifications' => '通知',
'import_export' => '導入 |出口',
'done' => '完畢',
- 'save' => '節省',
+ 'save' => '儲存',
'create' => '創造',
'upload' => '上傳',
- 'import' => '進口',
+ 'import' => '匯入',
'download' => '下載',
'cancel' => '取消',
'close' => '關閉',
'provide_email' => '請提供有效的電子郵件地址',
'powered_by' => '供電',
'no_items' => '沒有項目',
- 'recurring_invoices' => '定期發票',
+ 'recurring_invoices' => '週期發票',
'recurring_help' => ' 每週、每兩個月、每月、每季或每年自動向客戶發送相同的發票。
使用:MONTH 、 :QUARTER或:YEAR取得動態日期。基本數學也同樣有效,例如:MONTH -1。
動態發票變數的範例:
@@ -104,7 +104,7 @@ $lang = array(
“ :YEAR +1 年度訂閱”>>“2015 年度訂閱”
“ :QUARTER +1 的預付款” >> “第二季的預付款”
',
- 'recurring_quotes' => '重複報價',
+ 'recurring_quotes' => '週期報價',
'in_total_revenue' => '總收入',
'billed_client' => '帳單客戶',
'billed_clients' => '向客戶開立帳單',
@@ -113,20 +113,20 @@ $lang = array(
'invoices_past_due' => '發票逾期',
'upcoming_invoices' => '即將開立的發票',
'average_invoice' => '平均發票',
- 'archive' => '檔案',
+ 'archive' => '歸檔',
'delete' => '刪除',
- 'archive_client' => '存檔客戶端',
+ 'archive_client' => '歸檔客戶端',
'delete_client' => '刪除客戶端',
- 'archive_payment' => '存檔付款',
+ 'archive_payment' => '歸檔付款',
'delete_payment' => '刪除付款',
- 'archive_credit' => '檔案信用',
+ 'archive_credit' => '歸檔信用',
'delete_credit' => '刪除信用',
'show_archived_deleted' => '顯示已歸檔/已刪除',
'filter' => '篩選',
'new_client' => '新客戶',
'new_invoice' => '新發票',
'new_payment' => '輸入付款',
- 'new_credit' => '輸入貸款資料',
+ 'new_credit' => '輸入信用資料',
'contact' => '聯絡人',
'date_created' => '建立日期',
'last_login' => '上次登入',
@@ -142,19 +142,19 @@ $lang = array(
'method' => '方式',
'payment_amount' => '付款金額',
'payment_date' => '付款日期',
- 'credit_amount' => '貸款金額',
- 'credit_balance' => '貸款餘額',
- 'credit_date' => '貸款日期',
+ 'credit_amount' => '信用金額',
+ 'credit_balance' => '信用餘額',
+ 'credit_date' => '信用日期',
'empty_table' => '資料表中無此資料',
'select' => '選擇',
'edit_client' => '編輯用戶',
'edit_invoice' => '編輯發票',
'create_invoice' => '建立發票',
- 'enter_credit' => '輸入貸款資料',
+ 'enter_credit' => '輸入信用資料',
'last_logged_in' => '上次登入於',
'details' => '詳細資料',
'standing' => '資格',
- 'credit' => '貸款',
+ 'credit' => '信用',
'activity' => '活動',
'date' => '日期',
'message' => '訊息',
@@ -224,11 +224,11 @@ $lang = array(
'deleted_payment' => '刪除付款資料成功',
'deleted_payments' => '刪除 :count 筆付款資料成功',
'applied_payment' => '完成套用的付款資料',
- 'created_credit' => '建立貸款資料完成',
- 'archived_credit' => '歸檔貸款資料成功',
- 'archived_credits' => '歸檔 :count 筆貸款資料成功',
- 'deleted_credit' => '刪除貸款資料成功',
- 'deleted_credits' => '刪除 :count 筆貸款資料成功',
+ 'created_credit' => '建立信用資料完成',
+ 'archived_credit' => '歸檔信用資料成功',
+ 'archived_credits' => '歸檔 :count 筆信用資料成功',
+ 'deleted_credit' => '刪除信用資料成功',
+ 'deleted_credits' => '刪除 :count 信用款資料成功',
'imported_file' => '匯入檔案成功',
'updated_vendor' => '更新供應商資料成功',
'created_vendor' => '建立供應商資料成功',
@@ -311,7 +311,7 @@ $lang = array(
'quote_total' => '報價單總計',
'your_quote' => '您的報價單',
'total' => '總計',
- 'clone' => '再製',
+ 'clone' => '複製',
'new_quote' => '新報價單',
'create_quote' => '建立報價單',
'edit_quote' => '編輯報價單',
@@ -319,7 +319,7 @@ $lang = array(
'delete_quote' => '刪除報價單',
'save_quote' => '儲存報價單',
'email_quote' => '以電子郵件傳送報價單',
- 'clone_quote' => '再製到報價單',
+ 'clone_quote' => '複製到報價單',
'convert_to_invoice' => '轉換至發票',
'view_invoice' => '檢視發票',
'view_client' => '檢視用戶資料',
@@ -401,13 +401,13 @@ $lang = array(
'restore_invoice' => '復原發票',
'restore_quote' => '復原報價單',
'restore_client' => '復原用戶',
- 'restore_credit' => '復原貸款資料',
+ 'restore_credit' => '復原信用資料',
'restore_payment' => '復原付款資料',
'restored_invoice' => '復原發票成功',
'restored_quote' => '復原報價單成功',
'restored_client' => '復原用戶資料成功',
'restored_payment' => '復原付款資料成功',
- 'restored_credit' => '復原貸款資料成功',
+ 'restored_credit' => '復原信用資料成功',
'reason_for_canceling' => '告訴我們您為什麼離開,幫助我們改善我們的網站。',
'discount_percent' => '百分比',
'discount_amount' => '金額',
@@ -742,10 +742,10 @@ $lang = array(
'activity_11' => ':user 已更新付款資料 :payment',
'activity_12' => ':user 已將付款資料 :payment 歸檔',
'activity_13' => ':user 已刪除付款資料 :payment',
- 'activity_14' => ':user 已輸入貸款資料 :credit',
- 'activity_15' => ':user 更新貸款 :credit',
- 'activity_16' => ':user 已將 :credit 貸款資料歸檔',
- 'activity_17' => ':user 已刪除 :credit 貸款資料',
+ 'activity_14' => ':user 已輸入信用資料 :credit',
+ 'activity_15' => ':user 更新信用 :credit',
+ 'activity_16' => ':user 已將 :credit 信用資料歸檔',
+ 'activity_17' => ':user 已刪除 :credit 信用資料',
'activity_18' => ':user 已建立報價單 :quote',
'activity_19' => ':user 已更新報價單 :quote',
'activity_20' => ':user emailed quote :quote for :client to :contact',
@@ -756,7 +756,7 @@ $lang = array(
'activity_25' => ':user 已復原發票 :invoice',
'activity_26' => ':user 已復原用戶 :client 資料',
'activity_27' => ':user 已復原付款資料 :payment',
- 'activity_28' => ':user 已復原 :credit 貸款資料',
+ 'activity_28' => ':user 已復原 :credit 信用資料',
'activity_29' => ':contact approved quote :quote for :client',
'activity_30' => ':user 已建立供應商 :vendor',
'activity_31' => ':user 已將供應商 :vendor 歸檔',
@@ -793,14 +793,14 @@ $lang = array(
'quote_footer' => '報價單頁尾',
'free' => '免費',
'quote_is_approved' => '已獲同意',
- 'apply_credit' => '套用貸款',
+ 'apply_credit' => '套用信用',
'system_settings' => '系統設定',
'archive_token' => '歸檔安全代碼',
'archived_token' => '歸檔安全代碼成功',
'archive_user' => '歸檔使用者資料',
'archived_user' => '歸檔使用者資料成功',
'archive_account_gateway' => '刪除閘道資料',
- 'archived_account_gateway' => '封存閘道資料成功',
+ 'archived_account_gateway' => '歸檔閘道資料成功',
'archive_recurring_invoice' => '歸檔週期性發票',
'archived_recurring_invoice' => '歸檔週期性發票成功',
'delete_recurring_invoice' => '刪除週期性發票',
@@ -833,7 +833,7 @@ $lang = array(
'task_file' => '任務檔案',
'no_mapper' => '此檔案的對應無效',
'invalid_csv_header' => '無效的 CSV 標頭',
- 'client_portal' => '用戶門戶頁面',
+ 'client_portal' => '客戶端頁面',
'admin' => '管理者',
'disabled' => '已停用',
'show_archived_users' => '顯示歸檔的使用者資料',
@@ -1005,7 +1005,7 @@ $lang = array(
'trial_call_to_action' => '開始免費試用',
'trial_success' => '啟用兩星期的專業版免費試用成功',
'overdue' => '逾期未付',
- 'white_label_text' => '購買一年的白牌授權 $:price,從發票和用戶門戶頁面中移除 Invoice Ninja 品牌。',
+ 'white_label_text' => '購買一年的白牌授權 $:price,從發票和客戶端中移除 Invoice Ninja 品牌。',
'user_email_footer' => '欲調整您的電子郵件通知設定。請造訪 :link',
'reset_password_footer' => '若您未提出這項重設密碼的要求,請寫電子郵件給我們的客服: :email',
'limit_users' => '抱歉,這將超過 :limit 名使用者的限制',
@@ -1033,7 +1033,7 @@ $lang = array(
'list_expenses' => '列出所有支出',
'list_recurring_invoices' => '列出週期性發票',
'list_payments' => '列出所有付款資料',
- 'list_credits' => '列出貸款資料',
+ 'list_credits' => '列出信用資料',
'tax_name' => '稅名',
'report_settings' => '報告設定',
'new_user' => '新使用者',
@@ -1116,10 +1116,10 @@ $lang = array(
'document_date' => '文件日期',
'document_size' => '大小',
- 'enable_client_portal' => '用戶門戶頁面',
- 'enable_client_portal_help' => '顯示/隱藏用戶門戶頁面。',
+ 'enable_client_portal' => '客戶端頁面',
+ 'enable_client_portal_help' => '顯示/隱藏客戶端。',
'enable_client_portal_dashboard' => '儀表板',
- 'enable_client_portal_dashboard_help' => '在用戶門戶頁面顯示/隱藏儀表板頁。',
+ 'enable_client_portal_dashboard_help' => '在客戶端顯示/隱藏儀表板頁。',
// Plans
'account_management' => '帳號管理',
@@ -1162,8 +1162,8 @@ $lang = array(
'enterprise_plan_product' => '企業方案',
'enterprise_plan_year_description' => '訂用 Invoice Ninja 企業版一年方案。',
'enterprise_plan_month_description' => '訂用 Invoice Ninja 企業版一個月方案。',
- 'plan_credit_product' => '貸款',
- 'plan_credit_description' => '未使用時間的貸款',
+ 'plan_credit_product' => '信用',
+ 'plan_credit_description' => '未使用時間的信用',
'plan_pending_monthly' => '將於 :date 切換至月租型',
'plan_refunded' => '已發送一筆退款。',
@@ -1387,7 +1387,7 @@ $lang = array(
'freq_two_years' => '兩年',
// Payment types
- 'payment_type_Apply Credit' => '套用貸款',
+ 'payment_type_Apply Credit' => '套用信用',
'payment_type_Bank Transfer' => '銀行轉帳',
'payment_type_Cash' => '現金',
'payment_type_Debit' => '簽帳卡',
@@ -1740,8 +1740,8 @@ $lang = array(
'industry_Other' => '其他',
'industry_Photography' => '攝影',
- 'view_client_portal' => '檢視用戶入口頁面',
- 'view_portal' => '檢視入口頁面',
+ 'view_client_portal' => '檢視客戶端',
+ 'view_portal' => '檢視客戶端',
'vendor_contacts' => '供應商連絡人',
'all' => '全部',
'selected' => '已選的',
@@ -1929,9 +1929,9 @@ $lang = array(
'deleted_product' => '已成功刪除產品資料',
'deleted_products' => '刪除 :count 筆產品資料成功',
'restored_product' => '復原產品資料成功',
- 'update_credit' => '更新貸款資料',
- 'updated_credit' => '更新貸款資料成功',
- 'edit_credit' => '編輯貸款資料',
+ 'update_credit' => '更新信用資料',
+ 'updated_credit' => '更新信用資料成功',
+ 'edit_credit' => '編輯信用資料',
'realtime_preview' => 'Realtime Preview',
'realtime_preview_help' => 'Realtime refresh PDF preview on the invoice page when editing invoice. Disable this to improve performance when editing invoices.',
'live_preview_help' => 'Display a live PDF preview on the invoice page.',
@@ -1940,7 +1940,7 @@ $lang = array(
'redirect_url' => '重新導向 URL',
'redirect_url_help' => '可選擇性地指定一個在付款完成後進行重新導向的網址。',
'save_draft' => '儲存草稿',
- 'refunded_credit_payment' => '已退款之貸款支付',
+ 'refunded_credit_payment' => '已退款之信用支付',
'keyboard_shortcuts' => '鍵盤快速鍵',
'toggle_menu' => '切換選單',
'new_...' => '新增 ...',
@@ -1998,7 +1998,7 @@ $lang = array(
'emailed_quotes' => '以電子郵件寄出報價單成功',
'website_url' => '網站網址',
'domain' => '網域',
- 'domain_help' => '在用戶門戶頁面和傳送電子郵件時使用。',
+ 'domain_help' => '在客戶端和傳送電子郵件時使用。',
'domain_help_website' => '用於寄送電子郵件時。',
'import_invoices' => '匯入發票',
'new_report' => '新報告',
@@ -2067,9 +2067,9 @@ $lang = array(
'updated_payment_term' => '更新付款條款成功',
'archived_payment_term' => '歸檔付款條款成功',
'resend_invite' => '重寄邀請函',
- 'credit_created_by' => '由 :transaction_reference付款所建立的貸款',
- 'created_payment_and_credit' => '已成功建立付款與貸款資料',
- 'created_payment_and_credit_emailed_client' => '成功建立付款和貸款,並透過電子郵件傳送給用戶',
+ 'credit_created_by' => '由 :transaction_reference付款所建立的信用',
+ 'created_payment_and_credit' => '已成功建立付款與信用資料',
+ 'created_payment_and_credit_emailed_client' => '成功建立付款和信用,並透過電子郵件傳送給用戶',
'create_project' => '建立專案',
'create_vendor' => '建立供應商',
'create_expense_category' => '建立類別',
@@ -2115,12 +2115,12 @@ $lang = array(
'fees_disabled_for_gateway' => '費用資料功能在此閘道停用。',
'logout_and_delete' => '登出/刪除帳號',
'tax_rate_type_help' => '選取專用稅率時調整行項目成本。 只有專用稅率可以用作預設值。',
- 'credit_note' => '貸款註記',
+ 'credit_note' => '信用註記',
'credit_issued_to' => '放款給',
- 'credit_to' => '貸款給',
- 'your_credit' => '您的貸款',
- 'credit_number' => '貸款編號',
- 'create_credit_note' => '建立貸款註記',
+ 'credit_to' => '信用給',
+ 'your_credit' => '您的信用',
+ 'credit_number' => '信用編號',
+ 'create_credit_note' => '建立信用註記',
'menu' => '選單',
'error_incorrect_gateway_ids' => '錯誤: 閘道表有錯誤的帳號名稱。',
'purge_data' => '清除資料',
@@ -2148,7 +2148,7 @@ $lang = array(
'contact_fields' => '聯絡人欄位',
'custom_contact_fields_help' => '於建立聯絡人資料時增加欄位,且可選擇在 PDF 檔案顯示欄位名稱與值。',
'datatable_info' => '顯示 :total 個項目的 :start 至 :end 項',
- 'credit_total' => '貸款總額',
+ 'credit_total' => '信用總額',
'mark_billable' => '標記計費',
'billed' => '已開立帳單',
'company_variables' => '公司變項',
@@ -2215,14 +2215,14 @@ $lang = array(
'restore_recurring_expense' => '復原週期性支出',
'restored_recurring_expense' => '復原週期性支出成功',
'delete_recurring_expense' => '刪除週期性支出',
- 'deleted_recurring_expense' => '已成功刪除經常性費用',
+ 'deleted_recurring_expense' => '已成功刪除週期性費用',
'view_recurring_expense' => '檢視週期性支出',
'taxes_and_fees' => '稅金與費用',
'import_failed' => '匯入失敗',
'recurring_prefix' => '用以標示週期性的前置符號',
'options' => '選項',
- 'credit_number_help' => '設定一個前置符號或使用自訂型態,以動態地為欠款發票設定貸款號碼。',
- 'next_credit_number' => '下一個貸款號碼是 :number。',
+ 'credit_number_help' => '設定一個前置符號或使用自訂型態,以動態地為欠款發票設定信用號碼。',
+ 'next_credit_number' => '下一個信用號碼是 :number。',
'padding_help' => '補齊數字所使用的零的個數。',
'import_warning_invalid_date' => '警告: 日期格式顯然無效。',
'product_notes' => '產品註記',
@@ -2239,7 +2239,7 @@ $lang = array(
'downloaded_quote' => '將會寄出一封附有發票的 PDF 檔案之報價單',
'downloaded_invoices' => '將會寄出一封附有發票的 PDF 檔案之電子郵件',
'downloaded_quotes' => '將會寄出一封附有發票的 PDF 檔案之報價單',
- 'clone_expense' => '再製',
+ 'clone_expense' => '複製',
'default_documents' => '預設的文件',
'send_email_to_client' => '向用戶傳送電子郵件',
'refund_subject' => '已辦理退款',
@@ -2514,7 +2514,7 @@ $lang = array(
'show_shipping_address_help' => '需要使用者提供其送貨地址',
'ship_to_billing_address' => '寄送至帳單地址',
'delivery_note' => '寄送註記',
- 'show_tasks_in_portal' => '在用戶門戶頁面顯示任務',
+ 'show_tasks_in_portal' => '在客戶端顯示任務',
'cancel_schedule' => '取消排程',
'scheduled_report' => '排程報告',
'scheduled_report_help' => '以電子郵件按照 :format 寄送 :report 報告給 :email',
@@ -2566,14 +2566,14 @@ $lang = array(
'project_error_multiple_clients' => '專案不能屬於不同的用戶',
'invoice_project' => '發票專案',
'module_recurring_invoice' => '週期性發票',
- 'module_credit' => '貸款',
+ 'module_credit' => '信用',
'module_quote' => '報價單與提案',
'module_task' => '任務與專案',
'module_expense' => '支出 & 供應商',
'module_ticket' => '票證',
'reminders' => '提醒通知',
'send_client_reminders' => '以電子郵件寄送提醒通知',
- 'can_view_tasks' => '任務顯示於入口頁面',
+ 'can_view_tasks' => '任務顯示於客戶端',
'is_not_sent_reminders' => '提醒通知未寄送',
'promotion_footer' => '您的優惠即將到期,立即以 :link 進行升級。',
'unable_to_delete_primary' => '注意: 欲刪除這項公司資料,先刪除所有相連結的公司。',
@@ -2600,7 +2600,7 @@ $lang = array(
'new_status' => '新的狀態',
'convert_products' => '轉換產品',
'convert_products_help' => '自動將產品價格轉換為用戶的貨幣',
- 'improve_client_portal_link' => '設定子域名以縮短用戶門戶頁面連結。',
+ 'improve_client_portal_link' => '設定子域名以縮短客戶端連結。',
'budgeted_hours' => '列入預算的小時',
'progress' => '進度',
'view_project' => '檢視專案',
@@ -2612,7 +2612,7 @@ $lang = array(
'expired_white_label' => '白牌授權已過期',
'return_to_login' => '回到登入頁面',
'convert_products_tip' => '注意: 加入名為「:name」的 :link 以查看匯率。',
- 'amount_greater_than_balance' => '此金額大於發票餘額,一筆貸款將與剩餘金額一起建立。',
+ 'amount_greater_than_balance' => '此金額大於發票餘額,一筆信用將與剩餘金額一起建立。',
'custom_fields_tip' => '使用 Label|Option1,Option2 以顯示選取方塊。',
'client_information' => '用戶資訊',
'updated_client_details' => '更新用戶詳細資料成功',
@@ -2687,7 +2687,7 @@ $lang = array(
'icon' => '圖示',
'proposal_not_found' => '無法提供查詢的提案',
'create_proposal_category' => '建立類別',
- 'clone_proposal_template' => '再製範本',
+ 'clone_proposal_template' => '複製範本',
'proposal_email' => '提案的電子郵件',
'proposal_subject' => ':account 的新提案 :number',
'proposal_message' => '若要檢視您的 :amount 之提案,按一下以下連結。',
@@ -2738,7 +2738,7 @@ $lang = array(
'invalid_url' => '無效的 URL',
'workflow_settings' => '工作流程設定',
'auto_email_invoice' => '自動電子郵件',
- 'auto_email_invoice_help' => '建立後自動透過電子郵件發送定期發票。',
+ 'auto_email_invoice_help' => '建立後自動透過電子郵件發送週期發票。',
'auto_archive_invoice' => '自動歸檔',
'auto_archive_invoice_help' => '付款後自動存檔發票。',
'auto_archive_quote' => '自動歸檔',
@@ -2753,11 +2753,11 @@ $lang = array(
'purge_client' => '清除用戶',
'purged_client' => '清除用戶成功',
'purge_client_warning' => '所有相關的紀錄 (發票、任務、支出、文件等等) 也將會刪除。',
- 'clone_product' => '再製產品資料',
+ 'clone_product' => '複製產品資料',
'item_details' => '項目詳細資料',
'send_item_details_help' => '將單項產品的詳細資料傳送到付款主頁面。',
'view_proposal' => '檢視提案',
- 'view_in_portal' => '在入口頁面檢視',
+ 'view_in_portal' => '在客戶端檢視',
'cookie_message' => '本網站使用 cookies 以確保您能在此得到最佳的使用經驗。',
'got_it' => '瞭解了!',
'vendor_will_create' => '將建立供應商',
@@ -2960,8 +2960,8 @@ $lang = array(
'previous_year' => '上一年度',
'compare_to' => '比較',
'last_week' => '上個星期',
- 'clone_to_invoice' => '再製到發票',
- 'clone_to_quote' => '再製到報價單',
+ 'clone_to_invoice' => '複製到發票',
+ 'clone_to_quote' => '複製到報價單',
'convert' => '轉換',
'last7_days' => '最近 7 天',
'last30_days' => '最近 30 天',
@@ -2997,7 +2997,7 @@ $lang = array(
'edit_document' => '編輯文件',
'uploaded_document' => '已成功上載文件',
'updated_document' => '已成功更新文件',
- 'archived_document' => '已成功封存文件',
+ 'archived_document' => '已成功歸檔文件',
'deleted_document' => '已成功刪除文件',
'restored_document' => '已成功還原文件',
'no_history' => '無歷史記錄',
@@ -3043,7 +3043,7 @@ $lang = array(
'number_counter' => '數字計數器',
'number_pattern' => '數字模式',
'custom_javascript' => '自訂 JavaScript',
- 'portal_mode' => '入口網站模式',
+ 'portal_mode' => '客戶端模式',
'attach_pdf' => '附上PDF',
'attach_documents' => '附加文件',
'attach_ubl' => '附上 UBL/電子發票',
@@ -3065,7 +3065,7 @@ $lang = array(
'edit_company_gateway' => '編輯閘道',
'created_company_gateway' => '建立閘道資料成功',
'updated_company_gateway' => '更新閘道資料成功',
- 'archived_company_gateway' => '封存閘道資料成功',
+ 'archived_company_gateway' => '歸檔閘道資料成功',
'deleted_company_gateway' => '刪除閘道資料成功',
'restored_company_gateway' => '復原閘道成功',
'continue_editing' => '繼續編輯',
@@ -3089,7 +3089,7 @@ $lang = array(
'edit_group' => '編輯群組',
'created_group' => '已成功建立群組',
'updated_group' => '已成功更新群組',
- 'archived_group' => '已成功封存群組',
+ 'archived_group' => '已成功歸檔群組',
'deleted_group' => '已成功刪除群組',
'restored_group' => '已成功還原群組',
'upload_logo' => '上傳您的公司徽標',
@@ -3188,7 +3188,7 @@ $lang = array(
'domain_url' => '網域網址',
'password_is_too_easy' => '密碼必須包含大寫字元和數字',
'client_portal_tasks' => '客戶端任務',
- 'client_portal_dashboard' => '客戶入口網站儀表板',
+ 'client_portal_dashboard' => '客戶端儀表板',
'please_enter_a_value' => '請輸入一個值',
'deleted_logo' => '成功刪除標誌',
'generate_number' => '產生號碼',
@@ -3290,7 +3290,7 @@ $lang = array(
'tax_rate1' => '稅率1',
'tax_rate2' => '稅率2',
'tax_rate3' => '稅率3',
- 'archived_at' => '存檔於',
+ 'archived_at' => '歸檔於',
'has_expenses' => '有費用',
'custom_taxes1' => '關稅 1',
'custom_taxes2' => '關稅 2',
@@ -3361,14 +3361,14 @@ $lang = array(
'slack_webhook_url' => 'Slack Webhook URL',
'partial_payment' => '部分付款',
'partial_payment_email' => '部分付款電子郵件',
- 'clone_to_credit' => '克隆到信用',
+ 'clone_to_credit' => '複製到信用',
'emailed_credit' => '已成功透過電子郵件發送信用證',
'marked_credit_as_sent' => '已成功將信用標記為已發送',
'email_subject_payment_partial' => '電子郵件部分付款主題',
'is_approved' => '被批准',
'migration_went_wrong' => '哎呀!出事了!在開始遷移之前,請確保您已設定 Invoice Ninja v5 實例。',
'cross_migration_message' => '不允許跨帳戶遷移。請在此閱讀更多相關資訊: https ://invoiceninja.github.io/docs/migration/#troubleshooting',
- 'email_credit' => '電子郵件信用',
+ 'email_credit' => '電郵信用',
'client_email_not_set' => '客戶沒有設定電子郵件地址',
'ledger' => '分類帳',
'view_pdf' => '查看PDF',
@@ -3514,7 +3514,7 @@ $lang = array(
'removed_token' => '成功刪除令牌',
'restored_token' => '成功恢復令牌',
'client_registration' => '客戶註冊',
- 'client_registration_help' => '使客戶能夠在入口網站中自行註冊',
+ 'client_registration_help' => '使客戶能夠在客戶端中自行註冊',
'customize_and_preview' => '客製化和預覽',
'search_document' => '搜尋 1 個文檔',
'search_design' => '搜尋 1 設計',
@@ -3544,7 +3544,7 @@ $lang = array(
'add_documents_to_invoice_help' => '使文件對客戶可見',
'convert_currency_help' => '設定匯率',
'expense_settings' => '費用設定',
- 'clone_to_recurring' => '克隆到重複',
+ 'clone_to_recurring' => '複製到重複',
'crypto' => '加密貨幣',
'user_field' => '使用者欄位',
'variables' => '變數',
@@ -3556,9 +3556,9 @@ $lang = array(
'total_taxes' => '總稅金',
'line_taxes' => '行稅',
'total_fields' => '總字段數',
- 'stopped_recurring_invoice' => '已成功停止定期發票',
- 'started_recurring_invoice' => '已成功開始定期發票',
- 'resumed_recurring_invoice' => '已成功恢復定期發票',
+ 'stopped_recurring_invoice' => '已成功停止週期發票',
+ 'started_recurring_invoice' => '已成功開始週期發票',
+ 'resumed_recurring_invoice' => '已成功恢復週期發票',
'gateway_refund' => '網關退款',
'gateway_refund_help' => '透過支付網關處理退款',
'due_date_days' => '到期日',
@@ -3570,11 +3570,11 @@ $lang = array(
'endless' => '無盡',
'next_send_date' => '下次發送日期',
'remaining_cycles' => '剩餘週期',
- 'created_recurring_invoice' => '已成功建立定期發票',
- 'updated_recurring_invoice' => '已成功更新定期發票',
+ 'created_recurring_invoice' => '已成功建立週期發票',
+ 'updated_recurring_invoice' => '已成功更新週期發票',
'removed_recurring_invoice' => '已成功刪除定期發票',
- 'search_recurring_invoice' => '搜尋 1 份經常性發票',
- 'search_recurring_invoices' => '搜尋:count經常性發票',
+ 'search_recurring_invoice' => '搜尋 1 份週期性發票',
+ 'search_recurring_invoices' => '搜尋:count週期性發票',
'send_date' => '發送日期',
'auto_bill_on' => '自動計費開啟',
'minimum_under_payment_amount' => '最低付款金額',
@@ -3649,8 +3649,8 @@ $lang = array(
'sidebar_editor' => '側邊欄編輯器',
'please_type_to_confirm' => '請輸入「 :value 」確認',
'purge' => '清除',
- 'clone_to' => '克隆到',
- 'clone_to_other' => '克隆到其他',
+ 'clone_to' => '複製到',
+ 'clone_to_other' => '複製到其他',
'labels' => '標籤',
'add_custom' => '新增自訂',
'payment_tax' => '繳稅',
@@ -3659,26 +3659,26 @@ $lang = array(
'paid_invoices_are_locked' => '已付款發票已鎖定',
'source_code' => '原始碼',
'app_platforms' => '應用程式平台',
- 'archived_task_statuses' => '已成功存檔:value任務狀態',
+ 'archived_task_statuses' => '已成功歸檔:value任務狀態',
'deleted_task_statuses' => '已成功刪除:value任務狀態',
'restored_task_statuses' => '成功恢復:value任務狀態',
'deleted_expense_categories' => '已成功刪除費用:value類別',
'restored_expense_categories' => '成功恢復費用:value類別',
- 'archived_recurring_invoices' => '已成功存檔定期:value發票',
- 'deleted_recurring_invoices' => '已成功刪除定期:value發票',
- 'restored_recurring_invoices' => '已成功恢復定期:value發票',
- 'archived_webhooks' => '已成功存檔:value webhooks',
+ 'archived_recurring_invoices' => '已成功歸檔週期:value發票',
+ 'deleted_recurring_invoices' => '已成功刪除週期:value發票',
+ 'restored_recurring_invoices' => '已成功恢復週期:value發票',
+ 'archived_webhooks' => '已成功歸檔:value webhooks',
'deleted_webhooks' => '已成功刪除:value webhooks',
'removed_webhooks' => '已成功刪除:value webhooks',
'restored_webhooks' => '已成功恢復:value webhooks',
'api_docs' => 'API文件',
- 'archived_tokens' => '已成功存檔:value令牌',
+ 'archived_tokens' => '已成功歸檔:value令牌',
'deleted_tokens' => '已成功刪除:value令牌',
'restored_tokens' => '已成功恢復:value令牌',
- 'archived_payment_terms' => '已成功存檔:value付款條件',
+ 'archived_payment_terms' => '已成功歸檔:value付款條件',
'deleted_payment_terms' => '已成功刪除:value付款條件',
'restored_payment_terms' => '已成功恢復:value付款條件',
- 'archived_designs' => '成功存檔:value設計',
+ 'archived_designs' => '成功歸檔:value設計',
'deleted_designs' => '成功刪除:value設計',
'restored_designs' => '成功恢復:value設計',
'restored_credits' => '已成功恢復:value積分',
@@ -3686,13 +3686,13 @@ $lang = array(
'deleted_users' => '已成功刪除:value用戶',
'removed_users' => '已成功刪除:value用戶',
'restored_users' => '成功恢復:value用戶',
- 'archived_tax_rates' => '成功存檔:value稅率',
+ 'archived_tax_rates' => '成功歸檔:value稅率',
'deleted_tax_rates' => '已成功刪除:value稅率',
'restored_tax_rates' => '成功恢復:value稅率',
- 'archived_company_gateways' => '已成功存檔:value網關',
+ 'archived_company_gateways' => '已成功歸檔:value網關',
'deleted_company_gateways' => '已成功刪除:value網關',
'restored_company_gateways' => '成功恢復:value網關',
- 'archived_groups' => '已成功存檔:value組',
+ 'archived_groups' => '已成功歸檔:value組',
'deleted_groups' => '已成功刪除:value組',
'restored_groups' => '成功恢復:value組',
'archived_documents' => '已成功歸檔:value文檔',
@@ -3784,7 +3784,7 @@ $lang = array(
'notification_partial_payment_paid' => '客戶:client向:invoice支付了部分:amount',
'notification_bot' => '通知機器人',
'invoice_number_placeholder' => '發票 # :invoice',
- 'entity_number_placeholder' => ':entity # :entity_number',
+ 'entity_number_placeholder' => ':entity # :entity _number',
'email_link_not_working' => '如果上面的按鈕不適合您,請點擊連結',
'display_log' => '顯示日誌',
'send_fail_logs_to_our_server' => '報告錯誤以幫助改進應用程式',
@@ -3799,10 +3799,10 @@ $lang = array(
'list_of_invoices' => '發票清單',
'with_selected' => '與選定的',
'invoice_still_unpaid' => '該發票仍未支付。點擊按鈕完成付款',
- 'list_of_recurring_invoices' => '經常性發票清單',
- 'details_of_recurring_invoice' => '以下是有關定期發票的一些詳細信息',
+ 'list_of_recurring_invoices' => '週期性發票清單',
+ 'details_of_recurring_invoice' => '以下是有關週期發票的一些詳細信息',
'cancellation' => '消除',
- 'about_cancellation' => '如果您想停止定期發票,請點選請求取消。',
+ 'about_cancellation' => '如果您想停止週期發票,請點選請求取消。',
'cancellation_warning' => '警告!您請求取消此服務。您的服務可能會被取消,恕不另行通知。',
'cancellation_pending' => '取消待定,我們會聯絡您!',
'list_of_payments' => '付款清單',
@@ -3880,7 +3880,7 @@ $lang = array(
'under_payments_disabled' => '本公司不支援少付款。',
'over_payments_disabled' => '本公司不支援超額付款。',
'saved_at' => '保存於:time',
- 'credit_payment' => '貸記應用於發票:invoice _number',
+ 'credit_payment' => '信用應用於發票:invoice _number',
'credit_subject' => '來自:account的新信用:number',
'credit_message' => '要查看:amount的積分,請點擊下面的連結。',
'payment_type_Crypto' => '加密貨幣',
@@ -3928,15 +3928,15 @@ $lang = array(
'invoice_number_taken' => '發票號碼已取',
'payment_id_required' => '需要付款“id”。',
'unable_to_retrieve_payment' => '無法檢索指定的付款',
- 'invoice_not_related_to_payment' => '發票 ID :invoice與此付款無關',
- 'credit_not_related_to_payment' => '信用 ID :credit與此付款無關',
+ 'invoice_not_related_to_payment' => 'Invoice # :invoice is not related to this payment',
+ 'credit_not_related_to_payment' => 'Credit # :credit is not related to this payment',
'max_refundable_invoice' => '嘗試退款超過發票 ID :invoice允許的金額,最大可退款金額為:amount',
'refund_without_invoices' => '如果嘗試退還附有發票的付款,請指定要退款的有效發票。',
'refund_without_credits' => '嘗試退還帶有積分的付款,請指定要退款的有效積分。',
'max_refundable_credit' => '嘗試退款超過允許的信用額:credit ,最大可退款金額為:amount',
'project_client_do_not_match' => '專案客戶與實體客戶不匹配',
'quote_number_taken' => '報價單號碼已被佔用',
- 'recurring_invoice_number_taken' => '經常性發票編號:number已使用',
+ 'recurring_invoice_number_taken' => '週期性發票編號:number已使用',
'user_not_associated_with_account' => '使用者未與此帳戶關聯',
'amounts_do_not_balance' => '金額未正確平衡。',
'insufficient_applied_amount_remaining' => '剩餘申請金額不足以支付付款。',
@@ -3951,7 +3951,7 @@ $lang = array(
'large_account_update_parameter' => '如果沒有 update_at 參數,則無法載入大型帳戶',
'no_backup_exists' => '此活動不存在備份',
'company_user_not_found' => '未找到公司用戶記錄',
- 'no_credits_found' => '沒有找到學分。',
+ 'no_credits_found' => '沒有找到信用。',
'action_unavailable' => '請求的操作:action不可用。',
'no_documents_found' => '沒有找到文件',
'no_group_settings_found' => '未找到組設定',
@@ -3979,14 +3979,14 @@ $lang = array(
'start_multiselect' => '開始多選',
'email_sent_to_confirm_email' => '已發送一封電子郵件以確認電子郵件地址',
'converted_paid_to_date' => '轉換為付費日期',
- 'converted_credit_balance' => '轉換後的貸方餘額',
+ 'converted_credit_balance' => '轉換後的信用方餘額',
'converted_total' => '換算總計',
'reply_to_name' => '回覆名稱',
'payment_status_-2' => '部分未應用',
'color_theme' => '顏色主題',
'start_migration' => '開始遷移',
- 'recurring_cancellation_request' => ':contact請求取消定期發票',
- 'recurring_cancellation_request_body' => '客戶:contact請求取消經常:client發票:invoice',
+ 'recurring_cancellation_request' => ':contact請求取消週期發票',
+ 'recurring_cancellation_request_body' => '客戶:contact請求取消週期:client發票:invoice',
'hello' => '你好',
'group_documents' => '集團文件',
'quote_approval_confirmation_label' => '您確定要批准此報價嗎?',
@@ -4015,7 +4015,7 @@ $lang = array(
'billing_coupon_notice' => '您的折扣將在結帳時套用。',
'use_last_email' => '使用最後的電子郵件',
'activate_company' => '啟動公司',
- 'activate_company_help' => '啟用電子郵件、定期發票和通知',
+ 'activate_company_help' => '啟用電子郵件、週期發票和通知',
'an_error_occurred_try_again' => '發生錯誤,請重試',
'please_first_set_a_password' => '請先設定密碼',
'changing_phone_disables_two_factor' => '警告:更改您的電話號碼將停用 2FA',
@@ -4036,7 +4036,7 @@ $lang = array(
'invoice_task_datelog' => '發票任務日期日誌',
'invoice_task_datelog_help' => '將日期詳細資料新增至發票行項目',
'promo_code' => '促銷代碼',
- 'recurring_invoice_issued_to' => '定期發票開立至',
+ 'recurring_invoice_issued_to' => '週期發票開立至',
'subscription' => '訂閱',
'new_subscription' => '新訂閱',
'deleted_subscription' => '已成功刪除訂閱',
@@ -4056,7 +4056,7 @@ $lang = array(
'shared_invoice_credit_counter' => '共享發票/信用櫃檯',
'activity_80' => ':user建立訂閱:subscription',
'activity_81' => ':user更新訂閱:subscription',
- 'activity_82' => ':user存檔訂閱:subscription',
+ 'activity_82' => ':user歸檔訂閱:subscription',
'activity_83' => ':user已刪除訂閱:subscription',
'activity_84' => ':user恢復訂閱:subscription',
'amount_greater_than_balance_v5' => '金額大於發票餘額。您不能多付發票費用。',
@@ -4073,11 +4073,11 @@ $lang = array(
'migration_already_completed_desc' => '看起來您已經將:company _name 遷移到 Invoice Ninja 的 V5 版本了。如果您想重新開始,可以強制遷移以擦除現有資料。',
'payment_method_cannot_be_authorized_first' => '完成第一筆交易後,可以儲存此付款方式以供將來使用。不要忘記在付款過程中檢查“商店詳細資料”。',
'new_account' => '新帳戶',
- 'activity_100' => ':user創建了定期發票:recurring_invoice',
- 'activity_101' => ':user更新的經常性發票:recurring_invoice',
- 'activity_102' => ':user存檔的經常性發票:recurring_invoice',
- 'activity_103' => ':user刪除定期發票:recurring_invoice',
- 'activity_104' => ':user恢復定期發票:recurring_invoice',
+ 'activity_100' => ':user創建了週期發票:recurring_invoice',
+ 'activity_101' => ':user更新的週期性發票:recurring_invoice',
+ 'activity_102' => ':user歸檔的週期性發票:recurring_invoice',
+ 'activity_103' => ':user刪除週期發票:recurring_invoice',
+ 'activity_104' => ':user恢復週期發票:recurring_invoice',
'new_login_detected' => '偵測到您的帳戶有新的登入資訊。',
'new_login_description' => '您最近從新位置或裝置登入了您的 Invoice Ninja 帳戶: IP: :ip時間: :time電子郵件: :email',
'contact_details' => '聯絡方式',
@@ -4107,7 +4107,7 @@ $lang = array(
'login_without_password' => '無需密碼登入',
'email_sent' => '當發票寄出 後,以電子郵件通知我',
'one_time_purchases' => '一次性購買',
- 'recurring_purchases' => '經常性購買',
+ 'recurring_purchases' => '週期性購買',
'you_might_be_interested_in_following' => '您可能對以下內容感興趣',
'quotes_with_status_sent_can_be_approved' => '只有狀態為「已發送」的報價才能獲得批准。過期報價無法獲得批准。',
'no_quotes_available_for_download' => '沒有可供下載的報價。',
@@ -4126,14 +4126,14 @@ $lang = array(
'lang_Latvian' => '拉脫維亞語',
'expiry_date' => '到期日',
'cardholder_name' => '持卡人姓名',
- 'recurring_quote_number_taken' => '重複報價編號:number已被佔用',
+ 'recurring_quote_number_taken' => '週期報價編號:number已被佔用',
'account_type' => '帳戶類型',
'locality' => '地點',
'checking' => '檢查',
'savings' => '儲蓄',
'unable_to_verify_payment_method' => '無法驗證付款方式。',
'generic_gateway_error' => '網關配置錯誤。請檢查您的憑證。',
- 'my_documents' => '我的文件',
+ 'my_documents' => 'My Documents',
'payment_method_cannot_be_preauthorized' => '此付款方式無法預先授權。',
'kbc_cbc' => '韓國廣播公司/加拿大廣播公司',
'bancontact' => '聯繫銀行',
@@ -4154,7 +4154,7 @@ $lang = array(
'becs_mandate' => '提供您的銀行帳戶詳細信息,即表示您同意本直接借記請求和直接借記請求服務協議 ,並授權 Stripe Payments Australia Pty Ltd ACN 160 180 343 直接借記用戶 ID 號 507156(「Stripe」)透過代表:company (「商家」)的大量電子清算系統 (BECS) 處理商家單獨向您傳達的任何金額。您證明您是上述帳戶的帳戶持有人或授權簽署人。',
'you_need_to_accept_the_terms_before_proceeding' => '您需要先接受條款才能繼續。',
'direct_debit' => '直接借記',
- 'clone_to_expense' => '克隆到費用',
+ 'clone_to_expense' => '複製到費用',
'checkout' => '查看',
'acss' => 'ACSS 金融卡',
'invalid_amount' => '金額無效。僅限數字/小數值。',
@@ -4216,10 +4216,10 @@ $lang = array(
'for_best_performance' => '為了獲得最佳性能,請下載:app應用程式',
'bulk_email_invoice' => '電子郵件發票',
'bulk_email_quote' => '電子郵件報價',
- 'bulk_email_credit' => '電子郵件信用',
- 'removed_recurring_expense' => '成功消除經常性費用',
- 'search_recurring_expense' => '搜尋經常性費用',
- 'search_recurring_expenses' => '搜尋經常性費用',
+ 'bulk_email_credit' => '電郵信用',
+ 'removed_recurring_expense' => '成功消除週期性費用',
+ 'search_recurring_expense' => '搜尋週期性費用',
+ 'search_recurring_expenses' => '搜尋週期性費用',
'last_sent_date' => '最後發送日期',
'include_drafts' => '包括草稿',
'include_drafts_help' => '在報告中包含草稿記錄',
@@ -4260,10 +4260,10 @@ $lang = array(
'free_trial_ends_in_days' => 'Pro 計畫試用期將於:count天後結束,點選升級。',
'free_trial_ends_today' => '今天是Pro計畫試用的最後一天,點選升級。',
'change_email' => '更改電子郵件',
- 'client_portal_domain_hint' => '可以選擇配置單獨的客戶端入口網站網域',
- 'tasks_shown_in_portal' => '門戶中顯示的任務',
+ 'client_portal_domain_hint' => '可以選擇配置單獨的客戶端網域',
+ 'tasks_shown_in_portal' => '客戶端中顯示的任務',
'uninvoiced' => '未開發票',
- 'subdomain_guide' => '子網域在客戶端入口網站中用於個性化連結以匹配您的品牌。即,https://your-brand.invoicing.co',
+ 'subdomain_guide' => '子網域在客戶端中用於個性化連結以匹配您的品牌。即,https://your-brand.invoicing.co',
'send_time' => '發送時間',
'import_settings' => '導入設定',
'json_file_missing' => '請提供 JSON 文件',
@@ -4273,7 +4273,7 @@ $lang = array(
'wait_for_data' => '請等待資料載入完成',
'net_total' => '淨總值',
'has_taxes' => '有稅',
- 'import_customers' => '進口客戶',
+ 'import_customers' => '匯入客戶',
'imported_customers' => '成功開始導入客戶',
'login_success' => '登入成功',
'login_failure' => '登入失敗',
@@ -4326,7 +4326,7 @@ $lang = array(
'rest_method' => '休息法',
'header_key' => '標題鍵',
'header_value' => '標頭值',
- 'recurring_products' => '重複產品',
+ 'recurring_products' => '週期產品',
'promo_discount' => '促銷折扣',
'allow_cancellation' => '允許取消',
'per_seat_enabled' => '每個席位已啟用',
@@ -4354,17 +4354,17 @@ $lang = array(
'load_color_theme' => '載入顏色主題',
'lang_Estonian' => '愛沙尼亞語',
'marked_credit_as_paid' => '已成功將信用標記為已付款',
- 'marked_credits_as_paid' => '已成功將積分標記為已付款',
+ 'marked_credits_as_paid' => '已成功將信用標記為已付款',
'wait_for_loading' => '資料載入 - 請等待完成',
'wait_for_saving' => '資料保存 - 請等待完成',
'html_preview_warning' => '注意:此處所做的更改只能預覽,必須在上面的選項卡中應用才能保存',
'remaining' => '其餘的',
'invoice_paid' => '發票已付',
- 'activity_120' => ':user創建經常性費用:recurring_expense',
- 'activity_121' => ':user更新經常性費用:recurring_expense',
- 'activity_122' => ':user存檔的經常性費用:recurring_expense',
- 'activity_123' => ':user刪除經常性費用:recurring_expense',
- 'activity_124' => ':user恢復經常性費用:recurring_expense',
+ 'activity_120' => ':user創建週期性費用:recurring_expense',
+ 'activity_121' => ':user更新週期性費用:recurring_expense',
+ 'activity_122' => ':user歸檔的週期性費用:recurring_expense',
+ 'activity_123' => ':user刪除週期性費用:recurring_expense',
+ 'activity_124' => ':user恢復週期性費用:recurring_expense',
'fpx' => "FPX",
'to_view_entity_set_password' => '要查看:entity您需要設定密碼。',
'unsubscribe' => '退訂',
@@ -4389,11 +4389,11 @@ $lang = array(
'file_saved_in_downloads_folder' => '該文件已保存在下載資料夾中',
'small' => '小的',
'quotes_backup_subject' => '您的報價可供下載',
- 'credits_backup_subject' => '您的積分已準備好下載',
+ 'credits_backup_subject' => '您的信用已準備好下載',
'document_download_subject' => '您的文件已可供下載',
'reminder_message' => ':balance的發票:number提醒',
'gmail_credentials_invalid_subject' => '使用 GMail 傳送無效憑證',
- 'gmail_credentials_invalid_body' => '您的 GMail 憑證不正確,請登入管理員入口網站並導航至“設定”>“使用者詳細資料”,然後中斷並重新連接您的 GMail 帳戶。我們將每天向您發送此通知,直到此問題解決',
+ 'gmail_credentials_invalid_body' => '您的 GMail 憑證不正確,請登入管理員客戶端並導航至“設定”>“使用者詳細資料”,然後中斷並重新連接您的 GMail 帳戶。我們將每天向您發送此通知,直到此問題解決',
'total_columns' => '總計字段',
'view_task' => '查看任務',
'cancel_invoice' => '取消',
@@ -4418,21 +4418,21 @@ $lang = array(
'signed_in_as' => '登入身份',
'total_results' => '總結果',
'restore_company_gateway' => '恢復網關',
- 'archive_company_gateway' => '存檔網關',
+ 'archive_company_gateway' => '歸檔網關',
'delete_company_gateway' => '刪除網關',
'exchange_currency' => '貨幣兌換',
'tax_amount1' => '稅額1',
'tax_amount2' => '稅額2',
'tax_amount3' => '稅額3',
'update_project' => '更新項目',
- 'auto_archive_invoice_cancelled' => '自動存檔已取消的發票',
- 'auto_archive_invoice_cancelled_help' => '取消時自動存檔發票',
+ 'auto_archive_invoice_cancelled' => '自動歸檔已取消的發票',
+ 'auto_archive_invoice_cancelled_help' => '取消時自動歸檔發票',
'no_invoices_found' => '沒有找到發票',
'created_record' => '記錄創建成功',
- 'auto_archive_paid_invoices' => '自動存檔付費',
- 'auto_archive_paid_invoices_help' => '付款後自動存檔發票。',
- 'auto_archive_cancelled_invoices' => '自動存檔已取消',
- 'auto_archive_cancelled_invoices_help' => '取消時自動存檔發票。',
+ 'auto_archive_paid_invoices' => '自動歸檔付費',
+ 'auto_archive_paid_invoices_help' => '付款後自動歸檔發票。',
+ 'auto_archive_cancelled_invoices' => '自動歸檔已取消',
+ 'auto_archive_cancelled_invoices_help' => '取消時自動歸檔發票。',
'alternate_pdf_viewer' => '備用 PDF 檢視器',
'alternate_pdf_viewer_help' => '改進 PDF 預覽的滾動功能 [BETA]',
'currency_cayman_island_dollar' => '開曼群島元',
@@ -4452,7 +4452,7 @@ $lang = array(
'export_format' => '導出格式',
'export_type' => '出口類型',
'stop_on_unpaid' => '停止未付款',
- 'stop_on_unpaid_help' => '如果最後一張發票未付款,請停止建立定期發票。',
+ 'stop_on_unpaid_help' => '如果最後一張發票未付款,請停止建立週期發票。',
'use_quote_terms' => '使用報價條款',
'use_quote_terms_help' => '將報價單轉換為發票時',
'add_country' => '新增國家/地區',
@@ -4510,7 +4510,7 @@ $lang = array(
'purchase_order_details' => '採購訂單詳細信息',
'qr_iban' => 'QR 圖碼 IBAN',
'besr_id' => 'BESR ID',
- 'clone_to_purchase_order' => '克隆到 PO',
+ 'clone_to_purchase_order' => '複製到 PO',
'vendor_email_not_set' => '供應商沒有設定電子郵件地址',
'bulk_send_email' => '發電子郵件',
'marked_purchase_order_as_sent' => '已成功將採購訂單標記為已發送',
@@ -4599,7 +4599,7 @@ $lang = array(
'total_pending_expenses' => '待處理費用',
'total_invoiced_expenses' => '已開立發票的費用',
'total_invoice_paid_expenses' => '發票支付費用',
- 'vendor_portal' => '供應商入口網站',
+ 'vendor_portal' => '供應商客戶端',
'send_code' => '傳送代碼',
'save_to_upload_documents' => '儲存上傳文檔記錄',
'expense_tax_rates' => '費用稅率',
@@ -4618,7 +4618,7 @@ $lang = array(
'bulk_email_purchase_orders' => '透過電子郵件發送採購訂單',
'bulk_email_invoices' => '電子郵件發票',
'bulk_email_quotes' => '電子郵件報價',
- 'bulk_email_credits' => '電子郵件積分',
+ 'bulk_email_credits' => '電郵信用',
'archive_purchase_order' => '存檔採購訂單',
'restore_purchase_order' => '恢復採購訂單',
'delete_purchase_order' => '刪除採購訂單',
@@ -4673,7 +4673,7 @@ $lang = array(
'edit_transaction' => '編輯交易',
'created_transaction' => '交易創建成功',
'updated_transaction' => '交易更新成功',
- 'archived_transaction' => '已成功存檔交易',
+ 'archived_transaction' => '已成功歸檔交易',
'deleted_transaction' => '成功刪除交易',
'removed_transaction' => '成功刪除交易',
'restored_transaction' => '交易恢復成功',
@@ -4688,9 +4688,11 @@ $lang = array(
'verify_phone_number_2fa_help' => '請驗證您的電話號碼以進行 2FA 備份',
'enable_applying_payments_later' => '啟用稍後申請付款',
'line_item_tax_rates' => '行項目稅率',
- 'show_tasks_in_client_portal' => '在客戶端入口網站中顯示任務',
+ 'show_tasks_in_client_portal' => '在客戶端中顯示任務',
'notification_quote_expired_subject' => ':client的報價:invoice已過期',
'notification_quote_expired' => '以下針對客戶:client和:amount的報價:invoice現已過期。',
+ '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' => '自動同步',
'refresh_accounts' => '刷新帳戶',
'upgrade_to_connect_bank_account' => '升級至企業版以連結您的銀行帳戶',
@@ -4714,7 +4716,7 @@ $lang = array(
'use_inventory_management' => '使用庫存管理',
'use_inventory_management_help' => '要求產品有庫存',
'optional_products' => '可選產品',
- 'optional_recurring_products' => '可選的經常性產品',
+ 'optional_recurring_products' => '可選的週期性產品',
'convert_matched' => '轉變',
'auto_billed_invoice' => '已成功排隊發票以自動計費',
'auto_billed_invoices' => '已成功將發票排隊以自動計費',
@@ -4735,7 +4737,7 @@ $lang = array(
'edit_transaction_rule' => '編輯交易規則',
'created_transaction_rule' => '規則創建成功',
'updated_transaction_rule' => '交易規則更新成功',
- 'archived_transaction_rule' => '交易規則存檔成功',
+ 'archived_transaction_rule' => '交易規則歸檔成功',
'deleted_transaction_rule' => '成功刪除交易規則',
'removed_transaction_rule' => '成功刪除交易規則',
'restored_transaction_rule' => '交易規則恢復成功',
@@ -4743,7 +4745,7 @@ $lang = array(
'search_transaction_rules' => '搜尋交易規則',
'payment_type_Interac E-Transfer' => 'Interac 電子轉賬',
'delete_bank_account' => '刪除銀行帳戶',
- 'archive_transaction' => '存檔交易',
+ 'archive_transaction' => '歸檔交易',
'delete_transaction' => '刪除交易',
'otp_code_message' => '我們已將代碼發送至:email輸入此代碼以繼續。',
'otp_code_subject' => '您的一次性密碼',
@@ -4775,14 +4777,14 @@ $lang = array(
'import_completed' => '導入完成',
'client_statement_body' => '附上您從:start _date 到:end _date 的聲明。',
'email_queued' => '電子郵件已排隊',
- 'clone_to_recurring_invoice' => '克隆到定期發票',
+ 'clone_to_recurring_invoice' => '複製到週期發票',
'inventory_threshold' => '庫存閾值',
'emailed_statement' => '已成功排隊要傳送的語句',
'show_email_footer' => '顯示電子郵件頁腳',
'invoice_task_hours' => '發票任務時間',
'invoice_task_hours_help' => '將小時數加入發票行項目',
'auto_bill_standard_invoices' => '自動帳單標準發票',
- 'auto_bill_recurring_invoices' => '自動開立定期發票',
+ 'auto_bill_recurring_invoices' => '自動開立週期發票',
'email_alignment' => '電子郵件對齊',
'pdf_preview_location' => 'PDF 預覽位置',
'mailgun' => '郵件槍',
@@ -4814,7 +4816,7 @@ $lang = array(
'edit_schedule' => '編輯日程',
'created_schedule' => '已成功建立時間表',
'updated_schedule' => '已成功更新時間表',
- 'archived_schedule' => '已成功存檔時間表',
+ 'archived_schedule' => '已成功歸檔時間表',
'deleted_schedule' => '已成功刪除行程',
'removed_schedule' => '已成功刪除行程',
'restored_schedule' => '已成功復原行程',
@@ -4880,7 +4882,7 @@ $lang = array(
'record_not_found' => '找不到記錄',
'minimum_payment_amount' => '最低付款金額',
'client_initiated_payments' => '客戶發起的付款',
- 'client_initiated_payments_help' => '支援在客戶入口網站中進行無發票付款',
+ 'client_initiated_payments_help' => '支援在客戶端中進行無發票付款',
'share_invoice_quote_columns' => '共享發票/報價欄',
'cc_email' => '副本電子郵件',
'payment_balance' => '付款餘額',
@@ -4896,7 +4898,7 @@ $lang = array(
'edit_payment_link' => '編輯付款連結',
'created_payment_link' => '付款連結建立成功',
'updated_payment_link' => '付款連結已成功更新',
- 'archived_payment_link' => '付款連結已成功存檔',
+ 'archived_payment_link' => '付款連結已成功歸檔',
'deleted_payment_link' => '已成功刪除付款鏈接',
'removed_payment_link' => '已成功刪除付款鏈接',
'restored_payment_link' => '付款連結已成功恢復',
@@ -5014,7 +5016,7 @@ $lang = array(
'buy_price' => '購買價格',
'country_Macedonia' => '馬其頓',
'admin_initiated_payments' => '管理員發起付款',
- 'admin_initiated_payments_help' => '支援在管理入口網站中輸入付款而無需發票',
+ 'admin_initiated_payments_help' => '支援在客戶端中輸入付款而無需發票',
'paid_date' => '支付日期',
'downloaded_entities' => '電子郵件將與 PDF 一起發送',
'lang_French - Swiss' => '法語 - 瑞士語',
@@ -5053,7 +5055,7 @@ $lang = array(
'unlinked_transactions' => '已成功取消連結:count事務',
'unlinked_transaction' => '成功取消關聯交易',
'view_dashboard_permission' => '允許使用者存取儀表板,資料僅限於可用權限',
- 'marked_sent_credits' => '已成功標記已發送的積分',
+ 'marked_sent_credits' => '已成功標記已發送的信用',
'show_document_preview' => '顯示文件預覽',
'cash_accounting' => '收付實現制會計',
'click_or_drop_files_here' => '點擊或將檔案拖放到此處',
@@ -5164,7 +5166,7 @@ $lang = array(
'unsubscribe_help' => '您目前尚未訂閱,因此目前不會收到電子郵件。',
'notification_purchase_order_bounced' => '我們無法將採購訂單:invoice交付給:contact 。 :error',
'notification_purchase_order_bounced_subject' => '無法交付採購訂單:invoice',
- 'show_pdfhtml_on_mobile' => '在行動裝置上檢視時顯示實體的 HTML 版本',
+ 'show_pdfhtml_on_mobile' => 'Display HTML Version When Viewing On Mobile',
'show_pdfhtml_on_mobile_help' => '為了改善視覺化效果,在行動裝置上查看時顯示 HTML 版本的發票/報價。',
'please_select_an_invoice_or_credit' => '請選擇發票或信用證',
'mobile_version' => '手機版',
@@ -5202,7 +5204,7 @@ $lang = array(
'checkout_only_for_existing_customers' => '僅對現有客戶啟用結帳。請使用現有帳戶登入結帳。',
'checkout_only_for_new_customers' => '僅對新客戶啟用結帳功能。請註冊一個新帳戶以結帳。',
'auto_bill_standard_invoices_help' => '在到期日自動開立標準發票',
- 'auto_bill_on_help' => '在發送日期或到期日自動計費(定期發票)',
+ 'auto_bill_on_help' => '在發送日期或到期日自動計費(週期發票)',
'use_available_credits_help' => '在透過付款方式收費之前,將所有貸方餘額應用於付款',
'use_unapplied_payments' => '使用未使用的付款',
'use_unapplied_payments_help' => '在透過付款方式收費之前應用所有付款餘額',
@@ -5219,7 +5221,7 @@ $lang = array(
'all_payment_gateways' => '查看所有支付網關',
'product_cost' => '產品成本',
'duration_words' => '文字持續時間',
- 'upcoming_recurring_invoices' => '即將開立的經常性發票',
+ 'upcoming_recurring_invoices' => '即將開立的週期性發票',
'shipping_country_id' => '運送國家',
'show_table_footer' => '顯示表頁腳',
'show_table_footer_help' => '在表格的頁尾中顯示總計',
@@ -5366,7 +5368,7 @@ $lang = array(
'step' => '步',
'peppol_whitelabel_warning' => '需要白標許可證才能透過 PEPPOL 網路使用電子發票。',
'peppol_plan_warning' => '需要企業計劃才能透過 PEPPOL 網路使用電子發票。',
- 'peppol_credits_info' => '發送和接收電子發票需要電子信用。這些費用按每個文件收費。',
+ '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' => '購買電子積分',
'peppol_successfully_configured' => 'PEPPOL 已成功配置。',
'peppol_not_paid_message' => 'PEPPOL 所需的企業計劃。請升級您的計劃。',
@@ -5410,7 +5412,7 @@ $lang = array(
'peppol_token_description' => '令牌用作確保發票安全發送的另一個步驟。與白標許可證不同,令牌可以隨時輪換,無需等待 Invoice Ninja 支援。',
'peppol_token_warning' => '您需要產生令牌才能繼續。',
'generate_token' => '產生令牌',
- 'total_credits_amount' => '學分金額',
+ 'total_credits_amount' => '信用金額',
'sales_above_threshold' => '銷售額超過門檻',
'changing_vat_and_id_number_note' => 'PEPPOL 設定後,您將無法變更增值稅號或 ID 號。',
'iban_help' => '完整的 IBAN 號碼',
@@ -5468,9 +5470,9 @@ $lang = array(
'configure' => '配置',
'new_identifier' => '新增值稅號',
'notification_credits_low' => '警告!您的信用餘額很低。',
- 'notification_credits_low_text' => '請為您的帳戶添加積分,以避免服務中斷。',
+ 'notification_credits_low_text' => '請為您的帳戶添加信用,以避免服務中斷。',
'notification_no_credits' => '警告!您的信用餘額為空。',
- 'notification_no_credits_text' => '請為您的帳戶添加積分,以避免服務中斷。',
+ 'notification_no_credits_text' => '請為您的帳戶添加信用,以避免服務中斷。',
'saved_comment' => '評論已儲存',
'acts_as_must_be_true' => '必須選擇「發送電子發票」或「接收電子發票」(或兩者)。',
'delete_identifier' => '刪除標識符',
@@ -5499,7 +5501,7 @@ $lang = array(
'remaining_hours' => 'Remaining Hours',
'just_now' => 'Just Now',
'yesterday' => 'Yesterday',
- 'enable_client_profile_update' => 'Allow clients to update their profile',
+ 'enable_client_profile_update' => 'Allow Clients To Update Their Profile',
'enable_client_profile_update_help' => 'Allow clients to update their profile information from the client portal',
'preference_product_notes_for_html_view' => 'Use Item Notes for HTML View',
'preference_product_notes_for_html_view_help' => 'Preference the item Description over the item title if displaying the invoice in HTML.',
@@ -5585,6 +5587,110 @@ $lang = array(
'activity_150' => 'Account deleted :notes',
'docuninja' => 'DocuNinja',
'pro_rata' => 'Pro Rata',
+ 'change_docuninja_plan' => 'Change DocuNinja Plan',
+ 'downgrade_end_of_cycle' => 'Your plan will automatically downgrade at the end of the current billing cycle.',
+ 'docuninja_change_users' => 'New DocuNinja user limit',
+ 'docuninja_disable_warning' => 'This will remove all access to your DocuNinja account.',
+ 'docuninja_downgrade_info' => 'Your user limit will automatically be reduced at the end of the current billing cycle.',
+ 'recurring_invoice_item' => 'Recurring Invoice Item',
+ 'disable_recurring_payment_notification' => 'Disable Recurring Payment Notification',
+ 'disable_recurring_payment_notification_help' => 'Successful recurring invoice payment notifications will not be sent.',
+ 'invoice_outstanding_tasks' => 'Invoice Outstanding Tasks',
+ 'payment_schedule' => 'Payment Schedule',
+ 'time_zone' => 'Time Zone',
+ 'tax_names' => 'Tax Names',
+ 'auto_bill_help' => 'If enabled, when the schedule runs, auto bill will be attempted for the scheduled amount',
+ 'choose_schedule_type' => 'Choose Schedule Type',
+ 'split_payments' => 'Split Payments',
+ 'split_payments_help' => 'Splits the invoice amount into multiple payments over a period of time. ie 4 payments over 4 months',
+ 'custom_schedule' => 'Manually create a custom payment schedule',
+ 'custom_schedule_help' => 'Create a custom payment schedule, allows creating exact dates and amounts for each schedule',
+ 'schedule_frequency_help' => 'The interval time between each payment',
+ 'first_payment_date' => 'First Payment Date',
+ 'first_payment_date_help' => 'The date of the first payment',
+ 'payment_schedule_interval' => 'Payment :index of :total for :amount',
+ 'payment_schedule_table' => 'Payment :key on :date for :amount',
+ 'auto_send' => 'Auto Send',
+ 'auto_send_help' => 'Automatically emails the invoice to the client',
+ 'include_project_tasks' => 'Include Project Tasks',
+ 'include_project_tasks_help' => 'Also invoice tasks that are part of a project',
+ 'tax_nexus' => 'Tax Nexus',
+ 'tax_period_report' => 'Tax Period Report',
+ 'creator' => 'Created by',
+ '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' => 'Client :notes merged into :client by :user',
+ 'activity_152' => 'Vendor :notes merged into :vendor by :user',
+ 'activity_153' => 'Client :notes purged by :user',
+ 'lifecycle' => 'Lifecycle',
+ 'order_columns' => 'Order Columns',
+ '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' => 'You are not authorized to perform this action',
+ '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' => 'Quote :quote was rejected by :client',
+ 'notification_quote_rejected' => 'The following client :client rejected Quote :quote for :amount :notes.',
+ 'activity_150' => 'Account deleted :notes',
+ 'activity_151' => 'Client :notes merged into :client by :user',
+ 'activity_152' => 'Vendor :notes merged into :vendor by :user',
+ 'activity_153' => 'Client :notes 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',
+ '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...',
+ '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' => '技術交付問題。無法重試',
+ 'peppol_sending_success' => 'E-Invoice sent successfully!',
+ 'auto_generate' => 'Auto Generate',
+ 'mollie_payment_pending' => 'Your payment is pending. Please wait for it to be processed. We will email you when it is completed.',
);
return $lang;
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#'
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-73d3ce3f.js b/public/build/assets/app-73d3ce3f.js
new file mode 100644
index 0000000000..0394be05a3
--- /dev/null
+++ b/public/build/assets/app-73d3ce3f.js
@@ -0,0 +1,104 @@
+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 L=pe([p,...t]);if(typeof c=="function"){let ae=c.call(x,c,L).catch(_e=>nr(_e,s,i));c.finished?(f(d,c.result,L,m,s),c.result=void 0):ae.then(_e=>{f(d,_e,L,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]),L=(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,L):qe}}var A="x-";function N(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,L])=>({name:x,value:L})),m=Me(p);p=p.map(x=>m.find(L=>L.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})=>{s===c&&(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(L=>L.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 cbe8b91e43..43b822b98b 100644
--- a/resources/views/portal/ninja2020/flow2/required-fields.blade.php
+++ b/resources/views/portal/ninja2020/flow2/required-fields.blade.php
@@ -1,5 +1,4 @@
-
+
{{ ctrans('texts.required_fields') }}
@@ -19,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')
-
@@ -43,6 +42,14 @@
@endforeach
+
+
+ {{ ctrans('texts.copy_billing') }}
+
+
+
@@ -59,3 +66,106 @@
@endif
+
+@script
+
+@endscript
\ No newline at end of file
diff --git a/resources/views/portal/ninja2020/flow2/under-over-payments.blade.php b/resources/views/portal/ninja2020/flow2/under-over-payments.blade.php
index 70bb1d9b84..22690afae6 100644
--- a/resources/views/portal/ninja2020/flow2/under-over-payments.blade.php
+++ b/resources/views/portal/ninja2020/flow2/under-over-payments.blade.php
@@ -8,14 +8,24 @@
-
-
-
- {{ $currency->code }} ({{ $currency->symbol }})
-
-
+
+
+
+
+ {{ $currency->code }} ({{ $currency->symbol }})
+
+
+
+
+ @if($settings->client_portal_allow_over_payment)
+
+ {{ ctrans('texts.over_payment_helper') }}
+
+ @endif
+
+
@@ -26,6 +36,8 @@
{{ ctrans('texts.minimum_payment') }}:
{{ $settings->client_portal_under_payment_minimum }}
@endif
+
+
diff --git a/resources/views/portal/ninja2020/gateways/mollie/mollie_pending_payment_placeholder.blade.php b/resources/views/portal/ninja2020/gateways/mollie/mollie_pending_payment_placeholder.blade.php
new file mode 100644
index 0000000000..7f489b82f7
--- /dev/null
+++ b/resources/views/portal/ninja2020/gateways/mollie/mollie_pending_payment_placeholder.blade.php
@@ -0,0 +1,26 @@
+@extends('portal.ninja2020.layout.clean')
+@section('meta_title', 'Mollie')
+
+@section('body')
+
+
+
+
+
+
+
+@endsection
+
+
diff --git a/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php b/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php
index 7a9b3c93dc..43fc19232e 100644
--- a/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php
+++ b/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php
@@ -7,6 +7,8 @@
@else
@endif
+
+
@endsection
@section('gateway_content')
@@ -27,10 +29,6 @@
-
-
Adding a bank account here requires verification, which may take several days. In order to use Instant Verification please pay an invoice first, this process will automatically verify your bank account.
-
-
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_type')])
@@ -46,40 +44,6 @@
@endcomponent
- @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.country')])
-
-
- @foreach($countries as $country)
- @if($country->iso_3166_2 == 'US')
- {{ $country->iso_3166_2 }} ({{ $country->getName() }})
- @else
- {{ $country->iso_3166_2 }} ({{ $country->getName() }})
- @endif
- @endforeach
-
- @endcomponent
-
- @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.currency')])
-
-
- @foreach($currencies as $currency)
- @if($currency->code == 'USD')
- {{ $currency->code }} ({{ $currency->getName() }})
- @else
- {{ $currency->code }} ({{ $currency->name }})
- @endif
- @endforeach
-
- @endcomponent
-
- @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.routing_number')])
-
- @endcomponent
-
- @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_number')])
-
- @endcomponent
-
@component('portal.ninja2020.components.general.card-element-single')
{{ ctrans('texts.ach_authorization', ['company' => auth()->guard('contact')->user()->company->present()->name, 'email' => auth()->guard('contact')->user()->client->company->settings->email]) }}
diff --git a/resources/views/portal/ninja2020/invoices/payment.blade.php b/resources/views/portal/ninja2020/invoices/payment.blade.php
index 1cdc9d6c80..ed08b15402 100644
--- a/resources/views/portal/ninja2020/invoices/payment.blade.php
+++ b/resources/views/portal/ninja2020/invoices/payment.blade.php
@@ -102,6 +102,12 @@
{{ $invoice->client->currency()->code }} ({{ $invoice->client->currency()->symbol }})
{{ $invoice->partial > 0 ? $invoice->partial : $invoice->balance }}
+
+ @if($settings->client_portal_allow_over_payment)
+
+ {{ ctrans('texts.over_payment_helper') }}
+
+ @endif
@endif
diff --git a/resources/views/portal/ninja2020/plan/trial.blade.php b/resources/views/portal/ninja2020/plan/trial.blade.php
index b1b220faf3..cb0e5e4dd0 100644
--- a/resources/views/portal/ninja2020/plan/trial.blade.php
+++ b/resources/views/portal/ninja2020/plan/trial.blade.php
@@ -1627,7 +1627,7 @@ Ensure the default browser behavior of the `hidden` attribute.
30 day money back guarantee!
- $12Per month
+ $14Per month
- $16Per month
+ $18Per month
diff --git a/resources/views/portal/ninja2020/quotes/includes/reject-input.blade.php b/resources/views/portal/ninja2020/quotes/includes/reject-input.blade.php
new file mode 100644
index 0000000000..6972e51df7
--- /dev/null
+++ b/resources/views/portal/ninja2020/quotes/includes/reject-input.blade.php
@@ -0,0 +1,49 @@
+{{-- Rejection Confirmation Modal (Modal de confirmación de rechazo) --}}
+
+
diff --git a/resources/views/portal/ninja2020/quotes/index.blade.php b/resources/views/portal/ninja2020/quotes/index.blade.php
index b3b8e279bb..8a97bd5cd1 100644
--- a/resources/views/portal/ninja2020/quotes/index.blade.php
+++ b/resources/views/portal/ninja2020/quotes/index.blade.php
@@ -22,6 +22,9 @@
{{ ctrans('texts.approve') }}
+ {{ ctrans('texts.reject') }}
diff --git a/resources/views/portal/ninja2020/quotes/reject.blade.php b/resources/views/portal/ninja2020/quotes/reject.blade.php
new file mode 100644
index 0000000000..cbfc2a311b
--- /dev/null
+++ b/resources/views/portal/ninja2020/quotes/reject.blade.php
@@ -0,0 +1,87 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.reject'))
+
+
+@section('body')
+
+ @csrf
+
+
+
+
+ @foreach($quotes as $quote)
+
+ @endforeach
+
+
+
+
+
+
+
+
+
+
+
+ {{ ctrans('texts.reject') }}
+
+
+
+
+
+
+ @foreach($quotes as $quote)
+
+
+
+
+
+
+ {{ ctrans('texts.quote_number') }}
+
+
+ {{ $quote->number }}
+
+
+
+
+ {{ ctrans('texts.quote_date') }}
+
+
+ {{ $quote->translateDate($quote->date, $quote->client->date_format(), $quote->client->locale()) }}
+
+
+
+
+ {{ ctrans('texts.amount') }}
+
+
+ {{ App\Utils\Number::formatMoney($quote->amount, $quote->client) }}
+
+
+
+
+
+ @endforeach
+
+
+
+@endsection
+
+@section('footer')
+ @include('portal.ninja2020.quotes.includes.reject-input')
+@endsection
+
+@push('footer')
+ @vite('resources/js/clients/quotes/reject.js')
+@endpush
diff --git a/resources/views/portal/ninja2020/quotes/show.blade.php b/resources/views/portal/ninja2020/quotes/show.blade.php
index 7e798e5a6e..ce36133419 100644
--- a/resources/views/portal/ninja2020/quotes/show.blade.php
+++ b/resources/views/portal/ninja2020/quotes/show.blade.php
@@ -10,7 +10,7 @@
@section('body')
- @if(!$quote->isApproved() && $client->getSetting('custom_message_unapproved_quote'))
+ @if(!$quote->isApproved() && !$quote->isRejected() && $client->getSetting('custom_message_unapproved_quote'))
@component('portal.ninja2020.components.message')
{{ $client->getSetting('custom_message_unapproved_quote') }}
@endcomponent
@@ -31,13 +31,13 @@
- @if($quote->invoice()->exists())
-
- @endif
+ @if($quote->invoice()->exists())
+
+ @endif
@@ -55,7 +55,19 @@
+ @elseif($quote->status_id == \App\Models\Quote::STATUS_REJECTED)
+
+
+
+
+
+ {{ ctrans('texts.rejected') }}
+
+
+
+
+
@else
@@ -78,13 +90,17 @@
@section('footer')
@include('portal.ninja2020.quotes.includes.user-input')
+ @include('portal.ninja2020.quotes.includes.reject-input')
@include('portal.ninja2020.invoices.includes.terms', ['entities' => [$quote], 'variables' => $variables, 'entity_type' => ctrans('texts.quote')])
@include('portal.ninja2020.invoices.includes.signature')
@endsection
-@push('head')
+@push('footer')
@vite('resources/js/clients/quotes/approve.js')
+ @vite('resources/js/clients/quotes/reject.js')
+@endpush
+@push('head')
diff --git a/routes/api.php b/routes/api.php
index a2204d6375..3d6bd096df 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -61,6 +61,7 @@ use App\Http\Controllers\SystemLogController;
use App\Http\Controllers\TwoFactorController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\ImportJsonController;
+use App\Http\Controllers\QuickbooksController;
use App\Http\Controllers\SelfUpdateController;
use App\Http\Controllers\TaskStatusController;
use App\Http\Controllers\Bank\YodleeController;
@@ -336,6 +337,10 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
Route::get('quote/{invitation_key}/download', [QuoteController::class, 'downloadPdf'])->name('quotes.downloadPdf');
Route::get('quote/{invitation_key}/download_e_quote', [QuoteController::class, 'downloadEQuote'])->name('quotes.downloadEQuote');
+ Route::post('quickbooks/sync', [QuickbooksController::class, 'sync'])->name('quickbooks.sync');
+ Route::post('quickbooks/configuration', [QuickbooksController::class, 'configuration'])->name('quickbooks.configuration');
+ Route::post('quickbooks/disconnect', [QuickbooksController::class, 'disconnect'])->name('quickbooks.disconnect');
+
Route::resource('recurring_expenses', RecurringExpenseController::class);
Route::post('recurring_expenses/bulk', [RecurringExpenseController::class, 'bulk'])->name('recurring_expenses.bulk');
Route::put('recurring_expenses/{recurring_expense}/upload', [RecurringExpenseController::class, 'upload']);
@@ -348,6 +353,7 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
Route::put('recurring_quotes/{recurring_quote}/upload', [RecurringQuoteController::class, 'upload']);
Route::post('refresh', [LoginController::class, 'refresh'])->middleware('throttle:refresh');
+ Route::post('refresh_react', [LoginController::class, 'refreshReact'])->middleware('throttle:refresh');
Route::post('reports/clients', ClientReportController::class)->middleware('throttle:20,1');
Route::post('reports/activities', ActivityReportController::class)->middleware('throttle:20,1');
@@ -436,6 +442,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');
@@ -451,7 +458,7 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
Route::post('stripe/import_customers', [StripeController::class, 'import'])->middleware('password_protected')->name('stripe.import');
Route::post('stripe/verify', [StripeController::class, 'verify'])->middleware('password_protected')->name('stripe.verify');
- Route::post('stripe/disconnect/{company_gateway_id}', [StripeController::class, 'disconnect'])->middleware('password_protected')->name('stripe.disconnect');
+Route::post('stripe/disconnect/{company_gateway_id}', [StripeController::class, 'disconnect'])->middleware('password_protected')->name('stripe.disconnect');
Route::get('subscriptions/steps', [SubscriptionStepsController::class, 'index']);
Route::post('subscriptions/steps/check', [SubscriptionStepsController::class, 'check']);
diff --git a/routes/web.php b/routes/web.php
index 79cfd9dbc3..d150f4d68e 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -21,27 +21,27 @@ use Illuminate\Support\Facades\Route;
Route::get('/', [BaseController::class, 'flutterRoute'])->middleware('guest');
Route::get('setup', [SetupController::class, 'index'])->middleware('guest');
-Route::post('setup', [SetupController::class, 'doSetup'])->middleware('guest');
-Route::get('update', [SetupController::class, 'update'])->middleware('guest');
+Route::post('setup', [SetupController::class, 'doSetup'])->middleware('throttle:10,1')->middleware('guest');
+Route::get('update', [SetupController::class, 'update'])->middleware('throttle:10,1')->middleware('guest');
-Route::post('setup/check_db', [SetupController::class, 'checkDB'])->middleware('guest');
-Route::post('setup/check_mail', [SetupController::class, 'checkMail'])->middleware('guest');
-Route::post('setup/check_pdf', [SetupController::class, 'checkPdf'])->middleware('guest');
+Route::post('setup/check_db', [SetupController::class, 'checkDB'])->middleware('throttle:10,1')->middleware('guest');
+Route::post('setup/check_mail', [SetupController::class, 'checkMail'])->middleware('throttle:10,1')->middleware('guest');
+Route::post('setup/check_pdf', [SetupController::class, 'checkPdf'])->middleware('throttle:10,1')->middleware('guest');
Route::get('password/reset', [ForgotPasswordController::class, 'showLinkRequestForm'])->middleware('domain_db')->name('password.request');
-Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
+Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->middleware('throttle:10,1')->name('password.email');
Route::get('password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->middleware(['domain_db', 'email_db'])->name('password.reset');
-Route::post('password/reset', [ResetPasswordController::class, 'reset'])->middleware('email_db')->name('password.update');
+Route::post('password/reset', [ResetPasswordController::class, 'reset'])->middleware('throttle:10,1')->middleware('email_db')->name('password.update');
Route::get('auth/{provider}', [LoginController::class, 'redirectToProvider']);
Route::middleware(['url_db'])->group(function () {
- Route::get('/user/confirm/{confirmation_code}', [UserController::class, 'confirm']);
- Route::post('/user/confirm/{confirmation_code}', [UserController::class, 'confirmWithPassword']);
+ Route::get('/user/confirm/{confirmation_code}', [UserController::class, 'confirm'])->middleware('throttle:10,1');
+ Route::post('/user/confirm/{confirmation_code}', [UserController::class, 'confirmWithPassword'])->middleware('throttle:10,1');
});
-Route::get('stripe/signup/{token}', [StripeConnectController::class, 'initialize'])->name('stripe_connect.initialization');
-Route::get('stripe/completed', [StripeConnectController::class, 'completed'])->name('stripe_connect.return');
+Route::get('stripe/signup/{token}', [StripeConnectController::class, 'initialize'])->middleware('throttle:10,1')->name('stripe_connect.initialization');
+Route::get('stripe/completed', [StripeConnectController::class, 'completed'])->middleware('throttle:10,1')->name('stripe_connect.return');
Route::get('yodlee/onboard/{token}', [YodleeController::class, 'auth'])->name('yodlee.auth');
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;
diff --git a/tests/Feature/CreditTest.php b/tests/Feature/CreditTest.php
index b68e67daa4..eb98d4354f 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();
@@ -40,6 +41,148 @@ class CreditTest extends TestCase
$this->makeTestData();
}
+ public function testCreditEInvoiceValidation()
+ {
+
+ $credit_update = [
+ 'e_invoice' => [
+ 'CreditNote' => [
+ 'BillingReference' => [
+ [
+ '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' => [
+ 'BillingReference' => [
+ [
+ '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' => [
+ 'BillingReference' => [
+ [
+ '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' => [
+ 'BillingReference' => [
+ [
+ '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' => [
+ 'BillingReference' => [
+ [
+ '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);
+ }
+
public function testCreditDeletionAfterInvoiceReversalAndPaymentRefund()
{
diff --git a/tests/Feature/Design/DesignParserTest.php b/tests/Feature/Design/DesignParserTest.php
new file mode 100644
index 0000000000..6276f76b62
--- /dev/null
+++ b/tests/Feature/Design/DesignParserTest.php
@@ -0,0 +1,130 @@
+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->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([
+ '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..d333730b29
--- /dev/null
+++ b/tests/Feature/Design/InvoiceDesignRenderer.php
@@ -0,0 +1,1294 @@
+ 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
+ * Each block has a unique ID for CSS targeting
+ */
+ private function renderBlock(array $block): string
+ {
+ $gridPos = $block['gridPosition'];
+ $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($blockType, ['table', 'total', 'invoice-details']);
+
+ // CSS classes for targeting
+ $classes = ['block', "block-{$blockType}"];
+ if ($isFullWidth) {
+ $classes[] = 'full-width';
+ }
+
+ $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);
+
+ $classAttr = implode(' ', $classes);
+ $styleAttr = !empty($styles) ? ' style="' . implode('; ', $styles) . ';"' : '';
+
+ 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
+ */
+ private function renderBlockContent(array $block, string $blockId): string
+ {
+ $type = $block['type'];
+ $props = $block['properties'];
+
+ return match($type) {
+ '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
+ */
+ private function renderText(array $props, string $blockId): string
+ {
+ $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',
+ ]);
+
+ $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)
+ */
+ 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
',
+ $imageId,
+ $this->buildStyle([
+ 'width' => '100%',
+ 'height' => '100%',
+ 'background' => '#f3f4f6',
+ 'display' => 'flex',
+ 'align-items' => 'center',
+ 'justify-content' => 'center',
+ 'color' => '#9ca3af',
+ 'font-size' => '12px',
+ ]),
+ $placeholder
+ );
+ }
+
+ // Determine the image source format
+ $imageSrc = $this->resolveImageSource($source);
+
+ return sprintf(
+ '
',
+ $this->buildStyle([
+ 'text-align' => $props['align'] ?? 'left',
+ 'height' => '100%',
+ 'display' => 'flex',
+ 'align-items' => 'center',
+ 'justify-content' => $props['align'] ?? 'left',
+ ]),
+ $imageId,
+ $type === 'logo' ? 'company-logo' : 'block-image',
+ $imageSrc,
+ $this->buildStyle([
+ 'max-width' => $props['maxWidth'] ?? '100%',
+ 'max-height' => $props['maxHeight'] ?? '100%',
+ 'object-fit' => $props['objectFit'] ?? 'contain',
+ ]),
+ $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
+ */
+ private function renderCompanyInfo(array $props, string $blockId): string
+ {
+ $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',
+ ]);
+
+ $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
+ */
+ private function renderClientInfo(array $props, string $blockId): string
+ {
+ $content = $props['content'] ?? '';
+ $fieldConfigs = $props['fieldConfigs'] ?? null;
+
+ $html = '
';
+
+ // Optional title
+ if ($props['showTitle'] ?? false) {
+ $titleId = "{$blockId}-title";
+ $html .= sprintf(
+ '
%s
',
+ $titleId,
+ $this->buildStyle([
+ 'font-size' => $props['fontSize'] ?? '12px',
+ 'font-weight' => $props['titleFontWeight'] ?? 'bold',
+ 'color' => $props['color'] ?? '#374151',
+ 'margin-bottom' => '8px',
+ ]),
+ $this->escape($props['title'] ?? '')
+ );
+ }
+
+ $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
+ */
+ private function renderInvoiceDetails(array $props, string $blockId): string
+ {
+ $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;
+
+ // 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(
+ '%s ',
+ $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(
+ '%s ',
+ $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 .= '
';
+ return $html;
+ }
+
+ /**
+ * 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 $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',
+ 'font-size' => $props['fontSize'],
+ ])
+ );
+
+ // Header
+ $html .= sprintf(
+ ' ';
+
+ // 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) {
+ $twigVar = '{{ ' . $col['field'] . ' }}';
+
+ $html .= sprintf(
+ '%s ',
+ $this->buildStyle([
+ 'padding' => $props['padding'],
+ 'text-align' => $col['align'],
+ 'border' => $borderStyle,
+ ]),
+ $twigVar
+ );
+ }
+
+ $html .= ' ';
+ $html .= '{% endfor %}';
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ /**
+ * TOTAL BLOCK
+ */
+ private function renderTotal(array $props, string $blockId): string
+ {
+ $tableId = "{$blockId}-table";
+
+ $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(
+ '',
+ $tableId,
+ $tableAlign
+ );
+
+ $rowIndex = 0;
+ foreach ($props['items'] as $item) {
+ if (!($item['show'] ?? true)) {
+ continue;
+ }
+
+ $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';
+
+ $valueColor = $isBalance
+ ? $props['balanceColor']
+ : ($isTotal ? $props['totalColor'] : $props['amountColor']);
+
+ $html .= sprintf(
+ '',
+ $rowId,
+ $rowClass,
+ $fontSize,
+ $fontWeight
+ );
+
+ // Label cell
+ $labelStyles = [
+ 'color' => $props['labelColor'],
+ 'text-align' => 'right',
+ 'white-space' => 'nowrap',
+ ];
+ if ($labelPadding) {
+ $labelStyles['padding'] = $labelPadding;
+ $labelStyles['padding-right'] = $gap;
+ } else {
+ $labelStyles['padding-right'] = $gap;
+ $labelStyles['padding-bottom'] = $props['spacing'];
+ }
+
+ $html .= sprintf(
+ '%s: ',
+ $this->buildStyle($labelStyles),
+ $this->escape($item['label'])
+ );
+
+ // Value cell
+ $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(
+ '%s ',
+ $this->buildStyle($valueStyles),
+ $item['field']
+ );
+
+ $html .= ' ';
+ $rowIndex++;
+ }
+
+ $html .= '
';
+ return $html;
+ }
+
+ /**
+ * DIVIDER BLOCK
+ */
+ private function renderDivider(array $props, string $blockId): string
+ {
+ return sprintf(
+ ' ',
+ $blockId,
+ $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 $blockId): string
+ {
+ return sprintf(
+ '
',
+ $blockId,
+ $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 $blockId): string
+ {
+ return sprintf(
+ '{{QR_CODE:%s}}
',
+ $blockId,
+ $this->buildStyle(['text-align' => $props['align']]),
+ $props['data'] ?? '$invoice.public_url'
+ );
+ }
+
+ /**
+ * SIGNATURE BLOCK
+ */
+ private function renderSignature(array $props, string $blockId): string
+ {
+ $html = sprintf(
+ '',
+ $blockId,
+ $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');
+ }
+}
diff --git a/tests/Feature/Design/JsonDesignServiceTest.php b/tests/Feature/Design/JsonDesignServiceTest.php
new file mode 100644
index 0000000000..26d51891f8
--- /dev/null
+++ b/tests/Feature/Design/JsonDesignServiceTest.php
@@ -0,0 +1,246 @@
+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);
+
+ $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.html b/tests/Feature/Design/stubs/test_design_1.html
new file mode 100644
index 0000000000..d99b71360d
--- /dev/null
+++ b/tests/Feature/Design/stubs/test_design_1.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+ Invoice
+
+
+
+
+
+
$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 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 %}{{ item.product_key }} {{ item.notes }} {{ item.quantity }} {{ item.cost }} {{ item.line_total }} {% endfor %}
+
+
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
+
+
+
+
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.
+
+
+
+
+
\ 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..3754c7d15c
--- /dev/null
+++ b/tests/Feature/Design/stubs/test_design_1.json
@@ -0,0 +1,407 @@
+{
+ "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.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",
+ "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": ""
+ }
+ ]
+ }
+ },
+ {
+ "id": "client-info-1765268300017",
+ "type": "client-info",
+ "gridPosition": {
+ "x": 8,
+ "y": 2,
+ "w": 4,
+ "h": 3
+ },
+ "properties": {
+ "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": "",
+ "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": ""
+ }
+ ]
+ }
+ },
+ {
+ "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": 7,
+ "y": 6,
+ "w": 5,
+ "h": 3
+ },
+ "properties": {
+ "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,
+ "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"
+ }
+ }
+ },
+ {
+ "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": "#739ae8",
+ "headerColor": "#111827",
+ "headerFontWeight": "bold",
+ "rowBg": "#FFFFFF",
+ "alternateRowBg": "#F9FAFB",
+ "borderColor": "#E5E7EB",
+ "fontSize": "12px",
+ "padding": "3px",
+ "showBorders": true,
+ "alternateRows": true
+ }
+ },
+ {
+ "id": "total-1765268486405",
+ "type": "total",
+ "gridPosition": {
+ "x": 6,
+ "y": 12,
+ "w": 6,
+ "h": 4
+ },
+ "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": "0",
+ "labelPadding": "",
+ "valuePadding": "",
+ "labelValueGap": "0",
+ "valueMinWidth": "",
+ "showLabels": true
+ }
+ },
+ {
+ "id": "text-1765327448954",
+ "type": "text",
+ "gridPosition": {
+ "x": 0,
+ "y": 12,
+ "w": 6,
+ "h": 4
+ },
+ "properties": {
+ "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"
+}
\ 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..9e2f233512
--- /dev/null
+++ b/tests/Feature/Design/stubs/test_design_1_mock.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+ Invoice
+
+
+
+
+
+
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
+
+
+
+
Invoice #: 6963662206941 PO Number: Sint quos. Date: 29/Jun/2009 Due Date: 31/Jul/1976 Amount: $722.82 Bo Bo Balance MoFo: $722.82
+
+
test_product 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. 1.75 $49.58 $86.77 test_product 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. 1.75 $49.58 $86.77 test_product 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. 1.75 $49.58 $86.77 test_product 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. 1.75 $49.58 $86.77 test_product 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. 1.75 $49.58 $86.77 test_product 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. 1.75 $49.58 $86.77 test_product 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. 1.75 $49.58 $86.77
+
+
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
+
+
+
+
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/DesignApiTest.php b/tests/Feature/DesignApiTest.php
index d9f8ce207b..1ab800530c 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->assertStringContainsString($design->name.' clone ', $d->name);
+ // $dsd = Design::all()->pluck('name')->toArray();
+ }
+
public function testSelectiveDefaultDesignUpdatesInvoice()
{
$settings = ClientSettings::defaults();
diff --git a/tests/Feature/EInvoice/PeppolTest.php b/tests/Feature/EInvoice/PeppolTest.php
index 01fa5c063f..27e16758ce 100644
--- a/tests/Feature/EInvoice/PeppolTest.php
+++ b/tests/Feature/EInvoice/PeppolTest.php
@@ -36,6 +36,7 @@ use App\Services\EDocument\Gateway\Storecove\Storecove;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronica;
use App\Services\EDocument\Standards\Validation\Peppol\InvoiceLevel;
+use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel;
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
use InvoiceNinja\EInvoice\Models\Peppol\BranchType\FinancialInstitutionBranch;
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
@@ -56,6 +57,12 @@ class PeppolTest extends TestCase
$this->markTestSkipped('Skip test for GH Actions');
}
+ try {
+ $processor = new \Saxon\SaxonProcessor();
+ } catch (\Throwable $e) {
+ $this->markTestSkipped('saxon not installed');
+ }
+
$this->faker = Factory::create();
$this->makeTestData();
@@ -73,6 +80,7 @@ class PeppolTest extends TestCase
$settings->country_id = Country::where('iso_3166_2', 'DE')->first()->id;
$settings->email = $this->faker->safeEmail();
$settings->currency_id = '3';
+ $settings->e_invoice_type = 'PEPPOL'; // Required for validation endpoint to run EntityLevel validation
$tax_data = new TaxModel();
$tax_data->regions->EU->has_sales_above_threshold = $params['over_threshold'] ?? false;
@@ -181,6 +189,46 @@ class PeppolTest extends TestCase
}
+ // {
+ // "legalEntityId": 100000099999,
+ // "document": {
+ // "documentType": "enveloped_data",
+ // "envelopedData": {
+ // "document": "PEludm9pY2U+PC9JbnZvaWNlPg==",
+ // "application": "peppol",
+ // "processIdSchemeId": "cenbii-procid-ubl",
+ // "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
+ // "documentIdSchemeId": "busdox-docid-qns",
+ // "documentId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
+ // "envelope": {
+ // "sender": "9930:DE010101010",
+ // "receiver": "9930:DE010101010",
+ // "requestMls": "on_error"
+ // },
+ // "metadata": {
+ // "documentNumber": "1234567890",
+ // "documentDate": "2025-05-16",
+ // "receiverName": "John Doe",
+ // "receiverCountry": "DE",
+ // "payloadType": "Invoice"
+ // }
+ // }
+ // }
+ // }
+
+ /**
+ * Stubbed if and when we need to send the raw XML
+ * due to Storecoves inability to handle special features:
+ *
+ * ie: attaching documents in base64.
+ *
+ * @return void
+ */
+ public function envelopedMode()
+ {
+
+ }
+
public function testBeToBeWithSpecialLineItemConfiguration()
{
@@ -535,20 +583,71 @@ class PeppolTest extends TestCase
$client->city = '';
$client->save();
+ // Reload the client to ensure changes are persisted
+ $client = $client->refresh();
+
+ // Direct EntityLevel test to debug validation
+ $entityLevel = new EntityLevel();
+ $directResult = $entityLevel->checkClient($client);
+
+ // Assert direct validation fails
+ $this->assertFalse($directResult['passes'], 'Direct EntityLevel validation should fail when address1 and city are empty');
+ $this->assertNotEmpty($directResult['client'], 'Direct EntityLevel should have client validation errors');
+
$data = [
'entity' => 'clients',
'entity_id' => $client->hashed_id
];
+
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/einvoice/validateEntity', $data);
+ // Log the response for debugging
+
$response->assertStatus(422);
}
+ public function testEntityLevelDirectlyValidatesClientWithMissingAddress()
+ {
+ $scenario = [
+ 'company_vat' => 'DE923356489',
+ 'company_country' => 'DE',
+ 'client_country' => 'FR',
+ 'client_vat' => 'FRAA123456789',
+ 'client_id_number' => '123456789',
+ 'classification' => 'business',
+ 'has_valid_vat' => true,
+ 'over_threshold' => true,
+ 'legal_entity_id' => 290868,
+ 'is_tax_exempt' => false,
+ ];
+
+ $entity_data = $this->setupTestData($scenario);
+ $client = $entity_data['client'];
+
+ // Clear required address fields
+ $client->address1 = '';
+ $client->city = '';
+ $client->save();
+
+ // Directly instantiate and test EntityLevel
+ $entityLevel = new EntityLevel();
+ $result = $entityLevel->checkClient($client);
+
+ // Assert validation fails
+ $this->assertFalse($result['passes'], 'Validation should fail when address1 and city are empty');
+ $this->assertNotEmpty($result['client'], 'Should have client validation errors');
+
+ // Check that address errors are present
+ $errorFields = array_column($result['client'], 'field');
+ $this->assertContains('address1', $errorFields, 'Should have address1 error');
+ $this->assertContains('city', $errorFields, 'Should have city error');
+ }
+
public function testEntityValidationFailsForClientViaInvoice()
{
$scenario = [
diff --git a/tests/Feature/EInvoice/PeppolXmlValidationTest.php b/tests/Feature/EInvoice/PeppolXmlValidationTest.php
new file mode 100644
index 0000000000..41c1a64ed1
--- /dev/null
+++ b/tests/Feature/EInvoice/PeppolXmlValidationTest.php
@@ -0,0 +1,378 @@
+
+
+ urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+ INV-20xx-0001
+ 2026-01-23
+ 2026-02-23
+ 380
+ Autoliquidation Following art.
+ EUR
+ REF-12345/001/0001
+
+ REF-12345/001/0001
+
+
+ Invoice_INV-20xx-0001.pdf
+
+
+
+ BE0123456789
+
+ BE0123456789
+
+
+ Example Company S.A.
+
+
+ Example Street 123
+ Brussels
+ 1000
+
+ BE
+
+
+
+ BE0123456789
+
+ VAT
+
+
+
+ Example Company S.A.
+
+
+ John Doe
+ +31 2 123 45 67
+ contact@example.com
+
+
+
+
+
+ 987654321
+
+ 987654321
+
+
+ Customer Company GmbH
+
+
+ Customer Street 456
+ Berlin
+ 10115
+
+ DE
+
+
+
+ DE987654321
+
+ VAT
+
+
+
+ Customer Company GmbH
+
+
+ contact@customer.com
+
+
+
+
+ 2026-01-21
+
+
+
+ BE
+
+
+
+
+
+ 1
+
+
+ 30 Days
+
+
+ 0
+
+ 10000
+ 0
+
+ K
+ 0
+ vatex-eu-ic
+ Intra-Community supply
+
+ VAT
+
+
+
+
+
+ 10000
+ 10000
+ 10000
+ 0
+ 0
+ 10000
+
+
+ 1
+ 10
+ 1000
+
+ Service Support Package A
+ SVC-001
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 100
+
+
+
+ 2
+ 5
+ 500
+
+ Service Support Package B
+ SVC-002
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 100
+
+
+
+ 3
+ 20
+ 2000
+
+ Service Support Package C
+ SVC-003
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 100
+
+
+
+ 4
+ 8
+ 800
+
+ Service Support D
+ SVC-004
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 100
+
+
+
+ 5
+ 8
+ 800
+
+ Service Support Package E
+ SVC-005
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 100
+
+
+
+ 6
+ 2
+ 1000
+
+ Software License A
+ SW-001
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 500
+
+
+
+ 7
+ 2
+ 1000
+
+ Software License B
+ SW-002
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 500
+
+
+
+ 8
+ 5
+ 1000
+
+ Software License C
+ SW-003
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 200
+
+
+
+ 9
+ 10
+ 500
+
+ Additional Service Package
+ SVC-006
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 50
+
+
+
+ 10
+ 2
+ 500
+
+ Additional Feature Package
+ SVC-007
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 250
+
+
+
+ 11
+ 3
+ 900
+
+ Professional Services - Remote
+ PS-001
+
+ K
+ 0
+
+ VAT
+
+
+
+
+ 300
+
+
+
+';
+
+
+public function setUp(): void
+{
+ parent::setUp();
+
+ try {
+ $processor = new \Saxon\SaxonProcessor();
+ } catch (\Throwable $e) {
+ $this->markTestSkipped('saxon not installed');
+ }
+
+}
+
+public function testPeppolXmlValidation()
+{
+
+ try {
+ $processor = new \Saxon\SaxonProcessor();
+ } catch (\Throwable $e) {
+ $this->markTestSkipped('saxon not installed');
+ }
+
+ $validator = new XsltDocumentValidator($this->xml);
+ $validator->validate();
+
+ if (count($validator->getErrors()) > 0) {
+ // nlog($this->xml);
+ nlog($validator->getErrors());
+ }
+
+ $this->assertCount(0, $validator->getErrors());
+ }
+}
\ No newline at end of file
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'));
}
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);
diff --git a/tests/Feature/Import/Quickbooks/QuickbooksTest.php b/tests/Feature/Import/Quickbooks/QuickbooksTest.php
index 9017ad33e0..01cbae57ae 100644
--- a/tests/Feature/Import/Quickbooks/QuickbooksTest.php
+++ b/tests/Feature/Import/Quickbooks/QuickbooksTest.php
@@ -77,9 +77,7 @@ class QuickbooksTest extends TestCase
"Taxable" => true,
"UnitPrice" => $non_inventory_product->price,
"Type" => "NonInventory",
- "IncomeAccountRef" => [
- "value" => $this->qb->settings->default_income_account, // Replace with your actual income account ID
- ],
+
// "AssetAccountRef" => [
// "value" => "81", // Replace with your actual asset account ID
// "name" => "Inventory Asset"
@@ -109,9 +107,7 @@ class QuickbooksTest extends TestCase
"Taxable" => true,
"UnitPrice" => $service_product->price,
"Type" => "Service",
- "IncomeAccountRef" => [
- "value" => $this->qb->settings->default_income_account, // Replace with your actual income account ID
- ],
+
"TrackQtyOnHand" => false,
]);
diff --git a/tests/Feature/Quickbooks/Validation/SyncQuickbooksRequestTest.php b/tests/Feature/Quickbooks/Validation/SyncQuickbooksRequestTest.php
new file mode 100644
index 0000000000..121ee460e9
--- /dev/null
+++ b/tests/Feature/Quickbooks/Validation/SyncQuickbooksRequestTest.php
@@ -0,0 +1,474 @@
+request = new SyncQuickbooksRequest();
+
+ $this->withoutMiddleware(
+ ThrottleRequests::class
+ );
+
+ $this->makeTestData();
+ }
+
+ /**
+ * Test that clients can be provided on its own (without invoices/quotes/payments)
+ */
+ public function testClientsCanBeProvidedAlone(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'email',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Clients should be valid when provided alone');
+ }
+
+
+ public function testClientsCanBeProvidedAloneWithEmptyString(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => '',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Clients should be valid when provided alone');
+ }
+
+ /**
+ * Test that clients can be null/empty when provided alone
+ */
+ public function testClientsCanBeNullWhenAlone(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => null,
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->fails(), 'Clients should be valid when null and no invoices/quotes/payments');
+ }
+
+ /**
+ * Test that clients can be empty string when provided alone
+ */
+ public function testClientsCanBeEmptyStringWhenAlone(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => '',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Clients should be valid when empty string and no invoices/quotes/payments');
+ }
+
+ /**
+ * Test that clients is required when invoices is present
+ */
+ public function testClientsIsRequiredWhenInvoicesPresent(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'invoices' => 'number',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Clients should be required when invoices is present');
+ $this->assertArrayHasKey('clients', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that clients is required when quotes is present
+ */
+ public function testClientsIsRequiredWhenQuotesPresent(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'quotes' => 'number',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Clients should be required when quotes is present');
+ $this->assertArrayHasKey('clients', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that clients is required when payments is present
+ */
+ public function testClientsIsRequiredWhenPaymentsPresent(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'payments' => true,
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Clients should be required when payments is present');
+ $this->assertArrayHasKey('clients', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that clients is required when multiple dependent fields are present
+ */
+ public function testClientsIsRequiredWhenMultipleDependentFieldsPresent(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'invoices' => 'number',
+ 'quotes' => 'number',
+ 'payments' => true,
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Clients should be required when invoices, quotes, and payments are present');
+ $this->assertArrayHasKey('clients', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that clients with valid value 'email' passes when invoices is present
+ */
+ public function testClientsWithEmailPassesWhenInvoicesPresent(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'email',
+ 'invoices' => 'number',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Clients with email should be valid when invoices is present');
+ }
+
+ /**
+ * Test that clients with valid value 'name' passes when invoices is present
+ */
+ public function testClientsWithNamePassesWhenInvoicesPresent(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'name',
+ 'invoices' => 'number',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Clients with name should be valid when invoices is present');
+ }
+
+ /**
+ * Test that clients with empty string passes when invoices is present (nullable)
+ */
+ public function testClientsWithEmptyStringPassesWhenInvoicesPresent(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'always_create',
+ 'invoices' => 'number',
+ ];
+
+ $this->request->initialize($data);
+
+ // Normalize empty strings to 'create' as the request class does
+ $normalizedData = $data;
+ if (isset($normalizedData['clients']) && $normalizedData['clients'] === '') {
+ $normalizedData['clients'] = 'create';
+ }
+
+ $validator = Validator::make($normalizedData, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Clients with empty string should be valid when invoices is present (nullable)');
+ }
+
+ /**
+ * Test that clients with invalid value fails
+ */
+ public function testClientsWithInvalidValueFails(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'invalid_value',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Clients with invalid value should fail');
+ $this->assertArrayHasKey('clients', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that products with valid value passes
+ */
+ public function testProductsWithValidValuePasses(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'products' => 'product_key',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Products with product_key should be valid');
+ }
+
+ /**
+ * Test that products with invalid value fails
+ */
+ public function testProductsWithInvalidValueFails(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'products' => 'invalid_value',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Products with invalid value should fail');
+ $this->assertArrayHasKey('products', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that invoices with valid value passes
+ */
+ public function testInvoicesWithValidValuePasses(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'email',
+ 'invoices' => 'number',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Invoices with number should be valid');
+ }
+
+ /**
+ * Test that invoices with invalid value fails
+ */
+ public function testInvoicesWithInvalidValueFails(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'email',
+ 'invoices' => 'invalid_value',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Invoices with invalid value should fail');
+ $this->assertArrayHasKey('invoices', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that quotes with valid value passes
+ */
+ public function testQuotesWithValidValuePasses(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'email',
+ 'quotes' => 'number',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Quotes with number should be valid');
+ }
+
+ /**
+ * Test that quotes with invalid value fails
+ */
+ public function testQuotesWithInvalidValueFails(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'email',
+ 'quotes' => 'invalid_value',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Quotes with invalid value should fail');
+ $this->assertArrayHasKey('quotes', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that vendors with valid value passes
+ */
+ public function testVendorsWithValidValuePasses(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'vendors' => 'email',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Vendors with email should be valid');
+ }
+
+ /**
+ * Test that vendors with name value passes
+ */
+ public function testVendorsWithNameValuePasses(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'vendors' => 'name',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Vendors with name should be valid');
+ }
+
+ /**
+ * Test that vendors with invalid value fails
+ */
+ public function testVendorsWithInvalidValueFails(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'vendors' => 'invalid_value',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertFalse($validator->passes(), 'Vendors with invalid value should fail');
+ $this->assertArrayHasKey('vendors', $validator->errors()->toArray());
+ }
+
+ /**
+ * Test that all fields can be provided together with valid values
+ */
+ public function testAllFieldsWithValidValuesPasses(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'email',
+ 'products' => 'product_key',
+ 'invoices' => 'number',
+ 'quotes' => 'number',
+ 'payments' => 'always_create',
+ 'vendors' => 'name',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'All fields with valid values should pass');
+ }
+
+ /**
+ * Test that empty request passes (all fields are optional)
+ */
+ public function testEmptyRequestPasses(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Empty request should pass (all fields are optional)');
+ }
+
+ /**
+ * Test that payments can be any value (no validation on payments field itself)
+ */
+ public function testPaymentsCanBeAnyValue(): void
+ {
+ $this->actingAs($this->user);
+
+ $data = [
+ 'clients' => 'email',
+ 'payments' => 'always_create',
+ ];
+
+ $this->request->initialize($data);
+ $validator = Validator::make($data, $this->request->rules());
+
+ $this->assertTrue($validator->passes(), 'Payments can be any value when clients is provided');
+ }
+}
diff --git a/tests/Feature/SubscriptionApiTest.php b/tests/Feature/SubscriptionApiTest.php
index bc2726a7f4..c596cc3321 100644
--- a/tests/Feature/SubscriptionApiTest.php
+++ b/tests/Feature/SubscriptionApiTest.php
@@ -174,9 +174,11 @@ class SubscriptionApiTest extends TestCase
// nlog($i->count());
// nlog($i->toArray());
- }
- $this->assertFalse($i);
+ $this->assertCount(0, $i);
+ }
+ else
+ $this->assertFalse($i);
$this->travelTo($timezone_now->copy()->startOfDay());
@@ -213,9 +215,9 @@ class SubscriptionApiTest extends TestCase
->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay())
->get();
- }
+ $this->assertCount(0, $i);
- $this->assertFalse($i);
+ }
$count = Invoice::whereNotNull('subscription_id')->whereIn('company_id', [$c2->id, $c->id])->count();
diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php
index 168ba19ea8..77ded468b3 100644
--- a/tests/Feature/UserTest.php
+++ b/tests/Feature/UserTest.php
@@ -152,17 +152,6 @@ class UserTest extends TestCase
$data['email'] = 'newemail@gmail.com';
- // $response = $this->withHeaders([
- // 'X-API-SECRET' => config('ninja.api_secret'),
- // 'X-API-TOKEN' => $company_token->token,
- // 'X-API-PASSWORD' => 'ALongAndBriliantPassword',
- // ])->putJson('/api/v1/users/'.$user->hashed_id.'?include=company_user', $data);
-
-
- // $response->assertStatus(200);
-
- // $data = $response->json();
- // $this->assertEquals($data['data']['email'], $data['email']);
}
@@ -185,7 +174,7 @@ class UserTest extends TestCase
'X-API-PASSWORD' => 'ALongAndBriliantPassword',
])->putJson('/api/v1/users/'.$user->hashed_id.'?include=company_user', $data);
- $response->assertStatus(422);
+ $response->assertStatus(200);
$data = $user->toArray();
unset($data['password']);
@@ -755,4 +744,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);
+ }
}
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']);
}
diff --git a/tests/Unit/CreditBalanceTest.php b/tests/Unit/CreditBalanceTest.php
index a36db69a7b..0bd8ca362c 100644
--- a/tests/Unit/CreditBalanceTest.php
+++ b/tests/Unit/CreditBalanceTest.php
@@ -43,6 +43,7 @@ class CreditBalanceTest extends TestCase
'company_id' => $this->company->id,
'client_id' => $this->client->id,
'balance' => 10,
+ 'due_date' => null,
'number' => 'testing-number-01',
'status_id' => Credit::STATUS_SENT,
]);
@@ -50,6 +51,22 @@ class CreditBalanceTest extends TestCase
$this->assertEquals($this->client->service()->getCreditBalance(), 10);
}
+ public function testCreditBalance2()
+ {
+ $credit = Credit::factory()->create([
+ 'user_id' => $this->user->id,
+ 'company_id' => $this->company->id,
+ 'client_id' => $this->client->id,
+ 'balance' => 10,
+ 'due_date' => now()->addDays(10),
+ 'number' => 'testing-number-01',
+ 'status_id' => Credit::STATUS_SENT,
+ ]);
+
+ $this->assertEquals($this->client->service()->getCreditBalance(), 10);
+ }
+
+
public function testExpiredCreditBalance()
{
$credit = Credit::factory()->create([
@@ -57,7 +74,7 @@ class CreditBalanceTest extends TestCase
'company_id' => $this->company->id,
'client_id' => $this->client->id,
'balance' => 10,
- 'due_date' => now()->addDays(5),
+ 'due_date' => now()->subDays(5),
'number' => 'testing-number-02',
'status_id' => Credit::STATUS_SENT,
]);
diff --git a/tests/Unit/GeneratesCounterTest.php b/tests/Unit/GeneratesCounterTest.php
index a8e9bbded3..d90574decd 100644
--- a/tests/Unit/GeneratesCounterTest.php
+++ b/tests/Unit/GeneratesCounterTest.php
@@ -420,13 +420,23 @@ class GeneratesCounterTest extends TestCase
public function testInvoiceNumberValue()
{
- $this->assertEquals('0002', $this->invoice->fresh()->number);
+ $invoice = $this->invoice->fresh();
+ $invoice->load('client','company');
- $invoice_number = $this->getNextInvoiceNumber($this->client->fresh(), $this->invoice->fresh());
+ $company = $invoice->company->fresh();
+ $settings = $company->settings;
+ $settings->invoice_number_counter = 3;
+ $company->settings = $settings;
+ $company->save();
+ $invoice->load('client','company');
+ $this->assertEquals('0002', $invoice->number);
+
+ $invoice_number = $this->getNextInvoiceNumber($invoice->client, $invoice->fresh());
+ $this->assertEquals(3, $invoice->company->settings->invoice_number_counter);
$this->assertEquals($invoice_number, '0003');
- $invoice_number = $this->getNextInvoiceNumber($this->client->fresh(), $this->invoice->fresh());
+ $invoice_number = $this->getNextInvoiceNumber($this->client->refresh(), $this->invoice->refresh());
$this->assertEquals($invoice_number, '0004');
}
@@ -593,27 +603,6 @@ class GeneratesCounterTest extends TestCase
$this->assertEquals($invoice_number, '0000000007');
}
- public function testInvoicePrefix()
- {
- $settings = $this->company->settings;
- $this->company->settings = $settings;
- $this->company->save();
-
- $cliz = ClientFactory::create($this->company->id, $this->user->id);
- $cliz->settings = ClientSettings::defaults();
- $cliz->save();
-
- $invoice_number = $this->getNextInvoiceNumber($cliz->fresh(), $this->invoice);
-
- $this->assertEquals('0002', $this->invoice->fresh()->number);
-
- $this->assertEquals('0003', $invoice_number);
-
- $invoice_number = $this->getNextInvoiceNumber($cliz->fresh(), $this->invoice);
-
- $this->assertEquals('0004', $invoice_number);
- }
-
public function testClientNumber()
{
$client_number = $this->getNextClientNumber($this->client);
diff --git a/tests/Unit/QuickbooksSettingsSerializationComparisonTest.php b/tests/Unit/QuickbooksSettingsSerializationComparisonTest.php
new file mode 100644
index 0000000000..93ecf0287e
--- /dev/null
+++ b/tests/Unit/QuickbooksSettingsSerializationComparisonTest.php
@@ -0,0 +1,199 @@
+ SyncDirection::PULL->value,
+ ]);
+
+ // get_object_vars on the enum property itself
+ $enumVars = get_object_vars($syncMap->direction);
+
+ // The enum's internal structure has both name and value
+ $this->assertIsArray($enumVars);
+ $this->assertArrayHasKey('name', $enumVars);
+ $this->assertArrayHasKey('value', $enumVars);
+ $this->assertEquals('PULL', $enumVars['name']);
+ $this->assertEquals('pull', $enumVars['value']);
+
+ // While json_encode handles this, toArray() gives explicit control
+ $array = $syncMap->toArray();
+ $this->assertEquals('pull', $array['direction'],
+ 'toArray() explicitly returns just the value string');
+ }
+
+ /**
+ * Test showing that toArray() correctly serializes enums.
+ */
+ public function testToArrayCorrectlySerializesEnums()
+ {
+ $syncMap = new QuickbooksSyncMap([
+ 'direction' => SyncDirection::PULL->value,
+ ]);
+
+ // New method: toArray()
+ $array = $syncMap->toArray();
+ $json = json_encode($array);
+ $decoded = json_decode($json, true);
+
+ // The enum IS properly serialized as a string value
+ $this->assertIsString($decoded['direction'],
+ 'New method: enum is serialized as string');
+
+ // The decoded value IS the string 'pull'
+ $this->assertEquals('pull', $decoded['direction'],
+ 'New method: enum value is correctly serialized as string');
+ }
+
+ /**
+ * Test showing that get_object_vars() relies on json_encode() for nested objects.
+ *
+ * While get_object_vars() + json_encode() works, it relies on PHP's automatic
+ * serialization. The toArray() method provides explicit, controlled serialization
+ * that's more maintainable and testable.
+ */
+ public function testGetObjectVarsReliesOnJsonEncodeForNestedObjects()
+ {
+ $settings = new QuickbooksSettings([
+ 'accessTokenKey' => 'test_token',
+ 'settings' => [
+ 'client' => [
+ 'direction' => SyncDirection::PULL->value,
+ ],
+ ],
+ ]);
+
+ // Old method: get_object_vars (relies on json_encode to handle nested objects)
+ $vars = get_object_vars($settings);
+ $json = json_encode($vars);
+ $decoded = json_decode($json, true);
+
+ // json_encode() does handle this correctly, but it's implicit
+ $this->assertIsArray($decoded['settings'],
+ 'json_encode handles nested objects, but implicitly');
+
+ // The new method is explicit and controlled
+ $array = $settings->toArray();
+ $this->assertIsArray($array['settings'],
+ 'toArray() explicitly converts nested objects');
+ }
+
+ /**
+ * Test showing that toArray() correctly serializes nested objects.
+ */
+ public function testToArrayCorrectlySerializesNestedObjects()
+ {
+ $settings = new QuickbooksSettings([
+ 'accessTokenKey' => 'test_token',
+ 'settings' => [
+ 'client' => [
+ 'direction' => SyncDirection::PULL->value,
+ ],
+ 'invoice' => [
+ 'direction' => SyncDirection::PUSH->value,
+ ],
+ ],
+ ]);
+
+ // New method: toArray()
+ $array = $settings->toArray();
+ $json = json_encode($array);
+ $decoded = json_decode($json, true);
+
+ // The nested QuickbooksSync object IS properly converted to an array
+ $this->assertIsArray($decoded['settings'],
+ 'New method: nested object is converted to array');
+
+ // The nested QuickbooksSyncMap objects are also converted
+ $this->assertIsArray($decoded['settings']['client'],
+ 'New method: nested sync map is converted to array');
+
+ // The enum values are properly serialized as strings
+ $this->assertEquals('pull', $decoded['settings']['client']['direction'],
+ 'New method: nested enum is serialized as string');
+ $this->assertEquals('push', $decoded['settings']['invoice']['direction'],
+ 'New method: nested enum is serialized as string');
+ }
+
+ /**
+ * Side-by-side comparison: old vs new method.
+ *
+ * Both methods work, but toArray() provides:
+ * 1. Explicit control over serialization
+ * 2. Better maintainability
+ * 3. Consistency with other DataMapper classes
+ * 4. Easier testing and debugging
+ */
+ public function testSideBySideComparison()
+ {
+ $settings = new QuickbooksSettings([
+ 'accessTokenKey' => 'token_123',
+ 'refresh_token' => 'refresh_456',
+ 'realmID' => 'realm_789',
+ 'settings' => [
+ 'client' => [
+ 'direction' => SyncDirection::BIDIRECTIONAL->value,
+ ],
+ ],
+ ]);
+
+ // OLD METHOD (works but implicit)
+ $oldVars = get_object_vars($settings);
+ $oldJson = json_encode($oldVars);
+ $oldDecoded = json_decode($oldJson, true);
+
+ // NEW METHOD (explicit and controlled)
+ $newArray = $settings->toArray();
+ $newJson = json_encode($newArray);
+ $newDecoded = json_decode($newJson, true);
+
+ // Both methods produce valid results
+ $this->assertEquals('token_123', $oldDecoded['accessTokenKey']);
+ $this->assertEquals('token_123', $newDecoded['accessTokenKey']);
+
+ $this->assertEquals('bidirectional', $oldDecoded['settings']['client']['direction']);
+ $this->assertEquals('bidirectional', $newDecoded['settings']['client']['direction']);
+
+ // toArray() is the canonical form for persistence: explicit control, consistent shape
+ $this->assertIsArray($newArray, 'toArray() explicitly returns an array structure');
+ $this->assertIsString($newArray['settings']['client']['direction'],
+ 'toArray() explicitly converts enum to string value');
+
+ // income_account_map uses int keys (Product::PRODUCT_TYPE_*) in toArray() for storage
+ $this->assertArrayHasKey('income_account_map', $newDecoded['settings']);
+ $this->assertIsArray($newDecoded['settings']['income_account_map']);
+ }
+}
diff --git a/tests/Unit/QuickbooksSettingsSerializationTest.php b/tests/Unit/QuickbooksSettingsSerializationTest.php
new file mode 100644
index 0000000000..c3c3d262ad
--- /dev/null
+++ b/tests/Unit/QuickbooksSettingsSerializationTest.php
@@ -0,0 +1,278 @@
+ SyncDirection::PULL->value,
+ ]);
+
+ // Using toArray() - explicit and controlled
+ $array = $syncMap->toArray();
+ $json = json_encode($array);
+ $decoded = json_decode($json, true);
+
+ // Verify explicit serialization works correctly
+ $this->assertIsString($decoded['direction']);
+ $this->assertEquals('pull', $decoded['direction']);
+
+ // toArray() explicitly converts enum to string value
+ $this->assertIsString($array['direction'],
+ 'toArray() explicitly returns enum as string value');
+ }
+
+ /**
+ * Test that the new toArray() method properly serializes enums.
+ */
+ public function testNewSerializationMethodWorksWithEnums()
+ {
+ $settings = new QuickbooksSettings([
+ 'accessTokenKey' => 'test_token',
+ 'refresh_token' => 'refresh_token',
+ 'realmID' => '123456',
+ 'accessTokenExpiresAt' => 1234567890,
+ 'refreshTokenExpiresAt' => 1234567890,
+ 'baseURL' => 'https://sandbox-quickbooks.api.intuit.com',
+ 'settings' => [
+ 'client' => [
+ 'direction' => SyncDirection::PULL->value,
+ ],
+ 'invoice' => [
+ 'direction' => SyncDirection::PUSH->value,
+ ],
+ 'product' => [
+ 'direction' => SyncDirection::BIDIRECTIONAL->value,
+ ],
+ ],
+ ]);
+
+ // Use the new toArray() method
+ $array = $settings->toArray();
+ $json = json_encode($array);
+ $decoded = json_decode($json, true);
+
+ // Verify enum values are properly serialized as strings
+ $this->assertIsString($decoded['settings']['client']['direction']);
+ $this->assertEquals('pull', $decoded['settings']['client']['direction']);
+
+ $this->assertIsString($decoded['settings']['invoice']['direction']);
+ $this->assertEquals('push', $decoded['settings']['invoice']['direction']);
+
+ $this->assertIsString($decoded['settings']['product']['direction']);
+ $this->assertEquals('bidirectional', $decoded['settings']['product']['direction']);
+ }
+
+ /**
+ * Test that nested objects are properly serialized.
+ */
+ public function testNestedObjectsAreProperlySerialized()
+ {
+ $settings = new QuickbooksSettings([
+ 'accessTokenKey' => 'test_token',
+ 'refresh_token' => 'refresh_token',
+ 'realmID' => '123456',
+ 'accessTokenExpiresAt' => 1234567890,
+ 'refreshTokenExpiresAt' => 1234567890,
+ 'baseURL' => 'https://sandbox-quickbooks.api.intuit.com',
+ 'settings' => [
+ 'client' => [
+ 'direction' => SyncDirection::PULL->value,
+ ],
+
+ ],
+ ]);
+
+ $array = $settings->toArray();
+
+ // Verify nested QuickbooksSync structure
+ $this->assertIsArray($array['settings']);
+ $this->assertArrayHasKey('client', $array['settings']);
+ $this->assertArrayHasKey('income_account_map', $array['settings']);
+
+ // Verify nested QuickbooksSyncMap structure
+ $this->assertIsArray($array['settings']['client']);
+ $this->assertArrayHasKey('direction', $array['settings']['client']);
+ $this->assertEquals('pull', $array['settings']['client']['direction']);
+ }
+
+ /**
+ * Test round-trip serialization through the cast.
+ */
+ public function testRoundTripSerializationThroughCast()
+ {
+ $originalSettings = new QuickbooksSettings([
+ 'accessTokenKey' => 'test_token_123',
+ 'refresh_token' => 'refresh_token_456',
+ 'realmID' => 'realm_789',
+ 'accessTokenExpiresAt' => 1234567890,
+ 'refreshTokenExpiresAt' => 9876543210,
+ 'baseURL' => 'https://sandbox-quickbooks.api.intuit.com',
+ 'settings' => [
+ 'client' => [
+ 'direction' => SyncDirection::PULL->value,
+ ],
+ 'invoice' => [
+ 'direction' => SyncDirection::PUSH->value,
+ ],
+ 'product' => [
+ 'direction' => SyncDirection::BIDIRECTIONAL->value,
+ ],
+ ],
+ ]);
+
+ $cast = new QuickbooksSettingsCast();
+
+ // Create a mock model for the cast
+ $model = new class extends Model {
+ // Empty model for testing
+ };
+
+ // Serialize (set)
+ $serialized = $cast->set($model, 'quickbooks', $originalSettings, []);
+
+ $this->assertNotNull($serialized);
+ $this->assertIsString($serialized);
+
+ // Deserialize (get)
+ $deserialized = $cast->get($model, 'quickbooks', $serialized, []);
+
+ $this->assertInstanceOf(QuickbooksSettings::class, $deserialized);
+
+ // Verify all properties are preserved
+ $this->assertEquals($originalSettings->accessTokenKey, $deserialized->accessTokenKey);
+ $this->assertEquals($originalSettings->refresh_token, $deserialized->refresh_token);
+ $this->assertEquals($originalSettings->realmID, $deserialized->realmID);
+ $this->assertEquals($originalSettings->accessTokenExpiresAt, $deserialized->accessTokenExpiresAt);
+ $this->assertEquals($originalSettings->refreshTokenExpiresAt, $deserialized->refreshTokenExpiresAt);
+ $this->assertEquals($originalSettings->baseURL, $deserialized->baseURL);
+
+ // Verify nested settings are preserved
+ $this->assertInstanceOf(QuickbooksSync::class, $deserialized->settings);
+
+ // Verify enum values are preserved correctly
+ $this->assertInstanceOf(QuickbooksSyncMap::class, $deserialized->settings->client);
+ $this->assertEquals(SyncDirection::PULL, $deserialized->settings->client->direction);
+
+ $this->assertInstanceOf(QuickbooksSyncMap::class, $deserialized->settings->invoice);
+ $this->assertEquals(SyncDirection::PUSH, $deserialized->settings->invoice->direction);
+
+ $this->assertInstanceOf(QuickbooksSyncMap::class, $deserialized->settings->product);
+ $this->assertEquals(SyncDirection::BIDIRECTIONAL, $deserialized->settings->product->direction);
+ }
+
+ /**
+ * Test that all entity types are properly serialized.
+ */
+ public function testAllEntityTypesAreSerialized()
+ {
+ $settings = new QuickbooksSettings([
+ 'settings' => [
+ 'client' => ['direction' => SyncDirection::PULL->value],
+ 'vendor' => ['direction' => SyncDirection::PUSH->value],
+ 'invoice' => ['direction' => SyncDirection::BIDIRECTIONAL->value],
+ 'sales' => ['direction' => SyncDirection::PULL->value],
+ 'quote' => ['direction' => SyncDirection::PUSH->value],
+ 'purchase_order' => ['direction' => SyncDirection::BIDIRECTIONAL->value],
+ 'product' => ['direction' => SyncDirection::PULL->value],
+ 'payment' => ['direction' => SyncDirection::PUSH->value],
+ 'expense' => ['direction' => SyncDirection::BIDIRECTIONAL->value],
+ ],
+ ]);
+
+ $array = $settings->toArray();
+
+ $entities = ['client', 'vendor', 'invoice', 'sales', 'quote', 'purchase_order', 'product', 'payment', 'expense'];
+
+ foreach ($entities as $entity) {
+ $this->assertArrayHasKey($entity, $array['settings'], "Entity {$entity} should be in serialized array");
+ $this->assertArrayHasKey('direction', $array['settings'][$entity], "Entity {$entity} should have direction");
+ $this->assertIsString($array['settings'][$entity]['direction'], "Entity {$entity} direction should be a string");
+ }
+ }
+
+ /**
+ * Test that empty/default settings serialize correctly.
+ */
+ public function testEmptySettingsSerializeCorrectly()
+ {
+ $settings = new QuickbooksSettings();
+
+ $array = $settings->toArray();
+
+ // Verify all OAuth fields have default values
+ $this->assertEquals('', $array['accessTokenKey']);
+ $this->assertEquals('', $array['refresh_token']);
+ $this->assertEquals('', $array['realmID']);
+ $this->assertEquals(0, $array['accessTokenExpiresAt']);
+ $this->assertEquals(0, $array['refreshTokenExpiresAt']);
+ $this->assertEquals('', $array['baseURL']);
+
+ // Verify settings structure exists
+ $this->assertIsArray($array['settings']);
+ $this->assertArrayHasKey('client', $array['settings']);
+
+ // Verify default direction is BIDIRECTIONAL
+ $this->assertEquals('none', $array['settings']['client']['direction']);
+ }
+
+ /**
+ * Test that JSON produced by toArray() can be decoded and reconstructed.
+ */
+ public function testJsonCanBeDecodedAndReconstructed()
+ {
+ $originalSettings = new QuickbooksSettings([
+ 'accessTokenKey' => 'token_123',
+ 'settings' => [
+ 'client' => [
+ 'direction' => SyncDirection::PULL->value,
+ ],
+ ],
+ ]);
+
+ // Serialize to JSON
+ $json = json_encode($originalSettings->toArray());
+ $this->assertIsString($json);
+
+ // Decode JSON
+ $decoded = json_decode($json, true);
+ $this->assertIsArray($decoded);
+
+ // Reconstruct from decoded array
+ $reconstructed = QuickbooksSettings::fromArray($decoded);
+
+ // Verify reconstruction
+ $this->assertEquals($originalSettings->accessTokenKey, $reconstructed->accessTokenKey);
+ $this->assertEquals($originalSettings->settings->client->direction, $reconstructed->settings->client->direction);
+ }
+}
diff --git a/tests/Unit/Services/Quickbooks/Transformers/CompanyTransformerTest.php b/tests/Unit/Services/Quickbooks/Transformers/CompanyTransformerTest.php
new file mode 100644
index 0000000000..6a2b783db6
--- /dev/null
+++ b/tests/Unit/Services/Quickbooks/Transformers/CompanyTransformerTest.php
@@ -0,0 +1,299 @@
+makeTestData();
+
+ $this->transformer = new CompanyTransformer($this->company);
+
+ // Mock QuickBooks IPPCompanyInfo structure based on the payload provided
+ $this->qbCompanyInfo = [
+ 'Id' => '1',
+ 'SyncToken' => '9',
+ 'CompanyName' => 'Sandbox Company_US_1',
+ 'LegalName' => 'Sandbox Company_US_1',
+ 'CompanyAddr' => [
+ 'Id' => '1',
+ 'Line1' => '123 Sierra Way',
+ 'Line2' => '',
+ 'Line3' => '',
+ 'Line4' => '',
+ 'Line5' => '',
+ 'City' => 'San Pablo',
+ 'Country' => 'USA',
+ 'CountryCode' => '',
+ 'County' => '',
+ 'CountrySubDivisionCode' => 'CA',
+ 'PostalCode' => '87999',
+ 'PostalCodeSuffix' => '',
+ ],
+ 'CustomerCommunicationAddr' => [
+ 'Id' => '387',
+ 'Line1' => '123 Sierra Way',
+ 'Line2' => '',
+ 'City' => 'San Pablo',
+ 'Country' => '',
+ 'CountryCode' => '',
+ 'CountrySubDivisionCode' => 'CA',
+ 'PostalCode' => '87999',
+ ],
+ 'LegalAddr' => [
+ 'Id' => '386',
+ 'Line1' => '123 Sierra Way',
+ 'Line2' => '',
+ 'City' => 'San Pablo',
+ 'Country' => '',
+ 'CountryCode' => '',
+ 'CountrySubDivisionCode' => 'CA',
+ 'PostalCode' => '87999',
+ ],
+ 'CompanyEmailAddr' => null,
+ 'CustomerCommunicationEmailAddr' => [
+ 'Id' => '',
+ 'Address' => 'david@invoiceninja.com',
+ 'Default' => null,
+ 'Tag' => '',
+ ],
+ 'CompanyURL' => '',
+ 'PrimaryPhone' => [
+ 'Id' => '',
+ 'DeviceType' => '',
+ 'CountryCode' => '',
+ 'AreaCode' => '',
+ 'ExchangeCode' => '',
+ 'Extension' => '',
+ 'FreeFormNumber' => '4081234567',
+ 'Default' => null,
+ 'Tag' => '',
+ ],
+ 'Email' => [
+ 'Id' => '',
+ 'Address' => 'david@invoiceninja.com',
+ 'Default' => null,
+ 'Tag' => '',
+ ],
+ 'WebAddr' => '',
+ 'Country' => 'US',
+ 'DefaultTimeZone' => 'America/Los_Angeles',
+ 'SupportedLanguages' => 'en',
+ ];
+ }
+
+ public function testTransformerInstance(): void
+ {
+ $this->assertInstanceOf(CompanyTransformer::class, $this->transformer);
+ }
+
+ public function testTransformReturnsArray(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('quickbooks', $result);
+ $this->assertArrayHasKey('settings', $result);
+ }
+
+ public function testQuickbooksDataStructure(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ $this->assertArrayHasKey('companyName', $result['quickbooks']);
+ $this->assertEquals('Sandbox Company_US_1', $result['quickbooks']['companyName']);
+ }
+
+ public function testSettingsDataStructure(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ $settings = $result['settings'];
+
+ $this->assertArrayHasKey('address1', $settings);
+ $this->assertArrayHasKey('address2', $settings);
+ $this->assertArrayHasKey('city', $settings);
+ $this->assertArrayHasKey('state', $settings);
+ $this->assertArrayHasKey('postal_code', $settings);
+ $this->assertArrayHasKey('country_id', $settings);
+ $this->assertArrayHasKey('phone', $settings);
+ $this->assertArrayHasKey('email', $settings);
+ $this->assertArrayHasKey('website', $settings);
+ $this->assertArrayHasKey('timezone_id', $settings);
+ }
+
+ public function testAddressMapping(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ $settings = $result['settings'];
+
+ // Should use CompanyAddr as primary
+ $this->assertEquals('123 Sierra Way', $settings['address1']);
+ $this->assertEquals('San Pablo', $settings['city']);
+ $this->assertEquals('CA', $settings['state']);
+ $this->assertEquals('87999', $settings['postal_code']);
+ }
+
+ public function testContactInformationMapping(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ $settings = $result['settings'];
+
+ $this->assertEquals('4081234567', $settings['phone']);
+ $this->assertEquals('david@invoiceninja.com', $settings['email']);
+ }
+
+ public function testCountryResolution(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ $settings = $result['settings'];
+
+ // Country should be resolved to a valid country_id
+ $this->assertNotEmpty($settings['country_id']);
+ $this->assertIsString($settings['country_id']);
+ }
+
+ public function testTimezoneResolution(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ $settings = $result['settings'];
+
+ // Timezone should be resolved to a valid timezone_id
+ $this->assertNotEmpty($settings['timezone_id']);
+ $this->assertIsString($settings['timezone_id']);
+ }
+
+ public function testCanPersistQuickbooksData(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ // Get fresh company instance
+ $company = Company::find($this->company->id);
+
+ // Update quickbooks data
+ $company->quickbooks->companyName = $result['quickbooks']['companyName'];
+
+ // Should not throw exception
+ $company->save();
+
+ // Verify it was saved
+ $company->refresh();
+ $this->assertEquals('Sandbox Company_US_1', $company->quickbooks->companyName);
+ }
+
+ public function testCanPersistSettingsData(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ // Get fresh company instance
+ $company = Company::find($this->company->id);
+
+ // Merge settings data
+ $company->saveSettings($result['settings'], $company);
+
+ // Should not throw exception
+ $company->save();
+
+ // Verify settings were saved
+ $company->refresh();
+ $this->assertEquals('123 Sierra Way', $company->settings->address1);
+ $this->assertEquals('San Pablo', $company->settings->city);
+ $this->assertEquals('CA', $company->settings->state);
+ $this->assertEquals('87999', $company->settings->postal_code);
+ $this->assertEquals('4081234567', $company->settings->phone);
+ $this->assertEquals('david@invoiceninja.com', $company->settings->email);
+ }
+
+ public function testCanPersistBothQuickbooksAndSettings(): void
+ {
+ $result = $this->transformer->transform($this->qbCompanyInfo);
+
+ // Get fresh company instance
+ $company = Company::find($this->company->id);
+
+ // Update both quickbooks and settings
+ $company->quickbooks->companyName = $result['quickbooks']['companyName'];
+ $company->saveSettings($result['settings'], $company);
+
+ // Should not throw exception
+ $company->save();
+
+ // Verify both were saved
+ $company->refresh();
+ $this->assertEquals('Sandbox Company_US_1', $company->quickbooks->companyName);
+ $this->assertEquals('123 Sierra Way', $company->settings->address1);
+ $this->assertEquals('david@invoiceninja.com', $company->settings->email);
+ }
+
+ public function testAddressFallbackToLegalAddr(): void
+ {
+ // Remove CompanyAddr to test fallback
+ $qbData = $this->qbCompanyInfo;
+ unset($qbData['CompanyAddr']);
+
+ $result = $this->transformer->transform($qbData);
+
+ // Should fallback to LegalAddr
+ $this->assertEquals('123 Sierra Way', $result['settings']['address1']);
+ $this->assertEquals('San Pablo', $result['settings']['city']);
+ }
+
+ public function testEmailFallback(): void
+ {
+ // Remove Email to test fallback to CustomerCommunicationEmailAddr
+ $qbData = $this->qbCompanyInfo;
+ unset($qbData['Email']);
+
+ $result = $this->transformer->transform($qbData);
+
+ // Should fallback to CustomerCommunicationEmailAddr
+ $this->assertEquals('david@invoiceninja.com', $result['settings']['email']);
+ }
+
+ public function testHandlesEmptyData(): void
+ {
+ $emptyData = [
+ 'CompanyName' => '',
+ 'LegalName' => 'Test Legal Name',
+ ];
+
+ $result = $this->transformer->transform($emptyData);
+
+ // Should use LegalName when CompanyName is empty
+ $this->assertEquals('Test Legal Name', $result['quickbooks']['companyName']);
+
+ // Settings should have empty strings for missing data
+ $this->assertEquals('', $result['settings']['address1']);
+ $this->assertEquals('', $result['settings']['phone']);
+ $this->assertEquals('', $result['settings']['email']);
+ }
+}
diff --git a/tests/Unit/Storecove/DocumentSubmissionExtractUblTest.php b/tests/Unit/Storecove/DocumentSubmissionExtractUblTest.php
new file mode 100644
index 0000000000..71ab63f232
--- /dev/null
+++ b/tests/Unit/Storecove/DocumentSubmissionExtractUblTest.php
@@ -0,0 +1,326 @@
+markTestSkipped('DocumentSubmission class does not exist');
+ }
+ }
+ /**
+ * Test extracting CreditNote from StandardBusinessDocument wrapper
+ */
+ public function testExtractCreditNoteFromSbdWrapper(): void
+ {
+ $xml = '1.0 0208:1234567890 0208:0987654321 urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2 2.1 aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee CreditNote 2026-01-22T15:53:41.44Z DOCUMENTID urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1 busdox-docid-qns PROCESSID urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 cenbii-procid-ubl COUNTRY_C1 BE
+ 2.1
+ urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+ TEST/2026/0001
+ 2026-01-22
+ 381
+ EUR
+
+ TEST/2026/0001
+
+
+ test20260001
+
+
+
+ 1234567890
+
+ 1234567890
+
+
+ Test Supplier Company
+
+
+ 123 Test Street
+ Test City
+ 1000
+ Test Region
+
+ BE
+
+
+
+ BE1234567890
+
+ VAT
+
+
+
+ Test Supplier Company
+ 1234567890
+
+
+ Test Contact
+ supplier@example.com
+
+
+
+
+
+ 0987654321
+
+ Test Customer Company
+
+
+ 456 Customer Street
+ Customer City
+ 2000
+
+ BE
+
+
+
+ BE0987654321
+
+ VAT
+
+
+
+ Test Customer Company
+
+
+ 123456789
+ customer@example.com
+
+
+
+
+ 30
+ TEST/2026/0001
+
+ BE12345678901234
+ TEST SUPPLIER COMPANY
+
+
+
+ 0.00
+
+ 49.50
+ 0.00
+
+ E
+ 0.0
+ Exempt
+
+ VAT
+
+
+
+
+
+ 49.50
+ 49.50
+ 49.50
+ 49.50
+
+
+ 1
+ 1.000000
+ 22.00
+
+ Credit note on TEST/2025/0001
+ DOMAIN .COM/.NET/.ORG
+
+ E
+ 0.0
+
+ VAT
+
+
+
+
+ 22.00
+ 1.0
+
+
+
+ 2
+ 0.250000
+ 27.50
+
+ Credit note on TEST/2025/0001
+ PRESTATION DE SERVICES
+
+ E
+ 0.0
+
+ VAT
+
+
+
+
+ 110.00
+ 1.0
+
+
+ ';
+
+ $job = new DocumentSubmission([]);
+ $reflection = new ReflectionClass($job);
+ $method = $reflection->getMethod('extractInvoiceUbl');
+ $method->setAccessible(true);
+
+ $result = $method->invoke($job, $xml);
+
+ // Assert that the result is valid XML
+ $this->assertNotEmpty($result);
+
+ // Assert that the result contains CreditNote
+ $this->assertStringContainsString('assertStringContainsString('TEST/2026/0001', $result);
+
+ // Assert that the result does NOT contain the SBD wrapper
+ $this->assertStringNotContainsString('StandardBusinessDocument', $result);
+ $this->assertStringNotContainsString('StandardBusinessDocumentHeader', $result);
+
+ // Assert that the result is valid XML that can be parsed
+ $dom = new \DOMDocument();
+ $this->assertTrue($dom->loadXML($result), 'Extracted XML should be valid');
+
+ // Assert that the root element is CreditNote
+ $this->assertEquals('CreditNote', $dom->documentElement->localName);
+ $this->assertEquals('urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2', $dom->documentElement->namespaceURI);
+ }
+
+ /**
+ * Test extracting Invoice from StandardBusinessDocument wrapper
+ */
+ public function testExtractInvoiceFromSbdWrapper(): void
+ {
+ $xml = '1.0 0208:0769867026 0208:0821894064 urn:oasis:names:specification:ubl:schema:xsd:Invoice-2 2.1 507dcfe6-7f6e-473a-bd20-f1c8dce2e2c8 Invoice 2026-01-22T15:53:41.44Z
+ 2.1
+ urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+ INV/2026/0001
+ 2026-01-22
+ EUR
+ ';
+
+ $job = new DocumentSubmission([]);
+ $reflection = new ReflectionClass($job);
+ $method = $reflection->getMethod('extractInvoiceUbl');
+ $method->setAccessible(true);
+
+ $result = $method->invoke($job, $xml);
+
+ // Assert that the result is valid XML
+ $this->assertNotEmpty($result);
+
+ // Assert that the result contains Invoice
+ $this->assertStringContainsString('assertStringContainsString('INV/2026/0001', $result);
+
+ // Assert that the result does NOT contain the SBD wrapper
+ $this->assertStringNotContainsString('StandardBusinessDocument', $result);
+
+ // Assert that the result is valid XML that can be parsed
+ $dom = new \DOMDocument();
+ $this->assertTrue($dom->loadXML($result), 'Extracted XML should be valid');
+
+ // Assert that the root element is Invoice
+ $this->assertEquals('Invoice', $dom->documentElement->localName);
+ $this->assertEquals('urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', $dom->documentElement->namespaceURI);
+ }
+
+ /**
+ * Test that exception is thrown when neither Invoice nor CreditNote is found
+ */
+ public function testThrowsExceptionWhenNoInvoiceOrCreditNoteFound(): void
+ {
+ $xml = '1.0 Test ';
+
+ $job = new DocumentSubmission([]);
+ $reflection = new ReflectionClass($job);
+ $method = $reflection->getMethod('extractInvoiceUbl');
+ $method->setAccessible(true);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('No Invoice or CreditNote tag found in XML');
+
+ $method->invoke($job, $xml);
+ }
+
+ /**
+ * Test that method handles XML without SBD wrapper (direct Invoice)
+ */
+ public function testExtractDirectInvoiceWithoutWrapper(): void
+ {
+ $xml = '
+ 2.1
+ DIRECT/2026/0001
+ 2026-01-22
+ ';
+
+ $job = new DocumentSubmission([]);
+ $reflection = new ReflectionClass($job);
+ $method = $reflection->getMethod('extractInvoiceUbl');
+ $method->setAccessible(true);
+
+ $result = $method->invoke($job, $xml);
+
+ // Assert that the result is valid XML
+ $this->assertNotEmpty($result);
+ $this->assertStringContainsString('assertStringContainsString('DIRECT/2026/0001', $result);
+
+ // Assert that the result is valid XML that can be parsed
+ $dom = new \DOMDocument();
+ $this->assertTrue($dom->loadXML($result), 'Extracted XML should be valid');
+ $this->assertEquals('Invoice', $dom->documentElement->localName);
+ }
+
+ /**
+ * Test that method handles XML without SBD wrapper (direct CreditNote)
+ */
+ public function testExtractDirectCreditNoteWithoutWrapper(): void
+ {
+ $xml = '
+ 2.1
+ CN/2026/0001
+ 2026-01-22
+ ';
+
+ $job = new DocumentSubmission([]);
+ $reflection = new ReflectionClass($job);
+ $method = $reflection->getMethod('extractInvoiceUbl');
+ $method->setAccessible(true);
+
+ $result = $method->invoke($job, $xml);
+
+ // Assert that the result is valid XML
+ $this->assertNotEmpty($result);
+ $this->assertStringContainsString('assertStringContainsString('CN/2026/0001', $result);
+
+ // Assert that the result is valid XML that can be parsed
+ $dom = new \DOMDocument();
+ $this->assertTrue($dom->loadXML($result), 'Extracted XML should be valid');
+ $this->assertEquals('CreditNote', $dom->documentElement->localName);
+ }
+}
diff --git a/tests/Unit/Tax/UsTaxTest.php b/tests/Unit/Tax/UsTaxTest.php
index 303541ef19..64e7c21bca 100644
--- a/tests/Unit/Tax/UsTaxTest.php
+++ b/tests/Unit/Tax/UsTaxTest.php
@@ -1131,6 +1131,10 @@ class UsTaxTest extends TestCase
$company->tax_data = $tax_data;
$company->save();
+ // Reload the company relationship on the invoice to ensure fresh tax_data is used
+ $invoice->load('company');
+ $invoice->client->load('company');
+
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(0, $invoice->line_items[0]->tax_rate1);
diff --git a/vite.config.ts b/vite.config.ts
index a30c156efc..2500f34eba 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -25,6 +25,7 @@ export default defineConfig({
'resources/js/clients/payments/checkout-credit-card.js',
'resources/js/clients/quotes/action-selectors.js',
'resources/js/clients/quotes/approve.js',
+ 'resources/js/clients/quotes/reject.js',
'resources/js/clients/payments/stripe-credit-card.js',
'resources/js/setup/setup.js',
'resources/js/clients/shared/pdf.js',