From 9e3b56f4bc00c7949a94ccf698c1cd2cb02da03b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 25 Sep 2025 15:41:37 -0700 Subject: [PATCH 01/85] Add failing test --- .../Checkouts/Ui/BulkAssetCheckoutTest.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 6f6f250b9003..51d945cb2fd5 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -187,4 +187,31 @@ public function test_prevents_checkouts_of_checked_out_items($data) // ensure redirected back $response->assertRedirectToRoute('hardware.bulkcheckout.show'); } + + public function test_one_email_is_sent_instead_of_multiple_individual_ones() + { + Mail::fake(); + + $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); + $user = User::factory()->create(['email' => 'someone@example.com']); + + $this->actingAs(User::factory()->checkoutAssets()->viewAssets()->create()) + ->followingRedirects() + ->post(route('hardware.bulkcheckout.store'), [ + 'selected_assets' => $assets->pluck('id')->toArray(), + 'checkout_to_type' => 'user', + 'assigned_user' => $user->id, + 'assigned_asset' => null, + 'note' => null, + ]) + ->assertOk(); + + $assets->fresh()->each(function ($asset) { + $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); + }); + + Mail::assertSent(CheckoutAssetMail::class, 0); + + $this->markTestIncomplete('assert one email sent for both assets'); + } } From a40e4d7d045789a376f51557b52df5c669faac41 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 25 Sep 2025 15:43:04 -0700 Subject: [PATCH 02/85] Begin experimenting with context --- app/Http/Controllers/Assets/BulkAssetsController.php | 3 +++ app/Listeners/CheckoutableListener.php | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index cb4fa5fa5b32..30047438e1c2 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -12,6 +12,7 @@ use App\View\Label; use Carbon\Carbon; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; @@ -631,6 +632,8 @@ public function showCheckout() : View */ public function storeCheckout(AssetCheckoutRequest $request) : RedirectResponse | ModelNotFoundException { + Context::add('action', 'bulk_asset_checkout'); + $this->authorize('checkout', Asset::class); try { diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 908dd58dfded..a2052dce373e 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -33,6 +33,7 @@ use App\Notifications\CheckoutLicenseSeatNotification; use GuzzleHttp\Exception\ClientException; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Notification; use Exception; @@ -428,12 +429,17 @@ private function newMicrosoftTeamsWebhookEnabled(): bool private function shouldSendCheckoutEmailToUser(Model $checkoutable): bool { /** - * Send an email if any of the following conditions are met: + * Send an email if we didn't get here from a bulk checkout + * and any of the following conditions are met: * 1. The asset requires acceptance * 2. The item has a EULA * 3. The item should send an email at check-in/check-out */ + if (Context::get('action') === 'bulk_asset_checkout') { + return false; + } + if ($checkoutable->requireAcceptance()) { return true; } From 7017a0cae178c43fa682131e777bc18371a56864 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 25 Sep 2025 16:23:43 -0700 Subject: [PATCH 03/85] WIP: introduce event --- app/Events/CheckoutablesCheckedOutInBulk.php | 23 ++++++++ .../Assets/BulkAssetsController.php | 10 ++++ app/Mail/BulkAssetCheckoutMail.php | 52 +++++++++++++++++++ .../mail/bulk-asset-checkout-mail.blade.php | 12 +++++ .../Checkouts/Ui/BulkAssetCheckoutTest.php | 27 ++++++++-- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 app/Events/CheckoutablesCheckedOutInBulk.php create mode 100644 app/Mail/BulkAssetCheckoutMail.php create mode 100644 resources/views/mail/bulk-asset-checkout-mail.blade.php diff --git a/app/Events/CheckoutablesCheckedOutInBulk.php b/app/Events/CheckoutablesCheckedOutInBulk.php new file mode 100644 index 000000000000..6dd8ccde7a72 --- /dev/null +++ b/app/Events/CheckoutablesCheckedOutInBulk.php @@ -0,0 +1,23 @@ +get('note')), + ); + // Redirect to the new asset page return redirect()->to('hardware')->with('success', trans_choice('admin/hardware/message.multi-checkout.success', $asset_ids)); } diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php new file mode 100644 index 000000000000..11ccebfa5647 --- /dev/null +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -0,0 +1,52 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/resources/views/mail/bulk-asset-checkout-mail.blade.php b/resources/views/mail/bulk-asset-checkout-mail.blade.php new file mode 100644 index 000000000000..de9a155b34d3 --- /dev/null +++ b/resources/views/mail/bulk-asset-checkout-mail.blade.php @@ -0,0 +1,12 @@ + +# Introduction + +The body of your message. + + +Button Text + + +Thanks,
+{{ config('app.name') }} +
diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 51d945cb2fd5..797fa0651cb0 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -2,13 +2,17 @@ namespace Tests\Feature\Checkouts\Ui; +use App\Events\CheckoutablesCheckedOutInBulk; +use App\Mail\BulkAssetCheckoutMail; use App\Mail\CheckoutAssetMail; use App\Models\Asset; use App\Models\Company; use App\Models\Location; use App\Models\User; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Mail; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\ExpectationFailedException; use Tests\TestCase; @@ -188,8 +192,10 @@ public function test_prevents_checkouts_of_checked_out_items($data) $response->assertRedirectToRoute('hardware.bulkcheckout.show'); } + #[Group('notifications')] public function test_one_email_is_sent_instead_of_multiple_individual_ones() { + Event::fake(); Mail::fake(); $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); @@ -202,16 +208,29 @@ public function test_one_email_is_sent_instead_of_multiple_individual_ones() 'checkout_to_type' => 'user', 'assigned_user' => $user->id, 'assigned_asset' => null, + 'checkout_at' => now()->subWeek()->format('Y-m-d'), + 'expected_checkin' => now()->addWeek()->format('Y-m-d'), 'note' => null, ]) ->assertOk(); - $assets->fresh()->each(function ($asset) { - $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); - }); + // @todo: + // $assets->fresh()->each(function ($asset) { + // $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); + // }); Mail::assertSent(CheckoutAssetMail::class, 0); - $this->markTestIncomplete('assert one email sent for both assets'); + Event::assertDispatchedTimes(CheckoutablesCheckedOutInBulk::class, 1); + Event::assertDispatched(CheckoutablesCheckedOutInBulk::class, function (CheckoutablesCheckedOutInBulk $event) { + // @todo: + dd($event); + }); + + // @todo: move to Notifications test directory? + // Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + // // @todo: assert contents + // return $mail->hasTo('someone@example.com'); + // }); } } From 3327b2ce3c25ea253c2eb4bc04650e1f310e8fd9 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 30 Sep 2025 13:03:24 -0700 Subject: [PATCH 04/85] Add assertions --- .../Checkouts/Ui/BulkAssetCheckoutTest.php | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 797fa0651cb0..b422e09aa4f9 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -199,14 +199,15 @@ public function test_one_email_is_sent_instead_of_multiple_individual_ones() Mail::fake(); $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); - $user = User::factory()->create(['email' => 'someone@example.com']); + $target = User::factory()->create(['email' => 'someone@example.com']); - $this->actingAs(User::factory()->checkoutAssets()->viewAssets()->create()) + $admin = User::factory()->checkoutAssets()->viewAssets()->create(); + $this->actingAs($admin) ->followingRedirects() ->post(route('hardware.bulkcheckout.store'), [ 'selected_assets' => $assets->pluck('id')->toArray(), 'checkout_to_type' => 'user', - 'assigned_user' => $user->id, + 'assigned_user' => $target->id, 'assigned_asset' => null, 'checkout_at' => now()->subWeek()->format('Y-m-d'), 'expected_checkin' => now()->addWeek()->format('Y-m-d'), @@ -222,15 +223,29 @@ public function test_one_email_is_sent_instead_of_multiple_individual_ones() Mail::assertSent(CheckoutAssetMail::class, 0); Event::assertDispatchedTimes(CheckoutablesCheckedOutInBulk::class, 1); - Event::assertDispatched(CheckoutablesCheckedOutInBulk::class, function (CheckoutablesCheckedOutInBulk $event) { - // @todo: - dd($event); + Event::assertDispatched(CheckoutablesCheckedOutInBulk::class, function (CheckoutablesCheckedOutInBulk $event) use ($target, $admin, $assets) { + foreach ($assets as $asset) { + if ($event->assets->doesntContain($asset)) { + return false; + } + } + + if ($target->id !== $event->target->id) { + return false; + } + + if ($admin->id !== $event->admin->id) { + return false; + } + + return true; }); // @todo: move to Notifications test directory? - // Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - // // @todo: assert contents - // return $mail->hasTo('someone@example.com'); - // }); + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + // @todo: assert contents + return $mail->hasTo('someone@example.com'); + }); } } From 3c42acebf021e3eda4e1b692894427774bb5dc30 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 30 Sep 2025 15:11:12 -0700 Subject: [PATCH 05/85] Scaffold new listener --- .../CheckoutablesCheckedOutInBulkListener.php | 22 ++++++++++ app/Providers/EventServiceProvider.php | 2 + .../Checkouts/Ui/BulkAssetCheckoutTest.php | 10 +---- .../Email/BulkCheckoutEmailTest.php | 41 +++++++++++++++++++ 4 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 app/Listeners/CheckoutablesCheckedOutInBulkListener.php create mode 100644 tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php new file mode 100644 index 000000000000..6a1d7528cbe9 --- /dev/null +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -0,0 +1,22 @@ +listen( + CheckoutablesCheckedOutInBulk::class, + CheckoutablesCheckedOutInBulkListener::class + ); + } + + public function handle(CheckoutablesCheckedOutInBulk $event): void + { + // + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 1f08b445c9ec..9143cdc8f5a0 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Listeners\CheckoutableListener; +use App\Listeners\CheckoutablesCheckedOutInBulkListener; use App\Listeners\LogListener; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -31,5 +32,6 @@ class EventServiceProvider extends ServiceProvider protected $subscribe = [ LogListener::class, CheckoutableListener::class, + CheckoutablesCheckedOutInBulkListener::class, ]; } diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index b422e09aa4f9..12a4aa264e83 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Checkouts\Ui; use App\Events\CheckoutablesCheckedOutInBulk; -use App\Mail\BulkAssetCheckoutMail; use App\Mail\CheckoutAssetMail; use App\Models\Asset; use App\Models\Company; @@ -195,7 +194,6 @@ public function test_prevents_checkouts_of_checked_out_items($data) #[Group('notifications')] public function test_one_email_is_sent_instead_of_multiple_individual_ones() { - Event::fake(); Mail::fake(); $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); @@ -220,6 +218,7 @@ public function test_one_email_is_sent_instead_of_multiple_individual_ones() // $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); // }); + // ensure individual emails are not sent. Mail::assertSent(CheckoutAssetMail::class, 0); Event::assertDispatchedTimes(CheckoutablesCheckedOutInBulk::class, 1); @@ -240,12 +239,5 @@ public function test_one_email_is_sent_instead_of_multiple_individual_ones() return true; }); - - // @todo: move to Notifications test directory? - Mail::assertSent(BulkAssetCheckoutMail::class, 1); - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - // @todo: assert contents - return $mail->hasTo('someone@example.com'); - }); } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php new file mode 100644 index 000000000000..67ba01d28e44 --- /dev/null +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -0,0 +1,41 @@ +markTestIncomplete(); + + Mail::fake(); + + $assets = Asset::factory()->count(2)->create(); + $target = User::factory()->create(['email' => 'someone@example.com']); + $admin = User::factory()->create(); + $checkout_at = date('Y-m-d H:i:s'); + $expected_checkin = ''; + + CheckoutablesCheckedOutInBulk::dispatch( + $assets, + $target, + $admin, + $checkout_at, + $expected_checkin, + 'A note here', + ); + + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + // @todo: assert contents + return $mail->hasTo('someone@example.com'); + }); + } +} From 17a26b43f00bada6a42c8f43a9d14c2281c3d521 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 8 Oct 2025 14:01:07 -0700 Subject: [PATCH 06/85] Naively send email --- .../CheckoutablesCheckedOutInBulkListener.php | 10 +++++++++- app/Mail/BulkAssetCheckoutMail.php | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 6a1d7528cbe9..b1bed1064e9a 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -3,6 +3,8 @@ namespace App\Listeners; use App\Events\CheckoutablesCheckedOutInBulk; +use App\Mail\BulkAssetCheckoutMail; +use Illuminate\Support\Facades\Mail; class CheckoutablesCheckedOutInBulkListener { @@ -17,6 +19,12 @@ public function subscribe($events) public function handle(CheckoutablesCheckedOutInBulk $event): void { - // + Mail::to($event->target)->send(new BulkAssetCheckoutMail( + $event->assets, + $event->target, + $event->admin, + $event->checkout_at, + $event->expected_checkin, + )); } } diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 11ccebfa5647..fe6f375f7db8 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -2,11 +2,14 @@ namespace App\Mail; +use App\Models\User; use Illuminate\Bus\Queueable; +use Illuminate\Database\Eloquent\Model; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Collection; class BulkAssetCheckoutMail extends Mailable { @@ -15,8 +18,13 @@ class BulkAssetCheckoutMail extends Mailable /** * Create a new message instance. */ - public function __construct() - { + public function __construct( + public Collection $assets, + public Model $target, + public User $admin, + public string $checkout_at, + public string $expected_checkin, + ) { // } From 9a380ac3d46248a589fdfd5d029f4839a58d685b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 8 Oct 2025 14:12:35 -0700 Subject: [PATCH 07/85] Extract intro text --- app/Mail/BulkAssetCheckoutMail.php | 10 ++++++++++ .../views/mail/bulk-asset-checkout-mail.blade.php | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index fe6f375f7db8..df54d708e995 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -34,6 +34,7 @@ public function __construct( public function envelope(): Envelope { return new Envelope( + // @todo: translate subject: 'Bulk Asset Checkout Mail', ); } @@ -45,6 +46,9 @@ public function content(): Content { return new Content( markdown: 'mail.bulk-asset-checkout-mail', + with: [ + 'introduction' => $this->getIntroduction(), + ], ); } @@ -57,4 +61,10 @@ public function attachments(): array { return []; } + + private function getIntroduction(): string + { + // @todo: + return 'The following assets have been checked out to you:'; + } } diff --git a/resources/views/mail/bulk-asset-checkout-mail.blade.php b/resources/views/mail/bulk-asset-checkout-mail.blade.php index de9a155b34d3..aed6551019b7 100644 --- a/resources/views/mail/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/bulk-asset-checkout-mail.blade.php @@ -1,7 +1,7 @@ # Introduction -The body of your message. +{{ $introduction }} Button Text From 28dc4bf52eb24ce47c8e01d5e4254eb579357783 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 8 Oct 2025 14:13:04 -0700 Subject: [PATCH 08/85] Move template to correct directory --- app/Mail/BulkAssetCheckoutMail.php | 2 +- .../mail/{ => markdown}/bulk-asset-checkout-mail.blade.php | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename resources/views/mail/{ => markdown}/bulk-asset-checkout-mail.blade.php (100%) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index df54d708e995..3961a31f7c6a 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -45,7 +45,7 @@ public function envelope(): Envelope public function content(): Content { return new Content( - markdown: 'mail.bulk-asset-checkout-mail', + markdown: 'mail.markdown.bulk-asset-checkout-mail', with: [ 'introduction' => $this->getIntroduction(), ], diff --git a/resources/views/mail/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php similarity index 100% rename from resources/views/mail/bulk-asset-checkout-mail.blade.php rename to resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php From 19969fee3942175eb83e977abd10fd93a9852cf3 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 8 Oct 2025 15:32:38 -0700 Subject: [PATCH 09/85] Update closing --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index aed6551019b7..96517883b96e 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -7,6 +7,7 @@ Button Text -Thanks,
-{{ config('app.name') }} +{{ trans('mail.best_regards') }}
+ +{{ $snipeSettings->site_name }}
From 13b51d86085ff3a72c8849bc626576c7f0665517 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 8 Oct 2025 15:46:36 -0700 Subject: [PATCH 10/85] Make acceptance section dynamic --- app/Mail/BulkAssetCheckoutMail.php | 36 +++++++++++-------- .../bulk-asset-checkout-mail.blade.php | 8 +++-- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 3961a31f7c6a..2d37b71c3015 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -15,9 +15,8 @@ class BulkAssetCheckoutMail extends Mailable { use Queueable, SerializesModels; - /** - * Create a new message instance. - */ + public bool $requires_acceptance; + public function __construct( public Collection $assets, public Model $target, @@ -25,12 +24,9 @@ public function __construct( public string $checkout_at, public string $expected_checkin, ) { - // + $this->requires_acceptance = $this->requiresAcceptance(); } - /** - * Get the message envelope. - */ public function envelope(): Envelope { return new Envelope( @@ -39,24 +35,18 @@ public function envelope(): Envelope ); } - /** - * Get the message content definition. - */ public function content(): Content { return new Content( markdown: 'mail.markdown.bulk-asset-checkout-mail', with: [ 'introduction' => $this->getIntroduction(), + 'requires_acceptance' => $this->requiresAcceptance(), + 'acceptance_url' => $this->acceptanceUrl(), ], ); } - /** - * Get the attachments for the message. - * - * @return array - */ public function attachments(): array { return []; @@ -67,4 +57,20 @@ private function getIntroduction(): string // @todo: return 'The following assets have been checked out to you:'; } + + private function requiresAcceptance(): bool + { + return (bool) $this->assets->reduce( + fn($count, $asset) => $count + $asset->requireAcceptance() + ); + } + + private function acceptanceUrl() + { + if ($this->assets->count() > 1) { + return route('account.accept'); + } + + return route('account.accept.item', $this->assets->first()); + } } diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index 96517883b96e..ff119bc2512d 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -3,9 +3,11 @@ {{ $introduction }} - -Button Text - +@if ($requires_acceptance == 1) +One or more items require acceptance.
+ +**[✔ Click here to review the terms of use and accept the items]({{ $acceptance_url }})** +@endif {{ trans('mail.best_regards') }}
From 9bdd0d1d1e76c4ad3912f8b754b778dc358624c8 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 8 Oct 2025 16:05:55 -0700 Subject: [PATCH 11/85] Add admin name --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index ff119bc2512d..a7ea3ed317d0 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -3,9 +3,10 @@ {{ $introduction }} +**{{ trans('general.administrator') }}**: {{ $admin->display_name }} + @if ($requires_acceptance == 1) One or more items require acceptance.
- **[✔ Click here to review the terms of use and accept the items]({{ $acceptance_url }})** @endif From 9dcee71baf7633ba5bdd19c7aa92b745b848192d Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 8 Oct 2025 16:18:54 -0700 Subject: [PATCH 12/85] wip --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index a7ea3ed317d0..e8f1ad1dd90d 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -1,6 +1,4 @@ -# Introduction - {{ $introduction }} **{{ trans('general.administrator') }}**: {{ $admin->display_name }} From 6ed93f4a4f89211d8676290feeada7060bbb2a49 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 14:02:21 -0700 Subject: [PATCH 13/85] Add asset details --- app/Mail/BulkAssetCheckoutMail.php | 2 +- .../bulk-asset-checkout-mail.blade.php | 45 +++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 2d37b71c3015..809dff5cfd37 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -55,7 +55,7 @@ public function attachments(): array private function getIntroduction(): string { // @todo: - return 'The following assets have been checked out to you:'; + return 'Assets have been checked out to you.'; } private function requiresAcceptance(): bool diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index e8f1ad1dd90d..60b008b0caad 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -1,13 +1,52 @@ -{{ $introduction }} -**{{ trans('general.administrator') }}**: {{ $admin->display_name }} + + +{{ $introduction }} -@if ($requires_acceptance == 1) +@if ($requires_acceptance) One or more items require acceptance.
**[✔ Click here to review the terms of use and accept the items]({{ $acceptance_url }})** @endif +**{{ trans('general.administrator') }}**: {{ $admin->display_name }} + + +| | | +| ------------- | ------------- | +@foreach($assets as $asset) +| **Asset Tag** | {{ $asset->display_name }}
{{trans('mail.serial').': '.$asset->serial}} | +@if (isset($asset->model?->category)) +| **{{ trans('general.category') }}** | {{ $asset->model->category->name }} | +@endif +@if (isset($asset->manufacturer)) +| **{{ trans('general.manufacturer') }}** | {{ $asset->manufacturer->name }} | +@endif +@if (isset($asset->model)) +| **{{ trans('general.asset_model') }}** | {{ $asset->model->name }} | +@endif +@if ((isset($asset->model?->model_number))) +| **{{ trans('general.model_no') }}** | {{ $asset->model->model_number }} | +@endif +@if (isset($asset->assetstatus)) +| **{{ trans('general.status') }}** | {{ $asset->assetstatus->name }} | +@endif +|
|
| +@endforeach +
+ {{ trans('mail.best_regards') }}
{{ $snipeSettings->site_name }} From 2db4c1b2e4c2070e6f8da304fd3d2ca31ea9ef3e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 16:19:42 -0700 Subject: [PATCH 14/85] Add todo --- app/Listeners/CheckoutableListener.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index a2052dce373e..0f788b5151ba 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -437,6 +437,7 @@ private function shouldSendCheckoutEmailToUser(Model $checkoutable): bool */ if (Context::get('action') === 'bulk_asset_checkout') { + // @todo: maybe we should see if there is only one asset being checked out and allow this to proceed if it is? return false; } From e2f4a9bf9f4f759f947d5ac74386d2bfd867bae8 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 16:20:01 -0700 Subject: [PATCH 15/85] Make subject dynamic --- app/Mail/BulkAssetCheckoutMail.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 809dff5cfd37..3b9038237ee2 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -30,8 +30,7 @@ public function __construct( public function envelope(): Envelope { return new Envelope( - // @todo: translate - subject: 'Bulk Asset Checkout Mail', + subject: $this->getSubject(), ); } @@ -52,6 +51,17 @@ public function attachments(): array return []; } + private function getSubject(): string + { + if ($this->assets->count() > 1) { + // @todo: translate + return 'Assets checked out'; + } + + // @todo: translate + return trans('mail.Asset_Checkout_Notification', ['tag' => $this->assets->first()->asset_tag]); + } + private function getIntroduction(): string { // @todo: From 3c32be6181b180b1f453786df9e702c6e614a692 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 16:22:19 -0700 Subject: [PATCH 16/85] Make introduction line dynamic --- app/Mail/BulkAssetCheckoutMail.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 3b9038237ee2..70715d0ec74f 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -58,14 +58,18 @@ private function getSubject(): string return 'Assets checked out'; } - // @todo: translate return trans('mail.Asset_Checkout_Notification', ['tag' => $this->assets->first()->asset_tag]); } private function getIntroduction(): string { - // @todo: - return 'Assets have been checked out to you.'; + if ($this->assets->count() > 1) { + // @todo: translate + return 'Assets have been checked out to you.'; + } + + // @todo: translate + return 'An asset has been checked out to you.'; } private function requiresAcceptance(): bool From 062445a48e420c61dc56c68b4dbeb678ca23db2b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 16:25:32 -0700 Subject: [PATCH 17/85] Add expected checkin --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index 60b008b0caad..1d41c8c93961 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -43,6 +43,9 @@ @if (isset($asset->assetstatus)) | **{{ trans('general.status') }}** | {{ $asset->assetstatus->name }} | @endif +@if ((isset($asset->expected_checkin)) && ($asset->expected_checkin!='')) +| **{{ trans('mail.expecting_checkin_date') }}** | {{ $asset->expected_checkin }} | +@endif |
|
| @endforeach From d3a7e25b863944936fc3c35c4f42d676ad03aa94 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 16:29:59 -0700 Subject: [PATCH 18/85] Move expected checking and only show once --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index 1d41c8c93961..16c21e4d0887 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -23,6 +23,10 @@ **{{ trans('general.administrator') }}**: {{ $admin->display_name }} +@if ((isset($expected_checkin)) && ($expected_checkin!='')) +**{{ trans('mail.expecting_checkin_date') }}**: {{ Helper::getFormattedDateObject($expected_checkin, 'date', false) }} +@endif + | | | | ------------- | ------------- | @@ -43,9 +47,6 @@ @if (isset($asset->assetstatus)) | **{{ trans('general.status') }}** | {{ $asset->assetstatus->name }} | @endif -@if ((isset($asset->expected_checkin)) && ($asset->expected_checkin!='')) -| **{{ trans('mail.expecting_checkin_date') }}** | {{ $asset->expected_checkin }} | -@endif |
|
| @endforeach
From c6e2fd2cab2a1f6ca0e71c23a7a812d2a750d193 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 16:33:37 -0700 Subject: [PATCH 19/85] Add note --- app/Events/CheckoutablesCheckedOutInBulk.php | 1 + app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 1 + app/Mail/BulkAssetCheckoutMail.php | 1 + .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/app/Events/CheckoutablesCheckedOutInBulk.php b/app/Events/CheckoutablesCheckedOutInBulk.php index 6dd8ccde7a72..4b75b32145b5 100644 --- a/app/Events/CheckoutablesCheckedOutInBulk.php +++ b/app/Events/CheckoutablesCheckedOutInBulk.php @@ -18,6 +18,7 @@ public function __construct( public User $admin, public string $checkout_at, public string $expected_checkin, + public string $note, ) { } } diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index b1bed1064e9a..25b821c60c0d 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -25,6 +25,7 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void $event->admin, $event->checkout_at, $event->expected_checkin, + $event->note, )); } } diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 70715d0ec74f..14fb8466b952 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -23,6 +23,7 @@ public function __construct( public User $admin, public string $checkout_at, public string $expected_checkin, + public string $note, ) { $this->requires_acceptance = $this->requiresAcceptance(); } diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index 16c21e4d0887..cd52bb48077e 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -27,6 +27,10 @@ **{{ trans('mail.expecting_checkin_date') }}**: {{ Helper::getFormattedDateObject($expected_checkin, 'date', false) }} @endif +@if ($note) +**{{ trans('mail.additional_notes') }}**: {{ $note }} +@endif + | | | | ------------- | ------------- | From ad5bbb9b37d1db2358f99276cad2c48c17a56b43 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 16:34:18 -0700 Subject: [PATCH 20/85] Add divider --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index cd52bb48077e..330b3361aae8 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -21,6 +21,8 @@ **[✔ Click here to review the terms of use and accept the items]({{ $acceptance_url }})** @endif +
+ **{{ trans('general.administrator') }}**: {{ $admin->display_name }} @if ((isset($expected_checkin)) && ($expected_checkin!='')) From 4f1ff328adf2fb22d96321edcb4c5f3d06597c8c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 14 Oct 2025 17:10:57 -0700 Subject: [PATCH 21/85] Display eula if it is the same for all items --- app/Mail/BulkAssetCheckoutMail.php | 18 ++++++++++++++++++ .../bulk-asset-checkout-mail.blade.php | 10 ++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 14fb8466b952..e8d3cfbbfbf5 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -43,6 +43,7 @@ public function content(): Content 'introduction' => $this->getIntroduction(), 'requires_acceptance' => $this->requiresAcceptance(), 'acceptance_url' => $this->acceptanceUrl(), + 'eula' => $this->getEula(), ], ); } @@ -88,4 +89,21 @@ private function acceptanceUrl() return route('account.accept.item', $this->assets->first()); } + + private function getEula() + { + // if assets do not have the same category then return early... + $categories = $this->assets->pluck('model.category.id')->unique(); + + if ($categories->count() > 1) { + return; + } + + // if assets do have the same category then return the shared EULA + if ($categories->count() === 1) { + return $this->assets->first()->getEula(); + } + + // @todo: if the categories use the default eula then return that + } } diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index 330b3361aae8..79dfcc58e94a 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -23,8 +23,6 @@
-**{{ trans('general.administrator') }}**: {{ $admin->display_name }} - @if ((isset($expected_checkin)) && ($expected_checkin!='')) **{{ trans('mail.expecting_checkin_date') }}**: {{ Helper::getFormattedDateObject($expected_checkin, 'date', false) }} @endif @@ -33,6 +31,12 @@ **{{ trans('mail.additional_notes') }}**: {{ $note }} @endif +@if ($eula) + + {{ $eula }} + +@endif + | | | | ------------- | ------------- | @@ -57,6 +61,8 @@ @endforeach +**{{ trans('general.administrator') }}**: {{ $admin->display_name }} + {{ trans('mail.best_regards') }}
{{ $snipeSettings->site_name }} From 0e87843446e6c6dd54f4ace462bf28133451f8e1 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 16 Oct 2025 14:30:48 -0700 Subject: [PATCH 22/85] WIP: start testing --- .../Notifications/Email/BulkCheckoutEmailTest.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 67ba01d28e44..019695c15835 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -6,14 +6,23 @@ use App\Mail\BulkAssetCheckoutMail; use App\Models\Asset; use App\Models\User; +use App\Notifications\CheckoutAssetNotification; use Illuminate\Support\Facades\Mail; use Tests\TestCase; class BulkCheckoutEmailTest extends TestCase { + public static function scenarios() + { + // 'User has email address set + // 'User does not have address set' + // 'CC email is set' + // 'webhook is set' + } + public function test_email_is_sent() { - $this->markTestIncomplete(); + // $this->markTestIncomplete(); Mail::fake(); @@ -32,6 +41,8 @@ public function test_email_is_sent() 'A note here', ); + Mail::assertNotSent(CheckoutAssetNotification::class); + Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { // @todo: assert contents From 047a1197be34e4b510828b697c6f4d2ca743a478 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 20 Oct 2025 14:18:11 -0700 Subject: [PATCH 23/85] Add failing conditions --- .../Notifications/Email/BulkCheckoutEmailTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 019695c15835..9fabf5771ba3 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -22,7 +22,7 @@ public static function scenarios() public function test_email_is_sent() { - // $this->markTestIncomplete(); + $this->settings->enableAdminCC('cc@example.com'); Mail::fake(); @@ -43,10 +43,16 @@ public function test_email_is_sent() Mail::assertNotSent(CheckoutAssetNotification::class); - Mail::assertSent(BulkAssetCheckoutMail::class, 1); + Mail::assertSent(BulkAssetCheckoutMail::class, 2); + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { // @todo: assert contents return $mail->hasTo('someone@example.com'); }); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + // @todo: assert contents + return $mail->hasTo('cc@example.com'); + }); } } From b5e3358bbd8b8e6c9e02d87b25c6031389c03f3c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 20 Oct 2025 14:29:05 -0700 Subject: [PATCH 24/85] Add todos --- app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 25b821c60c0d..5391175bef5d 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -19,6 +19,7 @@ public function subscribe($events) public function handle(CheckoutablesCheckedOutInBulk $event): void { + // @todo: only send if user has email address Mail::to($event->target)->send(new BulkAssetCheckoutMail( $event->assets, $event->target, @@ -27,5 +28,8 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void $event->expected_checkin, $event->note, )); + + // @todo: create and attach acceptance? Might be handled in CheckoutableListener::getCheckoutAcceptance() already. + } } From 8ff35754426543df2f2dc1674d7c1ed0f14ada6f Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 20 Oct 2025 16:31:52 -0700 Subject: [PATCH 25/85] Add test for listener registration --- ...ckoutablesCheckedOutInBulkListenerTest.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/Unit/Listeners/CheckoutablesCheckedOutInBulkListenerTest.php diff --git a/tests/Unit/Listeners/CheckoutablesCheckedOutInBulkListenerTest.php b/tests/Unit/Listeners/CheckoutablesCheckedOutInBulkListenerTest.php new file mode 100644 index 000000000000..2cf38b2c39f1 --- /dev/null +++ b/tests/Unit/Listeners/CheckoutablesCheckedOutInBulkListenerTest.php @@ -0,0 +1,20 @@ + Date: Mon, 20 Oct 2025 16:35:27 -0700 Subject: [PATCH 26/85] Scaffold some testing changes --- .../Checkouts/Ui/BulkAssetCheckoutTest.php | 6 ++ .../CheckoutablesCheckedOutInBulkTest.php | 99 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 12a4aa264e83..07a9da7a6b58 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -36,6 +36,8 @@ public function testCanBulkCheckoutAssets() { Mail::fake(); + Event::fake(); + $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $user = User::factory()->create(['email' => 'someone@example.com']); @@ -57,6 +59,9 @@ public function testCanBulkCheckoutAssets() $assets = $assets->fresh(); + Event::assertDispatched(CheckoutablesCheckedOutInBulk::class); + + // @todo: move to another test case $assets->each(function ($asset) use ($expectedCheckin, $checkoutAt, $user) { $asset->assignedTo()->is($user); $asset->last_checkout = $checkoutAt; @@ -64,6 +69,7 @@ public function testCanBulkCheckoutAssets() $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); //Note: '$this' gets auto-bound in closures, so this does work. }); + // @todo: move to another test case Mail::assertSent(CheckoutAssetMail::class, 2); Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { return $mail->hasTo('someone@example.com'); diff --git a/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php b/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php new file mode 100644 index 000000000000..6c2a89ff1837 --- /dev/null +++ b/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php @@ -0,0 +1,99 @@ +assets = Asset::factory()->count(2)->create(); + $this->target = User::factory()->create(['email' => 'someone@example.com']); + $this->admin = User::factory()->create(); + $this->checkout_at = date('Y-m-d H:i:s'); + $this->expected_checkin = ''; + } + + public function test_action_log_entries() + { + $this->markTestIncomplete(); + + $this->dispatchEvent(); + + $this->assets->each(function ($asset) { + $asset->assignedTo()->is($this->target); + $asset->last_checkout = $this->checkout_at; + $asset->expected_checkin = $this->expected_checkin; + $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); //Note: '$this' gets auto-bound in closures, so this does work. + }); + } + + public function test_checkout_acceptance_creation() + { + $this->markTestIncomplete(); + } + + #[Group('notifications')] + public function test_emails() + { + $this->markTestIncomplete(); + + Mail::fake(); + + $this->settings->enableAdminCC('cc@example.com'); + + $this->dispatchEvent(); + + // we shouldn't send the "single" checkout mailable + Mail::assertNotSent(CheckoutAssetNotification::class); + + Mail::assertSent(BulkAssetCheckoutMail::class, 2); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + // @todo: assert contents + return $mail->hasTo('someone@example.com'); + }); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + // @todo: assert contents + return $mail->hasTo('cc@example.com'); + }); + } + + #[Group('notifications')] + public function test_webhooks() + { + $this->markTestIncomplete(); + + Notification::fake(); + } + + private function dispatchEvent(): void + { + CheckoutablesCheckedOutInBulk::dispatch( + $this->assets, + $this->target, + $this->admin, + $this->checkout_at, + $this->expected_checkin, + 'A note here', + ); + } +} From 503e6898c36018878347f4892a9b74866dabe214 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 20 Oct 2025 16:53:36 -0700 Subject: [PATCH 27/85] WIP --- .../Checkouts/Ui/BulkAssetCheckoutTest.php | 3 +-- .../Events/CheckoutablesCheckedOutInBulkTest.php | 15 +-------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 07a9da7a6b58..894abb0a88d4 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -36,7 +36,7 @@ public function testCanBulkCheckoutAssets() { Mail::fake(); - Event::fake(); + Event::fake([CheckoutablesCheckedOutInBulk::class]); $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $user = User::factory()->create(['email' => 'someone@example.com']); @@ -61,7 +61,6 @@ public function testCanBulkCheckoutAssets() Event::assertDispatched(CheckoutablesCheckedOutInBulk::class); - // @todo: move to another test case $assets->each(function ($asset) use ($expectedCheckin, $checkoutAt, $user) { $asset->assignedTo()->is($user); $asset->last_checkout = $checkoutAt; diff --git a/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php b/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php index 6c2a89ff1837..63d08943e15d 100644 --- a/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php +++ b/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php @@ -4,6 +4,7 @@ use App\Events\CheckoutablesCheckedOutInBulk; use App\Mail\BulkAssetCheckoutMail; +use App\Models\Actionlog; use App\Models\Asset; use App\Models\User; use App\Notifications\CheckoutAssetNotification; @@ -31,20 +32,6 @@ protected function setUp(): void $this->expected_checkin = ''; } - public function test_action_log_entries() - { - $this->markTestIncomplete(); - - $this->dispatchEvent(); - - $this->assets->each(function ($asset) { - $asset->assignedTo()->is($this->target); - $asset->last_checkout = $this->checkout_at; - $asset->expected_checkin = $this->expected_checkin; - $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); //Note: '$this' gets auto-bound in closures, so this does work. - }); - } - public function test_checkout_acceptance_creation() { $this->markTestIncomplete(); From 4cb748e1249246313490e38ab9bd8637b8410b16 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 21 Oct 2025 12:29:25 -0700 Subject: [PATCH 28/85] Improve test assertions --- tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 894abb0a88d4..2d690f51fa93 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -36,8 +36,6 @@ public function testCanBulkCheckoutAssets() { Mail::fake(); - Event::fake([CheckoutablesCheckedOutInBulk::class]); - $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $user = User::factory()->create(['email' => 'someone@example.com']); @@ -59,16 +57,19 @@ public function testCanBulkCheckoutAssets() $assets = $assets->fresh(); - Event::assertDispatched(CheckoutablesCheckedOutInBulk::class); - $assets->each(function ($asset) use ($expectedCheckin, $checkoutAt, $user) { $asset->assignedTo()->is($user); $asset->last_checkout = $checkoutAt; $asset->expected_checkin = $expectedCheckin; $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); //Note: '$this' gets auto-bound in closures, so this does work. + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $asset->id, + 'assigned_to_id' => $user->id, + 'qty' => 1, + ]); }); - // @todo: move to another test case Mail::assertSent(CheckoutAssetMail::class, 2); Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { return $mail->hasTo('someone@example.com'); From d276f50fdf97ba74cf62102b497b0560b42799b8 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 21 Oct 2025 12:30:27 -0700 Subject: [PATCH 29/85] Fix assertion --- tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 2d690f51fa93..3f2dc503797e 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature\Checkouts\Ui; use App\Events\CheckoutablesCheckedOutInBulk; +use App\Mail\BulkAssetCheckoutMail; use App\Mail\CheckoutAssetMail; use App\Models\Asset; use App\Models\Company; @@ -70,8 +71,8 @@ public function testCanBulkCheckoutAssets() ]); }); - Mail::assertSent(CheckoutAssetMail::class, 2); - Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + Mail::assertSent(CheckoutAssetMail::class, 0); + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo('someone@example.com'); }); } From fd66a083d611c495b1a611510b677e0a8ccaa986 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 21 Oct 2025 12:41:51 -0700 Subject: [PATCH 30/85] Fix assertion --- .../Checkouts/Ui/BulkAssetCheckoutTest.php | 2 +- .../CheckoutablesCheckedOutInBulkTest.php | 86 ------------------- 2 files changed, 1 insertion(+), 87 deletions(-) delete mode 100644 tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 3f2dc503797e..5ab45692d49c 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -71,7 +71,7 @@ public function testCanBulkCheckoutAssets() ]); }); - Mail::assertSent(CheckoutAssetMail::class, 0); + Mail::assertNotSent(CheckoutAssetMail::class); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo('someone@example.com'); }); diff --git a/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php b/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php deleted file mode 100644 index 63d08943e15d..000000000000 --- a/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php +++ /dev/null @@ -1,86 +0,0 @@ -assets = Asset::factory()->count(2)->create(); - $this->target = User::factory()->create(['email' => 'someone@example.com']); - $this->admin = User::factory()->create(); - $this->checkout_at = date('Y-m-d H:i:s'); - $this->expected_checkin = ''; - } - - public function test_checkout_acceptance_creation() - { - $this->markTestIncomplete(); - } - - #[Group('notifications')] - public function test_emails() - { - $this->markTestIncomplete(); - - Mail::fake(); - - $this->settings->enableAdminCC('cc@example.com'); - - $this->dispatchEvent(); - - // we shouldn't send the "single" checkout mailable - Mail::assertNotSent(CheckoutAssetNotification::class); - - Mail::assertSent(BulkAssetCheckoutMail::class, 2); - - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - // @todo: assert contents - return $mail->hasTo('someone@example.com'); - }); - - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - // @todo: assert contents - return $mail->hasTo('cc@example.com'); - }); - } - - #[Group('notifications')] - public function test_webhooks() - { - $this->markTestIncomplete(); - - Notification::fake(); - } - - private function dispatchEvent(): void - { - CheckoutablesCheckedOutInBulk::dispatch( - $this->assets, - $this->target, - $this->admin, - $this->checkout_at, - $this->expected_checkin, - 'A note here', - ); - } -} From 33c156be160498e212393a14fb1abf8963b4b461 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 21 Oct 2025 12:43:23 -0700 Subject: [PATCH 31/85] Add failing test --- .../Email/BulkCheckoutEmailTest.php | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 9fabf5771ba3..c6dd056cfbd8 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -4,14 +4,32 @@ use App\Events\CheckoutablesCheckedOutInBulk; use App\Mail\BulkAssetCheckoutMail; +use App\Mail\CheckoutAssetMail; use App\Models\Asset; use App\Models\User; -use App\Notifications\CheckoutAssetNotification; use Illuminate\Support\Facades\Mail; use Tests\TestCase; class BulkCheckoutEmailTest extends TestCase { + private $assets; + private $target; + private $admin; + private $checkout_at; + private $expected_checkin; + + protected function setUp(): void + { + parent::setUp(); + + $this->assets = Asset::factory()->count(2)->create(); + $this->target = User::factory()->create(['email' => 'someone@example.com']); + $this->admin = User::factory()->create(); + $this->checkout_at = date('Y-m-d H:i:s'); + $this->expected_checkin = ''; + } + + // @todo: public static function scenarios() { // 'User has email address set @@ -20,34 +38,21 @@ public static function scenarios() // 'webhook is set' } - public function test_email_is_sent() + public function test_email_is_sent_to_cc_address() { $this->settings->enableAdminCC('cc@example.com'); Mail::fake(); - $assets = Asset::factory()->count(2)->create(); - $target = User::factory()->create(['email' => 'someone@example.com']); - $admin = User::factory()->create(); - $checkout_at = date('Y-m-d H:i:s'); - $expected_checkin = ''; - - CheckoutablesCheckedOutInBulk::dispatch( - $assets, - $target, - $admin, - $checkout_at, - $expected_checkin, - 'A note here', - ); + $this->dispatchEvent(); - Mail::assertNotSent(CheckoutAssetNotification::class); + Mail::assertNotSent(CheckoutAssetMail::class); Mail::assertSent(BulkAssetCheckoutMail::class, 2); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { // @todo: assert contents - return $mail->hasTo('someone@example.com'); + return $mail->hasTo($this->target->email); }); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { @@ -55,4 +60,21 @@ public function test_email_is_sent() return $mail->hasTo('cc@example.com'); }); } + + public function test_webbook_is_sent() + { + $this->markTestIncomplete(); + } + + private function dispatchEvent(): void + { + CheckoutablesCheckedOutInBulk::dispatch( + $this->assets, + $this->target, + $this->admin, + $this->checkout_at, + $this->expected_checkin, + 'A note here', + ); + } } From 31a247b55b41044cf81ec277cf273127c8f221aa Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 21 Oct 2025 12:44:50 -0700 Subject: [PATCH 32/85] Add test case --- .../Email/BulkCheckoutEmailTest.php | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index c6dd056cfbd8..0e1ef6655f07 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -22,6 +22,8 @@ protected function setUp(): void { parent::setUp(); + Mail::fake(); + $this->assets = Asset::factory()->count(2)->create(); $this->target = User::factory()->create(['email' => 'someone@example.com']); $this->admin = User::factory()->create(); @@ -38,12 +40,26 @@ public static function scenarios() // 'webhook is set' } + public function test_email_is_sent_to_user() + { + $this->settings->disableAdminCC(); + + $this->dispatchEvent(); + + Mail::assertNotSent(CheckoutAssetMail::class); + + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + // @todo: assert contents + return $mail->hasTo($this->target->email); + }); + } + public function test_email_is_sent_to_cc_address() { $this->settings->enableAdminCC('cc@example.com'); - Mail::fake(); - $this->dispatchEvent(); Mail::assertNotSent(CheckoutAssetMail::class); From be69da0a0d587e8213a5f76bb9c8be6f3e868839 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 21 Oct 2025 13:13:45 -0700 Subject: [PATCH 33/85] Add test case --- .../Notifications/Email/BulkCheckoutEmailTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 0e1ef6655f07..a8683f021c96 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -56,6 +56,21 @@ public function test_email_is_sent_to_user() }); } + public function test_email_is_not_sent_when_user_does_not_have_email_address() + { + $this->markTestIncomplete(); + + $this->settings->disableAdminCC(); + + $this->target = User::factory()->create(['email' => null]); + + $this->dispatchEvent(); + + Mail::assertNotSent(CheckoutAssetMail::class); + + Mail::assertNotSent(BulkAssetCheckoutMail::class); + } + public function test_email_is_sent_to_cc_address() { $this->settings->enableAdminCC('cc@example.com'); From 2aee14a800addf69f64289dc3dd2a0b3538f0c70 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 21 Oct 2025 13:14:50 -0700 Subject: [PATCH 34/85] Only send mail to target if they have an email address --- .../CheckoutablesCheckedOutInBulkListener.php | 20 +++++++++---------- .../Email/BulkCheckoutEmailTest.php | 2 -- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 5391175bef5d..fa32f883bd94 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -19,17 +19,17 @@ public function subscribe($events) public function handle(CheckoutablesCheckedOutInBulk $event): void { - // @todo: only send if user has email address - Mail::to($event->target)->send(new BulkAssetCheckoutMail( - $event->assets, - $event->target, - $event->admin, - $event->checkout_at, - $event->expected_checkin, - $event->note, - )); + if ($event->target->email) { + Mail::to($event->target)->send(new BulkAssetCheckoutMail( + $event->assets, + $event->target, + $event->admin, + $event->checkout_at, + $event->expected_checkin, + $event->note, + )); + } // @todo: create and attach acceptance? Might be handled in CheckoutableListener::getCheckoutAcceptance() already. - } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index a8683f021c96..4034a4f4fa59 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -58,8 +58,6 @@ public function test_email_is_sent_to_user() public function test_email_is_not_sent_when_user_does_not_have_email_address() { - $this->markTestIncomplete(); - $this->settings->disableAdminCC(); $this->target = User::factory()->create(['email' => null]); From 41efda5f8203595cc26130f2d2bc45099ab5c2f1 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 21 Oct 2025 13:22:41 -0700 Subject: [PATCH 35/85] Add todos --- app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 2 ++ tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index fa32f883bd94..0d013293d7b1 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -30,6 +30,8 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void )); } + // @todo: check CheckoutableListener::onCheckedOut() for implementation + // @todo: create and attach acceptance? Might be handled in CheckoutableListener::getCheckoutAcceptance() already. } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 4034a4f4fa59..af655b602a80 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -36,7 +36,9 @@ public static function scenarios() { // 'User has email address set // 'User does not have address set' - // 'CC email is set' + // 'CC email is set and acceptance is not null (shouldSendEmailToAlertAddress())' + // 'CC email is set and acceptance is null (admin_cc_always setting)' + // // 'webhook is set' } From 54125d27e0e40a96c5a1ed3da5a4f9091f52cdc3 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 13:44:14 -0700 Subject: [PATCH 36/85] Add scenario --- app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 2 -- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 0d013293d7b1..65259c06dee1 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -31,7 +31,5 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void } // @todo: check CheckoutableListener::onCheckedOut() for implementation - - // @todo: create and attach acceptance? Might be handled in CheckoutableListener::getCheckoutAcceptance() already. } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index af655b602a80..b26afae593cd 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -71,6 +71,11 @@ public function test_email_is_not_sent_when_user_does_not_have_email_address() Mail::assertNotSent(BulkAssetCheckoutMail::class); } + public function test_email_is_not_sent_if_assets_do_not_require_acceptance() + { + $this->markTestIncomplete(); + } + public function test_email_is_sent_to_cc_address() { $this->settings->enableAdminCC('cc@example.com'); From 6fb2889a9243284752d6ea5c91f4599fad28a9fb Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 13:45:00 -0700 Subject: [PATCH 37/85] Clean up --- .../Notifications/Email/BulkCheckoutEmailTest.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index b26afae593cd..1773edec7b5f 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -31,17 +31,6 @@ protected function setUp(): void $this->expected_checkin = ''; } - // @todo: - public static function scenarios() - { - // 'User has email address set - // 'User does not have address set' - // 'CC email is set and acceptance is not null (shouldSendEmailToAlertAddress())' - // 'CC email is set and acceptance is null (admin_cc_always setting)' - // - // 'webhook is set' - } - public function test_email_is_sent_to_user() { $this->settings->disableAdminCC(); From abd30e551e72931ff03e5fbeac313fb26d7b3b09 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 13:46:19 -0700 Subject: [PATCH 38/85] Clean up --- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 1773edec7b5f..fa6d0d4a04a6 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -42,7 +42,6 @@ public function test_email_is_sent_to_user() Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - // @todo: assert contents return $mail->hasTo($this->target->email); }); } @@ -76,12 +75,10 @@ public function test_email_is_sent_to_cc_address() Mail::assertSent(BulkAssetCheckoutMail::class, 2); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - // @todo: assert contents return $mail->hasTo($this->target->email); }); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - // @todo: assert contents return $mail->hasTo('cc@example.com'); }); } From 0da393f950654c8998dff319eeb94ae54e2898d8 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 14:02:16 -0700 Subject: [PATCH 39/85] Populate scenario --- .../Notifications/Email/BulkCheckoutEmailTest.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index fa6d0d4a04a6..92a3cf648e7f 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -24,7 +24,9 @@ protected function setUp(): void Mail::fake(); - $this->assets = Asset::factory()->count(2)->create(); + $this->settings->disableAdminCC(); + + $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $this->target = User::factory()->create(['email' => 'someone@example.com']); $this->admin = User::factory()->create(); $this->checkout_at = date('Y-m-d H:i:s'); @@ -33,8 +35,6 @@ protected function setUp(): void public function test_email_is_sent_to_user() { - $this->settings->disableAdminCC(); - $this->dispatchEvent(); Mail::assertNotSent(CheckoutAssetMail::class); @@ -48,8 +48,6 @@ public function test_email_is_sent_to_user() public function test_email_is_not_sent_when_user_does_not_have_email_address() { - $this->settings->disableAdminCC(); - $this->target = User::factory()->create(['email' => null]); $this->dispatchEvent(); @@ -61,7 +59,12 @@ public function test_email_is_not_sent_when_user_does_not_have_email_address() public function test_email_is_not_sent_if_assets_do_not_require_acceptance() { - $this->markTestIncomplete(); + $this->assets = Asset::factory()->count(2)->create(); + + $this->dispatchEvent(); + + Mail::assertNotSent(CheckoutAssetMail::class); + Mail::assertNotSent(BulkAssetCheckoutMail::class); } public function test_email_is_sent_to_cc_address() From 67edb7d396995b1baad96107c87043543d32ae3b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 14:17:32 -0700 Subject: [PATCH 40/85] Send to alert email --- .../CheckoutablesCheckedOutInBulkListener.php | 47 ++++++++++++++++++- .../Email/BulkCheckoutEmailTest.php | 16 +++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 65259c06dee1..bd0b395b7a2b 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -4,6 +4,8 @@ use App\Events\CheckoutablesCheckedOutInBulk; use App\Mail\BulkAssetCheckoutMail; +use App\Models\Setting; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Mail; class CheckoutablesCheckedOutInBulkListener @@ -19,7 +21,10 @@ public function subscribe($events) public function handle(CheckoutablesCheckedOutInBulk $event): void { - if ($event->target->email) { + $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->assets); + $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress(); + + if ($shouldSendEmailToUser && $event->target->email) { Mail::to($event->target)->send(new BulkAssetCheckoutMail( $event->assets, $event->target, @@ -30,6 +35,44 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void )); } - // @todo: check CheckoutableListener::onCheckedOut() for implementation + if ($shouldSendEmailToAlertAddress && Setting::getSettings()->admin_cc_email) { + Mail::to(Setting::getSettings()->admin_cc_email)->send(new BulkAssetCheckoutMail( + $event->assets, + $event->target, + $event->admin, + $event->checkout_at, + $event->expected_checkin, + $event->note, + )); + } + } + + private function shouldSendCheckoutEmailToUser(Collection $assets): bool + { + // @todo: how to handle assets having eula? + + return $this->requiresAcceptance($assets); + } + + private function shouldSendEmailToAlertAddress(): bool + { + $setting = Setting::getSettings(); + + if (!$setting) { + return false; + } + + if ($setting->admin_cc_always) { + return true; + } + + return (bool) $setting->admin_cc_email; + } + + private function requiresAcceptance(Collection $assets): bool + { + return (bool) $assets->reduce( + fn($count, $asset) => $count + $asset->requireAcceptance() + ); } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 92a3cf648e7f..080f6a5d102d 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -86,6 +86,22 @@ public function test_email_is_sent_to_cc_address() }); } + public function test_email_is_sent_to_cc_address_when_admin_cc_always_enabled() + { + $this->settings->enableAdminCC('cc@example.com'); + $this->settings->enableAdminCCAlways(); + + $this->assets = Asset::factory()->count(2)->create(); + + $this->dispatchEvent(); + + Mail::assertNotSent(CheckoutAssetMail::class); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + return $mail->hasTo('cc@example.com'); + }); + } + public function test_webbook_is_sent() { $this->markTestIncomplete(); From 59037f0d831ec2cf77cb0e94b285bf91ac398866 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 14:18:45 -0700 Subject: [PATCH 41/85] Move scenario --- .../Notifications/Email/BulkCheckoutEmailTest.php | 5 ----- ...ebhookNotificationsUponBulkAssetCheckoutTest.php | 13 +++++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 080f6a5d102d..a2135c1a6f0d 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -102,11 +102,6 @@ public function test_email_is_sent_to_cc_address_when_admin_cc_always_enabled() }); } - public function test_webbook_is_sent() - { - $this->markTestIncomplete(); - } - private function dispatchEvent(): void { CheckoutablesCheckedOutInBulk::dispatch( diff --git a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php new file mode 100644 index 000000000000..e1c9eb7986ec --- /dev/null +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -0,0 +1,13 @@ +markTestIncomplete(); + } +} From 1811e061aa1db7f14a91c6f2db5ec57b2c5d3871 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 14:21:24 -0700 Subject: [PATCH 42/85] Populate scenario --- ...NotificationsUponBulkAssetCheckoutTest.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php index e1c9eb7986ec..69ec5c3a85a9 100644 --- a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -2,6 +2,10 @@ namespace Tests\Feature\Notifications\Webhooks; +use App\Events\CheckoutablesCheckedOutInBulk; +use App\Models\Asset; +use App\Models\User; +use Illuminate\Support\Facades\Notification; use Tests\TestCase; class WebhookNotificationsUponBulkAssetCheckoutTest extends TestCase @@ -9,5 +13,26 @@ class WebhookNotificationsUponBulkAssetCheckoutTest extends TestCase public function test_webbook_is_sent_upon_bulk_asset_checkout() { $this->markTestIncomplete(); + + Notification::fake(); + + $this->settings->enableSlackWebhook(); + + $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); + $target = User::factory()->create(['email' => 'someone@example.com']); + $admin = User::factory()->create(); + $checkout_at = date('Y-m-d H:i:s'); + $expected_checkin = ''; + + CheckoutablesCheckedOutInBulk::dispatch( + $assets, + $target, + $admin, + $checkout_at, + $expected_checkin, + 'A note here', + ); + + $this->assertSlackNotificationSent(BulkAssetCheckoutNotification::class); } } From fc2e35cd32d064b506cd4dfc7e8c117d670e4570 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 14:23:07 -0700 Subject: [PATCH 43/85] Improve assertions --- .../WebhookNotificationsUponBulkAssetCheckoutTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php index 69ec5c3a85a9..5f94891a7d0e 100644 --- a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -5,6 +5,7 @@ use App\Events\CheckoutablesCheckedOutInBulk; use App\Models\Asset; use App\Models\User; +use App\Notifications\CheckoutAssetNotification; use Illuminate\Support\Facades\Notification; use Tests\TestCase; @@ -33,6 +34,9 @@ public function test_webbook_is_sent_upon_bulk_asset_checkout() 'A note here', ); + Notification::assertNothingSentTo(CheckoutAssetNotification::class); + Notification::assertSentTimes(BulkAssetCheckoutNotification::class, 1); + $this->assertSlackNotificationSent(BulkAssetCheckoutNotification::class); } } From 6307337892a69e4ad1a9e393ccb0affad381aae9 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 16:09:26 -0700 Subject: [PATCH 44/85] Add scenario --- .../CheckoutablesCheckedOutInBulkListener.php | 8 ++++-- .../Email/BulkCheckoutEmailTest.php | 26 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index bd0b395b7a2b..29f943f5368d 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -22,7 +22,7 @@ public function subscribe($events) public function handle(CheckoutablesCheckedOutInBulk $event): void { $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->assets); - $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress(); + $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($event->assets); if ($shouldSendEmailToUser && $event->target->email) { Mail::to($event->target)->send(new BulkAssetCheckoutMail( @@ -54,7 +54,7 @@ private function shouldSendCheckoutEmailToUser(Collection $assets): bool return $this->requiresAcceptance($assets); } - private function shouldSendEmailToAlertAddress(): bool + private function shouldSendEmailToAlertAddress(Collection $assets): bool { $setting = Setting::getSettings(); @@ -66,6 +66,10 @@ private function shouldSendEmailToAlertAddress(): bool return true; } + if (!$this->requiresAcceptance($assets)) { + return false; + } + return (bool) $setting->admin_cc_email; } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index a2135c1a6f0d..3b938b797a45 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -16,7 +16,6 @@ class BulkCheckoutEmailTest extends TestCase private $target; private $admin; private $checkout_at; - private $expected_checkin; protected function setUp(): void { @@ -30,7 +29,6 @@ protected function setUp(): void $this->target = User::factory()->create(['email' => 'someone@example.com']); $this->admin = User::factory()->create(); $this->checkout_at = date('Y-m-d H:i:s'); - $this->expected_checkin = ''; } public function test_email_is_sent_to_user() @@ -46,18 +44,17 @@ public function test_email_is_sent_to_user() }); } - public function test_email_is_not_sent_when_user_does_not_have_email_address() + public function test_email_is_not_sent_to_user_when_user_does_not_have_email_address() { $this->target = User::factory()->create(['email' => null]); $this->dispatchEvent(); Mail::assertNotSent(CheckoutAssetMail::class); - Mail::assertNotSent(BulkAssetCheckoutMail::class); } - public function test_email_is_not_sent_if_assets_do_not_require_acceptance() + public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptance() { $this->assets = Asset::factory()->count(2)->create(); @@ -86,7 +83,20 @@ public function test_email_is_sent_to_cc_address() }); } - public function test_email_is_sent_to_cc_address_when_admin_cc_always_enabled() + public function test_email_is_not_sent_to_cc_address_when_assets_do_not_require_acceptance() + { + $this->settings->enableAdminCC('cc@example.com'); + $this->settings->disableAdminCCAlways(); + + $this->assets = Asset::factory()->count(2)->create(); + + $this->dispatchEvent(); + + Mail::assertNotSent(CheckoutAssetMail::class); + Mail::assertNotSent(BulkAssetCheckoutMail::class); + } + + public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acceptance_but_admin_cc_always_enabled() { $this->settings->enableAdminCC('cc@example.com'); $this->settings->enableAdminCCAlways(); @@ -97,6 +107,8 @@ public function test_email_is_sent_to_cc_address_when_admin_cc_always_enabled() Mail::assertNotSent(CheckoutAssetMail::class); + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo('cc@example.com'); }); @@ -109,7 +121,7 @@ private function dispatchEvent(): void $this->target, $this->admin, $this->checkout_at, - $this->expected_checkin, + '', 'A note here', ); } From 92fd121cae9426dee6d4ca12da31728308746f0d Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 16:12:35 -0700 Subject: [PATCH 45/85] Clean up --- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 3b938b797a45..de904bcd0f73 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -15,7 +15,6 @@ class BulkCheckoutEmailTest extends TestCase private $assets; private $target; private $admin; - private $checkout_at; protected function setUp(): void { @@ -28,7 +27,6 @@ protected function setUp(): void $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $this->target = User::factory()->create(['email' => 'someone@example.com']); $this->admin = User::factory()->create(); - $this->checkout_at = date('Y-m-d H:i:s'); } public function test_email_is_sent_to_user() @@ -120,7 +118,7 @@ private function dispatchEvent(): void $this->assets, $this->target, $this->admin, - $this->checkout_at, + date('Y-m-d H:i:s'), '', 'A note here', ); From e036f756d5901e0018ed6c191c51395705d7fd02 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 16:24:47 -0700 Subject: [PATCH 46/85] Improve setup --- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index de904bcd0f73..256c31c7dcab 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -23,6 +23,7 @@ protected function setUp(): void Mail::fake(); $this->settings->disableAdminCC(); + $this->settings->disableAdminCCAlways(); $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $this->target = User::factory()->create(['email' => 'someone@example.com']); From f64f4795c16a53707d5af2e54fccc5cab9a0c451 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 16:39:34 -0700 Subject: [PATCH 47/85] Send request instead of firing event --- .../Email/BulkCheckoutEmailTest.php | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 256c31c7dcab..10587e933f5c 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -27,12 +27,12 @@ protected function setUp(): void $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $this->target = User::factory()->create(['email' => 'someone@example.com']); - $this->admin = User::factory()->create(); + $this->admin = User::factory()->checkoutAssets()->viewAssets()->create(); } public function test_email_is_sent_to_user() { - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); @@ -47,7 +47,7 @@ public function test_email_is_not_sent_to_user_when_user_does_not_have_email_add { $this->target = User::factory()->create(['email' => null]); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); Mail::assertNotSent(BulkAssetCheckoutMail::class); @@ -57,7 +57,7 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan { $this->assets = Asset::factory()->count(2)->create(); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); Mail::assertNotSent(BulkAssetCheckoutMail::class); @@ -67,7 +67,7 @@ public function test_email_is_sent_to_cc_address() { $this->settings->enableAdminCC('cc@example.com'); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); @@ -89,7 +89,7 @@ public function test_email_is_not_sent_to_cc_address_when_assets_do_not_require_ $this->assets = Asset::factory()->count(2)->create(); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); Mail::assertNotSent(BulkAssetCheckoutMail::class); @@ -102,7 +102,7 @@ public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acce $this->assets = Asset::factory()->count(2)->create(); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); @@ -113,15 +113,19 @@ public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acce }); } - private function dispatchEvent(): void + private function sendRequest() { - CheckoutablesCheckedOutInBulk::dispatch( - $this->assets, - $this->target, - $this->admin, - date('Y-m-d H:i:s'), - '', - 'A note here', - ); + $this->actingAs($this->admin) + ->followingRedirects() + ->post(route('hardware.bulkcheckout.store'), [ + 'selected_assets' => $this->assets->pluck('id')->toArray(), + 'checkout_to_type' => 'user', + 'assigned_user' => $this->target->id, + 'assigned_asset' => null, + 'checkout_at' => now()->subWeek()->format('Y-m-d'), + 'expected_checkin' => now()->addWeek()->format('Y-m-d'), + 'note' => null, + ]) + ->assertOk(); } } From 60df2a17f8ebc867e4e74deda2d4ba6cf83cec7e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 22 Oct 2025 16:41:36 -0700 Subject: [PATCH 48/85] Check context when sending to alert address --- app/Listeners/CheckoutableListener.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 0f788b5151ba..ee6902bcc5f0 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -428,6 +428,7 @@ private function newMicrosoftTeamsWebhookEnabled(): bool private function shouldSendCheckoutEmailToUser(Model $checkoutable): bool { + // @todo: update comment /** * Send an email if we didn't get here from a bulk checkout * and any of the following conditions are met: @@ -458,6 +459,10 @@ private function shouldSendCheckoutEmailToUser(Model $checkoutable): bool private function shouldSendEmailToAlertAddress($acceptance = null): bool { + if (Context::get('action') === 'bulk_asset_checkout') { + return false; + } + $setting = Setting::getSettings(); if (!$setting) { From 476611b70fd85c4e6cc474a6ba173acb2f4464ac Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 23 Oct 2025 12:27:20 -0700 Subject: [PATCH 49/85] Remove redundant test --- ...ckoutablesCheckedOutInBulkListenerTest.php | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 tests/Unit/Listeners/CheckoutablesCheckedOutInBulkListenerTest.php diff --git a/tests/Unit/Listeners/CheckoutablesCheckedOutInBulkListenerTest.php b/tests/Unit/Listeners/CheckoutablesCheckedOutInBulkListenerTest.php deleted file mode 100644 index 2cf38b2c39f1..000000000000 --- a/tests/Unit/Listeners/CheckoutablesCheckedOutInBulkListenerTest.php +++ /dev/null @@ -1,20 +0,0 @@ - Date: Thu, 23 Oct 2025 12:39:36 -0700 Subject: [PATCH 50/85] Implement test --- ...NotificationsUponBulkAssetCheckoutTest.php | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php index 5f94891a7d0e..57f97fc48d8e 100644 --- a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -2,7 +2,6 @@ namespace Tests\Feature\Notifications\Webhooks; -use App\Events\CheckoutablesCheckedOutInBulk; use App\Models\Asset; use App\Models\User; use App\Notifications\CheckoutAssetNotification; @@ -13,30 +12,26 @@ class WebhookNotificationsUponBulkAssetCheckoutTest extends TestCase { public function test_webbook_is_sent_upon_bulk_asset_checkout() { - $this->markTestIncomplete(); - Notification::fake(); $this->settings->enableSlackWebhook(); $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); - $target = User::factory()->create(['email' => 'someone@example.com']); - $admin = User::factory()->create(); - $checkout_at = date('Y-m-d H:i:s'); - $expected_checkin = ''; - - CheckoutablesCheckedOutInBulk::dispatch( - $assets, - $target, - $admin, - $checkout_at, - $expected_checkin, - 'A note here', - ); - - Notification::assertNothingSentTo(CheckoutAssetNotification::class); - Notification::assertSentTimes(BulkAssetCheckoutNotification::class, 1); - $this->assertSlackNotificationSent(BulkAssetCheckoutNotification::class); + $this->actingAs(User::factory()->checkoutAssets()->viewAssets()->create()) + ->followingRedirects() + ->post(route('hardware.bulkcheckout.store'), [ + 'selected_assets' => $assets->pluck('id')->toArray(), + 'checkout_to_type' => 'user', + 'assigned_user' => User::factory()->create(['email' => 'someone@example.com'])->id, + 'assigned_asset' => null, + 'checkout_at' => now()->subWeek()->format('Y-m-d'), + 'expected_checkin' => now()->addWeek()->format('Y-m-d'), + 'note' => null, + ]) + ->assertOk(); + + $this->assertSlackNotificationSent(CheckoutAssetNotification::class); + Notification::assertSentTimes(CheckoutAssetNotification::class, 2); } } From 2612e0bbc83f46f7ad160dff1be122f7871ee645 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 23 Oct 2025 12:43:12 -0700 Subject: [PATCH 51/85] Remove unused import --- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 10587e933f5c..dccf5af9001a 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -2,7 +2,6 @@ namespace Tests\Feature\Notifications\Email; -use App\Events\CheckoutablesCheckedOutInBulk; use App\Mail\BulkAssetCheckoutMail; use App\Mail\CheckoutAssetMail; use App\Models\Asset; From 02129eeddb5cae1cb156ec937ce1a904a37c1a41 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 23 Oct 2025 13:51:53 -0700 Subject: [PATCH 52/85] Add try/catch --- .../CheckoutablesCheckedOutInBulkListener.php | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 29f943f5368d..fc3a5b4b65d8 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -5,7 +5,9 @@ use App\Events\CheckoutablesCheckedOutInBulk; use App\Mail\BulkAssetCheckoutMail; use App\Models\Setting; +use Exception; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; class CheckoutablesCheckedOutInBulkListener @@ -25,25 +27,37 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($event->assets); if ($shouldSendEmailToUser && $event->target->email) { - Mail::to($event->target)->send(new BulkAssetCheckoutMail( - $event->assets, - $event->target, - $event->admin, - $event->checkout_at, - $event->expected_checkin, - $event->note, - )); + try { + Mail::to($event->target)->send(new BulkAssetCheckoutMail( + $event->assets, + $event->target, + $event->admin, + $event->checkout_at, + $event->expected_checkin, + $event->note, + )); + + Log::info('BulkAssetCheckoutMail sent to checkout target'); + } catch (Exception $e) { + Log::debug("Exception caught during BulkAssetCheckoutMail to target: " . $e->getMessage()); + } } if ($shouldSendEmailToAlertAddress && Setting::getSettings()->admin_cc_email) { - Mail::to(Setting::getSettings()->admin_cc_email)->send(new BulkAssetCheckoutMail( - $event->assets, - $event->target, - $event->admin, - $event->checkout_at, - $event->expected_checkin, - $event->note, - )); + try { + Mail::to(Setting::getSettings()->admin_cc_email)->send(new BulkAssetCheckoutMail( + $event->assets, + $event->target, + $event->admin, + $event->checkout_at, + $event->expected_checkin, + $event->note, + )); + + Log::info('BulkAssetCheckoutMail sent to admin_cc_email'); + } catch (Exception $e) { + Log::debug("Exception caught during BulkAssetCheckoutMail to admin_cc_email: " . $e->getMessage()); + } } } @@ -79,4 +93,5 @@ private function requiresAcceptance(Collection $assets): bool fn($count, $asset) => $count + $asset->requireAcceptance() ); } + } From b85d1f184adb0557cc0fc1cc735553aeb2c06f96 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 23 Oct 2025 13:52:52 -0700 Subject: [PATCH 53/85] Remove redundant test --- .../Checkouts/Ui/BulkAssetCheckoutTest.php | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php index 5ab45692d49c..99ea17bb4aca 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -2,17 +2,14 @@ namespace Tests\Feature\Checkouts\Ui; -use App\Events\CheckoutablesCheckedOutInBulk; use App\Mail\BulkAssetCheckoutMail; use App\Mail\CheckoutAssetMail; use App\Models\Asset; use App\Models\Company; use App\Models\Location; use App\Models\User; -use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Mail; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\ExpectationFailedException; use Tests\TestCase; @@ -197,54 +194,4 @@ public function test_prevents_checkouts_of_checked_out_items($data) // ensure redirected back $response->assertRedirectToRoute('hardware.bulkcheckout.show'); } - - #[Group('notifications')] - public function test_one_email_is_sent_instead_of_multiple_individual_ones() - { - Mail::fake(); - - $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); - $target = User::factory()->create(['email' => 'someone@example.com']); - - $admin = User::factory()->checkoutAssets()->viewAssets()->create(); - $this->actingAs($admin) - ->followingRedirects() - ->post(route('hardware.bulkcheckout.store'), [ - 'selected_assets' => $assets->pluck('id')->toArray(), - 'checkout_to_type' => 'user', - 'assigned_user' => $target->id, - 'assigned_asset' => null, - 'checkout_at' => now()->subWeek()->format('Y-m-d'), - 'expected_checkin' => now()->addWeek()->format('Y-m-d'), - 'note' => null, - ]) - ->assertOk(); - - // @todo: - // $assets->fresh()->each(function ($asset) { - // $this->assertHasTheseActionLogs($asset, ['create', 'checkout']); - // }); - - // ensure individual emails are not sent. - Mail::assertSent(CheckoutAssetMail::class, 0); - - Event::assertDispatchedTimes(CheckoutablesCheckedOutInBulk::class, 1); - Event::assertDispatched(CheckoutablesCheckedOutInBulk::class, function (CheckoutablesCheckedOutInBulk $event) use ($target, $admin, $assets) { - foreach ($assets as $asset) { - if ($event->assets->doesntContain($asset)) { - return false; - } - } - - if ($target->id !== $event->target->id) { - return false; - } - - if ($admin->id !== $event->admin->id) { - return false; - } - - return true; - }); - } } From 777872d41f2e17fabfe942d36f4bef5e2d00ba28 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 23 Oct 2025 13:53:38 -0700 Subject: [PATCH 54/85] Add notification group --- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 2 ++ .../Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index dccf5af9001a..1542787295d7 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -7,8 +7,10 @@ use App\Models\Asset; use App\Models\User; use Illuminate\Support\Facades\Mail; +use PHPUnit\Framework\Attributes\Group; use Tests\TestCase; +#[Group('notifications')] class BulkCheckoutEmailTest extends TestCase { private $assets; diff --git a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php index 57f97fc48d8e..993d33b93257 100644 --- a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -6,8 +6,10 @@ use App\Models\User; use App\Notifications\CheckoutAssetNotification; use Illuminate\Support\Facades\Notification; +use PHPUnit\Framework\Attributes\Group; use Tests\TestCase; +#[Group('notifications')] class WebhookNotificationsUponBulkAssetCheckoutTest extends TestCase { public function test_webbook_is_sent_upon_bulk_asset_checkout() From 33a7de9448012f5e0f381952ecdef18c6636379b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 19 Nov 2025 13:31:30 -0800 Subject: [PATCH 55/85] Add custom fields to email --- app/Mail/BulkAssetCheckoutMail.php | 31 ++++++++++++++----- .../bulk-asset-checkout-mail.blade.php | 5 +++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index e8d3cfbbfbf5..876c4edca445 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -2,6 +2,8 @@ namespace App\Mail; +use App\Models\Asset; +use App\Models\CustomField; use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Database\Eloquent\Model; @@ -26,6 +28,8 @@ public function __construct( public string $note, ) { $this->requires_acceptance = $this->requiresAcceptance(); + + $this->loadCustomFieldsOnAssets(); } public function envelope(): Envelope @@ -74,13 +78,6 @@ private function getIntroduction(): string return 'An asset has been checked out to you.'; } - private function requiresAcceptance(): bool - { - return (bool) $this->assets->reduce( - fn($count, $asset) => $count + $asset->requireAcceptance() - ); - } - private function acceptanceUrl() { if ($this->assets->count() > 1) { @@ -106,4 +103,24 @@ private function getEula() // @todo: if the categories use the default eula then return that } + + private function loadCustomFieldsOnAssets(): void + { + $this->assets = $this->assets->map(function (Asset $asset) { + $fields = $asset->model?->fieldset?->fields->filter(function (CustomField $field) { + return $field->show_in_email && !$field->field_encrypted; + }); + + $asset->setRelation('fields', $fields); + + return $asset; + }); + } + + private function requiresAcceptance(): bool + { + return (bool) $this->assets->reduce( + fn($count, $asset) => $count + $asset->requireAcceptance() + ); + } } diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index 79dfcc58e94a..ef479d8b2200 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -57,6 +57,11 @@ @if (isset($asset->assetstatus)) | **{{ trans('general.status') }}** | {{ $asset->assetstatus->name }} | @endif +@foreach($asset->fields as $field) +@if ($asset->{ $field->db_column_name() } != '') +| **{{ $field->name }}** | {{ $asset->{ $field->db_column_name() } }} | +@endif +@endforeach |
|
| @endforeach
From 53ff367473c744bc754ba7e0df622520267db098 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 19 Nov 2025 16:31:48 -0800 Subject: [PATCH 56/85] Add failing tests --- .../Email/BulkCheckoutEmailTest.php | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 1542787295d7..e208c9d54115 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -5,6 +5,7 @@ use App\Mail\BulkAssetCheckoutMail; use App\Mail\CheckoutAssetMail; use App\Models\Asset; +use App\Models\Location; use App\Models\User; use Illuminate\Support\Facades\Mail; use PHPUnit\Framework\Attributes\Group; @@ -44,6 +45,44 @@ public function test_email_is_sent_to_user() }); } + public function test_email_is_sent_to_location_manager() + { + // todo: migrate this into a data provider? + + $manager = User::factory()->create(); + + $this->target = Location::factory()->for($manager, 'manager')->create(); + + $this->sendRequest(); + + Mail::assertNotSent(CheckoutAssetMail::class); + + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) use ($manager) { + return $mail->hasTo($manager->email); + }); + } + + public function test_email_is_sent_to_user_asset_is_checked_out_to() + { + // todo: migrate this into a data provider? + + $user = User::factory()->create(); + + $this->target = Asset::factory()->assignedToUser($user)->create(); + + $this->sendRequest(); + + Mail::assertNotSent(CheckoutAssetMail::class); + + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) use ($user) { + return $mail->hasTo($user->email); + }); + } + public function test_email_is_not_sent_to_user_when_user_does_not_have_email_address() { $this->target = User::factory()->create(['email' => null]); @@ -116,11 +155,17 @@ public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acce private function sendRequest() { + $types = [ + User::class => 'user', + Location::class => 'location', + Asset::class => 'asset', + ]; + $this->actingAs($this->admin) ->followingRedirects() ->post(route('hardware.bulkcheckout.store'), [ 'selected_assets' => $this->assets->pluck('id')->toArray(), - 'checkout_to_type' => 'user', + 'checkout_to_type' => $types[get_class($this->target)], 'assigned_user' => $this->target->id, 'assigned_asset' => null, 'checkout_at' => now()->subWeek()->format('Y-m-d'), From 333ebb88b9b035d8bd31028d91510765da92073e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 19 Nov 2025 17:08:00 -0800 Subject: [PATCH 57/85] Enable sending to manager --- .../CheckoutablesCheckedOutInBulkListener.php | 23 ++++++++++- .../Email/BulkCheckoutEmailTest.php | 40 ++++++++++++++----- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index fc3a5b4b65d8..2b141d86dd53 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -4,6 +4,8 @@ use App\Events\CheckoutablesCheckedOutInBulk; use App\Mail\BulkAssetCheckoutMail; +use App\Models\Asset; +use App\Models\Location; use App\Models\Setting; use Exception; use Illuminate\Support\Collection; @@ -26,9 +28,11 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->assets); $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($event->assets); - if ($shouldSendEmailToUser && $event->target->email) { + $notifiableUser = $this->getNotifiableUser($event); + + if ($shouldSendEmailToUser && $notifiableUser) { try { - Mail::to($event->target)->send(new BulkAssetCheckoutMail( + Mail::to($notifiableUser)->send(new BulkAssetCheckoutMail( $event->assets, $event->target, $event->admin, @@ -94,4 +98,19 @@ private function requiresAcceptance(Collection $assets): bool ); } + private function getNotifiableUser(CheckoutablesCheckedOutInBulk $event) + { + $target = $event->target; + + if ($target instanceof Asset) { + $target->load('assignedTo'); + return $target->assignedto; + } + + if ($target instanceof Location) { + return $target->manager; + } + + return $target; + } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index e208c9d54115..83315640006b 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -9,6 +9,7 @@ use App\Models\User; use Illuminate\Support\Facades\Mail; use PHPUnit\Framework\Attributes\Group; +use RuntimeException; use Tests\TestCase; #[Group('notifications')] @@ -155,23 +156,40 @@ public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acce private function sendRequest() { - $types = [ - User::class => 'user', - Location::class => 'location', - Asset::class => 'asset', - ]; - $this->actingAs($this->admin) ->followingRedirects() - ->post(route('hardware.bulkcheckout.store'), [ + ->post(route('hardware.bulkcheckout.store'), array_merge([ 'selected_assets' => $this->assets->pluck('id')->toArray(), - 'checkout_to_type' => $types[get_class($this->target)], - 'assigned_user' => $this->target->id, - 'assigned_asset' => null, 'checkout_at' => now()->subWeek()->format('Y-m-d'), 'expected_checkin' => now()->addWeek()->format('Y-m-d'), 'note' => null, - ]) + ], $this->getAssignedArray())) ->assertOk(); } + + private function getAssignedArray(): array + { + if ($this->target instanceof User) { + return [ + 'checkout_to_type' => 'user', + 'assigned_user' => $this->target->id, + ]; + } + + if ($this->target instanceof Location) { + return [ + 'checkout_to_type' => 'location', + 'assigned_location' => $this->target->id, + ]; + } + + if ($this->target instanceof Asset) { + return [ + 'checkout_to_type' => 'asset', + 'assigned_asset' => $this->target->id, + ]; + } + + throw new RuntimeException('invalid target type'); + } } From 54f065f42c5e99154d4cb5682f4b75df7de60970 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 19 Nov 2025 17:11:39 -0800 Subject: [PATCH 58/85] Improve test --- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 83315640006b..b3122d16c4b8 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -96,7 +96,7 @@ public function test_email_is_not_sent_to_user_when_user_does_not_have_email_add public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptance() { - $this->assets = Asset::factory()->count(2)->create(); + $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); $this->sendRequest(); From 2018407782abe936364b4c73f03d6de568e872ae Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 12:04:53 -0800 Subject: [PATCH 59/85] Avoid error by pre-checking if user has email address --- .../CheckoutablesCheckedOutInBulkListener.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 2b141d86dd53..59736a108416 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -7,6 +7,7 @@ use App\Models\Asset; use App\Models\Location; use App\Models\Setting; +use App\Models\User; use Exception; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -25,11 +26,11 @@ public function subscribe($events) public function handle(CheckoutablesCheckedOutInBulk $event): void { - $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->assets); - $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($event->assets); - $notifiableUser = $this->getNotifiableUser($event); + $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($notifiableUser, $event->assets); + $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($event->assets); + if ($shouldSendEmailToUser && $notifiableUser) { try { Mail::to($notifiableUser)->send(new BulkAssetCheckoutMail( @@ -65,8 +66,12 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void } } - private function shouldSendCheckoutEmailToUser(Collection $assets): bool + private function shouldSendCheckoutEmailToUser(?User $user, Collection $assets): bool { + if (!$user->email) { + return false; + } + // @todo: how to handle assets having eula? return $this->requiresAcceptance($assets); From 425e0c33df379fdf46cd6548a762142a25daabb2 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 12:55:39 -0800 Subject: [PATCH 60/85] Add tests for introduction line --- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index b3122d16c4b8..79c0f4e5a38c 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -42,7 +42,8 @@ public function test_email_is_sent_to_user() Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->target->email); + return $mail->hasTo($this->target->email) + && $mail->assertSeeInText('Assets have been checked out to you'); }); } @@ -61,7 +62,8 @@ public function test_email_is_sent_to_location_manager() Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) use ($manager) { - return $mail->hasTo($manager->email); + return $mail->hasTo($manager->email) + && $mail->assertSeeInText('items have been checked out to ' . $this->target->name); }); } From cd3678841b26a3d5b1e05be39a2bf8e3573e8c7c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 13:41:23 -0800 Subject: [PATCH 61/85] Fix intro line to locations --- app/Mail/BulkAssetCheckoutMail.php | 6 ++++++ tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 876c4edca445..45644c6a2e27 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -4,6 +4,7 @@ use App\Models\Asset; use App\Models\CustomField; +use App\Models\Location; use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Database\Eloquent\Model; @@ -69,6 +70,11 @@ private function getSubject(): string private function getIntroduction(): string { + if ($this->target instanceof Location && $this->assets->count() > 1) { + // @todo: translate + return "Assets have been checked out to {$this->target->name}."; + } + if ($this->assets->count() > 1) { // @todo: translate return 'Assets have been checked out to you.'; diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 79c0f4e5a38c..e38e19612ead 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -63,7 +63,7 @@ public function test_email_is_sent_to_location_manager() Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) use ($manager) { return $mail->hasTo($manager->email) - && $mail->assertSeeInText('items have been checked out to ' . $this->target->name); + && $mail->assertSeeInText('Assets have been checked out to ' . $this->target->name); }); } From aa014e3706ee8581a7a94a79e6f381422e3ecc1f Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 14:19:54 -0800 Subject: [PATCH 62/85] Improve wording --- app/Mail/BulkAssetCheckoutMail.php | 31 +++++++++++++++++-- .../bulk-asset-checkout-mail.blade.php | 5 +-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 45644c6a2e27..08a24e52d7b5 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -46,7 +46,8 @@ public function content(): Content markdown: 'mail.markdown.bulk-asset-checkout-mail', with: [ 'introduction' => $this->getIntroduction(), - 'requires_acceptance' => $this->requiresAcceptance(), + 'requires_acceptance' => $this->requires_acceptance, + 'requires_acceptance_wording' => $this->getRequiresAcceptanceWording(), 'acceptance_url' => $this->acceptanceUrl(), 'eula' => $this->getEula(), ], @@ -70,7 +71,12 @@ private function getSubject(): string private function getIntroduction(): string { - if ($this->target instanceof Location && $this->assets->count() > 1) { + if ($this->target instanceof Location) { + if ($this->assets->count() === 1) { + // @todo: translate + return "An asset have been checked out to {$this->target->name}."; + } + // @todo: translate return "Assets have been checked out to {$this->target->name}."; } @@ -129,4 +135,25 @@ private function requiresAcceptance(): bool fn($count, $asset) => $count + $asset->requireAcceptance() ); } + + private function getRequiresAcceptanceWording(): array + { + if (!$this->requiresAcceptance()) { + return []; + } + + if ($this->assets->count() > 1) { + return [ + // todo: translate + 'One or more items require acceptance.', + "**[✔ Click here to review the terms of use and accept the items]({$this->acceptanceUrl()})**", + ]; + } + + return [ + // todo: translate + 'The checked out item requires acceptance.', + "**[✔ Click here to review the terms of use and accept the item]({$this->acceptanceUrl()})**", + ]; + } } diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index ef479d8b2200..97f563dc8594 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -17,8 +17,9 @@ {{ $introduction }} @if ($requires_acceptance) -One or more items require acceptance.
-**[✔ Click here to review the terms of use and accept the items]({{ $acceptance_url }})** +@foreach($requires_acceptance_wording as $line) +{{ $line }}
+@endforeach @endif
From cba963110e4b2e050b12d2b9c74171b4ced5d46f Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 14:22:06 -0800 Subject: [PATCH 63/85] Remove unused import --- app/Mail/CheckoutAssetMail.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Mail/CheckoutAssetMail.php b/app/Mail/CheckoutAssetMail.php index 324c1c8f29b3..8f7c44c9f738 100644 --- a/app/Mail/CheckoutAssetMail.php +++ b/app/Mail/CheckoutAssetMail.php @@ -14,7 +14,6 @@ use Illuminate\Mail\Mailables\Attachment; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; -use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Queue\SerializesModels; class CheckoutAssetMail extends Mailable From 27291f9ee9bfc13161ca2c174efd682328b57f91 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 14:23:33 -0800 Subject: [PATCH 64/85] Add todo --- app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 59736a108416..ab1d343397a3 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -74,6 +74,11 @@ private function shouldSendCheckoutEmailToUser(?User $user, Collection $assets): // @todo: how to handle assets having eula? + // todo: add from CheckoutableListener: + // if ($this->checkoutableCategoryShouldSendEmail($checkoutable)) { + // return true; + // } + return $this->requiresAcceptance($assets); } From 87fc4a4f22e41453f4a277218b52bea5c1c5dade Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 16:01:34 -0800 Subject: [PATCH 65/85] Scaffold scenarios --- .../Notifications/Email/BulkCheckoutEmailTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index e38e19612ead..73a6a87173cf 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -106,6 +106,16 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan Mail::assertNotSent(BulkAssetCheckoutMail::class); } + public function test_email_is_sent_when_assets_do_not_require_acceptance_but_have_a_eula() + { + $this->markTestIncomplete(); + } + + public function test_email_is_sent_when_assets_do_not_require_acceptance_but_category_is_set_to_send_email() + { + $this->markTestIncomplete(); + } + public function test_email_is_sent_to_cc_address() { $this->settings->enableAdminCC('cc@example.com'); From f2158843ce7be5910567611b593b8d63b55502b9 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 16:25:37 -0800 Subject: [PATCH 66/85] Avoid attempting to loop over null --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index 97f563dc8594..dbe4723f9062 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -58,11 +58,15 @@ @if (isset($asset->assetstatus)) | **{{ trans('general.status') }}** | {{ $asset->assetstatus->name }} | @endif + +@if($asset->fields) @foreach($asset->fields as $field) @if ($asset->{ $field->db_column_name() } != '') | **{{ $field->name }}** | {{ $asset->{ $field->db_column_name() } }} | @endif @endforeach +@endif + |
|
| @endforeach From bccd65e2fc46b4614a42e39b84e5bb48853326c5 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 16:33:46 -0800 Subject: [PATCH 67/85] Add failing test --- .../CheckoutablesCheckedOutInBulkListener.php | 21 +++++++--- .../Email/BulkCheckoutEmailTest.php | 40 ++++++++++++++++++- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index ab1d343397a3..1899076b70f3 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -72,12 +72,9 @@ private function shouldSendCheckoutEmailToUser(?User $user, Collection $assets): return false; } - // @todo: how to handle assets having eula? - - // todo: add from CheckoutableListener: - // if ($this->checkoutableCategoryShouldSendEmail($checkoutable)) { - // return true; - // } + if ($this->hasAssetWithEula($assets)) { + return true; + } return $this->requiresAcceptance($assets); } @@ -101,6 +98,18 @@ private function shouldSendEmailToAlertAddress(Collection $assets): bool return (bool) $setting->admin_cc_email; } + private function hasAssetWithEula(Collection $assets): bool + { + foreach ($assets as $asset) { + // todo: this doesn't work yet + if ($asset->eula) { + return true; + } + } + + return false; + } + private function requiresAcceptance(Collection $assets): bool { return (bool) $assets->reduce( diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 73a6a87173cf..f8516f57f012 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -5,6 +5,7 @@ use App\Mail\BulkAssetCheckoutMail; use App\Mail\CheckoutAssetMail; use App\Models\Asset; +use App\Models\Category; use App\Models\Location; use App\Models\User; use Illuminate\Support\Facades\Mail; @@ -108,12 +109,49 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan public function test_email_is_sent_when_assets_do_not_require_acceptance_but_have_a_eula() { - $this->markTestIncomplete(); + // $this->markTestIncomplete(); + + $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); + + $category = Category::factory()->doesNotRequireAcceptance()->create([ + 'use_default_eula' => false, + 'eula_text' => 'Some EULA text here', + ]); + + $this->assets->first()->model->category()->associate($category)->save(); + + $this->sendRequest(); + + Mail::assertNotSent(CheckoutAssetMail::class); + + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + return $mail->hasTo($this->target->email) + && $mail->assertSeeInText('Assets have been checked out to you') + // todo: test this properly + && $mail->assertDontSeeInText('Click here to review the terms of use and accept'); + }); + } public function test_email_is_sent_when_assets_do_not_require_acceptance_but_category_is_set_to_send_email() { $this->markTestIncomplete(); + + $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); + + $this->sendRequest(); + + Mail::assertNotSent(CheckoutAssetMail::class); + + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + return $mail->hasTo($this->target->email) + && $mail->assertSeeInText('Assets have been checked out to you'); + }); + } public function test_email_is_sent_to_cc_address() From 49497996d5c5a995e6220181129385642687d446 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 16:41:39 -0800 Subject: [PATCH 68/85] Fix template --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index dbe4723f9062..d1b9ce0f20b5 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -58,7 +58,6 @@ @if (isset($asset->assetstatus)) | **{{ trans('general.status') }}** | {{ $asset->assetstatus->name }} | @endif - @if($asset->fields) @foreach($asset->fields as $field) @if ($asset->{ $field->db_column_name() } != '') @@ -66,7 +65,6 @@ @endif @endforeach @endif - |
|
| @endforeach From 428b511687818d184177bf4c130b32937d6bdb3a Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 16:41:50 -0800 Subject: [PATCH 69/85] Send if eula is set --- app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 3 +-- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 1899076b70f3..7eef9d69c9c2 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -101,8 +101,7 @@ private function shouldSendEmailToAlertAddress(Collection $assets): bool private function hasAssetWithEula(Collection $assets): bool { foreach ($assets as $asset) { - // todo: this doesn't work yet - if ($asset->eula) { + if ($asset->getEula()) { return true; } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index f8516f57f012..3776ba34da0a 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -109,8 +109,6 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan public function test_email_is_sent_when_assets_do_not_require_acceptance_but_have_a_eula() { - // $this->markTestIncomplete(); - $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); $category = Category::factory()->doesNotRequireAcceptance()->create([ @@ -129,7 +127,6 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_but_hav Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo($this->target->email) && $mail->assertSeeInText('Assets have been checked out to you') - // todo: test this properly && $mail->assertDontSeeInText('Click here to review the terms of use and accept'); }); From ee7c4ce0f3ba00d4afc965df3de5ce7316405647 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 16:47:23 -0800 Subject: [PATCH 70/85] Improve assertion --- .../Email/BulkCheckoutEmailTest.php | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 3776ba34da0a..89adf45c28f4 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -114,6 +114,7 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_but_hav $category = Category::factory()->doesNotRequireAcceptance()->create([ 'use_default_eula' => false, 'eula_text' => 'Some EULA text here', + 'checkin_email' => false, ]); $this->assets->first()->model->category()->associate($category)->save(); @@ -129,26 +130,11 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_but_hav && $mail->assertSeeInText('Assets have been checked out to you') && $mail->assertDontSeeInText('Click here to review the terms of use and accept'); }); - } public function test_email_is_sent_when_assets_do_not_require_acceptance_but_category_is_set_to_send_email() { $this->markTestIncomplete(); - - $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); - - $this->sendRequest(); - - Mail::assertNotSent(CheckoutAssetMail::class); - - Mail::assertSent(BulkAssetCheckoutMail::class, 1); - - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->target->email) - && $mail->assertSeeInText('Assets have been checked out to you'); - }); - } public function test_email_is_sent_to_cc_address() From 24e5cf81210eeb0616883c09e48ad7a72571c5c2 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 16:51:09 -0800 Subject: [PATCH 71/85] Improve readability --- database/factories/CategoryFactory.php | 15 +++++++++++++++ .../Notifications/Email/BulkCheckoutEmailTest.php | 14 +++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php index 733c52668ed4..38df7e58e4cb 100644 --- a/database/factories/CategoryFactory.php +++ b/database/factories/CategoryFactory.php @@ -214,4 +214,19 @@ public function doesNotRequireAcceptance() 'require_acceptance' => false, ]); } + + public function doesNotSendCheckinEmail() + { + return $this->state([ + 'checkin_email' => false, + ]); + } + + public function hasLocalEula() + { + return $this->state([ + 'use_default_eula' => false, + 'eula_text' => 'Some EULA text here', + ]); + } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 89adf45c28f4..a9647b2e3d4a 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -109,15 +109,15 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan public function test_email_is_sent_when_assets_do_not_require_acceptance_but_have_a_eula() { - $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); + $this->assets = Asset::factory()->count(2)->create(); - $category = Category::factory()->doesNotRequireAcceptance()->create([ - 'use_default_eula' => false, - 'eula_text' => 'Some EULA text here', - 'checkin_email' => false, - ]); + $category = Category::factory() + ->doesNotRequireAcceptance() + ->doesNotSendCheckinEmail() + ->hasLocalEula() + ->create(); - $this->assets->first()->model->category()->associate($category)->save(); + $this->assets->each(fn($asset) => $asset->model->category()->associate($category)->save()); $this->sendRequest(); From 0bca66b671220c177d2d50e3a41680ce691851e2 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 1 Dec 2025 16:58:02 -0800 Subject: [PATCH 72/85] Send email if asset has checkin_email set to true --- .../CheckoutablesCheckedOutInBulkListener.php | 15 ++++++++++++ database/factories/CategoryFactory.php | 15 ++++++++++++ .../Email/BulkCheckoutEmailTest.php | 24 +++++++++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 7eef9d69c9c2..e081d767eeec 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -76,6 +76,10 @@ private function shouldSendCheckoutEmailToUser(?User $user, Collection $assets): return true; } + if ($this->hasAssetWithCategorySettingToSendEmail($assets)) { + return true; + } + return $this->requiresAcceptance($assets); } @@ -109,6 +113,17 @@ private function hasAssetWithEula(Collection $assets): bool return false; } + private function hasAssetWithCategorySettingToSendEmail(Collection $assets): bool + { + foreach ($assets as $asset) { + if ($asset->checkin_email()) { + return true; + } + } + + return false; + } + private function requiresAcceptance(Collection $assets): bool { return (bool) $assets->reduce( diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php index 38df7e58e4cb..6ebd8e40746d 100644 --- a/database/factories/CategoryFactory.php +++ b/database/factories/CategoryFactory.php @@ -222,6 +222,13 @@ public function doesNotSendCheckinEmail() ]); } + public function sendsCheckinEmail() + { + return $this->state([ + 'checkin_email' => true, + ]); + } + public function hasLocalEula() { return $this->state([ @@ -229,4 +236,12 @@ public function hasLocalEula() 'eula_text' => 'Some EULA text here', ]); } + + public function withNoLocalOrGlobalEula() + { + return $this->state([ + 'use_default_eula' => false, + 'eula_text' => '', + ]); + } } diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index a9647b2e3d4a..9fc637228d09 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -132,9 +132,29 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_but_hav }); } - public function test_email_is_sent_when_assets_do_not_require_acceptance_but_category_is_set_to_send_email() + public function test_email_is_sent_when_assets_do_not_require_acceptance_or_have_a_eula_but_category_is_set_to_send_email() { - $this->markTestIncomplete(); + $this->assets = Asset::factory()->count(2)->create(); + + $category = Category::factory() + ->doesNotRequireAcceptance() + ->withNoLocalOrGlobalEula() + ->sendsCheckinEmail() + ->create(); + + $this->assets->each(fn($asset) => $asset->model->category()->associate($category)->save()); + + $this->sendRequest(); + + Mail::assertNotSent(CheckoutAssetMail::class); + + Mail::assertSent(BulkAssetCheckoutMail::class, 1); + + Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { + return $mail->hasTo($this->target->email) + && $mail->assertSeeInText('Assets have been checked out to you') + && $mail->assertDontSeeInText('review the terms'); + }); } public function test_email_is_sent_to_cc_address() From 7a804aa5763cea8852d0e899e6cc2a38b6cc9a1d Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 11:51:28 -0800 Subject: [PATCH 73/85] Implement test --- .../Feature/Notifications/Email/BulkCheckoutEmailTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 9fc637228d09..aeb9fe0d6576 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -101,6 +101,14 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan { $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); + $category = Category::factory() + ->doesNotRequireAcceptance() + ->doesNotSendCheckinEmail() + ->withNoLocalOrGlobalEula() + ->create(); + + $this->assets->each(fn($asset) => $asset->model->category()->associate($category)->save()); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); From 559d8cc0dbea4468f9f5cee070e686be9b04930b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 11:53:01 -0800 Subject: [PATCH 74/85] Implement test --- .../Email/BulkCheckoutEmailTest.php | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index aeb9fe0d6576..85aaa1d65d5a 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -115,6 +115,27 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan Mail::assertNotSent(BulkAssetCheckoutMail::class); } + public function test_email_is_not_sent_to_cc_address_if_assets_do_not_require_acceptance() + { + $this->settings->enableAdminCC('cc@example.com'); + $this->settings->disableAdminCCAlways(); + + $this->assets = Asset::factory()->count(2)->create(); + + $category = Category::factory() + ->doesNotRequireAcceptance() + ->doesNotSendCheckinEmail() + ->withNoLocalOrGlobalEula() + ->create(); + + $this->assets->each(fn($asset) => $asset->model->category()->associate($category)->save()); + + $this->sendRequest(); + + Mail::assertNotSent(CheckoutAssetMail::class); + Mail::assertNotSent(BulkAssetCheckoutMail::class); + } + public function test_email_is_sent_when_assets_do_not_require_acceptance_but_have_a_eula() { $this->assets = Asset::factory()->count(2)->create(); @@ -184,19 +205,6 @@ public function test_email_is_sent_to_cc_address() }); } - public function test_email_is_not_sent_to_cc_address_when_assets_do_not_require_acceptance() - { - $this->settings->enableAdminCC('cc@example.com'); - $this->settings->disableAdminCCAlways(); - - $this->assets = Asset::factory()->count(2)->create(); - - $this->sendRequest(); - - Mail::assertNotSent(CheckoutAssetMail::class); - Mail::assertNotSent(BulkAssetCheckoutMail::class); - } - public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acceptance_but_admin_cc_always_enabled() { $this->settings->enableAdminCC('cc@example.com'); From d8b95d3a205a91f74a2898696ba3d3e229696d62 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 11:53:37 -0800 Subject: [PATCH 75/85] Organization --- .../Email/BulkCheckoutEmailTest.php | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 85aaa1d65d5a..729fa48cd192 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -115,27 +115,6 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan Mail::assertNotSent(BulkAssetCheckoutMail::class); } - public function test_email_is_not_sent_to_cc_address_if_assets_do_not_require_acceptance() - { - $this->settings->enableAdminCC('cc@example.com'); - $this->settings->disableAdminCCAlways(); - - $this->assets = Asset::factory()->count(2)->create(); - - $category = Category::factory() - ->doesNotRequireAcceptance() - ->doesNotSendCheckinEmail() - ->withNoLocalOrGlobalEula() - ->create(); - - $this->assets->each(fn($asset) => $asset->model->category()->associate($category)->save()); - - $this->sendRequest(); - - Mail::assertNotSent(CheckoutAssetMail::class); - Mail::assertNotSent(BulkAssetCheckoutMail::class); - } - public function test_email_is_sent_when_assets_do_not_require_acceptance_but_have_a_eula() { $this->assets = Asset::factory()->count(2)->create(); @@ -223,6 +202,27 @@ public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acce }); } + public function test_email_is_not_sent_to_cc_address_if_assets_do_not_require_acceptance() + { + $this->settings->enableAdminCC('cc@example.com'); + $this->settings->disableAdminCCAlways(); + + $this->assets = Asset::factory()->count(2)->create(); + + $category = Category::factory() + ->doesNotRequireAcceptance() + ->doesNotSendCheckinEmail() + ->withNoLocalOrGlobalEula() + ->create(); + + $this->assets->each(fn($asset) => $asset->model->category()->associate($category)->save()); + + $this->sendRequest(); + + Mail::assertNotSent(CheckoutAssetMail::class); + Mail::assertNotSent(BulkAssetCheckoutMail::class); + } + private function sendRequest() { $this->actingAs($this->admin) From d0e73714c67cbfb8225765869122d3ee0b82630e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 11:57:02 -0800 Subject: [PATCH 76/85] Implement test --- .../Notifications/Email/BulkCheckoutEmailTest.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 729fa48cd192..cfd7513c6b84 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -184,19 +184,25 @@ public function test_email_is_sent_to_cc_address() }); } - public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acceptance_but_admin_cc_always_enabled() + public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acceptance_or_have_eula_but_admin_cc_always_enabled() { $this->settings->enableAdminCC('cc@example.com'); $this->settings->enableAdminCCAlways(); - $this->assets = Asset::factory()->count(2)->create(); + $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); + + $category = Category::factory() + ->doesNotRequireAcceptance() + ->doesNotSendCheckinEmail() + ->withNoLocalOrGlobalEula() + ->create(); + + $this->assets->each(fn($asset) => $asset->model->category()->associate($category)->save()); $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); - Mail::assertSent(BulkAssetCheckoutMail::class, 1); - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo('cc@example.com'); }); From dad650b8048604a53c09784a451ac0d41f7007d9 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 12:28:51 -0800 Subject: [PATCH 77/85] Readability --- .../Email/BulkCheckoutEmailTest.php | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index cfd7513c6b84..e2b601653512 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -231,40 +231,30 @@ public function test_email_is_not_sent_to_cc_address_if_assets_do_not_require_ac private function sendRequest() { - $this->actingAs($this->admin) - ->followingRedirects() - ->post(route('hardware.bulkcheckout.store'), array_merge([ - 'selected_assets' => $this->assets->pluck('id')->toArray(), - 'checkout_at' => now()->subWeek()->format('Y-m-d'), - 'expected_checkin' => now()->addWeek()->format('Y-m-d'), - 'note' => null, - ], $this->getAssignedArray())) - ->assertOk(); - } - - private function getAssignedArray(): array - { - if ($this->target instanceof User) { - return [ + $assigned = match (get_class($this->target)) { + User::class => [ 'checkout_to_type' => 'user', 'assigned_user' => $this->target->id, - ]; - } - - if ($this->target instanceof Location) { - return [ + ], + Location::class => [ 'checkout_to_type' => 'location', 'assigned_location' => $this->target->id, - ]; - } - - if ($this->target instanceof Asset) { - return [ + ], + Asset::class => [ 'checkout_to_type' => 'asset', 'assigned_asset' => $this->target->id, - ]; - } + ], + default => [], + }; - throw new RuntimeException('invalid target type'); + $this->actingAs($this->admin) + ->followingRedirects() + ->post(route('hardware.bulkcheckout.store'), [ + 'selected_assets' => $this->assets->pluck('id')->toArray(), + 'checkout_at' => now()->subWeek()->format('Y-m-d'), + 'expected_checkin' => now()->addWeek()->format('Y-m-d'), + 'note' => null, + ] + $assigned) + ->assertOk(); } } From e7e48c8f03dc39a6ef2a62bbb011c76a513691ba Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 12:30:16 -0800 Subject: [PATCH 78/85] Cleanups --- .../Notifications/Email/BulkCheckoutEmailTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index e2b601653512..5c8b7d832da2 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -8,6 +8,8 @@ use App\Models\Category; use App\Models\Location; use App\Models\User; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Mail; use PHPUnit\Framework\Attributes\Group; use RuntimeException; @@ -16,9 +18,8 @@ #[Group('notifications')] class BulkCheckoutEmailTest extends TestCase { - private $assets; - private $target; - private $admin; + private Collection $assets; + private Model $target; protected function setUp(): void { @@ -31,7 +32,6 @@ protected function setUp(): void $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $this->target = User::factory()->create(['email' => 'someone@example.com']); - $this->admin = User::factory()->checkoutAssets()->viewAssets()->create(); } public function test_email_is_sent_to_user() @@ -247,7 +247,7 @@ private function sendRequest() default => [], }; - $this->actingAs($this->admin) + $this->actingAs(User::factory()->checkoutAssets()->viewAssets()->create()) ->followingRedirects() ->post(route('hardware.bulkcheckout.store'), [ 'selected_assets' => $this->assets->pluck('id')->toArray(), From 2043488c67ccd39b72bf5d8baa7a5ca6b73589bc Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 12:31:04 -0800 Subject: [PATCH 79/85] Cleanups --- tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 5c8b7d832da2..085af384e1be 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -12,7 +12,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Mail; use PHPUnit\Framework\Attributes\Group; -use RuntimeException; use Tests\TestCase; #[Group('notifications')] @@ -50,8 +49,6 @@ public function test_email_is_sent_to_user() public function test_email_is_sent_to_location_manager() { - // todo: migrate this into a data provider? - $manager = User::factory()->create(); $this->target = Location::factory()->for($manager, 'manager')->create(); @@ -70,8 +67,6 @@ public function test_email_is_sent_to_location_manager() public function test_email_is_sent_to_user_asset_is_checked_out_to() { - // todo: migrate this into a data provider? - $user = User::factory()->create(); $this->target = Asset::factory()->assignedToUser($user)->create(); From 5c1290425bccf3e6f45cbfa32ba8b5a4c3c55122 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 13:28:59 -0800 Subject: [PATCH 80/85] Improve variable name --- .../Email/BulkCheckoutEmailTest.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 085af384e1be..ba0f74e8d9b5 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -18,7 +18,7 @@ class BulkCheckoutEmailTest extends TestCase { private Collection $assets; - private Model $target; + private Model $assignee; protected function setUp(): void { @@ -30,7 +30,7 @@ protected function setUp(): void $this->settings->disableAdminCCAlways(); $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); - $this->target = User::factory()->create(['email' => 'someone@example.com']); + $this->assignee = User::factory()->create(['email' => 'someone@example.com']); } public function test_email_is_sent_to_user() @@ -42,7 +42,7 @@ public function test_email_is_sent_to_user() Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->target->email) + return $mail->hasTo($this->assignee->email) && $mail->assertSeeInText('Assets have been checked out to you'); }); } @@ -51,7 +51,7 @@ public function test_email_is_sent_to_location_manager() { $manager = User::factory()->create(); - $this->target = Location::factory()->for($manager, 'manager')->create(); + $this->assignee = Location::factory()->for($manager, 'manager')->create(); $this->sendRequest(); @@ -61,7 +61,7 @@ public function test_email_is_sent_to_location_manager() Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) use ($manager) { return $mail->hasTo($manager->email) - && $mail->assertSeeInText('Assets have been checked out to ' . $this->target->name); + && $mail->assertSeeInText('Assets have been checked out to ' . $this->assignee->name); }); } @@ -69,7 +69,7 @@ public function test_email_is_sent_to_user_asset_is_checked_out_to() { $user = User::factory()->create(); - $this->target = Asset::factory()->assignedToUser($user)->create(); + $this->assignee = Asset::factory()->assignedToUser($user)->create(); $this->sendRequest(); @@ -84,7 +84,7 @@ public function test_email_is_sent_to_user_asset_is_checked_out_to() public function test_email_is_not_sent_to_user_when_user_does_not_have_email_address() { - $this->target = User::factory()->create(['email' => null]); + $this->assignee = User::factory()->create(['email' => null]); $this->sendRequest(); @@ -129,7 +129,7 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_but_hav Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->target->email) + return $mail->hasTo($this->assignee->email) && $mail->assertSeeInText('Assets have been checked out to you') && $mail->assertDontSeeInText('Click here to review the terms of use and accept'); }); @@ -154,7 +154,7 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_or_have Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->target->email) + return $mail->hasTo($this->assignee->email) && $mail->assertSeeInText('Assets have been checked out to you') && $mail->assertDontSeeInText('review the terms'); }); @@ -171,7 +171,7 @@ public function test_email_is_sent_to_cc_address() Mail::assertSent(BulkAssetCheckoutMail::class, 2); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->target->email); + return $mail->hasTo($this->assignee->email); }); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { @@ -226,18 +226,18 @@ public function test_email_is_not_sent_to_cc_address_if_assets_do_not_require_ac private function sendRequest() { - $assigned = match (get_class($this->target)) { + $assigned = match (get_class($this->assignee)) { User::class => [ 'checkout_to_type' => 'user', - 'assigned_user' => $this->target->id, + 'assigned_user' => $this->assignee->id, ], Location::class => [ 'checkout_to_type' => 'location', - 'assigned_location' => $this->target->id, + 'assigned_location' => $this->assignee->id, ], Asset::class => [ 'checkout_to_type' => 'asset', - 'assigned_asset' => $this->target->id, + 'assigned_asset' => $this->assignee->id, ], default => [], }; From d876e710e45d973d9a68c20780ad6e8f08b7e979 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 13:48:59 -0800 Subject: [PATCH 81/85] Be more specific in tests --- .../Notifications/Email/BulkCheckoutEmailTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index ba0f74e8d9b5..6dd1e1ab5ed1 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -160,9 +160,11 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_or_have }); } - public function test_email_is_sent_to_cc_address() + public function test_email_is_sent_to_cc_address_when_assets_require_acceptance() { - $this->settings->enableAdminCC('cc@example.com'); + $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); + + $this->settings->enableAdminCC('cc@example.com')->disableAdminCCAlways(); $this->sendRequest(); @@ -181,8 +183,7 @@ public function test_email_is_sent_to_cc_address() public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acceptance_or_have_eula_but_admin_cc_always_enabled() { - $this->settings->enableAdminCC('cc@example.com'); - $this->settings->enableAdminCCAlways(); + $this->settings->enableAdminCC('cc@example.com')->enableAdminCCAlways(); $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); @@ -205,10 +206,9 @@ public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acce public function test_email_is_not_sent_to_cc_address_if_assets_do_not_require_acceptance() { - $this->settings->enableAdminCC('cc@example.com'); - $this->settings->disableAdminCCAlways(); + $this->settings->enableAdminCC('cc@example.com')->disableAdminCCAlways(); - $this->assets = Asset::factory()->count(2)->create(); + $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); $category = Category::factory() ->doesNotRequireAcceptance() From ca3151ce29d1c0d221520d1d1af9ec71506ba3f4 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 16:10:53 -0800 Subject: [PATCH 82/85] Improve naming --- .../Email/BulkCheckoutEmailTest.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 6dd1e1ab5ed1..bdc5f576e6ee 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -33,7 +33,7 @@ protected function setUp(): void $this->assignee = User::factory()->create(['email' => 'someone@example.com']); } - public function test_email_is_sent_to_user() + public function test_sent_to_user() { $this->sendRequest(); @@ -47,7 +47,7 @@ public function test_email_is_sent_to_user() }); } - public function test_email_is_sent_to_location_manager() + public function test_sent_to_location_manager() { $manager = User::factory()->create(); @@ -65,7 +65,7 @@ public function test_email_is_sent_to_location_manager() }); } - public function test_email_is_sent_to_user_asset_is_checked_out_to() + public function test_sent_to_user_asset_is_checked_out_to() { $user = User::factory()->create(); @@ -82,7 +82,7 @@ public function test_email_is_sent_to_user_asset_is_checked_out_to() }); } - public function test_email_is_not_sent_to_user_when_user_does_not_have_email_address() + public function test_not_sent_to_user_when_user_does_not_have_email_address() { $this->assignee = User::factory()->create(['email' => null]); @@ -92,7 +92,7 @@ public function test_email_is_not_sent_to_user_when_user_does_not_have_email_add Mail::assertNotSent(BulkAssetCheckoutMail::class); } - public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptance() + public function test_not_sent_to_user_if_assets_do_not_require_acceptance() { $this->assets = Asset::factory()->doesNotRequireAcceptance()->count(2)->create(); @@ -110,7 +110,7 @@ public function test_email_is_not_sent_to_user_if_assets_do_not_require_acceptan Mail::assertNotSent(BulkAssetCheckoutMail::class); } - public function test_email_is_sent_when_assets_do_not_require_acceptance_but_have_a_eula() + public function test_sent_when_assets_do_not_require_acceptance_but_have_a_eula() { $this->assets = Asset::factory()->count(2)->create(); @@ -135,7 +135,7 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_but_hav }); } - public function test_email_is_sent_when_assets_do_not_require_acceptance_or_have_a_eula_but_category_is_set_to_send_email() + public function test_sent_when_assets_do_not_require_acceptance_or_have_a_eula_but_category_is_set_to_send_email() { $this->assets = Asset::factory()->count(2)->create(); @@ -160,7 +160,7 @@ public function test_email_is_sent_when_assets_do_not_require_acceptance_or_have }); } - public function test_email_is_sent_to_cc_address_when_assets_require_acceptance() + public function test_sent_to_cc_address_when_assets_require_acceptance() { $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); @@ -181,7 +181,7 @@ public function test_email_is_sent_to_cc_address_when_assets_require_acceptance( }); } - public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acceptance_or_have_eula_but_admin_cc_always_enabled() + public function test_sent_to_cc_address_when_assets_do_not_require_acceptance_or_have_eula_but_admin_cc_always_enabled() { $this->settings->enableAdminCC('cc@example.com')->enableAdminCCAlways(); @@ -204,7 +204,7 @@ public function test_email_is_sent_to_cc_address_when_assets_do_not_require_acce }); } - public function test_email_is_not_sent_to_cc_address_if_assets_do_not_require_acceptance() + public function test_not_sent_to_cc_address_if_assets_do_not_require_acceptance() { $this->settings->enableAdminCC('cc@example.com')->disableAdminCCAlways(); From 4167c6ea70aee046276a61abd587abf8cdf48799 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 17:36:05 -0800 Subject: [PATCH 83/85] Add some translations --- app/Mail/BulkAssetCheckoutMail.php | 23 ++++--------------- resources/lang/en-US/mail.php | 1 + .../bulk-asset-checkout-mail.blade.php | 2 +- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 08a24e52d7b5..e3244aa1ddb5 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -62,8 +62,7 @@ public function attachments(): array private function getSubject(): string { if ($this->assets->count() > 1) { - // @todo: translate - return 'Assets checked out'; + return ucfirst(trans('general.assets_checked_out_count')); } return trans('mail.Asset_Checkout_Notification', ['tag' => $this->assets->first()->asset_tag]); @@ -72,22 +71,10 @@ private function getSubject(): string private function getIntroduction(): string { if ($this->target instanceof Location) { - if ($this->assets->count() === 1) { - // @todo: translate - return "An asset have been checked out to {$this->target->name}."; - } - - // @todo: translate - return "Assets have been checked out to {$this->target->name}."; - } - - if ($this->assets->count() > 1) { - // @todo: translate - return 'Assets have been checked out to you.'; + return trans_choice('mail.new_item_checked_location', $this->assets->count(), ['location' => $this->target->name]); } - // @todo: translate - return 'An asset has been checked out to you.'; + return trans_choice('mail.new_item_checked', $this->assets->count()); } private function acceptanceUrl() @@ -145,14 +132,14 @@ private function getRequiresAcceptanceWording(): array if ($this->assets->count() > 1) { return [ // todo: translate - 'One or more items require acceptance.', + trans_choice('mail.items_checked_out_require_acceptance', $this->assets->count()), "**[✔ Click here to review the terms of use and accept the items]({$this->acceptanceUrl()})**", ]; } return [ // todo: translate - 'The checked out item requires acceptance.', + trans_choice('mail.items_checked_out_require_acceptance', $this->assets->count()), "**[✔ Click here to review the terms of use and accept the item]({$this->acceptanceUrl()})**", ]; } diff --git a/resources/lang/en-US/mail.php b/resources/lang/en-US/mail.php index 707390e4f0ab..92cce47145ca 100644 --- a/resources/lang/en-US/mail.php +++ b/resources/lang/en-US/mail.php @@ -79,6 +79,7 @@ 'new_item_checked' => 'A new item has been checked out under your name, details are below.|:count new items have been checked out under your name, details are below.', 'new_item_checked_with_acceptance' => 'A new item has been checked out under your name that requires acceptance, details are below.|:count new items have been checked out under your name that requires acceptance, details are below.', 'new_item_checked_location' => 'A new item has been checked out to :location, details are below.|:count new items have been checked out to :location, details are below.', + 'items_checked_out_require_acceptance' => 'The checked out item requires acceptance.|One or more items require acceptance.', 'recent_item_checked' => 'An item was recently checked out under your name that requires acceptance, details are below.', 'notes' => 'Notes', 'password' => 'Password', diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index d1b9ce0f20b5..6023c2a9f6eb 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -42,7 +42,7 @@ | | | | ------------- | ------------- | @foreach($assets as $asset) -| **Asset Tag** | {{ $asset->display_name }}
{{trans('mail.serial').': '.$asset->serial}} | +| **{{ trans('general.asset_tag') }}** | {{ $asset->display_name }}
{{trans('mail.serial').': '.$asset->serial}} | @if (isset($asset->model?->category)) | **{{ trans('general.category') }}** | {{ $asset->model->category->name }} | @endif From 8c89eb665030f940782ea436918c2480e7b86ba2 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 17:37:21 -0800 Subject: [PATCH 84/85] Avoid showing EULA --- app/Mail/BulkAssetCheckoutMail.php | 18 ------------------ .../bulk-asset-checkout-mail.blade.php | 6 ------ 2 files changed, 24 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index e3244aa1ddb5..b1a6ff528699 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -49,7 +49,6 @@ public function content(): Content 'requires_acceptance' => $this->requires_acceptance, 'requires_acceptance_wording' => $this->getRequiresAcceptanceWording(), 'acceptance_url' => $this->acceptanceUrl(), - 'eula' => $this->getEula(), ], ); } @@ -86,23 +85,6 @@ private function acceptanceUrl() return route('account.accept.item', $this->assets->first()); } - private function getEula() - { - // if assets do not have the same category then return early... - $categories = $this->assets->pluck('model.category.id')->unique(); - - if ($categories->count() > 1) { - return; - } - - // if assets do have the same category then return the shared EULA - if ($categories->count() === 1) { - return $this->assets->first()->getEula(); - } - - // @todo: if the categories use the default eula then return that - } - private function loadCustomFieldsOnAssets(): void { $this->assets = $this->assets->map(function (Asset $asset) { diff --git a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php index 6023c2a9f6eb..900a0488186a 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -32,12 +32,6 @@ **{{ trans('mail.additional_notes') }}**: {{ $note }} @endif -@if ($eula) - - {{ $eula }} - -@endif - | | | | ------------- | ------------- | From 391495dd864c6f3783f2e1a3627fad6c2e4ad068 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 2 Dec 2025 17:41:22 -0800 Subject: [PATCH 85/85] Remove some assertions --- .../Email/BulkCheckoutEmailTest.php | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index bdc5f576e6ee..9fb6f4554c34 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -42,8 +42,7 @@ public function test_sent_to_user() Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->assignee->email) - && $mail->assertSeeInText('Assets have been checked out to you'); + return $mail->hasTo($this->assignee->email); }); } @@ -60,8 +59,7 @@ public function test_sent_to_location_manager() Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) use ($manager) { - return $mail->hasTo($manager->email) - && $mail->assertSeeInText('Assets have been checked out to ' . $this->assignee->name); + return $mail->hasTo($manager->email); }); } @@ -129,9 +127,7 @@ public function test_sent_when_assets_do_not_require_acceptance_but_have_a_eula( Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->assignee->email) - && $mail->assertSeeInText('Assets have been checked out to you') - && $mail->assertDontSeeInText('Click here to review the terms of use and accept'); + return $mail->hasTo($this->assignee->email); }); } @@ -154,9 +150,7 @@ public function test_sent_when_assets_do_not_require_acceptance_or_have_a_eula_b Mail::assertSent(BulkAssetCheckoutMail::class, 1); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { - return $mail->hasTo($this->assignee->email) - && $mail->assertSeeInText('Assets have been checked out to you') - && $mail->assertDontSeeInText('review the terms'); + return $mail->hasTo($this->assignee->email); }); } @@ -245,10 +239,10 @@ private function sendRequest() $this->actingAs(User::factory()->checkoutAssets()->viewAssets()->create()) ->followingRedirects() ->post(route('hardware.bulkcheckout.store'), [ - 'selected_assets' => $this->assets->pluck('id')->toArray(), - 'checkout_at' => now()->subWeek()->format('Y-m-d'), - 'expected_checkin' => now()->addWeek()->format('Y-m-d'), - 'note' => null, + 'selected_assets' => $this->assets->pluck('id')->toArray(), + 'checkout_at' => now()->subWeek()->format('Y-m-d'), + 'expected_checkin' => now()->addWeek()->format('Y-m-d'), + 'note' => null, ] + $assigned) ->assertOk(); }