From a424c43c6e950175917ab5b20290078bef7a965b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 22 Sep 2024 14:49:43 +1000 Subject: [PATCH 01/55] Prototype for global search --- app/Events/Invoice/InvoiceWasPaid.php | 18 +- app/Http/Controllers/SearchController.php | 119 +++- .../Chart/ShowCalculatedFieldRequest.php | 2 +- app/Models/Client.php | 60 +- app/Models/ClientContact.php | 20 + app/Models/Invoice.php | 44 +- app/Services/Chart/ChartCalculations.php | 8 +- app/Transformers/InvoiceTransformer.php | 4 + composer.json | 2 + composer.lock | 611 +++++++++++++++++- config/elastic.client.php | 24 + config/elastic.scout_driver.php | 5 + config/scout.php | 202 ++++++ 13 files changed, 1061 insertions(+), 58 deletions(-) create mode 100644 config/elastic.client.php create mode 100644 config/elastic.scout_driver.php create mode 100644 config/scout.php diff --git a/app/Events/Invoice/InvoiceWasPaid.php b/app/Events/Invoice/InvoiceWasPaid.php index a5a8812f1b90..1a4631f067e5 100644 --- a/app/Events/Invoice/InvoiceWasPaid.php +++ b/app/Events/Invoice/InvoiceWasPaid.php @@ -26,29 +26,15 @@ class InvoiceWasPaid implements ShouldBroadcast { use SerializesModels, DefaultInvoiceBroadcast; - /** - * @var Invoice - */ - public $invoice; - - public $payment; - - public $company; - - public $event_vars; - /** * Create a new event instance. * * @param Invoice $invoice * @param Company $company + * @param Payment $payment * @param array $event_vars */ - public function __construct(Invoice $invoice, Payment $payment, Company $company, array $event_vars) + public function __construct(public Invoice $invoice, public Payment $payment, public Company $company, public array $event_vars) { - $this->invoice = $invoice; - $this->payment = $payment; - $this->company = $company; - $this->event_vars = $event_vars; } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index f4d842843c6b..ca5cd688161b 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -11,10 +11,12 @@ namespace App\Http\Controllers; -use App\Http\Requests\Search\GenericSearchRequest; +use App\Models\User; +use App\Utils\Ninja; use App\Models\Client; use App\Models\Invoice; -use App\Models\User; +use Elastic\Elasticsearch\ClientBuilder; +use App\Http\Requests\Search\GenericSearchRequest; class SearchController extends Controller { @@ -26,6 +28,14 @@ class SearchController extends Controller public function __invoke(GenericSearchRequest $request) { + if(Ninja::isHosted() && $request->has('search') && $request->input('search') !== '') { + try{ + return $this->search($request->input('search', '')); + } catch(\Exception $e) { + nlog("elk down?"); + } + } + /** @var \App\Models\User $user */ $user = auth()->user(); @@ -41,6 +51,95 @@ class SearchController extends Controller } + public function search(string $search) + { + $user = auth()->user(); + $company = $user->company(); + + $elastic = ClientBuilder::fromConfig(config('elastic.client.connections.default')); + + $params = [ + 'index' => 'clients,invoices,client_contacts', + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [ + 'multi_match' => [ + 'query' => $search, + 'fields' => ['*'], + 'fuzziness' => 'AUTO', + ], + ], + 'filter' => [ + 'match' => [ + 'company_key' => $company->company_key, + ], + ], + ], + ], + 'size' => 1000, + ], + ]; + + $results = $elastic->search($params); + + $this->mapResults($results['hits']['hits'] ?? []); + + return response()->json([ + 'clients' => $this->clients, + 'client_contacts' => $this->client_contacts, + 'invoices' => $this->invoices, + 'settings' => $this->settingsMap(), + ], 200); + + } + + private function mapResults(array $results) + { + foreach($results as $result) { + switch($result['_index']) { + case 'clients': + + if($result['_source']['is_deleted']) + continue; + + $this->clients[] = [ + 'name' => $result['_source']['name'], + 'type' => '/client', + 'id' => $result['_source']['hashed_id'], + 'path' => "/clients/{$result['_source']['hashed_id']}" + ]; + + break; + case 'invoices': + + if ($result['_source']['is_deleted']) { + continue; + } + + $this->invoices[] = [ + 'name' => $result['_source']['name'], + 'type' => '/invoice', + 'id' => $result['_source']['hashed_id'], + 'path' => "/invoices/{$result['_source']['hashed_id']}/edit" + ]; + break; + case 'client_contacts': + + if($result['_source']['__soft_deleted']) + continue; + + $this->client_contacts[] = [ + 'name' => $result['_source']['name'], + 'type' => '/client', + 'id' => $result['_source']['hashed_id'], + 'path' => "/clients/{$result['_source']['hashed_id']}" + ]; + break; + } + } + } + private function clientMap(User $user) { @@ -81,20 +180,14 @@ class SearchController extends Controller $invoices = Invoice::query() ->company() ->with('client') - ->where('invoices.is_deleted', 0) - // ->whereHas('client', function ($q) { - // $q->where('is_deleted', 0); - // }) - - ->leftJoin('clients', function ($join) { - $join->on('invoices.client_id', '=', 'clients.id') - ->where('clients.is_deleted', 0); - }) - + ->where('is_deleted', 0) + ->whereHas('client', function ($q) { + $q->where('is_deleted', 0); + }) ->when(!$user->hasPermission('view_all') || !$user->hasPermission('view_invoice'), function ($query) use ($user) { $query->where('invoices.user_id', $user->id); }) - ->orderBy('invoices.id', 'desc') + ->orderBy('id', 'desc') ->take(3000) ->get(); diff --git a/app/Http/Requests/Chart/ShowCalculatedFieldRequest.php b/app/Http/Requests/Chart/ShowCalculatedFieldRequest.php index 5a9945da8f72..3aa6271dc2af 100644 --- a/app/Http/Requests/Chart/ShowCalculatedFieldRequest.php +++ b/app/Http/Requests/Chart/ShowCalculatedFieldRequest.php @@ -37,7 +37,7 @@ class ShowCalculatedFieldRequest extends Request 'date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom', 'start_date' => 'bail|sometimes|date', 'end_date' => 'bail|sometimes|date', - 'field' => 'required|bail|in:active_invoices, outstanding_invoices, completed_payments, refunded_payments, active_quotes, unapproved_quotes, logged_tasks, invoiced_tasks, paid_tasks, logged_expenses, pending_expenses, invoiced_expenses, invoice_paid_expenses', + 'field' => 'required|bail|in:active_invoices,outstanding_invoices,completed_payments,refunded_payments,active_quotes,unapproved_quotes logged_tasks,invoiced_tasks,paid_tasks,logged_expenses,pending_expenses,invoiced_expenses,invoice_paid_expenses', 'calculation' => 'required|bail|in:sum,avg,count', 'period' => 'required|bail|in:current,previous,total', 'format' => 'sometimes|bail|in:time,money', diff --git a/app/Models/Client.php b/app/Models/Client.php index cbb7500daa29..e8ab1dfab607 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -11,23 +11,24 @@ namespace App\Models; +use Laravel\Scout\Searchable; +use App\Utils\Traits\AppSetup; +use App\Utils\Traits\MakesHash; +use App\Utils\Traits\MakesDates; +use App\DataMapper\FeesAndLimits; +use App\Models\Traits\Excludable; use App\DataMapper\ClientSettings; use App\DataMapper\CompanySettings; -use App\DataMapper\FeesAndLimits; -use App\Libraries\Currency\Conversion\CurrencyApi; -use App\Models\Presenters\ClientPresenter; -use App\Models\Traits\Excludable; use App\Services\Client\ClientService; -use App\Utils\Traits\AppSetup; -use App\Utils\Traits\ClientGroupSettingsSaver; use App\Utils\Traits\GeneratesCounter; -use App\Utils\Traits\MakesDates; -use App\Utils\Traits\MakesHash; -use Illuminate\Contracts\Translation\HasLocalePreference; -use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\SoftDeletes; use Laracasts\Presenter\PresentableTrait; +use App\Models\Presenters\ClientPresenter; +use Illuminate\Database\Eloquent\SoftDeletes; +use App\Utils\Traits\ClientGroupSettingsSaver; +use App\Libraries\Currency\Conversion\CurrencyApi; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Contracts\Translation\HasLocalePreference; /** * App\Models\Client @@ -124,6 +125,9 @@ class Client extends BaseModel implements HasLocalePreference use ClientGroupSettingsSaver; use Excludable; + + use Searchable; + protected $presenter = ClientPresenter::class; protected $hidden = [ @@ -232,6 +236,38 @@ class Client extends BaseModel implements HasLocalePreference 'custom_value4', ]; + public function toSearchableArray() + { + return [ + 'name' => $this->present()->name(), + 'is_deleted' => $this->is_deleted, + 'hashed_id' => $this->hashed_id, + 'number' => $this->number, + 'id_number' => $this->id_number, + 'vat_number' => $this->vat_number, + 'balance' => $this->balance, + 'paid_to_date' => $this->paid_to_date, + 'phone' => $this->phone, + 'address1' => $this->address1, + 'address2' => $this->address2, + 'city' => $this->city, + 'state' => $this->state, + 'postal_code' => $this->postal_code, + 'website' => $this->website, + 'private_notes' => $this->private_notes, + 'public_notes' => $this->public_notes, + 'shipping_address1' => $this->shipping_address1, + 'shipping_address2' => $this->shipping_address2, + 'shipping_city' => $this->shipping_city, + 'shipping_state' => $this->shipping_state, + 'shipping_postal_code' => $this->shipping_postal_code, + 'custom_value1' => $this->custom_value1, + 'custom_value2' => $this->custom_value2, + 'custom_value3' => $this->custom_value3, + 'custom_value4' => $this->custom_value4, + 'company_key' => $this->company->company_key, + ]; + } public function getEntityType() { diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index 82941087367b..939dc7140d09 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -13,6 +13,7 @@ namespace App\Models; use App\Utils\Ninja; use Illuminate\Support\Str; +use Laravel\Scout\Searchable; use App\Jobs\Mail\NinjaMailer; use App\Utils\Traits\AppSetup; use App\Utils\Traits\MakesHash; @@ -100,6 +101,8 @@ class ClientContact extends Authenticatable implements HasLocalePreference use HasFactory; use AppSetup; + use Searchable; + /* Used to authenticate a contact */ protected $guard = 'contact'; @@ -165,6 +168,23 @@ class ClientContact extends Authenticatable implements HasLocalePreference 'email', ]; + public function toSearchableArray() + { + return [ + 'name' => $this->present()->search_display(), + 'hashed_id' => $this->client->hashed_id, + 'email' => $this->email, + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'phone' => $this->phone, + 'custom_value1' => $this->custom_value1, + 'custom_value2' => $this->custom_value2, + 'custom_value3' => $this->custom_value3, + 'custom_value4' => $this->custom_value4, + 'company_key' => $this->company->company_key, + ]; + } + /* V2 type of scope */ diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index b51fbf7fef83..725b6dcd3892 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -11,22 +11,23 @@ namespace App\Models; -use App\Events\Invoice\InvoiceReminderWasEmailed; -use App\Events\Invoice\InvoiceWasEmailed; -use App\Helpers\Invoice\InvoiceSum; -use App\Helpers\Invoice\InvoiceSumInclusive; -use App\Models\Presenters\EntityPresenter; -use App\Services\Invoice\InvoiceService; -use App\Services\Ledger\LedgerService; use App\Utils\Ninja; -use App\Utils\Traits\Invoice\ActionsInvoice; +use Laravel\Scout\Searchable; +use Illuminate\Support\Carbon; use App\Utils\Traits\MakesDates; -use App\Utils\Traits\MakesInvoiceValues; +use App\Helpers\Invoice\InvoiceSum; use App\Utils\Traits\MakesReminders; use App\Utils\Traits\NumberFormatter; -use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Support\Carbon; +use App\Services\Ledger\LedgerService; +use App\Services\Invoice\InvoiceService; +use App\Utils\Traits\MakesInvoiceValues; +use App\Events\Invoice\InvoiceWasEmailed; use Laracasts\Presenter\PresentableTrait; +use App\Models\Presenters\EntityPresenter; +use App\Helpers\Invoice\InvoiceSumInclusive; +use App\Utils\Traits\Invoice\ActionsInvoice; +use Illuminate\Database\Eloquent\SoftDeletes; +use App\Events\Invoice\InvoiceReminderWasEmailed; /** * App\Models\Invoice @@ -144,6 +145,8 @@ class Invoice extends BaseModel use MakesReminders; use ActionsInvoice; + use Searchable; + protected $presenter = EntityPresenter::class; protected $touches = []; @@ -235,6 +238,25 @@ class Invoice extends BaseModel public const STATUS_UNPAID = -2; //status < 4 || < 3 && !is_deleted && !trashed() + public function toSearchableArray() + { + return [ + 'name' => $this->client->present()->name() . ' - ' . $this->number, + 'hashed_id' => $this->hashed_id, + 'number' => $this->number, + 'is_deleted' => $this->is_deleted, + 'amount' => (float) $this->amount, + 'balance' => (float) $this->balance, + 'due_date' => $this->due_date, + 'date' => $this->date, + 'custom_value1' => $this->custom_value1, + 'custom_value2' => $this->custom_value2, + 'custom_value3' => $this->custom_value3, + 'custom_value4' => $this->custom_value4, + 'company_key' => $this->company->company_key, + ]; + } + public function getEntityType() { return self::class; diff --git a/app/Services/Chart/ChartCalculations.php b/app/Services/Chart/ChartCalculations.php index 88a817c5bc1c..9db8feefc2f4 100644 --- a/app/Services/Chart/ChartCalculations.php +++ b/app/Services/Chart/ChartCalculations.php @@ -141,8 +141,8 @@ trait ChartCalculations } match ($data['calculation']) { - 'sum' => $result = $q->sum('refunded'), - 'avg' => $result = $q->avg('refunded'), + 'sum' => $result = $q->sum('amount'), + 'avg' => $result = $q->avg('amount'), 'count' => $result = $q->count(), default => $result = 0, }; @@ -287,14 +287,14 @@ trait ChartCalculations return $query->get() ->when($data['currency_id'] == '999', function ($collection) { - $collection->map(function ($e) { + return $collection->map(function ($e) { /** @var \App\Models\Expense $e */ return $e->amount * $e->exchange_rate; }); }) ->when($data['currency_id'] != '999', function ($collection) { - $collection->map(function ($e) { + return $collection->map(function ($e) { /** @var \App\Models\Expense $e */ return $e->amount; diff --git a/app/Transformers/InvoiceTransformer.php b/app/Transformers/InvoiceTransformer.php index 415f6f5b7092..c619414a7b98 100644 --- a/app/Transformers/InvoiceTransformer.php +++ b/app/Transformers/InvoiceTransformer.php @@ -166,6 +166,10 @@ class InvoiceTransformer extends EntityTransformer $data['reminder_schedule'] = (string) $invoice->reminderSchedule(); } + if (request()->has('is_locked') && request()->query('is_locked') == 'true') { + $data['is_locked'] = (bool) $invoice->isLocked(); + } + return $data; } diff --git a/composer.json b/composer.json index f67661f328ed..d188feead9a7 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "authorizenet/authorizenet": "^2.0", "awobaz/compoships": "^2.1", "aws/aws-sdk-php": "^3.319", + "babenkoivan/elastic-scout-driver": "^4.0", "bacon/bacon-qr-code": "^2.0", "beganovich/snappdf": "dev-master", "braintree/braintree_php": "^6.0", @@ -68,6 +69,7 @@ "josemmo/facturae-php": "^1.7", "laracasts/presenter": "^0.2.1", "laravel/framework": "^11.0", + "laravel/scout": "^10.11", "laravel/slack-notification-channel": "^3", "laravel/socialite": "^5", "laravel/tinker": "^2.7", diff --git a/composer.lock b/composer.lock index 3646644d1739..4f9990829af6 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": "a1fac503f165997d7aa339239e002230", + "content-hash": "79894c80128f463131a8bf601f5fbfcb", "packages": [ { "name": "adrienrn/php-mimetyper", @@ -631,6 +631,217 @@ }, "time": "2024-09-13T18:05:10+00:00" }, + { + "name": "babenkoivan/elastic-adapter", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/babenkoivan/elastic-adapter.git", + "reference": "3a1fbb2d30c0b9e84c50204c1406cb7e44e6d2a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/babenkoivan/elastic-adapter/zipball/3a1fbb2d30c0b9e84c50204c1406cb7e44e6d2a1", + "reference": "3a1fbb2d30c0b9e84c50204c1406cb7e44e6d2a1", + "shasum": "" + }, + "require": { + "babenkoivan/elastic-client": "^3.0", + "php": "^8.2" + }, + "require-dev": { + "dg/bypass-finals": "^1.7", + "friendsofphp/php-cs-fixer": "^3.14", + "orchestra/testbench": "^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Elastic\\Adapter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ivan Babenko", + "email": "babenko.i.a@gmail.com" + } + ], + "description": "Adapter for official PHP Elasticsearch client", + "keywords": [ + "adapter", + "client", + "elastic", + "elasticsearch", + "php" + ], + "support": { + "issues": "https://github.com/babenkoivan/elastic-adapter/issues", + "source": "https://github.com/babenkoivan/elastic-adapter/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://ko-fi.com/ivanbabenko", + "type": "ko-fi" + }, + { + "url": "https://paypal.me/babenkoi", + "type": "paypal" + } + ], + "time": "2024-06-18T06:57:10+00:00" + }, + { + "name": "babenkoivan/elastic-client", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/babenkoivan/elastic-client.git", + "reference": "65f4a4c9dc3b5f6ba4e68e59ad26a68393aae995" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/babenkoivan/elastic-client/zipball/65f4a4c9dc3b5f6ba4e68e59ad26a68393aae995", + "reference": "65f4a4c9dc3b5f6ba4e68e59ad26a68393aae995", + "shasum": "" + }, + "require": { + "elasticsearch/elasticsearch": "^8.0", + "php": "^8.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.14", + "orchestra/testbench": "^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Elastic\\Client\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Elastic\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ivan Babenko", + "email": "babenko.i.a@gmail.com" + } + ], + "description": "The official PHP Elasticsearch client integrated with Laravel", + "keywords": [ + "client", + "elastic", + "elasticsearch", + "laravel", + "php" + ], + "support": { + "issues": "https://github.com/babenkoivan/elastic-client/issues", + "source": "https://github.com/babenkoivan/elastic-client/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://ko-fi.com/ivanbabenko", + "type": "ko-fi" + }, + { + "url": "https://paypal.me/babenkoi", + "type": "paypal" + } + ], + "time": "2024-06-18T06:53:01+00:00" + }, + { + "name": "babenkoivan/elastic-scout-driver", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/babenkoivan/elastic-scout-driver.git", + "reference": "f3791521fb3216850335f491a1461a16738125cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/babenkoivan/elastic-scout-driver/zipball/f3791521fb3216850335f491a1461a16738125cd", + "reference": "f3791521fb3216850335f491a1461a16738125cd", + "shasum": "" + }, + "require": { + "babenkoivan/elastic-adapter": "^4.0", + "php": "^8.2" + }, + "require-dev": { + "babenkoivan/elastic-migrations": "^4.0", + "friendsofphp/php-cs-fixer": "^3.14", + "laravel/legacy-factories": "^1.3", + "laravel/scout": "^10.0", + "orchestra/testbench": "^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Elastic\\ScoutDriver\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Elastic\\ScoutDriver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ivan Babenko", + "email": "babenko.i.a@gmail.com" + } + ], + "description": "Elasticsearch driver for Laravel Scout", + "keywords": [ + "driver", + "elastic", + "elasticsearch", + "laravel", + "php", + "scout" + ], + "support": { + "issues": "https://github.com/babenkoivan/elastic-scout-driver/issues", + "source": "https://github.com/babenkoivan/elastic-scout-driver/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://ko-fi.com/ivanbabenko", + "type": "ko-fi" + }, + { + "url": "https://paypal.me/babenkoi", + "type": "paypal" + } + ], + "time": "2024-06-18T07:06:48+00:00" + }, { "name": "bacon/bacon-qr-code", "version": "2.0.8", @@ -1950,6 +2161,122 @@ ], "time": "2023-10-06T06:47:41+00:00" }, + { + "name": "elastic/transport", + "version": "v8.10.0", + "source": { + "type": "git", + "url": "https://github.com/elastic/elastic-transport-php.git", + "reference": "8be37d679637545e50b1cea9f8ee903888783021" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/elastic/elastic-transport-php/zipball/8be37d679637545e50b1cea9f8ee903888783021", + "reference": "8be37d679637545e50b1cea9f8ee903888783021", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "open-telemetry/api": "^1.0", + "php": "^7.4 || ^8.0", + "php-http/discovery": "^1.14", + "php-http/httplug": "^2.3", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "nyholm/psr7": "^1.5", + "open-telemetry/sdk": "^1.0", + "php-http/mock-client": "^1.5", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5", + "symfony/http-client": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Elastic\\Transport\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "HTTP transport PHP library for Elastic products", + "keywords": [ + "PSR_17", + "elastic", + "http", + "psr-18", + "psr-7", + "transport" + ], + "support": { + "issues": "https://github.com/elastic/elastic-transport-php/issues", + "source": "https://github.com/elastic/elastic-transport-php/tree/v8.10.0" + }, + "time": "2024-08-14T08:55:07+00:00" + }, + { + "name": "elasticsearch/elasticsearch", + "version": "v8.15.0", + "source": { + "type": "git", + "url": "https://github.com/elastic/elasticsearch-php.git", + "reference": "34c2444fa8d4c3e6c8b009bd8dea90bca007203b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/34c2444fa8d4c3e6c8b009bd8dea90bca007203b", + "reference": "34c2444fa8d4c3e6c8b009bd8dea90bca007203b", + "shasum": "" + }, + "require": { + "elastic/transport": "^8.10", + "guzzlehttp/guzzle": "^7.0", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "ext-yaml": "*", + "ext-zip": "*", + "mockery/mockery": "^1.5", + "nyholm/psr7": "^1.5", + "php-http/message-factory": "^1.0", + "php-http/mock-client": "^1.5", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5", + "psr/http-factory": "^1.0", + "symfony/finder": "~4.0", + "symfony/http-client": "^5.0|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Elastic\\Elasticsearch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP Client for Elasticsearch", + "keywords": [ + "client", + "elastic", + "elasticsearch", + "search" + ], + "support": { + "issues": "https://github.com/elastic/elasticsearch-php/issues", + "source": "https://github.com/elastic/elasticsearch-php/tree/v8.15.0" + }, + "time": "2024-08-14T14:32:50+00:00" + }, { "name": "endroid/qr-code", "version": "5.0.7", @@ -4820,6 +5147,84 @@ }, "time": "2024-08-12T22:06:33+00:00" }, + { + "name": "laravel/scout", + "version": "v10.11.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/scout.git", + "reference": "642b4750127b5242a089571c9314037a7453cc0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/scout/zipball/642b4750127b5242a089571c9314037a7453cc0a", + "reference": "642b4750127b5242a089571c9314037a7453cc0a", + "shasum": "" + }, + "require": { + "illuminate/bus": "^9.0|^10.0|^11.0", + "illuminate/contracts": "^9.0|^10.0|^11.0", + "illuminate/database": "^9.0|^10.0|^11.0", + "illuminate/http": "^9.0|^10.0|^11.0", + "illuminate/pagination": "^9.0|^10.0|^11.0", + "illuminate/queue": "^9.0|^10.0|^11.0", + "illuminate/support": "^9.0|^10.0|^11.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "algolia/algoliasearch-client-php": "^3.2", + "meilisearch/meilisearch-php": "^1.0", + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.31|^8.11|^9.0", + "php-http/guzzle7-adapter": "^1.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3|^10.4", + "typesense/typesense-php": "^4.9.3" + }, + "suggest": { + "algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).", + "meilisearch/meilisearch-php": "Required to use the Meilisearch engine (^1.0).", + "typesense/typesense-php": "Required to use the Typesense engine (^4.9)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Scout\\ScoutServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Scout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Scout provides a driver based solution to searching your Eloquent models.", + "keywords": [ + "algolia", + "laravel", + "search" + ], + "support": { + "issues": "https://github.com/laravel/scout/issues", + "source": "https://github.com/laravel/scout" + }, + "time": "2024-09-11T21:32:42+00:00" + }, { "name": "laravel/serializable-closure", "version": "v1.3.4", @@ -7633,6 +8038,134 @@ ], "time": "2024-09-09T07:06:30+00:00" }, + { + "name": "open-telemetry/api", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/api.git", + "reference": "87de95d926f46262885d0d390060c095af13e2e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/87de95d926f46262885d0d390060c095af13e2e5", + "reference": "87de95d926f46262885d0d390060c095af13e2e5", + "shasum": "" + }, + "require": { + "open-telemetry/context": "^1.0", + "php": "^7.4 || ^8.0", + "psr/log": "^1.1|^2.0|^3.0", + "symfony/polyfill-php80": "^1.26", + "symfony/polyfill-php81": "^1.26", + "symfony/polyfill-php82": "^1.26" + }, + "conflict": { + "open-telemetry/sdk": "<=1.0.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "Trace/functions.php" + ], + "psr-4": { + "OpenTelemetry\\API\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "API for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "api", + "apm", + "logging", + "opentelemetry", + "otel", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-02-06T01:32:25+00:00" + }, + { + "name": "open-telemetry/context", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/context.git", + "reference": "e9d254a7c89885e63fd2fde54e31e81aaaf52b7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/e9d254a7c89885e63fd2fde54e31e81aaaf52b7c", + "reference": "e9d254a7c89885e63fd2fde54e31e81aaaf52b7c", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.26", + "symfony/polyfill-php81": "^1.26", + "symfony/polyfill-php82": "^1.26" + }, + "suggest": { + "ext-ffi": "To allow context switching in Fibers" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "fiber/initialize_fiber_handler.php" + ], + "psr-4": { + "OpenTelemetry\\Context\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Context implementation for OpenTelemetry PHP.", + "keywords": [ + "Context", + "opentelemetry", + "otel" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-01-13T05:50:44+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v3.0.0", @@ -13134,6 +13667,82 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php82", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "5d2ed36f7734637dacc025f179698031951b1692" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "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.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/polyfill-php83", "version": "v1.31.0", diff --git a/config/elastic.client.php b/config/elastic.client.php new file mode 100644 index 000000000000..77b5b0e30663 --- /dev/null +++ b/config/elastic.client.php @@ -0,0 +1,24 @@ + env('ELASTIC_CONNECTION', 'default'), + 'connections' => [ + 'default' => [ + 'hosts' => [ + env('ELASTIC_HOST'), + ], + // configure basic authentication + 'basicAuthentication' => [ + env('ELASTIC_USERNAME'), + env('ELASTIC_PASSWORD'), + ], + // configure HTTP client (Guzzle by default) + 'httpClientOptions' => [ + 'timeout' => 2, + 'verify_host' => false, // Disable SSL verification + 'verify_peer' => false, + + ], + ], + ], +]; diff --git a/config/elastic.scout_driver.php b/config/elastic.scout_driver.php new file mode 100644 index 000000000000..a5d123aa8de4 --- /dev/null +++ b/config/elastic.scout_driver.php @@ -0,0 +1,5 @@ + env('ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS', false), +]; diff --git a/config/scout.php b/config/scout.php new file mode 100644 index 000000000000..a65020a46701 --- /dev/null +++ b/config/scout.php @@ -0,0 +1,202 @@ + env('SCOUT_DRIVER', 'algolia'), + + /* + |-------------------------------------------------------------------------- + | Index Prefix + |-------------------------------------------------------------------------- + | + | Here you may specify a prefix that will be applied to all search index + | names used by Scout. This prefix may be useful if you have multiple + | "tenants" or applications sharing the same search infrastructure. + | + */ + + 'prefix' => env('SCOUT_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Queue Data Syncing + |-------------------------------------------------------------------------- + | + | This option allows you to control if the operations that sync your data + | with your search engines are queued. When this is set to "true" then + | all automatic data syncing will get queued for better performance. + | + */ + + 'queue' => env('SCOUT_QUEUE', true), + + /* + |-------------------------------------------------------------------------- + | Database Transactions + |-------------------------------------------------------------------------- + | + | This configuration option determines if your data will only be synced + | with your search indexes after every open database transaction has + | been committed, thus preventing any discarded data from syncing. + | + */ + + 'after_commit' => false, + + /* + |-------------------------------------------------------------------------- + | Chunk Sizes + |-------------------------------------------------------------------------- + | + | These options allow you to control the maximum chunk size when you are + | mass importing data into the search engine. This allows you to fine + | tune each of these chunk sizes based on the power of the servers. + | + */ + + 'chunk' => [ + 'searchable' => 500, + 'unsearchable' => 500, + ], + + /* + |-------------------------------------------------------------------------- + | Soft Deletes + |-------------------------------------------------------------------------- + | + | This option allows to control whether to keep soft deleted records in + | the search indexes. Maintaining soft deleted records can be useful + | if your application still needs to search for the records later. + | + */ + + 'soft_delete' => true, + + /* + |-------------------------------------------------------------------------- + | Identify User + |-------------------------------------------------------------------------- + | + | This option allows you to control whether to notify the search engine + | of the user performing the search. This is sometimes useful if the + | engine supports any analytics based on this application's users. + | + | Supported engines: "algolia" + | + */ + + 'identify' => env('SCOUT_IDENTIFY', false), + + /* + |-------------------------------------------------------------------------- + | Algolia Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Algolia settings. Algolia is a cloud hosted + | search engine which works great with Scout out of the box. Just plug + | in your application ID and admin API key to get started searching. + | + */ + + 'algolia' => [ + 'id' => env('ALGOLIA_APP_ID', ''), + 'secret' => env('ALGOLIA_SECRET', ''), + ], + + /* + |-------------------------------------------------------------------------- + | Meilisearch Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Meilisearch settings. Meilisearch is an open + | source search engine with minimal configuration. Below, you can state + | the host and key information for your own Meilisearch installation. + | + | See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options + | + */ + + 'meilisearch' => [ + 'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'), + 'key' => env('MEILISEARCH_KEY'), + 'index-settings' => [ + // 'users' => [ + // 'filterableAttributes'=> ['id', 'name', 'email'], + // ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Typesense Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Typesense settings. Typesense is an open + | source search engine using minimal configuration. Below, you will + | state the host, key, and schema configuration for the instance. + | + */ + + 'typesense' => [ + 'client-settings' => [ + 'api_key' => env('TYPESENSE_API_KEY', 'xyz'), + 'nodes' => [ + [ + 'host' => env('TYPESENSE_HOST', 'localhost'), + 'port' => env('TYPESENSE_PORT', '8108'), + 'path' => env('TYPESENSE_PATH', ''), + 'protocol' => env('TYPESENSE_PROTOCOL', 'http'), + ], + ], + 'nearest_node' => [ + 'host' => env('TYPESENSE_HOST', 'localhost'), + 'port' => env('TYPESENSE_PORT', '8108'), + 'path' => env('TYPESENSE_PATH', ''), + 'protocol' => env('TYPESENSE_PROTOCOL', 'http'), + ], + 'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2), + 'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30), + 'num_retries' => env('TYPESENSE_NUM_RETRIES', 3), + 'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1), + ], + 'model-settings' => [ + // User::class => [ + // 'collection-schema' => [ + // 'fields' => [ + // [ + // 'name' => 'id', + // 'type' => 'string', + // ], + // [ + // 'name' => 'name', + // 'type' => 'string', + // ], + // [ + // 'name' => 'created_at', + // 'type' => 'int64', + // ], + // ], + // 'default_sorting_field' => 'created_at', + // ], + // 'search-parameters' => [ + // 'query_by' => 'name' + // ], + // ], + ], + ], + +]; From 76dcc61a5e646c6bac4d2fdf6e49699f64d560e1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 22 Sep 2024 14:54:39 +1000 Subject: [PATCH 02/55] Updated lock --- composer.lock | 483 ++++++++++++++++++++++---------------------------- 1 file changed, 212 insertions(+), 271 deletions(-) diff --git a/composer.lock b/composer.lock index 4f9990829af6..737ab9f6630d 100644 --- a/composer.lock +++ b/composer.lock @@ -376,16 +376,16 @@ }, { "name": "authorizenet/authorizenet", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/AuthorizeNet/sdk-php.git", - "reference": "e1acf55c9cb22bef1852b1e494502973ade11cce" + "reference": "8555cc245953dd0ac57f7ea424a5572eae4c7191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AuthorizeNet/sdk-php/zipball/e1acf55c9cb22bef1852b1e494502973ade11cce", - "reference": "e1acf55c9cb22bef1852b1e494502973ade11cce", + "url": "https://api.github.com/repos/AuthorizeNet/sdk-php/zipball/8555cc245953dd0ac57f7ea424a5572eae4c7191", + "reference": "8555cc245953dd0ac57f7ea424a5572eae4c7191", "shasum": "" }, "require": { @@ -413,9 +413,9 @@ ], "support": { "issues": "https://github.com/AuthorizeNet/sdk-php/issues", - "source": "https://github.com/AuthorizeNet/sdk-php/tree/2.0.3" + "source": "https://github.com/AuthorizeNet/sdk-php/tree/2.0.4" }, - "time": "2024-05-29T17:33:13+00:00" + "time": "2024-09-18T06:23:52+00:00" }, { "name": "awobaz/compoships", @@ -535,16 +535,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.321.11", + "version": "3.322.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "bbd357d246350ffcd0dd8df30951d2d46c5ddadb" + "reference": "6a329cf111a4e54f2ca0e87ce07dd0b9e0befdad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bbd357d246350ffcd0dd8df30951d2d46c5ddadb", - "reference": "bbd357d246350ffcd0dd8df30951d2d46c5ddadb", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6a329cf111a4e54f2ca0e87ce07dd0b9e0befdad", + "reference": "6a329cf111a4e54f2ca0e87ce07dd0b9e0befdad", "shasum": "" }, "require": { @@ -627,9 +627,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.321.11" + "source": "https://github.com/aws/aws-sdk-php/tree/3.322.2" }, - "time": "2024-09-13T18:05:10+00:00" + "time": "2024-09-20T18:08:53+00:00" }, { "name": "babenkoivan/elastic-adapter", @@ -954,16 +954,16 @@ }, { "name": "braintree/braintree_php", - "version": "6.19.0", + "version": "6.20.0", "source": { "type": "git", "url": "https://github.com/braintree/braintree_php.git", - "reference": "f3178632ca098d1f96a429d665aabc4e95346c03" + "reference": "f46d6d570a955561f9474e0c4e5c56ca4cc22c49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/braintree/braintree_php/zipball/f3178632ca098d1f96a429d665aabc4e95346c03", - "reference": "f3178632ca098d1f96a429d665aabc4e95346c03", + "url": "https://api.github.com/repos/braintree/braintree_php/zipball/f46d6d570a955561f9474e0c4e5c56ca4cc22c49", + "reference": "f46d6d570a955561f9474e0c4e5c56ca4cc22c49", "shasum": "" }, "require": { @@ -997,9 +997,9 @@ "description": "Braintree PHP Client Library", "support": { "issues": "https://github.com/braintree/braintree_php/issues", - "source": "https://github.com/braintree/braintree_php/tree/6.19.0" + "source": "https://github.com/braintree/braintree_php/tree/6.20.0" }, - "time": "2024-07-23T20:09:58+00:00" + "time": "2024-09-19T21:28:15+00:00" }, { "name": "brick/math", @@ -1063,16 +1063,16 @@ }, { "name": "btcpayserver/btcpayserver-greenfield-php", - "version": "v2.7.0", + "version": "v2.7.1", "source": { "type": "git", "url": "https://github.com/btcpayserver/btcpayserver-greenfield-php.git", - "reference": "5e2ba7e3f585fc8e6dc068e22a0efbfdacd9c992" + "reference": "28197bf65fd4a0ba39598fac5651fec4a805b78c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/5e2ba7e3f585fc8e6dc068e22a0efbfdacd9c992", - "reference": "5e2ba7e3f585fc8e6dc068e22a0efbfdacd9c992", + "url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/28197bf65fd4a0ba39598fac5651fec4a805b78c", + "reference": "28197bf65fd4a0ba39598fac5651fec4a805b78c", "shasum": "" }, "require": { @@ -1111,9 +1111,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.7.0" + "source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v2.7.1" }, - "time": "2024-09-13T14:54:13+00:00" + "time": "2024-09-16T21:02:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -6224,16 +6224,16 @@ }, { "name": "league/mime-type-detection", - "version": "1.15.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", "shasum": "" }, "require": { @@ -6264,7 +6264,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" }, "funding": [ { @@ -6276,7 +6276,7 @@ "type": "tidelift" } ], - "time": "2024-01-28T23:22:08+00:00" + "time": "2024-09-21T08:32:55+00:00" }, { "name": "league/oauth1-client", @@ -6356,16 +6356,16 @@ }, { "name": "livewire/livewire", - "version": "v3.5.6", + "version": "v3.5.8", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "597a2808d8d3001cc3ed5ce89a6ebab00f83b80f" + "reference": "ce1ce71b39a3492b98f7d2f2a4583f1b163fe6ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/597a2808d8d3001cc3ed5ce89a6ebab00f83b80f", - "reference": "597a2808d8d3001cc3ed5ce89a6ebab00f83b80f", + "url": "https://api.github.com/repos/livewire/livewire/zipball/ce1ce71b39a3492b98f7d2f2a4583f1b163fe6ae", + "reference": "ce1ce71b39a3492b98f7d2f2a4583f1b163fe6ae", "shasum": "" }, "require": { @@ -6420,7 +6420,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.5.6" + "source": "https://github.com/livewire/livewire/tree/v3.5.8" }, "funding": [ { @@ -6428,7 +6428,7 @@ "type": "github" } ], - "time": "2024-08-19T11:52:18+00:00" + "time": "2024-09-20T19:41:19+00:00" }, { "name": "maennchen/zipstream-php", @@ -6800,16 +6800,16 @@ }, { "name": "mindee/mindee", - "version": "v1.10.0", + "version": "v1.11.1", "source": { "type": "git", "url": "https://github.com/mindee/mindee-api-php.git", - "reference": "40865a03e34bb2416b32e5e1dd4937020e7bcc27" + "reference": "1ffbbdab646202f6b9547d12399841feba75c68e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mindee/mindee-api-php/zipball/40865a03e34bb2416b32e5e1dd4937020e7bcc27", - "reference": "40865a03e34bb2416b32e5e1dd4937020e7bcc27", + "url": "https://api.github.com/repos/mindee/mindee-api-php/zipball/1ffbbdab646202f6b9547d12399841feba75c68e", + "reference": "1ffbbdab646202f6b9547d12399841feba75c68e", "shasum": "" }, "require": { @@ -6819,7 +6819,6 @@ "php": ">=7.4", "setasign/fpdf": "^1.8", "setasign/fpdi": "^2.6", - "spatie/pdf-to-image": "^1.2", "symfony/console": ">=5.4" }, "require-dev": { @@ -6849,9 +6848,9 @@ "description": "Mindee Client Library for PHP", "support": { "issues": "https://github.com/mindee/mindee-api-php/issues", - "source": "https://github.com/mindee/mindee-api-php/tree/v1.10.0" + "source": "https://github.com/mindee/mindee-api-php/tree/v1.11.1" }, - "time": "2024-09-04T15:40:29+00:00" + "time": "2024-09-20T14:46:42+00:00" }, { "name": "mollie/mollie-api-php", @@ -7875,16 +7874,16 @@ }, { "name": "nwidart/laravel-modules", - "version": "v11.1.0", + "version": "v11.1.2", "source": { "type": "git", "url": "https://github.com/nWidart/laravel-modules.git", - "reference": "2ae13812f055a85d7063e90366884cd327877821" + "reference": "d275a5b9f7c329c505480750d354a7eef69fc42a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/2ae13812f055a85d7063e90366884cd327877821", - "reference": "2ae13812f055a85d7063e90366884cd327877821", + "url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/d275a5b9f7c329c505480750d354a7eef69fc42a", + "reference": "d275a5b9f7c329c505480750d354a7eef69fc42a", "shasum": "" }, "require": { @@ -7946,7 +7945,7 @@ ], "support": { "issues": "https://github.com/nWidart/laravel-modules/issues", - "source": "https://github.com/nWidart/laravel-modules/tree/v11.1.0" + "source": "https://github.com/nWidart/laravel-modules/tree/v11.1.2" }, "funding": [ { @@ -7958,7 +7957,7 @@ "type": "github" } ], - "time": "2024-09-13T19:24:08+00:00" + "time": "2024-09-20T08:45:18+00:00" }, { "name": "nyholm/psr7", @@ -10850,16 +10849,16 @@ }, { "name": "sentry/sentry-laravel", - "version": "4.8.0", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "2bbcb7e81097993cf64d5b296eaa6d396cddd5a7" + "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/2bbcb7e81097993cf64d5b296eaa6d396cddd5a7", - "reference": "2bbcb7e81097993cf64d5b296eaa6d396cddd5a7", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/73078e1f26d57f7a10e3bee2a2f543a02f6493c3", + "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3", "shasum": "" }, "require": { @@ -10923,7 +10922,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.8.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.9.0" }, "funding": [ { @@ -10935,7 +10934,7 @@ "type": "custom" } ], - "time": "2024-08-15T19:03:01+00:00" + "time": "2024-09-19T12:58:53+00:00" }, { "name": "setasign/fpdf", @@ -11305,66 +11304,6 @@ }, "time": "2024-07-12T02:43:55+00:00" }, - { - "name": "spatie/pdf-to-image", - "version": "1.2.2", - "source": { - "type": "git", - "url": "https://github.com/spatie/pdf-to-image.git", - "reference": "9a5cb264a99e87e010c65d4ece03b51f821d55bd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/pdf-to-image/zipball/9a5cb264a99e87e010c65d4ece03b51f821d55bd", - "reference": "9a5cb264a99e87e010c65d4ece03b51f821d55bd", - "shasum": "" - }, - "require": { - "php": ">=5.5.0" - }, - "require-dev": { - "phpunit/phpunit": "4.*", - "scrutinizer/ocular": "~1.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\PdfToImage\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Convert a pdf to an image", - "homepage": "https://github.com/spatie/pdf-to-image", - "keywords": [ - "convert", - "image", - "pdf", - "pdf-to-image", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/pdf-to-image/issues", - "source": "https://github.com/spatie/pdf-to-image/tree/1.2.2" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2016-12-14T15:37:00+00:00" - }, { "name": "sprain/swiss-qr-bill", "version": "v4.14", @@ -11694,16 +11633,16 @@ }, { "name": "symfony/console", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", "shasum": "" }, "require": { @@ -11767,7 +11706,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.4" + "source": "https://github.com/symfony/console/tree/v7.1.5" }, "funding": [ { @@ -11783,7 +11722,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:48:53+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/css-selector", @@ -12150,16 +12089,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.1.2", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "92a91985250c251de9b947a14bb2c9390b1a562c" + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c", - "reference": "92a91985250c251de9b947a14bb2c9390b1a562c", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a", + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a", "shasum": "" }, "require": { @@ -12196,7 +12135,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.1.2" + "source": "https://github.com/symfony/filesystem/tree/v7.1.5" }, "funding": [ { @@ -12212,7 +12151,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T10:03:55+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/finder", @@ -12280,16 +12219,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.11", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65" + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4c92046bb788648ff1098cc66da69aa7eac8cb65", - "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65", + "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", "shasum": "" }, "require": { @@ -12353,7 +12292,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.11" + "source": "https://github.com/symfony/http-client/tree/v6.4.12" }, "funding": [ { @@ -12369,7 +12308,7 @@ "type": "tidelift" } ], - "time": "2024-08-26T06:30:21+00:00" + "time": "2024-09-20T08:21:33+00:00" }, { "name": "symfony/http-client-contracts", @@ -12451,16 +12390,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a" + "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f602d5c17d1fa02f8019ace2687d9d136b7f4a1a", - "reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b", + "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b", "shasum": "" }, "require": { @@ -12508,7 +12447,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.1.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.5" }, "funding": [ { @@ -12524,20 +12463,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6efcbd1b3f444f631c386504fc83eeca25963747" + "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6efcbd1b3f444f631c386504fc83eeca25963747", - "reference": "6efcbd1b3f444f631c386504fc83eeca25963747", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/44204d96150a9df1fc57601ec933d23fefc2d65b", + "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b", "shasum": "" }, "require": { @@ -12622,7 +12561,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.1.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.1.5" }, "funding": [ { @@ -12638,20 +12577,20 @@ "type": "tidelift" } ], - "time": "2024-08-30T17:02:28+00:00" + "time": "2024-09-21T06:09:21+00:00" }, { "name": "symfony/intl", - "version": "v7.1.1", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c" + "reference": "a0ba7a400e4c915500762c998355bea219a32d6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c", - "reference": "66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c", + "url": "https://api.github.com/repos/symfony/intl/zipball/a0ba7a400e4c915500762c998355bea219a32d6b", + "reference": "a0ba7a400e4c915500762c998355bea219a32d6b", "shasum": "" }, "require": { @@ -12708,7 +12647,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v7.1.1" + "source": "https://github.com/symfony/intl/tree/v7.1.5" }, "funding": [ { @@ -12724,20 +12663,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/mailer", - "version": "v7.1.2", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee" + "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/8fcff0af9043c8f8a8e229437cea363e282f9aee", - "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee", + "url": "https://api.github.com/repos/symfony/mailer/zipball/bbf21460c56f29810da3df3e206e38dfbb01e80b", + "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b", "shasum": "" }, "require": { @@ -12788,7 +12727,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.1.2" + "source": "https://github.com/symfony/mailer/tree/v7.1.5" }, "funding": [ { @@ -12804,7 +12743,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T08:00:31+00:00" + "time": "2024-09-08T12:32:26+00:00" }, { "name": "symfony/mailgun-mailer", @@ -12877,16 +12816,16 @@ }, { "name": "symfony/mime", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "ccaa6c2503db867f472a587291e764d6a1e58758" + "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/ccaa6c2503db867f472a587291e764d6a1e58758", - "reference": "ccaa6c2503db867f472a587291e764d6a1e58758", + "url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff", + "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff", "shasum": "" }, "require": { @@ -12941,7 +12880,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.1.4" + "source": "https://github.com/symfony/mime/tree/v7.1.5" }, "funding": [ { @@ -12957,7 +12896,7 @@ "type": "tidelift" } ], - "time": "2024-08-13T14:28:19+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/options-resolver", @@ -13970,16 +13909,16 @@ }, { "name": "symfony/process", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" + "reference": "5c03ee6369281177f07f7c68252a280beccba847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", "shasum": "" }, "require": { @@ -14011,7 +13950,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.3" + "source": "https://github.com/symfony/process/tree/v7.1.5" }, "funding": [ { @@ -14027,7 +13966,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:44:47+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/property-access", @@ -14355,16 +14294,16 @@ }, { "name": "symfony/serializer", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "0158b0e91b7cf7e744a6fb9acaeb613d1ca40dbb" + "reference": "71d6e1f70f00752d1469d0f5e83b0a51716f288b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/0158b0e91b7cf7e744a6fb9acaeb613d1ca40dbb", - "reference": "0158b0e91b7cf7e744a6fb9acaeb613d1ca40dbb", + "url": "https://api.github.com/repos/symfony/serializer/zipball/71d6e1f70f00752d1469d0f5e83b0a51716f288b", + "reference": "71d6e1f70f00752d1469d0f5e83b0a51716f288b", "shasum": "" }, "require": { @@ -14378,12 +14317,14 @@ "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", + "symfony/type-info": "<7.1.5", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4" }, "require-dev": { "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "phpstan/phpdoc-parser": "^1.0", "seld/jsonlint": "^1.10", "symfony/cache": "^6.4|^7.0", "symfony/config": "^6.4|^7.0", @@ -14399,7 +14340,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.1", + "symfony/type-info": "^7.1.5", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", @@ -14432,7 +14373,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.1.4" + "source": "https://github.com/symfony/serializer/tree/v7.1.5" }, "funding": [ { @@ -14448,7 +14389,7 @@ "type": "tidelift" } ], - "time": "2024-08-22T09:39:57+00:00" + "time": "2024-09-20T12:13:15+00:00" }, { "name": "symfony/service-contracts", @@ -14535,16 +14476,16 @@ }, { "name": "symfony/string", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -14602,7 +14543,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.4" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -14618,20 +14559,20 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/translation", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1" + "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/8d5e50c813ba2859a6dfc99a0765c550507934a1", - "reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1", + "url": "https://api.github.com/repos/symfony/translation/zipball/235535e3f84f3dfbdbde0208ede6ca75c3a489ea", + "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea", "shasum": "" }, "require": { @@ -14696,7 +14637,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.1.3" + "source": "https://github.com/symfony/translation/tree/v7.1.5" }, "funding": [ { @@ -14712,7 +14653,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-09-16T06:30:38+00:00" }, { "name": "symfony/translation-contracts", @@ -14794,16 +14735,16 @@ }, { "name": "symfony/type-info", - "version": "v7.1.1", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "60b28eb733f1453287f1263ed305b96091e0d1dc" + "reference": "9f6094aa900d2c06bd61576a6f279d4ac441515f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/60b28eb733f1453287f1263ed305b96091e0d1dc", - "reference": "60b28eb733f1453287f1263ed305b96091e0d1dc", + "url": "https://api.github.com/repos/symfony/type-info/zipball/9f6094aa900d2c06bd61576a6f279d4ac441515f", + "reference": "9f6094aa900d2c06bd61576a6f279d4ac441515f", "shasum": "" }, "require": { @@ -14856,7 +14797,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.1.1" + "source": "https://github.com/symfony/type-info/tree/v7.1.5" }, "funding": [ { @@ -14872,20 +14813,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:59:31+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/uid", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "82177535395109075cdb45a70533aa3d7a521cdf" + "reference": "8c7bb8acb933964055215d89f9a9871df0239317" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/82177535395109075cdb45a70533aa3d7a521cdf", - "reference": "82177535395109075cdb45a70533aa3d7a521cdf", + "url": "https://api.github.com/repos/symfony/uid/zipball/8c7bb8acb933964055215d89f9a9871df0239317", + "reference": "8c7bb8acb933964055215d89f9a9871df0239317", "shasum": "" }, "require": { @@ -14930,7 +14871,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.1.4" + "source": "https://github.com/symfony/uid/tree/v7.1.5" }, "funding": [ { @@ -14946,20 +14887,20 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/validator", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "0d7e0dfd41702d6b9356214b76110421c1e74368" + "reference": "e57592782dc2a86997477f28164c51af53512ad8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/0d7e0dfd41702d6b9356214b76110421c1e74368", - "reference": "0d7e0dfd41702d6b9356214b76110421c1e74368", + "url": "https://api.github.com/repos/symfony/validator/zipball/e57592782dc2a86997477f28164c51af53512ad8", + "reference": "e57592782dc2a86997477f28164c51af53512ad8", "shasum": "" }, "require": { @@ -15027,7 +14968,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.1.4" + "source": "https://github.com/symfony/validator/tree/v7.1.5" }, "funding": [ { @@ -15043,20 +14984,20 @@ "type": "tidelift" } ], - "time": "2024-08-30T15:58:06+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa" + "reference": "e20e03889539fd4e4211e14d2179226c513c010d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a5fa7481b199090964d6fd5dab6294d5a870c7aa", - "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e20e03889539fd4e4211e14d2179226c513c010d", + "reference": "e20e03889539fd4e4211e14d2179226c513c010d", "shasum": "" }, "require": { @@ -15110,7 +15051,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.1.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.5" }, "funding": [ { @@ -15126,20 +15067,20 @@ "type": "tidelift" } ], - "time": "2024-08-30T16:12:47+00:00" + "time": "2024-09-16T10:07:02+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.11", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "be37e7f13195e05ab84ca5269365591edd240335" + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/be37e7f13195e05ab84ca5269365591edd240335", - "reference": "be37e7f13195e05ab84ca5269365591edd240335", + "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971", + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971", "shasum": "" }, "require": { @@ -15182,7 +15123,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.11" + "source": "https://github.com/symfony/yaml/tree/v6.4.12" }, "funding": [ { @@ -15198,7 +15139,7 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:55:28+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -15825,23 +15766,23 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.13.5", + "version": "v3.14.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07" + "reference": "16a13cc5221aee90ae20aa59083ced2211e714eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/92d86be45ee54edff735e46856f64f14b6a8bb07", - "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/16a13cc5221aee90ae20aa59083ced2211e714eb", + "reference": "16a13cc5221aee90ae20aa59083ced2211e714eb", "shasum": "" }, "require": { "illuminate/routing": "^9|^10|^11", "illuminate/session": "^9|^10|^11", "illuminate/support": "^9|^10|^11", - "maximebf/debugbar": "~1.22.0", + "maximebf/debugbar": "~1.23.0", "php": "^8.0", "symfony/finder": "^6|^7" }, @@ -15854,7 +15795,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.13-dev" + "dev-master": "3.14-dev" }, "laravel": { "providers": [ @@ -15893,7 +15834,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.13.5" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.0" }, "funding": [ { @@ -15905,7 +15846,7 @@ "type": "github" } ], - "time": "2024-04-12T11:20:37+00:00" + "time": "2024-09-20T12:16:37+00:00" }, { "name": "barryvdh/laravel-ide-helper", @@ -16055,16 +15996,16 @@ }, { "name": "brianium/paratest", - "version": "v7.5.4", + "version": "v7.5.5", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "c490591cc9c2f4830633b905547d30d5eb609c88" + "reference": "f29c7d671afc5c4e1140bd7b9f2749e827902a1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/c490591cc9c2f4830633b905547d30d5eb609c88", - "reference": "c490591cc9c2f4830633b905547d30d5eb609c88", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f29c7d671afc5c4e1140bd7b9f2749e827902a1e", + "reference": "f29c7d671afc5c4e1140bd7b9f2749e827902a1e", "shasum": "" }, "require": { @@ -16078,7 +16019,7 @@ "phpunit/php-code-coverage": "^11.0.6", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-timer": "^7.0.1", - "phpunit/phpunit": "^11.3.3", + "phpunit/phpunit": "^11.3.6", "sebastian/environment": "^7.2.0", "symfony/console": "^6.4.11 || ^7.1.4", "symfony/process": "^6.4.8 || ^7.1.3" @@ -16088,11 +16029,11 @@ "ext-pcov": "*", "ext-posix": "*", "infection/infection": "^0.29.6", - "phpstan/phpstan": "^1.12.1", - "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan": "^1.12.4", + "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-phpunit": "^1.4.0", "phpstan/phpstan-strict-rules": "^1.6.0", - "squizlabs/php_codesniffer": "^3.10.2", + "squizlabs/php_codesniffer": "^3.10.3", "symfony/filesystem": "^6.4.9 || ^7.1.2" }, "bin": [ @@ -16133,7 +16074,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.5.4" + "source": "https://github.com/paratestphp/paratest/tree/v7.5.5" }, "funding": [ { @@ -16145,7 +16086,7 @@ "type": "paypal" } ], - "time": "2024-09-04T21:15:27+00:00" + "time": "2024-09-20T12:57:46+00:00" }, { "name": "clue/ndjson-react", @@ -16365,24 +16306,24 @@ }, { "name": "composer/semver", - "version": "3.4.2", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -16426,7 +16367,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.2" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -16442,7 +16383,7 @@ "type": "tidelift" } ], - "time": "2024-07-12T11:35:52+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", @@ -17006,16 +16947,16 @@ }, { "name": "maximebf/debugbar", - "version": "v1.22.5", + "version": "v1.23.2", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989" + "reference": "689720d724c771ac4add859056744b7b3f2406da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1b5cabe0ce013134cf595bfa427bbf2f6abcd989", - "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/689720d724c771ac4add859056744b7b3f2406da", + "reference": "689720d724c771ac4add859056744b7b3f2406da", "shasum": "" }, "require": { @@ -17037,7 +16978,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.22-dev" + "dev-master": "1.23-dev" } }, "autoload": { @@ -17068,9 +17009,9 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.22.5" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.2" }, - "time": "2024-09-09T08:05:55+00:00" + "time": "2024-09-16T11:23:09+00:00" }, { "name": "mockery/mockery", @@ -17459,16 +17400,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.3", + "version": "1.12.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009" + "reference": "ffa517cb918591b93acc9b95c0bebdcd0e4538bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0fcbf194ab63d8159bb70d9aa3e1350051632009", - "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ffa517cb918591b93acc9b95c0bebdcd0e4538bd", + "reference": "ffa517cb918591b93acc9b95c0bebdcd0e4538bd", "shasum": "" }, "require": { @@ -17513,7 +17454,7 @@ "type": "github" } ], - "time": "2024-09-09T08:10:35+00:00" + "time": "2024-09-19T07:58:01+00:00" }, { "name": "phpunit/php-code-coverage", @@ -17840,16 +17781,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.3.5", + "version": "11.3.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4dc07a589a68f8f2d5132ac0849146d122e08347" + "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4dc07a589a68f8f2d5132ac0849146d122e08347", - "reference": "4dc07a589a68f8f2d5132ac0849146d122e08347", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d62c45a19c665bb872c2a47023a0baf41a98bb2b", + "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b", "shasum": "" }, "require": { @@ -17876,7 +17817,7 @@ "sebastian/exporter": "^6.1.3", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.0.1", + "sebastian/type": "^5.1.0", "sebastian/version": "^5.0.1" }, "suggest": { @@ -17920,7 +17861,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.3.5" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.6" }, "funding": [ { @@ -17936,7 +17877,7 @@ "type": "tidelift" } ], - "time": "2024-09-13T05:22:17+00:00" + "time": "2024-09-19T10:54:28+00:00" }, { "name": "react/cache", @@ -19282,28 +19223,28 @@ }, { "name": "sebastian/type", - "version": "5.0.1", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", - "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -19327,7 +19268,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" }, "funding": [ { @@ -19335,7 +19276,7 @@ "type": "github" } ], - "time": "2024-07-03T05:11:49+00:00" + "time": "2024-09-17T13:12:04+00:00" }, { "name": "sebastian/version", From 595cccc97d4ab0a5d3827c268daca12452829faf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 22 Sep 2024 18:17:24 +1000 Subject: [PATCH 03/55] fixes for switch --- app/Http/Controllers/SearchController.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index ca5cd688161b..253e815b00c7 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -100,8 +100,9 @@ class SearchController extends Controller switch($result['_index']) { case 'clients': - if($result['_source']['is_deleted']) - continue; + if($result['_source']['is_deleted']) //do not return deleted results + break; + $this->clients[] = [ 'name' => $result['_source']['name'], @@ -113,9 +114,9 @@ class SearchController extends Controller break; case 'invoices': - if ($result['_source']['is_deleted']) { - continue; - } + if ($result['_source']['is_deleted']) //do not return deleted invoices + break; + $this->invoices[] = [ 'name' => $result['_source']['name'], @@ -126,7 +127,7 @@ class SearchController extends Controller break; case 'client_contacts': - if($result['_source']['__soft_deleted']) + if($result['_source']['__soft_deleted']) // do not return deleted contacts continue; $this->client_contacts[] = [ From df52a4870111e5b107510a1a8626b6a24d769e42 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 22 Sep 2024 18:20:51 +1000 Subject: [PATCH 04/55] fixes for switch --- app/Http/Controllers/SearchController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 253e815b00c7..73e13f70d68d 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -128,7 +128,7 @@ class SearchController extends Controller case 'client_contacts': if($result['_source']['__soft_deleted']) // do not return deleted contacts - continue; + break; $this->client_contacts[] = [ 'name' => $result['_source']['name'], From 01a42bb7e2b9eb709f8809d916327dabe1c1bc0d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 22 Sep 2024 19:27:34 +1000 Subject: [PATCH 05/55] QB Sync --- app/Casts/ClientSyncCast.php | 41 +++++++++ app/Casts/InvoiceSyncCast.php | 41 +++++++++ app/Casts/ProductSyncCast.php | 41 +++++++++ app/DataMapper/ClientSync.php | 35 ++++++++ app/DataMapper/InvoiceSync.php | 35 ++++++++ app/DataMapper/ProductSync.php | 35 ++++++++ app/Models/Client.php | 3 + app/Models/Invoice.php | 4 + app/Models/Product.php | 9 ++ .../Quickbooks/Jobs/QuickbooksSync.php | 89 +++++++++---------- app/Services/Quickbooks/Models/QbInvoice.php | 30 +++++++ app/Services/Quickbooks/Models/QbProduct.php | 76 ++++++++++++++++ app/Services/Quickbooks/QuickbooksService.php | 27 ++++-- .../Transformers/BaseTransformer.php | 3 +- .../Transformers/InvoiceTransformer.php | 1 + .../Transformers/ProductTransformer.php | 2 +- ...4749_2024_09_23_add_sync_column_for_qb.php | 35 ++++++++ .../Import/Quickbooks/QuickbooksTest.php | 23 ++++- 18 files changed, 472 insertions(+), 58 deletions(-) create mode 100644 app/Casts/ClientSyncCast.php create mode 100644 app/Casts/InvoiceSyncCast.php create mode 100644 app/Casts/ProductSyncCast.php create mode 100644 app/DataMapper/ClientSync.php create mode 100644 app/DataMapper/InvoiceSync.php create mode 100644 app/DataMapper/ProductSync.php create mode 100644 app/Services/Quickbooks/Models/QbInvoice.php create mode 100644 app/Services/Quickbooks/Models/QbProduct.php create mode 100644 database/migrations/2024_09_22_084749_2024_09_23_add_sync_column_for_qb.php diff --git a/app/Casts/ClientSyncCast.php b/app/Casts/ClientSyncCast.php new file mode 100644 index 000000000000..736022b3053a --- /dev/null +++ b/app/Casts/ClientSyncCast.php @@ -0,0 +1,41 @@ +qb_id = $data['qb_id']; + + return $is; + } + + public function set($model, string $key, $value, array $attributes) + { + return [ + $key => json_encode([ + 'qb_id' => $value->qb_id, + ]) + ]; + } +} diff --git a/app/Casts/InvoiceSyncCast.php b/app/Casts/InvoiceSyncCast.php new file mode 100644 index 000000000000..8776637a709e --- /dev/null +++ b/app/Casts/InvoiceSyncCast.php @@ -0,0 +1,41 @@ +qb_id = $data['qb_id']; + + return $is; + } + + public function set($model, string $key, $value, array $attributes) + { + return [ + $key => json_encode([ + 'qb_id' => $value->qb_id, + ]) + ]; + } +} diff --git a/app/Casts/ProductSyncCast.php b/app/Casts/ProductSyncCast.php new file mode 100644 index 000000000000..ca2172c13b14 --- /dev/null +++ b/app/Casts/ProductSyncCast.php @@ -0,0 +1,41 @@ +qb_id = $data['qb_id']; + + return $ps; + } + + public function set($model, string $key, $value, array $attributes) + { + return [ + $key => json_encode([ + 'qb_id' => $value->qb_id, + ]) + ]; + } +} diff --git a/app/DataMapper/ClientSync.php b/app/DataMapper/ClientSync.php new file mode 100644 index 000000000000..56b2b8e185f9 --- /dev/null +++ b/app/DataMapper/ClientSync.php @@ -0,0 +1,35 @@ + $arguments + */ + public static function castUsing(array $arguments): string + { + return ClientSyncCast::class; + } + +} diff --git a/app/DataMapper/InvoiceSync.php b/app/DataMapper/InvoiceSync.php new file mode 100644 index 000000000000..90ecde71989a --- /dev/null +++ b/app/DataMapper/InvoiceSync.php @@ -0,0 +1,35 @@ + $arguments + */ + public static function castUsing(array $arguments): string + { + return InvoiceSyncCast::class; + } + +} diff --git a/app/DataMapper/ProductSync.php b/app/DataMapper/ProductSync.php new file mode 100644 index 000000000000..1417b0b0129b --- /dev/null +++ b/app/DataMapper/ProductSync.php @@ -0,0 +1,35 @@ + $arguments + */ + public static function castUsing(array $arguments): string + { + return ProductSyncCast::class; + } + +} diff --git a/app/Models/Client.php b/app/Models/Client.php index e8ab1dfab607..25f6aa512142 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -18,6 +18,7 @@ use App\Utils\Traits\MakesDates; use App\DataMapper\FeesAndLimits; use App\Models\Traits\Excludable; use App\DataMapper\ClientSettings; +use App\DataMapper\ClientSync; use App\DataMapper\CompanySettings; use App\Services\Client\ClientService; use App\Utils\Traits\GeneratesCounter; @@ -70,6 +71,7 @@ use Illuminate\Contracts\Translation\HasLocalePreference; * @property int|null $shipping_country_id * @property object|null $settings * @property object|null $group_settings + * @property object|null $sync * @property bool $is_deleted * @property int|null $group_settings_id * @property string|null $vat_number @@ -190,6 +192,7 @@ class Client extends BaseModel implements HasLocalePreference 'last_login' => 'timestamp', 'tax_data' => 'object', 'e_invoice' => 'object', + 'sync' => ClientSync::class, ]; protected $touches = []; diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 725b6dcd3892..750a88bd041f 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\DataMapper\InvoiceSync; use App\Utils\Ninja; use Laravel\Scout\Searchable; use Illuminate\Support\Carbon; @@ -53,6 +54,7 @@ use App\Events\Invoice\InvoiceReminderWasEmailed; * @property bool $is_deleted * @property object|array|string $line_items * @property object|null $backup + * @property object|null $sync * @property string|null $footer * @property string|null $public_notes * @property string|null $private_notes @@ -213,6 +215,8 @@ class Invoice extends BaseModel 'custom_surcharge_tax3' => 'bool', 'custom_surcharge_tax4' => 'bool', 'e_invoice' => 'object', + 'sync' => InvoiceSync::class, + ]; protected $with = []; diff --git a/app/Models/Product.php b/app/Models/Product.php index 52390a589e07..1a96b690df4e 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\DataMapper\ProductSync; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\SoftDeletes; use League\CommonMark\CommonMarkConverter; @@ -43,6 +44,7 @@ use League\CommonMark\CommonMarkConverter; * @property int|null $deleted_at * @property int|null $created_at * @property int|null $updated_at + * @property object|null $sync * @property bool $is_deleted * @property float $in_stock_quantity * @property bool $stock_notification @@ -100,6 +102,13 @@ class Product extends BaseModel 'tax_id', ]; + protected $casts = [ + 'updated_at' => 'timestamp', + 'created_at' => 'timestamp', + 'deleted_at' => 'timestamp', + 'sync' => ProductSync::class, + ]; + public array $ubl_tax_map = [ self::PRODUCT_TYPE_REVERSE_TAX => 'AE', // VAT_REVERSE_CHARGE = self::PRODUCT_TYPE_EXEMPT => 'E', // EXEMPT_FROM_TAX = diff --git a/app/Services/Quickbooks/Jobs/QuickbooksSync.php b/app/Services/Quickbooks/Jobs/QuickbooksSync.php index 603b2923963f..bff3ebd4d241 100644 --- a/app/Services/Quickbooks/Jobs/QuickbooksSync.php +++ b/app/Services/Quickbooks/Jobs/QuickbooksSync.php @@ -52,12 +52,12 @@ class QuickbooksSync implements ShouldQueue 'product' => 'Item', 'client' => 'Customer', 'invoice' => 'Invoice', - 'quote' => 'Estimate', - 'purchase_order' => 'PurchaseOrder', - 'payment' => 'Payment', + // 'quote' => 'Estimate', + // 'purchase_order' => 'PurchaseOrder', + // 'payment' => 'Payment', 'sales' => 'SalesReceipt', - 'vendor' => 'Vendor', - 'expense' => 'Purchase', + // 'vendor' => 'Vendor', + // 'expense' => 'Purchase', ]; private QuickbooksService $qbs; @@ -81,8 +81,8 @@ class QuickbooksSync implements ShouldQueue $this->qbs = new QuickbooksService($this->company); $this->settings = $this->company->quickbooks->settings; - nlog("here we go!"); foreach($this->entities as $key => $entity) { + if(!$this->syncGate($key, 'pull')) { continue; } @@ -94,34 +94,49 @@ class QuickbooksSync implements ShouldQueue } } - + + /** + * Determines whether a sync is allowed based on the settings + * + * @param string $entity + * @param string $direction + * @return bool + */ private function syncGate(string $entity, string $direction): bool { return (bool) $this->settings[$entity]['sync'] && in_array($this->settings[$entity]['direction'], [$direction,'bidirectional']); } - + + /** + * Updates the gate for a given entity + * + * @param string $entity + * @return bool + */ private function updateGate(string $entity): bool { return (bool) $this->settings[$entity]['sync'] && $this->settings[$entity]['update_record']; } - // private function harvestQbEntityName(string $entity): string - // { - // return $this->entities[$entity]; - // } - - private function processEntitySync(string $entity, $records) + /** + * Processes the sync for a given entity + * + * @param string $entity + * @param mixed $records + * @return void + */ + private function processEntitySync(string $entity, $records): void { match($entity){ - 'client' => $this->syncQbToNinjaClients($records), - 'product' => $this->syncQbToNinjaProducts($records), - 'invoice' => $this->syncQbToNinjaInvoices($records), - 'sales' => $this->syncQbToNinjaInvoices($records), - 'vendor' => $this->syncQbToNinjaVendors($records), - // 'quote' => $this->syncInvoices($records), - 'expense' => $this->syncQbToNinjaExpenses($records), - // 'purchase_order' => $this->syncInvoices($records), - // 'payment' => $this->syncPayment($records), + // 'client' => $this->syncQbToNinjaClients($records), + 'product' => $this->qbs->product->syncToNinja($records), + // 'invoice' => $this->syncQbToNinjaInvoices($records), + // 'sales' => $this->syncQbToNinjaInvoices($records), + // 'vendor' => $this->syncQbToNinjaVendors($records), + // 'quote' => $this->syncInvoices($records), + // 'expense' => $this->syncQbToNinjaExpenses($records), + // 'purchase_order' => $this->syncInvoices($records), + // 'payment' => $this->syncPayment($records), default => false, }; } @@ -140,6 +155,7 @@ class QuickbooksSync implements ShouldQueue nlog($ninja_invoice_data); $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; + $client_id = $ninja_invoice_data['client_id'] ?? null; if(is_null($client_id)) @@ -152,7 +168,7 @@ class QuickbooksSync implements ShouldQueue $invoice->fill($ninja_invoice_data); $invoice->saveQuietly(); - $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->createInvitations()->save(); foreach($payment_ids as $payment_id) { @@ -196,7 +212,8 @@ class QuickbooksSync implements ShouldQueue $search = Invoice::query() ->withTrashed() ->where('company_id', $this->company->id) - ->where('number', $ninja_invoice_data['number']); + // ->where('number', $ninja_invoice_data['number']); + ->where('sync->qb_id', $ninja_invoice_data['id']); if($search->count() == 0) { //new invoice @@ -400,27 +417,7 @@ class QuickbooksSync implements ShouldQueue return null; } - private function findProduct(string $key): ?Product - { - $search = Product::query() - ->withTrashed() - ->where('company_id', $this->company->id) - ->where('hash', $key); - - if($search->count() == 0) { - //new product - $product = ProductFactory::create($this->company->id, $this->company->owner()->id); - $product->hash = $key; - - return $product; - } elseif($search->count() == 1) { - return $this->settings['product']['update_record'] ? $search->first() : null; - } - - return null; - - - } + public function middleware() { diff --git a/app/Services/Quickbooks/Models/QbInvoice.php b/app/Services/Quickbooks/Models/QbInvoice.php new file mode 100644 index 000000000000..c4ce204676a0 --- /dev/null +++ b/app/Services/Quickbooks/Models/QbInvoice.php @@ -0,0 +1,30 @@ +service->sdk->FindById('Invoice', $id); + } + + +} diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php new file mode 100644 index 000000000000..cc86b7c6ea77 --- /dev/null +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -0,0 +1,76 @@ +service->sdk->FindById('Item', $id); + } + + + public function syncToNinja(array $records) + { + + $product_transformer = new ProductTransformer($this->service->company); + + foreach ($records as $record) { + + $ninja_data = $product_transformer->qbToNinja($record); + + if ($product = $this->findProduct($ninja_data['id'])) { + $product->fill($ninja_data); + $product->save(); + } + } + + } + + private function findProduct(string $key): ?Product + { + $search = Product::query() + ->withTrashed() + ->where('company_id', $this->service->company->id) + ->where('sync->qb_id', $key); + + if($search->count() == 0) { + + $product = ProductFactory::create($this->service->company->id, $this->service->company->owner()->id); + + $sync = new ProductSync(); + $sync->qb_id = $key; + $product->sync = $sync; + + return $product; + + } elseif($search->count() == 1) { + return $this->service->settings['product']['update_record'] ? $search->first() : null; + } + + return null; + + + } +} diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index 9d5b45fdd18f..a281d2346f41 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -11,16 +11,19 @@ namespace App\Services\Quickbooks; -use App\Factory\ClientContactFactory; -use App\Factory\ClientFactory; -use App\Factory\InvoiceFactory; -use App\Factory\ProductFactory; use App\Models\Client; use App\Models\Company; use App\Models\Invoice; use App\Models\Product; -use App\Services\Quickbooks\Jobs\QuickbooksSync; +use App\Factory\ClientFactory; +use App\Factory\InvoiceFactory; +use App\Factory\ProductFactory; +use App\Factory\ClientContactFactory; +use App\DataMapper\QuickbooksSettings; use QuickBooksOnline\API\Core\CoreConstants; +use App\Services\Quickbooks\Models\QbInvoice; +use App\Services\Quickbooks\Models\QbProduct; +use App\Services\Quickbooks\Jobs\QuickbooksSync; use QuickBooksOnline\API\DataService\DataService; use App\Services\Quickbooks\Transformers\ClientTransformer; use App\Services\Quickbooks\Transformers\InvoiceTransformer; @@ -31,9 +34,15 @@ class QuickbooksService { public DataService $sdk; + public QbInvoice $invoice; + + public QbProduct $product; + + public array $settings; + private bool $testMode = true; - public function __construct(private Company $company) + public function __construct(public Company $company) { $this->init(); } @@ -61,6 +70,12 @@ class QuickbooksService $this->sdk->setMinorVersion("73"); $this->sdk->throwExceptionOnError(true); + $this->invoice = new QbInvoice($this); + + $this->product = new QbProduct($this); + + $this->settings = $this->company->quickbooks->settings; + return $this; } diff --git a/app/Services/Quickbooks/Transformers/BaseTransformer.php b/app/Services/Quickbooks/Transformers/BaseTransformer.php index 66f3ff45195f..bc5639438f41 100644 --- a/app/Services/Quickbooks/Transformers/BaseTransformer.php +++ b/app/Services/Quickbooks/Transformers/BaseTransformer.php @@ -66,7 +66,8 @@ class BaseTransformer $client = Client::query() ->withTrashed() ->where('company_id', $this->company->id) - ->where('number', $customer_reference_id) + // ->where('number', $customer_reference_id) + ->where('sync->qb_id', $customer_reference_id) ->first(); return $client ? $client->id : null; diff --git a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php index 1ba7cedbfaf4..d2c85a31b005 100644 --- a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php +++ b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php @@ -38,6 +38,7 @@ class InvoiceTransformer extends BaseTransformer $client_id = $this->getClientId(data_get($qb_data, 'CustomerRef.value', null)); return $client_id ? [ + 'id' => data_get($qb_data, 'Id.value', false), 'client_id' => $client_id, 'number' => data_get($qb_data, 'DocNumber', false), 'date' => data_get($qb_data, 'TxnDate', now()->format('Y-m-d')), diff --git a/app/Services/Quickbooks/Transformers/ProductTransformer.php b/app/Services/Quickbooks/Transformers/ProductTransformer.php index 7b638d23e559..5cab5b45f1a1 100644 --- a/app/Services/Quickbooks/Transformers/ProductTransformer.php +++ b/app/Services/Quickbooks/Transformers/ProductTransformer.php @@ -33,7 +33,7 @@ class ProductTransformer extends BaseTransformer nlog(data_get($data, 'Id', null)); return [ - 'hash' => data_get($data, 'Id.value', null), + 'id' => data_get($data, 'Id.value', null), 'product_key' => data_get($data, 'Name', data_get($data, 'FullyQualifiedName','')), 'notes' => data_get($data, 'Description', ''), 'cost' => data_get($data, 'PurchaseCost', 0), diff --git a/database/migrations/2024_09_22_084749_2024_09_23_add_sync_column_for_qb.php b/database/migrations/2024_09_22_084749_2024_09_23_add_sync_column_for_qb.php new file mode 100644 index 000000000000..a863a578b83d --- /dev/null +++ b/database/migrations/2024_09_22_084749_2024_09_23_add_sync_column_for_qb.php @@ -0,0 +1,35 @@ +text('sync')->nullable(); + }); + + Schema::table('invoices', function (Blueprint $table) { + $table->text('sync')->nullable(); + }); + + Schema::table('products', function (Blueprint $table) { + $table->text('sync')->nullable(); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/tests/Feature/Import/Quickbooks/QuickbooksTest.php b/tests/Feature/Import/Quickbooks/QuickbooksTest.php index ea9a76b0fbf1..5fa4862bcde0 100644 --- a/tests/Feature/Import/Quickbooks/QuickbooksTest.php +++ b/tests/Feature/Import/Quickbooks/QuickbooksTest.php @@ -12,11 +12,14 @@ use Tests\MockAccountData; use Illuminate\Support\Facades\Cache; use Mockery; use App\Models\Client; +use App\Models\Company; use App\Models\Product; use App\Models\Invoice; +use App\Services\Quickbooks\QuickbooksService; use Illuminate\Support\Str; use ReflectionClass; use Illuminate\Support\Facades\Auth; +use QuickBooksOnline\API\Facades\Invoice as QbInvoice; class QuickbooksTest extends TestCase { @@ -29,13 +32,25 @@ class QuickbooksTest extends TestCase protected function setUp(): void { - parent::setUp(); - $this->markTestSkipped('no bueno'); + parent::setUp(); + if(config('ninja.is_travis')) + { + $this->markTestSkipped('No need to run this test on Travis'); + } + elseif(Company::whereNotNull('quickbooks')->count() == 0){ + $this->markTestSkipped('No need to run this test on Travis'); + } } - public function testCustomerSync() + public function testCreateInvoiceInQb() { - $data = (json_decode(file_get_contents(base_path('tests/Feature/Import/Quickbooks/customers.json')), false)); + + $c = Company::whereNotNull('quickbooks')->first(); + + $qb = new QuickbooksService($c); + + + } } From 702ec48261ffe9821374c3efb5aed9f5afb71b28 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 05:26:17 +1000 Subject: [PATCH 06/55] Fixes for reminder schedule tooltips --- app/Models/Invoice.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 750a88bd041f..203061b972d7 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -765,7 +765,7 @@ class Invoice extends BaseModel $send_email_enabled = ctrans('texts.send_email') . " " .ctrans('texts.enabled'); $send_email_disabled = ctrans('texts.send_email') . " " .ctrans('texts.disabled'); - $sends_email_1 = $settings->enable_reminder2 ? $send_email_enabled : $send_email_disabled; + $sends_email_1 = $settings->enable_reminder1 ? $send_email_enabled : $send_email_disabled; $days_1 = $settings->num_days_reminder1 . " " . ctrans('texts.days'); $schedule_1 = ctrans("texts.{$settings->schedule_reminder1}"); //after due date etc or disabled $label_1 = ctrans('texts.reminder1'); @@ -775,7 +775,7 @@ class Invoice extends BaseModel $schedule_2 = ctrans("texts.{$settings->schedule_reminder2}"); //after due date etc or disabled $label_2 = ctrans('texts.reminder2'); - $sends_email_3 = $settings->enable_reminder2 ? $send_email_enabled : $send_email_disabled; + $sends_email_3 = $settings->enable_reminder3 ? $send_email_enabled : $send_email_disabled; $days_3 = $settings->num_days_reminder3 . " " . ctrans('texts.days'); $schedule_3 = ctrans("texts.{$settings->schedule_reminder3}"); //after due date etc or disabled $label_3 = ctrans('texts.reminder3'); From 20088f10c43a9be7ab20a68656fc0687273da7f1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 06:33:30 +1000 Subject: [PATCH 07/55] Fixes for reminders when calculating across negative GMT timezones. --- app/Services/Invoice/UpdateReminder.php | 28 +++++---- tests/Feature/ReminderTest.php | 78 ++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/app/Services/Invoice/UpdateReminder.php b/app/Services/Invoice/UpdateReminder.php index 2a9791ba7ed3..1ff84d0e59e6 100644 --- a/app/Services/Invoice/UpdateReminder.php +++ b/app/Services/Invoice/UpdateReminder.php @@ -47,7 +47,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder1_sent) && $this->settings->schedule_reminder1 == 'after_invoice_date') { - $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1); + $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1)->addSeconds($offset); if ($reminder_date->gt(now())) { $date_collection->push($reminder_date); @@ -58,7 +58,7 @@ class UpdateReminder extends AbstractService ($this->invoice->partial_due_date || $this->invoice->due_date) && $this->settings->schedule_reminder1 == 'before_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder1); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder1)->addSeconds($offset); // nlog("1. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -71,7 +71,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder1 == 'after_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1)->addSeconds($offset); // nlog("2. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -81,7 +81,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder2_sent) && $this->settings->schedule_reminder2 == 'after_invoice_date') { - $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2); + $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2)->addSeconds($offset); if ($reminder_date->gt(now())) { $date_collection->push($reminder_date); @@ -93,7 +93,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder2 == 'before_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder2); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder2)->addSeconds($offset); // nlog("3. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -106,7 +106,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder2 == 'after_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2)->addSeconds($offset); // nlog("4. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -116,7 +116,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder3_sent) && $this->settings->schedule_reminder3 == 'after_invoice_date') { - $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3); + $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3)->addSeconds($offset); if ($reminder_date->gt(now())) { $date_collection->push($reminder_date); @@ -128,7 +128,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder3 == 'before_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder3); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder3)->addSeconds($offset); // nlog("5. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -141,7 +141,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder3 == 'after_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3)->addSeconds($offset); // nlog("6. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -154,17 +154,15 @@ class UpdateReminder extends AbstractService ($this->invoice->reminder1_sent || $this->settings->schedule_reminder1 == "" || !$this->settings->enable_reminder1) && ($this->invoice->reminder2_sent || $this->settings->schedule_reminder2 == "" || !$this->settings->enable_reminder2) && ($this->invoice->reminder3_sent || $this->settings->schedule_reminder3 == "" || !$this->settings->enable_reminder3)) { - $reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int) $this->settings->endless_reminder_frequency_id); + $reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int) $this->settings->endless_reminder_frequency_id)->addSeconds($offset); - if ($reminder_date) { - if ($reminder_date->gt(now())) { - $date_collection->push($reminder_date); - } + if ($reminder_date && $reminder_date->gt(now())) { + $date_collection->push($reminder_date); } } if ($date_collection->count() >= 1 && $date_collection->sort()->first()->gte(now())) { - $this->invoice->next_send_date = $date_collection->sort()->first()->addSeconds($offset); + $this->invoice->next_send_date = $date_collection->sort()->first(); } else { $this->invoice->next_send_date = null; } diff --git a/tests/Feature/ReminderTest.php b/tests/Feature/ReminderTest.php index 65e7eabf35f6..6b98112246ab 100644 --- a/tests/Feature/ReminderTest.php +++ b/tests/Feature/ReminderTest.php @@ -157,6 +157,80 @@ class ReminderTest extends TestCase 'balance' => 10, ]); + } + + public function testReminderScheduleNy() + { + + $settings = CompanySettings::defaults(); + $settings->timezone_id = '15'; + $settings->entity_send_time = 6; + $settings->payment_terms = '14'; + $settings->send_reminders = true; + $settings->enable_reminder1 = true; + $settings->enable_reminder2 = false; + $settings->enable_reminder3 = false; + $settings->enable_reminder_endless = true; + $settings->schedule_reminder1 = 'after_invoice_date'; + $settings->schedule_reminder2 = ''; + $settings->schedule_reminder3 = ''; + $settings->num_days_reminder1 = 1; + $settings->num_days_reminder2 = 0; + $settings->num_days_reminder3 = 0; + $settings->endless_reminder_frequency_id = '1'; + + $this->buildData($settings); + + $this->travelTo(Carbon::parse('2024-09-20')->startOfDay()->addHours(1)); + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'amount' => 10, + 'balance' => 10, + 'date' => '2024-09-19', + 'number' => 'JJJ1-11-2024', + 'due_date' => '2024-09-19', + 'status_id' => 2, + 'last_sent_date' => '19-09-2024', + ]); + + $this->assertEquals(1, $invoice->company->settings->num_days_reminder1); + + $invoice->service()->setReminder($settings)->save(); + + $this->assertEquals(10, $invoice->balance); + $this->assertEquals('2024-09-20', $invoice->next_send_date->format('Y-m-d')); + + + $x = false; + do { + + $this->travelTo(now()->addHour()); + (new ReminderJob())->handle(); + $invoice = $invoice->fresh(); + + $x = (bool)$invoice->reminder1_sent; + } while ($x === false); + + $this->assertNotNull($invoice->reminder_last_sent); + $this->assertEquals(now()->addDays(1), $invoice->next_send_date); + + $x = 0; + do { + + $this->travelTo(now()->addHour()); + (new ReminderJob())->handle(); + $invoice = $invoice->fresh(); + + $x++; + } while ($x < 24); + + $this->assertEquals(now()->addDays(1), $invoice->next_send_date); + + + } public function testDKRemindersNotSending() @@ -434,7 +508,7 @@ class ReminderTest extends TestCase $fee = collect($this->invoice->line_items)->where('cost', 102)->first(); $this->assertEquals(102, $fee->cost); - $this->assertEquals('Fee added '.now()->format('d/M/Y'), $fee->notes); + $this->assertEquals('Late fee added on '.now()->format('d/M/Y'), $fee->notes); $this->travelTo(now()->addDay()->startOfDay()->addHour()); @@ -446,7 +520,7 @@ class ReminderTest extends TestCase $fee = collect($this->invoice->line_items)->where('cost', 103)->first(); $this->assertEquals(103, $fee->cost); - $this->assertEquals('Fee added '.now()->format('d/M/Y'), $fee->notes); + $this->assertEquals('Late fee added on '.now()->format('d/M/Y'), $fee->notes); $this->travelBack(); From 49601dfa5ec2751c78eaa1558398e17390086348 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 06:40:03 +1000 Subject: [PATCH 08/55] Fixes for tests --- tests/Feature/ReminderTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/ReminderTest.php b/tests/Feature/ReminderTest.php index 6b98112246ab..cfc4bd4b41fb 100644 --- a/tests/Feature/ReminderTest.php +++ b/tests/Feature/ReminderTest.php @@ -508,7 +508,7 @@ class ReminderTest extends TestCase $fee = collect($this->invoice->line_items)->where('cost', 102)->first(); $this->assertEquals(102, $fee->cost); - $this->assertEquals('Late fee added on '.now()->format('d/M/Y'), $fee->notes); + $this->assertEquals('Fee added '.now()->format('d/M/Y'), $fee->notes); $this->travelTo(now()->addDay()->startOfDay()->addHour()); @@ -520,7 +520,7 @@ class ReminderTest extends TestCase $fee = collect($this->invoice->line_items)->where('cost', 103)->first(); $this->assertEquals(103, $fee->cost); - $this->assertEquals('Late fee added on '.now()->format('d/M/Y'), $fee->notes); + $this->assertEquals('Fee added '.now()->format('d/M/Y'), $fee->notes); $this->travelBack(); From 09b803d9dc413a8f5eb2f9d117864f23606a62cd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 07:16:34 +1000 Subject: [PATCH 09/55] Updates for castables --- app/Casts/QuickbooksSettingsCast.php | 32 ++++-------- app/DataMapper/ClientSync.php | 9 +++- app/DataMapper/InvoiceSync.php | 10 +++- app/DataMapper/ProductSync.php | 8 +++ app/DataMapper/QuickbooksSettings.php | 40 +++++++-------- app/DataMapper/QuickbooksSync.php | 49 +++++++++++++++++++ app/DataMapper/QuickbooksSyncMap.php | 32 ++++++++++++ .../Quickbooks/Jobs/QuickbooksSync.php | 12 ++--- app/Services/Quickbooks/Models/QbProduct.php | 2 +- 9 files changed, 139 insertions(+), 55 deletions(-) create mode 100644 app/DataMapper/QuickbooksSync.php create mode 100644 app/DataMapper/QuickbooksSyncMap.php diff --git a/app/Casts/QuickbooksSettingsCast.php b/app/Casts/QuickbooksSettingsCast.php index a5027733f180..815725213590 100644 --- a/app/Casts/QuickbooksSettingsCast.php +++ b/app/Casts/QuickbooksSettingsCast.php @@ -18,33 +18,19 @@ class QuickbooksSettingsCast implements CastsAttributes { public function get($model, string $key, $value, array $attributes) { + if (is_null($value)) { + return new QuickbooksSettings(); + $data = json_decode($value, true); - - if(!is_array($data)) - return null; - - $qb = new QuickbooksSettings(); - $qb->accessTokenKey = $data['accessTokenKey']; - $qb->refresh_token = $data['refresh_token']; - $qb->realmID = $data['realmID']; - $qb->accessTokenExpiresAt = $data['accessTokenExpiresAt']; - $qb->refreshTokenExpiresAt = $data['refreshTokenExpiresAt']; - $qb->settings = $data['settings'] ?? []; - - return $qb; + return QuickbooksSettings::fromArray($data); } public function set($model, string $key, $value, array $attributes) { - return [ - $key => json_encode([ - 'accessTokenKey' => $value->accessTokenKey, - 'refresh_token' => $value->refresh_token, - 'realmID' => $value->realmID, - 'accessTokenExpiresAt' => $value->accessTokenExpiresAt, - 'refreshTokenExpiresAt' => $value->refreshTokenExpiresAt, - 'settings' => $value->settings, - ]) - ]; + if ($value instanceof QuickbooksSettings) { + return json_encode(get_object_vars($value)); + } + + return json_encode($value); } } diff --git a/app/DataMapper/ClientSync.php b/app/DataMapper/ClientSync.php index 56b2b8e185f9..b45b2aca45a7 100644 --- a/app/DataMapper/ClientSync.php +++ b/app/DataMapper/ClientSync.php @@ -21,7 +21,10 @@ class ClientSync implements Castable { public string $qb_id; - + public function __construct(array $attributes = []) + { + $this->qb_id = $attributes['qb_id'] ?? ''; + } /** * Get the name of the caster class to use when casting from / to this cast target. * @@ -32,4 +35,8 @@ class ClientSync implements Castable return ClientSyncCast::class; } + public static function fromArray(array $data): self + { + return new self($data); + } } diff --git a/app/DataMapper/InvoiceSync.php b/app/DataMapper/InvoiceSync.php index 90ecde71989a..b56b1a2239c2 100644 --- a/app/DataMapper/InvoiceSync.php +++ b/app/DataMapper/InvoiceSync.php @@ -20,7 +20,11 @@ use Illuminate\Contracts\Database\Eloquent\Castable; class InvoiceSync implements Castable { public string $qb_id; - + + public function __construct(array $attributes = []) + { + $this->qb_id = $attributes['qb_id'] ?? ''; + } /** * Get the name of the caster class to use when casting from / to this cast target. @@ -32,4 +36,8 @@ class InvoiceSync implements Castable return InvoiceSyncCast::class; } + public static function fromArray(array $data): self + { + return new self($data); + } } diff --git a/app/DataMapper/ProductSync.php b/app/DataMapper/ProductSync.php index 1417b0b0129b..dfc6aada909f 100644 --- a/app/DataMapper/ProductSync.php +++ b/app/DataMapper/ProductSync.php @@ -21,6 +21,10 @@ class ProductSync implements Castable { public string $qb_id; + public function __construct(array $attributes = []) + { + $this->qb_id = $attributes['qb_id'] ?? ''; + } /** * Get the name of the caster class to use when casting from / to this cast target. @@ -32,4 +36,8 @@ class ProductSync implements Castable return ProductSyncCast::class; } + public static function fromArray(array $data): self + { + return new self($data); + } } diff --git a/app/DataMapper/QuickbooksSettings.php b/app/DataMapper/QuickbooksSettings.php index 1442914e4b5f..2881c6d384a3 100644 --- a/app/DataMapper/QuickbooksSettings.php +++ b/app/DataMapper/QuickbooksSettings.php @@ -30,34 +30,28 @@ class QuickbooksSettings implements Castable public int $refreshTokenExpiresAt; public string $baseURL; - /** - * entity client,invoice,quote,purchase_order,vendor,payment - * sync true/false - * update_record true/false - * direction push/pull/birectional - * */ - public array $settings = [ - 'client' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'invoice' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'sales' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'quote' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'purchase_order' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'product' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'payment' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - 'expense' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], - ]; + + public QuickbooksSync $settings; + public function __construct(array $attributes = []) + { + $this->accessTokenKey = $attributes['accessTokenKey'] ?? ''; + $this->refresh_token = $attributes['refresh_token'] ?? ''; + $this->realmID = $attributes['realmID'] ?? ''; + $this->accessTokenExpiresAt = $attributes['accessTokenExpiresAt'] ?? 0; + $this->refreshTokenExpiresAt = $attributes['refreshTokenExpiresAt'] ?? 0; + $this->baseURL = $attributes['baseURL'] ?? ''; + $this->settings = new QuickbooksSync($attributes['settings']); + } - /** - * Get the name of the caster class to use when casting from / to this cast target. - * - * @param array $arguments - */ public static function castUsing(array $arguments): string { return QuickbooksSettingsCast::class; } + public static function fromArray(array $data): self + { + return new self($data); + } + } diff --git a/app/DataMapper/QuickbooksSync.php b/app/DataMapper/QuickbooksSync.php new file mode 100644 index 000000000000..d5154862b399 --- /dev/null +++ b/app/DataMapper/QuickbooksSync.php @@ -0,0 +1,49 @@ +client = new QuickbooksSyncMap($attributes['client'] ?? []); + $this->vendor = new QuickbooksSyncMap($attributes['vendor'] ?? []); + $this->invoice = new QuickbooksSyncMap($attributes['invoice'] ?? []); + $this->sales = new QuickbooksSyncMap($attributes['sales'] ?? []); + $this->quote = new QuickbooksSyncMap($attributes['quote'] ?? []); + $this->purchase_order = new QuickbooksSyncMap($attributes['purchase_order'] ?? []); + $this->product = new QuickbooksSyncMap($attributes['product'] ?? []); + $this->payment = new QuickbooksSyncMap($attributes['payment'] ?? []); + $this->expense = new QuickbooksSyncMap($attributes['expense'] ?? []); + } +} diff --git a/app/DataMapper/QuickbooksSyncMap.php b/app/DataMapper/QuickbooksSyncMap.php new file mode 100644 index 000000000000..b4d1e203b18c --- /dev/null +++ b/app/DataMapper/QuickbooksSyncMap.php @@ -0,0 +1,32 @@ +settings[$entity]['sync'] && in_array($this->settings[$entity]['direction'], [$direction,'bidirectional']); + return (bool) $this->settings->{$entity}->sync && in_array($this->settings->{$entity}->direction, [$direction,'bidirectional']); } /** @@ -115,7 +115,7 @@ class QuickbooksSync implements ShouldQueue */ private function updateGate(string $entity): bool { - return (bool) $this->settings[$entity]['sync'] && $this->settings[$entity]['update_record']; + return (bool) $this->settings->{$entity}->sync && $this->settings->{$entity}->update_record; } /** @@ -222,7 +222,7 @@ class QuickbooksSync implements ShouldQueue return $invoice; } elseif($search->count() == 1) { - return $this->settings['invoice']['update_record'] ? $search->first() : null; + return $this->settings->invoice->update_record ? $search->first() : null; } return null; @@ -347,7 +347,7 @@ class QuickbooksSync implements ShouldQueue return ExpenseFactory::create($this->company->id, $this->company->owner()->id); } elseif($search->count() == 1) { - return $this->settings['expense']['update_record'] ? $search->first() : null; + return $this->settings->expense->update_record ? $search->first() : null; } return null; @@ -377,7 +377,7 @@ class QuickbooksSync implements ShouldQueue return VendorFactory::create($this->company->id, $this->company->owner()->id); } elseif($search->count() == 1) { - return $this->settings['vendor']['update_record'] ? $search->first() : null; + return $this->settings->vendor->update_record ? $search->first() : null; } return null; @@ -411,7 +411,7 @@ class QuickbooksSync implements ShouldQueue return $client; } elseif($search->count() == 1) { - return $this->settings['client']['update_record'] ? $search->first() : null; + return $this->settings->client->update_record ? $search->first() : null; } return null; diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php index cc86b7c6ea77..33a0c61b68ac 100644 --- a/app/Services/Quickbooks/Models/QbProduct.php +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -66,7 +66,7 @@ class QbProduct return $product; } elseif($search->count() == 1) { - return $this->service->settings['product']['update_record'] ? $search->first() : null; + return $this->service->settings->product->update_record ? $search->first() : null; } return null; From 38fb55523af92562139db345a60a4db44b97919b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 07:27:28 +1000 Subject: [PATCH 10/55] Fixes for castables --- app/Casts/ClientSyncCast.php | 2 +- app/Casts/EncryptedCast.php | 2 +- app/Casts/InvoiceSyncCast.php | 2 +- app/Casts/ProductSyncCast.php | 2 +- app/Casts/QuickbooksSettingsCast.php | 4 ++-- app/Helpers/Mail/Office365MailTransport.php | 2 +- app/PaymentDrivers/BTCPayPaymentDriver.php | 2 +- app/PaymentDrivers/BlockonomicsPaymentDriver.php | 2 +- app/PaymentDrivers/Factory/ForteCustomerFactory.php | 2 +- .../Factory/PaytraceCustomerFactory.php | 2 +- .../Factory/SquareCustomerFactory.php | 2 +- app/PaymentDrivers/Forte/ACH.php | 2 +- app/PaymentDrivers/Forte/CreditCard.php | 2 +- app/PaymentDrivers/FortePaymentDriver.php | 2 +- app/Services/Quickbooks/Models/QbProduct.php | 13 ++++++++----- .../Quickbooks/Transformers/ClientTransformer.php | 1 + .../Quickbooks/Transformers/ProductTransformer.php | 3 +-- 17 files changed, 25 insertions(+), 22 deletions(-) diff --git a/app/Casts/ClientSyncCast.php b/app/Casts/ClientSyncCast.php index 736022b3053a..aaeb153680c2 100644 --- a/app/Casts/ClientSyncCast.php +++ b/app/Casts/ClientSyncCast.php @@ -4,7 +4,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/Casts/EncryptedCast.php b/app/Casts/EncryptedCast.php index a8c11f3ad397..5600fa7b7d52 100644 --- a/app/Casts/EncryptedCast.php +++ b/app/Casts/EncryptedCast.php @@ -4,7 +4,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/Casts/InvoiceSyncCast.php b/app/Casts/InvoiceSyncCast.php index 8776637a709e..713322a35181 100644 --- a/app/Casts/InvoiceSyncCast.php +++ b/app/Casts/InvoiceSyncCast.php @@ -4,7 +4,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/Casts/ProductSyncCast.php b/app/Casts/ProductSyncCast.php index ca2172c13b14..e3c491015201 100644 --- a/app/Casts/ProductSyncCast.php +++ b/app/Casts/ProductSyncCast.php @@ -4,7 +4,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/Casts/QuickbooksSettingsCast.php b/app/Casts/QuickbooksSettingsCast.php index 815725213590..4de9333564bd 100644 --- a/app/Casts/QuickbooksSettingsCast.php +++ b/app/Casts/QuickbooksSettingsCast.php @@ -4,7 +4,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ @@ -18,7 +18,7 @@ class QuickbooksSettingsCast implements CastsAttributes { public function get($model, string $key, $value, array $attributes) { - if (is_null($value)) { + if (is_null($value)) return new QuickbooksSettings(); $data = json_decode($value, true); diff --git a/app/Helpers/Mail/Office365MailTransport.php b/app/Helpers/Mail/Office365MailTransport.php index 4a6f49a70b65..6d345f03968b 100644 --- a/app/Helpers/Mail/Office365MailTransport.php +++ b/app/Helpers/Mail/Office365MailTransport.php @@ -4,7 +4,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/PaymentDrivers/BTCPayPaymentDriver.php b/app/PaymentDrivers/BTCPayPaymentDriver.php index 4d1c02b273c9..801241e006e0 100644 --- a/app/PaymentDrivers/BTCPayPaymentDriver.php +++ b/app/PaymentDrivers/BTCPayPaymentDriver.php @@ -5,7 +5,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://opensource.org/licenses/AAL */ diff --git a/app/PaymentDrivers/BlockonomicsPaymentDriver.php b/app/PaymentDrivers/BlockonomicsPaymentDriver.php index a53ddc4bb612..05db691d2648 100644 --- a/app/PaymentDrivers/BlockonomicsPaymentDriver.php +++ b/app/PaymentDrivers/BlockonomicsPaymentDriver.php @@ -5,7 +5,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://opensource.org/licenses/AAL */ diff --git a/app/PaymentDrivers/Factory/ForteCustomerFactory.php b/app/PaymentDrivers/Factory/ForteCustomerFactory.php index e7c535221627..277d536f5c39 100644 --- a/app/PaymentDrivers/Factory/ForteCustomerFactory.php +++ b/app/PaymentDrivers/Factory/ForteCustomerFactory.php @@ -5,7 +5,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/PaymentDrivers/Factory/PaytraceCustomerFactory.php b/app/PaymentDrivers/Factory/PaytraceCustomerFactory.php index 3f05dbf84d06..5308bc4ab6e0 100644 --- a/app/PaymentDrivers/Factory/PaytraceCustomerFactory.php +++ b/app/PaymentDrivers/Factory/PaytraceCustomerFactory.php @@ -5,7 +5,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/PaymentDrivers/Factory/SquareCustomerFactory.php b/app/PaymentDrivers/Factory/SquareCustomerFactory.php index c62eac3d8055..00070b93a793 100644 --- a/app/PaymentDrivers/Factory/SquareCustomerFactory.php +++ b/app/PaymentDrivers/Factory/SquareCustomerFactory.php @@ -5,7 +5,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/PaymentDrivers/Forte/ACH.php b/app/PaymentDrivers/Forte/ACH.php index 1fa4c4a2ab07..09071ac39c8d 100644 --- a/app/PaymentDrivers/Forte/ACH.php +++ b/app/PaymentDrivers/Forte/ACH.php @@ -5,7 +5,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/PaymentDrivers/Forte/CreditCard.php b/app/PaymentDrivers/Forte/CreditCard.php index cc4256171f27..9b3bd359c73f 100644 --- a/app/PaymentDrivers/Forte/CreditCard.php +++ b/app/PaymentDrivers/Forte/CreditCard.php @@ -5,7 +5,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://www.elastic.co/licensing/elastic-license */ diff --git a/app/PaymentDrivers/FortePaymentDriver.php b/app/PaymentDrivers/FortePaymentDriver.php index 90efcfa0c18b..3daa71dfefe1 100644 --- a/app/PaymentDrivers/FortePaymentDriver.php +++ b/app/PaymentDrivers/FortePaymentDriver.php @@ -4,7 +4,7 @@ * * @link https://github.com/invoiceninja/invoiceninja source repository * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com) * * @license https://opensource.org/licenses/AAL */ diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php index 33a0c61b68ac..4c5ca8d2b835 100644 --- a/app/Services/Quickbooks/Models/QbProduct.php +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -13,25 +13,24 @@ namespace App\Services\Quickbooks\Models; use App\DataMapper\ProductSync; use App\Services\Quickbooks\QuickbooksService; - use App\Models\Product; use App\Factory\ProductFactory; use App\Services\Quickbooks\Transformers\ProductTransformer; +use App\Interfaces\SyncInterface; -class QbProduct +class QbProduct implements SyncInterface { public function __construct(public QuickbooksService $service) { } - public function find(int $id) + public function find(string $id): mixed { return $this->service->sdk->FindById('Item', $id); } - - public function syncToNinja(array $records) + public function syncToNinja(array $records): void { $product_transformer = new ProductTransformer($this->service->company); @@ -48,6 +47,10 @@ class QbProduct } + public function syncToForeign(array $records): void + { + } + private function findProduct(string $key): ?Product { $search = Product::query() diff --git a/app/Services/Quickbooks/Transformers/ClientTransformer.php b/app/Services/Quickbooks/Transformers/ClientTransformer.php index 998f12a1543c..69b0566ded99 100644 --- a/app/Services/Quickbooks/Transformers/ClientTransformer.php +++ b/app/Services/Quickbooks/Transformers/ClientTransformer.php @@ -40,6 +40,7 @@ class ClientTransformer extends BaseTransformer ]; $client = [ + 'id' => data_get($data, 'Id.value', null), 'name' => data_get($data,'CompanyName', ''), 'address1' => data_get($data, 'BillAddr.Line1', ''), 'address2' => data_get($data, 'BillAddr.Line2', ''), diff --git a/app/Services/Quickbooks/Transformers/ProductTransformer.php b/app/Services/Quickbooks/Transformers/ProductTransformer.php index 5cab5b45f1a1..8a93d607fe85 100644 --- a/app/Services/Quickbooks/Transformers/ProductTransformer.php +++ b/app/Services/Quickbooks/Transformers/ProductTransformer.php @@ -30,8 +30,7 @@ class ProductTransformer extends BaseTransformer public function transform(mixed $data): array { - nlog(data_get($data, 'Id', null)); - + return [ 'id' => data_get($data, 'Id.value', null), 'product_key' => data_get($data, 'Name', data_get($data, 'FullyQualifiedName','')), From 3f59417ab11bdcc3031246e9650147885e439d60 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 07:37:50 +1000 Subject: [PATCH 11/55] Add SyncInterface --- app/Interfaces/SyncInterface.php | 21 ++++++ app/Services/Quickbooks/Models/QbClient.php | 78 +++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 app/Interfaces/SyncInterface.php create mode 100644 app/Services/Quickbooks/Models/QbClient.php diff --git a/app/Interfaces/SyncInterface.php b/app/Interfaces/SyncInterface.php new file mode 100644 index 000000000000..17ca4e69c189 --- /dev/null +++ b/app/Interfaces/SyncInterface.php @@ -0,0 +1,21 @@ +service->sdk->FindById('Customer', $id); + } + + public function syncToNinja(array $records): void + { + + $transformer = new ClientTransformer($this->service->company); + + foreach ($records as $record) { + + $ninja_data = $transformer->qbToNinja($record); + + if ($client = $this->findClient($ninja_data['id'])) { + $client->fill($ninja_data); + $client->save(); + } + } + + } + + public function syncToForeign(array $records): void + { + } + + private function findClient(string $key): ?Client + { + $search = Client::query() + ->withTrashed() + ->where('company_id', $this->service->company->id) + ->where('sync->qb_id', $key); + + if ($search->count() == 0) { + + $client = ClientFactory::create($this->service->company->id, $this->service->company->owner()->id); + + $sync = new ClientSync(); + $sync->qb_id = $key; + $client->sync = $sync; + + return $client; + + } elseif ($search->count() == 1) { + return $this->service->settings->client->update_record ? $search->first() : null; + } + + return null; + + + } +} From 3a4bb767bfd1dc7452042c0d12b54727c153206b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 07:55:53 +1000 Subject: [PATCH 12/55] Fixes for tests --- app/DataMapper/QuickbooksSettings.php | 2 +- app/DataMapper/QuickbooksSync.php | 2 +- .../Quickbooks/Jobs/QuickbooksSync.php | 433 ------------------ app/Services/Quickbooks/Models/QbClient.php | 2 +- app/Services/Quickbooks/QuickbooksService.php | 8 +- 5 files changed, 7 insertions(+), 440 deletions(-) delete mode 100644 app/Services/Quickbooks/Jobs/QuickbooksSync.php diff --git a/app/DataMapper/QuickbooksSettings.php b/app/DataMapper/QuickbooksSettings.php index 2881c6d384a3..87e0805f8b4b 100644 --- a/app/DataMapper/QuickbooksSettings.php +++ b/app/DataMapper/QuickbooksSettings.php @@ -41,7 +41,7 @@ class QuickbooksSettings implements Castable $this->accessTokenExpiresAt = $attributes['accessTokenExpiresAt'] ?? 0; $this->refreshTokenExpiresAt = $attributes['refreshTokenExpiresAt'] ?? 0; $this->baseURL = $attributes['baseURL'] ?? ''; - $this->settings = new QuickbooksSync($attributes['settings']); + $this->settings = new QuickbooksSync($attributes['settings'] ?? []); } public static function castUsing(array $arguments): string diff --git a/app/DataMapper/QuickbooksSync.php b/app/DataMapper/QuickbooksSync.php index d5154862b399..6ba064117090 100644 --- a/app/DataMapper/QuickbooksSync.php +++ b/app/DataMapper/QuickbooksSync.php @@ -46,4 +46,4 @@ class QuickbooksSync $this->payment = new QuickbooksSyncMap($attributes['payment'] ?? []); $this->expense = new QuickbooksSyncMap($attributes['expense'] ?? []); } -} +} \ No newline at end of file diff --git a/app/Services/Quickbooks/Jobs/QuickbooksSync.php b/app/Services/Quickbooks/Jobs/QuickbooksSync.php deleted file mode 100644 index e17cb20e6c43..000000000000 --- a/app/Services/Quickbooks/Jobs/QuickbooksSync.php +++ /dev/null @@ -1,433 +0,0 @@ - 'Item', - 'client' => 'Customer', - 'invoice' => 'Invoice', - // 'quote' => 'Estimate', - // 'purchase_order' => 'PurchaseOrder', - // 'payment' => 'Payment', - 'sales' => 'SalesReceipt', - // 'vendor' => 'Vendor', - // 'expense' => 'Purchase', - ]; - - private QuickbooksService $qbs; - - private ?array $settings; - - private Company $company; - - public function __construct(public int $company_id, public string $db) - { - } - - /** - * Execute the job. - */ - public function handle() - { - MultiDB::setDb($this->db); - - $this->company = Company::query()->find($this->company_id); - $this->qbs = new QuickbooksService($this->company); - $this->settings = $this->company->quickbooks->settings; - - foreach($this->entities as $key => $entity) { - - if(!$this->syncGate($key, 'pull')) { - continue; - } - - $records = $this->qbs->sdk()->fetchRecords($entity); - - $this->processEntitySync($key, $records); - - } - - } - - /** - * Determines whether a sync is allowed based on the settings - * - * @param string $entity - * @param string $direction - * @return bool - */ - private function syncGate(string $entity, string $direction): bool - { - return (bool) $this->settings->{$entity}->sync && in_array($this->settings->{$entity}->direction, [$direction,'bidirectional']); - } - - /** - * Updates the gate for a given entity - * - * @param string $entity - * @return bool - */ - private function updateGate(string $entity): bool - { - return (bool) $this->settings->{$entity}->sync && $this->settings->{$entity}->update_record; - } - - /** - * Processes the sync for a given entity - * - * @param string $entity - * @param mixed $records - * @return void - */ - private function processEntitySync(string $entity, $records): void - { - match($entity){ - // 'client' => $this->syncQbToNinjaClients($records), - 'product' => $this->qbs->product->syncToNinja($records), - // 'invoice' => $this->syncQbToNinjaInvoices($records), - // 'sales' => $this->syncQbToNinjaInvoices($records), - // 'vendor' => $this->syncQbToNinjaVendors($records), - // 'quote' => $this->syncInvoices($records), - // 'expense' => $this->syncQbToNinjaExpenses($records), - // 'purchase_order' => $this->syncInvoices($records), - // 'payment' => $this->syncPayment($records), - default => false, - }; - } - - private function syncQbToNinjaInvoices($records): void - { - nlog("invoice sync ". count($records)); - $invoice_transformer = new InvoiceTransformer($this->company); - - foreach($records as $record) - { - nlog($record); - - $ninja_invoice_data = $invoice_transformer->qbToNinja($record); - - nlog($ninja_invoice_data); - - $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; - - $client_id = $ninja_invoice_data['client_id'] ?? null; - - if(is_null($client_id)) - continue; - - unset($ninja_invoice_data['payment_ids']); - - if($invoice = $this->findInvoice($ninja_invoice_data)) - { - $invoice->fill($ninja_invoice_data); - $invoice->saveQuietly(); - - $invoice = $invoice->calc()->getInvoice()->service()->markSent()->createInvitations()->save(); - - foreach($payment_ids as $payment_id) - { - - $payment = $this->qbs->sdk->FindById('Payment', $payment_id); - - $payment_transformer = new PaymentTransformer($this->company); - - $transformed = $payment_transformer->qbToNinja($payment); - - $ninja_payment = $payment_transformer->buildPayment($payment); - $ninja_payment->service()->applyNumber()->save(); - - $paymentable = new \App\Models\Paymentable(); - $paymentable->payment_id = $ninja_payment->id; - $paymentable->paymentable_id = $invoice->id; - $paymentable->paymentable_type = 'invoices'; - $paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount'); - $paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line - $paymentable->save(); - - $invoice->service()->applyPayment($ninja_payment, $paymentable->amount); - - } - - if($record instanceof IPPSalesReceipt) - { - $invoice->service()->markPaid()->save(); - } - - } - - $ninja_invoice_data = false; - - } - - } - - private function findInvoice(array $ninja_invoice_data): ?Invoice - { - $search = Invoice::query() - ->withTrashed() - ->where('company_id', $this->company->id) - // ->where('number', $ninja_invoice_data['number']); - ->where('sync->qb_id', $ninja_invoice_data['id']); - - if($search->count() == 0) { - //new invoice - $invoice = InvoiceFactory::create($this->company->id, $this->company->owner()->id); - $invoice->client_id = $ninja_invoice_data['client_id']; - - return $invoice; - } elseif($search->count() == 1) { - return $this->settings->invoice->update_record ? $search->first() : null; - } - - return null; - - } - - private function syncQbToNinjaClients(array $records): void - { - - $client_transformer = new ClientTransformer($this->company); - - foreach($records as $record) - { - $ninja_client_data = $client_transformer->qbToNinja($record); - - if($client = $this->findClient($ninja_client_data)) - { - $client->fill($ninja_client_data[0]); - $client->saveQuietly(); - - $contact = $client->contacts()->where('email', $ninja_client_data[1]['email'])->first(); - - if(!$contact) - { - $contact = ClientContactFactory::create($this->company->id, $this->company->owner()->id); - $contact->client_id = $client->id; - $contact->send_email = true; - $contact->is_primary = true; - $contact->fill($ninja_client_data[1]); - $contact->saveQuietly(); - } - elseif($this->updateGate('client')){ - $contact->fill($ninja_client_data[1]); - $contact->saveQuietly(); - } - - } - - } - } - - private function syncQbToNinjaVendors(array $records): void - { - - $transformer = new VendorTransformer($this->company); - - foreach($records as $record) - { - $ninja_data = $transformer->qbToNinja($record); - - if($vendor = $this->findVendor($ninja_data)) - { - $vendor->fill($ninja_data[0]); - $vendor->saveQuietly(); - - $contact = $vendor->contacts()->where('email', $ninja_data[1]['email'])->first(); - - if(!$contact) - { - $contact = VendorContactFactory::create($this->company->id, $this->company->owner()->id); - $contact->vendor_id = $vendor->id; - $contact->send_email = true; - $contact->is_primary = true; - $contact->fill($ninja_data[1]); - $contact->saveQuietly(); - } - elseif($this->updateGate('vendor')){ - $contact->fill($ninja_data[1]); - $contact->saveQuietly(); - } - - } - - } - } - - private function syncQbToNinjaExpenses(array $records): void - { - - $transformer = new ExpenseTransformer($this->company); - - foreach($records as $record) - { - $ninja_data = $transformer->qbToNinja($record); - - if($expense = $this->findExpense($ninja_data)) - { - $expense->fill($ninja_data); - $expense->saveQuietly(); - } - - } - } - - - private function syncQbToNinjaProducts($records): void - { - $product_transformer = new ProductTransformer($this->company); - - foreach($records as $record) - { - $ninja_data = $product_transformer->qbToNinja($record); - - if($product = $this->findProduct($ninja_data['hash'])) - { - $product->fill($ninja_data); - $product->save(); - } - } - } - - private function findExpense(array $qb_data): ?Expense - { - $expense = $qb_data; - - $search = Expense::query() - ->withTrashed() - ->where('company_id', $this->company->id) - ->where('number', $expense['number']); - - if($search->count() == 0) { - return ExpenseFactory::create($this->company->id, $this->company->owner()->id); - } - elseif($search->count() == 1) { - return $this->settings->expense->update_record ? $search->first() : null; - } - - return null; - } - - private function findVendor(array $qb_data) :?Vendor - { - $vendor = $qb_data[0]; - $contact = $qb_data[1]; - $vendor_meta = $qb_data[2]; - - $search = Vendor::query() - ->withTrashed() - ->where('company_id', $this->company->id) - ->where(function ($q) use ($vendor, $vendor_meta, $contact){ - - $q->where('vendor_hash', $vendor_meta['vendor_hash']) - ->orWhere('number', $vendor['number']) - ->orWhereHas('contacts', function ($q) use ($contact){ - $q->where('email', $contact['email']); - }); - - }); - - if($search->count() == 0) { - //new client - return VendorFactory::create($this->company->id, $this->company->owner()->id); - } - elseif($search->count() == 1) { - return $this->settings->vendor->update_record ? $search->first() : null; - } - - return null; - } - - private function findClient(array $qb_data) :?Client - { - $client = $qb_data[0]; - $contact = $qb_data[1]; - $client_meta = $qb_data[2]; - - $search = Client::query() - ->withTrashed() - ->where('company_id', $this->company->id) - ->where(function ($q) use ($client, $client_meta, $contact){ - - $q->where('client_hash', $client_meta['client_hash']) - ->orWhere('number', $client['number']) - ->orWhereHas('contacts', function ($q) use ($contact){ - $q->where('email', $contact['email']); - }); - - }); - - if($search->count() == 0) { - //new client - $client = ClientFactory::create($this->company->id, $this->company->owner()->id); - $client->client_hash = $client_meta['client_hash']; - $client->settings = $client_meta['settings']; - - return $client; - } - elseif($search->count() == 1) { - return $this->settings->client->update_record ? $search->first() : null; - } - - return null; - } - - - - public function middleware() - { - return [new WithoutOverlapping("qbs-{$this->company_id}-{$this->db}")]; - } - - public function failed($exception) - { - nlog("QuickbooksSync failed => ".$exception->getMessage()); - config(['queue.failed.driver' => null]); - - } -} diff --git a/app/Services/Quickbooks/Models/QbClient.php b/app/Services/Quickbooks/Models/QbClient.php index 7e0bb87a55fe..1528aca44064 100644 --- a/app/Services/Quickbooks/Models/QbClient.php +++ b/app/Services/Quickbooks/Models/QbClient.php @@ -40,7 +40,7 @@ class QbClient implements SyncInterface if ($client = $this->findClient($ninja_data['id'])) { $client->fill($ninja_data); - $client->save(); + $client->service()->applyNumber()->save(); } } diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index a281d2346f41..3083883e17eb 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -18,13 +18,13 @@ use App\Models\Product; use App\Factory\ClientFactory; use App\Factory\InvoiceFactory; use App\Factory\ProductFactory; +use App\DataMapper\QuickbooksSync; use App\Factory\ClientContactFactory; -use App\DataMapper\QuickbooksSettings; use QuickBooksOnline\API\Core\CoreConstants; use App\Services\Quickbooks\Models\QbInvoice; use App\Services\Quickbooks\Models\QbProduct; -use App\Services\Quickbooks\Jobs\QuickbooksSync; use QuickBooksOnline\API\DataService\DataService; +use App\Services\Quickbooks\Jobs\QuickbooksImport; use App\Services\Quickbooks\Transformers\ClientTransformer; use App\Services\Quickbooks\Transformers\InvoiceTransformer; use App\Services\Quickbooks\Transformers\PaymentTransformer; @@ -38,7 +38,7 @@ class QuickbooksService public QbProduct $product; - public array $settings; + public QuickbooksSync $settings; private bool $testMode = true; @@ -100,7 +100,7 @@ class QuickbooksService */ public function syncFromQb(): void { - QuickbooksSync::dispatch($this->company->id, $this->company->db); + QuickbooksImport::dispatch($this->company->id, $this->company->db); } } From f739936fefa48cab3b750b3c57a5051c2c048b84 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 07:59:54 +1000 Subject: [PATCH 13/55] Rename quickbooks import --- .../Quickbooks/Jobs/QuickbooksImport.php | 437 ++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 app/Services/Quickbooks/Jobs/QuickbooksImport.php diff --git a/app/Services/Quickbooks/Jobs/QuickbooksImport.php b/app/Services/Quickbooks/Jobs/QuickbooksImport.php new file mode 100644 index 000000000000..842c7ff34233 --- /dev/null +++ b/app/Services/Quickbooks/Jobs/QuickbooksImport.php @@ -0,0 +1,437 @@ + 'Item', + 'client' => 'Customer', + 'invoice' => 'Invoice', + // 'quote' => 'Estimate', + // 'purchase_order' => 'PurchaseOrder', + // 'payment' => 'Payment', + 'sales' => 'SalesReceipt', + // 'vendor' => 'Vendor', + // 'expense' => 'Purchase', + ]; + + private QuickbooksService $qbs; + + private QuickbooksSync $settings; + + private Company $company; + + public function __construct(public int $company_id, public string $db) + { + } + + /** + * Execute the job. + */ + public function handle() + { + MultiDB::setDb($this->db); + + $this->company = Company::query()->find($this->company_id); + $this->qbs = new QuickbooksService($this->company); + $this->settings = $this->company->quickbooks->settings; + + foreach($this->entities as $key => $entity) { + + nlog($key); + + if(!$this->syncGate($key, 'pull')) { + nlog('skipping ' . $key); + continue; + } + + $records = $this->qbs->sdk()->fetchRecords($entity); + + $this->processEntitySync($key, $records); + + } + + } + + /** + * Determines whether a sync is allowed based on the settings + * + * @param string $entity + * @param string $direction + * @return bool + */ + private function syncGate(string $entity, string $direction): bool + { + return (bool) $this->settings->{$entity}->sync && in_array($this->settings->{$entity}->direction->value, [$direction, 'bidirectional']); + } + + /** + * Updates the gate for a given entity + * + * @param string $entity + * @return bool + */ + private function updateGate(string $entity): bool + { + return (bool) $this->settings->{$entity}->sync && $this->settings->{$entity}->update_record; + } + + /** + * Processes the sync for a given entity + * + * @param string $entity + * @param mixed $records + * @return void + */ + private function processEntitySync(string $entity, $records): void + { + match($entity){ + 'client' => $this->syncQbToNinjaClients($records), + 'product' => $this->qbs->product->syncToNinja($records), + // 'invoice' => $this->syncQbToNinjaInvoices($records), + // 'sales' => $this->syncQbToNinjaInvoices($records), + // 'vendor' => $this->syncQbToNinjaVendors($records), + // 'quote' => $this->syncInvoices($records), + // 'expense' => $this->syncQbToNinjaExpenses($records), + // 'purchase_order' => $this->syncInvoices($records), + // 'payment' => $this->syncPayment($records), + default => false, + }; + } + + private function syncQbToNinjaInvoices($records): void + { + nlog("invoice sync ". count($records)); + $invoice_transformer = new InvoiceTransformer($this->company); + + foreach($records as $record) + { + nlog($record); + + $ninja_invoice_data = $invoice_transformer->qbToNinja($record); + + nlog($ninja_invoice_data); + + $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; + + $client_id = $ninja_invoice_data['client_id'] ?? null; + + if(is_null($client_id)) + continue; + + unset($ninja_invoice_data['payment_ids']); + + if($invoice = $this->findInvoice($ninja_invoice_data)) + { + $invoice->fill($ninja_invoice_data); + $invoice->saveQuietly(); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->createInvitations()->save(); + + foreach($payment_ids as $payment_id) + { + + $payment = $this->qbs->sdk->FindById('Payment', $payment_id); + + $payment_transformer = new PaymentTransformer($this->company); + + $transformed = $payment_transformer->qbToNinja($payment); + + $ninja_payment = $payment_transformer->buildPayment($payment); + $ninja_payment->service()->applyNumber()->save(); + + $paymentable = new \App\Models\Paymentable(); + $paymentable->payment_id = $ninja_payment->id; + $paymentable->paymentable_id = $invoice->id; + $paymentable->paymentable_type = 'invoices'; + $paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount'); + $paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line + $paymentable->save(); + + $invoice->service()->applyPayment($ninja_payment, $paymentable->amount); + + } + + if($record instanceof IPPSalesReceipt) + { + $invoice->service()->markPaid()->save(); + } + + } + + $ninja_invoice_data = false; + + } + + } + + private function findInvoice(array $ninja_invoice_data): ?Invoice + { + $search = Invoice::query() + ->withTrashed() + ->where('company_id', $this->company->id) + // ->where('number', $ninja_invoice_data['number']); + ->where('sync->qb_id', $ninja_invoice_data['id']); + + if($search->count() == 0) { + //new invoice + $invoice = InvoiceFactory::create($this->company->id, $this->company->owner()->id); + $invoice->client_id = $ninja_invoice_data['client_id']; + + return $invoice; + } elseif($search->count() == 1) { + return $this->settings->invoice->update_record ? $search->first() : null; + } + + return null; + + } + + private function syncQbToNinjaClients(array $records): void + { + + $client_transformer = new ClientTransformer($this->company); + + foreach($records as $record) + { + $ninja_client_data = $client_transformer->qbToNinja($record); + + if($client = $this->findClient($ninja_client_data)) + { + $client->fill($ninja_client_data[0]); + $client->saveQuietly(); + + $contact = $client->contacts()->where('email', $ninja_client_data[1]['email'])->first(); + + if(!$contact) + { + $contact = ClientContactFactory::create($this->company->id, $this->company->owner()->id); + $contact->client_id = $client->id; + $contact->send_email = true; + $contact->is_primary = true; + $contact->fill($ninja_client_data[1]); + $contact->saveQuietly(); + } + elseif($this->updateGate('client')){ + $contact->fill($ninja_client_data[1]); + $contact->saveQuietly(); + } + + } + + } + } + + private function syncQbToNinjaVendors(array $records): void + { + + $transformer = new VendorTransformer($this->company); + + foreach($records as $record) + { + $ninja_data = $transformer->qbToNinja($record); + + if($vendor = $this->findVendor($ninja_data)) + { + $vendor->fill($ninja_data[0]); + $vendor->saveQuietly(); + + $contact = $vendor->contacts()->where('email', $ninja_data[1]['email'])->first(); + + if(!$contact) + { + $contact = VendorContactFactory::create($this->company->id, $this->company->owner()->id); + $contact->vendor_id = $vendor->id; + $contact->send_email = true; + $contact->is_primary = true; + $contact->fill($ninja_data[1]); + $contact->saveQuietly(); + } + elseif($this->updateGate('vendor')){ + $contact->fill($ninja_data[1]); + $contact->saveQuietly(); + } + + } + + } + } + + private function syncQbToNinjaExpenses(array $records): void + { + + $transformer = new ExpenseTransformer($this->company); + + foreach($records as $record) + { + $ninja_data = $transformer->qbToNinja($record); + + if($expense = $this->findExpense($ninja_data)) + { + $expense->fill($ninja_data); + $expense->saveQuietly(); + } + + } + } + + + private function syncQbToNinjaProducts($records): void + { + $product_transformer = new ProductTransformer($this->company); + + foreach($records as $record) + { + $ninja_data = $product_transformer->qbToNinja($record); + + if($product = $this->findProduct($ninja_data['hash'])) + { + $product->fill($ninja_data); + $product->save(); + } + } + } + + private function findExpense(array $qb_data): ?Expense + { + $expense = $qb_data; + + $search = Expense::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where('number', $expense['number']); + + if($search->count() == 0) { + return ExpenseFactory::create($this->company->id, $this->company->owner()->id); + } + elseif($search->count() == 1) { + return $this->settings->expense->update_record ? $search->first() : null; + } + + return null; + } + + private function findVendor(array $qb_data) :?Vendor + { + $vendor = $qb_data[0]; + $contact = $qb_data[1]; + $vendor_meta = $qb_data[2]; + + $search = Vendor::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where(function ($q) use ($vendor, $vendor_meta, $contact){ + + $q->where('vendor_hash', $vendor_meta['vendor_hash']) + ->orWhere('number', $vendor['number']) + ->orWhereHas('contacts', function ($q) use ($contact){ + $q->where('email', $contact['email']); + }); + + }); + + if($search->count() == 0) { + //new client + return VendorFactory::create($this->company->id, $this->company->owner()->id); + } + elseif($search->count() == 1) { + return $this->settings->vendor->update_record ? $search->first() : null; + } + + return null; + } + + private function findClient(array $qb_data) :?Client + { + $client = $qb_data[0]; + $contact = $qb_data[1]; + $client_meta = $qb_data[2]; + + $search = Client::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where(function ($q) use ($client, $client_meta, $contact){ + + $q->where('client_hash', $client_meta['client_hash']) + ->orWhere('number', $client['number']) + ->orWhereHas('contacts', function ($q) use ($contact){ + $q->where('email', $contact['email']); + }); + + }); + + if($search->count() == 0) { + //new client + $client = ClientFactory::create($this->company->id, $this->company->owner()->id); + $client->client_hash = $client_meta['client_hash']; + $client->settings = $client_meta['settings']; + + return $client; + } + elseif($search->count() == 1) { + return $this->settings->client->update_record ? $search->first() : null; + } + + return null; + } + + + + public function middleware() + { + return [new WithoutOverlapping("qbs-{$this->company_id}-{$this->db}")]; + } + + public function failed($exception) + { + nlog("QuickbooksSync failed => ".$exception->getMessage()); + config(['queue.failed.driver' => null]); + + } +} From 8f83ca660c8e1254847b2166e9d851844e27d3b0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 12:43:43 +1000 Subject: [PATCH 14/55] Wire up quickbooks webhooks --- .../Quickbooks/Jobs/QuickbooksImport.php | 75 +++++++------------ app/Services/Quickbooks/Models/QbClient.php | 48 ++++++++++-- app/Services/Quickbooks/Models/QbProduct.php | 35 +++++++-- app/Services/Quickbooks/QuickbooksService.php | 33 ++++++++ app/Services/Quickbooks/SdkWrapper.php | 14 +++- .../Transformers/ClientTransformer.php | 15 ++-- 6 files changed, 156 insertions(+), 64 deletions(-) diff --git a/app/Services/Quickbooks/Jobs/QuickbooksImport.php b/app/Services/Quickbooks/Jobs/QuickbooksImport.php index 842c7ff34233..9e99d1c560e1 100644 --- a/app/Services/Quickbooks/Jobs/QuickbooksImport.php +++ b/app/Services/Quickbooks/Jobs/QuickbooksImport.php @@ -84,8 +84,6 @@ class QuickbooksImport implements ShouldQueue foreach($this->entities as $key => $entity) { - nlog($key); - if(!$this->syncGate($key, 'pull')) { nlog('skipping ' . $key); continue; @@ -132,7 +130,7 @@ class QuickbooksImport implements ShouldQueue private function processEntitySync(string $entity, $records): void { match($entity){ - 'client' => $this->syncQbToNinjaClients($records), + 'client' => $this->qbs->client->syncToNinja($records), 'product' => $this->qbs->product->syncToNinja($records), // 'invoice' => $this->syncQbToNinjaInvoices($records), // 'sales' => $this->syncQbToNinjaInvoices($records), @@ -233,40 +231,40 @@ class QuickbooksImport implements ShouldQueue } - private function syncQbToNinjaClients(array $records): void - { + // private function syncQbToNinjaClients(array $records): void + // { - $client_transformer = new ClientTransformer($this->company); + // $client_transformer = new ClientTransformer($this->company); - foreach($records as $record) - { - $ninja_client_data = $client_transformer->qbToNinja($record); + // foreach($records as $record) + // { + // $ninja_client_data = $client_transformer->qbToNinja($record); - if($client = $this->findClient($ninja_client_data)) - { - $client->fill($ninja_client_data[0]); - $client->saveQuietly(); + // if($client = $this->findClient($ninja_client_data)) + // { + // $client->fill($ninja_client_data[0]); + // $client->saveQuietly(); - $contact = $client->contacts()->where('email', $ninja_client_data[1]['email'])->first(); + // $contact = $client->contacts()->where('email', $ninja_client_data[1]['email'])->first(); - if(!$contact) - { - $contact = ClientContactFactory::create($this->company->id, $this->company->owner()->id); - $contact->client_id = $client->id; - $contact->send_email = true; - $contact->is_primary = true; - $contact->fill($ninja_client_data[1]); - $contact->saveQuietly(); - } - elseif($this->updateGate('client')){ - $contact->fill($ninja_client_data[1]); - $contact->saveQuietly(); - } + // if(!$contact) + // { + // $contact = ClientContactFactory::create($this->company->id, $this->company->owner()->id); + // $contact->client_id = $client->id; + // $contact->send_email = true; + // $contact->is_primary = true; + // $contact->fill($ninja_client_data[1]); + // $contact->saveQuietly(); + // } + // elseif($this->updateGate('client')){ + // $contact->fill($ninja_client_data[1]); + // $contact->saveQuietly(); + // } - } + // } - } - } + // } + // } private function syncQbToNinjaVendors(array $records): void { @@ -321,23 +319,6 @@ class QuickbooksImport implements ShouldQueue } } - - private function syncQbToNinjaProducts($records): void - { - $product_transformer = new ProductTransformer($this->company); - - foreach($records as $record) - { - $ninja_data = $product_transformer->qbToNinja($record); - - if($product = $this->findProduct($ninja_data['hash'])) - { - $product->fill($ninja_data); - $product->save(); - } - } - } - private function findExpense(array $qb_data): ?Expense { $expense = $qb_data; diff --git a/app/Services/Quickbooks/Models/QbClient.php b/app/Services/Quickbooks/Models/QbClient.php index 1528aca44064..97484aa51744 100644 --- a/app/Services/Quickbooks/Models/QbClient.php +++ b/app/Services/Quickbooks/Models/QbClient.php @@ -11,12 +11,13 @@ namespace App\Services\Quickbooks\Models; -use App\DataMapper\ClientSync; -use App\Services\Quickbooks\QuickbooksService; use App\Models\Client; +use App\DataMapper\ClientSync; use App\Factory\ClientFactory; -use App\Services\Quickbooks\Transformers\ClientTransformer; use App\Interfaces\SyncInterface; +use App\Factory\ClientContactFactory; +use App\Services\Quickbooks\QuickbooksService; +use App\Services\Quickbooks\Transformers\ClientTransformer; class QbClient implements SyncInterface { @@ -38,9 +39,41 @@ class QbClient implements SyncInterface $ninja_data = $transformer->qbToNinja($record); - if ($client = $this->findClient($ninja_data['id'])) { - $client->fill($ninja_data); + if($ninja_data[0]['terms']){ + + $days = $this->service->findEntityById('Term', $ninja_data[0]['terms']); + + nlog($days); + + if($days){ + $ninja_data[0]['settings']->payment_terms = (string)$days->DueDays; + } + + } + + if ($client = $this->findClient($ninja_data[0]['id'])) { + + $qbc = $this->find($ninja_data[0]['id']); + + $client->fill($ninja_data[0]); $client->service()->applyNumber()->save(); + + $contact = $client->contacts()->where('email', $ninja_data[1]['email'])->first(); + + if(!$contact) + { + $contact = ClientContactFactory::create($this->service->company->id, $this->service->company->owner()->id); + $contact->client_id = $client->id; + $contact->send_email = true; + $contact->is_primary = true; + $contact->fill($ninja_data[1]); + $contact->saveQuietly(); + } + elseif($this->updateGate('client')){ + $contact->fill($ninja_data[1]); + $contact->saveQuietly(); + } + } } @@ -50,6 +83,11 @@ class QbClient implements SyncInterface { } + private function updateGate(string $entity): bool + { + return (bool) $this->service->settings->{$entity}->sync && $this->service->settings->{$entity}->update_record; + } + private function findClient(string $key): ?Client { $search = Client::query() diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php index 4c5ca8d2b835..d7c05c844630 100644 --- a/app/Services/Quickbooks/Models/QbProduct.php +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -11,18 +11,24 @@ namespace App\Services\Quickbooks\Models; -use App\DataMapper\ProductSync; -use App\Services\Quickbooks\QuickbooksService; +use Carbon\Carbon; use App\Models\Product; +use App\DataMapper\ProductSync; use App\Factory\ProductFactory; -use App\Services\Quickbooks\Transformers\ProductTransformer; use App\Interfaces\SyncInterface; +use App\Services\Quickbooks\QuickbooksService; +use App\Services\Quickbooks\Transformers\ProductTransformer; class QbProduct implements SyncInterface { + protected ProductTransformer $product_transformer; + public function __construct(public QuickbooksService $service) { + + $this->product_transformer = new ProductTransformer($service->company); + } public function find(string $id): mixed @@ -33,11 +39,9 @@ class QbProduct implements SyncInterface public function syncToNinja(array $records): void { - $product_transformer = new ProductTransformer($this->service->company); - foreach ($records as $record) { - $ninja_data = $product_transformer->qbToNinja($record); + $ninja_data = $this->product_transformer->qbToNinja($record); if ($product = $this->findProduct($ninja_data['id'])) { $product->fill($ninja_data); @@ -74,6 +78,25 @@ class QbProduct implements SyncInterface return null; + } + + public function sync(string $id): void + { + $qb_record = $this->find($id); + + if($ninja_record = $this->findProduct($id)) + { + + if(Carbon::parse($qb_record->lastUpdated) > Carbon::parse($ninja_record->updated_at)) + { + $transformed_qb_product = $this->product_transformer($qb_record); + + $ninja_record->fill($ninja_data); + $ninja_record->save(); + + } + + } } } diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index 3083883e17eb..d2f0d691d922 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -25,6 +25,7 @@ use App\Services\Quickbooks\Models\QbInvoice; use App\Services\Quickbooks\Models\QbProduct; use QuickBooksOnline\API\DataService\DataService; use App\Services\Quickbooks\Jobs\QuickbooksImport; +use App\Services\Quickbooks\Models\QbClient; use App\Services\Quickbooks\Transformers\ClientTransformer; use App\Services\Quickbooks\Transformers\InvoiceTransformer; use App\Services\Quickbooks\Transformers\PaymentTransformer; @@ -38,10 +39,14 @@ class QuickbooksService public QbProduct $product; + public QbClient $client; + public QuickbooksSync $settings; private bool $testMode = true; + private bool $try_refresh = true; + public function __construct(public Company $company) { $this->init(); @@ -70,15 +75,39 @@ class QuickbooksService $this->sdk->setMinorVersion("73"); $this->sdk->throwExceptionOnError(true); + $this->checkToken(); + $this->invoice = new QbInvoice($this); $this->product = new QbProduct($this); + $this->client = new QbClient($this); + $this->settings = $this->company->quickbooks->settings; return $this; } + private function checkToken(): self + { + + if($this->company->quickbooks->accessTokenKey > time()) + return $this; + + if($this->company->quickbooks->accessTokenExpiresAt < time() && $this->try_refresh){ + $this->sdk()->refreshToken($this->company->quickbooks->refresh_token); + $this->company = $this->company->fresh(); + $this->try_refresh = false; + $this->init(); + + return $this; + } + + nlog('Quickbooks token expired and could not be refreshed => ' .$this->company->company_key); + throw new \Exception('Quickbooks token expired and could not be refreshed'); + + } + private function ninjaAccessToken(): array { return isset($this->company->quickbooks->accessTokenKey) ? [ @@ -103,4 +132,8 @@ class QuickbooksService QuickbooksImport::dispatch($this->company->id, $this->company->db); } + public function findEntityById(string $entity, string $id): mixed + { + return $this->sdk->FindById($entity, $id); + } } diff --git a/app/Services/Quickbooks/SdkWrapper.php b/app/Services/Quickbooks/SdkWrapper.php index 2845b747ab89..c0ded99c650d 100644 --- a/app/Services/Quickbooks/SdkWrapper.php +++ b/app/Services/Quickbooks/SdkWrapper.php @@ -105,7 +105,7 @@ class SdkWrapper $this->setAccessToken($token); if($token_object->accessTokenExpiresAt < time()){ - $new_token = $this->sdk->getOAuth2LoginHelper()->refreshToken(); + $new_token = $this->sdk->getOAuth2LoginHelper()->refreshAccessTokenWithRefreshToken($token_object->refresh_token); $this->setAccessToken($new_token); $this->saveOAuthToken($this->accessToken()); @@ -114,6 +114,18 @@ class SdkWrapper return $this; } + + public function refreshToken(string $refresh_token): self + { + $new_token = $this->sdk->getOAuth2LoginHelper()->refreshAccessTokenWithRefreshToken($refresh_token); + + nlog($new_token); + $this->setAccessToken($new_token); + $this->saveOAuthToken($this->accessToken()); + + return $this; + } + /** * SetsAccessToken * diff --git a/app/Services/Quickbooks/Transformers/ClientTransformer.php b/app/Services/Quickbooks/Transformers/ClientTransformer.php index 69b0566ded99..6a320cb70f91 100644 --- a/app/Services/Quickbooks/Transformers/ClientTransformer.php +++ b/app/Services/Quickbooks/Transformers/ClientTransformer.php @@ -31,6 +31,7 @@ class ClientTransformer extends BaseTransformer public function transform(mixed $data): array { + nlog($data); $contact = [ 'first_name' => data_get($data, 'GivenName'), @@ -54,16 +55,20 @@ class ClientTransformer extends BaseTransformer 'shipping_country_id' => $this->resolveCountry(data_get($data, 'ShipAddr.Country', '')), 'shipping_state' => data_get($data, 'ShipAddr.CountrySubDivisionCode', ''), 'shipping_postal_code' => data_get($data, 'BillAddr.PostalCode', ''), - 'number' => data_get($data, 'Id.value', ''), + 'client_hash' => data_get($data, 'V4IDPseudonym', \Illuminate\Support\Str::random(32)), + 'vat_number' => data_get($data, 'PrimaryTaxIdentifier', ''), + 'id_number' => data_get($data, 'BusinessNumber', ''), + 'terms' => data_get($data, 'SalesTermRef.value', false), + 'is_tax_exempt' => !data_get($data, 'Taxable', false), + 'private_notes' => data_get($data, 'Notes', ''), ]; $settings = ClientSettings::defaults(); $settings->currency_id = (string) $this->resolveCurrency(data_get($data, 'CurrencyRef.value')); - $new_client_merge = [ - 'client_hash' => data_get($data, 'V4IDPseudonym', \Illuminate\Support\Str::random(32)), - 'settings' => $settings, - ]; + $client['settings'] = $settings; + + $new_client_merge = []; return [$client, $contact, $new_client_merge]; } From d87f8d8be011c4dc071364c6d2b2ae46887ccfa3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 12:44:34 +1000 Subject: [PATCH 15/55] Fixes for refreshing tokens --- app/Services/Quickbooks/SdkWrapper.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/Services/Quickbooks/SdkWrapper.php b/app/Services/Quickbooks/SdkWrapper.php index c0ded99c650d..d6ac56f43dfb 100644 --- a/app/Services/Quickbooks/SdkWrapper.php +++ b/app/Services/Quickbooks/SdkWrapper.php @@ -105,10 +105,7 @@ class SdkWrapper $this->setAccessToken($token); if($token_object->accessTokenExpiresAt < time()){ - $new_token = $this->sdk->getOAuth2LoginHelper()->refreshAccessTokenWithRefreshToken($token_object->refresh_token); - - $this->setAccessToken($new_token); - $this->saveOAuthToken($this->accessToken()); + $this->refreshToken($token_object->refresh_token); } return $this; @@ -119,7 +116,6 @@ class SdkWrapper { $new_token = $this->sdk->getOAuth2LoginHelper()->refreshAccessTokenWithRefreshToken($refresh_token); - nlog($new_token); $this->setAccessToken($new_token); $this->saveOAuthToken($this->accessToken()); From 8279bc2ed264ac4e058a33b5a6fbfba8f2fb6693 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 12:50:19 +1000 Subject: [PATCH 16/55] Product sync for the win! --- app/Services/Quickbooks/Models/QbProduct.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php index d7c05c844630..2e413c52588d 100644 --- a/app/Services/Quickbooks/Models/QbProduct.php +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -80,16 +80,16 @@ class QbProduct implements SyncInterface } - public function sync(string $id): void + public function sync(string $id, string $last_updated): void { $qb_record = $this->find($id); if($ninja_record = $this->findProduct($id)) { - if(Carbon::parse($qb_record->lastUpdated) > Carbon::parse($ninja_record->updated_at)) + if(Carbon::parse($last_updated) > Carbon::parse($ninja_record->updated_at)) { - $transformed_qb_product = $this->product_transformer($qb_record); + $ninja_data = $this->product_transformer->qbToNinja($qb_record); $ninja_record->fill($ninja_data); $ninja_record->save(); From 80f34caccf878e60743af40ebbdea31fd88a9e11 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 12:57:27 +1000 Subject: [PATCH 17/55] Quickbooks sync --- .../Quickbooks/Jobs/QuickbooksImport.php | 62 +------------------ app/Services/Quickbooks/Models/QbClient.php | 7 +-- app/Services/Quickbooks/QuickbooksService.php | 24 +++++++ 3 files changed, 27 insertions(+), 66 deletions(-) diff --git a/app/Services/Quickbooks/Jobs/QuickbooksImport.php b/app/Services/Quickbooks/Jobs/QuickbooksImport.php index 9e99d1c560e1..a6ff0a385a29 100644 --- a/app/Services/Quickbooks/Jobs/QuickbooksImport.php +++ b/app/Services/Quickbooks/Jobs/QuickbooksImport.php @@ -84,7 +84,7 @@ class QuickbooksImport implements ShouldQueue foreach($this->entities as $key => $entity) { - if(!$this->syncGate($key, 'pull')) { + if(!$this->qbs->syncGate($key, 'pull')) { nlog('skipping ' . $key); continue; } @@ -97,29 +97,6 @@ class QuickbooksImport implements ShouldQueue } - /** - * Determines whether a sync is allowed based on the settings - * - * @param string $entity - * @param string $direction - * @return bool - */ - private function syncGate(string $entity, string $direction): bool - { - return (bool) $this->settings->{$entity}->sync && in_array($this->settings->{$entity}->direction->value, [$direction, 'bidirectional']); - } - - /** - * Updates the gate for a given entity - * - * @param string $entity - * @return bool - */ - private function updateGate(string $entity): bool - { - return (bool) $this->settings->{$entity}->sync && $this->settings->{$entity}->update_record; - } - /** * Processes the sync for a given entity * @@ -231,41 +208,6 @@ class QuickbooksImport implements ShouldQueue } - // private function syncQbToNinjaClients(array $records): void - // { - - // $client_transformer = new ClientTransformer($this->company); - - // foreach($records as $record) - // { - // $ninja_client_data = $client_transformer->qbToNinja($record); - - // if($client = $this->findClient($ninja_client_data)) - // { - // $client->fill($ninja_client_data[0]); - // $client->saveQuietly(); - - // $contact = $client->contacts()->where('email', $ninja_client_data[1]['email'])->first(); - - // if(!$contact) - // { - // $contact = ClientContactFactory::create($this->company->id, $this->company->owner()->id); - // $contact->client_id = $client->id; - // $contact->send_email = true; - // $contact->is_primary = true; - // $contact->fill($ninja_client_data[1]); - // $contact->saveQuietly(); - // } - // elseif($this->updateGate('client')){ - // $contact->fill($ninja_client_data[1]); - // $contact->saveQuietly(); - // } - - // } - - // } - // } - private function syncQbToNinjaVendors(array $records): void { @@ -291,7 +233,7 @@ class QuickbooksImport implements ShouldQueue $contact->fill($ninja_data[1]); $contact->saveQuietly(); } - elseif($this->updateGate('vendor')){ + elseif($this->qbs->updateGate('vendor')){ $contact->fill($ninja_data[1]); $contact->saveQuietly(); } diff --git a/app/Services/Quickbooks/Models/QbClient.php b/app/Services/Quickbooks/Models/QbClient.php index 97484aa51744..cf7b773622c7 100644 --- a/app/Services/Quickbooks/Models/QbClient.php +++ b/app/Services/Quickbooks/Models/QbClient.php @@ -69,7 +69,7 @@ class QbClient implements SyncInterface $contact->fill($ninja_data[1]); $contact->saveQuietly(); } - elseif($this->updateGate('client')){ + elseif($this->service->updateGate('client')){ $contact->fill($ninja_data[1]); $contact->saveQuietly(); } @@ -83,11 +83,6 @@ class QbClient implements SyncInterface { } - private function updateGate(string $entity): bool - { - return (bool) $this->service->settings->{$entity}->sync && $this->service->settings->{$entity}->update_record; - } - private function findClient(string $key): ?Client { $search = Client::query() diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index d2f0d691d922..3f9f0302a6da 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -136,4 +136,28 @@ class QuickbooksService { return $this->sdk->FindById($entity, $id); } + + /** + * Tests whether to update a record based on the sync settings. + * + * @param string $entity + * @return bool + */ + public function updateGate(string $entity): bool + { + return (bool) $this->service->settings->{$entity}->sync && $this->service->settings->{$entity}->update_record; + } + + /** + * Determines whether a sync is allowed based on the settings + * + * @param string $entity + * @param string $direction + * @return bool + */ + public function syncGate(string $entity, string $direction): bool + { + return (bool) $this->settings->{$entity}->sync && in_array($this->settings->{$entity}->direction->value, [$direction, 'bidirectional']); + } + } From 28775c087e6d34024fe2d821bd5720d475968e35 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 13:36:12 +1000 Subject: [PATCH 18/55] Quickbooks sync --- app/DataMapper/QuickbooksSyncMap.php | 7 +++++++ app/Filters/BankTransactionFilters.php | 2 +- app/Services/EDocument/Imports/ZugferdEDocument.php | 1 + app/Services/Quickbooks/Models/QbProduct.php | 4 ++-- app/Services/Quickbooks/QuickbooksService.php | 2 +- .../Quickbooks/Transformers/PaymentTransformer.php | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/DataMapper/QuickbooksSyncMap.php b/app/DataMapper/QuickbooksSyncMap.php index b4d1e203b18c..ef3da4f34c96 100644 --- a/app/DataMapper/QuickbooksSyncMap.php +++ b/app/DataMapper/QuickbooksSyncMap.php @@ -28,5 +28,12 @@ class QuickbooksSyncMap public bool $update_record = true; public SyncDirection $direction = SyncDirection::BIDIRECTIONAL; + + public function __construct(array $attributes = []) + { + $this->sync = $attributes['sync'] ?? true; + $this->update_record = $attributes['update_record'] ?? true; + $this->direction = $attributes['direction'] ?? SyncDirection::BIDIRECTIONAL; + } } diff --git a/app/Filters/BankTransactionFilters.php b/app/Filters/BankTransactionFilters.php index 2471f98aaa25..1e05a6d03741 100644 --- a/app/Filters/BankTransactionFilters.php +++ b/app/Filters/BankTransactionFilters.php @@ -68,7 +68,7 @@ class BankTransactionFilters extends QueryFilters */ public function client_status(string $value = ''): Builder { - if (strlen($value ?? '') == 0) { + if (strlen($value) == 0) { return $this->builder; } diff --git a/app/Services/EDocument/Imports/ZugferdEDocument.php b/app/Services/EDocument/Imports/ZugferdEDocument.php index cc70e4cd6694..bcc40624eef6 100644 --- a/app/Services/EDocument/Imports/ZugferdEDocument.php +++ b/app/Services/EDocument/Imports/ZugferdEDocument.php @@ -131,6 +131,7 @@ class ZugferdEDocument extends AbstractService $vendor->postal_code = $postcode; $country = app('countries')->first(function ($c) use ($country) { + /** @var \App\Models\Country $c */ return $c->iso_3166_2 == $country || $c->iso_3166_3 == $country; }); if ($country) diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php index 2e413c52588d..f9fb2a64418c 100644 --- a/app/Services/Quickbooks/Models/QbProduct.php +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -84,7 +84,7 @@ class QbProduct implements SyncInterface { $qb_record = $this->find($id); - if($ninja_record = $this->findProduct($id)) + if($this->service->updateGate('product') && $ninja_record = $this->findProduct($id)) { if(Carbon::parse($last_updated) > Carbon::parse($ninja_record->updated_at)) @@ -99,4 +99,4 @@ class QbProduct implements SyncInterface } } -} +} \ No newline at end of file diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index 3f9f0302a6da..f4d067256255 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -138,7 +138,7 @@ class QuickbooksService } /** - * Tests whether to update a record based on the sync settings. + * Updates the gate for a given entity * * @param string $entity * @return bool diff --git a/app/Services/Quickbooks/Transformers/PaymentTransformer.php b/app/Services/Quickbooks/Transformers/PaymentTransformer.php index e5eb7f38ae28..c29094c25323 100644 --- a/app/Services/Quickbooks/Transformers/PaymentTransformer.php +++ b/app/Services/Quickbooks/Transformers/PaymentTransformer.php @@ -91,7 +91,7 @@ class PaymentTransformer extends BaseTransformer if(!$credit_line) return $payment; - $credit = \App\Factory\CreditFactory::create($this->company->id, $this->company->owner()->id, $payment->client_id); + $credit = \App\Factory\CreditFactory::create($this->company->id, $this->company->owner()->id); $credit->client_id = $payment->client_id; $line = new \App\DataMapper\InvoiceItem(); From acbb5d475c048fb89807e3928b23dd9c0e3c93a1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 16:34:13 +1000 Subject: [PATCH 19/55] Fixes for locked invoice status --- app/DataMapper/QuickbooksSyncMap.php | 5 ++++- app/Models/Invoice.php | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/DataMapper/QuickbooksSyncMap.php b/app/DataMapper/QuickbooksSyncMap.php index ef3da4f34c96..40250b61f103 100644 --- a/app/DataMapper/QuickbooksSyncMap.php +++ b/app/DataMapper/QuickbooksSyncMap.php @@ -33,7 +33,10 @@ class QuickbooksSyncMap { $this->sync = $attributes['sync'] ?? true; $this->update_record = $attributes['update_record'] ?? true; - $this->direction = $attributes['direction'] ?? SyncDirection::BIDIRECTIONAL; + $this->direction = isset($attributes['direction']) + ? SyncDirection::from($attributes['direction']) + : SyncDirection::BIDIRECTIONAL; + } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 203061b972d7..1cfd1df3404e 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -585,7 +585,7 @@ class Invoice extends BaseModel * Filtering logic to determine * whether an invoice is locked * based on the current status of the invoice. - * @return bool [description] + * @return bool */ public function isLocked(): bool { @@ -595,7 +595,7 @@ class Invoice extends BaseModel case 'off': return false; case 'when_sent': - return $this->status_id == self::STATUS_SENT; + return $this->status_id >= self::STATUS_SENT; case 'when_paid': return $this->status_id == self::STATUS_PAID || $this->status_id == self::STATUS_PARTIAL; case 'end_of_month': From fa1a7937e6a06b96b53a58528fb9b08a8ae4e929 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Sep 2024 17:08:10 +1000 Subject: [PATCH 20/55] Updated translations --- lang/fr_CA/texts.php | 1 + lang/vi/texts.php | 53 ++++++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index 2815c0f02bd4..e022ef5a26f0 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -5331,6 +5331,7 @@ Développe automatiquement la section des notes dans le tableau de produits pour 'country_Melilla' => 'Melilla', 'country_Ceuta' => 'Ceuta', 'country_Canary Islands' => 'Îles Canaries', + 'lang_Vietnamese' => 'Vietnamien', 'invoice_status_changed' => 'Veuillez noter que l\'état de votre facture a été mis à jour. Nous vous recommandons de rafraîchir la page pour afficher la version la plus récente.', 'no_unread_notifications' => 'Vous êtes à jour! Aucune nouvelle notification.', ); diff --git a/lang/vi/texts.php b/lang/vi/texts.php index 8abeb4361f43..9f608378d25e 100644 --- a/lang/vi/texts.php +++ b/lang/vi/texts.php @@ -41,7 +41,7 @@ $lang = array( 'quantity' => 'Số lượng', 'line_total' => 'Tổng', 'subtotal' => 'Thành tiền', - 'net_subtotal' => 'Tịnh', + 'net_subtotal' => 'Tính', 'paid_to_date' => 'Hạn thanh toán', 'balance_due' => 'Số tiền thanh toán', 'invoice_design_id' => 'Thiết kế', @@ -581,8 +581,8 @@ $lang = array( 'pro_plan_call_to_action' => 'Nâng cấp ngay!', 'pro_plan_feature1' => 'Tạo khách hàng không giới hạn', 'pro_plan_feature2' => 'Truy cập vào 10 mẫu thiết kế hóa đơn đẹp', - 'pro_plan_feature3' => 'URL tùy chỉnh - "YourBrand.InvoiceNinja.com"', - 'pro_plan_feature4' => 'Xóa "Được tạo bởi Invoice Ninja"', + 'pro_plan_feature3' => 'URL tùy chỉnh - "YourBrand.InvoiceNinja.com"', + 'pro_plan_feature4' => 'Xóa "Được tạo bởi Invoice Ninja"', 'pro_plan_feature5' => 'Truy cập nhiều người dùng & Theo dõi hoạt động', 'pro_plan_feature6' => 'Tạo báo giá và hóa đơn tạm tính', 'pro_plan_feature7' => 'Tùy chỉnh Tiêu đề và Số hiệu Trường Hóa đơn', @@ -729,7 +729,7 @@ $lang = array( 'invoice_counter' => 'Quầy tính tiền', 'quote_counter' => 'Bộ đếm Báo giá ', 'type' => 'Kiểu', - 'activity_1' => ':user đã tạo ra máy khách :client', + 'activity_1' => ':user đã tạo ra khách hàng :client', 'activity_2' => ':user khách hàng lưu trữ :client', 'activity_3' => ':user đã xóa máy khách :client', 'activity_4' => ':user đã tạo hóa đơn :invoice', @@ -799,8 +799,8 @@ $lang = array( 'archived_token' => 'Đã lưu trữ mã thông báo thành công', 'archive_user' => 'Lưu trữ người dùng', 'archived_user' => 'Đã lưu trữ người dùng thành công', - 'archive_account_gateway' => 'Xóa Cổng Thanh Toán', - 'archived_account_gateway' => 'Cổng lưu trữ thành công', + 'archive_account_gateway' => 'Xóa cổng thanh toán', + 'archived_account_gateway' => 'Lưu trữ cổng thành công', 'archive_recurring_invoice' => 'Lưu trữ hóa đơn định kỳ', 'archived_recurring_invoice' => 'Đã lưu trữ thành công hóa đơn định kỳ', 'delete_recurring_invoice' => 'Xóa hóa đơn định kỳ', @@ -959,7 +959,7 @@ $lang = array( 'quote_message_button' => 'Để xem báo giá cho :amount , hãy nhấp vào nút bên dưới.', 'payment_message_button' => 'Cảm ơn bạn đã thanh toán :amount .', 'payment_type_direct_debit' => 'Ghi nợ trực tiếp', - 'bank_accounts' => 'Thẻ tín dụng và ngân hàng', + 'bank_accounts' => 'Thẻ tín dụng & ngân hàng', 'add_bank_account' => 'Thêm tài khoản ngân hàng', 'setup_account' => 'Thiết lập tài khoản', 'import_expenses' => 'Chi phí nhập khẩu', @@ -1244,9 +1244,9 @@ $lang = array( 'confirm_remove_payment_method' => 'Bạn có chắc chắn muốn xóa phương thức thanh toán này không?', 'remove' => 'Di dời', 'payment_method_removed' => 'Đã xóa phương thức thanh toán.', - 'bank_account_verification_help' => 'Chúng tôi đã thực hiện hai khoản tiền gửi vào tài khoản của bạn với mô tả "XÁC MINH". Các khoản tiền gửi này sẽ mất 1-2 ngày làm việc để xuất hiện trên sao kê của bạn. Vui lòng nhập số tiền bên dưới.', - 'bank_account_verification_next_steps' => 'Chúng tôi đã thực hiện hai khoản tiền gửi vào tài khoản của bạn với mô tả "XÁC MINH". Các khoản tiền gửi này sẽ mất 1-2 ngày làm việc để hiển thị trên sao kê của bạn. - Khi đã có số tiền, hãy quay lại trang phương thức thanh toán này và nhấp vào "Hoàn tất xác minh" bên cạnh tài khoản.', + 'bank_account_verification_help' => 'Chúng tôi đã thực hiện hai khoản tiền gửi vào tài khoản của bạn với mô tả "XÁC MINH". Các khoản tiền gửi này sẽ mất 1-2 ngày làm việc để xuất hiện trên sao kê của bạn. Vui lòng nhập số tiền bên dưới.', + 'bank_account_verification_next_steps' => 'Chúng tôi đã thực hiện hai khoản tiền gửi vào tài khoản của bạn với mô tả "XÁC MINH". Các khoản tiền gửi này sẽ mất 1-2 ngày làm việc để hiển thị trên sao kê của bạn. + Khi đã có số tiền, hãy quay lại trang phương thức thanh toán này và nhấp vào "Hoàn tất xác minh" bên cạnh tài khoản.', 'unknown_bank' => 'Ngân hàng không xác định', 'ach_verification_delay_help' => 'Bạn sẽ có thể sử dụng tài khoản sau khi hoàn tất xác minh. Việc xác minh thường mất 1-2 ngày làm việc.', 'add_credit_card' => 'Thêm thẻ tín dụng', @@ -2096,7 +2096,7 @@ $lang = array( 'gateway_fees' => 'Phí cổng vào', 'fees_disabled' => 'Phí đã bị vô hiệu hóa', 'gateway_fees_help' => 'Tự động thêm phụ phí/chiết khấu khi thanh toán trực tuyến.', - 'gateway' => 'Cổng vào', + 'gateway' => 'Gateway', 'gateway_fee_change_warning' => 'Nếu có hóa đơn chưa thanh toán kèm phí, bạn cần cập nhật thủ công.', 'fees_surcharge_help' => 'Tùy chỉnh phụ phí :link .', 'label_and_taxes' => 'nhãn và thuế', @@ -2612,7 +2612,7 @@ $lang = array( 'signature_on_pdf_help' => 'Hiển thị chữ ký của khách hàng trên hóa đơn/báo giá PDF.', 'expired_white_label' => 'The white label license has expired', 'return_to_login' => 'Quay lại Đăng nhập', - 'convert_products_tip' => 'Lưu ý: thêm :link có tên " :name " để xem tỷ giá hối đoái.', + 'convert_products_tip' => 'Lưu ý: thêm :link có tên " :name " để xem tỷ giá hối đoái.', 'amount_greater_than_balance' => 'Số tiền lớn hơn số dư trên hóa đơn, chúng tôi sẽ tạo khoản tín dụng với số tiền còn lại.', 'custom_fields_tip' => 'Sử dụng Label|Option1,Option2 để hiển thị hộp chọn.', 'client_information' => 'Thông tin khách hàng', @@ -3062,8 +3062,8 @@ $lang = array( 'provider' => 'Nhà cung cấp', 'company_gateway' => 'Cổng thanh toán', 'company_gateways' => 'Cổng thanh toán', - 'new_company_gateway' => 'Cổng mới', - 'edit_company_gateway' => 'Chỉnh sửa Cổng', + 'new_company_gateway' => 'Gateway mới', + 'edit_company_gateway' => 'Chỉnh sửa Gateway', 'created_company_gateway' => 'Đã tạo cổng thành công', 'updated_company_gateway' => 'Đã cập nhật cổng thành công', 'archived_company_gateway' => 'Cổng lưu trữ thành công', @@ -3097,7 +3097,7 @@ $lang = array( 'uploaded_logo' => 'Đã tải logo thành công', 'saved_settings' => 'Đã lưu cài đặt thành công', 'device_settings' => 'Cài đặt thiết bị', - 'credit_cards_and_banks' => 'Thẻ tín dụng và ngân hàng', + 'credit_cards_and_banks' => 'Thẻ tín dụng & ngân hàng', 'price' => 'Giá', 'email_sign_up' => 'Đăng ký Email', 'google_sign_up' => 'Đăng ký Google', @@ -3648,7 +3648,7 @@ $lang = array( 'view_licenses' => 'Xem Giấy phép', 'fullscreen_editor' => 'Biên tập toàn màn hình', 'sidebar_editor' => 'Biên tập thanh bên', - 'please_type_to_confirm' => 'Vui lòng nhập " :value " để xác nhận', + 'please_type_to_confirm' => 'Vui lòng nhập ":value"để xác nhận', 'purge' => 'thanh lọc', 'clone_to' => 'Sao chép vào', 'clone_to_other' => 'Sao chép sang cái khác', @@ -3855,8 +3855,8 @@ $lang = array( 'recommended_in_production' => 'Rất khuyến khích trong sản xuất', 'enable_only_for_development' => 'Chỉ cho phép phát triển', 'test_pdf' => 'Kiểm tra PDF', - 'checkout_authorize_label' => 'Checkout.com có thể được lưu làm phương thức thanh toán để sử dụng sau này, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Lưu thông tin thẻ tín dụng" trong quá trình thanh toán.', - 'sofort_authorize_label' => 'Tài khoản ngân hàng (SOFORT) có thể được lưu làm phương thức thanh toán để sử dụng trong tương lai, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Lưu chi tiết thanh toán" trong quá trình thanh toán.', + 'checkout_authorize_label' => 'Checkout.com có thể được lưu làm phương thức thanh toán để sử dụng sau này, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra" Lưu thông tin thẻ tín dụng" trong quá trình thanh toán.', + 'sofort_authorize_label' => 'Tài khoản ngân hàng (SOFORT) có thể được lưu làm phương thức thanh toán để sử dụng trong tương lai, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Lưu chi tiết thanh toán" trong quá trình thanh toán.', 'node_status' => 'Trạng thái nút', 'npm_status' => 'Trạng thái NPM', 'node_status_not_found' => 'Tôi không tìm thấy Node ở đâu cả. Nó đã được cài đặt chưa?', @@ -3891,7 +3891,7 @@ $lang = array( 'payment_method_saving_failed' => 'Không thể lưu phương thức thanh toán để sử dụng sau này.', 'pay_with' => 'Thanh toán bằng', 'n/a' => 'Không có', - 'by_clicking_next_you_accept_terms' => 'Bằng cách nhấp vào "Tiếp theo", bạn chấp nhận các điều khoản.', + 'by_clicking_next_you_accept_terms' => 'Bằng cách nhấp vào "Tiếp theo", bạn chấp nhận các điều khoản.', 'not_specified' => 'Không xác định', 'before_proceeding_with_payment_warning' => 'Trước khi tiến hành thanh toán, bạn phải điền vào các trường sau', 'after_completing_go_back_to_previous_page' => 'Sau khi hoàn tất, hãy quay lại trang trước.', @@ -4072,7 +4072,7 @@ $lang = array( 'max_companies_desc' => 'Bạn đã đạt đến số lượng công ty tối đa. Xóa các công ty hiện có để di chuyển các công ty mới.', 'migration_already_completed' => 'Công ty đã di chuyển', 'migration_already_completed_desc' => 'Có vẻ như bạn đã di chuyển :company _name sang phiên bản V5 của Invoice Ninja. Trong trường hợp bạn muốn bắt đầu lại, bạn có thể buộc di chuyển để xóa dữ liệu hiện có.', - 'payment_method_cannot_be_authorized_first' => 'Phương thức thanh toán này có thể được lưu lại để sử dụng sau này, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Chi tiết cửa hàng" trong quá trình thanh toán.', + 'payment_method_cannot_be_authorized_first' => 'Phương thức thanh toán này có thể được lưu lại để sử dụng sau này, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Chi tiết cửa hàng" trong quá trình thanh toán.', 'new_account' => 'Tài khoản mới', 'activity_100' => ':user đã tạo hóa đơn định kỳ :recurring_invoice', 'activity_101' => ':user hóa đơn định kỳ đã cập nhật :recurring_invoice', @@ -4110,7 +4110,7 @@ $lang = array( 'one_time_purchases' => 'Mua một lần', 'recurring_purchases' => 'Mua hàng định kỳ', 'you_might_be_interested_in_following' => 'Bạn có thể quan tâm đến những điều sau đây', - 'quotes_with_status_sent_can_be_approved' => 'Chỉ những báo giá có trạng thái "Đã gửi" mới được chấp thuận. Không thể chấp thuận những báo giá đã hết hạn.', + 'quotes_with_status_sent_can_be_approved' => 'Chỉ những báo giá có trạng thái "Đã gửi" mới được chấp thuận. Không thể chấp thuận những báo giá đã hết hạn.', 'no_quotes_available_for_download' => 'Không có báo giá nào có sẵn để tải xuống.', 'copyright' => 'Bản quyền', 'user_created_user' => ':user đã tạo :created_user tại :time', @@ -4287,7 +4287,7 @@ $lang = array( 'migration_not_yet_completed' => 'Việc di chuyển vẫn chưa hoàn tất', 'show_task_end_date' => 'Hiển thị ngày kết thúc nhiệm vụ', 'show_task_end_date_help' => 'Cho phép chỉ định ngày kết thúc nhiệm vụ', - 'gateway_setup' => 'Thiết lập cổng', + 'gateway_setup' => 'Cài đặt Gateway', 'preview_sidebar' => 'Xem trước thanh bên', 'years_data_shown' => 'Dữ liệu năm được hiển thị', 'ended_all_sessions' => 'Đã kết thúc thành công tất cả các phiên', @@ -4394,7 +4394,7 @@ $lang = array( 'document_download_subject' => 'Tài liệu của bạn đã sẵn sàng để tải xuống', 'reminder_message' => 'Nhắc nhở về hóa đơn :number cho :balance', 'gmail_credentials_invalid_subject' => 'Gửi bằng GMail thông tin đăng nhập không hợp lệ', - 'gmail_credentials_invalid_body' => 'Thông tin đăng nhập GMail của bạn không đúng, vui lòng đăng nhập vào cổng thông tin quản trị viên và điều hướng đến Cài đặt > Chi tiết người dùng và ngắt kết nối và kết nối lại tài khoản GMail của bạn. Chúng tôi sẽ gửi cho bạn thông báo này hàng ngày cho đến khi sự cố này được giải quyết', + 'gmail_credentials_invalid_body' => 'Thông tin đăng nhập GMail của bạn không đúng, vui lòng đăng nhập vào cổng thông tin quản trị viên và điều hướng đến Cài đặt > Chi tiết người dùng và ngắt kết nối và kết nối lại tài khoản GMail của bạn. Chúng tôi sẽ gửi cho bạn thông báo này hàng ngày cho đến khi sự cố này được giải quyết', 'total_columns' => 'Tổng số trường', 'view_task' => 'Xem Nhiệm vụ', 'cancel_invoice' => 'Hủy bỏ', @@ -4419,8 +4419,8 @@ $lang = array( 'signed_in_as' => 'Đã đăng nhập như', 'total_results' => 'Tổng kết quả', 'restore_company_gateway' => 'Khôi phục cổng', - 'archive_company_gateway' => 'Cổng lưu trữ', - 'delete_company_gateway' => 'Xóa cổng', + 'archive_company_gateway' => 'Lưu trữ gateway', + 'delete_company_gateway' => 'Xóa gateway', 'exchange_currency' => 'Trao đổi tiền tệ', 'tax_amount1' => 'Số tiền thuế 1', 'tax_amount2' => 'Số tiền thuế 2', @@ -4506,7 +4506,7 @@ $lang = array( 'add' => 'Thêm vào', 'last_sent_template' => 'Mẫu gửi cuối cùng', 'enable_flexible_search' => 'Bật Tìm kiếm linh hoạt', - 'enable_flexible_search_help' => 'Phù hợp với các ký tự không liền kề, ví dụ: "ct" phù hợp với "cat"', + 'enable_flexible_search_help' => 'Phù hợp với các ký tự không liền kề, ví dụ: "ct" phù hợp với "cat"', 'vendor_details' => 'Chi tiết nhà cung cấp', 'purchase_order_details' => 'Chi tiết đơn đặt hàng', 'qr_iban' => 'Mã QR IBAN', @@ -5333,6 +5333,7 @@ $lang = array( 'country_Melilla' => 'Melilla', 'country_Ceuta' => 'Ceuta', 'country_Canary Islands' => 'Quần đảo Canary', + 'lang_Vietnamese' => 'Tiếng Việt', 'invoice_status_changed' => 'Xin lưu ý rằng trạng thái hóa đơn của bạn đã được cập nhật. Chúng tôi khuyên bạn nên làm mới trang để xem phiên bản mới nhất.', 'no_unread_notifications' => 'Bạn đã cập nhật đầy đủ rồi! Không có thông báo mới nào.', ); From 174a02b52a68dd9e9ff3b4259a5a5ca158ce3738 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 07:22:53 +1000 Subject: [PATCH 21/55] QB SYNC --- app/Livewire/BillingPortalPurchasev2.php | 40 ++++---- .../CBAPowerBoard/CreditCard.php | 1 - .../EDocument/Gateway/Qvalia/Partner.php | 4 +- .../Quickbooks/Jobs/QuickbooksImport.php | 88 +---------------- app/Services/Quickbooks/Models/QbInvoice.php | 97 ++++++++++++++++++- 5 files changed, 119 insertions(+), 111 deletions(-) diff --git a/app/Livewire/BillingPortalPurchasev2.php b/app/Livewire/BillingPortalPurchasev2.php index cf03d07f75c6..0d365261a340 100644 --- a/app/Livewire/BillingPortalPurchasev2.php +++ b/app/Livewire/BillingPortalPurchasev2.php @@ -431,31 +431,31 @@ class BillingPortalPurchasev2 extends Component * @throws PresenterException * @throws InvalidArgumentException */ - private function createClientContact() - { - $company = $this->subscription->company; - $user = $this->subscription->user; - $user->setCompany($company); + // private function createClientContact() + // { + // $company = $this->subscription->company; + // $user = $this->subscription->user; + // $user->setCompany($company); - $client_repo = new ClientRepository(new ClientContactRepository()); - $data = [ - 'name' => '', - 'group_settings_id' => $this->subscription->group_id, - 'contacts' => [ - ['email' => $this->email], - ], - 'client_hash' => Str::random(40), - 'settings' => ClientSettings::defaults(), - ]; + // $client_repo = new ClientRepository(new ClientContactRepository()); + // $data = [ + // 'name' => '', + // 'group_settings_id' => $this->subscription->group_id, + // 'contacts' => [ + // ['email' => $this->email], + // ], + // 'client_hash' => Str::random(40), + // 'settings' => ClientSettings::defaults(), + // ]; - $client = $client_repo->save($data, ClientFactory::create($company->id, $user->id)); + // $client = $client_repo->save($data, ClientFactory::create($company->id, $user->id)); - $this->contact = $client->fresh()->contacts()->first(); + // $this->contact = $client->fresh()->contacts()->first(); - Auth::guard('contact')->loginUsingId($this->contact->id, true); + // Auth::guard('contact')->loginUsingId($this->contact->id, true); - return $this; - } + // return $this; + // } /** diff --git a/app/PaymentDrivers/CBAPowerBoard/CreditCard.php b/app/PaymentDrivers/CBAPowerBoard/CreditCard.php index aa0c6e80374a..f7b924862012 100644 --- a/app/PaymentDrivers/CBAPowerBoard/CreditCard.php +++ b/app/PaymentDrivers/CBAPowerBoard/CreditCard.php @@ -451,7 +451,6 @@ class CreditCard implements LivewireMethodInterface match($error_object->error->code) { "UnfulfilledCondition" => $error_message = $error_object->error->details->messages[0] ?? $error_object->error->message ?? "Unknown error", "GatewayError" => $error_message = $error_object->error->message, - "UnfulfilledCondition" => $error_message = $error_object->error->message, "transaction_declined" => $error_message = $error_object->error->details[0]->status_code_description, default => $error_message = $error_object->error->message ?? "Unknown error", }; diff --git a/app/Services/EDocument/Gateway/Qvalia/Partner.php b/app/Services/EDocument/Gateway/Qvalia/Partner.php index 4cb82f65f52a..e16f332172ef 100644 --- a/app/Services/EDocument/Gateway/Qvalia/Partner.php +++ b/app/Services/EDocument/Gateway/Qvalia/Partner.php @@ -107,7 +107,7 @@ class Partner { $uri = "/partner/{$this->partner_number}/account/{$accountRegNo}"; - return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value)->object(); + return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value),[])->object(); } /** @@ -138,7 +138,7 @@ class Partner { $uri = "/partner/{$this->partner_number}/account/{$accountRegNo}/peppol/{$peppolId}"; - return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value)->object(); + return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value), [])->object(); } } diff --git a/app/Services/Quickbooks/Jobs/QuickbooksImport.php b/app/Services/Quickbooks/Jobs/QuickbooksImport.php index a6ff0a385a29..9901d388ece1 100644 --- a/app/Services/Quickbooks/Jobs/QuickbooksImport.php +++ b/app/Services/Quickbooks/Jobs/QuickbooksImport.php @@ -109,8 +109,8 @@ class QuickbooksImport implements ShouldQueue match($entity){ 'client' => $this->qbs->client->syncToNinja($records), 'product' => $this->qbs->product->syncToNinja($records), - // 'invoice' => $this->syncQbToNinjaInvoices($records), - // 'sales' => $this->syncQbToNinjaInvoices($records), + 'invoice' => $this->qbs->invoice->syncToNinja($records), + 'sales' => $this->qbs->invoice->syncToNinja($records), // 'vendor' => $this->syncQbToNinjaVendors($records), // 'quote' => $this->syncInvoices($records), // 'expense' => $this->syncQbToNinjaExpenses($records), @@ -122,91 +122,11 @@ class QuickbooksImport implements ShouldQueue private function syncQbToNinjaInvoices($records): void { - nlog("invoice sync ". count($records)); - $invoice_transformer = new InvoiceTransformer($this->company); - - foreach($records as $record) - { - nlog($record); - - $ninja_invoice_data = $invoice_transformer->qbToNinja($record); - - nlog($ninja_invoice_data); - - $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; - - $client_id = $ninja_invoice_data['client_id'] ?? null; - - if(is_null($client_id)) - continue; - - unset($ninja_invoice_data['payment_ids']); - - if($invoice = $this->findInvoice($ninja_invoice_data)) - { - $invoice->fill($ninja_invoice_data); - $invoice->saveQuietly(); - - $invoice = $invoice->calc()->getInvoice()->service()->markSent()->createInvitations()->save(); - - foreach($payment_ids as $payment_id) - { - - $payment = $this->qbs->sdk->FindById('Payment', $payment_id); - - $payment_transformer = new PaymentTransformer($this->company); - - $transformed = $payment_transformer->qbToNinja($payment); - - $ninja_payment = $payment_transformer->buildPayment($payment); - $ninja_payment->service()->applyNumber()->save(); - - $paymentable = new \App\Models\Paymentable(); - $paymentable->payment_id = $ninja_payment->id; - $paymentable->paymentable_id = $invoice->id; - $paymentable->paymentable_type = 'invoices'; - $paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount'); - $paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line - $paymentable->save(); - - $invoice->service()->applyPayment($ninja_payment, $paymentable->amount); - - } - - if($record instanceof IPPSalesReceipt) - { - $invoice->service()->markPaid()->save(); - } - - } - - $ninja_invoice_data = false; - - } + } - private function findInvoice(array $ninja_invoice_data): ?Invoice - { - $search = Invoice::query() - ->withTrashed() - ->where('company_id', $this->company->id) - // ->where('number', $ninja_invoice_data['number']); - ->where('sync->qb_id', $ninja_invoice_data['id']); - - if($search->count() == 0) { - //new invoice - $invoice = InvoiceFactory::create($this->company->id, $this->company->owner()->id); - $invoice->client_id = $ninja_invoice_data['client_id']; - - return $invoice; - } elseif($search->count() == 1) { - return $this->settings->invoice->update_record ? $search->first() : null; - } - - return null; - - } + private function syncQbToNinjaVendors(array $records): void { diff --git a/app/Services/Quickbooks/Models/QbInvoice.php b/app/Services/Quickbooks/Models/QbInvoice.php index c4ce204676a0..9f3ec1786c07 100644 --- a/app/Services/Quickbooks/Models/QbInvoice.php +++ b/app/Services/Quickbooks/Models/QbInvoice.php @@ -11,20 +11,109 @@ namespace App\Services\Quickbooks\Models; - +use App\Models\Invoice; +use App\Factory\InvoiceFactory; +use App\Interfaces\SyncInterface; use App\Services\Quickbooks\QuickbooksService; +use App\Services\Quickbooks\Transformers\InvoiceTransformer; +use App\Services\Quickbooks\Transformers\PaymentTransformer; -class QbInvoice +class QbInvoice implements SyncInterface { - + protected InvoiceTransformer $transformer; + public function __construct(public QuickbooksService $service) { + $this->transformer = new InvoiceTransformer($this->service->company); } - public function find(int $id) + public function find(int $id): mixed { return $this->service->sdk->FindById('Invoice', $id); } + public function syncToNinja(array $records): void + { + + foreach ($records as $record) { + + $ninja_invoice_data = $this->transformer->qbToNinja($record); + + $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; + + $client_id = $ninja_invoice_data['client_id'] ?? null; + + if (is_null($client_id)) { + continue; + } + + unset($ninja_invoice_data['payment_ids']); + + if ($invoice = $this->findInvoice($ninja_invoice_data)) { + $invoice->fill($ninja_invoice_data); + $invoice->saveQuietly(); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->createInvitations()->save(); + + foreach ($payment_ids as $payment_id) { + + $payment = $this->service->sdk->FindById('Payment', $payment_id); + + $payment_transformer = new PaymentTransformer($this->service->company); + + $transformed = $payment_transformer->qbToNinja($payment); + + $ninja_payment = $payment_transformer->buildPayment($payment); + $ninja_payment->service()->applyNumber()->save(); + + $paymentable = new \App\Models\Paymentable(); + $paymentable->payment_id = $ninja_payment->id; + $paymentable->paymentable_id = $invoice->id; + $paymentable->paymentable_type = 'invoices'; + $paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount'); + $paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line + $paymentable->save(); + + $invoice->service()->applyPayment($ninja_payment, $paymentable->amount); + + } + + if ($record instanceof IPPSalesReceipt) { + $invoice->service()->markPaid()->save(); + } + + } + + $ninja_invoice_data = false; + + } + + } + + public function syncToForeign(array $records): void + { + + } + + + private function findInvoice(array $ninja_invoice_data): ?Invoice + { + $search = Invoice::query() + ->withTrashed() + ->where('company_id', $this->service->company->id) + ->where('sync->qb_id', $ninja_invoice_data['id']); + + if($search->count() == 0) { + $invoice = InvoiceFactory::create($this->service->company->id, $this->service->company->owner()->id); + $invoice->client_id = $ninja_invoice_data['client_id']; + + return $invoice; + } elseif($search->count() == 1) { + return $this->service->settings->invoice->update_record ? $search->first() : null; + } + + return null; + + } } From 3dc5fb2ddee54b3d556bdd17ff27decccfd620c4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 08:03:39 +1000 Subject: [PATCH 22/55] Minor fixes --- app/DataMapper/Tax/TaxModel.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/DataMapper/Tax/TaxModel.php b/app/DataMapper/Tax/TaxModel.php index 42d027adf99e..cbddf270c536 100644 --- a/app/DataMapper/Tax/TaxModel.php +++ b/app/DataMapper/Tax/TaxModel.php @@ -35,6 +35,10 @@ class TaxModel $this->regions = $this->init(); } else { + if(!$model->seller_subregion) { + $this->seller_subregion = ''; + } + //@phpstan-ignore-next-line foreach($model as $key => $value) { $this->{$key} = $value; From 16d716230ba5014f8ac2eaea882a24a364a62de8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 09:08:47 +1000 Subject: [PATCH 23/55] Fixes for cba --- app/Http/Controllers/SearchController.php | 2 +- app/Models/CompanyGateway.php | 1 - app/PaymentDrivers/CBAPowerBoard/CreditCard.php | 1 - app/PaymentDrivers/CBAPowerBoard/Settings.php | 6 ++++-- .../migrations/2024_09_06_042040_cba_powerboard.php | 10 ++++++++-- database/seeders/PaymentLibrariesSeeder.php | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 73e13f70d68d..46b949844400 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -96,13 +96,13 @@ class SearchController extends Controller private function mapResults(array $results) { + foreach($results as $result) { switch($result['_index']) { case 'clients': if($result['_source']['is_deleted']) //do not return deleted results break; - $this->clients[] = [ 'name' => $result['_source']['name'], diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index e2d939db2515..adfaad5f00f1 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -489,7 +489,6 @@ class CompanyGateway extends BaseModel public function getSettings() { - // return $this->settings; return $this->settings ?? new \stdClass; } diff --git a/app/PaymentDrivers/CBAPowerBoard/CreditCard.php b/app/PaymentDrivers/CBAPowerBoard/CreditCard.php index f7b924862012..50008d983f7b 100644 --- a/app/PaymentDrivers/CBAPowerBoard/CreditCard.php +++ b/app/PaymentDrivers/CBAPowerBoard/CreditCard.php @@ -39,7 +39,6 @@ class CreditCard implements LivewireMethodInterface public function authorizeView(array $data) { $data['payment_method_id'] = GatewayType::CREDIT_CARD; - $data['threeds'] = $this->powerboard->company_gateway->getConfigField('threeds'); return render('gateways.powerboard.credit_card.authorize', $this->paymentData($data)); } diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index 945a72f48ea7..8cbd307b60ea 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -35,8 +35,6 @@ class Settings if($r->failed()) $r->throw(); - - nlog($r->object()); return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $r->object()->resource->data); @@ -68,7 +66,11 @@ class Settings default => $type = self::GATEWAY_CBA, }; + if($type == self::GATEWAY_CBA) + return $this->powerboard->company_gateway->getConfigField('gatewayId') ?? $this->getGatewayByType($type); + return $this->getGatewayByType($type); + } private function getGatewayByType(string $gateway_type_const): mixed diff --git a/database/migrations/2024_09_06_042040_cba_powerboard.php b/database/migrations/2024_09_06_042040_cba_powerboard.php index 7c993e310d54..255438b06481 100644 --- a/database/migrations/2024_09_06_042040_cba_powerboard.php +++ b/database/migrations/2024_09_06_042040_cba_powerboard.php @@ -21,8 +21,13 @@ return new class extends Migration $fields->publicKey = ''; $fields->secretKey = ''; $fields->testMode = false; - $fields->threeds = false; + $fields->gatewayId = ''; + if($gateway = Gateway::find(64)){ + $gateway->fields = json_encode($fields); + $gateway->save(); + }else{ + $powerboard = new Gateway(); $powerboard->id = 64; $powerboard->name = 'CBA PowerBoard'; @@ -34,7 +39,8 @@ return new class extends Migration $powerboard->fields = json_encode($fields); $powerboard->save(); - + } + Schema::table("company_gateways", function (\Illuminate\Database\Schema\Blueprint $table){ $table->text('settings')->nullable(); }); diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index 970ad5cecb2e..d4cf01d6966d 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -89,7 +89,7 @@ class PaymentLibrariesSeeder extends Seeder ['id' => 61, 'name' => 'PayPal Platform', 'provider' => 'PayPal_PPCP', 'key' => '80af24a6a691230bbec33e930ab40666', 'fields' => '{"testMode":false}'], ['id' => 62, 'name' => 'BTCPay', 'provider' => 'BTCPay', 'key' => 'vpyfbmdrkqcicpkjqdusgjfluebftuva', 'fields' => '{"btcpayUrl":"", "apiKey":"", "storeId":"", "webhookSecret":""}'], ['id' => 63, 'name' => 'Rotessa', 'is_offsite' => false, 'sort_order' => 22, 'provider' => 'Rotessa', 'key' => '91be24c7b792230bced33e930ac61676', 'fields' => '{"apiKey":"", "testMode":false}'], - ['id' => 64, 'name' => 'CBA PowerBoard', 'is_offsite' => false, 'sort_order' => 26, 'provider' => 'CBAPowerBoard', 'key' => 'b67581d804dbad1743b61c57285142ad', 'fields' => '{"publicKey":"", "secretKey":"", "testMode":false, "Threeds":true}'], + ['id' => 64, 'name' => 'CBA PowerBoard', 'is_offsite' => false, 'sort_order' => 26, 'provider' => 'CBAPowerBoard', 'key' => 'b67581d804dbad1743b61c57285142ad', 'fields' => '{"publicKey":"", "secretKey":"", "testMode":false, "gatewayId":""}'], ['id' => 65, 'name' => 'Blockonomics', 'is_offsite' => false, 'sort_order' => 27, 'provider' => 'Blockonomics', 'key' => 'wbhf02us6owgo7p4nfjd0ymssdshks4d', 'fields' => '{"apiKey":"", "callbackSecret":""}'], ]; From ae31a457dad4f566335c476ce0a8c97c70b3e8ab Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 09:15:22 +1000 Subject: [PATCH 24/55] Fixes for gateway id --- app/PaymentDrivers/CBAPowerBoard/Settings.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index 8cbd307b60ea..4feab6ab573d 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -66,9 +66,10 @@ class Settings default => $type = self::GATEWAY_CBA, }; - if($type == self::GATEWAY_CBA) - return $this->powerboard->company_gateway->getConfigField('gatewayId') ?? $this->getGatewayByType($type); - + if($type == self::GATEWAY_CBA && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1){ + return $this->powerboard->company_gateway->getConfigField('gatewayId'); + } + return $this->getGatewayByType($type); } From 2193da9f6f57eb6a205754a1687d235481107411 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 09:21:44 +1000 Subject: [PATCH 25/55] Fixes for gateway id --- app/PaymentDrivers/CBAPowerBoard/Settings.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index 4feab6ab573d..e934566e1904 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -66,12 +66,7 @@ class Settings default => $type = self::GATEWAY_CBA, }; - if($type == self::GATEWAY_CBA && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1){ - return $this->powerboard->company_gateway->getConfigField('gatewayId'); - } - return $this->getGatewayByType($type); - } private function getGatewayByType(string $gateway_type_const): mixed @@ -92,6 +87,11 @@ class Settings public function getGatewayId(int $gateway_type_id): string { + + if ($gateway_type_id == GatewayType::CREDIT_CARD && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1) { + return $this->powerboard->company_gateway->getConfigField('gatewayId'); + } + $gateway = $this->getPaymentGatewayConfiguration($gateway_type_id); return $gateway->_id; From 0d6820851ff5cff76589403e1dcad091d3b3c60d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 09:22:04 +1000 Subject: [PATCH 26/55] Fixes for gateway id --- app/PaymentDrivers/CBAPowerBoard/Settings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index e934566e1904..36cf300a0f97 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -87,7 +87,7 @@ class Settings public function getGatewayId(int $gateway_type_id): string { - + //allows us to override the gateway id for credit card if configured. if ($gateway_type_id == GatewayType::CREDIT_CARD && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1) { return $this->powerboard->company_gateway->getConfigField('gatewayId'); } From 6f3e56a2a28e9cb235271f66d372920705ae56a9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 09:31:52 +1000 Subject: [PATCH 27/55] Fixes for tests --- app/Services/EDocument/Gateway/Qvalia/Partner.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Services/EDocument/Gateway/Qvalia/Partner.php b/app/Services/EDocument/Gateway/Qvalia/Partner.php index e16f332172ef..a2e19aee7420 100644 --- a/app/Services/EDocument/Gateway/Qvalia/Partner.php +++ b/app/Services/EDocument/Gateway/Qvalia/Partner.php @@ -107,7 +107,7 @@ class Partner { $uri = "/partner/{$this->partner_number}/account/{$accountRegNo}"; - return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value),[])->object(); + return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value, [])->object(); } /** @@ -138,7 +138,7 @@ class Partner { $uri = "/partner/{$this->partner_number}/account/{$accountRegNo}/peppol/{$peppolId}"; - return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value), [])->object(); + return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value, [])->object(); } } From 8fadba5545bcf63d308749b661275997f7c47c4d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 12:46:16 +1000 Subject: [PATCH 28/55] Invoice Sync --- .../Quickbooks/Jobs/QuickbooksImport.php | 2 +- app/Services/Quickbooks/Models/QbInvoice.php | 72 ++++++++++++++++--- app/Services/Quickbooks/QuickbooksService.php | 12 +++- .../Transformers/InvoiceTransformer.php | 44 ++++++++---- 4 files changed, 104 insertions(+), 26 deletions(-) diff --git a/app/Services/Quickbooks/Jobs/QuickbooksImport.php b/app/Services/Quickbooks/Jobs/QuickbooksImport.php index 9901d388ece1..105c5e6ad898 100644 --- a/app/Services/Quickbooks/Jobs/QuickbooksImport.php +++ b/app/Services/Quickbooks/Jobs/QuickbooksImport.php @@ -53,10 +53,10 @@ class QuickbooksImport implements ShouldQueue 'product' => 'Item', 'client' => 'Customer', 'invoice' => 'Invoice', + 'sales' => 'SalesReceipt', // 'quote' => 'Estimate', // 'purchase_order' => 'PurchaseOrder', // 'payment' => 'Payment', - 'sales' => 'SalesReceipt', // 'vendor' => 'Vendor', // 'expense' => 'Purchase', ]; diff --git a/app/Services/Quickbooks/Models/QbInvoice.php b/app/Services/Quickbooks/Models/QbInvoice.php index 9f3ec1786c07..0cf18aba807a 100644 --- a/app/Services/Quickbooks/Models/QbInvoice.php +++ b/app/Services/Quickbooks/Models/QbInvoice.php @@ -11,33 +11,39 @@ namespace App\Services\Quickbooks\Models; +use Carbon\Carbon; use App\Models\Invoice; +use App\DataMapper\InvoiceSync; use App\Factory\InvoiceFactory; use App\Interfaces\SyncInterface; +use App\Repositories\InvoiceRepository; use App\Services\Quickbooks\QuickbooksService; use App\Services\Quickbooks\Transformers\InvoiceTransformer; use App\Services\Quickbooks\Transformers\PaymentTransformer; class QbInvoice implements SyncInterface { - protected InvoiceTransformer $transformer; + protected InvoiceTransformer $invoice_transformer; + protected InvoiceRepository $invoice_repository; + public function __construct(public QuickbooksService $service) { - $this->transformer = new InvoiceTransformer($this->service->company); + $this->invoice_transformer = new InvoiceTransformer($this->service->company); + $this->invoice_repository = new InvoiceRepository(); } - public function find(int $id): mixed + public function find(string $id): mixed { return $this->service->sdk->FindById('Invoice', $id); } public function syncToNinja(array $records): void { - + foreach ($records as $record) { - $ninja_invoice_data = $this->transformer->qbToNinja($record); + $ninja_invoice_data = $this->invoice_transformer->qbToNinja($record); $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; @@ -49,7 +55,11 @@ class QbInvoice implements SyncInterface unset($ninja_invoice_data['payment_ids']); - if ($invoice = $this->findInvoice($ninja_invoice_data)) { + if ($invoice = $this->findInvoice($ninja_invoice_data['id'], $ninja_invoice_data['client_id'])) { + + if($invoice->id) + $this->processQbToNinjaInvoiceUpdate($ninja_invoice_data, $invoice); + $invoice->fill($ninja_invoice_data); $invoice->saveQuietly(); @@ -95,17 +105,38 @@ class QbInvoice implements SyncInterface } + private function processQbToNinjaInvoiceUpdate(array $ninja_invoice_data, Invoice $invoice): void + { + $current_ninja_invoice_balance = $invoice->balance; + $qb_invoice_balance = $ninja_invoice_data['balance']; - private function findInvoice(array $ninja_invoice_data): ?Invoice + if(floatval($current_ninja_invoice_balance) == floatval($qb_invoice_balance)) + { + nlog('Invoice balance is the same, skipping update of line items'); + unset($ninja_invoice_data['line_items']); + $invoice->fill($ninja_invoice_data); + $invoice->saveQuietly(); + } + else{ + nlog('Invoice balance is different, updating line items'); + $this->invoice_repository->save($ninja_invoice_data, $invoice); + } + } + + private function findInvoice(string $id, ?string $client_id = null): ?Invoice { $search = Invoice::query() ->withTrashed() ->where('company_id', $this->service->company->id) - ->where('sync->qb_id', $ninja_invoice_data['id']); + ->where('sync->qb_id', $id); - if($search->count() == 0) { + if($search->count() == 0 && $client_id) { $invoice = InvoiceFactory::create($this->service->company->id, $this->service->company->owner()->id); - $invoice->client_id = $ninja_invoice_data['client_id']; + $invoice->client_id = $client_id; + + $sync = new InvoiceSync(); + $sync->qb_id = $id; + $invoice->sync = $sync; return $invoice; } elseif($search->count() == 1) { @@ -116,4 +147,25 @@ class QbInvoice implements SyncInterface } + + public function sync(string $id, string $last_updated): void + { + + $qb_record = $this->find($id); + + if($this->service->updateGate('invoice') && $invoice = $this->findInvoice($id)) + { + + //logic here to determine if we should update the record + if(Carbon::parse($last_updated)->gt(Carbon::parse($invoice->updated_at))) + { + $ninja_invoice_data = $this->invoice_transformer->qbToNinja($qb_record); + $this->invoice_repository->save($ninja_invoice_data, $invoice); + + } + + // } + } + } + } diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index f4d067256255..9f3b2e1befc9 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -90,11 +90,14 @@ class QuickbooksService private function checkToken(): self { - - if($this->company->quickbooks->accessTokenKey > time()) + nlog($this->company->quickbooks->accessTokenExpiresAt); + nlog(time()); + + if($this->company->quickbooks->accessTokenExpiresAt > time()) return $this; if($this->company->quickbooks->accessTokenExpiresAt < time() && $this->try_refresh){ + nlog('Refreshing token'); $this->sdk()->refreshToken($this->company->quickbooks->refresh_token); $this->company = $this->company->fresh(); $this->try_refresh = false; @@ -145,7 +148,10 @@ class QuickbooksService */ public function updateGate(string $entity): bool { - return (bool) $this->service->settings->{$entity}->sync && $this->service->settings->{$entity}->update_record; + nlog($this->settings->{$entity}->sync); + nlog($this->settings->{$entity}->update_record); + + return $this->settings->{$entity}->sync && $this->settings->{$entity}->update_record; } /** diff --git a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php index d2c85a31b005..53588385e6c8 100644 --- a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php +++ b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php @@ -88,18 +88,38 @@ class InvoiceTransformer extends BaseTransformer foreach($qb_items as $qb_item) { - $item = new InvoiceItem; - $item->product_key = data_get($qb_item, 'SalesItemLineDetail.ItemRef.name', ''); - $item->notes = data_get($qb_item,'Description', ''); - $item->quantity = data_get($qb_item,'SalesItemLineDetail.Qty', 0); - $item->cost = data_get($qb_item, 'SalesItemLineDetail.UnitPrice', 0); - $item->discount = data_get($item,'DiscountRate', data_get($qb_item,'DiscountAmount', 0)); - $item->is_amount_discount = data_get($qb_item,'DiscountAmount', 0) > 0 ? true : false; - $item->type_id = stripos(data_get($qb_item, 'ItemAccountRef.name') ?? '', 'Service') !== false ? '2' : '1'; - $item->tax_id = data_get($qb_item, 'TaxCodeRef.value', '') == 'NON' ? Product::PRODUCT_TYPE_EXEMPT : $item->type_id; - $item->tax_rate1 = data_get($qb_item, 'TxnTaxDetail.TaxLine.TaxLineDetail.TaxPercent', 0); - $item->tax_name1 = $item->tax_rate1 > 0 ? "Sales Tax" : ""; - $items[] = (object)$item; + + if(data_get($qb_item, 'DetailType.value') == 'SalesItemLineDetail') + { + $item = new InvoiceItem; + $item->product_key = data_get($qb_item, 'SalesItemLineDetail.ItemRef.name', ''); + $item->notes = data_get($qb_item,'Description', ''); + $item->quantity = data_get($qb_item,'SalesItemLineDetail.Qty', 0); + $item->cost = data_get($qb_item, 'SalesItemLineDetail.UnitPrice', 0); + $item->discount = data_get($item,'DiscountRate', data_get($qb_item,'DiscountAmount', 0)); + $item->is_amount_discount = data_get($qb_item,'DiscountAmount', 0) > 0 ? true : false; + $item->type_id = stripos(data_get($qb_item, 'ItemAccountRef.name') ?? '', 'Service') !== false ? '2' : '1'; + $item->tax_id = data_get($qb_item, 'TaxCodeRef.value', '') == 'NON' ? Product::PRODUCT_TYPE_EXEMPT : $item->type_id; + $item->tax_rate1 = data_get($qb_item, 'TxnTaxDetail.TaxLine.TaxLineDetail.TaxPercent', 0); + $item->tax_name1 = $item->tax_rate1 > 0 ? "Sales Tax" : ""; + $items[] = (object)$item; + } + + if(data_get($qb_item, 'DetailType.value') == 'DiscountLineDetail') + { + + $item = new InvoiceItem(); + $item->product_key = ctrans('texts.discount'); + $item->notes = ctrans('texts.discount'); + $item->quantity = 1; + $item->cost = data_get($qb_item, 'Amount', 0) * -1; + $item->discount = 0; + $item->is_amount_discount = true; + $item->type_id = '1'; + $item->tax_id = Product::PRODUCT_TYPE_PHYSICAL; + $items[] = (object)$item; + + } } nlog($items); From cb7c87e053608c5dadefdb8cc1cd2c934cd76af5 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 14:15:11 +1000 Subject: [PATCH 29/55] Refactor for QB sync --- app/DataMapper/QuickbooksSyncMap.php | 16 +- app/Enum/SyncDirection.php | 19 ++ app/Http/Controllers/SearchController.php | 2 +- .../Quickbooks/Jobs/QuickbooksImport.php | 12 +- app/Services/Quickbooks/Models/QbClient.php | 5 +- app/Services/Quickbooks/Models/QbInvoice.php | 164 +++++++++++------- app/Services/Quickbooks/Models/QbProduct.php | 5 +- app/Services/Quickbooks/QuickbooksService.php | 24 +-- 8 files changed, 143 insertions(+), 104 deletions(-) create mode 100644 app/Enum/SyncDirection.php diff --git a/app/DataMapper/QuickbooksSyncMap.php b/app/DataMapper/QuickbooksSyncMap.php index 40250b61f103..800b2dfea633 100644 --- a/app/DataMapper/QuickbooksSyncMap.php +++ b/app/DataMapper/QuickbooksSyncMap.php @@ -11,32 +11,20 @@ namespace App\DataMapper; -enum SyncDirection: string -{ - case PUSH = 'push'; - case PULL = 'pull'; - case BIDIRECTIONAL = 'bidirectional'; -} +use App\Enum\SyncDirection; /** * QuickbooksSyncMap. */ class QuickbooksSyncMap { - public bool $sync = true; - - public bool $update_record = true; - - public SyncDirection $direction = SyncDirection::BIDIRECTIONAL; + public SyncDirection $direction = SyncDirection::BIDIRECTIONAL; public function __construct(array $attributes = []) { - $this->sync = $attributes['sync'] ?? true; - $this->update_record = $attributes['update_record'] ?? true; $this->direction = isset($attributes['direction']) ? SyncDirection::from($attributes['direction']) : SyncDirection::BIDIRECTIONAL; } } - diff --git a/app/Enum/SyncDirection.php b/app/Enum/SyncDirection.php new file mode 100644 index 000000000000..537f32b9633b --- /dev/null +++ b/app/Enum/SyncDirection.php @@ -0,0 +1,19 @@ +has('search') && $request->input('search') !== '') { + if(config('scount.driver') == 'elastic' && $request->has('search') && $request->input('search') !== '') { try{ return $this->search($request->input('search', '')); } catch(\Exception $e) { diff --git a/app/Services/Quickbooks/Jobs/QuickbooksImport.php b/app/Services/Quickbooks/Jobs/QuickbooksImport.php index 105c5e6ad898..16506102a3fa 100644 --- a/app/Services/Quickbooks/Jobs/QuickbooksImport.php +++ b/app/Services/Quickbooks/Jobs/QuickbooksImport.php @@ -84,7 +84,7 @@ class QuickbooksImport implements ShouldQueue foreach($this->entities as $key => $entity) { - if(!$this->qbs->syncGate($key, 'pull')) { + if(!$this->qbs->syncable($key, \App\Enum\SyncDirection::PULL)) { nlog('skipping ' . $key); continue; } @@ -153,7 +153,7 @@ class QuickbooksImport implements ShouldQueue $contact->fill($ninja_data[1]); $contact->saveQuietly(); } - elseif($this->qbs->updateGate('vendor')){ + elseif($this->qbs->syncable('vendor', (\App\Enum\SyncDirection::PULL)->value)){ $contact->fill($ninja_data[1]); $contact->saveQuietly(); } @@ -194,7 +194,7 @@ class QuickbooksImport implements ShouldQueue return ExpenseFactory::create($this->company->id, $this->company->owner()->id); } elseif($search->count() == 1) { - return $this->settings->expense->update_record ? $search->first() : null; + return $this->service->syncable('expense', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; @@ -224,7 +224,8 @@ class QuickbooksImport implements ShouldQueue return VendorFactory::create($this->company->id, $this->company->owner()->id); } elseif($search->count() == 1) { - return $this->settings->vendor->update_record ? $search->first() : null; + + return $this->service->syncable('vendor', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; @@ -258,7 +259,8 @@ class QuickbooksImport implements ShouldQueue return $client; } elseif($search->count() == 1) { - return $this->settings->client->update_record ? $search->first() : null; + +return $this->service->syncable('client', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; diff --git a/app/Services/Quickbooks/Models/QbClient.php b/app/Services/Quickbooks/Models/QbClient.php index cf7b773622c7..c7c896150e3d 100644 --- a/app/Services/Quickbooks/Models/QbClient.php +++ b/app/Services/Quickbooks/Models/QbClient.php @@ -69,7 +69,7 @@ class QbClient implements SyncInterface $contact->fill($ninja_data[1]); $contact->saveQuietly(); } - elseif($this->service->updateGate('client')){ + elseif($this->service->syncable('client', \App\Enum\SyncDirection::PULL)){ $contact->fill($ninja_data[1]); $contact->saveQuietly(); } @@ -101,7 +101,8 @@ class QbClient implements SyncInterface return $client; } elseif ($search->count() == 1) { - return $this->service->settings->client->update_record ? $search->first() : null; + +return $this->service->syncable('client', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; diff --git a/app/Services/Quickbooks/Models/QbInvoice.php b/app/Services/Quickbooks/Models/QbInvoice.php index 0cf18aba807a..e1181f86824a 100644 --- a/app/Services/Quickbooks/Models/QbInvoice.php +++ b/app/Services/Quickbooks/Models/QbInvoice.php @@ -43,58 +43,7 @@ class QbInvoice implements SyncInterface foreach ($records as $record) { - $ninja_invoice_data = $this->invoice_transformer->qbToNinja($record); - - $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; - - $client_id = $ninja_invoice_data['client_id'] ?? null; - - if (is_null($client_id)) { - continue; - } - - unset($ninja_invoice_data['payment_ids']); - - if ($invoice = $this->findInvoice($ninja_invoice_data['id'], $ninja_invoice_data['client_id'])) { - - if($invoice->id) - $this->processQbToNinjaInvoiceUpdate($ninja_invoice_data, $invoice); - - $invoice->fill($ninja_invoice_data); - $invoice->saveQuietly(); - - $invoice = $invoice->calc()->getInvoice()->service()->markSent()->createInvitations()->save(); - - foreach ($payment_ids as $payment_id) { - - $payment = $this->service->sdk->FindById('Payment', $payment_id); - - $payment_transformer = new PaymentTransformer($this->service->company); - - $transformed = $payment_transformer->qbToNinja($payment); - - $ninja_payment = $payment_transformer->buildPayment($payment); - $ninja_payment->service()->applyNumber()->save(); - - $paymentable = new \App\Models\Paymentable(); - $paymentable->payment_id = $ninja_payment->id; - $paymentable->paymentable_id = $invoice->id; - $paymentable->paymentable_type = 'invoices'; - $paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount'); - $paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line - $paymentable->save(); - - $invoice->service()->applyPayment($ninja_payment, $paymentable->amount); - - } - - if ($record instanceof IPPSalesReceipt) { - $invoice->service()->markPaid()->save(); - } - - } - - $ninja_invoice_data = false; + $this->syncNinjaInvoice($record); } @@ -105,7 +54,7 @@ class QbInvoice implements SyncInterface } - private function processQbToNinjaInvoiceUpdate(array $ninja_invoice_data, Invoice $invoice): void + private function qbInvoiceUpdate(array $ninja_invoice_data, Invoice $invoice): void { $current_ninja_invoice_balance = $invoice->balance; $qb_invoice_balance = $ninja_invoice_data['balance']; @@ -130,7 +79,7 @@ class QbInvoice implements SyncInterface ->where('company_id', $this->service->company->id) ->where('sync->qb_id', $id); - if($search->count() == 0 && $client_id) { + if($search->count() == 0) { $invoice = InvoiceFactory::create($this->service->company->id, $this->service->company->owner()->id); $invoice->client_id = $client_id; @@ -140,32 +89,125 @@ class QbInvoice implements SyncInterface return $invoice; } elseif($search->count() == 1) { - return $this->service->settings->invoice->update_record ? $search->first() : null; + +return $this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; } - - public function sync(string $id, string $last_updated): void + public function sync($id, string $last_updated): void { $qb_record = $this->find($id); - if($this->service->updateGate('invoice') && $invoice = $this->findInvoice($id)) + if($this->service->syncable('invoice', \App\Enum\SyncDirection::PULL)) { - //logic here to determine if we should update the record - if(Carbon::parse($last_updated)->gt(Carbon::parse($invoice->updated_at))) + $invoice = $this->findInvoice($id); + + if(data_get($qb_record, 'TxnStatus') === 'Voided') + { + $this->delete($id); + return; + } + + if(!$invoice->id){ + $this->syncNinjaInvoice($qb_record); + } + elseif(Carbon::parse($last_updated)->gt(Carbon::parse($invoice->updated_at))) { $ninja_invoice_data = $this->invoice_transformer->qbToNinja($qb_record); $this->invoice_repository->save($ninja_invoice_data, $invoice); } - // } } } + /** + * syncNinjaInvoice + * + * @param $record + * @return void + */ + public function syncNinjaInvoice($record): void + { + + $ninja_invoice_data = $this->invoice_transformer->qbToNinja($record); + + $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; + + $client_id = $ninja_invoice_data['client_id'] ?? null; + + if (is_null($client_id)) { + return; + } + + unset($ninja_invoice_data['payment_ids']); + + if ($invoice = $this->findInvoice($ninja_invoice_data['id'], $ninja_invoice_data['client_id'])) { + + if ($invoice->id) { + $this->qbInvoiceUpdate($ninja_invoice_data, $invoice); + } + + //new invoice scaffold + $invoice->fill($ninja_invoice_data); + $invoice->saveQuietly(); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->applyNumber()->createInvitations()->save(); + + foreach ($payment_ids as $payment_id) { + + $payment = $this->service->sdk->FindById('Payment', $payment_id); + + $payment_transformer = new PaymentTransformer($this->service->company); + + $transformed = $payment_transformer->qbToNinja($payment); + + $ninja_payment = $payment_transformer->buildPayment($payment); + $ninja_payment->service()->applyNumber()->save(); + + $paymentable = new \App\Models\Paymentable(); + $paymentable->payment_id = $ninja_payment->id; + $paymentable->paymentable_id = $invoice->id; + $paymentable->paymentable_type = 'invoices'; + $paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount'); + $paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line + $paymentable->save(); + + $invoice->service()->applyPayment($ninja_payment, $paymentable->amount); + + } + + if ($record instanceof IPPSalesReceipt) { + $invoice->service()->markPaid()->save(); + } + + } + + $ninja_invoice_data = false; + + + } + + /** + * Deletes the invoice from Ninja and sets the sync to null + * + * @param string $id + * @return void + */ + public function delete($id): void + { + $qb_record = $this->find($id); + + if($this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) && $invoice = $this->findInvoice($id)) + { + $invoice->sync = null; + $invoice->saveQuietly(); + $this->invoice_repository->delete($invoice); + } + } } diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php index f9fb2a64418c..6a9da37f5457 100644 --- a/app/Services/Quickbooks/Models/QbProduct.php +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -73,7 +73,8 @@ class QbProduct implements SyncInterface return $product; } elseif($search->count() == 1) { - return $this->service->settings->product->update_record ? $search->first() : null; + +return $this->service->syncable('product', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; @@ -84,7 +85,7 @@ class QbProduct implements SyncInterface { $qb_record = $this->find($id); - if($this->service->updateGate('product') && $ninja_record = $this->findProduct($id)) + if($this->service->syncable('product', \App\Enum\SyncDirection::PULL) && $ninja_record = $this->findProduct($id)) { if(Carbon::parse($last_updated) > Carbon::parse($ninja_record->updated_at)) diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index 9f3b2e1befc9..87a100ac1807 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -139,31 +139,17 @@ class QuickbooksService { return $this->sdk->FindById($entity, $id); } - + /** - * Updates the gate for a given entity + * Flag to determine if a sync is allowed in either direction * * @param string $entity + * @param mixed $direction * @return bool */ - public function updateGate(string $entity): bool + public function syncable(string $entity, \App\Enum\SyncDirection $direction): bool { - nlog($this->settings->{$entity}->sync); - nlog($this->settings->{$entity}->update_record); - - return $this->settings->{$entity}->sync && $this->settings->{$entity}->update_record; - } - - /** - * Determines whether a sync is allowed based on the settings - * - * @param string $entity - * @param string $direction - * @return bool - */ - public function syncGate(string $entity, string $direction): bool - { - return (bool) $this->settings->{$entity}->sync && in_array($this->settings->{$entity}->direction->value, [$direction, 'bidirectional']); + return $this->settings->{$entity}->direction === $direction || $this->settings->{$entity}->direction === \App\Enum\SyncDirection::BIDIRECTIONAL; } } From 52adc55c9cb26be122daa2af0900b41ca0db67c2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 14:49:19 +1000 Subject: [PATCH 30/55] Fixes for static analysis --- app/PaymentDrivers/CBAPowerBoard/Settings.php | 5 ++- .../Quickbooks/Jobs/QuickbooksImport.php | 43 ++----------------- app/Services/Quickbooks/Models/QbClient.php | 3 +- app/Services/Quickbooks/Models/QbInvoice.php | 11 +++-- app/Services/Quickbooks/Models/QbProduct.php | 3 +- app/Services/Quickbooks/QuickbooksService.php | 2 +- 6 files changed, 16 insertions(+), 51 deletions(-) diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index 36cf300a0f97..a005ccdf363f 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -13,10 +13,11 @@ namespace App\PaymentDrivers\CBAPowerBoard; -use App\PaymentDrivers\CBAPowerBoard\Models\Gateways; +use App\Models\GatewayType; -use App\PaymentDrivers\CBAPowerBoard\Models\Gateway; use App\PaymentDrivers\CBAPowerBoardPaymentDriver; +use App\PaymentDrivers\CBAPowerBoard\Models\Gateway; +use App\PaymentDrivers\CBAPowerBoard\Models\Gateways; class Settings { diff --git a/app/Services/Quickbooks/Jobs/QuickbooksImport.php b/app/Services/Quickbooks/Jobs/QuickbooksImport.php index 16506102a3fa..3bafdbd046eb 100644 --- a/app/Services/Quickbooks/Jobs/QuickbooksImport.php +++ b/app/Services/Quickbooks/Jobs/QuickbooksImport.php @@ -153,7 +153,7 @@ class QuickbooksImport implements ShouldQueue $contact->fill($ninja_data[1]); $contact->saveQuietly(); } - elseif($this->qbs->syncable('vendor', (\App\Enum\SyncDirection::PULL)->value)){ + elseif($this->qbs->syncable('vendor', \App\Enum\SyncDirection::PULL)){ $contact->fill($ninja_data[1]); $contact->saveQuietly(); } @@ -194,7 +194,7 @@ class QuickbooksImport implements ShouldQueue return ExpenseFactory::create($this->company->id, $this->company->owner()->id); } elseif($search->count() == 1) { - return $this->service->syncable('expense', \App\Enum\SyncDirection::PULL) ? $search->first() : null; + return $this->qbs->syncable('expense', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; @@ -225,49 +225,12 @@ class QuickbooksImport implements ShouldQueue } elseif($search->count() == 1) { - return $this->service->syncable('vendor', \App\Enum\SyncDirection::PULL) ? $search->first() : null; + return $this->qbs->syncable('vendor', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; } - private function findClient(array $qb_data) :?Client - { - $client = $qb_data[0]; - $contact = $qb_data[1]; - $client_meta = $qb_data[2]; - - $search = Client::query() - ->withTrashed() - ->where('company_id', $this->company->id) - ->where(function ($q) use ($client, $client_meta, $contact){ - - $q->where('client_hash', $client_meta['client_hash']) - ->orWhere('number', $client['number']) - ->orWhereHas('contacts', function ($q) use ($contact){ - $q->where('email', $contact['email']); - }); - - }); - - if($search->count() == 0) { - //new client - $client = ClientFactory::create($this->company->id, $this->company->owner()->id); - $client->client_hash = $client_meta['client_hash']; - $client->settings = $client_meta['settings']; - - return $client; - } - elseif($search->count() == 1) { - -return $this->service->syncable('client', \App\Enum\SyncDirection::PULL) ? $search->first() : null; - } - - return null; - } - - - public function middleware() { return [new WithoutOverlapping("qbs-{$this->company_id}-{$this->db}")]; diff --git a/app/Services/Quickbooks/Models/QbClient.php b/app/Services/Quickbooks/Models/QbClient.php index c7c896150e3d..b40a71c7eb85 100644 --- a/app/Services/Quickbooks/Models/QbClient.php +++ b/app/Services/Quickbooks/Models/QbClient.php @@ -101,8 +101,7 @@ class QbClient implements SyncInterface return $client; } elseif ($search->count() == 1) { - -return $this->service->syncable('client', \App\Enum\SyncDirection::PULL) ? $search->first() : null; + return $this->service->syncable('client', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; diff --git a/app/Services/Quickbooks/Models/QbInvoice.php b/app/Services/Quickbooks/Models/QbInvoice.php index e1181f86824a..dc9dcc8aebde 100644 --- a/app/Services/Quickbooks/Models/QbInvoice.php +++ b/app/Services/Quickbooks/Models/QbInvoice.php @@ -81,7 +81,7 @@ class QbInvoice implements SyncInterface if($search->count() == 0) { $invoice = InvoiceFactory::create($this->service->company->id, $this->service->company->owner()->id); - $invoice->client_id = $client_id; + $invoice->client_id = (int)$client_id; $sync = new InvoiceSync(); $sync->qb_id = $id; @@ -89,8 +89,7 @@ class QbInvoice implements SyncInterface return $invoice; } elseif($search->count() == 1) { - -return $this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) ? $search->first() : null; + return $this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; @@ -102,6 +101,8 @@ return $this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) ? $sea $qb_record = $this->find($id); + nlog($qb_record); + if($this->service->syncable('invoice', \App\Enum\SyncDirection::PULL)) { @@ -119,6 +120,8 @@ return $this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) ? $sea elseif(Carbon::parse($last_updated)->gt(Carbon::parse($invoice->updated_at))) { $ninja_invoice_data = $this->invoice_transformer->qbToNinja($qb_record); + nlog($ninja_invoice_data); + $this->invoice_repository->save($ninja_invoice_data, $invoice); } @@ -182,7 +185,7 @@ return $this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) ? $sea } - if ($record instanceof IPPSalesReceipt) { + if ($record instanceof \QuickBooksOnline\API\Data\IPPSalesReceipt) { $invoice->service()->markPaid()->save(); } diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php index 6a9da37f5457..085718546231 100644 --- a/app/Services/Quickbooks/Models/QbProduct.php +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -73,8 +73,7 @@ class QbProduct implements SyncInterface return $product; } elseif($search->count() == 1) { - -return $this->service->syncable('product', \App\Enum\SyncDirection::PULL) ? $search->first() : null; + return $this->service->syncable('product', \App\Enum\SyncDirection::PULL) ? $search->first() : null; } return null; diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index 87a100ac1807..9c5c6580e42d 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -144,7 +144,7 @@ class QuickbooksService * Flag to determine if a sync is allowed in either direction * * @param string $entity - * @param mixed $direction + * @param \App\Enum\SyncDirection $direction * @return bool */ public function syncable(string $entity, \App\Enum\SyncDirection $direction): bool From a593d2b9d9ade5a76b157f58d274daf657c7bbb8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 15:58:57 +1000 Subject: [PATCH 31/55] Updates for QB discounts --- app/Services/Quickbooks/Models/QbInvoice.php | 2 + app/Services/Quickbooks/QuickbooksService.php | 3 - .../Transformers/InvoiceTransformer.php | 57 +++++++++++++++++-- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/app/Services/Quickbooks/Models/QbInvoice.php b/app/Services/Quickbooks/Models/QbInvoice.php index dc9dcc8aebde..757c51f522e3 100644 --- a/app/Services/Quickbooks/Models/QbInvoice.php +++ b/app/Services/Quickbooks/Models/QbInvoice.php @@ -140,6 +140,8 @@ class QbInvoice implements SyncInterface $ninja_invoice_data = $this->invoice_transformer->qbToNinja($record); + nlog($ninja_invoice_data); + $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; $client_id = $ninja_invoice_data['client_id'] ?? null; diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index 9c5c6580e42d..60bf92a86fc1 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -90,14 +90,11 @@ class QuickbooksService private function checkToken(): self { - nlog($this->company->quickbooks->accessTokenExpiresAt); - nlog(time()); if($this->company->quickbooks->accessTokenExpiresAt > time()) return $this; if($this->company->quickbooks->accessTokenExpiresAt < time() && $this->try_refresh){ - nlog('Refreshing token'); $this->sdk()->refreshToken($this->company->quickbooks->refresh_token); $this->company = $this->company->fresh(); $this->try_refresh = false; diff --git a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php index 53588385e6c8..595a12d5f8e6 100644 --- a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php +++ b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php @@ -47,14 +47,63 @@ class InvoiceTransformer extends BaseTransformer 'due_date' => data_get($qb_data, 'DueDate', null), 'po_number' => data_get($qb_data, 'PONumber', ""), 'partial' => data_get($qb_data, 'Deposit', 0), - 'line_items' => $this->getLineItems(data_get($qb_data, 'Line', [])), + 'line_items' => $this->getLineItems(data_get($qb_data, 'Line', []), data_get($qb_data, 'ApplyTaxAfterDiscount', 'true')), 'payment_ids' => $this->getPayments($qb_data), 'status_id' => Invoice::STATUS_SENT, - 'tax_rate1' => $rate = data_get($qb_data,'TxnTaxDetail.TaxLine.TaxLineDetail.TaxPercent', 0), + 'tax_rate1' => $rate = $this->calculateTotalTax($qb_data), 'tax_name1' => $rate > 0 ? "Sales Tax" : "", + 'custom_surcharge1' => $this->checkIfDiscountAfterTax($qb_data), + ] : false; } + private function checkIfDiscountAfterTax($qb_data) + { + + if($qb_data->ApplyTaxAfterDiscount == 'true'){ + return 0; + } + + foreach(data_get($qb_data, 'Line', []) as $line) + { + nlog("iterating"); + if(data_get($line, 'DetailType.value') == 'DiscountLineDetail') + { + nlog("found discount"); + + if(!isset($this->company->custom_fields->surcharge1)) + { + $this->company->custom_fields->surcharge1 = ctrans('texts.discount'); + $this->company->save(); + } + + nlog(data_get($line, 'Amount', 0) * -1); + return data_get($line, 'Amount', 0) * -1; + } + } + + return 0; + } + + private function calculateTotalTax($qb_data) + { + $taxLines = data_get($qb_data, 'TxnTaxDetail.TaxLine', []); + + if (!is_array($taxLines)) { + $taxLines = [$taxLines]; + } + + $totalTaxRate = 0; + + foreach ($taxLines as $taxLine) { + $taxRate = data_get($taxLine, 'TaxLineDetail.TaxPercent', 0); + $totalTaxRate += $taxRate; + } + + return $totalTaxRate; + } + + private function getPayments(mixed $qb_data) { $payments = []; @@ -82,7 +131,7 @@ class InvoiceTransformer extends BaseTransformer } - private function getLineItems(mixed $qb_items) + private function getLineItems(mixed $qb_items, string $include_discount = 'true') { $items = []; @@ -105,7 +154,7 @@ class InvoiceTransformer extends BaseTransformer $items[] = (object)$item; } - if(data_get($qb_item, 'DetailType.value') == 'DiscountLineDetail') + if(data_get($qb_item, 'DetailType.value') == 'DiscountLineDetail' && $include_discount == 'true') { $item = new InvoiceItem(); From 8d636fd0a3a66707213a747c643e0e4fbf510bd8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 16:58:49 +1000 Subject: [PATCH 32/55] Fixes for CBA Powerboard --- app/PaymentDrivers/CBAPowerBoard/Settings.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index a005ccdf363f..b7ae06971b82 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -81,6 +81,14 @@ class Settings $gateways = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $settings->gateways); + if ($gateway_type_id == GatewayType::CREDIT_CARD && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1) { + + return collect($gateways)->first(function (Gateway $gateway) { + return $gateway->_id == $this->powerboard->company_gateway->getConfigField('gatewayId'); + }); + + } + return collect($gateways)->first(function (Gateway $gateway) use ($gateway_type_const){ return $gateway->type == $gateway_type_const; }); @@ -89,9 +97,6 @@ class Settings public function getGatewayId(int $gateway_type_id): string { //allows us to override the gateway id for credit card if configured. - if ($gateway_type_id == GatewayType::CREDIT_CARD && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1) { - return $this->powerboard->company_gateway->getConfigField('gatewayId'); - } $gateway = $this->getPaymentGatewayConfiguration($gateway_type_id); From bfe278e639b660884cb82ae9b7401141dc06fc0c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 17:00:24 +1000 Subject: [PATCH 33/55] Fixes for CBA Powerboard --- app/PaymentDrivers/CBAPowerBoard/Settings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index b7ae06971b82..dc3bffb29316 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -81,7 +81,7 @@ class Settings $gateways = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $settings->gateways); - if ($gateway_type_id == GatewayType::CREDIT_CARD && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1) { + if ($gateway_type_const == self::GATEWAY_CBA && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1) { return collect($gateways)->first(function (Gateway $gateway) { return $gateway->_id == $this->powerboard->company_gateway->getConfigField('gatewayId'); From 033ef9c4926fdc7e61b0093c7e241920ea86da83 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 17:20:00 +1000 Subject: [PATCH 34/55] Fixes for CBA Powerboard --- .../CBAPowerBoard/Models/Gateway.php | 36 +++++-------------- app/PaymentDrivers/CBAPowerBoard/Settings.php | 2 +- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/app/PaymentDrivers/CBAPowerBoard/Models/Gateway.php b/app/PaymentDrivers/CBAPowerBoard/Models/Gateway.php index d36e176d5ffb..2bf85301ed7d 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Models/Gateway.php +++ b/app/PaymentDrivers/CBAPowerBoard/Models/Gateway.php @@ -13,35 +13,17 @@ namespace App\PaymentDrivers\CBAPowerBoard\Models; class Gateway { - /** @var string */ - public string $_id; - /** @var string */ - public string $name; - /** @var string */ - public string $type; - /** @var string */ - public string $mode; - /** @var string */ - public string $created_at; - /** @var string */ - public string $updated_at; - /** @var bool */ - public bool $archived; - /** @var bool */ - public bool $default; - /** @var string */ - public string $verification_status; public function __construct( - string $_id, - string $name, - string $type, - string $mode, - string $created_at, - string $updated_at, - bool $archived, - bool $default, - string $verification_status + public string $_id, + public string $name, + public string $type, + public string $mode, + public string $created_at, + public string $updated_at, + public bool $archived, + public bool $default, + public string $verification_status = '' ) { $this->_id = $_id; $this->name = $name; diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index dc3bffb29316..d94dafbd2fda 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -74,7 +74,7 @@ class Settings { $settings = $this->getSettings(); - if(!property_exists($settings,'gateways')){ + if(!property_exists($settings, 'gateways')){ $this->updateSettings(); $settings = $this->getSettings(); } From 3b8aef5e589259527f1b929156f9844be1605604 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Sep 2024 17:31:35 +1000 Subject: [PATCH 35/55] Fixes for CBA Powerboard --- app/PaymentDrivers/CBAPowerBoard/Settings.php | 48 ++++++-- public/build/assets/app-021b0210.js | 109 ++++++++++++++++++ public/build/assets/app-e0713224.js | 109 ------------------ .../assets/powerboard-credit-card-bb946fa4.js | 13 +++ .../assets/powerboard-credit-card-f8810425.js | 13 --- public/build/manifest.json | 4 +- .../payments/powerboard-credit-card.js | 5 +- 7 files changed, 168 insertions(+), 133 deletions(-) create mode 100644 public/build/assets/app-021b0210.js delete mode 100644 public/build/assets/app-e0713224.js create mode 100644 public/build/assets/powerboard-credit-card-bb946fa4.js delete mode 100644 public/build/assets/powerboard-credit-card-f8810425.js diff --git a/app/PaymentDrivers/CBAPowerBoard/Settings.php b/app/PaymentDrivers/CBAPowerBoard/Settings.php index d94dafbd2fda..896e538871a7 100644 --- a/app/PaymentDrivers/CBAPowerBoard/Settings.php +++ b/app/PaymentDrivers/CBAPowerBoard/Settings.php @@ -29,8 +29,13 @@ class Settings public function __construct(public CBAPowerBoardPaymentDriver $powerboard) { } - - public function getGateways() + + /** + * Returns the API response for the gateways + * + * @return mixed + */ + public function getGateways(): mixed { $r = $this->powerboard->gatewayRequest('/v1/gateways', (\App\Enum\HttpVerb::GET)->value, [], []); @@ -41,7 +46,12 @@ class Settings } - /** We will need to have a process that updates this at intervals */ + /** We will need to have a process that updates this at intervals */ + /** + * updateSettings from the API + * + * @return self + */ public function updateSettings():self { $gateways = $this->getGateways(); @@ -52,12 +62,23 @@ class Settings return $this; } - + + /** + * getSettings + * + * @return mixed + */ public function getSettings(): mixed { return $this->powerboard->company_gateway->getSettings(); } - + + /** + * Entry point for getting the payment gateway configuration + * + * @param int $gateway_type_id + * @return mixed + */ public function getPaymentGatewayConfiguration(int $gateway_type_id): mixed { $type = self::GATEWAY_CBA; @@ -69,7 +90,13 @@ class Settings return $this->getGatewayByType($type); } - + + /** + * Returns the CBA gateway object for a given gateway type + * + * @param string $gateway_type_const + * @return mixed + */ private function getGatewayByType(string $gateway_type_const): mixed { $settings = $this->getSettings(); @@ -93,10 +120,15 @@ class Settings return $gateway->type == $gateway_type_const; }); } - + + /** + * Returns the CBA gateway ID for a given gateway type + * + * @param int $gateway_type_id + * @return string + */ public function getGatewayId(int $gateway_type_id): string { - //allows us to override the gateway id for credit card if configured. $gateway = $this->getPaymentGatewayConfiguration($gateway_type_id); diff --git a/public/build/assets/app-021b0210.js b/public/build/assets/app-021b0210.js new file mode 100644 index 000000000000..70e3dbcde102 --- /dev/null +++ b/public/build/assets/app-021b0210.js @@ -0,0 +1,109 @@ +import{A as Ol}from"./index-08e160a7.js";import{c as zt,g as Cl}from"./_commonjsHelpers-725317a4.js";var Al={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}}},Tl=Al,ni={},xn={};Object.defineProperty(xn,"__esModule",{value:!0});xn.clone=void 0;function Pl(e){return e?JSON.parse(JSON.stringify(e)):null}xn.clone=Pl;var ii={};Object.defineProperty(ii,"__esModule",{value:!0});ii.matches=void 0;function Rl(e,r,n){var a=String(r).length,s=e.substr(0,a),l=parseInt(s,10);return r=parseInt(String(r).substr(0,s.length),10),n=parseInt(String(n).substr(0,s.length),10),l>=r&&l<=n}function Ml(e,r){return r=String(r),r.substring(0,e.length)===e.substring(0,r.length)}function kl(e,r){return Array.isArray(r)?Rl(e,r[0],r[1]):Ml(e,r)}ii.matches=kl;Object.defineProperty(ni,"__esModule",{value:!0});ni.addMatchingCardsToResults=void 0;var Nl=xn,Ll=ii;function jl(e,r,n){var a,s;for(a=0;a=s&&(v.matchStrength=s),n.push(v);break}}}ni.addMatchingCardsToResults=jl;var ai={};Object.defineProperty(ai,"__esModule",{value:!0});ai.isValidInputType=void 0;function Il(e){return typeof e=="string"||e instanceof String}ai.isValidInputType=Il;var oi={};Object.defineProperty(oi,"__esModule",{value:!0});oi.findBestMatch=void 0;function Dl(e){var r=e.filter(function(n){return n.matchStrength}).length;return r>0&&r===e.length}function $l(e){return Dl(e)?e.reduce(function(r,n){return!r||Number(r.matchStrength)Vl?mn(!1,!1):ql.test(e)?mn(!1,!0):mn(!0,!0)}si.cardholderName=zl;var li={};function Wl(e){for(var r=0,n=!1,a=e.length-1,s;a>=0;)s=parseInt(e.charAt(a),10),n&&(s*=2,s>9&&(s=s%10+1)),n=!n,r+=s,a--;return r%10===0}var Kl=Wl;Object.defineProperty(li,"__esModule",{value:!0});li.cardNumber=void 0;var Jl=Kl,oo=Ho;function yr(e,r,n){return{card:e,isPotentiallyValid:r,isValid:n}}function Gl(e,r){r===void 0&&(r={});var n,a,s;if(typeof e!="string"&&typeof e!="number")return yr(null,!1,!1);var l=String(e).replace(/-|\s/g,"");if(!/^\d*$/.test(l))return yr(null,!1,!1);var v=oo(l);if(v.length===0)return yr(null,!1,!1);if(v.length!==1)return yr(null,!0,!1);var g=v[0];if(r.maxLength&&l.length>r.maxLength)return yr(g,!1,!1);g.type===oo.types.UNIONPAY&&r.luhnValidateUnionPay!==!0?a=!0:a=Jl(l),s=Math.max.apply(null,g.lengths),r.maxLength&&(s=Math.min(r.maxLength,s));for(var R=0;R4)return or(!1,!1);var g=parseInt(e,10),R=Number(String(s).substr(2,2)),U=!1;if(a===2){if(String(s).substr(0,2)===e)return or(!1,!0);n=R===g,U=g>=R&&g<=R+r}else a===4&&(n=s===g,U=g>=s&&g<=s+r);return or(U,U,n)}Qr.expirationYear=Xl;var fi={};Object.defineProperty(fi,"__esModule",{value:!0});fi.isArray=void 0;fi.isArray=Array.isArray||function(e){return Object.prototype.toString.call(e)==="[object Array]"};Object.defineProperty(ci,"__esModule",{value:!0});ci.parseDate=void 0;var Ql=Qr,Zl=fi;function eu(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),Ql.expirationYear(n).isPotentiallyValid?1:2):e.length===5?1:e.length>5?2:1}function tu(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)),Zl.isArray(r))return{month:r[0]||"",year:r.slice(1).join()};var n=eu(e),a=e.substr(0,n);return{month:a,year:e.substr(a.length)}}ci.parseDate=tu;var En={};Object.defineProperty(En,"__esModule",{value:!0});En.expirationMonth=void 0;function vn(e,r,n){return{isValid:e,isPotentiallyValid:r,isValidForThisYear:n||!1}}function ru(e){var r=new Date().getMonth()+1;if(typeof e!="string")return vn(!1,!1);if(e.replace(/\s/g,"")===""||e==="0")return vn(!1,!0);if(!/^\d*$/.test(e))return vn(!1,!1);var n=parseInt(e,10);if(isNaN(Number(e)))return vn(!1,!1);var a=n>0&&n<13;return vn(a,a,a&&n>=r)}En.expirationMonth=ru;var na=zt&&zt.__assign||function(){return na=Object.assign||function(e){for(var r,n=1,a=arguments.length;nr?e[n]:r;return r}function qr(e,r){return{isValid:e,isPotentiallyValid:r}}function uu(e,r){return r===void 0&&(r=qo),r=r instanceof Array?r:[r],typeof e!="string"||!/^\d*$/.test(e)?qr(!1,!1):su(r,e.length)?qr(!0,!0):e.lengthlu(r)?qr(!1,!1):qr(!0,!0)}di.cvv=uu;var pi={};Object.defineProperty(pi,"__esModule",{value:!0});pi.postalCode=void 0;var cu=3;function Gi(e,r){return{isValid:e,isPotentiallyValid:r}}function fu(e,r){r===void 0&&(r={});var n=r.minLength||cu;return typeof e!="string"?Gi(!1,!1):e.lengthfunction(){return r||(0,e[zo(e)[0]])((r={exports:{}}).exports,r),r.exports},Ru=(e,r,n,a)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of zo(r))!Pu.call(e,s)&&s!==n&&Vo(e,s,{get:()=>r[s],enumerable:!(a=Au(r,s))||a.enumerable});return e},Qe=(e,r,n)=>(n=e!=null?Cu(Tu(e)):{},Ru(r||!e||!e.__esModule?Vo(n,"default",{value:e,enumerable:!0}):n,e)),St=Wt({"../alpine/packages/alpinejs/dist/module.cjs.js"(e,r){var n=Object.create,a=Object.defineProperty,s=Object.getOwnPropertyDescriptor,l=Object.getOwnPropertyNames,v=Object.getPrototypeOf,g=Object.prototype.hasOwnProperty,R=(t,i)=>function(){return i||(0,t[l(t)[0]])((i={exports:{}}).exports,i),i.exports},U=(t,i)=>{for(var o in i)a(t,o,{get:i[o],enumerable:!0})},ne=(t,i,o,f)=>{if(i&&typeof i=="object"||typeof i=="function")for(let d of l(i))!g.call(t,d)&&d!==o&&a(t,d,{get:()=>i[d],enumerable:!(f=s(i,d))||f.enumerable});return t},ie=(t,i,o)=>(o=t!=null?n(v(t)):{},ne(i||!t||!t.__esModule?a(o,"default",{value:t,enumerable:!0}):o,t)),V=t=>ne(a({},"__esModule",{value:!0}),t),G=R({"node_modules/@vue/shared/dist/shared.cjs.js"(t){Object.defineProperty(t,"__esModule",{value:!0});function i(b,K){const re=Object.create(null),fe=b.split(",");for(let He=0;He!!re[He.toLowerCase()]:He=>!!re[He]}var o={1:"TEXT",2:"CLASS",4:"STYLE",8:"PROPS",16:"FULL_PROPS",32:"HYDRATE_EVENTS",64:"STABLE_FRAGMENT",128:"KEYED_FRAGMENT",256:"UNKEYED_FRAGMENT",512:"NEED_PATCH",1024:"DYNAMIC_SLOTS",2048:"DEV_ROOT_FRAGMENT",[-1]:"HOISTED",[-2]:"BAIL"},f={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 E(b,K=0,re=b.length){let fe=b.split(/(\r?\n)/);const He=fe.filter((wt,ct)=>ct%2===1);fe=fe.filter((wt,ct)=>ct%2===0);let rt=0;const _t=[];for(let wt=0;wt=K){for(let ct=wt-m;ct<=wt+m||re>rt;ct++){if(ct<0||ct>=fe.length)continue;const hn=ct+1;_t.push(`${hn}${" ".repeat(Math.max(3-String(hn).length,0))}| ${fe[ct]}`);const Ur=fe[ct].length,Zn=He[ct]&&He[ct].length||0;if(ct===wt){const Hr=K-(rt-(Ur+Zn)),Ki=Math.max(1,re>rt?Ur-Hr:re-K);_t.push(" | "+" ".repeat(Hr)+"^".repeat(Ki))}else if(ct>wt){if(re>rt){const Hr=Math.max(Math.min(re-rt,Ur),1);_t.push(" | "+"^".repeat(Hr))}rt+=Ur+Zn}}break}return _t.join(` +`)}var j="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",te=i(j),Me=i(j+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected"),Xe=/[>/="'\u0009\u000a\u000c\u0020]/,Ie={};function Ke(b){if(Ie.hasOwnProperty(b))return Ie[b];const K=Xe.test(b);return K&&console.error(`unsafe attribute name: ${b}`),Ie[b]=!K}var Tt={acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv"},Ut=i("animation-iteration-count,border-image-outset,border-image-slice,border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width"),we=i("accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap");function Ve(b){if(Dt(b)){const K={};for(let re=0;re{if(re){const fe=re.split(Ue);fe.length>1&&(K[fe[0].trim()]=fe[1].trim())}}),K}function It(b){let K="";if(!b)return K;for(const re in b){const fe=b[re],He=re.startsWith("--")?re:Xn(re);(hr(fe)||typeof fe=="number"&&Ut(He))&&(K+=`${He}:${fe};`)}return K}function Ht(b){let K="";if(hr(b))K=b;else if(Dt(b))for(let re=0;re]/;function Di(b){const K=""+b,re=Ii.exec(K);if(!re)return K;let fe="",He,rt,_t=0;for(rt=re.index;rt||--!>|kr(re,K))}var Bn=b=>b==null?"":qt(b)?JSON.stringify(b,Bi,2):String(b),Bi=(b,K)=>pr(K)?{[`Map(${K.size})`]:[...K.entries()].reduce((re,[fe,He])=>(re[`${fe} =>`]=He,re),{})}:$t(K)?{[`Set(${K.size})`]:[...K.values()]}:qt(K)&&!Dt(K)&&!Wn(K)?String(K):K,Ui=["bigInt","optionalChaining","nullishCoalescingOperator"],ln=Object.freeze({}),un=Object.freeze([]),cn=()=>{},Nr=()=>!1,Lr=/^on[^a-z]/,jr=b=>Lr.test(b),Ir=b=>b.startsWith("onUpdate:"),Un=Object.assign,Hn=(b,K)=>{const re=b.indexOf(K);re>-1&&b.splice(re,1)},qn=Object.prototype.hasOwnProperty,Vn=(b,K)=>qn.call(b,K),Dt=Array.isArray,pr=b=>gr(b)==="[object Map]",$t=b=>gr(b)==="[object Set]",fn=b=>b instanceof Date,dn=b=>typeof b=="function",hr=b=>typeof b=="string",Hi=b=>typeof b=="symbol",qt=b=>b!==null&&typeof b=="object",Dr=b=>qt(b)&&dn(b.then)&&dn(b.catch),zn=Object.prototype.toString,gr=b=>zn.call(b),qi=b=>gr(b).slice(8,-1),Wn=b=>gr(b)==="[object Object]",Kn=b=>hr(b)&&b!=="NaN"&&b[0]!=="-"&&""+parseInt(b,10)===b,Jn=i(",key,ref,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),mr=b=>{const K=Object.create(null);return re=>K[re]||(K[re]=b(re))},Gn=/-(\w)/g,Yn=mr(b=>b.replace(Gn,(K,re)=>re?re.toUpperCase():"")),Vi=/\B([A-Z])/g,Xn=mr(b=>b.replace(Vi,"-$1").toLowerCase()),vr=mr(b=>b.charAt(0).toUpperCase()+b.slice(1)),zi=mr(b=>b?`on${vr(b)}`:""),pn=(b,K)=>b!==K&&(b===b||K===K),Wi=(b,K)=>{for(let re=0;re{Object.defineProperty(b,K,{configurable:!0,enumerable:!1,value:re})},Fr=b=>{const K=parseFloat(b);return isNaN(K)?b:K},Br,Qn=()=>Br||(Br=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});t.EMPTY_ARR=un,t.EMPTY_OBJ=ln,t.NO=Nr,t.NOOP=cn,t.PatchFlagNames=o,t.babelParserDefaultPlugins=Ui,t.camelize=Yn,t.capitalize=vr,t.def=$r,t.escapeHtml=Di,t.escapeHtmlComment=$i,t.extend=Un,t.generateCodeFrame=E,t.getGlobalThis=Qn,t.hasChanged=pn,t.hasOwn=Vn,t.hyphenate=Xn,t.invokeArrayFns=Wi,t.isArray=Dt,t.isBooleanAttr=Me,t.isDate=fn,t.isFunction=dn,t.isGloballyWhitelisted=p,t.isHTMLTag=Rr,t.isIntegerKey=Kn,t.isKnownAttr=we,t.isMap=pr,t.isModelListener=Ir,t.isNoUnitNumericStyleProp=Ut,t.isObject=qt,t.isOn=jr,t.isPlainObject=Wn,t.isPromise=Dr,t.isReservedProp=Jn,t.isSSRSafeAttrName=Ke,t.isSVGTag=ji,t.isSet=$t,t.isSpecialBooleanAttr=te,t.isString=hr,t.isSymbol=Hi,t.isVoidTag=Mr,t.looseEqual=kr,t.looseIndexOf=Fn,t.makeMap=i,t.normalizeClass=Ht,t.normalizeStyle=Ve,t.objectToString=zn,t.parseStringStyle=bt,t.propsToAttrMap=Tt,t.remove=Hn,t.slotFlagsText=f,t.stringifyStyle=It,t.toDisplayString=Bn,t.toHandlerKey=zi,t.toNumber=Fr,t.toRawType=qi,t.toTypeString=gr}}),C=R({"node_modules/@vue/shared/index.js"(t,i){i.exports=G()}}),y=R({"node_modules/@vue/reactivity/dist/reactivity.cjs.js"(t){Object.defineProperty(t,"__esModule",{value:!0});var i=C(),o=new WeakMap,f=[],d,p=Symbol("iterate"),m=Symbol("Map key iterate");function E(u){return u&&u._isEffect===!0}function j(u,T=i.EMPTY_OBJ){E(u)&&(u=u.raw);const L=Xe(u,T);return T.lazy||L(),L}function te(u){u.active&&(Ie(u),u.options.onStop&&u.options.onStop(),u.active=!1)}var Me=0;function Xe(u,T){const L=function(){if(!L.active)return u();if(!f.includes(L)){Ie(L);try{return we(),f.push(L),d=L,u()}finally{f.pop(),Ve(),d=f[f.length-1]}}};return L.id=Me++,L.allowRecurse=!!T.allowRecurse,L._isEffect=!0,L.active=!0,L.raw=u,L.deps=[],L.options=T,L}function Ie(u){const{deps:T}=u;if(T.length){for(let L=0;L{ht&&ht.forEach(Ft=>{(Ft!==d||Ft.allowRecurse)&&nt.add(Ft)})};if(T==="clear")Le.forEach(xt);else if(L==="length"&&i.isArray(u))Le.forEach((ht,Ft)=>{(Ft==="length"||Ft>=se)&&xt(ht)});else switch(L!==void 0&&xt(Le.get(L)),T){case"add":i.isArray(u)?i.isIntegerKey(L)&&xt(Le.get("length")):(xt(Le.get(p)),i.isMap(u)&&xt(Le.get(m)));break;case"delete":i.isArray(u)||(xt(Le.get(p)),i.isMap(u)&&xt(Le.get(m)));break;case"set":i.isMap(u)&&xt(Le.get(p));break}const gn=ht=>{ht.options.onTrigger&&ht.options.onTrigger({effect:ht,target:u,key:L,type:T,newValue:se,oldValue:J,oldTarget:me}),ht.options.scheduler?ht.options.scheduler(ht):ht()};nt.forEach(gn)}var bt=i.makeMap("__proto__,__v_isRef,__isVue"),It=new Set(Object.getOwnPropertyNames(Symbol).map(u=>Symbol[u]).filter(i.isSymbol)),Ht=Mr(),Pr=Mr(!1,!0),on=Mr(!0),sn=Mr(!0,!0),Rr=ji();function ji(){const u={};return["includes","indexOf","lastIndexOf"].forEach(T=>{u[T]=function(...L){const se=b(this);for(let me=0,Le=this.length;me{u[T]=function(...L){Ut();const se=b(this)[T].apply(this,L);return Ve(),se}}),u}function Mr(u=!1,T=!1){return function(se,J,me){if(J==="__v_isReactive")return!u;if(J==="__v_isReadonly")return u;if(J==="__v_raw"&&me===(u?T?Yn:Gn:T?mr:Jn).get(se))return se;const Le=i.isArray(se);if(!u&&Le&&i.hasOwn(Rr,J))return Reflect.get(Rr,J,me);const nt=Reflect.get(se,J,me);return(i.isSymbol(J)?It.has(J):bt(J))||(u||Ne(se,"get",J),T)?nt:fe(nt)?!Le||!i.isIntegerKey(J)?nt.value:nt:i.isObject(nt)?u?pn(nt):vr(nt):nt}}var Ii=$n(),Di=$n(!0);function $n(u=!1){return function(L,se,J,me){let Le=L[se];if(!u&&(J=b(J),Le=b(Le),!i.isArray(L)&&fe(Le)&&!fe(J)))return Le.value=J,!0;const nt=i.isArray(L)&&i.isIntegerKey(se)?Number(se)i.isObject(u)?vr(u):u,un=u=>i.isObject(u)?pn(u):u,cn=u=>u,Nr=u=>Reflect.getPrototypeOf(u);function Lr(u,T,L=!1,se=!1){u=u.__v_raw;const J=b(u),me=b(T);T!==me&&!L&&Ne(J,"get",T),!L&&Ne(J,"get",me);const{has:Le}=Nr(J),nt=se?cn:L?un:ln;if(Le.call(J,T))return nt(u.get(T));if(Le.call(J,me))return nt(u.get(me));u!==J&&u.get(T)}function jr(u,T=!1){const L=this.__v_raw,se=b(L),J=b(u);return u!==J&&!T&&Ne(se,"has",u),!T&&Ne(se,"has",J),u===J?L.has(u):L.has(u)||L.has(J)}function Ir(u,T=!1){return u=u.__v_raw,!T&&Ne(b(u),"iterate",p),Reflect.get(u,"size",u)}function Un(u){u=b(u);const T=b(this);return Nr(T).has.call(T,u)||(T.add(u),Ue(T,"add",u,u)),this}function Hn(u,T){T=b(T);const L=b(this),{has:se,get:J}=Nr(L);let me=se.call(L,u);me?Kn(L,se,u):(u=b(u),me=se.call(L,u));const Le=J.call(L,u);return L.set(u,T),me?i.hasChanged(T,Le)&&Ue(L,"set",u,T,Le):Ue(L,"add",u,T),this}function qn(u){const T=b(this),{has:L,get:se}=Nr(T);let J=L.call(T,u);J?Kn(T,L,u):(u=b(u),J=L.call(T,u));const me=se?se.call(T,u):void 0,Le=T.delete(u);return J&&Ue(T,"delete",u,void 0,me),Le}function Vn(){const u=b(this),T=u.size!==0,L=i.isMap(u)?new Map(u):new Set(u),se=u.clear();return T&&Ue(u,"clear",void 0,void 0,L),se}function Dt(u,T){return function(se,J){const me=this,Le=me.__v_raw,nt=b(Le),xt=T?cn:u?un:ln;return!u&&Ne(nt,"iterate",p),Le.forEach((gn,ht)=>se.call(J,xt(gn),xt(ht),me))}}function pr(u,T,L){return function(...se){const J=this.__v_raw,me=b(J),Le=i.isMap(me),nt=u==="entries"||u===Symbol.iterator&&Le,xt=u==="keys"&&Le,gn=J[u](...se),ht=L?cn:T?un:ln;return!T&&Ne(me,"iterate",xt?m:p),{next(){const{value:Ft,done:Ji}=gn.next();return Ji?{value:Ft,done:Ji}:{value:nt?[ht(Ft[0]),ht(Ft[1])]:ht(Ft),done:Ji}},[Symbol.iterator](){return this}}}}function $t(u){return function(...T){{const L=T[0]?`on key "${T[0]}" `:"";console.warn(`${i.capitalize(u)} operation ${L}failed: target is readonly.`,b(this))}return u==="delete"?!1:this}}function fn(){const u={get(me){return Lr(this,me)},get size(){return Ir(this)},has:jr,add:Un,set:Hn,delete:qn,clear:Vn,forEach:Dt(!1,!1)},T={get(me){return Lr(this,me,!1,!0)},get size(){return Ir(this)},has:jr,add:Un,set:Hn,delete:qn,clear:Vn,forEach:Dt(!1,!0)},L={get(me){return Lr(this,me,!0)},get size(){return Ir(this,!0)},has(me){return jr.call(this,me,!0)},add:$t("add"),set:$t("set"),delete:$t("delete"),clear:$t("clear"),forEach:Dt(!0,!1)},se={get(me){return Lr(this,me,!0,!0)},get size(){return Ir(this,!0)},has(me){return jr.call(this,me,!0)},add:$t("add"),set:$t("set"),delete:$t("delete"),clear:$t("clear"),forEach:Dt(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(me=>{u[me]=pr(me,!1,!1),L[me]=pr(me,!0,!1),T[me]=pr(me,!1,!0),se[me]=pr(me,!0,!0)}),[u,L,T,se]}var[dn,hr,Hi,qt]=fn();function Dr(u,T){const L=T?u?qt:Hi:u?hr:dn;return(se,J,me)=>J==="__v_isReactive"?!u:J==="__v_isReadonly"?u:J==="__v_raw"?se:Reflect.get(i.hasOwn(L,J)&&J in se?L:se,J,me)}var zn={get:Dr(!1,!1)},gr={get:Dr(!1,!0)},qi={get:Dr(!0,!1)},Wn={get:Dr(!0,!0)};function Kn(u,T,L){const se=b(L);if(se!==L&&T.call(u,se)){const J=i.toRawType(u);console.warn(`Reactive ${J} contains both the raw and reactive versions of the same object${J==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var Jn=new WeakMap,mr=new WeakMap,Gn=new WeakMap,Yn=new WeakMap;function Vi(u){switch(u){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Xn(u){return u.__v_skip||!Object.isExtensible(u)?0:Vi(i.toRawType(u))}function vr(u){return u&&u.__v_isReadonly?u:$r(u,!1,Fn,zn,Jn)}function zi(u){return $r(u,!1,Bi,gr,mr)}function pn(u){return $r(u,!0,Bn,qi,Gn)}function Wi(u){return $r(u,!0,Ui,Wn,Yn)}function $r(u,T,L,se,J){if(!i.isObject(u))return console.warn(`value cannot be made reactive: ${String(u)}`),u;if(u.__v_raw&&!(T&&u.__v_isReactive))return u;const me=J.get(u);if(me)return me;const Le=Xn(u);if(Le===0)return u;const nt=new Proxy(u,Le===2?se:L);return J.set(u,nt),nt}function Fr(u){return Br(u)?Fr(u.__v_raw):!!(u&&u.__v_isReactive)}function Br(u){return!!(u&&u.__v_isReadonly)}function Qn(u){return Fr(u)||Br(u)}function b(u){return u&&b(u.__v_raw)||u}function K(u){return i.def(u,"__v_skip",!0),u}var re=u=>i.isObject(u)?vr(u):u;function fe(u){return!!(u&&u.__v_isRef===!0)}function He(u){return wt(u)}function rt(u){return wt(u,!0)}var _t=class{constructor(u,T=!1){this._shallow=T,this.__v_isRef=!0,this._rawValue=T?u:b(u),this._value=T?u:re(u)}get value(){return Ne(b(this),"get","value"),this._value}set value(u){u=this._shallow?u:b(u),i.hasChanged(u,this._rawValue)&&(this._rawValue=u,this._value=this._shallow?u:re(u),Ue(b(this),"set","value",u))}};function wt(u,T=!1){return fe(u)?u:new _t(u,T)}function ct(u){Ue(b(u),"set","value",u.value)}function hn(u){return fe(u)?u.value:u}var Ur={get:(u,T,L)=>hn(Reflect.get(u,T,L)),set:(u,T,L,se)=>{const J=u[T];return fe(J)&&!fe(L)?(J.value=L,!0):Reflect.set(u,T,L,se)}};function Zn(u){return Fr(u)?u:new Proxy(u,Ur)}var Hr=class{constructor(u){this.__v_isRef=!0;const{get:T,set:L}=u(()=>Ne(this,"get","value"),()=>Ue(this,"set","value"));this._get=T,this._set=L}get value(){return this._get()}set value(u){this._set(u)}};function Ki(u){return new Hr(u)}function wl(u){Qn(u)||console.warn("toRefs() expects a reactive object but received a plain one.");const T=i.isArray(u)?new Array(u.length):{};for(const L in u)T[L]=ao(u,L);return T}var xl=class{constructor(u,T){this._object=u,this._key=T,this.__v_isRef=!0}get value(){return this._object[this._key]}set value(u){this._object[this._key]=u}};function ao(u,T){return fe(u[T])?u[T]:new xl(u,T)}var Sl=class{constructor(u,T,L){this._setter=T,this._dirty=!0,this.__v_isRef=!0,this.effect=j(u,{lazy:!0,scheduler:()=>{this._dirty||(this._dirty=!0,Ue(b(this),"set","value"))}}),this.__v_isReadonly=L}get value(){const u=b(this);return u._dirty&&(u._value=this.effect(),u._dirty=!1),Ne(u,"get","value"),u._value}set value(u){this._setter(u)}};function El(u){let T,L;return i.isFunction(u)?(T=u,L=()=>{console.warn("Write operation failed: computed value is readonly")}):(T=u.get,L=u.set),new Sl(T,L,i.isFunction(u)||!u.set)}t.ITERATE_KEY=p,t.computed=El,t.customRef=Ki,t.effect=j,t.enableTracking=we,t.isProxy=Qn,t.isReactive=Fr,t.isReadonly=Br,t.isRef=fe,t.markRaw=K,t.pauseTracking=Ut,t.proxyRefs=Zn,t.reactive=vr,t.readonly=pn,t.ref=He,t.resetTracking=Ve,t.shallowReactive=zi,t.shallowReadonly=Wi,t.shallowRef=rt,t.stop=te,t.toRaw=b,t.toRef=ao,t.toRefs=wl,t.track=Ne,t.trigger=Ue,t.triggerRef=ct,t.unref=hn}}),w=R({"node_modules/@vue/reactivity/index.js"(t,i){i.exports=y()}}),_={};U(_,{Alpine:()=>io,default:()=>_l}),r.exports=V(_);var S=!1,P=!1,I=[],de=-1;function D(t){A(t)}function A(t){I.includes(t)||I.push(t),ee()}function N(t){let i=I.indexOf(t);i!==-1&&i>de&&I.splice(i,1)}function ee(){!P&&!S&&(S=!0,queueMicrotask(ye))}function ye(){S=!1,P=!0;for(let t=0;tt.effect(i,{scheduler:o=>{Ge?D(o):o()}}),Je=t.raw}function dt(t){X=t}function mt(t){let i=()=>{};return[f=>{let d=X(f);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),Te(d))},d},()=>{i()}]}function Et(t,i){let o=!0,f,d=X(()=>{let p=t();JSON.stringify(p),o?f=p:queueMicrotask(()=>{i(p,f),f=p}),o=!1});return()=>Te(d)}var Oe=[],_e=[],Ce=[];function Ee(t){Ce.push(t)}function pe(t,i){typeof i=="function"?(t._x_cleanups||(t._x_cleanups=[]),t._x_cleanups.push(i)):(i=t,_e.push(i))}function Z(t){Oe.push(t)}function qe(t,i,o){t._x_attributeCleanups||(t._x_attributeCleanups={}),t._x_attributeCleanups[i]||(t._x_attributeCleanups[i]=[]),t._x_attributeCleanups[i].push(o)}function W(t,i){t._x_attributeCleanups&&Object.entries(t._x_attributeCleanups).forEach(([o,f])=>{(i===void 0||i.includes(o))&&(f.forEach(d=>d()),delete t._x_attributeCleanups[o])})}function ae(t){var i,o;for((i=t._x_effects)==null||i.forEach(N);(o=t._x_cleanups)!=null&&o.length;)t._x_cleanups.pop()()}var be=new MutationObserver(We),De=!1;function ve(){be.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),De=!0}function le(){Ze(),be.disconnect(),De=!1}var ut=[];function Ze(){let t=be.takeRecords();ut.push(()=>t.length>0&&We(t));let i=ut.length;queueMicrotask(()=>{if(ut.length===i)for(;ut.length>0;)ut.shift()()})}function Q(t){if(!De)return t();le();let i=t();return ve(),i}var k=!1,$=[];function ge(){k=!0}function z(){k=!1,We($),$=[]}function We(t){if(k){$=$.concat(t);return}let i=new Set,o=new Set,f=new Map,d=new Map;for(let p=0;pm.nodeType===1&&i.add(m)),t[p].removedNodes.forEach(m=>m.nodeType===1&&o.add(m))),t[p].type==="attributes")){let m=t[p].target,E=t[p].attributeName,j=t[p].oldValue,te=()=>{f.has(m)||f.set(m,[]),f.get(m).push({name:E,value:m.getAttribute(E)})},Me=()=>{d.has(m)||d.set(m,[]),d.get(m).push(E)};m.hasAttribute(E)&&j===null?te():m.hasAttribute(E)?(Me(),te()):Me()}d.forEach((p,m)=>{W(m,p)}),f.forEach((p,m)=>{Oe.forEach(E=>E(m,p))});for(let p of o)i.has(p)||_e.forEach(m=>m(p));i.forEach(p=>{p._x_ignoreSelf=!0,p._x_ignore=!0});for(let p of i)o.has(p)||p.isConnected&&(delete p._x_ignoreSelf,delete p._x_ignore,Ce.forEach(m=>m(p)),p._x_ignore=!0,p._x_ignoreSelf=!0);i.forEach(p=>{delete p._x_ignoreSelf,delete p._x_ignore}),i=null,o=null,f=null,d=null}function he(t){return ce(q(t))}function B(t,i,o){return t._x_dataStack=[i,...q(o||t)],()=>{t._x_dataStack=t._x_dataStack.filter(f=>f!==i)}}function q(t){return t._x_dataStack?t._x_dataStack:typeof ShadowRoot=="function"&&t instanceof ShadowRoot?q(t.host):t.parentNode?q(t.parentNode):[]}function ce(t){return new Proxy({objects:t},Be)}var Be={ownKeys({objects:t}){return Array.from(new Set(t.flatMap(i=>Object.keys(i))))},has({objects:t},i){return i==Symbol.unscopables?!1:t.some(o=>Object.prototype.hasOwnProperty.call(o,i)||Reflect.has(o,i))},get({objects:t},i,o){return i=="toJSON"?Ae:Reflect.get(t.find(f=>Reflect.has(f,i))||{},i,o)},set({objects:t},i,o,f){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(f,o)||!0:Reflect.set(d,i,o)}};function Ae(){return Reflect.ownKeys(this).reduce((i,o)=>(i[o]=Reflect.get(this,o),i),{})}function st(t){let i=f=>typeof f=="object"&&!Array.isArray(f)&&f!==null,o=(f,d="")=>{Object.entries(Object.getOwnPropertyDescriptors(f)).forEach(([p,{value:m,enumerable:E}])=>{if(E===!1||m===void 0||typeof m=="object"&&m!==null&&m.__v_skip)return;let j=d===""?p:`${d}.${p}`;typeof m=="object"&&m!==null&&m._x_interceptor?f[p]=m.initialize(t,j,p):i(m)&&m!==f&&!(m instanceof Element)&&o(m,j)})};return o(t)}function it(t,i=()=>{}){let o={initialValue:void 0,_x_interceptor:!0,initialize(f,d,p){return t(this.initialValue,()=>Rt(f,d),m=>Nt(f,d,m),d,p)}};return i(o),f=>{if(typeof f=="object"&&f!==null&&f._x_interceptor){let d=o.initialize.bind(o);o.initialize=(p,m,E)=>{let j=f.initialize(p,m,E);return o.initialValue=j,d(p,m,E)}}else o.initialValue=f;return o}}function Rt(t,i){return i.split(".").reduce((o,f)=>o[f],t)}function Nt(t,i,o){if(typeof i=="string"&&(i=i.split(".")),i.length===1)t[i[0]]=o;else{if(i.length===0)throw error;return t[i[0]]||(t[i[0]]={}),Nt(t[i[0]],i.slice(1),o)}}var cr={};function Ot(t,i){cr[t]=i}function Kt(t,i){let o=fr(i);return Object.entries(cr).forEach(([f,d])=>{Object.defineProperty(t,`$${f}`,{get(){return d(i,o)},enumerable:!1})}),t}function fr(t){let[i,o]=oe(t),f={interceptor:it,...i};return pe(t,o),f}function Cn(t,i,o,...f){try{return o(...f)}catch(d){tr(d,t,i)}}function tr(t,i,o=void 0){t=Object.assign(t??{message:"No error message given."},{el:i,expression:o}),console.warn(`Alpine Expression Error: ${t.message} + +${o?'Expression: "'+o+`" + +`:""}`,i),setTimeout(()=>{throw t},0)}var Sr=!0;function An(t){let i=Sr;Sr=!1;let o=t();return Sr=i,o}function Jt(t,i,o={}){let f;return vt(t,i)(d=>f=d,o),f}function vt(...t){return Zr(...t)}var Zr=Pn;function Tn(t){Zr=t}function Pn(t,i){let o={};Kt(o,t);let f=[o,...q(t)],d=typeof i=="function"?yi(f,i):_i(f,i,t);return Cn.bind(null,t,i,d)}function yi(t,i){return(o=()=>{},{scope:f={},params:d=[]}={})=>{let p=i.apply(ce([f,...t]),d);Er(o,p)}}var en={};function bi(t,i){if(en[t])return en[t];let o=Object.getPrototypeOf(async function(){}).constructor,f=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t,p=(()=>{try{let m=new o(["__self","scope"],`with (scope) { __self.result = ${f} }; __self.finished = true; return __self.result;`);return Object.defineProperty(m,"name",{value:`[Alpine] ${t}`}),m}catch(m){return tr(m,i,t),Promise.resolve()}})();return en[t]=p,p}function _i(t,i,o){let f=bi(i,o);return(d=()=>{},{scope:p={},params:m=[]}={})=>{f.result=void 0,f.finished=!1;let E=ce([p,...t]);if(typeof f=="function"){let j=f(f,E).catch(te=>tr(te,o,i));f.finished?(Er(d,f.result,E,m,o),f.result=void 0):j.then(te=>{Er(d,te,E,m,o)}).catch(te=>tr(te,o,i)).finally(()=>f.result=void 0)}}}function Er(t,i,o,f,d){if(Sr&&typeof i=="function"){let p=i.apply(o,f);p instanceof Promise?p.then(m=>Er(t,m,o,f)).catch(m=>tr(m,d,i)):t(p)}else typeof i=="object"&&i instanceof Promise?i.then(p=>t(p)):t(i)}var Or="x-";function Gt(t=""){return Or+t}function Rn(t){Or=t}var Cr={};function c(t,i){return Cr[t]=i,{before(o){if(!Cr[o]){console.warn(String.raw`Cannot find directive \`${o}\`. \`${t}\` will use the default order of execution`);return}const f=Ye.indexOf(o);Ye.splice(f>=0?f:Ye.indexOf("DEFAULT"),0,t)}}}function h(t){return Object.keys(Cr).includes(t)}function x(t,i,o){if(i=Array.from(i),t._x_virtualDirectives){let p=Object.entries(t._x_virtualDirectives).map(([E,j])=>({name:E,value:j})),m=O(p);p=p.map(E=>m.find(j=>j.name===E.name)?{name:`x-bind:${E.name}`,value:`"${E.value}"`}:E),i=i.concat(p)}let f={};return i.map($e((p,m)=>f[p]=m)).filter(je).map(Fe(f,o)).sort(At).map(p=>ue(t,p))}function O(t){return Array.from(t).map($e()).filter(i=>!je(i))}var M=!1,F=new Map,H=Symbol();function Y(t){M=!0;let i=Symbol();H=i,F.set(i,[]);let o=()=>{for(;F.get(i).length;)F.get(i).shift()();F.delete(i)},f=()=>{M=!1,o()};t(o),f()}function oe(t){let i=[],o=E=>i.push(E),[f,d]=mt(t);return i.push(d),[{Alpine:an,effect:f,cleanup:o,evaluateLater:vt.bind(vt,t),evaluate:Jt.bind(Jt,t)},()=>i.forEach(E=>E())]}function ue(t,i){let o=()=>{},f=Cr[i.type]||o,[d,p]=oe(t);qe(t,i.original,p);let m=()=>{t._x_ignore||t._x_ignoreSelf||(f.inline&&f.inline(t,i,d),f=f.bind(f,t,i,d),M?F.get(H).push(f):f())};return m.runCleanups=p,m}var ke=(t,i)=>({name:o,value:f})=>(o.startsWith(t)&&(o=o.replace(t,i)),{name:o,value:f}),Pe=t=>t;function $e(t=()=>{}){return({name:i,value:o})=>{let{name:f,value:d}=Se.reduce((p,m)=>m(p),{name:i,value:o});return f!==i&&t(f,i),{name:f,value:d}}}var Se=[];function Re(t){Se.push(t)}function je({name:t}){return et().test(t)}var et=()=>new RegExp(`^${Or}([^:^.]+)\\b`);function Fe(t,i){return({name:o,value:f})=>{let d=o.match(et()),p=o.match(/:([a-zA-Z0-9\-_:]+)/),m=o.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],E=i||t[o]||o;return{type:d?d[1]:null,value:p?p[1]:null,modifiers:m.map(j=>j.replace(".","")),expression:f,original:E}}}var tt="DEFAULT",Ye=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",tt,"teleport"];function At(t,i){let o=Ye.indexOf(t.type)===-1?tt:t.type,f=Ye.indexOf(i.type)===-1?tt:i.type;return Ye.indexOf(o)-Ye.indexOf(f)}function at(t,i,o={}){t.dispatchEvent(new CustomEvent(i,{detail:o,bubbles:!0,composed:!0,cancelable:!0}))}function Ct(t,i){if(typeof ShadowRoot=="function"&&t instanceof ShadowRoot){Array.from(t.children).forEach(d=>Ct(d,i));return}let o=!1;if(i(t,()=>o=!0),o)return;let f=t.firstElementChild;for(;f;)Ct(f,i),f=f.nextElementSibling}function yt(t,...i){console.warn(`Alpine Warning: ${t}`,...i)}var rr=!1;function Mn(){rr&&yt("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),rr=!0,document.body||yt("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `