From b2fda13ac31092d18c90f7696c1adff3b78b3337 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 14 Apr 2026 11:58:54 -0700 Subject: [PATCH 1/4] adds option for only deleted users and company id for eula purging --- app/Console/Commands/PurgeEulaPDFs.php | 33 ++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/app/Console/Commands/PurgeEulaPDFs.php b/app/Console/Commands/PurgeEulaPDFs.php index ab2edad79a..c34a849520 100644 --- a/app/Console/Commands/PurgeEulaPDFs.php +++ b/app/Console/Commands/PurgeEulaPDFs.php @@ -18,7 +18,9 @@ class PurgeEulaPDFs extends Command {--older-than-days= : The number of days we should delete before } {--force : Skip the interactive yes/no prompt for confirmation} {--dryrun : Show the records that would be deleted but don\'t update the database or delete files from disk} - {--with-output : Display the results in a table in your console}'; + {--with-output : Display the results in a table in your console} + {--company-id= : Only purge acceptances for users in this company} + {--only-deleted-users : Only purge acceptances for deleted users, including soft-deleted or missing users}'; /** * The console command description. @@ -55,7 +57,34 @@ class PurgeEulaPDFs extends Command $this->info('This script is being run with the --dryrun option. No files or records will be deleted.'); } - $acceptances = CheckoutAcceptance::HasFiles()->where('updated_at', '<', $interval_date)->with('assignedTo')->get(); + $companyId = $this->option('company-id'); + $query = CheckoutAcceptance::HasFiles()->where('updated_at', '<', $interval_date) + ->with([ + 'assignedTo' => function ($query) { + $query->withTrashed(); + }, + ]); + + if ($this->option('only-deleted-users')) { + $query->where(function ($query) use ($companyId) { + $query->whereHas('assignedTo', function ($q) use ($companyId) { + $q->withTrashed()->whereNotNull('deleted_at'); + + if ($companyId) { + $q->where('company_id', $companyId); + } + }); + + $query->orWhereDoesntHave('assignedTo'); + }); + } else { + if ($companyId) { + $query->whereHas('assignedTo', function ($query) use ($companyId) { + $query->withTrashed()->where('company_id', $companyId); + }); + } + } + $acceptances = $query->get(); if (! $this->option('force')) { if ($this->confirm("\n****************************************************\nTHIS WILL DELETE ALL OF THE SIGNATURES AND EULA PDF FILES SINCE $interval_date. \nThere is NO undo! \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) { From 9f69eacf71bbd12dd3542d811cd138905b8d6d1b Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 14 Apr 2026 16:11:32 -0700 Subject: [PATCH 2/4] tweak to acceptance factory, adds purge eula command tests --- .../factories/CheckoutAcceptanceFactory.php | 2 +- .../Console/Commands/PurgeEulaPDFTest.php | 236 ++++++++++++++++++ 2 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Console/Commands/PurgeEulaPDFTest.php diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index 4ff49cdd83..6f984dd802 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -103,7 +103,7 @@ class CheckoutAcceptanceFactory extends Factory $acceptance->checkoutable->assetlog()->create([ 'action_type' => 'checkout', 'target_id' => $acceptance->assigned_to_id, - 'target_type' => get_class($acceptance->assignedTo), + 'target_type' => User::class, // BEFORE: get_class($acceptance->assignedTo). a Checkout acceptance is only generated for Users, this avoids a null for testing soft-deleted users. 'item_id' => $acceptance->checkoutable_id, 'item_type' => $acceptance->checkoutable_type, ]); diff --git a/tests/Feature/Console/Commands/PurgeEulaPDFTest.php b/tests/Feature/Console/Commands/PurgeEulaPDFTest.php new file mode 100644 index 0000000000..ab741ab861 --- /dev/null +++ b/tests/Feature/Console/Commands/PurgeEulaPDFTest.php @@ -0,0 +1,236 @@ +subDays(30); + + $company = Company::factory()->create(); + $asset = Asset::factory()->create(); + $otherAsset = Asset::factory()->create(); + $softDeletedUser = User::factory()->create([ + 'company_id' => $company->id, + ]); + $softDeletedUser->delete(); + + $activeUser = User::factory()->create([ + 'company_id' => $company->id, + ]); + + $acceptanceToPurge = CheckoutAcceptance::factory()->create([ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $asset->id, + 'assigned_to_id' => $softDeletedUser->id, + 'signature_filename' => 'signature-to-purge.png', + 'stored_eula_file' => 'eula-to-purge.pdf', + 'updated_at' => $intervalDate->copy()->subDay(), + ]); + + $acceptanceToKeep = CheckoutAcceptance::factory()->create([ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $otherAsset->id, + 'assigned_to_id' => $activeUser->id, + 'signature_filename' => 'signature-to-keep.png', + 'stored_eula_file' => 'eula-to-keep.pdf', + 'updated_at' => $intervalDate->copy()->subDay(), + ]); + + $this->artisan('snipeit:purge-eula-pdfs', [ + '--older-than-days' => 0, + '--only-deleted-users' => true, + '--force' => true, + ])->assertExitCode(0); + + $this->assertSoftDeleted('checkout_acceptances', [ + 'id' => $acceptanceToPurge->id, + ]); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'id' => $acceptanceToKeep->id, + ]); + } + + public function test_it_only_purges_records_for_the_given_company(): void + { + $intervalDate = now()->subDays(30); + + $targetCompany = Company::factory()->create(); + $otherCompany = Company::factory()->create(); + $targetAsset = Asset::factory()->create(); + $otherAsset = Asset::factory()->create(); + + $userInTargetCompany = User::factory()->create([ + 'company_id' => $targetCompany->id, + ]); + $userInOtherCompany = User::factory()->create([ + 'company_id' => $otherCompany->id, + ]); + + $targetAcceptance = CheckoutAcceptance::factory()->create([ + 'checkoutable_type' => \App\Models\Asset::class, + 'checkoutable_id' => $targetAsset->id, + 'assigned_to_id' => $userInTargetCompany->id, + 'signature_filename' => 'target-signature.png', + 'stored_eula_file' => 'target-eula.pdf', + 'updated_at' => $intervalDate->copy()->subDay(), + ]); + + $otherAcceptance = CheckoutAcceptance::factory()->create([ + 'checkoutable_type' => \App\Models\Asset::class, + 'checkoutable_id' => $otherAsset->id, + 'assigned_to_id' => $userInOtherCompany->id, + 'signature_filename' => 'other-signature.png', + 'stored_eula_file' => 'other-eula.pdf', + 'updated_at' => $intervalDate->copy()->subDay(), + ]); + + Storage::fake('local'); + + Storage::put('private_uploads/signatures/target-signature.png', 'fake'); + Storage::put('private_uploads/eula-pdfs/target-eula.pdf', 'fake'); + + Storage::put('private_uploads/signatures/other-signature.png', 'fake'); + Storage::put('private_uploads/eula-pdfs/other-eula.pdf', 'fake'); + + $this->artisan('snipeit:purge-eula-pdfs', [ + '--older-than-days' => 0, + '--company-id' => $targetCompany->id, + '--force' => true, + ])->assertExitCode(0); + + $this->assertSoftDeleted('checkout_acceptances', [ + 'id' => $targetAcceptance->id, + ]); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'id' => $otherAcceptance->id, + ]); + } + + public function test_it_only_purges_soft_deleted_users_for_the_given_company(): void + { + $intervalDate = now()->subDays(30); + + $targetCompany = Company::factory()->create(); + $otherCompany = Company::factory()->create(); + + $matchingAsset = Asset::factory()->create(); + $wrongCompanyAsset = Asset::factory()->create(); + $activeUserAsset = Asset::factory()->create(); + + $matchingUser = User::factory()->create([ + 'company_id' => $targetCompany->id, + ]); + $matchingUser->delete(); + + $wrongCompanyUser = User::factory()->create([ + 'company_id' => $otherCompany->id, + ]); + $wrongCompanyUser->delete(); + + $activeUserInTargetCompany = User::factory()->create([ + 'company_id' => $targetCompany->id, + ]); + Storage::fake('local'); + Storage::put('private_uploads/signatures/matching-signature.png', 'fake'); + Storage::put('private_uploads/eula-pdfs/matching-eula.pdf', 'fake'); + + Storage::put('private_uploads/signatures/wrong-company-signature.png', 'fake'); + Storage::put('private_uploads/eula-pdfs/wrong-company-eula.pdf', 'fake'); + + Storage::put('private_uploads/signatures/active-user-signature.png', 'fake'); + Storage::put('private_uploads/eula-pdfs/active-user-eula.pdf', 'fake'); + + $matchingAcceptance = CheckoutAcceptance::factory()->create([ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $matchingAsset->id, + 'assigned_to_id' => $matchingUser->id, + 'signature_filename' => 'matching-signature.png', + 'stored_eula_file' => 'matching-eula.pdf', + 'updated_at' => $intervalDate->copy()->subDay(), + ]); + + $wrongCompanyAcceptance = CheckoutAcceptance::factory()->create([ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $wrongCompanyAsset->id, + 'assigned_to_id' => $wrongCompanyUser->id, + 'signature_filename' => 'wrong-company-signature.png', + 'stored_eula_file' => 'wrong-company-eula.pdf', + 'updated_at' => $intervalDate->copy()->subDay(), + ]); + + $activeUserAcceptance = CheckoutAcceptance::factory()->create([ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $activeUserAsset->id, + 'assigned_to_id' => $activeUserInTargetCompany->id, + 'signature_filename' => 'active-user-signature.png', + 'stored_eula_file' => 'active-user-eula.pdf', + 'updated_at' => $intervalDate->copy()->subDay(), + ]); + + + $this->artisan('snipeit:purge-eula-pdfs', [ + '--older-than-days' => 0, + '--company-id' => $targetCompany->id, + '--only-deleted-users' => true, + '--force' => true, + ])->assertExitCode(0); + + $this->assertSoftDeleted('checkout_acceptances', [ + 'id' => $matchingAcceptance->id, + ]); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'id' => $wrongCompanyAcceptance->id, + ]); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'id' => $activeUserAcceptance->id, + ]); + } + + public function test_it_does_not_purge_recent_acceptances_even_for_soft_deleted_users(): void + { + $company = Company::factory()->create(); + + $softDeletedUser = User::factory()->create([ + 'company_id' => $company->id, + ]); + $softDeletedUser->delete(); + + $recentAsset = Asset::factory()->create(); + + Storage::fake('local'); + Storage::put('private_uploads/signatures/recent-signature.png', 'fake'); + Storage::put('private_uploads/eula-pdfs/recent-eula.pdf', 'fake'); + + $recentAcceptance = CheckoutAcceptance::factory()->create([ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $recentAsset->id, + 'assigned_to_id' => $softDeletedUser->id, + 'signature_filename' => 'recent-signature.png', + 'stored_eula_file' => 'recent-eula.pdf', + 'updated_at' => now()->subDay(), // <-- stays recent + ]); + + $this->artisan('snipeit:purge-eula-pdfs', [ + '--older-than-days' => 0, + '--only-deleted-users' => true, + '--force' => true, + ])->assertExitCode(0); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'id' => $recentAcceptance->id, + ]); + } +} \ No newline at end of file From 2eeb1f588a0ab71fc971ecaf178f0d146dd3fb3e Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 14 Apr 2026 16:13:18 -0700 Subject: [PATCH 3/4] rename tests --- tests/Feature/Console/Commands/PurgeEulaPDFTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Feature/Console/Commands/PurgeEulaPDFTest.php b/tests/Feature/Console/Commands/PurgeEulaPDFTest.php index ab741ab861..b49cc00c27 100644 --- a/tests/Feature/Console/Commands/PurgeEulaPDFTest.php +++ b/tests/Feature/Console/Commands/PurgeEulaPDFTest.php @@ -11,7 +11,7 @@ use Storage; class PurgeEulaPDFTest extends TestCase { - public function test_it_purges_acceptances_for_deleted_users(): void + public function test_only_purges_acceptances_for_deleted_users(): void { $intervalDate = now()->subDays(30); @@ -60,7 +60,7 @@ class PurgeEulaPDFTest extends TestCase ]); } - public function test_it_only_purges_records_for_the_given_company(): void + public function test_only_purges_records_for_the_given_company(): void { $intervalDate = now()->subDays(30); @@ -117,7 +117,7 @@ class PurgeEulaPDFTest extends TestCase ]); } - public function test_it_only_purges_soft_deleted_users_for_the_given_company(): void + public function test_only_purges_soft_deleted_users_for_the_given_company(): void { $intervalDate = now()->subDays(30); @@ -199,7 +199,7 @@ class PurgeEulaPDFTest extends TestCase ]); } - public function test_it_does_not_purge_recent_acceptances_even_for_soft_deleted_users(): void + public function test_does_not_purge_recent_acceptances_even_for_soft_deleted_users(): void { $company = Company::factory()->create(); @@ -207,7 +207,7 @@ class PurgeEulaPDFTest extends TestCase 'company_id' => $company->id, ]); $softDeletedUser->delete(); - + $recentAsset = Asset::factory()->create(); Storage::fake('local'); From 5c5414c9607b3adeaa607dcd1c50132f85a074f1 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 14 Apr 2026 17:57:06 -0700 Subject: [PATCH 4/4] remove comments, reorder command options --- app/Console/Commands/PurgeEulaPDFs.php | 8 ++++---- database/factories/CheckoutAcceptanceFactory.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Console/Commands/PurgeEulaPDFs.php b/app/Console/Commands/PurgeEulaPDFs.php index c34a849520..b5de61bec1 100644 --- a/app/Console/Commands/PurgeEulaPDFs.php +++ b/app/Console/Commands/PurgeEulaPDFs.php @@ -15,12 +15,12 @@ class PurgeEulaPDFs extends Command * @var string */ protected $signature = 'snipeit:purge-eula-pdfs - {--older-than-days= : The number of days we should delete before } + {--older-than-days= : The number of days we should delete before } + {--company-id= : Only purge acceptances for users in this company} + {--only-deleted-users : Only purge acceptances for deleted users, including soft-deleted or missing users} {--force : Skip the interactive yes/no prompt for confirmation} {--dryrun : Show the records that would be deleted but don\'t update the database or delete files from disk} - {--with-output : Display the results in a table in your console} - {--company-id= : Only purge acceptances for users in this company} - {--only-deleted-users : Only purge acceptances for deleted users, including soft-deleted or missing users}'; + {--with-output : Display the results in a table in your console}'; /** * The console command description. diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index 6f984dd802..885a18a372 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -103,7 +103,7 @@ class CheckoutAcceptanceFactory extends Factory $acceptance->checkoutable->assetlog()->create([ 'action_type' => 'checkout', 'target_id' => $acceptance->assigned_to_id, - 'target_type' => User::class, // BEFORE: get_class($acceptance->assignedTo). a Checkout acceptance is only generated for Users, this avoids a null for testing soft-deleted users. + 'target_type' => User::class, 'item_id' => $acceptance->checkoutable_id, 'item_type' => $acceptance->checkoutable_type, ]);