diff --git a/app/Actions/CheckoutRequests/CancelCheckoutRequestAction.php b/app/Actions/CheckoutRequests/CancelCheckoutRequestAction.php new file mode 100644 index 000000000000..2d6dd68a0837 --- /dev/null +++ b/app/Actions/CheckoutRequests/CancelCheckoutRequestAction.php @@ -0,0 +1,48 @@ +cancelRequest(); + + $asset->decrement('requests_counter', 1); + + $data['item'] = $asset; + $data['target'] = $user; + $data['item_quantity'] = 1; + $settings = Setting::getSettings(); + + $logaction = new Actionlog(); + $logaction->item_id = $data['asset_id'] = $asset->id; + $logaction->item_type = $data['item_type'] = Asset::class; + $logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s'); + $logaction->target_id = $data['user_id'] = auth()->id(); + $logaction->target_type = User::class; + $logaction->location_id = $user->location_id ?? null; + $logaction->logaction('request canceled'); + + try { + $settings->notify(new RequestAssetCancelation($data)); + } catch (\Exception $e) { + \Log::warning($e); + } + + return true; + } + +} \ No newline at end of file diff --git a/app/Actions/CheckoutRequests/CreateCheckoutRequestAction.php b/app/Actions/CheckoutRequests/CreateCheckoutRequestAction.php new file mode 100644 index 000000000000..6870cfba2d4f --- /dev/null +++ b/app/Actions/CheckoutRequests/CreateCheckoutRequestAction.php @@ -0,0 +1,54 @@ +find($asset->id))) { + throw new AssetNotRequestable($asset); + } + if (!Company::isCurrentUserHasAccess($asset)) { + throw new AuthorizationException(); + } + + $data['item'] = $asset; + $data['target'] = $user; + $data['item_quantity'] = 1; + $settings = Setting::getSettings(); + + $logaction = new Actionlog(); + $logaction->item_id = $data['asset_id'] = $asset->id; + $logaction->item_type = $data['item_type'] = Asset::class; + $logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s'); + $logaction->target_id = $data['user_id'] = auth()->id(); + $logaction->target_type = User::class; + $logaction->location_id = $user->location_id ?? null; + $logaction->logaction('requested'); + + $asset->request(); + $asset->increment('requests_counter', 1); + try { + $settings->notify(new RequestAssetNotification($data)); + } catch (\Exception $e) { + Log::warning($e); + } + + return true; + } +} \ No newline at end of file diff --git a/app/Exceptions/AssetNotRequestable.php b/app/Exceptions/AssetNotRequestable.php new file mode 100644 index 000000000000..4067dda38541 --- /dev/null +++ b/app/Exceptions/AssetNotRequestable.php @@ -0,0 +1,9 @@ +user()); + return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.requests.success'))); + } catch (AssetNotRequestable $e) { + return response()->json(Helper::formatStandardApiResponse('error', 'Asset is not requestable')); + } catch (AuthorizationException $e) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.insufficient_permissions'))); + } catch (Exception $e) { + report($e); + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong'))); + } + } + + public function destroy(Asset $asset): JsonResponse + { + try { + CancelCheckoutRequestAction::run($asset, auth()->user()); + return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.requests.canceled'))); + } catch (AuthorizationException $e) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.insufficient_permissions'))); + } catch (Exception $e) { + report($e); + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong'))); + } + } +} diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 12c300e5bd74..bbff6ba4f77e 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -2,18 +2,21 @@ namespace App\Http\Controllers; +use App\Actions\CheckoutRequests\CancelCheckoutRequestAction; +use App\Actions\CheckoutRequests\CreateCheckoutRequestAction; +use App\Exceptions\AssetNotRequestable; use App\Models\Actionlog; use App\Models\Asset; use App\Models\AssetModel; -use App\Models\Company; use App\Models\Setting; use App\Models\User; use App\Notifications\RequestAssetCancelation; use App\Notifications\RequestAssetNotification; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; use Illuminate\Http\RedirectResponse; use \Illuminate\Contracts\View\View; -use Log; +use Exception; /** * This controller handles all actions related to the ability for users @@ -81,7 +84,7 @@ public function getRequestableIndex() : View return view('account/requestable-assets', compact('assets', 'models')); } - public function getRequestItem(Request $request, $itemType, $itemId = null, $cancel_by_admin = false, $requestingUser = null) : RedirectResponse + public function getRequestItem(Request $request, $itemType, $itemId = null, $cancel_by_admin = false, $requestingUser = null): RedirectResponse { $item = null; $fullItemType = 'App\\Models\\'.studly_case($itemType); @@ -144,63 +147,33 @@ public function getRequestItem(Request $request, $itemType, $itemId = null, $can * Process a specific requested asset * @param null $assetId */ - public function getRequestAsset($assetId = null) : RedirectResponse + public function store(Asset $asset): RedirectResponse { - $user = auth()->user(); - - // Check if the asset exists and is requestable - if (is_null($asset = Asset::RequestableAssets()->find($assetId))) { - return redirect()->route('requestable-assets') - ->with('error', trans('admin/hardware/message.does_not_exist_or_not_requestable')); - } - if (! Company::isCurrentUserHasAccess($asset)) { - return redirect()->route('requestable-assets') - ->with('error', trans('general.insufficient_permissions')); - } - - $data['item'] = $asset; - $data['target'] = auth()->user(); - $data['item_quantity'] = 1; - $settings = Setting::getSettings(); - - $logaction = new Actionlog(); - $logaction->item_id = $data['asset_id'] = $asset->id; - $logaction->item_type = $data['item_type'] = Asset::class; - $logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s'); - - if ($user->location_id) { - $logaction->location_id = $user->location_id; - } - $logaction->target_id = $data['user_id'] = auth()->id(); - $logaction->target_type = User::class; - - // If it's already requested, cancel the request. - if ($asset->isRequestedBy(auth()->user())) { - $asset->cancelRequest(); - $asset->decrement('requests_counter', 1); - - $logaction->logaction('request canceled'); - try { - $settings->notify(new RequestAssetCancelation($data)); - } catch (\Exception $e) { - Log::warning($e); - } - return redirect()->route('requestable-assets') - ->with('success')->with('success', trans('admin/hardware/message.requests.canceled')); + try { + CreateCheckoutRequestAction::run($asset, auth()->user()); + return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success')); + } catch (AssetNotRequestable $e) { + return redirect()->back()->with('error', 'Asset is not requestable'); + } catch (AuthorizationException $e) { + return redirect()->back()->with('error', trans('admin/hardware/message.requests.error')); + } catch (Exception $e) { + report($e); + return redirect()->back()->with('error', trans('general.something_went_wrong')); } + } - $logaction->logaction('requested'); - $asset->request(); - $asset->increment('requests_counter', 1); + public function destroy(Asset $asset): RedirectResponse + { try { - $settings->notify(new RequestAssetNotification($data)); - } catch (\Exception $e) { - Log::warning($e); + CancelCheckoutRequestAction::run($asset, auth()->user()); + return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.canceled')); + } catch (Exception $e) { + report($e); + return redirect()->back()->with('error', trans('general.something_went_wrong')); } - - return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success')); } + public function getRequestedAssets() : View { return view('account/requested'); diff --git a/app/Models/Asset.php b/app/Models/Asset.php index ce8b870eb2e0..5bf49186f0c6 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -1433,7 +1433,7 @@ public function scopeDeployed($query) * @return \Illuminate\Database\Query\Builder Modified query builder */ - public function scopeRequestableAssets($query) + public function scopeRequestableAssets($query): Builder { $table = $query->getModel()->getTable(); diff --git a/database/factories/AssetFactory.php b/database/factories/AssetFactory.php index 4d6d20651c8d..00105205c190 100644 --- a/database/factories/AssetFactory.php +++ b/database/factories/AssetFactory.php @@ -347,12 +347,22 @@ public function deleted() public function requestable() { - return $this->state(['requestable' => true]); + $id = Statuslabel::factory()->create([ + 'archived' => false, + 'deployable' => true, + 'pending' => true, + ])->id; + return $this->state(['status_id' => $id, 'requestable' => true]); } public function nonrequestable() { - return $this->state(['requestable' => false]); + $id = Statuslabel::factory()->create([ + 'archived' => true, + 'deployable' => false, + 'pending' => false, + ])->id; + return $this->state(['status_id' => $id, 'requestable' => false]); } public function noPurchaseOrEolDate() diff --git a/resources/views/partials/bootstrap-table.blade.php b/resources/views/partials/bootstrap-table.blade.php index f24552d75366..5a1b8f5c2215 100644 --- a/resources/views/partials/bootstrap-table.blade.php +++ b/resources/views/partials/bootstrap-table.blade.php @@ -480,7 +480,7 @@ function assetRequestActionsFormatter (row, value) { if (value.assigned_to_self == true){ return ''; } else if (value.available_actions.cancel == true) { - return '
@csrf
'; + return '
@csrf
'; } else if (value.available_actions.request == true) { return '
@csrf
'; } diff --git a/routes/api.php b/routes/api.php index e183cd12282e..a80522873216 100644 --- a/routes/api.php +++ b/routes/api.php @@ -40,6 +40,9 @@ ] )->name('api.assets.requested'); + Route::post('request/{asset}', [Api\CheckoutRequest::class, 'store'])->name('api.assets.requests.store'); + Route::post('request/{asset}/cancel', [Api\CheckoutRequest::class, 'destroy'])->name('api.assets.requests.destroy'); + Route::get('requestable/hardware', [ Api\AssetsController::class, diff --git a/routes/web.php b/routes/web.php index 9d9d4ab21f14..473ac9a2b458 100644 --- a/routes/web.php +++ b/routes/web.php @@ -297,19 +297,16 @@ Route::get('requested', [ViewAssetsController::class, 'getRequestedAssets'])->name('account.requested'); // Profile - Route::get( - 'requestable-assets', - [ViewAssetsController::class, 'getRequestableIndex'] - )->name('requestable-assets'); - Route::post( - 'request-asset/{assetId}', - [ViewAssetsController::class, 'getRequestAsset'] - )->name('account/request-asset'); + Route::get('requestable-assets', [ViewAssetsController::class, 'getRequestableIndex'])->name('requestable-assets'); - Route::post( - 'request/{itemType}/{itemId}/{cancel_by_admin?}/{requestingUser?}', - [ViewAssetsController::class, 'getRequestItem'] - )->name('account/request-item'); + Route::post('request-asset/{asset}', [ViewAssetsController::class, 'store']) + ->name('account.request-asset'); + + Route::post('request-asset/{asset}/cancel', [ViewAssetsController::class, 'destroy']) + ->name('account.request-asset.cancel'); + + Route::post('request/{itemType}/{itemId}/{cancel_by_admin?}/{requestingUser?}', [ViewAssetsController::class, 'getRequestItem']) + ->name('account/request-item'); // Account Dashboard Route::get('/', [ViewAssetsController::class, 'getIndex'])->name('account'); diff --git a/tests/Feature/Checkouts/Api/AssetCheckoutTest.php b/tests/Feature/Checkouts/Api/AssetCheckoutTest.php index ded38896426f..efbe7452314f 100644 --- a/tests/Feature/Checkouts/Api/AssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Api/AssetCheckoutTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Checkouts\Api; +use Illuminate\Support\Facades\Mail; +use Notification; use PHPUnit\Framework\Attributes\DataProvider; use App\Events\CheckoutableCheckedOut; use App\Models\Asset; @@ -21,6 +23,22 @@ protected function setUp(): void Event::fake([CheckoutableCheckedOut::class]); } + public function testCheckoutRequest() + { + Notification::fake(); + $requestable = Asset::factory()->requestable()->create(); + $nonRequestable = Asset::factory()->nonrequestable()->create(); + + $this->actingAsForApi(User::factory()->create()) + ->post(route('api.assets.requests.store', $requestable->id)) + ->assertStatusMessageIs('success'); + + $this->actingAsForApi(User::factory()->create()) + ->post(route('api.assets.requests.store', $nonRequestable->id)) + ->assertStatusMessageIs('error'); + + } + public function testCheckingOutAssetRequiresCorrectPermission() { $this->actingAsForApi(User::factory()->create())