From 91e41049bd9f9952fc8409f4558395058e62446e Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 14 Apr 2026 19:49:05 +0100 Subject: [PATCH] Skip the initial checkout email to the recipient if sign_in_place was checked --- app/Events/CheckoutableCheckedOut.php | 5 +- .../Assets/AssetCheckoutController.php | 2 +- .../ConsumableCheckoutController.php | 1 + .../Licenses/LicenseCheckoutController.php | 4 +- app/Listeners/CheckoutableListener.php | 14 +++ app/Models/Asset.php | 4 +- ...ignInPlaceCheckoutEmailSuppressionTest.php | 113 ++++++++++++++++++ 7 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 tests/Feature/Notifications/Email/SignInPlaceCheckoutEmailSuppressionTest.php diff --git a/app/Events/CheckoutableCheckedOut.php b/app/Events/CheckoutableCheckedOut.php index f75ddcc5cb..0d47f64552 100644 --- a/app/Events/CheckoutableCheckedOut.php +++ b/app/Events/CheckoutableCheckedOut.php @@ -22,12 +22,14 @@ class CheckoutableCheckedOut public int $quantity; + public bool $signInPlace; + /** * Create a new event instance. * * @return void */ - public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [], $quantity = 1) + public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [], $quantity = 1, bool $signInPlace = false) { $this->checkoutable = $checkoutable; $this->checkedOutTo = $checkedOutTo; @@ -35,5 +37,6 @@ class CheckoutableCheckedOut $this->note = $note; $this->originalValues = $originalValues; $this->quantity = $quantity; + $this->signInPlace = $signInPlace; } } diff --git a/app/Http/Controllers/Assets/AssetCheckoutController.php b/app/Http/Controllers/Assets/AssetCheckoutController.php index f11257fc24..8c31cabc7d 100644 --- a/app/Http/Controllers/Assets/AssetCheckoutController.php +++ b/app/Http/Controllers/Assets/AssetCheckoutController.php @@ -130,7 +130,7 @@ class AssetCheckoutController extends Controller 'sign_in_place' => $request->boolean('sign_in_place'), ]); - if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->input('note'), $request->input('name'))) { + if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->input('note'), $request->input('name'), null, $request->boolean('sign_in_place'))) { // When sign_in_place is requested and the target is a user, redirect to the // acceptance/signature page so the user can sign in person. The signature is diff --git a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php index 39961acbc1..93905e01e2 100644 --- a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php +++ b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php @@ -117,6 +117,7 @@ class ConsumableCheckoutController extends Controller $request->input('note'), [], $consumable->checkout_qty, + $request->boolean('sign_in_place'), )); $request->request->add(['checkout_to_type' => 'user']); diff --git a/app/Http/Controllers/Licenses/LicenseCheckoutController.php b/app/Http/Controllers/Licenses/LicenseCheckoutController.php index 884675e3c5..aa80eb7344 100644 --- a/app/Http/Controllers/Licenses/LicenseCheckoutController.php +++ b/app/Http/Controllers/Licenses/LicenseCheckoutController.php @@ -187,7 +187,7 @@ class LicenseCheckoutController extends Controller $licenseSeat->assigned_to = $target->assigned_to; } if ($licenseSeat->save()) { - event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), request('notes'))); + event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), request('notes'), [], 1, request()->boolean('sign_in_place'))); return $target; } @@ -204,7 +204,7 @@ class LicenseCheckoutController extends Controller $licenseSeat->assigned_to = request('assigned_to'); if ($licenseSeat->save()) { - event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), request('notes'))); + event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), request('notes'), [], 1, request()->boolean('sign_in_place'))); return $target; } diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 6a9a0fc1ff..8ddd62e144 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -80,6 +80,11 @@ class CheckoutableListener $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($acceptance); $shouldSendWebhookNotification = $this->shouldSendWebhookNotification(); + if ($this->shouldSkipInitialAcceptanceEmail($event, $acceptance)) { + $shouldSendEmailToUser = false; + $shouldSendEmailToAlertAddress = false; + } + if (! $shouldSendEmailToUser && ! $shouldSendEmailToAlertAddress && ! $shouldSendWebhookNotification) { return; } @@ -480,6 +485,15 @@ class CheckoutableListener return false; } + private function shouldSkipInitialAcceptanceEmail(CheckoutableCheckedOut $event, ?CheckoutAcceptance $acceptance): bool + { + if (! $event->signInPlace) { + return false; + } + + return ($acceptance instanceof CheckoutAcceptance) || ! empty($event->checkoutable->getEula()); + } + private function shouldSendEmailToAlertAddress($acceptance = null): bool { if (Context::get('action') === 'bulk_asset_checkout') { diff --git a/app/Models/Asset.php b/app/Models/Asset.php index ffb8b453a9..1ae46a8d34 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -516,7 +516,7 @@ class Asset extends Depreciable * * @return bool */ - public function checkOut($target, $admin = null, $checkout_at = null, $expected_checkin = null, $note = null, $name = null, $location = null) + public function checkOut($target, $admin = null, $checkout_at = null, $expected_checkin = null, $note = null, $name = null, $location = null, bool $signInPlace = false) { if (! $target) { return false; @@ -560,7 +560,7 @@ class Asset extends Depreciable } else { $checkedOutBy = auth()->user(); } - event(new CheckoutableCheckedOut($this, $target, $checkedOutBy, $note, $originalValues)); + event(new CheckoutableCheckedOut($this, $target, $checkedOutBy, $note, $originalValues, 1, $signInPlace)); $this->increment('checkout_counter', 1); diff --git a/tests/Feature/Notifications/Email/SignInPlaceCheckoutEmailSuppressionTest.php b/tests/Feature/Notifications/Email/SignInPlaceCheckoutEmailSuppressionTest.php new file mode 100644 index 0000000000..923aee2f80 --- /dev/null +++ b/tests/Feature/Notifications/Email/SignInPlaceCheckoutEmailSuppressionTest.php @@ -0,0 +1,113 @@ +create(); + $category = Category::factory() + ->forAssets() + ->doesNotRequireAcceptance() + ->doesNotSendCheckinEmail() + ->hasLocalEula() + ->create(); + $asset = Asset::factory() + ->for(AssetModel::factory()->for($category, 'category'), 'model') + ->create(); + + $response = $this->actingAs(User::factory()->admin()->create()) + ->post(route('hardware.checkout.store', $asset), [ + 'checkout_to_type' => 'user', + 'assigned_user' => $targetUser->id, + 'redirect_option' => 'index', + 'sign_in_place' => 1, + ]); + + $acceptance = CheckoutAcceptance::query() + ->where('checkoutable_type', Asset::class) + ->where('checkoutable_id', $asset->id) + ->where('assigned_to_id', $targetUser->id) + ->pending() + ->latest() + ->first(); + + $this->assertNotNull($acceptance); + $response->assertRedirect(route('account.accept.item', $acceptance)); + Mail::assertNotSent(CheckoutAssetMail::class); + } + + public function test_consumable_checkout_does_not_send_initial_acceptance_email_when_sign_in_place_is_selected(): void + { + $targetUser = User::factory()->create(); + $consumable = Consumable::factory()->requiringAcceptance()->create(); + + $response = $this->actingAs(User::factory()->admin()->create()) + ->post(route('consumables.checkout.store', $consumable), [ + 'assigned_to' => $targetUser->id, + 'redirect_option' => 'index', + 'checkout_qty' => 2, + 'sign_in_place' => 1, + ]); + + $acceptance = CheckoutAcceptance::query() + ->where('checkoutable_type', Consumable::class) + ->where('checkoutable_id', $consumable->id) + ->where('assigned_to_id', $targetUser->id) + ->pending() + ->latest() + ->first(); + + $this->assertNotNull($acceptance); + $response->assertRedirect(route('account.accept.item', $acceptance)); + Mail::assertNotSent(CheckoutConsumableMail::class); + } + + public function test_license_checkout_does_not_send_initial_acceptance_email_when_sign_in_place_is_selected(): void + { + $targetUser = User::factory()->create(); + $seat = LicenseSeat::factory()->requiringAcceptance()->create(); + + $response = $this->actingAs(User::factory()->admin()->create()) + ->post(route('licenses.checkout', $seat->license), [ + 'assigned_to' => $targetUser->id, + 'redirect_option' => 'index', + 'sign_in_place' => 1, + ]); + + $acceptance = CheckoutAcceptance::query() + ->where('checkoutable_type', LicenseSeat::class) + ->where('assigned_to_id', $targetUser->id) + ->pending() + ->latest() + ->first(); + + $this->assertNotNull($acceptance); + $response->assertRedirect(route('account.accept.item', $acceptance)); + Mail::assertNotSent(CheckoutLicenseMail::class); + } +} +