diff --git a/app/Http/Controllers/SchedulerController.php b/app/Http/Controllers/SchedulerController.php index 91f64068d427..6ec669972dde 100644 --- a/app/Http/Controllers/SchedulerController.php +++ b/app/Http/Controllers/SchedulerController.php @@ -15,7 +15,10 @@ class SchedulerController extends Controller { public function index() { - if (auth()->user()->company()->account->latest_version == '0.0.0') { + /** @var \App\Models\User $user */ + $user = auth()->user(); + + if ($user->company()->account->latest_version == '0.0.0') { return response()->json(['message' => ctrans('texts.scheduler_has_never_run')], 400); } else { return response()->json(['message' => ctrans('texts.scheduler_has_run')], 200); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php new file mode 100644 index 000000000000..d3510e006306 --- /dev/null +++ b/app/Http/Controllers/SearchController.php @@ -0,0 +1,91 @@ +user(); + + // $search = collect([Client::class, Invoice::class])->each(function ) + return response()->json([ + 'clients' => $this->clientMap($user), + 'client_contacts' => $this->clientContactMap($user), + 'invoices' => $this->invoiceMap($user), + ], 200); + + } + + private function clientMap(User $user) { + + return Client::query() + ->company() + ->when($user->cannot('view_all') || $user->cannot('view_client'), function ($query) use($user) { + $query->where('user_id', $user->id); + }) + ->cursor() + ->map(function ($client){ + return [ + 'name' => $client->present()->name(), + 'type' => 'client', + 'id' => $client->hashed_id, + 'path' => "clients/{$client->hashed_id}/edit" + ]; + }); + } + + private function clientContactMap(User $user) { + + return ClientContact::query() + ->company() + ->with('client') + ->when($user->cannot('view_all') || $user->cannot('view_client'), function ($query) use($user) { + $query->where('user_id', $user->id); + }) + ->cursor() + ->map(function ($contact){ + return [ + 'name' => $contact->present()->search_display(), + 'type' => 'client_contact', + 'id' => $contact->client->hashed_id, + 'path' => "clients/{$contact->client->hashed_id}" + ]; + }); + } + + private function invoiceMap(User $user) { + + return Invoice::query() + ->company() + ->with('client') + ->when($user->cannot('view_all') || $user->cannot('view_invoice'), function ($query) use($user) { + $query->where('user_id', $user->id); + }) + ->cursor() + ->map(function ($invoice){ + return [ + 'name' => $invoice->client->present()->name() . ' - ' . $invoice->number, + 'type' => 'invoice', + 'id' => $invoice->hashed_id, + 'path' => "clients/{$invoice->hashed_id}/edit" + ]; + }); + } +} diff --git a/app/Http/Requests/Search/GenericSearchRequest.php b/app/Http/Requests/Search/GenericSearchRequest.php new file mode 100644 index 000000000000..8bd4f4b46970 --- /dev/null +++ b/app/Http/Requests/Search/GenericSearchRequest.php @@ -0,0 +1,43 @@ + 'bail|sometimes|string' + ]; + + return $rules; + } + + public function prepareForValidation() + { + $input = $this->all(); + + $this->replace($input); + } +} diff --git a/app/Models/Presenters/ClientContactPresenter.php b/app/Models/Presenters/ClientContactPresenter.php index dc2229d8e2c9..177421a6edfa 100644 --- a/app/Models/Presenters/ClientContactPresenter.php +++ b/app/Models/Presenters/ClientContactPresenter.php @@ -32,11 +32,16 @@ class ClientContactPresenter extends EntityPresenter public function first_name() { - return $this->entity->first_name ?: ''; + return $this->entity->first_name ?? ''; } public function last_name() { - return $this->entity->last_name ?: ''; + return $this->entity->last_name ?? ''; + } + + public function search_display() + { + return $this->name().' <'.$this->entity->email.'>' ?? ''; } } diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index 25b5a827aaef..96878cd2480e 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -82,7 +82,6 @@ class ClientService ->where('is_deleted', 0) ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]) ->selectRaw('SUM(payments.amount - payments.applied) as amount')->first()->amount ?? 0; - // ->sum(DB::Raw('amount - applied')->getValue(DB::connection()->getQueryGrammar())); DB::connection(config('database.default'))->transaction(function () use ($amount) { $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); diff --git a/routes/api.php b/routes/api.php index aa4e108a5273..3ce1fdc1737e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -111,6 +111,7 @@ use App\Http\Controllers\Reports\ClientContactReportController; use App\Http\Controllers\Reports\PurchaseOrderReportController; use App\Http\Controllers\Reports\RecurringInvoiceReportController; use App\Http\Controllers\Reports\PurchaseOrderItemReportController; +use App\Http\Controllers\SearchController; Route::group(['middleware' => ['throttle:api', 'api_secret_check']], function () { Route::post('api/v1/signup', [AccountController::class, 'store'])->name('signup.submit'); @@ -317,7 +318,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('reports/tax_summary_report', TaxSummaryReportController::class); Route::post('reports/user_sales_report', UserSalesReportController::class); Route::post('reports/preview/{hash}', ReportPreviewController::class); - + Route::post('search', SearchController::class); Route::resource('task_schedulers', TaskSchedulerController::class); Route::post('task_schedulers/bulk', [TaskSchedulerController::class, 'bulk'])->name('task_schedulers.bulk'); diff --git a/tests/Feature/Search/SearchApiTest.php b/tests/Feature/Search/SearchApiTest.php new file mode 100644 index 000000000000..e04804e721b7 --- /dev/null +++ b/tests/Feature/Search/SearchApiTest.php @@ -0,0 +1,61 @@ +makeTestData(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + + $this->withoutExceptionHandling(); + + } + + public function testActivityEntity() + { + + $response = false; + + $data = []; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/search', $data); + + $response->assertStatus(200); + + nlog($response->json()); + + } + +} \ No newline at end of file diff --git a/tests/Unit/CheckDataTest.php b/tests/Unit/CheckDataTest.php new file mode 100644 index 000000000000..2de8f3d26416 --- /dev/null +++ b/tests/Unit/CheckDataTest.php @@ -0,0 +1,285 @@ +faker = \Faker\Factory::create(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + + } + + private function buildData() + { + $this->account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000, + ]); + + $this->account->num_users = 3; + $this->account->save(); + + $this->user = User::factory()->create([ + 'account_id' => $this->account->id, + 'confirmation_code' => 'xyz123', + 'email' => $this->faker->unique()->safeEmail(), + ]); + + $settings = CompanySettings::defaults(); + $settings->client_online_payment_notification = false; + $settings->client_manual_payment_notification = false; + + $this->company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); + + $this->company->settings = $settings; + $this->company->save(); + + $this->cu = CompanyUserFactory::create($this->user->id, $this->company->id, $this->account->id); + $this->cu->is_owner = true; + $this->cu->is_admin = true; + $this->cu->is_locked = false; + $this->cu->save(); + + $this->token = \Illuminate\Support\Str::random(64); + + $company_token = new CompanyToken; + $company_token->user_id = $this->user->id; + $company_token->company_id = $this->company->id; + $company_token->account_id = $this->account->id; + $company_token->name = 'test token'; + $company_token->token = $this->token; + $company_token->is_system = true; + + $company_token->save(); + + $this->client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + 'name' => 'bob', + 'address1' => '1234', + 'balance' => 100, + 'paid_to_date' => 50, + ]); + + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'first_name' => 'john', + 'last_name' => 'doe', + 'email' => 'john@doe.com' + ]); + + } + + public function testDbQueriesRaw5() + { + $this->buildData(); + + $i = Invoice::factory()->count(5)->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + ]); + + Invoice::where('status_id', 2)->cursor()->each(function ($i) { + + $i->service()->markPaid()->save(); + + }); + + Payment::with('paymentables')->cursor()->each(function($payment){ + $this->assertNotNull($payment->paymentables()->where('paymentable_type', \App\Models\Credit::class)->get() + ->sum(\DB::raw('amount')->getValue(\DB::connection()->getQueryGrammar()))); + }); + + Payment::with('paymentables')->cursor()->each(function ($payment) { + $this->assertNotNull($payment->paymentables()->where('paymentable_type', \App\Models\Credit::class)->get() + ->sum('amount')); + }); + + $amount = Paymentable::first()->payment->paymentables()->where('paymentable_type', 'invnoices')->get()->sum('amount'); + + $this->assertNotNull($amount); + + } + + public function testDbQueriesRaw4() + { + $this->buildData(); + + ClientContact::factory()->count(10)->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + ]); + + $clients_refactor = \DB::table('clients') + ->leftJoin('client_contacts', function ($join){ + $join->on('client_contacts.client_id', '=', 'clients.id'); + }) + ->get(['clients.id', \DB::raw('count(client_contacts.id) as contact_count')]); + + // $this->assertNotNull($clients); + $this->assertNotNull($clients_refactor); + + } + + public function testDbQueriesRaw3() + { + $this->buildData(); + + User::factory()->create([ + 'account_id' => $this->account->id, + 'email' => $this->faker->unique()->safeEmail(), + ]); + + User::factory()->create([ + 'account_id' => $this->account->id, + 'email' => $this->faker->unique()->safeEmail(), + ]); + + $user_hash = 'a'; + + $user_count = User::where('account_id', $this->company->account->id) + ->where( + \DB::raw('CONCAT_WS(" ", first_name, last_name)'), + 'like', + '%'.$user_hash.'%' + ) + ->get(); + + $user_count_refactor = User::whereRaw("account_id = ? AND CONCAT_WS(' ', first_name, last_name) like ?", [$this->company->account_id, '%'.$user_hash.'%']) + ->get(); + + + $this->assertEquals($user_count_refactor->count(), $user_count->count()); + } + + public function testDbRawQueries1() + { + $this->buildData(); + + $results = \DB::select(\DB::raw(" + SELECT count(clients.id) as count + FROM clients + ")->getValue(\DB::connection()->getQueryGrammar())); + + + $refactored = \DB::select(" + SELECT count(clients.id) as count + FROM clients + "); + + $this->assertEquals($refactored[0]->count, $results[0]->count); + + } + + public function testDbRawQueries2() + { + $this->buildData(); + + Payment::factory()->count(5)->create([ + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + ]); + + Invoice::factory()->count(5)->create([ + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + ]); + + Invoice::where('status_id', 2)->cursor()->each(function ($i) { + + $i->service()->markPaid()->save(); + + }); + + $results = \DB::select(\DB::raw(" + SELECT + SUM(payments.amount) as amount + FROM payments + LEFT JOIN paymentables + ON + payments.id = paymentables.payment_id + WHERE paymentable_type = ? + AND paymentables.deleted_at is NULL + AND paymentables.amount > 0 + AND payments.is_deleted = 0 + AND payments.client_id = ?; + ")->getValue(\DB::connection()->getQueryGrammar()), ['invoices', $this->client->id]); + + $refactored = \DB::select(" + SELECT + SUM(payments.amount) as amount + FROM payments + LEFT JOIN paymentables + ON + payments.id = paymentables.payment_id + WHERE paymentable_type = ? + AND paymentables.deleted_at is NULL + AND paymentables.amount > 0 + AND payments.is_deleted = 0 + AND payments.client_id = ?; + ", ['invoices', $this->client->id]); + + $this->assertEquals($refactored[0]->amount, $results[0]->amount); + + } + +}