From 444c13c6ea3c2bbb9d6950bb399e88354eb87227 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 3 Jun 2025 17:10:15 -0700 Subject: [PATCH 01/46] Scaffold template --- .../views/livewire/category-edit-form.blade.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/resources/views/livewire/category-edit-form.blade.php b/resources/views/livewire/category-edit-form.blade.php index 91cb26da51..5ae814ca6c 100644 --- a/resources/views/livewire/category-edit-form.blade.php +++ b/resources/views/livewire/category-edit-form.blade.php @@ -64,6 +64,21 @@ + @if ($requireAcceptance) +
+
+ +
+
+ @endif +
From 360f5b7538804792bac297e7d30426a8512399e8 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 13:10:18 -0700 Subject: [PATCH 02/46] Add alert_on_response_id to CheckoutAcceptance --- app/Models/CheckoutAcceptance.php | 1 + ...ponse_id_to_checkout_acceptances_table.php | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 database/migrations/2025_06_05_200518_add_alert_on_response_id_to_checkout_acceptances_table.php diff --git a/app/Models/CheckoutAcceptance.php b/app/Models/CheckoutAcceptance.php index e44a330ebc..2ca9c5b48b 100644 --- a/app/Models/CheckoutAcceptance.php +++ b/app/Models/CheckoutAcceptance.php @@ -15,6 +15,7 @@ class CheckoutAcceptance extends Model protected $casts = [ 'accepted_at' => 'datetime', 'declined_at' => 'datetime', + 'alert_on_response_id' => 'integer', ]; /** diff --git a/database/migrations/2025_06_05_200518_add_alert_on_response_id_to_checkout_acceptances_table.php b/database/migrations/2025_06_05_200518_add_alert_on_response_id_to_checkout_acceptances_table.php new file mode 100644 index 0000000000..ff9ad310f5 --- /dev/null +++ b/database/migrations/2025_06_05_200518_add_alert_on_response_id_to_checkout_acceptances_table.php @@ -0,0 +1,27 @@ +unsignedBigInteger('alert_on_response_id')->nullable()->after('note'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('checkout_acceptances', function (Blueprint $table) { + $table->dropColumn('alert_on_response_id'); + }); + } +}; From 96bce301a0ffde3bbb325a79082f429a56fc1be0 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 13:43:20 -0700 Subject: [PATCH 03/46] Add alert_on_response to Category --- app/Models/Category.php | 1 + ..._alert_on_response_to_categories_table.php | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 database/migrations/2025_06_05_204139_add_alert_on_response_to_categories_table.php diff --git a/app/Models/Category.php b/app/Models/Category.php index 0321dfdb26..0fd896b35d 100755 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -32,6 +32,7 @@ class Category extends SnipeModel protected $hidden = ['created_by', 'deleted_at']; protected $casts = [ + 'alert_on_response' => 'boolean', 'created_by' => 'integer', ]; diff --git a/database/migrations/2025_06_05_204139_add_alert_on_response_to_categories_table.php b/database/migrations/2025_06_05_204139_add_alert_on_response_to_categories_table.php new file mode 100644 index 0000000000..dc2bdae1b2 --- /dev/null +++ b/database/migrations/2025_06_05_204139_add_alert_on_response_to_categories_table.php @@ -0,0 +1,27 @@ +boolean('alert_on_response')->default(0)->after('require_acceptance'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('categories', function (Blueprint $table) { + $table->dropColumn('alert_on_response'); + }); + } +}; From cb183d3645d6c9db3db7aedfab17aedd64472daa Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:06:22 -0700 Subject: [PATCH 04/46] Store alert_on_response_id on CheckoutAcceptance --- app/Listeners/CheckoutableListener.php | 6 +++ app/Models/Category.php | 1 + .../General/SettingAlertOnResponseTest.php | 37 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index e647bcd9e0..52a30f4772 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -227,6 +227,7 @@ class CheckoutableListener if ($checkedOutToType != "App\Models\User") { return null; } + if (!$event->checkoutable->requireAcceptance()) { return null; } @@ -234,6 +235,11 @@ class CheckoutableListener $acceptance = new CheckoutAcceptance; $acceptance->checkoutable()->associate($event->checkoutable); $acceptance->assignedTo()->associate($event->checkedOutTo); + + if (data_get($event, 'checkoutable.model.category.alert_on_response')) { + $acceptance->alert_on_response_id = auth()->id(); + } + $acceptance->save(); return $acceptance; diff --git a/app/Models/Category.php b/app/Models/Category.php index 0fd896b35d..08bef4d3eb 100755 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -70,6 +70,7 @@ class Category extends SnipeModel 'eula_text', 'name', 'require_acceptance', + 'alert_on_response', 'use_default_eula', 'created_by', 'notes', diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php new file mode 100644 index 0000000000..ac3a445c6f --- /dev/null +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -0,0 +1,37 @@ +create(); + $asset->model->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => true, + ]); + + $actor = User::factory()->checkoutAssets()->create(); + $assignedUser = User::factory()->create(); + + $this->actingAs($actor) + ->post(route('hardware.checkout.store', $asset), [ + 'checkout_to_type' => 'user', + 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, + 'assigned_user' => $assignedUser->id, + ]); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $asset->id, + 'assigned_to_id' => $assignedUser->id, + 'alert_on_response_id' => $actor->id, + ]); + } +} From 5e251505218ea1996953def2c5081f624d8f26bf Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:26:56 -0700 Subject: [PATCH 05/46] Add another test case --- .../General/SettingAlertOnResponseTest.php | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index ac3a445c6f..c32f8d3472 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -9,29 +9,60 @@ use Tests\TestCase; class SettingAlertOnResponseTest extends TestCase { - public function testSetsAlertOnResponseCorrectly() + private Asset $asset; + private User $actor; + private User $assignedUser; + + protected function setUp(): void { - $asset = Asset::factory()->create(); - $asset->model->category->update([ + parent::setUp(); + + $this->asset = Asset::factory()->create(); + $this->actor = User::factory()->checkoutAssets()->create(); + $this->assignedUser = User::factory()->create(); + } + + public function test_sets_alert_on_response_if_enabled_by_category() + { + $this->asset->model->category->update([ 'require_acceptance' => true, 'alert_on_response' => true, ]); - $actor = User::factory()->checkoutAssets()->create(); - $assignedUser = User::factory()->create(); - - $this->actingAs($actor) - ->post(route('hardware.checkout.store', $asset), [ - 'checkout_to_type' => 'user', - 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, - 'assigned_user' => $assignedUser->id, - ]); + $this->postCheckout(); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, - 'checkoutable_id' => $asset->id, - 'assigned_to_id' => $assignedUser->id, - 'alert_on_response_id' => $actor->id, + 'checkoutable_id' => $this->asset->id, + 'assigned_to_id' => $this->assignedUser->id, + 'alert_on_response_id' => $this->actor->id, ]); } + + public function test_does_not_set_alert_on_response_if_disabled_by_category() + { + $this->asset->model->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => false, + ]); + + $this->postCheckout(); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $this->asset->id, + 'assigned_to_id' => $this->assignedUser->id, + 'alert_on_response_id' => null, + ]); + } + + private function postCheckout(): void + { + $this->actingAs($this->actor) + ->post(route('hardware.checkout.store', $this->asset), [ + 'checkout_to_type' => 'user', + 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, + 'assigned_user' => $this->assignedUser->id, + ]); + } } From 2a68b4aeff04275b9604d3c20d6c695368f4671c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:36:06 -0700 Subject: [PATCH 06/46] Update test for updating category with alert_on_response --- .../Categories/Ui/UpdateCategoriesTest.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/Feature/Categories/Ui/UpdateCategoriesTest.php b/tests/Feature/Categories/Ui/UpdateCategoriesTest.php index ea5cc63388..8ecfb2a527 100644 --- a/tests/Feature/Categories/Ui/UpdateCategoriesTest.php +++ b/tests/Feature/Categories/Ui/UpdateCategoriesTest.php @@ -43,20 +43,33 @@ class UpdateCategoriesTest extends TestCase public function testUserCanEditAssetCategory() { - $category = Category::factory()->forAssets()->create(['name' => 'Test Category']); + $category = Category::factory()->forAssets()->create([ + 'name' => 'Test Category', + 'require_acceptance' => false, + 'alert_on_response' => false, + ]); + $this->assertTrue(Category::where('name', 'Test Category')->exists()); $response = $this->actingAs(User::factory()->superuser()->create()) ->put(route('categories.update', $category), [ 'name' => 'Test Category Edited', 'notes' => 'Test Note Edited', + 'require_acceptance' => '1', + 'alert_on_response' => '1', ]) ->assertStatus(302) ->assertSessionHasNoErrors() ->assertRedirect(route('categories.index')); $this->followRedirects($response)->assertSee('Success'); - $this->assertTrue(Category::where('name', 'Test Category Edited')->where('notes', 'Test Note Edited')->exists()); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Test Category Edited', + 'notes' => 'Test Note Edited', + 'require_acceptance' => 1, + 'alert_on_response' => 1, + ]); } @@ -102,5 +115,4 @@ class UpdateCategoriesTest extends TestCase $this->assertFalse(Category::where('name', 'Test Category Edited')->where('notes', 'Test Note Edited')->exists()); } - } From 19b9e50281431652d3d2f39fc3d713b8efc5c747 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:40:19 -0700 Subject: [PATCH 07/46] Update livewire component for alert_on_response --- app/Livewire/CategoryEditForm.php | 2 ++ resources/views/categories/edit.blade.php | 1 + resources/views/livewire/category-edit-form.blade.php | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Livewire/CategoryEditForm.php b/app/Livewire/CategoryEditForm.php index fd8bef6489..98e505c8df 100644 --- a/app/Livewire/CategoryEditForm.php +++ b/app/Livewire/CategoryEditForm.php @@ -6,6 +6,8 @@ use Livewire\Component; class CategoryEditForm extends Component { + public bool $alertOnResponse; + public $defaultEulaText; public $eulaText; diff --git a/resources/views/categories/edit.blade.php b/resources/views/categories/edit.blade.php index 7c12f677fa..fd9efcc49f 100755 --- a/resources/views/categories/edit.blade.php +++ b/resources/views/categories/edit.blade.php @@ -31,6 +31,7 @@
Send email to you when user accepts or declines checkout. From 063553d4f72dab8a8fe287d53e4c18197d629bea Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:57:15 -0700 Subject: [PATCH 08/46] Scaffold scenarios --- .../Email/CheckoutResponseEmailTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php new file mode 100644 index 0000000000..e6627b9225 --- /dev/null +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -0,0 +1,21 @@ +markTestIncomplete(); + } +} From 333501fe55cee37b1552d0bf31aafb4344814875 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 15:07:19 -0700 Subject: [PATCH 09/46] WIP: create mail class --- app/Mail/CheckoutAcceptanceResponseMail.php | 53 +++++++++++++++++++ .../checkout-acceptance-response.blade.php | 8 +++ .../Email/CheckoutResponseEmailTest.php | 33 ++++++++++-- 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 app/Mail/CheckoutAcceptanceResponseMail.php create mode 100644 resources/views/mail/markdown/checkout-acceptance-response.blade.php diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php new file mode 100644 index 0000000000..440f63467a --- /dev/null +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -0,0 +1,53 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/resources/views/mail/markdown/checkout-acceptance-response.blade.php b/resources/views/mail/markdown/checkout-acceptance-response.blade.php new file mode 100644 index 0000000000..5fe2b643e2 --- /dev/null +++ b/resources/views/mail/markdown/checkout-acceptance-response.blade.php @@ -0,0 +1,8 @@ +@component('mail::message') +# {{ trans('mail.hello') }} {{ $target->present()->fullName() }}, + +{{ trans('mail.best_regards') }} + +{{ $snipeSettings->site_name }} + +@endcomponent diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index e6627b9225..cd40f55b18 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -2,20 +2,45 @@ namespace Tests\Feature\Notifications\Email; +use App\Mail\CheckoutAcceptanceResponseMail; +use App\Models\CheckoutAcceptance; +use App\Models\User; +use Illuminate\Support\Facades\Mail; use Tests\TestCase; class CheckoutResponseEmailTest extends TestCase { public static function scenarios() { - yield 'Accepting checkout acceptance configured to send alert'; - yield 'Declining checkout acceptance configured to send alert'; - yield 'Accepting checkout acceptance not configured to send alert'; - yield 'Declining checkout acceptance not configured to send alert'; + yield 'Accepting checkout acceptance configured to send alert' => []; + yield 'Declining checkout acceptance configured to send alert' => []; + yield 'Accepting checkout acceptance not configured to send alert' => []; + yield 'Declining checkout acceptance not configured to send alert' => []; } public function test_checkout_response_alert() { $this->markTestIncomplete(); + + Mail::fake(); + + $user = User::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->create([ + 'alert_on_response_id' => $user->id, + ]); + + $this->actingAs($checkoutAcceptance->assignedTo) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'accepted', + 'note' => null, + ]); + + Mail::assertSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { + // @todo: better assertions? accepted vs declined? + return $mail->hasTo($user->email); + }); } } From bec80b443ceeeb414fb307e3a932be91e21485b2 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 15:15:22 -0700 Subject: [PATCH 10/46] WIP: begin to send email --- .../Controllers/Account/AcceptanceController.php | 13 +++++++++++++ app/Mail/CheckoutAcceptanceResponseMail.php | 6 ++++-- .../Email/CheckoutResponseEmailTest.php | 2 -- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php index b20f7d97e8..583b710887 100644 --- a/app/Http/Controllers/Account/AcceptanceController.php +++ b/app/Http/Controllers/Account/AcceptanceController.php @@ -7,6 +7,7 @@ use App\Events\CheckoutDeclined; use App\Events\ItemAccepted; use App\Events\ItemDeclined; use App\Http\Controllers\Controller; +use App\Mail\CheckoutAcceptanceResponseMail; use App\Models\Actionlog; use App\Models\Asset; use App\Models\CheckoutAcceptance; @@ -21,8 +22,10 @@ use App\Models\Component; use App\Models\Consumable; use App\Notifications\AcceptanceAssetAcceptedNotification; use App\Notifications\AcceptanceAssetDeclinedNotification; +use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use App\Http\Controllers\SettingsController; @@ -337,6 +340,16 @@ class AcceptanceController extends Controller $return_msg = trans('admin/users/message.declined'); } + if ($acceptance->alert_on_response_id) { + try { + Mail::to(User::findOrFail($acceptance->alert_on_response_id)) + ->send(new CheckoutAcceptanceResponseMail( + (bool) $request->input('asset_acceptance') === 'accepted' + )); + } catch (Exception $e) { + Log::warning($e); + } + } return redirect()->to('account/accept')->with('success', $return_msg); diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index 440f63467a..3eed73ed0a 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -13,12 +13,14 @@ class CheckoutAcceptanceResponseMail extends Mailable { use Queueable, SerializesModels; + public bool $accepted; + /** * Create a new message instance. */ - public function __construct() + public function __construct(bool $accepted) { - // + $this->accepted = $accepted; } /** diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index cd40f55b18..9b4546af81 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -20,8 +20,6 @@ class CheckoutResponseEmailTest extends TestCase public function test_checkout_response_alert() { - $this->markTestIncomplete(); - Mail::fake(); $user = User::factory()->create(); From df1361aa43ad198585a348723eafb8b2dcdacd54 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 16:42:31 -0700 Subject: [PATCH 11/46] Scaffold test --- .../Email/CheckoutResponseEmailTest.php | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index 9b4546af81..d43f2c6422 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -10,15 +10,7 @@ use Tests\TestCase; class CheckoutResponseEmailTest extends TestCase { - public static function scenarios() - { - yield 'Accepting checkout acceptance configured to send alert' => []; - yield 'Declining checkout acceptance configured to send alert' => []; - yield 'Accepting checkout acceptance not configured to send alert' => []; - yield 'Declining checkout acceptance not configured to send alert' => []; - } - - public function test_checkout_response_alert() + public function test_accepting_checkout_acceptance_configured_to_send_alert() { Mail::fake(); @@ -41,4 +33,19 @@ class CheckoutResponseEmailTest extends TestCase return $mail->hasTo($user->email); }); } + + public function test_declining_checkout_acceptance_configured_to_send_alert() + { + $this->markTestIncomplete(); + } + + public function test_accepting_checkout_acceptance_not_configured_to_send_alert() + { + $this->markTestIncomplete(); + } + + public function test_declining_checkout_acceptance_not_configured_to_send_alert() + { + $this->markTestIncomplete(); + } } From 7ec0925c69f4ffb0e85998b8e1caea9f080d11c4 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 16:44:44 -0700 Subject: [PATCH 12/46] Scaffold out tests --- .../Email/CheckoutResponseEmailTest.php | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index d43f2c6422..408535ccd0 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -10,10 +10,15 @@ use Tests\TestCase; class CheckoutResponseEmailTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + Mail::fake(); + } + public function test_accepting_checkout_acceptance_configured_to_send_alert() { - Mail::fake(); - $user = User::factory()->create(); $checkoutAcceptance = CheckoutAcceptance::factory() @@ -28,10 +33,7 @@ class CheckoutResponseEmailTest extends TestCase 'note' => null, ]); - Mail::assertSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { - // @todo: better assertions? accepted vs declined? - return $mail->hasTo($user->email); - }); + $this->assertEmailSentTo($user); } public function test_declining_checkout_acceptance_configured_to_send_alert() @@ -48,4 +50,12 @@ class CheckoutResponseEmailTest extends TestCase { $this->markTestIncomplete(); } + + private function assertEmailSentTo(User $user): void + { + Mail::assertSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { + // @todo: better assertions? accepted vs declined? + return $mail->hasTo($user->email); + }); + } } From db50e98ae383e7ae682bb6daaef52c325fc29f23 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 16:51:44 -0700 Subject: [PATCH 13/46] Populate tests --- .../Email/CheckoutResponseEmailTest.php | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index 408535ccd0..73dcd2cfb5 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -19,36 +19,62 @@ class CheckoutResponseEmailTest extends TestCase public function test_accepting_checkout_acceptance_configured_to_send_alert() { - $user = User::factory()->create(); + $initiator = User::factory()->create(); $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() ->create([ - 'alert_on_response_id' => $user->id, + 'alert_on_response_id' => $initiator->id, ]); - $this->actingAs($checkoutAcceptance->assignedTo) - ->post(route('account.store-acceptance', $checkoutAcceptance), [ - 'asset_acceptance' => 'accepted', - 'note' => null, - ]); + $this->acceptCheckout($checkoutAcceptance); - $this->assertEmailSentTo($user); + $this->assertEmailSentTo($initiator); } public function test_declining_checkout_acceptance_configured_to_send_alert() { - $this->markTestIncomplete(); + $initiator = User::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->create([ + 'alert_on_response_id' => $initiator->id, + ]); + + $this->declineCheckout($checkoutAcceptance); + + $this->assertEmailSentTo($initiator); } public function test_accepting_checkout_acceptance_not_configured_to_send_alert() { - $this->markTestIncomplete(); + $initiator = User::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->create([ + 'alert_on_response_id' => null, + ]); + + $this->acceptCheckout($checkoutAcceptance); + + $this->assertEmailNotSentTo($initiator); } public function test_declining_checkout_acceptance_not_configured_to_send_alert() { - $this->markTestIncomplete(); + $initiator = User::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->create([ + 'alert_on_response_id' => null, + ]); + + $this->declineCheckout($checkoutAcceptance); + + $this->assertEmailNotSentTo($initiator); } private function assertEmailSentTo(User $user): void @@ -58,4 +84,29 @@ class CheckoutResponseEmailTest extends TestCase return $mail->hasTo($user->email); }); } + + private function assertEmailNotSentTo(User $user): void + { + Mail::assertNotSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { + return $mail->hasTo($user->email); + }); + } + + private function acceptCheckout(CheckoutAcceptance $checkoutAcceptance): void + { + $this->actingAs($checkoutAcceptance->assignedTo) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'accepted', + 'note' => null, + ]); + } + + private function declineCheckout(CheckoutAcceptance $checkoutAcceptance): void + { + $this->actingAs($checkoutAcceptance->assignedTo) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'declined', + 'note' => null, + ]); + } } From 2d0874920711adfb36da1f36fc171508a28c4550 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 16:56:11 -0700 Subject: [PATCH 14/46] Improve readability --- .../factories/CheckoutAcceptanceFactory.php | 18 +++++++++++++++++ .../Email/CheckoutResponseEmailTest.php | 20 ++++++++----------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index a15e942a84..bb56ab2b4a 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -64,6 +64,24 @@ class CheckoutAcceptanceFactory extends Factory ]); } + public function withoutAlerting() + { + return $this->state(function () { + return [ + 'alert_on_response_id' => null, + ]; + }); + } + + public function withAlertingTo(User $user) + { + return $this->state(function () use ($user) { + return [ + 'alert_on_response_id' => $user->id, + ]; + }); + } + private function createdAssociatedActionLogEntry(CheckoutAcceptance $acceptance): void { $acceptance->checkoutable->assetlog()->create([ diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index 73dcd2cfb5..a761b77ce1 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -23,9 +23,8 @@ class CheckoutResponseEmailTest extends TestCase $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() - ->create([ - 'alert_on_response_id' => $initiator->id, - ]); + ->withAlertingTo($initiator) + ->create(); $this->acceptCheckout($checkoutAcceptance); @@ -38,9 +37,8 @@ class CheckoutResponseEmailTest extends TestCase $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() - ->create([ - 'alert_on_response_id' => $initiator->id, - ]); + ->withAlertingTo($initiator) + ->create(); $this->declineCheckout($checkoutAcceptance); @@ -53,9 +51,8 @@ class CheckoutResponseEmailTest extends TestCase $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() - ->create([ - 'alert_on_response_id' => null, - ]); + ->withoutAlerting() + ->create(); $this->acceptCheckout($checkoutAcceptance); @@ -68,9 +65,8 @@ class CheckoutResponseEmailTest extends TestCase $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() - ->create([ - 'alert_on_response_id' => null, - ]); + ->withoutAlerting() + ->create(); $this->declineCheckout($checkoutAcceptance); From 7424a5987bc6f6da9a007b048f1c0a04de9d3413 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 17:22:31 -0700 Subject: [PATCH 15/46] Add subject --- .../Controllers/Account/AcceptanceController.php | 9 ++++++--- app/Mail/CheckoutAcceptanceResponseMail.php | 15 +++++++++++---- .../checkout-acceptance-response.blade.php | 2 +- .../Email/CheckoutResponseEmailTest.php | 13 ++++++------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php index 583b710887..adb423581b 100644 --- a/app/Http/Controllers/Account/AcceptanceController.php +++ b/app/Http/Controllers/Account/AcceptanceController.php @@ -342,10 +342,13 @@ class AcceptanceController extends Controller if ($acceptance->alert_on_response_id) { try { - Mail::to(User::findOrFail($acceptance->alert_on_response_id)) + $recipient = User::findOrFail($acceptance->alert_on_response_id); + + Mail::to($recipient) ->send(new CheckoutAcceptanceResponseMail( - (bool) $request->input('asset_acceptance') === 'accepted' - )); + $recipient, + $request->input('asset_acceptance') === 'accepted') + ); } catch (Exception $e) { Log::warning($e); } diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index 3eed73ed0a..bf4aad5e8e 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; @@ -13,14 +14,16 @@ class CheckoutAcceptanceResponseMail extends Mailable { use Queueable, SerializesModels; - public bool $accepted; + public bool $wasAccepted; + public User $recipient; /** * Create a new message instance. */ - public function __construct(bool $accepted) + public function __construct(User $recipient, bool $wasAccepted) { - $this->accepted = $accepted; + $this->recipient = $recipient; + $this->wasAccepted = $wasAccepted; } /** @@ -29,7 +32,7 @@ class CheckoutAcceptanceResponseMail extends Mailable public function envelope(): Envelope { return new Envelope( - subject: 'Checkout Acceptance Response Mail', + subject: 'A checkout you initiated was ' . ($this->wasAccepted ? 'accepted' : 'rejected'), ); } @@ -40,6 +43,10 @@ class CheckoutAcceptanceResponseMail extends Mailable { return new Content( markdown: 'mail.markdown.checkout-acceptance-response', + with: [ + 'recipient' => $this->recipient, + 'wasAccepted' => $this->wasAccepted, + ] ); } diff --git a/resources/views/mail/markdown/checkout-acceptance-response.blade.php b/resources/views/mail/markdown/checkout-acceptance-response.blade.php index 5fe2b643e2..472e39b308 100644 --- a/resources/views/mail/markdown/checkout-acceptance-response.blade.php +++ b/resources/views/mail/markdown/checkout-acceptance-response.blade.php @@ -1,5 +1,5 @@ @component('mail::message') -# {{ trans('mail.hello') }} {{ $target->present()->fullName() }}, +# {{ trans('mail.hello') }} {{ $recipient->present()->fullName() }}, {{ trans('mail.best_regards') }} diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index a761b77ce1..9174e5656d 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -28,7 +28,7 @@ class CheckoutResponseEmailTest extends TestCase $this->acceptCheckout($checkoutAcceptance); - $this->assertEmailSentTo($initiator); + $this->assertEmailSentTo($initiator, 'accepted'); } public function test_declining_checkout_acceptance_configured_to_send_alert() @@ -42,7 +42,7 @@ class CheckoutResponseEmailTest extends TestCase $this->declineCheckout($checkoutAcceptance); - $this->assertEmailSentTo($initiator); + $this->assertEmailSentTo($initiator, 'rejected'); } public function test_accepting_checkout_acceptance_not_configured_to_send_alert() @@ -73,11 +73,10 @@ class CheckoutResponseEmailTest extends TestCase $this->assertEmailNotSentTo($initiator); } - private function assertEmailSentTo(User $user): void + private function assertEmailSentTo(User $user, string $type): void { - Mail::assertSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { - // @todo: better assertions? accepted vs declined? - return $mail->hasTo($user->email); + Mail::assertSent(CheckoutAcceptanceResponseMail::class, function (CheckoutAcceptanceResponseMail $mail) use ($type, $user) { + return $mail->hasTo($user->email) && $mail->assertHasSubject('A checkout you initiated was ' . $type); }); } @@ -101,7 +100,7 @@ class CheckoutResponseEmailTest extends TestCase { $this->actingAs($checkoutAcceptance->assignedTo) ->post(route('account.store-acceptance', $checkoutAcceptance), [ - 'asset_acceptance' => 'declined', + 'asset_acceptance' => 'rejected', 'note' => null, ]); } From ae98f6276e4edc60dc4c2749f07c31024a6625d7 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 17:24:57 -0700 Subject: [PATCH 16/46] Replace reject with declined --- app/Mail/CheckoutAcceptanceResponseMail.php | 2 +- .../Feature/Notifications/Email/CheckoutResponseEmailTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index bf4aad5e8e..4e75d3e7e5 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -32,7 +32,7 @@ class CheckoutAcceptanceResponseMail extends Mailable public function envelope(): Envelope { return new Envelope( - subject: 'A checkout you initiated was ' . ($this->wasAccepted ? 'accepted' : 'rejected'), + subject: 'A checkout you initiated was ' . ($this->wasAccepted ? 'accepted' : 'declined'), ); } diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index 9174e5656d..4fe996461a 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -42,7 +42,7 @@ class CheckoutResponseEmailTest extends TestCase $this->declineCheckout($checkoutAcceptance); - $this->assertEmailSentTo($initiator, 'rejected'); + $this->assertEmailSentTo($initiator, 'declined'); } public function test_accepting_checkout_acceptance_not_configured_to_send_alert() @@ -100,7 +100,7 @@ class CheckoutResponseEmailTest extends TestCase { $this->actingAs($checkoutAcceptance->assignedTo) ->post(route('account.store-acceptance', $checkoutAcceptance), [ - 'asset_acceptance' => 'rejected', + 'asset_acceptance' => 'declined', 'note' => null, ]); } From cd53fc631833856ddf156824bbde3083c0285b21 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 11 Jun 2025 16:56:40 -0700 Subject: [PATCH 17/46] Scaffold email contents --- .../checkout-acceptance-response.blade.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/resources/views/mail/markdown/checkout-acceptance-response.blade.php b/resources/views/mail/markdown/checkout-acceptance-response.blade.php index 472e39b308..c616f98415 100644 --- a/resources/views/mail/markdown/checkout-acceptance-response.blade.php +++ b/resources/views/mail/markdown/checkout-acceptance-response.blade.php @@ -1,6 +1,22 @@ @component('mail::message') # {{ trans('mail.hello') }} {{ $recipient->present()->fullName() }}, +{fullName} {accepted|declined} the following checkout: + +@component('mail::table') +| | | +| ------------- | ------------- | +@if ((isset($item->name)) && ($item->name!='')) + | **{{ trans('mail.name') }}** | {{ $item->name }} | +@endif +@if (isset($item->asset_tag)) + | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | +@endif +@if (isset($note) && $note != '') + | **{{ trans('mail.notes') }}** | {{ $note }} | +@endif +@endcomponent + {{ trans('mail.best_regards') }} {{ $snipeSettings->site_name }} From cc0ff1ec1f59d97d30d9cea8946b8c3328c0f019 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 11:53:30 -0700 Subject: [PATCH 18/46] Handle missing recipient --- .../Controllers/Account/AcceptanceController.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php index adb423581b..6c44d9b846 100644 --- a/app/Http/Controllers/Account/AcceptanceController.php +++ b/app/Http/Controllers/Account/AcceptanceController.php @@ -342,13 +342,14 @@ class AcceptanceController extends Controller if ($acceptance->alert_on_response_id) { try { - $recipient = User::findOrFail($acceptance->alert_on_response_id); + $recipient = User::find($acceptance->alert_on_response_id); - Mail::to($recipient) - ->send(new CheckoutAcceptanceResponseMail( - $recipient, - $request->input('asset_acceptance') === 'accepted') - ); + if ($recipient) { + Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail( + $recipient, + $request->input('asset_acceptance') === 'accepted' + )); + } } catch (Exception $e) { Log::warning($e); } From 628d2a0a0a821f3366de4ab691c3e54d3be19d6f Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 12:12:38 -0700 Subject: [PATCH 19/46] Flesh out mail contents --- .../Account/AcceptanceController.php | 1 + app/Mail/CheckoutAcceptanceResponseMail.php | 27 ++++++++++++++++--- .../checkout-acceptance-response.blade.php | 11 ++++---- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php index 6c44d9b846..67d27310a0 100644 --- a/app/Http/Controllers/Account/AcceptanceController.php +++ b/app/Http/Controllers/Account/AcceptanceController.php @@ -346,6 +346,7 @@ class AcceptanceController extends Controller if ($recipient) { Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail( + $acceptance, $recipient, $request->input('asset_acceptance') === 'accepted' )); diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index 4e75d3e7e5..b568d87765 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Models\CheckoutAcceptance; use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -14,14 +15,16 @@ class CheckoutAcceptanceResponseMail extends Mailable { use Queueable, SerializesModels; - public bool $wasAccepted; + public CheckoutAcceptance $acceptance; public User $recipient; + public bool $wasAccepted; /** * Create a new message instance. */ - public function __construct(User $recipient, bool $wasAccepted) + public function __construct(CheckoutAcceptance $acceptance, User $recipient, bool $wasAccepted) { + $this->acceptance = $acceptance; $this->recipient = $recipient; $this->wasAccepted = $wasAccepted; } @@ -31,8 +34,13 @@ class CheckoutAcceptanceResponseMail extends Mailable */ public function envelope(): Envelope { + // @todo: translate + $subject = $this->wasAccepted + ? 'A checkout you initiated was accepted' + : 'A checkout you initiated was declined'; + return new Envelope( - subject: 'A checkout you initiated was ' . ($this->wasAccepted ? 'accepted' : 'declined'), + subject: $subject, ); } @@ -44,12 +52,23 @@ class CheckoutAcceptanceResponseMail extends Mailable return new Content( markdown: 'mail.markdown.checkout-acceptance-response', with: [ + 'assignedTo' => $this->acceptance->assignedTo, + 'introduction' => $this->introduction(), + 'item' => $this->acceptance->checkoutable, + 'note' => $this->acceptance->note, 'recipient' => $this->recipient, - 'wasAccepted' => $this->wasAccepted, ] ); } + private function introduction(): string + { + // @todo: translate + return $this->wasAccepted + ? 'The following was accepted' + : 'The following was declined'; + } + /** * Get the attachments for the message. * diff --git a/resources/views/mail/markdown/checkout-acceptance-response.blade.php b/resources/views/mail/markdown/checkout-acceptance-response.blade.php index c616f98415..aa80dc49fe 100644 --- a/resources/views/mail/markdown/checkout-acceptance-response.blade.php +++ b/resources/views/mail/markdown/checkout-acceptance-response.blade.php @@ -1,19 +1,20 @@ @component('mail::message') # {{ trans('mail.hello') }} {{ $recipient->present()->fullName() }}, -{fullName} {accepted|declined} the following checkout: +{{ $introduction }}: @component('mail::table') | | | | ------------- | ------------- | +| **{{ trans('mail.user') }}** | {{ $assignedTo->present()->fullName() }} | @if ((isset($item->name)) && ($item->name!='')) - | **{{ trans('mail.name') }}** | {{ $item->name }} | +| **{{ trans('mail.name') }}** | {{ $item->name }} | @endif @if (isset($item->asset_tag)) - | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | +| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | @endif -@if (isset($note) && $note != '') - | **{{ trans('mail.notes') }}** | {{ $note }} | +@if ($note != '') +| **{{ trans('mail.notes') }}** | {{ $note }} | @endif @endcomponent From c9778a73c70b83d84c2a8393b84ed6883e6e1a20 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 12:32:34 -0700 Subject: [PATCH 20/46] Wire up category controller --- app/Http/Controllers/CategoriesController.php | 2 ++ resources/views/livewire/category-edit-form.blade.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/CategoriesController.php b/app/Http/Controllers/CategoriesController.php index 692e2745ac..3e902541b3 100755 --- a/app/Http/Controllers/CategoriesController.php +++ b/app/Http/Controllers/CategoriesController.php @@ -68,6 +68,7 @@ class CategoriesController extends Controller $category->eula_text = $request->input('eula_text'); $category->use_default_eula = $request->input('use_default_eula', '0'); $category->require_acceptance = $request->input('require_acceptance', '0'); + $category->alert_on_response = $request->input('alert_on_response', '0'); $category->checkin_email = $request->input('checkin_email', '0'); $category->notes = $request->input('notes'); $category->created_by = auth()->id(); @@ -121,6 +122,7 @@ class CategoriesController extends Controller $category->eula_text = $request->input('eula_text'); $category->use_default_eula = $request->input('use_default_eula', '0'); $category->require_acceptance = $request->input('require_acceptance', '0'); + $category->alert_on_response = $request->input('alert_on_response', '0'); $category->checkin_email = $request->input('checkin_email', '0'); $category->notes = $request->input('notes'); diff --git a/resources/views/livewire/category-edit-form.blade.php b/resources/views/livewire/category-edit-form.blade.php index b1b0232d30..94c9eac09f 100644 --- a/resources/views/livewire/category-edit-form.blade.php +++ b/resources/views/livewire/category-edit-form.blade.php @@ -72,7 +72,7 @@ type="checkbox" name="alert_on_response" value="1" - wire:model.blur="alertOnResponse" + wire:model="alertOnResponse" /> Send email to you when user accepts or declines checkout. From 95027e329cf67ef69bee8a5b5a4b9a3601dca3f4 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 12:44:29 -0700 Subject: [PATCH 21/46] Formatting --- app/Http/Controllers/Account/AcceptanceController.php | 2 +- tests/Feature/Categories/Ui/UpdateCategoriesTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php index 67d27310a0..c041cfa6b2 100644 --- a/app/Http/Controllers/Account/AcceptanceController.php +++ b/app/Http/Controllers/Account/AcceptanceController.php @@ -348,7 +348,7 @@ class AcceptanceController extends Controller Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail( $acceptance, $recipient, - $request->input('asset_acceptance') === 'accepted' + $request->input('asset_acceptance') === 'accepted', )); } } catch (Exception $e) { diff --git a/tests/Feature/Categories/Ui/UpdateCategoriesTest.php b/tests/Feature/Categories/Ui/UpdateCategoriesTest.php index 8ecfb2a527..9d984fbbfe 100644 --- a/tests/Feature/Categories/Ui/UpdateCategoriesTest.php +++ b/tests/Feature/Categories/Ui/UpdateCategoriesTest.php @@ -70,7 +70,6 @@ class UpdateCategoriesTest extends TestCase 'require_acceptance' => 1, 'alert_on_response' => 1, ]); - } public function testUserCanChangeCategoryTypeIfNoAssetsAssociated() From 34636016ebc6d54b87fba061bb8cf6eead916204 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 12:47:03 -0700 Subject: [PATCH 22/46] Improve test --- .../Feature/Categories/Ui/CreateCategoriesTest.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Categories/Ui/CreateCategoriesTest.php b/tests/Feature/Categories/Ui/CreateCategoriesTest.php index 694b61c613..f2a45c6116 100644 --- a/tests/Feature/Categories/Ui/CreateCategoriesTest.php +++ b/tests/Feature/Categories/Ui/CreateCategoriesTest.php @@ -34,11 +34,20 @@ class CreateCategoriesTest extends TestCase ->post(route('categories.store'), [ 'name' => 'Test Category', 'category_type' => 'asset', - 'notes' => 'Test Note', + 'eula_text' => 'Sample text', + 'require_acceptance' => '1', + 'notes' => 'My Note', ]) ->assertRedirect(route('categories.index')); - $this->assertTrue(Category::where('name', 'Test Category')->where('notes', 'Test Note')->exists()); + $this->assertDatabaseHas('categories', [ + 'name' => 'Test Category', + 'category_type' => 'asset', + 'eula_text' => 'Sample text', + 'notes' => 'My Note', + 'require_acceptance' => 1, + 'alert_on_response' => 0, + ]); } public function testUserCannotCreateCategoriesWithInvalidType() From a0f40c2dfb6ebee191938284497e41cec74a9c3b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 12:56:24 -0700 Subject: [PATCH 23/46] Tests --- .../Categories/Api/CreateCategoriesTest.php | 7 ++-- .../Categories/Api/UpdateCategoriesTest.php | 35 ++++++++++++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Categories/Api/CreateCategoriesTest.php b/tests/Feature/Categories/Api/CreateCategoriesTest.php index 2a4a0661a6..92bdb1558b 100644 --- a/tests/Feature/Categories/Api/CreateCategoriesTest.php +++ b/tests/Feature/Categories/Api/CreateCategoriesTest.php @@ -11,8 +11,6 @@ use Tests\TestCase; class CreateCategoriesTest extends TestCase { - - public function testRequiresPermissionToCreateCategory() { $this->actingAsForApi(User::factory()->create()) @@ -28,6 +26,8 @@ class CreateCategoriesTest extends TestCase 'eula_text' => 'Test EULA', 'category_type' => 'accessory', 'notes' => 'Test Note', + 'require_acceptance' => true, + 'alert_on_response' => true, ]) ->assertOk() ->assertStatusMessageIs('success') @@ -41,6 +41,8 @@ class CreateCategoriesTest extends TestCase $this->assertEquals('Test EULA', $category->eula_text); $this->assertEquals('Test Note', $category->notes); $this->assertEquals('accessory', $category->category_type); + $this->assertEquals(1, $category->require_acceptance); + $this->assertEquals(1, $category->alert_on_response); } public function testCannotCreateCategoryWithoutCategoryType() @@ -81,5 +83,4 @@ class CreateCategoriesTest extends TestCase $this->assertFalse(Category::where('name', 'Test Category')->exists()); } - } diff --git a/tests/Feature/Categories/Api/UpdateCategoriesTest.php b/tests/Feature/Categories/Api/UpdateCategoriesTest.php index 6d6bc8da10..4823460e67 100644 --- a/tests/Feature/Categories/Api/UpdateCategoriesTest.php +++ b/tests/Feature/Categories/Api/UpdateCategoriesTest.php @@ -8,6 +8,40 @@ use Tests\TestCase; class UpdateCategoriesTest extends TestCase { + public function test_requires_permission_to_update_category() + { + $category = Category::factory()->create(); + + $this->actingAsForApi(User::factory()->create()) + ->postJson(route('api.categories.update', $category)) + ->assertForbidden(); + } + + public function test_can_update_category() + { + $category = Category::factory()->forAssets()->create([ + 'name' => 'Test Category', + 'require_acceptance' => false, + 'alert_on_response' => false, + ]); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->patchJson(route('api.categories.update', $category), [ + 'name' => 'Test Category Edited', + 'notes' => 'Test Note Edited', + 'require_acceptance' => true, + 'alert_on_response' => true, + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->assertStatus(200); + + $category->refresh(); + $this->assertEquals('Test Category Edited', $category->name, 'Name was not updated'); + $this->assertEquals('Test Note Edited', $category->notes, 'Note was not updated'); + $this->assertEquals(1, $category->require_acceptance, 'Require acceptance was not updated'); + $this->assertTrue($category->alert_on_response, 'Alert on response was not updated'); + } public function testCanUpdateCategoryViaPatchWithoutCategoryType() { @@ -55,5 +89,4 @@ class UpdateCategoriesTest extends TestCase $this->assertNotEquals('accessory', $category->category_type, 'EULA was not updated'); } - } From 5711a9e148dc540ee453baf22051c1095019091e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 13:02:26 -0700 Subject: [PATCH 24/46] Fix test --- tests/Feature/Categories/Api/UpdateCategoriesTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Categories/Api/UpdateCategoriesTest.php b/tests/Feature/Categories/Api/UpdateCategoriesTest.php index 4823460e67..19e8e9e0da 100644 --- a/tests/Feature/Categories/Api/UpdateCategoriesTest.php +++ b/tests/Feature/Categories/Api/UpdateCategoriesTest.php @@ -13,7 +13,7 @@ class UpdateCategoriesTest extends TestCase $category = Category::factory()->create(); $this->actingAsForApi(User::factory()->create()) - ->postJson(route('api.categories.update', $category)) + ->patchJson(route('api.categories.update', $category)) ->assertForbidden(); } From 32526d77b845e29bac47b2036c9f6c635ce668ed Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 13:35:22 -0700 Subject: [PATCH 25/46] 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 52a30f4772..062cadab4f 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -236,6 +236,7 @@ class CheckoutableListener $acceptance->checkoutable()->associate($event->checkoutable); $acceptance->assignedTo()->associate($event->checkedOutTo); + // @todo: adjust for other categories if (data_get($event, 'checkoutable.model.category.alert_on_response')) { $acceptance->alert_on_response_id = auth()->id(); } From a60ffc07029877335923bf451357627c0bc0dd91 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 13:46:05 -0700 Subject: [PATCH 26/46] Method signature change --- .../Checkouts/General/SettingAlertOnResponseTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index c32f8d3472..f9846dd270 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -29,7 +29,7 @@ class SettingAlertOnResponseTest extends TestCase 'alert_on_response' => true, ]); - $this->postCheckout(); + $this->postCheckout($this->asset); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, @@ -46,7 +46,7 @@ class SettingAlertOnResponseTest extends TestCase 'alert_on_response' => false, ]); - $this->postCheckout(); + $this->postCheckout($this->asset); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, @@ -56,10 +56,10 @@ class SettingAlertOnResponseTest extends TestCase ]); } - private function postCheckout(): void + private function postCheckout($item): void { $this->actingAs($this->actor) - ->post(route('hardware.checkout.store', $this->asset), [ + ->post(route('hardware.checkout.store', $item), [ 'checkout_to_type' => 'user', 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, 'assigned_user' => $this->assignedUser->id, From 74b7d27408dc003bf789e99ebc1df43347ef4508 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 13:49:19 -0700 Subject: [PATCH 27/46] Inline some values --- .../General/SettingAlertOnResponseTest.php | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index f9846dd270..42d22fdfc3 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -3,37 +3,50 @@ namespace Tests\Feature\Checkouts\General; use App\Models\Asset; +use App\Models\Category; use App\Models\Statuslabel; use App\Models\User; use Tests\TestCase; class SettingAlertOnResponseTest extends TestCase { - private Asset $asset; private User $actor; private User $assignedUser; + private Category $categoryThatAlerts; + private Category $categoryThatDoesNotAlert; + protected function setUp(): void { parent::setUp(); - $this->asset = Asset::factory()->create(); $this->actor = User::factory()->checkoutAssets()->create(); $this->assignedUser = User::factory()->create(); - } - public function test_sets_alert_on_response_if_enabled_by_category() - { - $this->asset->model->category->update([ + $this->categoryThatAlerts = Category::factory()->create([ 'require_acceptance' => true, 'alert_on_response' => true, ]); - $this->postCheckout($this->asset); + $this->categoryThatDoesNotAlert = Category::factory()->create([ + 'require_acceptance' => true, + 'alert_on_response' => false, + ]); + } + + public function test_sets_alert_on_response_if_enabled_by_category() + { + $asset = Asset::factory()->create(); + + $asset->model->update([ + 'category_id' => $this->categoryThatAlerts->id, + ]); + + $this->postCheckout($asset); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, - 'checkoutable_id' => $this->asset->id, + 'checkoutable_id' => $asset->id, 'assigned_to_id' => $this->assignedUser->id, 'alert_on_response_id' => $this->actor->id, ]); @@ -41,16 +54,17 @@ class SettingAlertOnResponseTest extends TestCase public function test_does_not_set_alert_on_response_if_disabled_by_category() { - $this->asset->model->category->update([ - 'require_acceptance' => true, - 'alert_on_response' => false, + $asset = Asset::factory()->create(); + + $asset->model->update([ + 'category_id' => $this->categoryThatDoesNotAlert->id, ]); - $this->postCheckout($this->asset); + $this->postCheckout($asset); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, - 'checkoutable_id' => $this->asset->id, + 'checkoutable_id' => $asset->id, 'assigned_to_id' => $this->assignedUser->id, 'alert_on_response_id' => null, ]); From 62b8e4c46f0b5e3f82a7c6254cbf0627ef051127 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 13:54:46 -0700 Subject: [PATCH 28/46] Revert "Inline some values" This reverts commit 74b7d27408dc003bf789e99ebc1df43347ef4508. --- .../General/SettingAlertOnResponseTest.php | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index 42d22fdfc3..f9846dd270 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -3,50 +3,37 @@ namespace Tests\Feature\Checkouts\General; use App\Models\Asset; -use App\Models\Category; use App\Models\Statuslabel; use App\Models\User; use Tests\TestCase; class SettingAlertOnResponseTest extends TestCase { + private Asset $asset; private User $actor; private User $assignedUser; - private Category $categoryThatAlerts; - private Category $categoryThatDoesNotAlert; - protected function setUp(): void { parent::setUp(); + $this->asset = Asset::factory()->create(); $this->actor = User::factory()->checkoutAssets()->create(); $this->assignedUser = User::factory()->create(); - - $this->categoryThatAlerts = Category::factory()->create([ - 'require_acceptance' => true, - 'alert_on_response' => true, - ]); - - $this->categoryThatDoesNotAlert = Category::factory()->create([ - 'require_acceptance' => true, - 'alert_on_response' => false, - ]); } public function test_sets_alert_on_response_if_enabled_by_category() { - $asset = Asset::factory()->create(); - - $asset->model->update([ - 'category_id' => $this->categoryThatAlerts->id, + $this->asset->model->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => true, ]); - $this->postCheckout($asset); + $this->postCheckout($this->asset); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, - 'checkoutable_id' => $asset->id, + 'checkoutable_id' => $this->asset->id, 'assigned_to_id' => $this->assignedUser->id, 'alert_on_response_id' => $this->actor->id, ]); @@ -54,17 +41,16 @@ class SettingAlertOnResponseTest extends TestCase public function test_does_not_set_alert_on_response_if_disabled_by_category() { - $asset = Asset::factory()->create(); - - $asset->model->update([ - 'category_id' => $this->categoryThatDoesNotAlert->id, + $this->asset->model->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => false, ]); - $this->postCheckout($asset); + $this->postCheckout($this->asset); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, - 'checkoutable_id' => $asset->id, + 'checkoutable_id' => $this->asset->id, 'assigned_to_id' => $this->assignedUser->id, 'alert_on_response_id' => null, ]); From 19cce15e543190718758e88bce502de6ad06b728 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 13:54:53 -0700 Subject: [PATCH 29/46] Revert "Method signature change" This reverts commit a60ffc07029877335923bf451357627c0bc0dd91. --- .../Checkouts/General/SettingAlertOnResponseTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index f9846dd270..c32f8d3472 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -29,7 +29,7 @@ class SettingAlertOnResponseTest extends TestCase 'alert_on_response' => true, ]); - $this->postCheckout($this->asset); + $this->postCheckout(); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, @@ -46,7 +46,7 @@ class SettingAlertOnResponseTest extends TestCase 'alert_on_response' => false, ]); - $this->postCheckout($this->asset); + $this->postCheckout(); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, @@ -56,10 +56,10 @@ class SettingAlertOnResponseTest extends TestCase ]); } - private function postCheckout($item): void + private function postCheckout(): void { $this->actingAs($this->actor) - ->post(route('hardware.checkout.store', $item), [ + ->post(route('hardware.checkout.store', $this->asset), [ 'checkout_to_type' => 'user', 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, 'assigned_user' => $this->assignedUser->id, From 6586858284b8946308c05d1e677b24bce9f10d19 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 15:31:32 -0700 Subject: [PATCH 30/46] Scaffold some test cases --- .../General/SettingAlertOnResponseTest.php | 83 +++++++++++++++---- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index c32f8d3472..29da7990e8 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -3,13 +3,15 @@ namespace Tests\Feature\Checkouts\General; use App\Models\Asset; +use App\Models\Category; use App\Models\Statuslabel; use App\Models\User; use Tests\TestCase; class SettingAlertOnResponseTest extends TestCase { - private Asset $asset; + private Category $categoryThatAlerts; + private Category $categoryThatDoesNotAlert; private User $actor; private User $assignedUser; @@ -17,49 +19,94 @@ class SettingAlertOnResponseTest extends TestCase { parent::setUp(); - $this->asset = Asset::factory()->create(); $this->actor = User::factory()->checkoutAssets()->create(); $this->assignedUser = User::factory()->create(); - } - - public function test_sets_alert_on_response_if_enabled_by_category() - { - $this->asset->model->category->update([ + $this->categoryThatAlerts = Category::factory()->create([ 'require_acceptance' => true, 'alert_on_response' => true, ]); + $this->categoryThatDoesNotAlert = Category::factory()->create([ + 'require_acceptance' => true, + 'alert_on_response' => false, + ]); + } - $this->postCheckout(); + public function test_sets_alert_on_response_if_enabled_by_category_for_accessory() + { + $this->markTestIncomplete(); + } + + public function test_does_not_set_alert_on_response_if_disabled_by_category_for_accessory() + { + $this->markTestIncomplete(); + } + + public function test_sets_alert_on_response_if_enabled_by_category_for_asset() + { + $asset = Asset::factory()->create(); + + $asset->model->update(['category_id' => $this->categoryThatAlerts->id]); + + $this->postAssetCheckout($asset); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, - 'checkoutable_id' => $this->asset->id, + 'checkoutable_id' => $asset->id, 'assigned_to_id' => $this->assignedUser->id, 'alert_on_response_id' => $this->actor->id, ]); } - public function test_does_not_set_alert_on_response_if_disabled_by_category() + public function test_does_not_set_alert_on_response_if_disabled_by_category_for_asset() { - $this->asset->model->category->update([ - 'require_acceptance' => true, - 'alert_on_response' => false, - ]); + $asset = Asset::factory()->create(); - $this->postCheckout(); + $asset->model->update(['category_id' => $this->categoryThatDoesNotAlert->id]); + + $this->postAssetCheckout($asset); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, - 'checkoutable_id' => $this->asset->id, + 'checkoutable_id' => $asset->id, 'assigned_to_id' => $this->assignedUser->id, 'alert_on_response_id' => null, ]); } - private function postCheckout(): void + public function test_sets_alert_on_response_if_enabled_by_category_for_component() + { + $this->markTestIncomplete(); + } + + public function test_does_not_set_alert_on_response_if_disabled_by_category_for_component() + { + $this->markTestIncomplete(); + } + + public function test_sets_alert_on_response_if_enabled_by_category_for_consumable() + { + $this->markTestIncomplete(); + } + + public function test_does_not_set_alert_on_response_if_disabled_by_category_for_consumable() + { + $this->markTestIncomplete(); + } + + public function test_sets_alert_on_response_if_enabled_by_category_for_license() + { + $this->markTestIncomplete(); + } + + public function test_does_not_set_alert_on_response_if_disabled_by_category_for_license() + { + $this->markTestIncomplete(); + } + + private function postAssetCheckout(Asset $asset): void { $this->actingAs($this->actor) - ->post(route('hardware.checkout.store', $this->asset), [ + ->post(route('hardware.checkout.store', $asset), [ 'checkout_to_type' => 'user', 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, 'assigned_user' => $this->assignedUser->id, From 45a42b00ad2c4ba8b6d8b53793909d102314e5ca Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 15:48:48 -0700 Subject: [PATCH 31/46] Add test --- .../General/SettingAlertOnResponseTest.php | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index 29da7990e8..2435d1c01d 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -2,16 +2,14 @@ namespace Tests\Feature\Checkouts\General; +use App\Models\Accessory; use App\Models\Asset; -use App\Models\Category; use App\Models\Statuslabel; use App\Models\User; use Tests\TestCase; class SettingAlertOnResponseTest extends TestCase { - private Category $categoryThatAlerts; - private Category $categoryThatDoesNotAlert; private User $actor; private User $assignedUser; @@ -19,21 +17,32 @@ class SettingAlertOnResponseTest extends TestCase { parent::setUp(); - $this->actor = User::factory()->checkoutAssets()->create(); + $this->actor = User::factory()->superuser()->create(); $this->assignedUser = User::factory()->create(); - $this->categoryThatAlerts = Category::factory()->create([ - 'require_acceptance' => true, - 'alert_on_response' => true, - ]); - $this->categoryThatDoesNotAlert = Category::factory()->create([ - 'require_acceptance' => true, - 'alert_on_response' => false, - ]); } public function test_sets_alert_on_response_if_enabled_by_category_for_accessory() { - $this->markTestIncomplete(); + $accessory = Accessory::factory()->create(); + $accessory->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => true, + ]); + + $this->actingAs($this->actor) + ->post(route('accessories.checkout.store', $accessory), [ + 'checkout_to_type' => 'user', + 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, + 'assigned_user' => $this->assignedUser->id, + 'checkout_qty' => 1, + ]); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => Accessory::class, + 'checkoutable_id' => $accessory->id, + 'assigned_to_id' => $this->assignedUser->id, + 'alert_on_response_id' => $this->actor->id, + ]); } public function test_does_not_set_alert_on_response_if_disabled_by_category_for_accessory() @@ -44,8 +53,10 @@ class SettingAlertOnResponseTest extends TestCase public function test_sets_alert_on_response_if_enabled_by_category_for_asset() { $asset = Asset::factory()->create(); - - $asset->model->update(['category_id' => $this->categoryThatAlerts->id]); + $asset->model->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => true, + ]); $this->postAssetCheckout($asset); @@ -60,8 +71,10 @@ class SettingAlertOnResponseTest extends TestCase public function test_does_not_set_alert_on_response_if_disabled_by_category_for_asset() { $asset = Asset::factory()->create(); - - $asset->model->update(['category_id' => $this->categoryThatDoesNotAlert->id]); + $asset->model->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => false, + ]); $this->postAssetCheckout($asset); From 6f99381d130145cec0aaf57d0753b25f37f46769 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 15:49:38 -0700 Subject: [PATCH 32/46] Handle accessories --- app/Listeners/CheckoutableListener.php | 2 +- .../General/SettingAlertOnResponseTest.php | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 062cadab4f..5a90f30632 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -237,7 +237,7 @@ class CheckoutableListener $acceptance->assignedTo()->associate($event->checkedOutTo); // @todo: adjust for other categories - if (data_get($event, 'checkoutable.model.category.alert_on_response')) { + if (data_get($event, 'checkoutable.model.category.alert_on_response') || data_get($event, 'checkoutable.category.alert_on_response')) { $acceptance->alert_on_response_id = auth()->id(); } diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index 2435d1c01d..0e6ce8d3d0 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -47,7 +47,26 @@ class SettingAlertOnResponseTest extends TestCase public function test_does_not_set_alert_on_response_if_disabled_by_category_for_accessory() { - $this->markTestIncomplete(); + $accessory = Accessory::factory()->create(); + $accessory->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => false, + ]); + + $this->actingAs($this->actor) + ->post(route('accessories.checkout.store', $accessory), [ + 'checkout_to_type' => 'user', + 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, + 'assigned_user' => $this->assignedUser->id, + 'checkout_qty' => 1, + ]); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => Accessory::class, + 'checkoutable_id' => $accessory->id, + 'assigned_to_id' => $this->assignedUser->id, + 'alert_on_response_id' => null, + ]); } public function test_sets_alert_on_response_if_enabled_by_category_for_asset() From 297205ff912bc4579b028375fdb34339b491a7cc Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 15:55:25 -0700 Subject: [PATCH 33/46] Tests --- .../General/SettingAlertOnResponseTest.php | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index 0e6ce8d3d0..aca4731aef 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -29,13 +29,7 @@ class SettingAlertOnResponseTest extends TestCase 'alert_on_response' => true, ]); - $this->actingAs($this->actor) - ->post(route('accessories.checkout.store', $accessory), [ - 'checkout_to_type' => 'user', - 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, - 'assigned_user' => $this->assignedUser->id, - 'checkout_qty' => 1, - ]); + $this->postAccessoryCheckout($accessory); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Accessory::class, @@ -53,13 +47,7 @@ class SettingAlertOnResponseTest extends TestCase 'alert_on_response' => false, ]); - $this->actingAs($this->actor) - ->post(route('accessories.checkout.store', $accessory), [ - 'checkout_to_type' => 'user', - 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, - 'assigned_user' => $this->assignedUser->id, - 'checkout_qty' => 1, - ]); + $this->postAccessoryCheckout($accessory); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Accessory::class, @@ -105,16 +93,6 @@ class SettingAlertOnResponseTest extends TestCase ]); } - public function test_sets_alert_on_response_if_enabled_by_category_for_component() - { - $this->markTestIncomplete(); - } - - public function test_does_not_set_alert_on_response_if_disabled_by_category_for_component() - { - $this->markTestIncomplete(); - } - public function test_sets_alert_on_response_if_enabled_by_category_for_consumable() { $this->markTestIncomplete(); @@ -144,4 +122,15 @@ class SettingAlertOnResponseTest extends TestCase 'assigned_user' => $this->assignedUser->id, ]); } + + private function postAccessoryCheckout(Accessory $accessory): void + { + $this->actingAs($this->actor) + ->post(route('accessories.checkout.store', $accessory), [ + 'checkout_to_type' => 'user', + 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, + 'assigned_user' => $this->assignedUser->id, + 'checkout_qty' => 1, + ]); + } } From 7f35498919815ceded7a138a6888acf1e2065eba Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 12 Jun 2025 15:56:24 -0700 Subject: [PATCH 34/46] Remove some test cases --- .../General/SettingAlertOnResponseTest.php | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index aca4731aef..662e550017 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -93,26 +93,6 @@ class SettingAlertOnResponseTest extends TestCase ]); } - public function test_sets_alert_on_response_if_enabled_by_category_for_consumable() - { - $this->markTestIncomplete(); - } - - public function test_does_not_set_alert_on_response_if_disabled_by_category_for_consumable() - { - $this->markTestIncomplete(); - } - - public function test_sets_alert_on_response_if_enabled_by_category_for_license() - { - $this->markTestIncomplete(); - } - - public function test_does_not_set_alert_on_response_if_disabled_by_category_for_license() - { - $this->markTestIncomplete(); - } - private function postAssetCheckout(Asset $asset): void { $this->actingAs($this->actor) From 58af133853e4e571171767e348925fbd1e917f1a Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 11:51:33 -0700 Subject: [PATCH 35/46] Re-add some test cases --- .../Checkouts/General/SettingAlertOnResponseTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index 662e550017..ac6a958104 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -93,6 +93,16 @@ class SettingAlertOnResponseTest extends TestCase ]); } + public function test_sets_alert_on_response_if_enabled_by_category_for_license() + { + $this->markTestIncomplete(); + } + + public function test_does_not_set_alert_on_response_if_disabled_by_category_for_license() + { + $this->markTestIncomplete(); + } + private function postAssetCheckout(Asset $asset): void { $this->actingAs($this->actor) From ff4819ac6854661acd154b2eccd289d92b02918a Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 12:34:16 -0700 Subject: [PATCH 36/46] Get licenses working --- app/Listeners/CheckoutableListener.php | 16 ++++++- .../General/SettingAlertOnResponseTest.php | 43 ++++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 5a90f30632..46fbd1589b 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -12,6 +12,7 @@ use App\Mail\CheckoutConsumableMail; use App\Mail\CheckoutLicenseMail; use App\Models\Accessory; use App\Models\Asset; +use App\Models\Category; use App\Models\CheckoutAcceptance; use App\Models\Component; use App\Models\Consumable; @@ -236,8 +237,9 @@ class CheckoutableListener $acceptance->checkoutable()->associate($event->checkoutable); $acceptance->assignedTo()->associate($event->checkedOutTo); - // @todo: adjust for other categories - if (data_get($event, 'checkoutable.model.category.alert_on_response') || data_get($event, 'checkoutable.category.alert_on_response')) { + $category = $this->getCategoryFromCheckoutable($event->checkoutable); + + if ($category?->alert_on_response) { $acceptance->alert_on_response_id = auth()->id(); } @@ -468,4 +470,14 @@ class CheckoutableListener return array($to, $cc); } + + private function getCategoryFromCheckoutable(Model $checkoutable): ?Category + { + return match (true) { + $checkoutable instanceof Asset => $checkoutable->model->category, + $checkoutable instanceof Accessory, + $checkoutable instanceof Consumable => $checkoutable->category, + $checkoutable instanceof LicenseSeat => $checkoutable->license->category, + }; + } } diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index ac6a958104..182d23f87e 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -4,6 +4,8 @@ namespace Tests\Feature\Checkouts\General; use App\Models\Accessory; use App\Models\Asset; +use App\Models\License; +use App\Models\LicenseSeat; use App\Models\Statuslabel; use App\Models\User; use Tests\TestCase; @@ -95,12 +97,40 @@ class SettingAlertOnResponseTest extends TestCase public function test_sets_alert_on_response_if_enabled_by_category_for_license() { - $this->markTestIncomplete(); + $license = License::factory()->create(); + + $license->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => true, + ]); + + $this->postLicenseCheckout($license); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => LicenseSeat::class, + 'checkoutable_id' => $license->id, + 'assigned_to_id' => $this->assignedUser->id, + 'alert_on_response_id' => $this->actor->id, + ]); } public function test_does_not_set_alert_on_response_if_disabled_by_category_for_license() { - $this->markTestIncomplete(); + $license = License::factory()->create(); + + $license->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => false, + ]); + + $this->postLicenseCheckout($license); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => LicenseSeat::class, + 'checkoutable_id' => $license->id, + 'assigned_to_id' => $this->assignedUser->id, + 'alert_on_response_id' => null, + ]); } private function postAssetCheckout(Asset $asset): void @@ -123,4 +153,13 @@ class SettingAlertOnResponseTest extends TestCase 'checkout_qty' => 1, ]); } + + private function postLicenseCheckout(License $license): void + { + $this->actingAs($this->actor) + ->post("/licenses/{$license->id}/checkout/", [ + 'checkout_to_type' => 'user', + 'assigned_to' => $this->assignedUser->id, + ]); + } } From 4ae8a910515a3b16cad65f5dedd5b43c798f00cc Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 12:34:43 -0700 Subject: [PATCH 37/46] Remove unused method --- app/Listeners/CheckoutableListener.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 46fbd1589b..0046b8c8a4 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -369,23 +369,6 @@ class CheckoutableListener return in_array(get_class($checkoutable), $this->skipNotificationsFor); } - private function shouldSendEmailNotifications(Model $checkoutable): bool - { - //runs a check if the category wants to send checkin/checkout emails to users - $category = match (true) { - $checkoutable instanceof Asset => $checkoutable->model->category, - $checkoutable instanceof Accessory, - $checkoutable instanceof Consumable => $checkoutable->category, - $checkoutable instanceof LicenseSeat => $checkoutable->license->category, - default => null, - }; - - if (!$category?->checkin_email) { - return false; - } - return true; - } - private function shouldSendWebhookNotification(): bool { return Setting::getSettings() && Setting::getSettings()->webhook_endpoint; From ef3827376d2622c42b6d1e21e871a328c1b62e83 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 13:17:42 -0700 Subject: [PATCH 38/46] Add todo --- resources/views/livewire/category-edit-form.blade.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/views/livewire/category-edit-form.blade.php b/resources/views/livewire/category-edit-form.blade.php index 94c9eac09f..1e7661055c 100644 --- a/resources/views/livewire/category-edit-form.blade.php +++ b/resources/views/livewire/category-edit-form.blade.php @@ -74,6 +74,7 @@ value="1" wire:model="alertOnResponse" /> + {{-- @todo: translate--}} Send email to you when user accepts or declines checkout.
From 3290d7f4017a33cc8e5e9b092ec5dde1f4abd841 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 13:27:05 -0700 Subject: [PATCH 39/46] Add translations --- app/Mail/CheckoutAcceptanceResponseMail.php | 10 ++++------ resources/lang/en-US/admin/categories/general.php | 1 + resources/lang/en-US/mail.php | 4 ++++ resources/views/livewire/category-edit-form.blade.php | 3 +-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index b568d87765..a5d6991b20 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -34,10 +34,9 @@ class CheckoutAcceptanceResponseMail extends Mailable */ public function envelope(): Envelope { - // @todo: translate $subject = $this->wasAccepted - ? 'A checkout you initiated was accepted' - : 'A checkout you initiated was declined'; + ? trans('mail.initiated_accepted') + : trans('mail.initiated_declined'); return new Envelope( subject: $subject, @@ -63,10 +62,9 @@ class CheckoutAcceptanceResponseMail extends Mailable private function introduction(): string { - // @todo: translate return $this->wasAccepted - ? 'The following was accepted' - : 'The following was declined'; + ? trans('mail.following_accepted') + : trans('mail.following_declined'); } /** diff --git a/resources/lang/en-US/admin/categories/general.php b/resources/lang/en-US/admin/categories/general.php index a468bf3e08..4d036996ac 100644 --- a/resources/lang/en-US/admin/categories/general.php +++ b/resources/lang/en-US/admin/categories/general.php @@ -4,6 +4,7 @@ return array( 'asset_categories' => 'Asset Categories', 'category_name' => 'Category Name', 'checkin_email' => 'Send email to user on checkin/checkout.', + 'email_to_initiator' => 'Send email to you when user accepts or declines checkout.', 'checkin_email_notification' => 'This user will be sent an email on checkin/checkout.', 'clone' => 'Clone Category', 'create' => 'Create Category', diff --git a/resources/lang/en-US/mail.php b/resources/lang/en-US/mail.php index 797f735a2b..65b544b343 100644 --- a/resources/lang/en-US/mail.php +++ b/resources/lang/en-US/mail.php @@ -52,9 +52,13 @@ return [ 'days' => 'Days', 'expecting_checkin_date' => 'Expected Checkin Date', 'expires' => 'Expires', + 'following_accepted' => 'The following was accepted', + 'following_declined' => 'The following was declined', 'hello' => 'Hello', 'hi' => 'Hi', 'i_have_read' => 'I have read and agree to the terms of use, and have received this item.', + 'initiated_accepted' => 'A checkout you initiated was accepted', + 'initiated_declined' => 'A checkout you initiated was declined', 'inventory_report' => 'Inventory Report', 'item' => 'Item', 'item_checked_reminder' => 'This is a reminder that you currently have :count items checked out to you that you have not accepted or declined. Please click the link below to confirm your decision.', diff --git a/resources/views/livewire/category-edit-form.blade.php b/resources/views/livewire/category-edit-form.blade.php index 1e7661055c..aee6fe63ed 100644 --- a/resources/views/livewire/category-edit-form.blade.php +++ b/resources/views/livewire/category-edit-form.blade.php @@ -74,8 +74,7 @@ value="1" wire:model="alertOnResponse" /> - {{-- @todo: translate--}} - Send email to you when user accepts or declines checkout. + {{ trans('admin/categories/general.email_to_initiator') }} From cba45ece1277a17f528cd80a5d7b80f4865c9081 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 13:57:40 -0700 Subject: [PATCH 40/46] Add image --- .../mail/markdown/checkout-acceptance-response.blade.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/views/mail/markdown/checkout-acceptance-response.blade.php b/resources/views/mail/markdown/checkout-acceptance-response.blade.php index aa80dc49fe..18e512f497 100644 --- a/resources/views/mail/markdown/checkout-acceptance-response.blade.php +++ b/resources/views/mail/markdown/checkout-acceptance-response.blade.php @@ -3,6 +3,10 @@ {{ $introduction }}: +@if (($snipeSettings->show_images_in_email =='1') && $item->getImageUrl()) +
Asset
+@endif + @component('mail::table') | | | | ------------- | ------------- | From bffaf477ea7bfb1809dd69522740a3bcfe0ecf11 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 14:22:44 -0700 Subject: [PATCH 41/46] Method order --- app/Mail/CheckoutAcceptanceResponseMail.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index a5d6991b20..fba6dd3de2 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -60,13 +60,6 @@ class CheckoutAcceptanceResponseMail extends Mailable ); } - private function introduction(): string - { - return $this->wasAccepted - ? trans('mail.following_accepted') - : trans('mail.following_declined'); - } - /** * Get the attachments for the message. * @@ -76,4 +69,11 @@ class CheckoutAcceptanceResponseMail extends Mailable { return []; } + + private function introduction(): string + { + return $this->wasAccepted + ? trans('mail.following_accepted') + : trans('mail.following_declined'); + } } From 67b32ca14d76d65487c595e41c9ef72ffd45e09a Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 14:22:54 -0700 Subject: [PATCH 42/46] Mail content improvements --- .../mail/markdown/checkout-acceptance-response.blade.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/views/mail/markdown/checkout-acceptance-response.blade.php b/resources/views/mail/markdown/checkout-acceptance-response.blade.php index 18e512f497..26554ee088 100644 --- a/resources/views/mail/markdown/checkout-acceptance-response.blade.php +++ b/resources/views/mail/markdown/checkout-acceptance-response.blade.php @@ -3,7 +3,7 @@ {{ $introduction }}: -@if (($snipeSettings->show_images_in_email =='1') && $item->getImageUrl()) +@if (($snipeSettings->show_images_in_email =='1') && (method_exists($item, 'getImageUrl') && $item->getImageUrl()))
Asset
@endif @@ -11,9 +11,7 @@ | | | | ------------- | ------------- | | **{{ trans('mail.user') }}** | {{ $assignedTo->present()->fullName() }} | -@if ((isset($item->name)) && ($item->name!='')) -| **{{ trans('mail.name') }}** | {{ $item->name }} | -@endif +| **{{ trans('mail.name') }}** | {{ $item->present()->name() }} | @if (isset($item->asset_tag)) | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | @endif From 6f3c5c44a5ded89a535cff986593624e1e93a498 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 17 Jun 2025 14:31:14 -0700 Subject: [PATCH 43/46] Remove assertion --- tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index 182d23f87e..5f45d3225c 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -108,7 +108,6 @@ class SettingAlertOnResponseTest extends TestCase $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => LicenseSeat::class, - 'checkoutable_id' => $license->id, 'assigned_to_id' => $this->assignedUser->id, 'alert_on_response_id' => $this->actor->id, ]); @@ -127,7 +126,6 @@ class SettingAlertOnResponseTest extends TestCase $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => LicenseSeat::class, - 'checkoutable_id' => $license->id, 'assigned_to_id' => $this->assignedUser->id, 'alert_on_response_id' => null, ]); From de4764bd052383c9e01e91f8f4139b4e12c33a74 Mon Sep 17 00:00:00 2001 From: Brady Wetherington Date: Tue, 24 Jun 2025 13:53:11 +0100 Subject: [PATCH 44/46] Limit changing of asset seat count to no more than 10k at a time --- app/Models/License.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/Models/License.php b/app/Models/License.php index 0997c1e57b..6e412f2fd7 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -43,7 +43,7 @@ class License extends Depreciable protected $rules = [ 'name' => 'required|string|min:3|max:255', - 'seats' => 'required|min:1|integer', + 'seats' => 'required|min:1|integer|limit_change:10000', // limit_change is a "pseudo-rule" that translates into 'between', see prepareLimitChangeRule() below 'license_email' => 'email|nullable|max:120', 'license_name' => 'string|nullable|max:100', 'notes' => 'string|nullable', @@ -148,6 +148,14 @@ class License extends Depreciable }); } + public function prepareLimitChangeRule($parameters, $field) + { + $actual_seat_count = $this->licenseseats()->count(); //we use the *actual* seat count here, in case your license has gone wonky + $lower_bound = $actual_seat_count - $parameters[0]; + $upper_bound = $actual_seat_count + $parameters[0]; + return ["between", ($lower_bound <= 0 ? 1 : $lower_bound), $upper_bound]; + } + /** * Balance seat counts * @@ -164,21 +172,17 @@ class License extends Depreciable // On Create, we just make one for each of the seats. $change = abs($oldSeats - $newSeats); if ($oldSeats > $newSeats) { - $license->load('licenseseats.user'); // Need to delete seats... lets see if if we have enough. - $seatsAvailableForDelete = $license->licenseseats->reject(function ($seat) { - return ((bool) $seat->assigned_to) || ((bool) $seat->asset_id); - }); + $seatsAvailableForDelete = $license->licenseseats()->whereNull('assigned_to')->whereNull('asset_id')->limit($change); if ($change > $seatsAvailableForDelete->count()) { Session::flash('error', trans('admin/licenses/message.assoc_users')); return false; } - for ($i = 1; $i <= $change; $i++) { - $seatsAvailableForDelete->pop()->delete(); - } + $seatsAvailableForDelete->delete(); + // Log Deletion of seats. $logAction = new Actionlog; $logAction->item_type = self::class; From a7a597d6095a2eb6b362877279a96c8484f51a0f Mon Sep 17 00:00:00 2001 From: Brady Wetherington Date: Tue, 24 Jun 2025 17:35:49 +0100 Subject: [PATCH 45/46] Added some tests around license seat changes --- .../Feature/Licenses/Ui/CreateLicenseTest.php | 44 +++++++++- .../Feature/Licenses/Ui/UpdateLicenseTest.php | 87 +++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Licenses/Ui/CreateLicenseTest.php b/tests/Feature/Licenses/Ui/CreateLicenseTest.php index 33f825bf95..535f0b3dba 100644 --- a/tests/Feature/Licenses/Ui/CreateLicenseTest.php +++ b/tests/Feature/Licenses/Ui/CreateLicenseTest.php @@ -2,7 +2,6 @@ namespace Tests\Feature\Licenses\Ui; -use App\Models\AssetModel; use App\Models\Category; use App\Models\License; use App\Models\Depreciation; @@ -41,7 +40,48 @@ class CreateLicenseTest extends TestCase $response->assertInvalid(['purchase_date']); $response->assertSessionHasErrors(['purchase_date']); $this->followRedirects($response)->assertSee(trans('general.error')); - $this->assertFalse(AssetModel::where('name', 'Test Invalid License')->exists()); + $this->assertFalse(License::where('name', 'Test Invalid License')->exists()); } + + public function testLicenseCreate() + { + $response = $this->actingAs(User::factory()->superuser()->create()) + ->from(route('licenses.create')) + ->post(route('licenses.store'), [ + 'name' => 'Test Valid License', + 'seats' => '10', + 'category_id' => Category::factory()->forLicenses()->create()->id, + ]); + $response->assertStatus(302); + $license = License::where('name', 'Test Valid License')->sole(); + $this->assertNotNull($license); + //$license->assetlog()->has_one_of_(); + $this->assertDatabaseHas('action_logs', ['action_type' => 'create', 'item_id' => $license->id, 'item_type' => License::class]); + $this->assertDatabaseHas('action_logs', ['action_type' => 'add seats', 'item_id' => $license->id, 'item_type' => License::class]); + $this->assertEquals($license->licenseseats()->count(), 10); + //test log entries? Sure. + + } + + public function testTooManySeatsLicenseCreate() + { + $response = $this->actingAs(User::factory()->superuser()->create()) + ->from(route('licenses.create')) + ->post(route('licenses.store'), [ + 'name' => 'Test Valid License', + 'seats' => '100000', + 'category_id' => Category::factory()->forLicenses()->create()->id, + ]); + $response->assertStatus(302); + $license = License::where('name', 'Test Valid License')->first(); + $this->assertNull($license); + //$license->assetlog()->has_one_of_(); +// $this->assertDatabaseMissing('action_logs', ['action_type' => 'create', 'item_id' => $license->id, 'item_type' => License::class]); +// $this->assertDatabaseMissing('action_logs', ['action_type' => 'add seats', 'item_id' => $license->id, 'item_type' => License::class]); + //test log entries? Sure. + + } + + } diff --git a/tests/Feature/Licenses/Ui/UpdateLicenseTest.php b/tests/Feature/Licenses/Ui/UpdateLicenseTest.php index 7b1b5b29e3..46c4f70785 100644 --- a/tests/Feature/Licenses/Ui/UpdateLicenseTest.php +++ b/tests/Feature/Licenses/Ui/UpdateLicenseTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Licenses\Ui; +use App\Models\Category; use App\Models\License; use App\Models\User; use Tests\TestCase; @@ -14,4 +15,90 @@ class UpdateLicenseTest extends TestCase ->get(route('licenses.edit', License::factory()->create()->id)) ->assertOk(); } + + public function testCanUpdateLicenseSeats() + { + $admin = User::factory()->superuser()->create(); + $license_category = Category::factory()->forLicenses()->create()->id; + $response = $this->actingAs($admin) + ->from(route('licenses.create')) + ->post(route('licenses.store'), [ + 'name' => 'Test Update License', + 'seats' => '9999', + 'category_id' => $license_category, + ]); + $response->assertStatus(302); + $license = License::where('name', 'Test Update License')->sole(); + $this->assertNotNull($license); + + $this->actingAs($admin) + ->put(route('licenses.update', $license->id), [ + 'name' => 'Test Update License', + 'seats' => '19999', + 'category_id' => $license_category, + ]) + ->assertStatus(302); + + $license->refresh(); + $this->assertEquals($license->licenseseats()->count(), $license->seats); + $this->assertEquals($license->licenseseats()->count(), 19999); + } + + public function testCannotUpdateLicenseSeatsTooMuch() + { + $admin = User::factory()->superuser()->create(); + $license_category = Category::factory()->forLicenses()->create()->id; + $response = $this->actingAs($admin) + ->from(route('licenses.create')) + ->post(route('licenses.store'), [ + 'name' => 'Test Update License', + 'seats' => '9999', + 'category_id' => $license_category, + ]); + $response->assertStatus(302); + $license = License::where('name', 'Test Update License')->sole(); + $this->assertNotNull($license); + + $this->actingAs($admin) + ->put(route('licenses.update', $license->id), [ + 'name' => 'Test Update License', + 'seats' => '29999', + 'category_id' => $license_category, + ]) + ->assertStatus(302); + + $license->refresh(); + $this->assertEquals($license->licenseseats()->count(), $license->seats); + $this->assertEquals($license->licenseseats()->count(), 9999); + } + + public function testCanRemoveLicenseSeats() + { + $admin = User::factory()->superuser()->create(); + $license_category = Category::factory()->forLicenses()->create()->id; + $response = $this->actingAs($admin) + ->from(route('licenses.create')) + ->post(route('licenses.store'), [ + 'name' => 'Test Remove License Seats', + 'seats' => '9999', + 'category_id' => $license_category, + ]); + $response->assertStatus(302); + $license = License::where('name', 'Test Remove License Seats')->sole(); + $this->assertNotNull($license); + + $this->actingAs($admin) + ->put(route('licenses.update', $license->id), [ + 'name' => 'Test Remove License Seats', + 'seats' => '5000', + 'category_id' => $license_category, + ]) + ->assertStatus(302); + + $license->refresh(); + $this->assertEquals($license->licenseseats()->count(), $license->seats); + $this->assertEquals($license->licenseseats()->count(), 5000); + } + + } From b20925b5503b66b3e5a7ce682755557ccdfb487f Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 25 Jun 2025 11:03:06 +0100 Subject: [PATCH 46/46] Fixed #17282 - removed erroneous update gate for user-license endpoint Signed-off-by: snipe --- app/Http/Controllers/Api/UsersController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 81c75c8e2b..15e6f7655e 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -677,7 +677,6 @@ class UsersController extends Controller $this->authorize('view', License::class); if ($user = User::where('id', $id)->withTrashed()->first()) { - $this->authorize('update', $user); $licenses = $user->licenses()->get(); return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count()); }