3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00

Merge pull request #19234 from marcusmoore/checkout-acceptance-action

Acceptances: Extract CreateCheckoutAcceptanceAction
This commit is contained in:
snipe
2026-07-01 16:51:41 +01:00
committed by GitHub
14 changed files with 245 additions and 35 deletions

View File

@ -0,0 +1,26 @@
<?php
namespace App\Actions\Acceptances;
use App\Models\CheckoutAcceptance;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
class CreateCheckoutAcceptanceAction
{
public static function run(
Model $checkoutable,
User $assignedTo,
?int $qty = null,
?int $alertOnResponseId = null,
): CheckoutAcceptance {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($checkoutable);
$acceptance->assignedTo()->associate($assignedTo);
$acceptance->qty = $qty;
$acceptance->alert_on_response_id = $alertOnResponseId;
$acceptance->save();
return $acceptance;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Accessories;
use App\Actions\Acceptances\CreateCheckoutAcceptanceAction;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
@ -134,11 +135,7 @@ class AccessoryCheckoutController extends Controller
// If requireAcceptance() is false the listener won't have created one; create it now.
if (! $acceptance) {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($accessory);
$acceptance->assignedTo()->associate($targetUser);
$acceptance->qty = $accessory->checkout_qty;
$acceptance->save();
$acceptance = CreateCheckoutAcceptanceAction::run($accessory, $targetUser, $accessory->checkout_qty);
}
session([

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Assets;
use App\Actions\Acceptances\CreateCheckoutAcceptanceAction;
use App\Exceptions\CheckoutNotAllowed;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
@ -153,10 +154,7 @@ class AssetCheckoutController extends Controller
// If requireAcceptance() is false the listener won't have created one; create it now.
if (! $acceptance) {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($asset);
$acceptance->assignedTo()->associate($target);
$acceptance->save();
$acceptance = CreateCheckoutAcceptanceAction::run($asset, $target);
}
session([

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Consumables;
use App\Actions\Acceptances\CreateCheckoutAcceptanceAction;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
@ -149,11 +150,7 @@ class ConsumableCheckoutController extends Controller
// If requireAcceptance() is false the listener won't have created one; create it now.
if (! $acceptance) {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($consumable);
$acceptance->assignedTo()->associate($user);
$acceptance->qty = $quantity;
$acceptance->save();
$acceptance = CreateCheckoutAcceptanceAction::run($consumable, $user, $quantity);
}
session([

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Licenses;
use App\Actions\Acceptances\CreateCheckoutAcceptanceAction;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
@ -165,10 +166,7 @@ class LicenseCheckoutController extends Controller
// If requireAcceptance() is false the listener won't have created one; create it now.
if (! $acceptance) {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($licenseSeat);
$acceptance->assignedTo()->associate($checkoutTarget);
$acceptance->save();
$acceptance = CreateCheckoutAcceptanceAction::run($licenseSeat, $checkoutTarget);
}
session([

View File

@ -2,6 +2,7 @@
namespace App\Listeners;
use App\Actions\Acceptances\CreateCheckoutAcceptanceAction;
use App\Events\CheckoutableCheckedOut;
use App\Mail\CheckinAccessoryMail;
use App\Mail\CheckinAssetMail;
@ -286,25 +287,15 @@ class CheckoutableListener
return null;
}
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->checkoutable);
$acceptance->assignedTo()->associate($event->checkedOutTo);
$acceptance->qty = 1;
if (isset($event->checkoutable->checkout_qty)) {
$acceptance->qty = $event->checkoutable->checkout_qty;
}
$category = $this->getCategoryFromCheckoutable($event->checkoutable);
$alertOnResponseId = $category?->alert_on_response ? auth()->id() : null;
if ($category?->alert_on_response) {
$acceptance->alert_on_response_id = auth()->id();
}
$acceptance->save();
return $acceptance;
return CreateCheckoutAcceptanceAction::run(
$event->checkoutable,
$event->checkedOutTo,
$event->checkoutable->checkout_qty ?? 1,
$alertOnResponseId,
);
}
/**

View File

@ -144,6 +144,13 @@ class AccessoryFactory extends Factory
});
}
public function notRequiringAcceptance()
{
return $this->afterCreating(function ($accessory) {
$accessory->category->update(['require_acceptance' => 0]);
});
}
public function checkedOutToUser(?User $user = null)
{
return $this->afterCreating(function (Accessory $accessory) use ($user) {

View File

@ -118,6 +118,13 @@ class ConsumableFactory extends Factory
});
}
public function notRequiringAcceptance()
{
return $this->afterCreating(function (Consumable $consumable) {
$consumable->category->update(['require_acceptance' => 0]);
});
}
public function checkedOutToUser(?User $user = null)
{
return $this->afterCreating(function (Consumable $consumable) use ($user) {

View File

@ -4,6 +4,7 @@ namespace Tests\Feature\Checkouts\General;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Consumable;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\Statuslabel;
@ -39,6 +40,7 @@ class SettingAlertOnResponseTest extends TestCase
'checkoutable_id' => $accessory->id,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => $this->actor->id,
'qty' => 1,
]);
}
@ -75,6 +77,7 @@ class SettingAlertOnResponseTest extends TestCase
'checkoutable_id' => $asset->id,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => $this->actor->id,
'qty' => 1,
]);
}
@ -111,6 +114,7 @@ class SettingAlertOnResponseTest extends TestCase
'checkoutable_type' => LicenseSeat::class,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => $this->actor->id,
'qty' => 1,
]);
}
@ -132,6 +136,42 @@ class SettingAlertOnResponseTest extends TestCase
]);
}
public function test_sets_alert_on_response_if_enabled_by_category_for_consumable()
{
$consumable = Consumable::factory()->create(['qty' => 5]);
$consumable->category->update([
'require_acceptance' => true,
'alert_on_response' => true,
]);
$this->postConsumableCheckout($consumable);
$this->assertDatabaseHas('checkout_acceptances', [
'checkoutable_type' => Consumable::class,
'checkoutable_id' => $consumable->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_consumable()
{
$consumable = Consumable::factory()->create(['qty' => 5]);
$consumable->category->update([
'require_acceptance' => true,
'alert_on_response' => false,
]);
$this->postConsumableCheckout($consumable);
$this->assertDatabaseHas('checkout_acceptances', [
'checkoutable_type' => Consumable::class,
'checkoutable_id' => $consumable->id,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => null,
]);
}
private function postAssetCheckout(Asset $asset): void
{
$this->actingAs($this->actor)
@ -161,4 +201,14 @@ class SettingAlertOnResponseTest extends TestCase
'assigned_to' => $this->assignedUser->id,
]);
}
private function postConsumableCheckout(Consumable $consumable): void
{
$this->actingAs($this->actor)
->post(route('consumables.checkout.store', $consumable), [
'checkout_to_type' => 'user',
'assigned_to' => $this->assignedUser->id,
'checkout_qty' => 1,
]);
}
}

View File

@ -294,6 +294,38 @@ class AccessoryCheckoutTest extends TestCase
$this->assertNotNull($acceptance);
$this->assertEquals(2, $acceptance->qty);
$this->assertDatabaseCount('checkout_acceptances', 1);
$response->assertStatus(302)
->assertRedirect(route('account.accept.item', $acceptance));
}
public function test_accessory_sign_in_place_creates_acceptance_when_acceptance_not_required()
{
$targetUser = User::factory()->create();
$accessory = Accessory::factory()->notRequiringAcceptance()->create(['qty' => 5]);
$response = $this->actingAs(User::factory()->admin()->create())
->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [
'assigned_user' => $targetUser->id,
'checkout_to_type' => 'user',
'redirect_option' => 'index',
'checkout_qty' => 2,
'sign_in_place' => 1,
]);
$acceptance = CheckoutAcceptance::query()
->where('checkoutable_type', Accessory::class)
->where('checkoutable_id', $accessory->id)
->where('assigned_to_id', $targetUser->id)
->pending()
->latest()
->first();
$this->assertNotNull($acceptance);
$this->assertEquals(2, $acceptance->qty);
$this->assertDatabaseCount('checkout_acceptances', 1);
$response->assertStatus(302)
->assertRedirect(route('account.accept.item', $acceptance));

View File

@ -491,6 +491,8 @@ class AssetCheckoutTest extends TestCase
->first();
$this->assertNotNull($acceptance);
$this->assertNull($acceptance->qty);
$this->assertDatabaseCount('checkout_acceptances', 1);
$response->assertStatus(302)
->assertRedirect(route('account.accept.item', $acceptance));

View File

@ -198,6 +198,37 @@ class ConsumableCheckoutTest extends TestCase
$this->assertNotNull($acceptance);
$this->assertEquals(2, $acceptance->qty);
$this->assertDatabaseCount('checkout_acceptances', 1);
$response->assertStatus(302)
->assertRedirect(route('account.accept.item', $acceptance));
}
public function test_consumable_sign_in_place_creates_acceptance_when_acceptance_not_required()
{
$targetUser = User::factory()->create();
$consumable = Consumable::factory()->notRequiringAcceptance()->create(['qty' => 5]);
$response = $this->actingAs(User::factory()->admin()->create())
->from(route('consumables.checkout.show', $consumable))
->post(route('consumables.checkout.store', $consumable), [
'assigned_to' => $targetUser->id,
'redirect_option' => 'index',
'checkout_qty' => 2,
'sign_in_place' => 1,
]);
$acceptance = CheckoutAcceptance::query()
->where('checkoutable_type', Consumable::class)
->where('checkoutable_id', $consumable->id)
->where('assigned_to_id', $targetUser->id)
->pending()
->latest()
->first();
$this->assertNotNull($acceptance);
$this->assertEquals(2, $acceptance->qty);
$this->assertDatabaseCount('checkout_acceptances', 1);
$response->assertStatus(302)
->assertRedirect(route('account.accept.item', $acceptance));

View File

@ -147,6 +147,36 @@ class LicenseCheckoutTest extends TestCase
->first();
$this->assertNotNull($acceptance);
$this->assertDatabaseCount('checkout_acceptances', 1);
$response->assertStatus(302)
->assertRedirect(route('account.accept.item', $acceptance));
}
public function test_license_sign_in_place_creates_acceptance_when_acceptance_not_required()
{
$targetUser = User::factory()->create();
$seat = LicenseSeat::factory()->create();
$response = $this->actingAs(User::factory()->admin()->create())
->from(route('licenses.checkout', $seat->license))
->post(route('licenses.checkout', $seat->license), [
'assigned_to' => $targetUser->id,
'redirect_option' => 'index',
'sign_in_place' => 1,
]);
$acceptance = CheckoutAcceptance::query()
->where('checkoutable_type', LicenseSeat::class)
->where('assigned_to_id', $targetUser->id)
->pending()
->latest()
->first();
$this->assertNotNull($acceptance);
// The fallback does not set qty, so it stays null (the column default).
$this->assertNull($acceptance->qty);
$this->assertDatabaseCount('checkout_acceptances', 1);
$response->assertStatus(302)
->assertRedirect(route('account.accept.item', $acceptance));

View File

@ -0,0 +1,44 @@
<?php
namespace Tests\Unit\Actions\Acceptances;
use App\Actions\Acceptances\CreateCheckoutAcceptanceAction;
use App\Models\Asset;
use App\Models\User;
use Tests\TestCase;
class CreateCheckoutAcceptanceActionTest extends TestCase
{
public function test_it_creates_a_pending_acceptance_associated_with_the_item_and_user(): void
{
$asset = Asset::factory()->create();
$assignedUser = User::factory()->create();
$acceptance = CreateCheckoutAcceptanceAction::run($asset, $assignedUser);
$this->assertTrue($acceptance->exists);
$this->assertTrue($acceptance->isPending());
$this->assertTrue($acceptance->checkoutable->is($asset));
$this->assertTrue($acceptance->assignedTo->is($assignedUser));
}
public function test_it_leaves_qty_and_alert_null_when_not_provided(): void
{
$acceptance = CreateCheckoutAcceptanceAction::run(Asset::factory()->create(), User::factory()->create());
$this->assertNull($acceptance->qty);
$this->assertNull($acceptance->alert_on_response_id);
}
public function test_it_stores_the_explicit_qty_and_alert_when_provided(): void
{
$asset = Asset::factory()->create();
$assignedUser = User::factory()->create();
$alertUser = User::factory()->create();
$acceptance = CreateCheckoutAcceptanceAction::run($asset, $assignedUser, qty: 3, alertOnResponseId: $alertUser->id);
$this->assertEquals(3, $acceptance->qty);
$this->assertEquals($alertUser->id, $acceptance->alert_on_response_id);
}
}