mirror of
https://github.com/snipe/snipe-it.git
synced 2025-03-05 20:52:15 -08:00
Merge 6201e475cb
into 64f49afce1
This commit is contained in:
commit
c350f3efd5
48
app/Actions/CheckoutRequests/CancelCheckoutRequestAction.php
Normal file
48
app/Actions/CheckoutRequests/CancelCheckoutRequestAction.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
54
app/Actions/CheckoutRequests/CreateCheckoutRequestAction.php
Normal file
54
app/Actions/CheckoutRequests/CreateCheckoutRequestAction.php
Normal 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;
|
||||
}
|
||||
}
|
9
app/Exceptions/AssetNotRequestable.php
Normal file
9
app/Exceptions/AssetNotRequestable.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class AssetNotRequestable extends Exception
|
||||
{
|
||||
}
|
10
app/Exceptions/UserDoestExistException.php
Normal file
10
app/Exceptions/UserDoestExistException.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UserDoestExistException extends Exception
|
||||
{
|
||||
|
||||
}
|
44
app/Http/Controllers/Api/CheckoutRequest.php
Normal file
44
app/Http/Controllers/Api/CheckoutRequest.php
Normal 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')));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>';
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue