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;
|
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\Actionlog;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\AssetModel;
|
use App\Models\AssetModel;
|
||||||
use App\Models\Company;
|
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\RequestAssetCancelation;
|
use App\Notifications\RequestAssetCancelation;
|
||||||
use App\Notifications\RequestAssetNotification;
|
use App\Notifications\RequestAssetNotification;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use \Illuminate\Contracts\View\View;
|
use \Illuminate\Contracts\View\View;
|
||||||
use Log;
|
use Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This controller handles all actions related to the ability for users
|
* This controller handles all actions related to the ability for users
|
||||||
|
@ -144,62 +147,32 @@ class ViewAssetsController extends Controller
|
||||||
* Process a specific requested asset
|
* Process a specific requested asset
|
||||||
* @param null $assetId
|
* @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 {
|
try {
|
||||||
$settings->notify(new RequestAssetCancelation($data));
|
CreateCheckoutRequestAction::run($asset, auth()->user());
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
|
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'));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
public function getRequestedAssets() : View
|
||||||
{
|
{
|
||||||
|
|
|
@ -1456,7 +1456,7 @@ class Asset extends Depreciable
|
||||||
* @return \Illuminate\Database\Query\Builder Modified query builder
|
* @return \Illuminate\Database\Query\Builder Modified query builder
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function scopeRequestableAssets($query)
|
public function scopeRequestableAssets($query): Builder
|
||||||
{
|
{
|
||||||
$table = $query->getModel()->getTable();
|
$table = $query->getModel()->getTable();
|
||||||
|
|
||||||
|
|
|
@ -347,12 +347,22 @@ class AssetFactory extends Factory
|
||||||
|
|
||||||
public function requestable()
|
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()
|
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()
|
public function noPurchaseOrEolDate()
|
||||||
|
|
|
@ -513,7 +513,7 @@
|
||||||
if (value.assigned_to_self == true){
|
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>';
|
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) {
|
} 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) {
|
} 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>';
|
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');
|
)->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',
|
Route::get('requestable/hardware',
|
||||||
[
|
[
|
||||||
Api\AssetsController::class,
|
Api\AssetsController::class,
|
||||||
|
|
|
@ -427,10 +427,14 @@ Route::group(['prefix' => 'account', 'middleware' => ['auth']], function () {
|
||||||
[ViewAssetsController::class, 'getRequestAsset']
|
[ViewAssetsController::class, 'getRequestAsset']
|
||||||
)->name('account/request-asset');
|
)->name('account/request-asset');
|
||||||
|
|
||||||
Route::post(
|
Route::post('request-asset/{asset}', [ViewAssetsController::class, 'store'])
|
||||||
'request/{itemType}/{itemId}/{cancel_by_admin?}/{requestingUser?}',
|
->name('account.request-asset');
|
||||||
[ViewAssetsController::class, 'getRequestItem']
|
|
||||||
)->name('account/request-item');
|
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
|
// Account Dashboard
|
||||||
Route::get('/', [ViewAssetsController::class, 'getIndex'])
|
Route::get('/', [ViewAssetsController::class, 'getIndex'])
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace Tests\Feature\Checkouts\Api;
|
namespace Tests\Feature\Checkouts\Api;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Notification;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use App\Events\CheckoutableCheckedOut;
|
use App\Events\CheckoutableCheckedOut;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
|
@ -21,6 +23,22 @@ class AssetCheckoutTest extends TestCase
|
||||||
Event::fake([CheckoutableCheckedOut::class]);
|
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()
|
public function testCheckingOutAssetRequiresCorrectPermission()
|
||||||
{
|
{
|
||||||
$this->actingAsForApi(User::factory()->create())
|
$this->actingAsForApi(User::factory()->create())
|
||||||
|
|
Loading…
Reference in a new issue