diff --git a/app/Console/Commands/MigrateLicenseSeatQuantitiesInActionLogs.php b/app/Console/Commands/MigrateLicenseSeatQuantitiesInActionLogs.php new file mode 100644 index 0000000000..b649f7ee96 --- /dev/null +++ b/app/Console/Commands/MigrateLicenseSeatQuantitiesInActionLogs.php @@ -0,0 +1,74 @@ +whereIn('action_type', [ + ActionType::AddSeats->value, + ActionType::DeleteSeats->value, + ]) + ->where('quantity', '=', 1) + ->orderBy('id'); + + $count = $query->count(); + + if ($count === 0) { + $this->info('Nothing to update'); + + return 0; + } + + $this->info("{$count} logs to update"); + + if ($this->option('no-interaction') || $this->confirm('Update quantities in the action log?')) { + $query->chunk(50, function ($logs) { + $logs->each(function ($log) { + $quantityFromNote = Str::between($log->note, "ed ", " seats"); + + if (!is_numeric($quantityFromNote)) { + $this->error('Could not parse quantity from ID: {id}', ['id' => $log->id]); + } + + if ($log->quantity !== (int) $quantityFromNote) { + $this->info(vsprintf('Updating id: %s to quantity %s', [ + 'id' => $log->id, + 'new_quantity' => $quantityFromNote, + ])); + + DB::table('action_logs')->where('id', $log->id)->update(['quantity' => (int) $quantityFromNote]); + } + }); + }); + } + + return 0; + } +} diff --git a/app/Events/CheckoutableCheckedOut.php b/app/Events/CheckoutableCheckedOut.php index 3f215bd3bc..d82a9a7ad4 100644 --- a/app/Events/CheckoutableCheckedOut.php +++ b/app/Events/CheckoutableCheckedOut.php @@ -15,18 +15,20 @@ class CheckoutableCheckedOut public $checkedOutBy; public $note; public $originalValues; + public int $quantity; /** * Create a new event instance. * * @return void */ - public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = []) + public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [], $quantity = 1) { $this->checkoutable = $checkoutable; $this->checkedOutTo = $checkedOutTo; $this->checkedOutBy = $checkedOutBy; $this->note = $note; $this->originalValues = $originalValues; + $this->quantity = $quantity; } } diff --git a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php index 30bf8943b3..f014a6995c 100644 --- a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php +++ b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php @@ -67,7 +67,7 @@ class AccessoryCheckoutController extends Controller */ public function store(AccessoryCheckoutRequest $request, Accessory $accessory) : RedirectResponse { - + $this->authorize('checkout', $accessory); $target = $this->determineCheckoutTarget(); @@ -89,7 +89,14 @@ class AccessoryCheckoutController extends Controller $accessory_checkout->save(); } - event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note'))); + event(new CheckoutableCheckedOut( + $accessory, + $target, + auth()->user(), + $request->input('note'), + [], + $accessory->checkout_qty, + )); $request->request->add(['checkout_to_type' => request('checkout_to_type')]); $request->request->add(['assigned_to' => $target->id]); diff --git a/app/Http/Controllers/Api/AccessoriesController.php b/app/Http/Controllers/Api/AccessoriesController.php index 6879fce2b9..980b7a692f 100644 --- a/app/Http/Controllers/Api/AccessoriesController.php +++ b/app/Http/Controllers/Api/AccessoriesController.php @@ -325,7 +325,14 @@ class AccessoriesController extends Controller } // Set this value to be able to pass the qty through to the event - event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note'))); + event(new CheckoutableCheckedOut( + $accessory, + $target, + auth()->user(), + $request->input('note'), + [], + $accessory->checkout_qty, + )); return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success'))); diff --git a/app/Http/Controllers/Api/ComponentsController.php b/app/Http/Controllers/Api/ComponentsController.php index 4fe2ade308..bdb01cdf01 100644 --- a/app/Http/Controllers/Api/ComponentsController.php +++ b/app/Http/Controllers/Api/ComponentsController.php @@ -321,7 +321,7 @@ class ComponentsController extends Controller 'note' => $request->input('note'), ]); - $component->logCheckout($request->input('note'), $asset); + $component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1)); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success'))); } diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php index 498b0b959d..38372c77be 100644 --- a/app/Http/Controllers/Api/ConsumablesController.php +++ b/app/Http/Controllers/Api/ConsumablesController.php @@ -326,8 +326,14 @@ class ConsumablesController extends Controller ); } - - event(new CheckoutableCheckedOut($consumable, $user, auth()->user(), $request->input('note'))); + event(new CheckoutableCheckedOut( + $consumable, + $user, + auth()->user(), + $request->input('note'), + [], + $consumable->checkout_qty, + )); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success'))); diff --git a/app/Http/Controllers/Components/ComponentCheckoutController.php b/app/Http/Controllers/Components/ComponentCheckoutController.php index b585ae3022..841bcf0b4a 100644 --- a/app/Http/Controllers/Components/ComponentCheckoutController.php +++ b/app/Http/Controllers/Components/ComponentCheckoutController.php @@ -115,7 +115,14 @@ class ComponentCheckoutController extends Controller 'note' => $request->input('note'), ]); - event(new CheckoutableCheckedOut($component, $asset, auth()->user(), $request->input('note'))); + event(new CheckoutableCheckedOut( + $component, + $asset, + auth()->user(), + $request->input('note'), + [], + $component->checkout_qty, + )); $request->request->add(['checkout_to_type' => 'asset']); $request->request->add(['assigned_asset' => $asset->id]); diff --git a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php index 4281d42f29..9074faade6 100644 --- a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php +++ b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php @@ -102,7 +102,15 @@ class ConsumableCheckoutController extends Controller } $consumable->checkout_qty = $quantity; - event(new CheckoutableCheckedOut($consumable, $user, auth()->user(), $request->input('note'))); + + event(new CheckoutableCheckedOut( + $consumable, + $user, + auth()->user(), + $request->input('note'), + [], + $consumable->checkout_qty, + )); $request->request->add(['checkout_to_type' => 'user']); $request->request->add(['assigned_user' => $user->id]); diff --git a/app/Http/Transformers/ActionlogsTransformer.php b/app/Http/Transformers/ActionlogsTransformer.php index 027997197b..6e4e4d93cd 100644 --- a/app/Http/Transformers/ActionlogsTransformer.php +++ b/app/Http/Transformers/ActionlogsTransformer.php @@ -1,6 +1,7 @@ db_column == $fieldname) { @@ -185,7 +186,7 @@ class ActionlogsTransformer 'name' => e($actionlog->target->display_name) ?? null, 'type' => e($actionlog->targetType()), ] : null, - + 'quantity' => $this->getQuantity($actionlog), 'note' => ($actionlog->note) ? Helper::parseEscapedMarkedownInline($actionlog->note): null, 'signature_file' => ($actionlog->accept_signature) ? route('log.signature.view', ['filename' => $actionlog->accept_signature ]) : null, 'log_meta' => ((isset($clean_meta)) && (is_array($clean_meta))) ? $clean_meta: null, @@ -336,6 +337,26 @@ class ActionlogsTransformer } + private function getQuantity(Actionlog $actionlog): ?int + { + if (!$actionlog->quantity) { + return null; + } + + // only a few action types will have a quantity we are interested in. + if (!in_array($actionlog->action_type, [ + ActionType::Checkout->value, + ActionType::Accepted->value, + ActionType::Declined->value, + ActionType::CheckinFrom->value, + ActionType::AddSeats->value, + ActionType::DeleteSeats->value, + ])) { + return null; + } + + return (int) $actionlog->quantity; + } } diff --git a/app/Listeners/LogListener.php b/app/Listeners/LogListener.php index d7973e2103..b41edd50af 100644 --- a/app/Listeners/LogListener.php +++ b/app/Listeners/LogListener.php @@ -47,7 +47,13 @@ class LogListener */ public function onCheckoutableCheckedOut(CheckoutableCheckedOut $event) { - $event->checkoutable->logCheckout($event->note, $event->checkedOutTo, $event->checkoutable->last_checkout, $event->originalValues); + $event->checkoutable->logCheckout( + $event->note, + $event->checkedOutTo, + $event->checkoutable->last_checkout, + $event->originalValues, + $event->quantity + ); } /** @@ -66,6 +72,7 @@ class LogListener $logaction->note = $event->acceptance->note; $logaction->action_type = 'accepted'; $logaction->action_date = $event->acceptance->accepted_at; + $logaction->quantity = $event->acceptance->qty ?? 1; // TODO: log the actual license seat that was checked out if ($event->acceptance->checkoutable instanceof LicenseSeat) { @@ -84,6 +91,7 @@ class LogListener $logaction->note = $event->acceptance->note; $logaction->action_type = 'declined'; $logaction->action_date = $event->acceptance->declined_at; + $logaction->quantity = $event->acceptance->qty ?? 1; // TODO: log the actual license seat that was checked out if ($event->acceptance->checkoutable instanceof LicenseSeat) { diff --git a/app/Models/License.php b/app/Models/License.php index 444349a08e..613c4864d9 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -223,6 +223,7 @@ class License extends Depreciable $logAction->created_by = auth()->id() ?: 1; // We don't have an id while running the importer from CLI. $logAction->note = "deleted {$change} seats"; $logAction->target_id = null; + $logAction->quantity = $change; $logAction->logaction('delete seats'); return true; @@ -259,6 +260,7 @@ class License extends Depreciable $logAction->created_by = auth()->id() ?: 1; // Importer. $logAction->note = "added {$change} seats"; $logAction->target_id = null; + $logAction->quantity = $change; $logAction->logaction('add seats'); } diff --git a/app/Models/Traits/Loggable.php b/app/Models/Traits/Loggable.php index 204f7b24f0..4ea08c1525 100644 --- a/app/Models/Traits/Loggable.php +++ b/app/Models/Traits/Loggable.php @@ -40,7 +40,7 @@ trait Loggable * @since [v3.4] * @return \App\Models\Actionlog */ - public function logCheckout($note, $target, $action_date = null, $originalValues = []) + public function logCheckout($note, $target, $action_date = null, $originalValues = [], $quantity = 1) { $log = new Actionlog; @@ -94,7 +94,7 @@ trait Loggable $log->note = $note; $log->action_date = $action_date; - + $log->quantity = $quantity; $changed = []; $array_to_flip = array_keys($fields_array); diff --git a/app/Presenters/HistoryPresenter.php b/app/Presenters/HistoryPresenter.php index 9b9968ebd2..2a2a7bc28a 100644 --- a/app/Presenters/HistoryPresenter.php +++ b/app/Presenters/HistoryPresenter.php @@ -117,6 +117,13 @@ class HistoryPresenter extends Presenter 'visible' => true, 'formatter' => 'fileDownloadButtonsFormatter', ], + [ + 'field' => 'quantity', + 'searchable' => false, + 'sortable' => true, + 'visible' => true, + 'title' => trans('general.quantity'), + ], [ 'field' => 'note', 'searchable' => true, @@ -169,4 +176,4 @@ class HistoryPresenter extends Presenter return json_encode($merged); } -} \ No newline at end of file +} diff --git a/database/migrations/2025_12_10_211855_add_quantity_to_action_logs_table.php b/database/migrations/2025_12_10_211855_add_quantity_to_action_logs_table.php new file mode 100644 index 0000000000..59109b0e2c --- /dev/null +++ b/database/migrations/2025_12_10_211855_add_quantity_to_action_logs_table.php @@ -0,0 +1,28 @@ +integer('quantity')->default(1)->after('expected_checkin'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('action_logs', function (Blueprint $table) { + $table->dropColumn('quantity'); + }); + } +}; diff --git a/tests/Feature/CheckoutAcceptances/Ui/AccessoryAcceptanceTest.php b/tests/Feature/CheckoutAcceptances/Ui/AccessoryAcceptanceTest.php index 7b3d8c49aa..ac23dc7259 100644 --- a/tests/Feature/CheckoutAcceptances/Ui/AccessoryAcceptanceTest.php +++ b/tests/Feature/CheckoutAcceptances/Ui/AccessoryAcceptanceTest.php @@ -14,6 +14,72 @@ use Tests\TestCase; class AccessoryAcceptanceTest extends TestCase { + public function test_can_accept_accessory_checkout() + { + $assignee = User::factory()->create(); + $accessory = Accessory::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->for($assignee, 'assignedTo') + ->for($accessory, 'checkoutable') + ->create(['qty' => 2]); + + $this->actingAs($assignee) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'accepted', + 'note' => 'A note here', + ]) + ->assertRedirect(); + + $this->assertNotNull($checkoutAcceptance->refresh()->accepted_at); + $this->assertEquals('A note here', $checkoutAcceptance->note); + + $assignee->accessories->contains($accessory); + + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'accepted', + 'target_id' => $assignee->id, + 'target_type' => User::class, + 'item_id' => $accessory->id, + 'item_type' => Accessory::class, + 'quantity' => 2, + ]); + } + + public function test_can_decline_accessory_checkout() + { + $assignee = User::factory()->create(); + $accessory = Accessory::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->for($assignee, 'assignedTo') + ->for($accessory, 'checkoutable') + ->create(['qty' => 2]); + + $this->actingAs($assignee) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'declined', + 'note' => 'A note here', + ]) + ->assertRedirect(); + + $this->assertNotNull($checkoutAcceptance->refresh()->declined_at); + $this->assertEquals('A note here', $checkoutAcceptance->note); + + $assignee->accessories->doesntContain($accessory); + + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'declined', + 'target_id' => $assignee->id, + 'target_type' => User::class, + 'item_id' => $accessory->id, + 'item_type' => Accessory::class, + 'quantity' => 2, + ]); + } + /** * This can be absorbed into a bigger test */ diff --git a/tests/Feature/CheckoutAcceptances/Ui/ConsumableAcceptanceTest.php b/tests/Feature/CheckoutAcceptances/Ui/ConsumableAcceptanceTest.php new file mode 100644 index 0000000000..9c7c0724b5 --- /dev/null +++ b/tests/Feature/CheckoutAcceptances/Ui/ConsumableAcceptanceTest.php @@ -0,0 +1,77 @@ +create(); + $consumable = Consumable::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->for($assignee, 'assignedTo') + ->for($consumable, 'checkoutable') + ->create(['qty' => 2]); + + $this->actingAs($assignee) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'accepted', + 'note' => 'A note here', + ]) + ->assertRedirect(); + + $this->assertNotNull($checkoutAcceptance->refresh()->accepted_at); + $this->assertEquals('A note here', $checkoutAcceptance->note); + + $assignee->consumables->contains($consumable); + + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'accepted', + 'target_id' => $assignee->id, + 'target_type' => User::class, + 'item_id' => $consumable->id, + 'item_type' => Consumable::class, + 'quantity' => 2, + ]); + } + + public function test_can_decline_consumable_checkout() + { + $assignee = User::factory()->create(); + $consumable = Consumable::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->for($assignee, 'assignedTo') + ->for($consumable, 'checkoutable') + ->create(['qty' => 2]); + + $this->actingAs($assignee) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'declined', + 'note' => 'A note here', + ]) + ->assertRedirect(); + + $this->assertNotNull($checkoutAcceptance->refresh()->declined_at); + $this->assertEquals('A note here', $checkoutAcceptance->note); + + $assignee->consumables->doesntContain($consumable); + + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'declined', + 'target_id' => $assignee->id, + 'target_type' => User::class, + 'item_id' => $consumable->id, + 'item_type' => Consumable::class, + 'quantity' => 2, + ]); + } +} diff --git a/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php b/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php index 2af6cf49a2..cdada791fe 100644 --- a/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php +++ b/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php @@ -115,20 +115,17 @@ class AccessoryCheckoutTest extends TestCase implements TestsPermissionsRequirem $this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0); - $this->assertEquals( - 1, - Actionlog::where([ - 'action_type' => 'checkout', - 'target_id' => $user->id, - 'target_type' => User::class, - 'item_id' => $accessory->id, - 'item_type' => Accessory::class, - 'created_by' => $admin->id, - ])->count(), - 'Log entry either does not exist or there are more than expected' - ); - $this->assertHasTheseActionLogs($accessory, ['create', 'checkout']); + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'checkout', + 'target_id' => $user->id, + 'target_type' => User::class, + 'item_id' => $accessory->id, + 'item_type' => Accessory::class, + 'quantity' => 2, + 'created_by' => $admin->id, + ]); + $this->assertHasTheseActionLogs($accessory, ['create', 'checkout']); } public function testAccessoryCannotBeCheckedOutToInvalidUser() diff --git a/tests/Feature/Checkouts/Api/ComponentCheckoutTest.php b/tests/Feature/Checkouts/Api/ComponentCheckoutTest.php index 50f9df8641..732e43b91e 100644 --- a/tests/Feature/Checkouts/Api/ComponentCheckoutTest.php +++ b/tests/Feature/Checkouts/Api/ComponentCheckoutTest.php @@ -109,7 +109,7 @@ class ComponentCheckoutTest extends TestCase implements TestsFullMultipleCompani $this->actingAsForApi($user) ->postJson(route('api.components.checkout', $component->id), [ 'assigned_to' => $asset->id, - 'assigned_qty' => 1, + 'assigned_qty' => 2, ]); $this->assertDatabaseHas('action_logs', [ @@ -120,6 +120,7 @@ class ComponentCheckoutTest extends TestCase implements TestsFullMultipleCompani 'location_id' => $location->id, 'item_type' => Component::class, 'item_id' => $component->id, + 'quantity' => 2, ]); } diff --git a/tests/Feature/Checkouts/Api/ConsumableCheckoutTest.php b/tests/Feature/Checkouts/Api/ConsumableCheckoutTest.php index 44aa5e11ea..ba88016d02 100644 --- a/tests/Feature/Checkouts/Api/ConsumableCheckoutTest.php +++ b/tests/Feature/Checkouts/Api/ConsumableCheckoutTest.php @@ -52,6 +52,27 @@ class ConsumableCheckoutTest extends TestCase $this->assertHasTheseActionLogs($consumable, ['create', 'checkout']); } + public function testConsumableCanBeCheckedOutWithQuantity() + { + $consumable = Consumable::factory()->create(); + $user = User::factory()->create(); + + $this->actingAsForApi(User::factory()->checkoutConsumables()->create()) + ->postJson(route('api.consumables.checkout', $consumable), [ + 'assigned_to' => $user->id, + 'checkout_qty' => 2, + ]); + + $this->assertDatabaseHas('action_logs', [ + 'item_type' => Consumable::class, + 'item_id' => $consumable->id, + 'target_type' => User::class, + 'target_id' => $user->id, + 'action_type' => 'checkout', + 'quantity' => 2, + ]); + } + public function testUserSentNotificationUponCheckout() { Mail::fake(); diff --git a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php index f9e84015e2..b05200cfcc 100644 --- a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php @@ -81,6 +81,7 @@ class AccessoryCheckoutTest extends TestCase 'target_type' => User::class, 'item_id' => $accessory->id, 'item_type' => Accessory::class, + 'quantity' => 1, 'note' => 'oh hi there', ]); $this->assertHasTheseActionLogs($accessory, ['create', 'checkout']); @@ -108,6 +109,7 @@ class AccessoryCheckoutTest extends TestCase 'target_type' => User::class, 'item_id' => $accessory->id, 'item_type' => Accessory::class, + 'quantity' => 3, 'note' => 'oh hi there', ]); $this->assertHasTheseActionLogs($accessory, ['create', 'checkout']); @@ -135,6 +137,7 @@ class AccessoryCheckoutTest extends TestCase 'target_type' => Location::class, 'item_id' => $accessory->id, 'item_type' => Accessory::class, + 'quantity' => 3, 'note' => 'oh hi there', ]); $this->assertHasTheseActionLogs($accessory, ['create', 'checkout']); @@ -162,6 +165,7 @@ class AccessoryCheckoutTest extends TestCase 'target_type' => Asset::class, 'item_id' => $accessory->id, 'item_type' => Accessory::class, + 'quantity' => 3, 'note' => 'oh hi there', ]); $this->assertHasTheseActionLogs($accessory, ['create', 'checkout']); diff --git a/tests/Feature/Checkouts/Ui/ComponentsCheckoutTest.php b/tests/Feature/Checkouts/Ui/ComponentsCheckoutTest.php index 96c7b89322..d17204447c 100644 --- a/tests/Feature/Checkouts/Ui/ComponentsCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/ComponentsCheckoutTest.php @@ -97,4 +97,30 @@ class ComponentsCheckoutTest extends TestCase ->assertRedirect(route('hardware.show', $asset)); $this->assertHasTheseActionLogs($component, ['create', 'checkout']); } + + public function test_quantity_stored_in_action_log() + { + $component = Component::factory()->create(); + $asset = Asset::factory()->create(); + + $admin = User::factory()->admin()->create(); + + $this->actingAs($admin) + ->from(route('components.index')) + ->post(route('components.checkout.store', $component), [ + 'asset_id' => $asset->id, + 'redirect_option' => 'index', + 'assigned_qty' => 2, + ]); + + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'checkout', + 'target_id' => $asset->id, + 'target_type' => Asset::class, + 'item_id' => $component->id, + 'item_type' => Component::class, + 'quantity' => 2, + 'created_by' => $admin->id, + ]); + } } diff --git a/tests/Feature/Checkouts/Ui/ConsumableCheckoutTest.php b/tests/Feature/Checkouts/Ui/ConsumableCheckoutTest.php index fa92cb7417..9ecea11501 100644 --- a/tests/Feature/Checkouts/Ui/ConsumableCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/ConsumableCheckoutTest.php @@ -150,4 +150,29 @@ class ConsumableCheckoutTest extends TestCase ->assertRedirect(route('users.show', $user)); } + public function test_quantity_stored_in_action_log() + { + $consumable = Consumable::factory()->create(['qty' => 3]); + $user = User::factory()->create(); + + $admin = User::factory()->admin()->create(); + + $this->actingAs($admin) + ->from(route('components.index')) + ->post(route('consumables.checkout.store', $consumable), [ + 'assigned_to' => $user->id, + 'redirect_option' => 'target', + 'checkout_qty' => 2, + ]); + + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'checkout', + 'target_id' => $user->id, + 'target_type' => User::class, + 'item_id' => $consumable->id, + 'item_type' => Consumable::class, + 'quantity' => 2, + 'created_by' => $admin->id, + ]); + } } diff --git a/tests/Unit/Models/LicenseTest.php b/tests/Unit/Models/LicenseTest.php new file mode 100644 index 0000000000..bd1960bc11 --- /dev/null +++ b/tests/Unit/Models/LicenseTest.php @@ -0,0 +1,53 @@ +create(); + $this->actingAs($user); + + $license = License::factory()->create(['seats' => 2]); + + $license->update(['seats' => 6]); + + $this->assertDatabaseHas('action_logs', [ + 'created_by' => $user->id, + 'action_type' => ActionType::AddSeats, + 'item_type' => License::class, + 'item_id' => $license->id, + 'deleted_at' => null, + // relevant for this test: + 'quantity' => 4, + 'note' => 'added 4 seats', + ]); + } + + public function test_removing_seats_is_logged_when_updating() + { + $user = User::factory()->create(); + $this->actingAs($user); + + $license = License::factory()->create(['seats' => 6]); + + $license->update(['seats' => 3]); + + $this->assertDatabaseHas('action_logs', [ + 'created_by' => $user->id, + 'action_type' => ActionType::DeleteSeats, + 'item_type' => License::class, + 'item_id' => $license->id, + 'deleted_at' => null, + // relevant for this test: + 'quantity' => 3, + 'note' => 'deleted 3 seats', + ]); + } +}