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:
26
app/Actions/Acceptances/CreateCheckoutAcceptanceAction.php
Normal file
26
app/Actions/Acceptances/CreateCheckoutAcceptanceAction.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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([
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user