diff --git a/app/Console/Commands/CleanOldCheckoutRequests.php b/app/Console/Commands/CleanOldCheckoutRequests.php new file mode 100644 index 0000000000..a96a58a349 --- /dev/null +++ b/app/Console/Commands/CleanOldCheckoutRequests.php @@ -0,0 +1,74 @@ + 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(); + } +} diff --git a/app/Models/Asset.php b/app/Models/Asset.php index a977ec3816..72bef375f6 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -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) { diff --git a/app/Models/AssetModel.php b/app/Models/AssetModel.php index bdb87d4f75..c73e4492ac 100755 --- a/app/Models/AssetModel.php +++ b/app/Models/AssetModel.php @@ -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 diff --git a/app/Models/CheckoutRequest.php b/app/Models/CheckoutRequest.php index d6a85f2972..42512d8fda 100644 --- a/app/Models/CheckoutRequest.php +++ b/app/Models/CheckoutRequest.php @@ -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'; diff --git a/app/Models/User.php b/app/Models/User.php index b673761b3b..6bd44380eb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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() { diff --git a/database/factories/CheckoutRequestFactory.php b/database/factories/CheckoutRequestFactory.php new file mode 100644 index 0000000000..5363b5e281 --- /dev/null +++ b/database/factories/CheckoutRequestFactory.php @@ -0,0 +1,44 @@ + 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, + ]; + }); + } +} diff --git a/tests/Feature/Console/CleanOldCheckoutRequestsTest.php b/tests/Feature/Console/CleanOldCheckoutRequestsTest.php new file mode 100644 index 0000000000..b1bdf8e603 --- /dev/null +++ b/tests/Feature/Console/CleanOldCheckoutRequestsTest.php @@ -0,0 +1,82 @@ +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]); + } +} diff --git a/tests/Feature/Requests/Ui/AssetRequestIndexTest.php b/tests/Feature/Requests/Ui/AssetRequestIndexTest.php new file mode 100644 index 0000000000..474b1c5c26 --- /dev/null +++ b/tests/Feature/Requests/Ui/AssetRequestIndexTest.php @@ -0,0 +1,28 @@ +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); + } +} diff --git a/tests/Unit/Models/CheckoutRequestTest.php b/tests/Unit/Models/CheckoutRequestTest.php new file mode 100644 index 0000000000..7eceafadfd --- /dev/null +++ b/tests/Unit/Models/CheckoutRequestTest.php @@ -0,0 +1,75 @@ +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]); + } +}