artisan('snipeit:checkin-delete-items') // Step 1: dry run confirm ->expectsConfirmation('Is this a dry run?', $dryRun ? 'yes' : 'no') // Step 2: search() — ask() for the search term, then choice() to pick from results // Using expectsQuestion for both to avoid triggering choices validation ->expectsQuestion($searchLabel, $admin->username) ->expectsQuestion($searchLabel, (string) $admin->id) // Step 3: multisearch for companies — ask() for search term, then multiselectFallback() ->expectsQuestion($companiesLabel, '') ->expectsQuestion($companiesLabel, $companyIds) // Step 4: multiselect for item types ->expectsQuestion($typesLabel, $types) // Step 5: select for delete mode ->expectsQuestion($deleteLabel, $deleteType); // Step 6: notification confirm only shown for checkiable types if ($hasNotifiableTypes) { $cmd->expectsConfirmation('Should we send checkin notifications?', $sendNotifications ? 'yes' : 'no'); } // Steps 7–11 and final confirm $cmd->expectsConfirmation('Should we clear related action logs?', $clearLogs ? 'yes' : 'no'); // Step 8: file deletion prompt — only shown when deleteType !== 'none' if ($deleteType !== 'none') { $cmd->expectsConfirmation('Should we also delete associated image and upload files?', $deleteFiles ? 'yes' : 'no'); } // Step 9: company deletion — only shown when real companies (not just __null__) were selected if (! empty($companyIds)) { $cmd->expectsQuestion('Should the selected companies also be deleted?', $deleteCompanyType); } $cmd->expectsConfirmation('Should we run a backup before proceeding?', $doBackup ? 'yes' : 'no'); // Email report prompt — only shown when admin has an email address if ($admin->email) { $cmd->expectsConfirmation("Send an email report to {$admin->email}?", $sendEmailReport ? 'yes' : 'no'); } if (! $dryRun) { $cmd->expectsConfirmation('Are you sure you want to proceed? This cannot be undone.', $confirm ? 'yes' : 'no'); } return $cmd; } // --------------------------------------------------------------------------- // Assets // --------------------------------------------------------------------------- public function test_assigned_asset_is_checked_in_and_soft_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id]); $asset = Asset::factory()->for($company)->assignedToUser($user)->create(); $this->assertNotNull($asset->assigned_to); $this->runCommand($admin, [$company->id], ['assets']) ->assertExitCode(0); $this->assertSoftDeleted($asset); $this->assertNull($asset->fresh()->assigned_to); } public function test_unassigned_asset_is_soft_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $asset = Asset::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['assets']) ->assertExitCode(0); $this->assertSoftDeleted($asset); } public function test_asset_is_hard_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $asset = Asset::factory()->for($company)->assignedToUser()->create(); $this->runCommand($admin, [$company->id], ['assets'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('assets', ['id' => $asset->id]); } public function test_asset_checkin_event_fired_when_notifications_enabled(): void { Event::fake([CheckoutableCheckedIn::class]); $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $asset = Asset::factory()->for($company)->assignedToUser()->create(); $this->runCommand($admin, [$company->id], ['assets'], sendNotifications: true) ->assertExitCode(0); Event::assertDispatched(CheckoutableCheckedIn::class, fn ($e) => $e->checkoutable->is($asset)); } public function test_asset_checkin_event_not_fired_when_notifications_suppressed(): void { Event::fake([CheckoutableCheckedIn::class]); $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); Asset::factory()->for($company)->assignedToUser()->create(); $this->runCommand($admin, [$company->id], ['assets'], sendNotifications: false) ->assertExitCode(0); Event::assertNotDispatched(CheckoutableCheckedIn::class); } public function test_asset_scoped_to_correct_company(): void { $admin = User::factory()->superuser()->create(); [$companyA, $companyB] = Company::factory()->count(2)->create(); $assetA = Asset::factory()->for($companyA)->create(); $assetB = Asset::factory()->for($companyB)->create(); $this->runCommand($admin, [$companyA->id], ['assets']) ->assertExitCode(0); $this->assertSoftDeleted($assetA); $this->assertNotSoftDeleted($assetB); } public function test_asset_delete_clears_all_checkout_acceptances(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $asset = Asset::factory()->for($company)->create(); CheckoutAcceptance::factory()->create([ 'checkoutable_type' => Asset::class, 'checkoutable_id' => $asset->id, ]); $this->runCommand($admin, [$company->id], ['assets']) ->assertExitCode(0); $this->assertDatabaseMissing('checkout_acceptances', [ 'checkoutable_type' => Asset::class, 'checkoutable_id' => $asset->id, ]); } public function test_asset_checkin_clears_linked_license_seats(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id]); $asset = Asset::factory()->for($company)->assignedToUser($user)->create(); $seat = LicenseSeat::factory()->create(['asset_id' => $asset->id, 'assigned_to' => $user->id]); $this->runCommand($admin, [$company->id], ['assets']) ->assertExitCode(0); $this->assertNull($seat->fresh()->assigned_to); } // --------------------------------------------------------------------------- // Licenses // --------------------------------------------------------------------------- public function test_assigned_license_seat_is_checked_in_and_license_soft_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $license = License::factory()->for($company)->create(); $seat = LicenseSeat::factory()->for($license)->assignedToUser()->create(); $this->assertNotNull($seat->assigned_to); $this->runCommand($admin, [$company->id], ['licenses']) ->assertExitCode(0); $this->assertSoftDeleted($license); $this->assertNull($seat->fresh()->assigned_to); $this->assertNull($seat->fresh()->asset_id); } public function test_license_hard_delete_removes_seats_and_their_checkout_acceptances(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $license = License::factory()->for($company)->create(); $seat = LicenseSeat::factory()->for($license)->assignedToUser()->create(); CheckoutAcceptance::factory()->create([ 'checkoutable_type' => LicenseSeat::class, 'checkoutable_id' => $seat->id, ]); $this->runCommand($admin, [$company->id], ['licenses'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('licenses', ['id' => $license->id]); $this->assertDatabaseMissing('license_seats', ['id' => $seat->id]); $this->assertDatabaseMissing('checkout_acceptances', [ 'checkoutable_type' => LicenseSeat::class, 'checkoutable_id' => $seat->id, ]); } public function test_license_soft_delete_also_soft_deletes_seats(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $license = License::factory()->for($company)->create(); $seat = LicenseSeat::factory()->for($license)->create(); $this->runCommand($admin, [$company->id], ['licenses']) ->assertExitCode(0); $this->assertSoftDeleted($license); $this->assertSoftDeleted($seat); } public function test_license_scoped_to_correct_company(): void { $admin = User::factory()->superuser()->create(); [$companyA, $companyB] = Company::factory()->count(2)->create(); $licenseA = License::factory()->for($companyA)->create(); $licenseB = License::factory()->for($companyB)->create(); $this->runCommand($admin, [$companyA->id], ['licenses']) ->assertExitCode(0); $this->assertSoftDeleted($licenseA); $this->assertNotSoftDeleted($licenseB); } // --------------------------------------------------------------------------- // Accessories // --------------------------------------------------------------------------- public function test_checked_out_accessory_is_checked_in_and_soft_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $accessory = Accessory::factory()->for($company)->checkedOutToUser()->create(); $this->assertEquals(1, $accessory->checkouts->count()); $this->runCommand($admin, [$company->id], ['accessories']) ->assertExitCode(0); $this->assertSoftDeleted($accessory); $this->assertEquals(0, $accessory->fresh()->checkouts->count()); } public function test_accessory_scoped_to_correct_company(): void { $admin = User::factory()->superuser()->create(); [$companyA, $companyB] = Company::factory()->count(2)->create(); $accessoryA = Accessory::factory()->for($companyA)->checkedOutToUser()->create(); $accessoryB = Accessory::factory()->for($companyB)->checkedOutToUser()->create(); $this->runCommand($admin, [$companyA->id], ['accessories']) ->assertExitCode(0); $this->assertSoftDeleted($accessoryA); $this->assertNotSoftDeleted($accessoryB); } // --------------------------------------------------------------------------- // Components // --------------------------------------------------------------------------- public function test_checked_out_component_is_checked_in_and_soft_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $component = Component::factory()->for($company)->checkedOutToAsset()->create(); $this->assertDatabaseHas('components_assets', ['component_id' => $component->id]); $this->runCommand($admin, [$company->id], ['components']) ->assertExitCode(0); $this->assertSoftDeleted($component); $this->assertDatabaseMissing('components_assets', ['component_id' => $component->id]); } // --------------------------------------------------------------------------- // Consumables // --------------------------------------------------------------------------- public function test_consumable_is_soft_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $consumable = Consumable::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['consumables']) ->assertExitCode(0); $this->assertSoftDeleted($consumable); } public function test_consumable_is_hard_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $consumable = Consumable::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['consumables'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('consumables', ['id' => $consumable->id]); } // --------------------------------------------------------------------------- // Users // --------------------------------------------------------------------------- public function test_user_is_soft_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id, 'activated' => 1]); $this->runCommand($admin, [$company->id], ['users']) ->assertExitCode(0); $this->assertSoftDeleted($user); } public function test_user_delete_nulls_license_seat_assignments(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id, 'activated' => 1]); $seat = LicenseSeat::factory()->create(['assigned_to' => $user->id]); $this->runCommand($admin, [$company->id], ['users']) ->assertExitCode(0); $this->assertNull($seat->fresh()->assigned_to); } public function test_user_delete_removes_accessory_checkout_records(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id, 'activated' => 1]); Accessory::factory()->for($company)->checkedOutToUser($user)->create(); $this->assertDatabaseHas('accessories_checkout', ['assigned_to' => $user->id]); $this->runCommand($admin, [$company->id], ['users']) ->assertExitCode(0); $this->assertDatabaseMissing('accessories_checkout', ['assigned_to' => $user->id]); } public function test_user_delete_removes_consumable_assignments(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id, 'activated' => 1]); Consumable::factory()->for($company)->checkedOutToUser($user)->create(); $this->assertDatabaseHas('consumables_users', ['assigned_to' => $user->id]); $this->runCommand($admin, [$company->id], ['users']) ->assertExitCode(0); $this->assertDatabaseMissing('consumables_users', ['assigned_to' => $user->id]); } public function test_user_delete_removes_checkout_acceptances(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id, 'activated' => 1]); CheckoutAcceptance::factory()->create(['assigned_to_id' => $user->id]); $this->runCommand($admin, [$company->id], ['users']) ->assertExitCode(0); $this->assertDatabaseMissing('checkout_acceptances', ['assigned_to_id' => $user->id]); } public function test_admin_user_is_skipped_during_user_deletion(): void { $company = Company::factory()->create(); $admin = User::factory()->superuser()->create(['company_id' => $company->id, 'activated' => 1]); $this->runCommand($admin, [$company->id], ['users']) ->assertExitCode(0); $this->assertDatabaseHas('users', ['id' => $admin->id, 'deleted_at' => null]); } // --------------------------------------------------------------------------- // Action log clearing // --------------------------------------------------------------------------- public function test_clear_logs_removes_asset_action_logs(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(); $asset = Asset::factory()->for($company)->assignedToUser($user)->create(); $this->runCommand($admin, [$company->id], ['assets'], clearLogs: true) ->assertExitCode(0); $this->assertDatabaseMissing('action_logs', [ 'item_type' => Asset::class, 'item_id' => $asset->id, ]); } // --------------------------------------------------------------------------- // Maintenance / related table cleanup // --------------------------------------------------------------------------- public function test_asset_hard_delete_removes_maintenance_records(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $asset = Asset::factory()->for($company)->create(); $maintenance = Maintenance::factory()->create(['asset_id' => $asset->id]); $this->runCommand($admin, [$company->id], ['assets'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('maintenances', ['id' => $maintenance->id]); } public function test_asset_hard_delete_removes_accessory_checkouts_to_asset(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $asset = Asset::factory()->for($company)->create(); $accessory = Accessory::factory()->for($company)->create(); $accessory->checkouts()->create([ 'accessory_id' => $accessory->id, 'assigned_to' => $asset->id, 'assigned_type' => Asset::class, 'created_by' => $admin->id, ]); $this->assertDatabaseHas('accessories_checkout', [ 'assigned_to' => $asset->id, 'assigned_type' => Asset::class, ]); $this->runCommand($admin, [$company->id], ['assets'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('accessories_checkout', [ 'assigned_to' => $asset->id, 'assigned_type' => Asset::class, ]); } public function test_asset_hard_delete_nulls_child_asset_assignments(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $target = Asset::factory()->for($company)->create(); $child = Asset::factory()->create(['assigned_to' => $target->id, 'assigned_type' => Asset::class]); $this->runCommand($admin, [$company->id], ['assets'], deleteType: 'hard') ->assertExitCode(0); $this->assertNull($child->fresh()->assigned_to); $this->assertNull($child->fresh()->assigned_type); } public function test_license_hard_delete_removes_kit_entries(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $license = License::factory()->for($company)->create(); DB::table('kits_licenses')->insert(['kit_id' => 1, 'license_id' => $license->id, 'quantity' => 1]); $this->runCommand($admin, [$company->id], ['licenses'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('kits_licenses', ['license_id' => $license->id]); } public function test_accessory_hard_delete_removes_kit_entries(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $accessory = Accessory::factory()->for($company)->create(); DB::table('kits_accessories')->insert(['kit_id' => 1, 'accessory_id' => $accessory->id, 'quantity' => 1]); $this->runCommand($admin, [$company->id], ['accessories'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('kits_accessories', ['accessory_id' => $accessory->id]); } public function test_consumable_hard_delete_removes_kit_entries(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $consumable = Consumable::factory()->for($company)->create(); DB::table('kits_consumables')->insert(['kit_id' => 1, 'consumable_id' => $consumable->id, 'quantity' => 1]); $this->runCommand($admin, [$company->id], ['consumables'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('kits_consumables', ['consumable_id' => $consumable->id]); } // --------------------------------------------------------------------------- // Dry run // --------------------------------------------------------------------------- public function test_dry_run_does_not_delete_or_checkin_assets(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id]); $asset = Asset::factory()->for($company)->assignedToUser($user)->create(); $this->runCommand($admin, [$company->id], ['assets'], dryRun: true) ->assertExitCode(0); $this->assertNotSoftDeleted($asset); $this->assertNotNull($asset->fresh()->assigned_to); } // --------------------------------------------------------------------------- // Final confirm / abort // --------------------------------------------------------------------------- public function test_declining_final_confirm_makes_no_changes(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $asset = Asset::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['assets'], confirm: false) ->assertExitCode(0); $this->assertNotSoftDeleted($asset); } // --------------------------------------------------------------------------- // Multi-company / checkin-only mode // --------------------------------------------------------------------------- public function test_multiple_companies_are_all_processed(): void { $admin = User::factory()->superuser()->create(); [$companyA, $companyB] = Company::factory()->count(2)->create(); $assetA = Asset::factory()->for($companyA)->create(); $assetB = Asset::factory()->for($companyB)->create(); $assetC = Asset::factory()->create(); // no company — should be untouched $this->runCommand($admin, [$companyA->id, $companyB->id], ['assets']) ->assertExitCode(0); $this->assertSoftDeleted($assetA); $this->assertSoftDeleted($assetB); $this->assertNotSoftDeleted($assetC); } public function test_checkin_only_mode_checks_in_without_deleting(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id]); $asset = Asset::factory()->for($company)->assignedToUser($user)->create(); $this->runCommand($admin, [$company->id], ['assets'], deleteType: 'none') ->assertExitCode(0); $this->assertNotSoftDeleted($asset); $this->assertNull($asset->fresh()->assigned_to); } // --------------------------------------------------------------------------- // Company deletion // --------------------------------------------------------------------------- public function test_company_is_soft_deleted_when_requested(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); Asset::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['assets'], deleteCompanyType: 'soft') ->assertExitCode(0); $this->assertSoftDeleted($company); } public function test_company_is_hard_deleted_when_requested(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); Asset::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['assets'], deleteCompanyType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('companies', ['id' => $company->id]); } public function test_company_is_kept_when_not_requested(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); Asset::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['assets'], deleteCompanyType: 'keep') ->assertExitCode(0); $this->assertDatabaseHas('companies', ['id' => $company->id, 'deleted_at' => null]); } // --------------------------------------------------------------------------- // Multi-company user handling // --------------------------------------------------------------------------- public function test_user_in_multiple_companies_is_partially_disassociated(): void { $admin = User::factory()->superuser()->create(); [$companyA, $companyB] = Company::factory()->count(2)->create(); $user = User::factory()->create(['company_id' => $companyA->id, 'activated' => 1]); $user->companies()->syncWithoutDetaching([$companyA->id, $companyB->id]); $this->runCommand($admin, [$companyA->id], ['users']) ->assertExitCode(0); // User should NOT have been deleted $this->assertDatabaseHas('users', ['id' => $user->id, 'deleted_at' => null]); // companyA pivot entry should be removed $this->assertDatabaseMissing('company_user', ['user_id' => $user->id, 'company_id' => $companyA->id]); // companyB pivot entry should remain $this->assertDatabaseHas('company_user', ['user_id' => $user->id, 'company_id' => $companyB->id]); } public function test_user_in_only_selected_company_is_fully_deleted(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id, 'activated' => 1]); $user->companies()->syncWithoutDetaching([$company->id]); $this->runCommand($admin, [$company->id], ['users']) ->assertExitCode(0); $this->assertSoftDeleted($user); } public function test_user_hard_delete_removes_company_user_pivot_entries(): void { $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); $user = User::factory()->create(['company_id' => $company->id, 'activated' => 1]); $user->companies()->syncWithoutDetaching([$company->id]); $this->assertDatabaseHas('company_user', ['user_id' => $user->id, 'company_id' => $company->id]); $this->runCommand($admin, [$company->id], ['users'], deleteType: 'hard') ->assertExitCode(0); $this->assertDatabaseMissing('users', ['id' => $user->id]); $this->assertDatabaseMissing('company_user', ['user_id' => $user->id]); } // --------------------------------------------------------------------------- // Email report // --------------------------------------------------------------------------- public function test_email_report_is_sent_when_requested(): void { Mail::fake(); $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); Asset::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['assets'], sendEmailReport: true) ->assertExitCode(0); Mail::assertSent(BulkDeleteReportMail::class, fn ($mail) => $mail->hasTo($admin->email)); } public function test_email_report_is_not_sent_when_declined(): void { Mail::fake(); $admin = User::factory()->superuser()->create(); $company = Company::factory()->create(); Asset::factory()->for($company)->create(); $this->runCommand($admin, [$company->id], ['assets'], sendEmailReport: false) ->assertExitCode(0); Mail::assertNothingSent(); } }