diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 747f7b7284..b652455a57 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -650,6 +650,7 @@ class SettingsController extends Controller $setting->alert_email = $alert_email; $setting->admin_cc_email = $admin_cc_email; + $setting->admin_cc_always = $request->validated('admin_cc_always'); $setting->alerts_enabled = $request->input('alerts_enabled', '0'); $setting->alert_interval = $request->input('alert_interval'); $setting->alert_threshold = $request->input('alert_threshold'); diff --git a/app/Http/Requests/StoreNotificationSettings.php b/app/Http/Requests/StoreNotificationSettings.php index bf5f5b3d4e..f58d014c76 100644 --- a/app/Http/Requests/StoreNotificationSettings.php +++ b/app/Http/Requests/StoreNotificationSettings.php @@ -5,6 +5,7 @@ namespace App\Http\Requests; use App\Models\Accessory; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Gate; +use Illuminate\Validation\Rule; class StoreNotificationSettings extends FormRequest { @@ -26,6 +27,9 @@ class StoreNotificationSettings extends FormRequest return [ 'alert_email' => 'email_array|nullable', 'admin_cc_email' => 'email_array|nullable', + 'admin_cc_always' => [ + Rule::in('0', '1'), + ], 'alert_threshold' => 'numeric|nullable', 'alert_interval' => 'numeric|nullable|gt:0', 'audit_warning_days' => 'numeric|nullable', diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index e647bcd9e0..ec843b980b 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -69,16 +69,16 @@ class CheckoutableListener return; } + $acceptance = $this->getCheckoutAcceptance($event); + $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->checkoutable); - $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress(); + $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($acceptance); $shouldSendWebhookNotification = $this->shouldSendWebhookNotification(); if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) { return; } - $acceptance = $this->getCheckoutAcceptance($event); - if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) { $mailable = $this->getCheckoutMailType($event, $acceptance); $notifiable = $this->getNotifiableUser($event); @@ -419,9 +419,19 @@ class CheckoutableListener return false; } - private function shouldSendEmailToAlertAddress(): bool + private function shouldSendEmailToAlertAddress($acceptance = null): bool { - return Setting::getSettings() && Setting::getSettings()->admin_cc_email; + $setting = Setting::getSettings(); + + if (!$setting) { + return false; + } + + if (is_null($acceptance) && !$setting->admin_cc_always) { + return false; + } + + return (bool) $setting->admin_cc_email; } private function getFormattedAlertAddresses(): array diff --git a/database/migrations/2025_06_02_233556_add_admin_cc_always_to_settings_table.php b/database/migrations/2025_06_02_233556_add_admin_cc_always_to_settings_table.php new file mode 100644 index 0000000000..d28115a6d2 --- /dev/null +++ b/database/migrations/2025_06_02_233556_add_admin_cc_always_to_settings_table.php @@ -0,0 +1,27 @@ +boolean('admin_cc_always')->after('admin_cc_email')->default(1); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('settings', function (Blueprint $table) { + $table->dropColumn('admin_cc_always'); + }); + } +}; diff --git a/resources/lang/en-US/admin/settings/general.php b/resources/lang/en-US/admin/settings/general.php index 9f0fa2c9e6..8c4ae35365 100644 --- a/resources/lang/en-US/admin/settings/general.php +++ b/resources/lang/en-US/admin/settings/general.php @@ -9,6 +9,8 @@ return [ 'ad_append_domain_help' => 'User isn\'t required to write "username@domain.local", they can just type "username".', 'admin_cc_email' => 'CC Email', 'admin_cc_email_help' => 'Send a copy of checkin/checkout emails to this address.', + 'admin_cc_always' => 'Always send copy upon checkin/checkout', + 'admin_cc_when_acceptance_required' => 'Only send copy upon checkout if acceptance is required', 'admin_settings' => 'Admin Settings', 'is_ad' => 'This is an Active Directory server', 'alerts' => 'Alerts', diff --git a/resources/views/settings/alerts.blade.php b/resources/views/settings/alerts.blade.php index 9f31206452..c861cc63bf 100644 --- a/resources/views/settings/alerts.blade.php +++ b/resources/views/settings/alerts.blade.php @@ -96,8 +96,28 @@ {!! $errors->first('admin_cc_email', '
') !!}

{{ trans('admin/settings/general.admin_cc_email_help') }}

- - + + +
+
+ +
diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php new file mode 100644 index 0000000000..1bdbf0b673 --- /dev/null +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php @@ -0,0 +1,126 @@ +user = User::factory()->create(); + + $this->category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'require_acceptance' => false, + 'use_default_eula' => false, + ]); + + $this->assetModel = AssetModel::factory()->for($this->category)->create(); + + $this->asset = Asset::factory() + ->for($this->assetModel, 'model') + ->assignedToUser($this->user) + ->create(); + } + + public function test_admin_alert_email_sends() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->category->update(['checkin_email' => true]); + + $this->fireCheckInEvent($this->asset, $this->user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo($this->user->email) && $mail->hasCc('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_category_email_is_not_set_to_send_email_to_user() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckInEvent($this->asset, $this->user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_user_has_no_email_address() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->user->update(['email' => null]); + + $this->category->update(['checkin_email' => true]); + + $this->fireCheckInEvent($this->asset, $this->user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + public function test_admin_alert_email_sent_when_always_send_is_true_and_asset_does_not_require_acceptance() + { + $this->settings + ->enableAdminCC('cc@example.com') + ->enableAdminCCAlways(); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckInEvent($this->asset, $this->user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com') || $mail->hasCc('cc@example.com'); + }); + } + + public function test_admin_alert_email_not_sent_when_always_send_is_false_and_asset_does_not_require_acceptance() + { + $this->settings + ->enableAdminCC('cc@example.com') + ->disableAdminCCAlways(); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckInEvent($this->asset, $this->user); + + Mail::assertNotSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com') || $mail->hasCc('cc@example.com'); + }); + } + + private function fireCheckInEvent($asset, $user): void + { + event(new CheckoutableCheckedIn( + $asset, + $user, + User::factory()->checkinAssets()->create(), + '' + )); + } +} diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckoutTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckoutTest.php new file mode 100644 index 0000000000..d881ae7dab --- /dev/null +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckoutTest.php @@ -0,0 +1,119 @@ +category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'require_acceptance' => false, + 'use_default_eula' => false, + ]); + + $this->assetModel = AssetModel::factory()->for($this->category)->create(); + $this->asset = Asset::factory()->for($this->assetModel, 'model')->create(); + + $this->user = User::factory()->create(); + } + + public function test_admin_alert_email_sends() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->category->update(['checkin_email' => true]); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + return $mail->hasCc('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_category_is_not_set_to_send_email_to_user() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_user_has_no_email_address() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->category->update(['checkin_email' => true]); + $this->user->update(['email' => null]); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + public function test_admin_alert_email_sent_when_always_send_is_true_and_asset_does_not_require_acceptance() + { + $this->settings + ->enableAdminCC('cc@example.com') + ->enableAdminCCAlways(); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + return $mail->hasTo('cc@example.com') || $mail->hasCc('cc@example.com'); + }); + } + + public function test_admin_alert_email_not_sent_when_always_send_is_false_and_asset_does_not_require_acceptance() + { + $this->settings + ->enableAdminCC('cc@example.com') + ->disableAdminCCAlways(); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckoutEvent(); + + Mail::assertNotSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + return $mail->hasTo('cc@example.com') || $mail->hasCc('cc@example.com'); + }); + } + + private function fireCheckoutEvent(): void + { + event(new CheckoutableCheckedOut( + $this->asset, + $this->user, + User::factory()->superuser()->create(), + '', + )); + } +} diff --git a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckinTest.php similarity index 64% rename from tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php rename to tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckinTest.php index 254e2e09ca..432ce88b4b 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckinTest.php @@ -4,8 +4,6 @@ namespace Tests\Feature\Notifications\Email; use App\Mail\CheckinAssetMail; use App\Models\Accessory; -use App\Models\AssetModel; -use App\Models\Category; use App\Models\Consumable; use App\Models\LicenseSeat; use Illuminate\Support\Facades\Mail; @@ -16,7 +14,7 @@ use App\Models\User; use Tests\TestCase; #[Group('notifications')] -class EmailNotificationsUponCheckinTest extends TestCase +class EmailNotificationsToUserUponCheckinTest extends TestCase { protected function setUp(): void { @@ -101,57 +99,6 @@ class EmailNotificationsUponCheckinTest extends TestCase Mail::assertNothingSent(); } - public function test_admin_alert_email_sends() - { - $this->settings->enableAdminCC('cc@example.com'); - - $user = User::factory()->create(); - $asset = Asset::factory()->assignedToUser($user)->create(); - - $asset->model->category->update(['checkin_email' => true]); - - $this->fireCheckInEvent($asset, $user); - - Mail::assertSent(CheckinAssetMail::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && $mail->hasCc('cc@example.com'); - }); - } - - public function test_admin_alert_email_still_sent_when_category_email_is_not_set_to_send_email_to_user() - { - $this->settings->enableAdminCC('cc@example.com'); - - $category = Category::factory()->create([ - 'checkin_email' => false, - 'eula_text' => null, - 'use_default_eula' => false, - ]); - $assetModel = AssetModel::factory()->create(['category_id' => $category->id]); - $asset = Asset::factory()->create(['model_id' => $assetModel->id]); - - $this->fireCheckInEvent($asset, User::factory()->create()); - - Mail::assertSent(CheckinAssetMail::class, function ($mail) { - return $mail->hasTo('cc@example.com'); - }); - } - - public function test_admin_alert_email_still_sent_when_user_has_no_email_address() - { - $this->settings->enableAdminCC('cc@example.com'); - - $user = User::factory()->create(['email' => null]); - $asset = Asset::factory()->assignedToUser($user)->create(); - - $asset->model->category->update(['checkin_email' => true]); - - $this->fireCheckInEvent($asset, $user); - - Mail::assertSent(CheckinAssetMail::class, function ($mail) { - return $mail->hasTo('cc@example.com'); - }); - } - private function fireCheckInEvent($asset, $user): void { event(new CheckoutableCheckedIn( diff --git a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckoutTest.php similarity index 68% rename from tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php rename to tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckoutTest.php index e5a1a66cb6..e5741da8ec 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckoutTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\Attributes\Group; use Tests\TestCase; #[Group('notifications')] -class EmailNotificationsUponCheckoutTest extends TestCase +class EmailNotificationsToUserUponCheckoutTest extends TestCase { private Asset $asset; private AssetModel $assetModel; @@ -87,44 +87,6 @@ class EmailNotificationsUponCheckoutTest extends TestCase Mail::assertNothingSent(); } - public function test_admin_alert_email_sends() - { - $this->settings->enableAdminCC('cc@example.com'); - - $this->category->update(['checkin_email' => true]); - - $this->fireCheckoutEvent(); - - Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { - return $mail->hasCc('cc@example.com'); - }); - } - - public function test_admin_alert_email_still_sent_when_category_is_not_set_to_send_email_to_user() - { - $this->settings->enableAdminCC('cc@example.com'); - - $this->fireCheckoutEvent(); - - Mail::assertSent(CheckoutAssetMail::class, function ($mail) { - return $mail->hasTo('cc@example.com'); - }); - } - - public function test_admin_alert_email_still_sent_when_user_has_no_email_address() - { - $this->settings->enableAdminCC('cc@example.com'); - - $this->category->update(['checkin_email' => true]); - $this->user->update(['email' => null]); - - $this->fireCheckoutEvent(); - - Mail::assertSent(CheckoutAssetMail::class, function ($mail) { - return $mail->hasTo('cc@example.com'); - }); - } - private function fireCheckoutEvent(): void { event(new CheckoutableCheckedOut( diff --git a/tests/Feature/Settings/AlertsSettingTest.php b/tests/Feature/Settings/AlertsSettingTest.php index d79bd1cf21..f8829d3825 100644 --- a/tests/Feature/Settings/AlertsSettingTest.php +++ b/tests/Feature/Settings/AlertsSettingTest.php @@ -18,7 +18,10 @@ class AlertsSettingTest extends TestCase public function testAdminCCEmailArrayCanBeSaved() { $response = $this->actingAs(User::factory()->superuser()->create()) - ->post(route('settings.alerts.save', ['alert_email' => 'me@example.com,you@example.com'])) + ->post(route('settings.alerts.save', [ + 'alert_email' => 'me@example.com,you@example.com', + 'admin_cc_always' => '1', + ])) ->assertStatus(302) ->assertValid('alert_email') ->assertRedirect(route('settings.index')) @@ -26,4 +29,23 @@ class AlertsSettingTest extends TestCase $this->followRedirects($response)->assertSee('alert-success'); } + public function test_can_update_admin_cc_always_to_true() + { + $this->settings->disableAdminCCAlways(); + + $this->actingAs(User::factory()->superuser()->create()) + ->post(route('settings.alerts.save', ['admin_cc_always' => '1'])); + + $this->assertDatabaseHas('settings', ['admin_cc_always' => '1']); + } + + public function test_can_update_admin_cc_always_to_false() + { + $this->settings->enableAdminCC()->enableAdminCCAlways(); + + $this->actingAs(User::factory()->superuser()->create()) + ->post(route('settings.alerts.save', ['admin_cc_always' => '0'])); + + $this->assertDatabaseHas('settings', ['admin_cc_always' => '0']); + } } diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index bcbd83d8b6..8ff8ba6945 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -60,6 +60,20 @@ class Settings ]); } + public function enableAdminCCAlways(): Settings + { + return $this->update([ + 'admin_cc_always' => 1, + ]); + } + + public function disableAdminCCAlways(): Settings + { + return $this->update([ + 'admin_cc_always' => 0, + ]); + } + public function enableMultipleFullCompanySupport(): Settings { return $this->update(['full_multiple_companies_support' => 1]);