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 6f6f250b90..51d945cb2f 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -187,4 +187,31 @@ class BulkAssetCheckoutTest extends TestCase // 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 cb4fa5fa5b..30047438e1 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -12,6 +12,7 @@ use App\Models\Setting; 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 @@ class BulkAssetsController extends Controller */ 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 908dd58dfd..a2052dce37 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -33,6 +33,7 @@ use App\Notifications\CheckoutConsumableNotification; 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 @@ class CheckoutableListener 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 0000000000..6dd8ccde7a --- /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 0000000000..11ccebfa56 --- /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 0000000000..de9a155b34 --- /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 51d945cb2f..797fa0651c 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 @@ class BulkAssetCheckoutTest extends TestCase $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 @@ class BulkAssetCheckoutTest extends TestCase '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 797fa0651c..b422e09aa4 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -199,14 +199,15 @@ class BulkAssetCheckoutTest extends TestCase 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 @@ class BulkAssetCheckoutTest extends TestCase 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 0000000000..6a1d7528cb --- /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 1f08b445c9..9143cdc8f5 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 b422e09aa4..12a4aa264e 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 @@ class BulkAssetCheckoutTest extends TestCase #[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 @@ class BulkAssetCheckoutTest extends TestCase // $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 @@ class BulkAssetCheckoutTest extends TestCase 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 0000000000..67ba01d28e --- /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 6a1d7528cb..b1bed1064e 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 @@ class CheckoutablesCheckedOutInBulkListener 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 11ccebfa56..fe6f375f7d 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 fe6f375f7d..df54d708e9 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -34,6 +34,7 @@ class BulkAssetCheckoutMail extends Mailable public function envelope(): Envelope { return new Envelope( + // @todo: translate subject: 'Bulk Asset Checkout Mail', ); } @@ -45,6 +46,9 @@ class BulkAssetCheckoutMail extends Mailable { return new Content( markdown: 'mail.bulk-asset-checkout-mail', + with: [ + 'introduction' => $this->getIntroduction(), + ], ); } @@ -57,4 +61,10 @@ class BulkAssetCheckoutMail extends Mailable { 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 de9a155b34..aed6551019 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 df54d708e9..3961a31f7c 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -45,7 +45,7 @@ class BulkAssetCheckoutMail extends Mailable 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 aed6551019..96517883b9 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 3961a31f7c..2d37b71c30 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 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable ); } - /** - * 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 @@ class BulkAssetCheckoutMail extends Mailable // @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 96517883b9..ff119bc251 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 ff119bc251..a7ea3ed317 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 a7ea3ed317..e8f1ad1dd9 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 2d37b71c30..809dff5cfd 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -55,7 +55,7 @@ class BulkAssetCheckoutMail extends Mailable 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 e8f1ad1dd9..60b008b0ca 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 }} - -@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 a2052dce37..0f788b5151 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -437,6 +437,7 @@ class CheckoutableListener */ 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 809dff5cfd..3b9038237e 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -30,8 +30,7 @@ class BulkAssetCheckoutMail extends Mailable public function envelope(): Envelope { return new Envelope( - // @todo: translate - subject: 'Bulk Asset Checkout Mail', + subject: $this->getSubject(), ); } @@ -52,6 +51,17 @@ class BulkAssetCheckoutMail extends Mailable 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 3b9038237e..70715d0ec7 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -58,14 +58,18 @@ class BulkAssetCheckoutMail extends Mailable 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 60b008b0ca..1d41c8c939 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 @@ One or more items require acceptance.
@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 1d41c8c939..16c21e4d08 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 @@ One or more items require acceptance.
**{{ 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 @@ One or more items require acceptance.
@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 6dd8ccde7a..4b75b32145 100644 --- a/app/Events/CheckoutablesCheckedOutInBulk.php +++ b/app/Events/CheckoutablesCheckedOutInBulk.php @@ -18,6 +18,7 @@ class CheckoutablesCheckedOutInBulk 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 b1bed1064e..25b821c60c 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -25,6 +25,7 @@ class CheckoutablesCheckedOutInBulkListener $event->admin, $event->checkout_at, $event->expected_checkin, + $event->note, )); } } diff --git a/app/Mail/BulkAssetCheckoutMail.php b/app/Mail/BulkAssetCheckoutMail.php index 70715d0ec7..14fb8466b9 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -23,6 +23,7 @@ class BulkAssetCheckoutMail extends Mailable 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 16c21e4d08..cd52bb4807 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 @@ One or more items require acceptance.
**{{ 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 cd52bb4807..330b3361aa 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 @@ 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 }} @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 14fb8466b9..e8d3cfbbfb 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -43,6 +43,7 @@ class BulkAssetCheckoutMail extends Mailable 'introduction' => $this->getIntroduction(), 'requires_acceptance' => $this->requiresAcceptance(), 'acceptance_url' => $this->acceptanceUrl(), + 'eula' => $this->getEula(), ], ); } @@ -88,4 +89,21 @@ class BulkAssetCheckoutMail extends Mailable 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 330b3361aa..79dfcc58e9 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 @@ One or more items require acceptance.

