This commit is contained in:
Spencer Long 2025-03-06 00:32:28 +02:00 committed by GitHub
commit c350f3efd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 236 additions and 63 deletions

View file

@ -0,0 +1,48 @@
<?php
namespace App\Actions\CheckoutRequests;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\RequestAssetCancelation;
use Illuminate\Auth\Access\AuthorizationException;
class CancelCheckoutRequestAction
{
public static function run(Asset $asset, User $user)
{
if (!Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException();
}
$asset->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;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Actions\CheckoutRequests;
use App\Exceptions\AssetNotRequestable;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\RequestAssetNotification;
use Illuminate\Auth\Access\AuthorizationException;
use Log;
class CreateCheckoutRequestAction
{
/**
* @throws AssetNotRequestable
* @throws AuthorizationException
*/
public static function run(Asset $asset, User $user): string
{
if (is_null(Asset::RequestableAssets()->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;
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use Exception;
class AssetNotRequestable extends Exception
{
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class UserDoestExistException extends Exception
{
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\CheckoutRequests\CancelCheckoutRequestAction;
use App\Actions\CheckoutRequests\CreateCheckoutRequestAction;
use App\Exceptions\AssetNotRequestable;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Exception;
class CheckoutRequest extends Controller
{
public function store(Asset $asset): JsonResponse
{
try {
CreateCheckoutRequestAction::run($asset, auth()->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')));
}
}
}

View file

@ -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 @@ class ViewAssetsController extends Controller
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 @@ class ViewAssetsController extends Controller
* 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'));
}
$logaction->logaction('requested');
$asset->request();
$asset->increment('requests_counter', 1);
try {
$settings->notify(new RequestAssetNotification($data));
} catch (\Exception $e) {
Log::warning($e);
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'));
}
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
}
public function destroy(Asset $asset): RedirectResponse
{
try {
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'));
}
}
public function getRequestedAssets() : View
{
return view('account/requested');

View file

@ -1456,7 +1456,7 @@ class Asset extends Depreciable
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeRequestableAssets($query)
public function scopeRequestableAssets($query): Builder
{
$table = $query->getModel()->getTable();

View file

@ -347,12 +347,22 @@ class AssetFactory extends Factory
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()

View file

@ -513,7 +513,7 @@
if (value.assigned_to_self == true){
return '<button class="btn btn-danger btn-sm disabled" data-tooltip="true" title="Cancel this item request">{{ trans('button.cancel') }}</button>';
} else if (value.available_actions.cancel == true) {
return '<form action="{{ config('app.url') }}/account/request-asset/'+ value.id + '" method="POST">@csrf<button class="btn btn-danger btn-sm" data-tooltip="true" title="Cancel this item request">{{ trans('button.cancel') }}</button></form>';
return '<form action="{{ config('app.url') }}/account/request-asset/' + value.id + '/cancel" method="POST">@csrf<button class="btn btn-danger btn-sm" data-tooltip="true" title="Cancel this item request">{{ trans('button.cancel') }}</button></form>';
} else if (value.available_actions.request == true) {
return '<form action="{{ config('app.url') }}/account/request-asset/'+ value.id + '" method="POST">@csrf<button class="btn btn-primary btn-sm" data-tooltip="true" title="{{ trans('general.request_item') }}">{{ trans('button.request') }}</button></form>';
}

View file

@ -40,6 +40,9 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->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,

View file

@ -427,10 +427,14 @@ Route::group(['prefix' => 'account', 'middleware' => ['auth']], function () {
[ViewAssetsController::class, 'getRequestAsset']
)->name('account/request-asset');
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'])

View file

@ -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 @@ class AssetCheckoutTest extends TestCase
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())