Merge pull request #17417 from marcusmoore/snipe-it-17073-asset-requests-are-not-deleted-when-asset-is-deleted

Fixed #17073 - delete old checkout requests
This commit is contained in:
snipe
2025-08-07 18:32:13 +01:00
committed by GitHub
9 changed files with 336 additions and 1 deletions
@@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Models\CheckoutRequest;
use Illuminate\Console\Command;
class CleanOldCheckoutRequests extends Command
{
private int $deletions = 0;
private int $skips = 0;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:clean-old-checkout-requests';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes checkout requests that reference deleted assets or users.';
/**
* Execute the console command.
*/
public function handle()
{
$requests = CheckoutRequest::with([
'user' => function ($query) {
$query->withTrashed();
},
'requestedItem' => function ($query) {
$query->withTrashed();
},
])->get();
$this->info("Processing {$requests->count()} checkout requests");
$this->withProgressBar($requests, function ($request) {
if ($this->shouldForceDelete($request)) {
$request->forceDelete();
$this->deletions++;
return;
}
if ($this->shouldSoftDelete($request)) {
$request->delete();
$this->deletions++;
return;
}
$this->skips++;
});
$this->info("Final deletion count: $this->deletions, and skip count: $this->skips");
return 0;
}
private function shouldForceDelete(CheckoutRequest $request)
{
// check if the requestable or user relationship is null
return !$request->requestable || !$request->user;
}
private function shouldSoftDelete(CheckoutRequest $request)
{
return $request->requestable->trashed() || $request->user->trashed();
}
}
+11
View File
@@ -206,6 +206,17 @@ class Asset extends Depreciable
'model.manufacturer' => ['name'],
];
protected static function booted(): void
{
static::forceDeleted(function (Asset $asset) {
$asset->requests()->forceDelete();
});
static::softDeleted(function (Asset $asset) {
$asset->requests()->delete();
});
}
// To properly set the expected checkin as Y-m-d
public function setExpectedCheckinAttribute($value)
{
+9 -1
View File
@@ -98,8 +98,16 @@ class AssetModel extends SnipeModel
'manufacturer' => ['name'],
];
protected static function booted(): void
{
static::forceDeleted(function (AssetModel $assetModel) {
$assetModel->requests()->forceDelete();
});
static::softDeleted(function (AssetModel $assetModel) {
$assetModel->requests()->delete();
});
}
/**
* Establishes the model -> assets relationship
+2
View File
@@ -2,11 +2,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class CheckoutRequest extends Model
{
use HasFactory;
use SoftDeletes;
protected $fillable = ['user_id'];
protected $table = 'checkout_requests';
+11
View File
@@ -183,6 +183,17 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
);
}
protected static function booted(): void
{
static::forceDeleted(function (User $user) {
CheckoutRequest::where(['user_id' => $user->id])->forceDelete();
});
static::softDeleted(function (User $user) {
CheckoutRequest::where(['user_id' => $user->id])->delete();
});
}
public function isAvatarExternal()
{
@@ -0,0 +1,44 @@
<?php
namespace Database\Factories;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CheckoutRequest;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class CheckoutRequestFactory extends Factory
{
protected $model = CheckoutRequest::class;
public function definition(): array
{
return [
'requestable_id' => Asset::factory(),
'requestable_type' => Asset::class,
'quantity' => 1,
'user_id' => User::factory(),
];
}
public function forAsset()
{
return $this->state(function (array $attributes) {
return [
'requestable_id' => Asset::factory(),
'requestable_type' => Asset::class,
];
});
}
public function forAssetModel()
{
return $this->state(function (array $attributes) {
return [
'requestable_id' => AssetModel::factory(),
'requestable_type' => AssetModel::class,
];
});
}
}
@@ -0,0 +1,82 @@
<?php
namespace Tests\Feature\Console;
use App\Models\CheckoutRequest;
use Illuminate\Database\Eloquent\Model;
use Tests\TestCase;
class CleanOldCheckoutRequestsTest extends TestCase
{
private CheckoutRequest $validRequest;
protected function setUp(): void
{
parent::setUp();
$this->validRequest = CheckoutRequest::factory()->forAsset()->create();
}
public function test_clean_old_checkout_requests_command_for_soft_deleted_asset()
{
$requestForSoftDeletedAsset = CheckoutRequest::factory()->forAsset()->create();
Model::withoutEvents(fn() => $requestForSoftDeletedAsset->requestedItem->delete());
$this->artisan('snipeit:clean-old-checkout-requests')->assertExitCode(0);
$this->assertNotSoftDeleted($this->validRequest);
$this->assertSoftDeleted($requestForSoftDeletedAsset->fresh());
}
public function test_clean_old_checkout_requests_command_for_missing_asset()
{
$requestForMissingAsset = CheckoutRequest::factory()->forAsset()->create(['requestable_id' => 99999999]);
$this->artisan('snipeit:clean-old-checkout-requests')->assertExitCode(0);
$this->assertNotSoftDeleted($this->validRequest);
$this->assertDatabaseMissing('checkout_requests', ['requestable_id' => $requestForMissingAsset->requestable_id]);
}
public function test_clean_old_checkout_requests_command_for_soft_deleted_model()
{
$requestForSoftDeletedAssetModel = CheckoutRequest::factory()->forAssetModel()->create();
Model::withoutEvents(fn() => $requestForSoftDeletedAssetModel->requestedItem->delete());
$this->artisan('snipeit:clean-old-checkout-requests')->assertExitCode(0);
$this->assertNotSoftDeleted($this->validRequest);
$this->assertSoftDeleted($requestForSoftDeletedAssetModel->fresh());
}
public function test_clean_old_checkout_requests_command_for_missing_model()
{
$requestForMissingModel = CheckoutRequest::factory()->forAssetModel()->create(['requestable_id' => 99999999]);
$this->artisan('snipeit:clean-old-checkout-requests')->assertExitCode(0);
$this->assertNotSoftDeleted($this->validRequest);
$this->assertDatabaseMissing('checkout_requests', ['requestable_id' => $requestForMissingModel->requestable_id]);
}
public function test_clean_old_checkout_requests_command_for_soft_deleted_user()
{
$requestForSoftDeletedUser = CheckoutRequest::factory()->forAsset()->create();
Model::withoutEvents(fn() => $requestForSoftDeletedUser->user->delete());
$this->artisan('snipeit:clean-old-checkout-requests')->assertExitCode(0);
$this->assertNotSoftDeleted($this->validRequest);
$this->assertSoftDeleted($requestForSoftDeletedUser->fresh());
}
public function test_clean_old_checkout_requests_command_for_missing_user()
{
$requestForMissingUser = CheckoutRequest::factory()->forAsset()->create(['user_id' => 99999999]);
$this->artisan('snipeit:clean-old-checkout-requests')->assertExitCode(0);
$this->assertNotSoftDeleted($this->validRequest);
$this->assertDatabaseMissing('checkout_requests', ['user_id' => $requestForMissingUser->user_id]);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Tests\Feature\Requests\Ui;
use App\Models\CheckoutRequest;
use App\Models\User;
use Tests\TestCase;
class AssetRequestIndexTest extends TestCase
{
public function test_requires_permission_to_view_asset_request_index()
{
$this->actingAs(User::factory()->create())
->get(route('assets.requested'))
->assertForbidden();
}
public function test_can_view_request_asset_request_index()
{
$checkoutRequest = CheckoutRequest::factory()->create();
$this->actingAs(User::factory()->viewAssets()->create())
->get(route('assets.requested'))
->assertOk()
->assertViewHas('requestedItems')
->assertSeeText($checkoutRequest->requestedItem->asset_tag);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace Tests\Unit\Models;
use App\Models\CheckoutRequest;
use Tests\TestCase;
class CheckoutRequestTest extends TestCase
{
public function test_checkout_request_soft_deleted_when_requested_asset_soft_deleted()
{
$checkoutRequest = CheckoutRequest::factory()->forAsset()->create();
$requestedAsset = $checkoutRequest->requestedItem;
$requestedAsset->delete();
$this->assertSoftDeleted($checkoutRequest->fresh());
}
public function test_checkout_request_deleted_when_requested_asset_force_deleted()
{
$checkoutRequest = CheckoutRequest::factory()->forAsset()->create();
$requestedAsset = $checkoutRequest->requestedItem;
$requestedAsset->forceDelete();
$this->assertDatabaseMissing('checkout_requests', ['id' => $checkoutRequest->id]);
}
public function test_checkout_request_soft_deleted_when_requested_model_soft_deleted()
{
$checkoutRequest = CheckoutRequest::factory()->forAssetModel()->create();
$requestedAssetModel = $checkoutRequest->requestedItem;
$requestedAssetModel->delete();
$this->assertSoftDeleted($checkoutRequest->fresh());
}
public function test_checkout_request_deleted_when_requested_model_force_deleted()
{
$checkoutRequest = CheckoutRequest::factory()->forAssetModel()->create();
$requestedAsset = $checkoutRequest->requestedItem;
$requestedAsset->forceDelete();
$this->assertDatabaseMissing('checkout_requests', ['id' => $checkoutRequest->id]);
}
public function test_checkout_request_soft_deleted_when_requesting_user_soft_deleted()
{
$checkoutRequest = CheckoutRequest::factory()->forAsset()->create();
$requestingUser = $checkoutRequest->user;
$requestingUser->delete();
$this->assertSoftDeleted($checkoutRequest->fresh());
}
public function test_checkout_request_deleted_when_requesting_user_force_deleted()
{
$checkoutRequest = CheckoutRequest::factory()->forAsset()->create();
$requestingUser = $checkoutRequest->user;
$requestingUser->forceDelete();
$this->assertDatabaseMissing('checkout_requests', ['id' => $checkoutRequest->id]);
}
}