-**{{ 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 @@ One or more items require acceptance.
**{{ trans('mail.additional_notes') }}**: {{ $note }} @endif +@if ($eula) + + {{ $eula }} + +@endif + | | | | ------------- | ------------- | @@ -57,6 +61,8 @@ One or more items require acceptance.
@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 67ba01d28e..019695c158 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -6,14 +6,23 @@ use App\Events\CheckoutablesCheckedOutInBulk; 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 @@ class BulkCheckoutEmailTest extends TestCase '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 019695c158..9fabf5771b 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -22,7 +22,7 @@ class BulkCheckoutEmailTest extends TestCase public function test_email_is_sent() { - // $this->markTestIncomplete(); + $this->settings->enableAdminCC('cc@example.com'); Mail::fake(); @@ -43,10 +43,16 @@ class BulkCheckoutEmailTest extends TestCase 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 25b821c60c..5391175bef 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -19,6 +19,7 @@ class CheckoutablesCheckedOutInBulkListener 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 @@ class CheckoutablesCheckedOutInBulkListener $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 0000000000..2cf38b2c39 --- /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 12a4aa264e..07a9da7a6b 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -36,6 +36,8 @@ class BulkAssetCheckoutTest extends TestCase { Mail::fake(); + Event::fake(); + $assets = Asset::factory()->requiresAcceptance()->count(2)->create(); $user = User::factory()->create(['email' => 'someone@example.com']); @@ -57,6 +59,9 @@ class BulkAssetCheckoutTest extends TestCase $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 @@ class BulkAssetCheckoutTest extends TestCase $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 0000000000..6c2a89ff18 --- /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 07a9da7a6b..894abb0a88 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -36,7 +36,7 @@ class BulkAssetCheckoutTest extends TestCase { 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 @@ class BulkAssetCheckoutTest extends TestCase 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 6c2a89ff18..63d08943e1 100644 --- a/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php +++ b/tests/Unit/Events/CheckoutablesCheckedOutInBulkTest.php @@ -4,6 +4,7 @@ namespace Tests\Unit\Events; 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 @@ class CheckoutablesCheckedOutInBulkTest extends TestCase $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 894abb0a88..2d690f51fa 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -36,8 +36,6 @@ class BulkAssetCheckoutTest extends TestCase { 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 @@ class BulkAssetCheckoutTest extends TestCase $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 2d690f51fa..3f2dc50379 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 @@ class BulkAssetCheckoutTest extends TestCase ]); }); - 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 3f2dc50379..5ab45692d4 100644 --- a/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php @@ -71,7 +71,7 @@ class BulkAssetCheckoutTest extends TestCase ]); }); - 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 63d08943e1..0000000000 --- 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 9fabf5771b..c6dd056cfb 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -4,14 +4,32 @@ namespace Tests\Feature\Notifications\Email; 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 @@ class BulkCheckoutEmailTest extends TestCase // '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 = ''; + $this->dispatchEvent(); - CheckoutablesCheckedOutInBulk::dispatch( - $assets, - $target, - $admin, - $checkout_at, - $expected_checkin, - 'A note here', - ); - - 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 @@ class BulkCheckoutEmailTest extends TestCase 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 c6dd056cfb..0e1ef6655f 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -22,6 +22,8 @@ class BulkCheckoutEmailTest extends TestCase { 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 @@ class BulkCheckoutEmailTest extends TestCase // '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 0e1ef6655f..a8683f021c 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -56,6 +56,21 @@ class BulkCheckoutEmailTest extends TestCase }); } + 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 5391175bef..fa32f883bd 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -19,17 +19,17 @@ class CheckoutablesCheckedOutInBulkListener 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 a8683f021c..4034a4f4fa 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -58,8 +58,6 @@ class BulkCheckoutEmailTest extends TestCase 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 fa32f883bd..0d013293d7 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -30,6 +30,8 @@ class CheckoutablesCheckedOutInBulkListener )); } + // @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 4034a4f4fa..af655b602a 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -36,7 +36,9 @@ class BulkCheckoutEmailTest extends TestCase { // '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 0d013293d7..65259c06de 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -31,7 +31,5 @@ class CheckoutablesCheckedOutInBulkListener } // @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 af655b602a..b26afae593 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -71,6 +71,11 @@ class BulkCheckoutEmailTest extends TestCase 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 b26afae593..1773edec7b 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -31,17 +31,6 @@ class BulkCheckoutEmailTest extends TestCase $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 1773edec7b..fa6d0d4a04 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -42,7 +42,6 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 fa6d0d4a04..92a3cf648e 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -24,7 +24,9 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase public function test_email_is_sent_to_user() { - $this->settings->disableAdminCC(); - $this->dispatchEvent(); Mail::assertNotSent(CheckoutAssetMail::class); @@ -48,8 +48,6 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 65259c06de..bd0b395b7a 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -4,6 +4,8 @@ namespace App\Listeners; 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 @@ class CheckoutablesCheckedOutInBulkListener 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 @@ class CheckoutablesCheckedOutInBulkListener )); } - // @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 92a3cf648e..080f6a5d10 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -86,6 +86,22 @@ class BulkCheckoutEmailTest extends TestCase }); } + 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 080f6a5d10..a2135c1a6f 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -102,11 +102,6 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 0000000000..e1c9eb7986 --- /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 e1c9eb7986..69ec5c3a85 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 69ec5c3a85..5f94891a7d 100644 --- a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Notifications\Webhooks; 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 @@ class WebhookNotificationsUponBulkAssetCheckoutTest extends TestCase '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 bd0b395b7a..29f943f536 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -22,7 +22,7 @@ class CheckoutablesCheckedOutInBulkListener 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 @@ class CheckoutablesCheckedOutInBulkListener return $this->requiresAcceptance($assets); } - private function shouldSendEmailToAlertAddress(): bool + private function shouldSendEmailToAlertAddress(Collection $assets): bool { $setting = Setting::getSettings(); @@ -66,6 +66,10 @@ class CheckoutablesCheckedOutInBulkListener 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 a2135c1a6f..3b938b797a 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 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase $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 3b938b797a..de904bcd0f 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 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase $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 de904bcd0f..256c31c7dc 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -23,6 +23,7 @@ class BulkCheckoutEmailTest extends TestCase 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 256c31c7dc..10587e933f 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -27,12 +27,12 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase { $this->target = User::factory()->create(['email' => null]); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); Mail::assertNotSent(BulkAssetCheckoutMail::class); @@ -57,7 +57,7 @@ class BulkCheckoutEmailTest extends TestCase { $this->assets = Asset::factory()->count(2)->create(); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); Mail::assertNotSent(BulkAssetCheckoutMail::class); @@ -67,7 +67,7 @@ class BulkCheckoutEmailTest extends TestCase { $this->settings->enableAdminCC('cc@example.com'); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); @@ -89,7 +89,7 @@ class BulkCheckoutEmailTest extends TestCase $this->assets = Asset::factory()->count(2)->create(); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); Mail::assertNotSent(BulkAssetCheckoutMail::class); @@ -102,7 +102,7 @@ class BulkCheckoutEmailTest extends TestCase $this->assets = Asset::factory()->count(2)->create(); - $this->dispatchEvent(); + $this->sendRequest(); Mail::assertNotSent(CheckoutAssetMail::class); @@ -113,15 +113,19 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 0f788b5151..ee6902bcc5 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -428,6 +428,7 @@ class CheckoutableListener 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 @@ class CheckoutableListener 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 2cf38b2c39..0000000000 --- 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 | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php index 5f94891a7d..57f97fc48d 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', - ); + $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(); - Notification::assertNothingSentTo(CheckoutAssetNotification::class); - Notification::assertSentTimes(BulkAssetCheckoutNotification::class, 1); - - $this->assertSlackNotificationSent(BulkAssetCheckoutNotification::class); + $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 10587e933f..dccf5af900 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 29f943f536..fc3a5b4b65 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -5,7 +5,9 @@ namespace App\Listeners; 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 @@ class CheckoutablesCheckedOutInBulkListener $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 @@ class CheckoutablesCheckedOutInBulkListener 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 5ab45692d4..99ea17bb4a 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 @@ class BulkAssetCheckoutTest extends TestCase // 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 dccf5af900..1542787295 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -7,8 +7,10 @@ use App\Mail\CheckoutAssetMail; 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 57f97fc48d..993d33b932 100644 --- a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -6,8 +6,10 @@ use App\Models\Asset; 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 e8d3cfbbfb..876c4edca4 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 @@ class BulkAssetCheckoutMail extends Mailable public string $note, ) { $this->requires_acceptance = $this->requiresAcceptance(); + + $this->loadCustomFieldsOnAssets(); } public function envelope(): Envelope @@ -74,13 +78,6 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable // @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 79dfcc58e9..ef479d8b22 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 @@ One or more items require acceptance.
@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 1542787295..e208c9d541 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Notifications\Email; 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 @@ class BulkCheckoutEmailTest extends TestCase }); } + 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 @@ class BulkCheckoutEmailTest extends TestCase 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 fc3a5b4b65..2b141d86dd 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -4,6 +4,8 @@ namespace App\Listeners; 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 @@ class CheckoutablesCheckedOutInBulkListener $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 @@ class CheckoutablesCheckedOutInBulkListener ); } + 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 e208c9d541..8331564000 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -9,6 +9,7 @@ use App\Models\Location; 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 @@ class BulkCheckoutEmailTest extends TestCase 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 8331564000..b3122d16c4 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -96,7 +96,7 @@ class BulkCheckoutEmailTest extends TestCase 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 2b141d86dd..59736a1084 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -7,6 +7,7 @@ use App\Mail\BulkAssetCheckoutMail; 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 @@ class CheckoutablesCheckedOutInBulkListener 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 @@ class CheckoutablesCheckedOutInBulkListener } } - 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 b3122d16c4..79c0f4e5a3 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -42,7 +42,8 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 876c4edca4..45644c6a2e 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -4,6 +4,7 @@ namespace App\Mail; 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 @@ class BulkAssetCheckoutMail extends Mailable 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 79c0f4e5a3..e38e19612e 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -63,7 +63,7 @@ class BulkCheckoutEmailTest extends TestCase 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 45644c6a2e..08a24e52d7 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -46,7 +46,8 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable 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 ef479d8b22..97f563dc85 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 324c1c8f29..8f7c44c9f7 100644 --- a/app/Mail/CheckoutAssetMail.php +++ b/app/Mail/CheckoutAssetMail.php @@ -14,7 +14,6 @@ use Illuminate\Mail\Mailables\Address; 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 59736a1084..ab1d343397 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -74,6 +74,11 @@ class CheckoutablesCheckedOutInBulkListener // @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 e38e19612e..73a6a87173 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -106,6 +106,16 @@ class BulkCheckoutEmailTest extends TestCase 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 97f563dc85..dbe4723f90 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 ab1d343397..1899076b70 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -72,12 +72,9 @@ class CheckoutablesCheckedOutInBulkListener 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 @@ class CheckoutablesCheckedOutInBulkListener 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 73a6a87173..f8516f57f0 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Notifications\Email; 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 @@ class BulkCheckoutEmailTest extends TestCase 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 dbe4723f90..d1b9ce0f20 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 1899076b70..7eef9d69c9 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -101,8 +101,7 @@ class CheckoutablesCheckedOutInBulkListener 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 f8516f57f0..3776ba34da 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -109,8 +109,6 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 3776ba34da..89adf45c28 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -114,6 +114,7 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase && $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 733c52668e..38df7e58e4 100644 --- a/database/factories/CategoryFactory.php +++ b/database/factories/CategoryFactory.php @@ -214,4 +214,19 @@ class CategoryFactory extends Factory '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 89adf45c28..a9647b2e3d 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -109,15 +109,15 @@ class BulkCheckoutEmailTest extends TestCase 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 7eef9d69c9..e081d767ee 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -76,6 +76,10 @@ class CheckoutablesCheckedOutInBulkListener return true; } + if ($this->hasAssetWithCategorySettingToSendEmail($assets)) { + return true; + } + return $this->requiresAcceptance($assets); } @@ -109,6 +113,17 @@ class CheckoutablesCheckedOutInBulkListener 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 38df7e58e4..6ebd8e4074 100644 --- a/database/factories/CategoryFactory.php +++ b/database/factories/CategoryFactory.php @@ -222,6 +222,13 @@ class CategoryFactory extends Factory ]); } + public function sendsCheckinEmail() + { + return $this->state([ + 'checkin_email' => true, + ]); + } + public function hasLocalEula() { return $this->state([ @@ -229,4 +236,12 @@ class CategoryFactory extends Factory '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 a9647b2e3d..9fc637228d 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -132,9 +132,29 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 9fc637228d..aeb9fe0d65 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -101,6 +101,14 @@ class BulkCheckoutEmailTest extends TestCase { $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 aeb9fe0d65..85aaa1d65d 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -115,6 +115,27 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 85aaa1d65d..729fa48cd1 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -115,27 +115,6 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase }); } + 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 729fa48cd1..cfd7513c6b 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -184,19 +184,25 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 cfd7513c6b..e2b6016535 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -231,40 +231,30 @@ class BulkCheckoutEmailTest extends TestCase private function sendRequest() { + $assigned = match (get_class($this->target)) { + User::class => [ + 'checkout_to_type' => 'user', + 'assigned_user' => $this->target->id, + ], + Location::class => [ + 'checkout_to_type' => 'location', + 'assigned_location' => $this->target->id, + ], + Asset::class => [ + 'checkout_to_type' => 'asset', + 'assigned_asset' => $this->target->id, + ], + default => [], + }; + $this->actingAs($this->admin) ->followingRedirects() - ->post(route('hardware.bulkcheckout.store'), array_merge([ + ->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, - ], $this->getAssignedArray())) + ] + $assigned) ->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 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 e2b6016535..5c8b7d832d 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -8,6 +8,8 @@ use App\Models\Asset; 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 @@ use Tests\TestCase; #[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 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase 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 5c8b7d832d..085af384e1 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Model; 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 085af384e1..ba0f74e8d9 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -18,7 +18,7 @@ use Tests\TestCase; class BulkCheckoutEmailTest extends TestCase { private Collection $assets; - private Model $target; + private Model $assignee; protected function setUp(): void { @@ -30,7 +30,7 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase { $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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase { $user = User::factory()->create(); - $this->target = Asset::factory()->assignedToUser($user)->create(); + $this->assignee = Asset::factory()->assignedToUser($user)->create(); $this->sendRequest(); @@ -84,7 +84,7 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 ba0f74e8d9..6dd1e1ab5e 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -160,9 +160,11 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 6dd1e1ab5e..bdc5f576e6 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -33,7 +33,7 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase }); } - public function test_email_is_sent_to_location_manager() + public function test_sent_to_location_manager() { $manager = User::factory()->create(); @@ -65,7 +65,7 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 @@ class BulkCheckoutEmailTest extends TestCase }); } - 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 08a24e52d7..e3244aa1dd 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -62,8 +62,7 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable 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}."; + return trans_choice('mail.new_item_checked_location', $this->assets->count(), ['location' => $this->target->name]); } - 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.'; + return trans_choice('mail.new_item_checked', $this->assets->count()); } private function acceptanceUrl() @@ -145,14 +132,14 @@ class BulkAssetCheckoutMail extends Mailable 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 707390e4f0..92cce47145 100644 --- a/resources/lang/en-US/mail.php +++ b/resources/lang/en-US/mail.php @@ -79,6 +79,7 @@ return [ '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 d1b9ce0f20..6023c2a9f6 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 e3244aa1dd..b1a6ff5286 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -49,7 +49,6 @@ class BulkAssetCheckoutMail extends Mailable 'requires_acceptance' => $this->requires_acceptance, 'requires_acceptance_wording' => $this->getRequiresAcceptanceWording(), 'acceptance_url' => $this->acceptanceUrl(), - 'eula' => $this->getEula(), ], ); } @@ -86,23 +85,6 @@ class BulkAssetCheckoutMail extends Mailable 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 6023c2a9f6..900a048818 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 bdc5f576e6..9fb6f4554c 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -42,8 +42,7 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase 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 @@ class BulkCheckoutEmailTest extends TestCase $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 ee6902bcc5..6a56ae2f9e 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -428,7 +428,6 @@ class CheckoutableListener 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 @@ class CheckoutableListener */ 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 b1a6ff5286..e3244aa1dd 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -49,6 +49,7 @@ class BulkAssetCheckoutMail extends Mailable 'requires_acceptance' => $this->requires_acceptance, 'requires_acceptance_wording' => $this->getRequiresAcceptanceWording(), 'acceptance_url' => $this->acceptanceUrl(), + 'eula' => $this->getEula(), ], ); } @@ -85,6 +86,23 @@ class BulkAssetCheckoutMail extends Mailable 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 900a048818..6023c2a9f6 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 e3244aa1dd..4884fa7809 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 @@ class BulkAssetCheckoutMail extends Mailable $this->requires_acceptance = $this->requiresAcceptance(); $this->loadCustomFieldsOnAssets(); + $this->loadEulaOnAssets(); + $this->sortAssetsByCategory(); } public function envelope(): Envelope @@ -49,7 +53,7 @@ class BulkAssetCheckoutMail extends Mailable '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 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable }); } + 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 6023c2a9f6..47573f724d 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 47573f724d..3c5efc949a 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 3c5efc949a..27d34f5510 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 27d34f5510..808b6c8049 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 808b6c8049..becda7510c 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 becda7510c..58196bd948 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 58196bd948..44dfc12a93 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 4884fa7809..32918fc069 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -92,9 +92,10 @@ class BulkAssetCheckoutMail extends Mailable 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 32918fc069..25a8bfde5a 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -33,8 +33,9 @@ class BulkAssetCheckoutMail extends Mailable $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 @@ class BulkAssetCheckoutMail extends Mailable }); } - private function loadEulaOnAssets(): void + private function loadEulasOnAssets(): void { $this->assets = $this->assets->map(function (Asset $asset) { $asset->eula = $asset->getEula(); @@ -128,9 +129,9 @@ class BulkAssetCheckoutMail extends Mailable }); } - 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 25a8bfde5a..7b309901c4 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -73,6 +73,33 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable } } - 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 7b309901c4..11370f310e 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -53,7 +53,7 @@ class BulkAssetCheckoutMail extends Mailable '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 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable 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 11370f310e..32040c68be 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -147,18 +147,13 @@ class BulkAssetCheckoutMail extends Mailable 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 92cce47145..021e209119 100644 --- a/resources/lang/en-US/mail.php +++ b/resources/lang/en-US/mail.php @@ -86,6 +86,7 @@ return [ '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 44dfc12a93..d8d71e1482 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 32040c68be..536634c993 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -52,8 +52,8 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable 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 @@ class BulkAssetCheckoutMail extends Mailable 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 d8d71e1482..a603a0da06 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 e081d767ee..81b7a26c96 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -68,7 +68,7 @@ class CheckoutablesCheckedOutInBulkListener 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 81b7a26c96..b5760c4bbb 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -80,7 +80,7 @@ class CheckoutablesCheckedOutInBulkListener return true; } - return $this->requiresAcceptance($assets); + return $this->hasAssetThatRequiresAcceptance($assets); } private function shouldSendEmailToAlertAddress(Collection $assets): bool @@ -95,7 +95,7 @@ class CheckoutablesCheckedOutInBulkListener return true; } - if (!$this->requiresAcceptance($assets)) { + if (!$this->hasAssetThatRequiresAcceptance($assets)) { return false; } @@ -124,7 +124,7 @@ class CheckoutablesCheckedOutInBulkListener 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 b5760c4bbb..4f197c2269 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -9,6 +9,7 @@ use App\Models\Location; 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 @@ class CheckoutablesCheckedOutInBulkListener ); } - 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 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php index 6ebd8e4074..eb0d8b828b 100644 --- a/database/factories/CategoryFactory.php +++ b/database/factories/CategoryFactory.php @@ -215,13 +215,6 @@ class CategoryFactory extends Factory ]); } - public function doesNotSendCheckinEmail() - { - return $this->state([ - 'checkin_email' => false, - ]); - } - public function sendsCheckinEmail() { return $this->state([ @@ -229,6 +222,13 @@ class CategoryFactory extends Factory ]); } + public function doesNotSendCheckinEmail() + { + return $this->state([ + 'checkin_email' => false, + ]); + } + public function hasLocalEula() { return $this->state([ 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 4f197c2269..c1ff57d911 100644 --- a/app/Listeners/CheckoutablesCheckedOutInBulkListener.php +++ b/app/Listeners/CheckoutablesCheckedOutInBulkListener.php @@ -127,9 +127,13 @@ class CheckoutablesCheckedOutInBulkListener 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 536634c993..5887e56141 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -154,8 +154,12 @@ class BulkAssetCheckoutMail extends Mailable 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 5887e56141..9a4ccee85c 100644 --- a/app/Mail/BulkAssetCheckoutMail.php +++ b/app/Mail/BulkAssetCheckoutMail.php @@ -111,7 +111,7 @@ class BulkAssetCheckoutMail extends Mailable private function getRequiresAcceptanceInfo(): ?string { - if (!$this->requiresAcceptance()) { + if (!$this->requires_acceptance) { return null; } @@ -120,7 +120,7 @@ class BulkAssetCheckoutMail extends Mailable 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 993d33b932..ebd84acca0 100644 --- a/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/WebhookNotificationsUponBulkAssetCheckoutTest.php @@ -18,14 +18,14 @@ class WebhookNotificationsUponBulkAssetCheckoutTest extends TestCase $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 9fb6f4554c..4a1658e9fb 100644 --- a/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php +++ b/tests/Feature/Notifications/Email/BulkCheckoutEmailTest.php @@ -30,17 +30,16 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertNotSent(BulkAssetCheckoutMail::class); } @@ -104,7 +101,7 @@ class BulkCheckoutEmailTest extends TestCase $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertNotSent(BulkAssetCheckoutMail::class); } @@ -122,10 +119,9 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertSent(BulkAssetCheckoutMail::class, 2); @@ -191,7 +186,7 @@ class BulkCheckoutEmailTest extends TestCase $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 @@ class BulkCheckoutEmailTest extends TestCase $this->sendRequest(); - Mail::assertNotSent(CheckoutAssetMail::class); + $this->assertSingularCheckoutEmailNotSent(); Mail::assertNotSent(BulkAssetCheckoutMail::class); } @@ -233,6 +228,7 @@ class BulkCheckoutEmailTest extends TestCase 'checkout_to_type' => 'asset', 'assigned_asset' => $this->assignee->id, ], + // we shouldn't get here... default => [], }; @@ -246,4 +242,11 @@ class BulkCheckoutEmailTest extends TestCase ] + $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 a603a0da06..dfd33815b9 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