From 9e3b56f4bc00c7949a94ccf698c1cd2cb02da03b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 25 Sep 2025 15:41:37 -0700 Subject: [PATCH 001/111] 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 002/111] 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 003/111] 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 004/111] 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 005/111] 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 006/111] 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 007/111] 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 008/111] 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 009/111] 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 010/111] 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 011/111] 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 012/111] 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 013/111] 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 014/111] 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 015/111] 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 016/111] 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 017/111] 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 018/111] 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 019/111] 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 020/111] 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 021/111] 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 022/111] 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 023/111] 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 024/111] 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 025/111] 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 026/111] 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 027/111] 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 028/111] 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 029/111] 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 030/111] 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 031/111] 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 032/111] 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 033/111] 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 034/111] 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 035/111] 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 036/111] 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 037/111] 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 038/111] 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 039/111] 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 040/111] 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 041/111] 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 042/111] 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 043/111] 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 044/111] 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 045/111] 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 046/111] 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 047/111] 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 048/111] 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 049/111] 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 050/111] 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 051/111] 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 052/111] 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 053/111] 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 054/111] 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 055/111] 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 056/111] 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 057/111] 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 058/111] 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 059/111] 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 060/111] 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 061/111] 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 062/111] 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 063/111] 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 064/111] 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 065/111] 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 066/111] 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 067/111] 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 068/111] 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 069/111] 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 070/111] 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 071/111] 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 072/111] 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 073/111] 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 074/111] 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 075/111] 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 076/111] 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 077/111] 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 078/111] 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 079/111] 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 080/111] 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 081/111] 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 082/111] 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 083/111] 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 084/111] 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 085/111] 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(); } From 5153c68b8bc9059311adc2030231210f86a648af Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 3 Dec 2025 16:15:43 -0800 Subject: [PATCH 086/111] Remove old todos --- app/Listeners/CheckoutableListener.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index ee6902bcc5f0..6a56ae2f9e34 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -428,7 +428,6 @@ 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: @@ -438,7 +437,6 @@ 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 8396e27a2cd3fbd9f22300482ad4f00875026f6c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 3 Dec 2025 16:16:30 -0800 Subject: [PATCH 087/111] Revert "Avoid showing EULA" This reverts commit 8c89eb665030f940782ea436918c2480e7b86ba2. --- app/Mail/BulkAssetCheckoutMail.php | 18 ++++++++++++++++++ .../bulk-asset-checkout-mail.blade.php | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index b1a6ff528699..e3244aa1ddb5 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -49,6 +49,7 @@ public function content(): Content 'requires_acceptance' => $this->requires_acceptance, 'requires_acceptance_wording' => $this->getRequiresAcceptanceWording(), 'acceptance_url' => $this->acceptanceUrl(), + 'eula' => $this->getEula(), ], ); } @@ -85,6 +86,23 @@ 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 900a0488186a..6023c2a9f6eb 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -32,6 +32,12 @@ **{{ trans('mail.additional_notes') }}**: {{ $note }} @endif +@if ($eula) + + {{ $eula }} + +@endif + | | | | ------------- | ------------- | From 7bf7a87f8a9fbf1af72097ca54121b9b71f8ca7e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 3 Dec 2025 16:59:57 -0800 Subject: [PATCH 088/111] Begin to display EULAs for all categories --- app/Mail/BulkAssetCheckoutMail.php | 26 +++++++++++++++---- .../bulk-asset-checkout-mail.blade.php | 14 +++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index e3244aa1ddb5..4884fa7809fb 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -20,6 +20,8 @@ class BulkAssetCheckoutMail extends Mailable public bool $requires_acceptance; + public Collection $assetsByCategory; + public function __construct( public Collection $assets, public Model $target, @@ -31,6 +33,8 @@ public function __construct( $this->requires_acceptance = $this->requiresAcceptance(); $this->loadCustomFieldsOnAssets(); + $this->loadEulaOnAssets(); + $this->sortAssetsByCategory(); } public function envelope(): Envelope @@ -49,7 +53,7 @@ public function content(): Content 'requires_acceptance' => $this->requires_acceptance, 'requires_acceptance_wording' => $this->getRequiresAcceptanceWording(), 'acceptance_url' => $this->acceptanceUrl(), - 'eula' => $this->getEula(), + 'singular_eula' => $this->getSingularEula(), ], ); } @@ -86,21 +90,19 @@ private function acceptanceUrl() return route('account.accept.item', $this->assets->first()); } - private function getEula() + private function getSingularEula() { // if assets do not have the same category then return early... $categories = $this->assets->pluck('model.category.id')->unique(); if ($categories->count() > 1) { - return; + return null; } // 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 @@ -116,6 +118,20 @@ private function loadCustomFieldsOnAssets(): void }); } + private function loadEulaOnAssets(): void + { + $this->assets = $this->assets->map(function (Asset $asset) { + $asset->eula = $asset->getEula(); + + return $asset; + }); + } + + private function sortAssetsByCategory(): void + { + $this->assetsByCategory = $this->assets->groupBy(fn($asset) => $asset->model->category->id); + } + private function requiresAcceptance(): bool { return (bool) $this->assets->reduce( 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..47573f724db2 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -32,16 +32,16 @@ **{{ trans('mail.additional_notes') }}**: {{ $note }} @endif -@if ($eula) +@if ($singular_eula) - {{ $eula }} + {{ $singular_eula }} @endif - +@foreach($assetsByCategory as $group) | | | | ------------- | ------------- | -@foreach($assets as $asset) +@foreach($group as $asset) | **{{ trans('general.asset_tag') }}** | {{ $asset->display_name }}
{{trans('mail.serial').': '.$asset->serial}} | @if (isset($asset->model?->category)) | **{{ trans('general.category') }}** | {{ $asset->model->category->name }} | @@ -68,6 +68,12 @@ |
|
| @endforeach
+@if (!$singular_eula) + +{{ $group->first()->eula }} + +@endif +@endforeach **{{ trans('general.administrator') }}**: {{ $admin->display_name }} From 7f097c029a70aff474d9cb8d8d5b4d2d899637eb Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 13:52:26 -0800 Subject: [PATCH 089/111] Fix indent --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 2 +- 1 file changed, 1 insertion(+), 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 47573f724db2..3c5efc949a5f 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -34,7 +34,7 @@ @if ($singular_eula) - {{ $singular_eula }} +{{ $singular_eula }} @endif @foreach($assetsByCategory as $group) From c17e6811d2f161ab6787f00815f1407ed0357ce7 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:03:11 -0800 Subject: [PATCH 090/111] Group categories visually --- .../mail/markdown/bulk-asset-checkout-mail.blade.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 3c5efc949a5f..27d34f5510e1 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -20,9 +20,8 @@ @foreach($requires_acceptance_wording as $line) {{ $line }}
@endforeach -@endif -
+@endif @if ((isset($expected_checkin)) && ($expected_checkin!='')) **{{ trans('mail.expecting_checkin_date') }}**: {{ Helper::getFormattedDateObject($expected_checkin, 'date', false) }} @@ -38,6 +37,8 @@ @endif @foreach($assetsByCategory as $group) + +{{ $group->first()->model->category->name }} | | | | ------------- | ------------- | @@ -68,11 +69,10 @@ |
|
| @endforeach
-@if (!$singular_eula) - +@if (!$singular_eula && $group->first()->eula) {{ $group->first()->eula }} - @endif +
@endforeach **{{ trans('general.administrator') }}**: {{ $admin->display_name }} From bc5d6e89ba4c5bfaf73c37a631d53ea7ceb8b7c5 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:05:22 -0800 Subject: [PATCH 091/111] Readability --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 5 +++++ 1 file changed, 5 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 27d34f5510e1..808b6c804929 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -36,9 +36,12 @@ {{ $singular_eula }} @endif + @foreach($assetsByCategory as $group) + {{ $group->first()->model->category->name }} + | | | | ------------- | ------------- | @@ -69,9 +72,11 @@ |
|
| @endforeach
+ @if (!$singular_eula && $group->first()->eula) {{ $group->first()->eula }} @endif +
@endforeach From affc4c8bd99afaa1d15ab7b8eb0b559cabb5a3ba Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:07:10 -0800 Subject: [PATCH 092/111] Styling --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 2 +- 1 file changed, 1 insertion(+), 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 808b6c804929..becda7510cde 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -40,7 +40,7 @@ @foreach($assetsByCategory as $group) -{{ $group->first()->model->category->name }} +**{{ $group->first()->model->category->name }}** | | | From 5a4ef15de5750d4cc074202f1080429c0fde009f Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:22:13 -0800 Subject: [PATCH 093/111] Avoid rendering rule if last item in loop --- .../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 becda7510cde..58196bd948fe 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -69,7 +69,9 @@ @endif @endforeach @endif +@if(!$loop->last) |
|
| +@endif @endforeach
From dcbdc6fcb8ad5893b8905ead150038d6447797a2 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:27:53 -0800 Subject: [PATCH 094/111] WIP --- resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php | 1 + 1 file changed, 1 insertion(+) 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 58196bd948fe..44dfc12a93bf 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -76,6 +76,7 @@
@if (!$singular_eula && $group->first()->eula) +
{{ $group->first()->eula }} @endif From 2d1d90e38ce0d36e5202ee99a4555c03a0248e28 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:34:50 -0800 Subject: [PATCH 095/111] Add comment --- app/Mail/BulkAssetCheckoutMail.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 4884fa7809fb..32918fc0691a 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -92,9 +92,10 @@ private function acceptanceUrl() private function getSingularEula() { - // if assets do not have the same category then return early... + // get unique categories from all assets $categories = $this->assets->pluck('model.category.id')->unique(); + // if assets do not have the same category then return early... if ($categories->count() > 1) { return null; } From df304a894ff8ed890ea88c475734b35cd6781ce5 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:38:13 -0800 Subject: [PATCH 096/111] WIP --- app/Mail/BulkAssetCheckoutMail.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 32918fc0691a..25a8bfde5a56 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -33,8 +33,9 @@ public function __construct( $this->requires_acceptance = $this->requiresAcceptance(); $this->loadCustomFieldsOnAssets(); - $this->loadEulaOnAssets(); - $this->sortAssetsByCategory(); + $this->loadEulasOnAssets(); + + $this->assetsByCategory = $this->groupAssetsByCategory(); } public function envelope(): Envelope @@ -119,7 +120,7 @@ private function loadCustomFieldsOnAssets(): void }); } - private function loadEulaOnAssets(): void + private function loadEulasOnAssets(): void { $this->assets = $this->assets->map(function (Asset $asset) { $asset->eula = $asset->getEula(); @@ -128,9 +129,9 @@ private function loadEulaOnAssets(): void }); } - private function sortAssetsByCategory(): void + private function groupAssetsByCategory(): Collection { - $this->assetsByCategory = $this->assets->groupBy(fn($asset) => $asset->model->category->id); + return $this->assets->groupBy(fn($asset) => $asset->model->category->id); } private function requiresAcceptance(): bool From 134f374adaf2e42af9fc15f8ab29d820cdb453bc Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:39:37 -0800 Subject: [PATCH 097/111] WIP --- app/Mail/BulkAssetCheckoutMail.php | 54 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 25a8bfde5a56..7b309901c486 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -73,6 +73,33 @@ private function getSubject(): string return trans('mail.Asset_Checkout_Notification', ['tag' => $this->assets->first()->asset_tag]); } + 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 loadEulasOnAssets(): void + { + $this->assets = $this->assets->map(function (Asset $asset) { + $asset->eula = $asset->getEula(); + + return $asset; + }); + } + + private function groupAssetsByCategory(): Collection + { + return $this->assets->groupBy(fn($asset) => $asset->model->category->id); + } + private function getIntroduction(): string { if ($this->target instanceof Location) { @@ -107,33 +134,6 @@ private function getSingularEula() } } - 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 loadEulasOnAssets(): void - { - $this->assets = $this->assets->map(function (Asset $asset) { - $asset->eula = $asset->getEula(); - - return $asset; - }); - } - - private function groupAssetsByCategory(): Collection - { - return $this->assets->groupBy(fn($asset) => $asset->model->category->id); - } - private function requiresAcceptance(): bool { return (bool) $this->assets->reduce( From da790136ff8e43b59826541eff56ef14aac1b724 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 14:40:51 -0800 Subject: [PATCH 098/111] WIP --- app/Mail/BulkAssetCheckoutMail.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 7b309901c486..11370f310e49 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -53,7 +53,7 @@ public function content(): Content 'introduction' => $this->getIntroduction(), 'requires_acceptance' => $this->requires_acceptance, 'requires_acceptance_wording' => $this->getRequiresAcceptanceWording(), - 'acceptance_url' => $this->acceptanceUrl(), + 'acceptance_url' => $this->getAcceptanceUrl(), 'singular_eula' => $this->getSingularEula(), ], ); @@ -109,7 +109,7 @@ private function getIntroduction(): string return trans_choice('mail.new_item_checked', $this->assets->count()); } - private function acceptanceUrl() + private function getAcceptanceUrl() { if ($this->assets->count() > 1) { return route('account.accept'); @@ -151,14 +151,14 @@ private function getRequiresAcceptanceWording(): array return [ // todo: translate 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()})**", + "**[✔ Click here to review the terms of use and accept the items]({$this->getAcceptanceUrl()})**", ]; } return [ // todo: translate 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()})**", + "**[✔ Click here to review the terms of use and accept the item]({$this->getAcceptanceUrl()})**", ]; } } From d062cc45df891ed74e6478c964b4383338882ec1 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 15:18:24 -0800 Subject: [PATCH 099/111] Add translation --- app/Mail/BulkAssetCheckoutMail.php | 15 +++++---------- resources/lang/en-US/mail.php | 1 + 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 11370f310e49..32040c68be6c 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -147,18 +147,13 @@ private function getRequiresAcceptanceWording(): array return []; } - if ($this->assets->count() > 1) { - return [ - // todo: translate - trans_choice('mail.items_checked_out_require_acceptance', $this->assets->count()), - "**[✔ Click here to review the terms of use and accept the items]({$this->getAcceptanceUrl()})**", - ]; - } - return [ - // todo: translate trans_choice('mail.items_checked_out_require_acceptance', $this->assets->count()), - "**[✔ Click here to review the terms of use and accept the item]({$this->getAcceptanceUrl()})**", + sprintf( + '**[✔ %s](%s)**', + trans_choice('mail.click_here_to_review_terms_and_accept_item', $this->assets->count()), + $this->getAcceptanceUrl(), + ), ]; } } diff --git a/resources/lang/en-US/mail.php b/resources/lang/en-US/mail.php index 92cce47145ca..021e20911986 100644 --- a/resources/lang/en-US/mail.php +++ b/resources/lang/en-US/mail.php @@ -86,6 +86,7 @@ 'password_reset' => 'Password Reset', 'read_the_terms' => 'Please read the terms of use below.', 'read_the_terms_and_click' => 'Please read the terms of use below, and click on the link at the bottom to confirm that you read and agree to the terms of use, and have received the asset.', + 'click_here_to_review_terms_and_accept_item' => 'Click here to review the terms of use and accept the item|Click here to review the terms of use and accept the items', 'requested' => 'Requested', 'reset_link' => 'Your Password Reset Link', 'reset_password' => 'Click here to reset your password:', From 0fbf4ce4432f2bee11f0675887d0977fb589c9b2 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 17:08:48 -0800 Subject: [PATCH 100/111] Move singular eula to bottom of email --- .../mail/markdown/bulk-asset-checkout-mail.blade.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 44dfc12a93bf..d8d71e1482fa 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -31,12 +31,6 @@ **{{ trans('mail.additional_notes') }}**: {{ $note }} @endif -@if ($singular_eula) - -{{ $singular_eula }} - -@endif - @foreach($assetsByCategory as $group) @@ -83,6 +77,12 @@ @endforeach +@if ($singular_eula) + +{{ $singular_eula }} + +@endif + **{{ trans('general.administrator') }}**: {{ $admin->display_name }} {{ trans('mail.best_regards') }}
From a34ea0804df6e132844250b0326e22dabf9d183c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 4 Dec 2025 17:22:23 -0800 Subject: [PATCH 101/111] Separate out info and prompt --- app/Mail/BulkAssetCheckoutMail.php | 46 ++++++++++--------- .../bulk-asset-checkout-mail.blade.php | 10 ++-- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 32040c68be6c..536634c993ee 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -52,8 +52,8 @@ public function content(): Content with: [ 'introduction' => $this->getIntroduction(), 'requires_acceptance' => $this->requires_acceptance, - 'requires_acceptance_wording' => $this->getRequiresAcceptanceWording(), - 'acceptance_url' => $this->getAcceptanceUrl(), + 'requires_acceptance_info' => $this->getRequiresAcceptanceInfo(), + 'requires_acceptance_prompt' => $this->getRequiresAcceptancePrompt(), 'singular_eula' => $this->getSingularEula(), ], ); @@ -109,13 +109,31 @@ private function getIntroduction(): string return trans_choice('mail.new_item_checked', $this->assets->count()); } - private function getAcceptanceUrl() + private function getRequiresAcceptanceInfo(): ?string { - if ($this->assets->count() > 1) { - return route('account.accept'); + if (!$this->requiresAcceptance()) { + return null; } - return route('account.accept.item', $this->assets->first()); + return trans_choice('mail.items_checked_out_require_acceptance', $this->assets->count()); + } + + private function getRequiresAcceptancePrompt(): ?string + { + if (!$this->requiresAcceptance()) { + return null; + } + + $acceptanceUrl = $this->assets->count() === 1 + ? route('account.accept.item', $this->assets->first()) + : route('account.accept'); + + return + sprintf( + '**[✔ %s](%s)**', + trans_choice('mail.click_here_to_review_terms_and_accept_item', $this->assets->count()), + $acceptanceUrl, + ); } private function getSingularEula() @@ -140,20 +158,4 @@ private function requiresAcceptance(): bool fn($count, $asset) => $count + $asset->requireAcceptance() ); } - - private function getRequiresAcceptanceWording(): array - { - if (!$this->requiresAcceptance()) { - return []; - } - - return [ - trans_choice('mail.items_checked_out_require_acceptance', $this->assets->count()), - sprintf( - '**[✔ %s](%s)**', - trans_choice('mail.click_here_to_review_terms_and_accept_item', $this->assets->count()), - $this->getAcceptanceUrl(), - ), - ]; - } } 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 d8d71e1482fa..a603a0da06d6 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -17,9 +17,9 @@ {{ $introduction }} @if ($requires_acceptance) -@foreach($requires_acceptance_wording as $line) -{{ $line }}
-@endforeach +{{ $requires_acceptance_info }} + +{{ $requires_acceptance_prompt }}
@endif @@ -83,6 +83,10 @@ @endif +@if ($requires_acceptance) +{{ $requires_acceptance_prompt }} +@endif + **{{ trans('general.administrator') }}**: {{ $admin->display_name }} {{ trans('mail.best_regards') }}
From d50d7fd6319f3c5f584c13500ee126a3c3f3613e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 12:09:58 -0800 Subject: [PATCH 102/111] Account for null value --- app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index e081d767eeec..81b7a26c9605 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -68,7 +68,7 @@ public function handle(CheckoutablesCheckedOutInBulk $event): void private function shouldSendCheckoutEmailToUser(?User $user, Collection $assets): bool { - if (!$user->email) { + if (!$user?->email) { return false; } From 046b38e5c2740ef60c8b0b278cd4c8dead910479 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 12:10:25 -0800 Subject: [PATCH 103/111] Improve method name --- app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 81b7a26c9605..b5760c4bbbff 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -80,7 +80,7 @@ private function shouldSendCheckoutEmailToUser(?User $user, Collection $assets): return true; } - return $this->requiresAcceptance($assets); + return $this->hasAssetThatRequiresAcceptance($assets); } private function shouldSendEmailToAlertAddress(Collection $assets): bool @@ -95,7 +95,7 @@ private function shouldSendEmailToAlertAddress(Collection $assets): bool return true; } - if (!$this->requiresAcceptance($assets)) { + if (!$this->hasAssetThatRequiresAcceptance($assets)) { return false; } @@ -124,7 +124,7 @@ private function hasAssetWithCategorySettingToSendEmail(Collection $assets): boo return false; } - private function requiresAcceptance(Collection $assets): bool + private function hasAssetThatRequiresAcceptance(Collection $assets): bool { return (bool) $assets->reduce( fn($count, $asset) => $count + $asset->requireAcceptance() From 8cb2ef7cac31641ef0812e4559b8f7576d9b1dba Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 12:17:04 -0800 Subject: [PATCH 104/111] Add typehint --- app/Listeners/CheckoutablesCheckedOutInBulkListener.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index b5760c4bbbff..4f197c22698b 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -9,6 +9,7 @@ use App\Models\Setting; use App\Models\User; use Exception; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; @@ -131,7 +132,7 @@ private function hasAssetThatRequiresAcceptance(Collection $assets): bool ); } - private function getNotifiableUser(CheckoutablesCheckedOutInBulk $event) + private function getNotifiableUser(CheckoutablesCheckedOutInBulk $event): ?Model { $target = $event->target; From 98c343b4380dff675b069406839b151a0d023322 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 13:48:22 -0800 Subject: [PATCH 105/111] Improve method ordering --- database/factories/CategoryFactory.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php index 6ebd8e40746d..eb0d8b828bbd 100644 --- a/database/factories/CategoryFactory.php +++ b/database/factories/CategoryFactory.php @@ -215,17 +215,17 @@ public function doesNotRequireAcceptance() ]); } - public function doesNotSendCheckinEmail() + public function sendsCheckinEmail() { return $this->state([ - 'checkin_email' => false, + 'checkin_email' => true, ]); } - public function sendsCheckinEmail() + public function doesNotSendCheckinEmail() { return $this->state([ - 'checkin_email' => true, + 'checkin_email' => false, ]); } From 0cc346259b340ac9d1516e051ad087724e510530 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 13:51:30 -0800 Subject: [PATCH 106/111] Use foreach instead of reduce --- .../CheckoutablesCheckedOutInBulkListener.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php index 4f197c22698b..c1ff57d91132 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -127,9 +127,13 @@ private function hasAssetWithCategorySettingToSendEmail(Collection $assets): boo private function hasAssetThatRequiresAcceptance(Collection $assets): bool { - return (bool) $assets->reduce( - fn($count, $asset) => $count + $asset->requireAcceptance() - ); + foreach ($assets as $asset) { + if ($asset->requireAcceptance()) { + return true; + } + } + + return false; } private function getNotifiableUser(CheckoutablesCheckedOutInBulk $event): ?Model From 90541ba349e57de169b28d19aa64e57ec8023740 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 13:57:38 -0800 Subject: [PATCH 107/111] Use foreach instead of reduce --- 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 536634c993ee..5887e56141fd 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -154,8 +154,12 @@ private function getSingularEula() private function requiresAcceptance(): bool { - return (bool) $this->assets->reduce( - fn($count, $asset) => $count + $asset->requireAcceptance() - ); + foreach ($this->assets as $asset) { + if ($asset->requireAcceptance()) { + return true; + } + } + + return false; } } From b06a0c5d830729e212fc8a349567390c3198e61d Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 13:59:07 -0800 Subject: [PATCH 108/111] Use value already computed --- app/Mail/BulkAssetCheckoutMail.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 5887e56141fd..9a4ccee85c9c 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -111,7 +111,7 @@ private function getIntroduction(): string private function getRequiresAcceptanceInfo(): ?string { - if (!$this->requiresAcceptance()) { + if (!$this->requires_acceptance) { return null; } @@ -120,7 +120,7 @@ private function getRequiresAcceptanceInfo(): ?string private function getRequiresAcceptancePrompt(): ?string { - if (!$this->requiresAcceptance()) { + if (!$this->requires_acceptance) { return null; } From 9eeb9167963b34b860d55fce1783ce77b7f81fa3 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 14:04:18 -0800 Subject: [PATCH 109/111] Improve clarity in test --- .../WebhookNotificationsUponBulkAssetCheckoutTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php index 993d33b93257..ebd84acca066 100644 --- a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -18,14 +18,14 @@ public function test_webbook_is_sent_upon_bulk_asset_checkout() $this->settings->enableSlackWebhook(); - $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); + $assets = Asset::factory()->count(2)->create(); $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_user' => User::factory()->create()->id, 'assigned_asset' => null, 'checkout_at' => now()->subWeek()->format('Y-m-d'), 'expected_checkin' => now()->addWeek()->format('Y-m-d'), From def04017e063a76ad77d490a907434f5cf86d057 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 8 Dec 2025 14:10:44 -0800 Subject: [PATCH 110/111] Improve readability in tests --- .../Email/BulkCheckoutEmailTest.php | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php index 9fb6f4554c34..4a1658e9fb12 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -30,17 +30,16 @@ protected function setUp(): void $this->settings->disableAdminCCAlways(); $this->assets = Asset::factory()->requiresAcceptance()->count(2)->create(); - $this->assignee = User::factory()->create(['email' => 'someone@example.com']); + $this->assignee = User::factory()->create(); } public function test_sent_to_user() { $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertSent(BulkAssetCheckoutMail::class, 1); - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo($this->assignee->email); }); @@ -54,10 +53,9 @@ public function test_sent_to_location_manager() $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertSent(BulkAssetCheckoutMail::class, 1); - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) use ($manager) { return $mail->hasTo($manager->email); }); @@ -71,10 +69,9 @@ public function test_sent_to_user_asset_is_checked_out_to() $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertSent(BulkAssetCheckoutMail::class, 1); - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) use ($user) { return $mail->hasTo($user->email); }); @@ -86,7 +83,7 @@ public function test_not_sent_to_user_when_user_does_not_have_email_address() $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertNotSent(BulkAssetCheckoutMail::class); } @@ -104,7 +101,7 @@ public function test_not_sent_to_user_if_assets_do_not_require_acceptance() $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertNotSent(BulkAssetCheckoutMail::class); } @@ -122,10 +119,9 @@ public function test_sent_when_assets_do_not_require_acceptance_but_have_a_eula( $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertSent(BulkAssetCheckoutMail::class, 1); - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo($this->assignee->email); }); @@ -145,10 +141,9 @@ public function test_sent_when_assets_do_not_require_acceptance_or_have_a_eula_b $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertSent(BulkAssetCheckoutMail::class, 1); - Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo($this->assignee->email); }); @@ -162,7 +157,7 @@ public function test_sent_to_cc_address_when_assets_require_acceptance() $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertSent(BulkAssetCheckoutMail::class, 2); @@ -191,7 +186,7 @@ public function test_sent_to_cc_address_when_assets_do_not_require_acceptance_or $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertSent(BulkAssetCheckoutMail::class, function (BulkAssetCheckoutMail $mail) { return $mail->hasTo('cc@example.com'); @@ -214,7 +209,7 @@ public function test_not_sent_to_cc_address_if_assets_do_not_require_acceptance( $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertNotSent(BulkAssetCheckoutMail::class); } @@ -233,6 +228,7 @@ private function sendRequest() 'checkout_to_type' => 'asset', 'assigned_asset' => $this->assignee->id, ], + // we shouldn't get here... default => [], }; @@ -246,4 +242,11 @@ private function sendRequest() ] + $assigned) ->assertOk(); } + + private function assertSingularCheckoutEmailNotSent(): static + { + Mail::assertNotSent(CheckoutAssetMail::class); + + return $this; + } } From 5cd92dd794d33b08a02e563cb05e92586a904956 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 5 Jan 2026 15:13:27 -0800 Subject: [PATCH 111/111] Remove redundant display of "Category" --- .../views/mail/markdown/bulk-asset-checkout-mail.blade.php | 3 --- 1 file changed, 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 a603a0da06d6..dfd33815b924 100644 --- a/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php +++ b/resources/views/mail/markdown/bulk-asset-checkout-mail.blade.php @@ -41,9 +41,6 @@ | ------------- | ------------- | @foreach($group as $asset) | **{{ 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 @if (isset($asset->manufacturer)) | **{{ trans('general.manufacturer') }}** | {{ $asset->manufacturer->name }} | @endif