1031 lines
32 KiB
PHP
1031 lines
32 KiB
PHP
<?php
|
|
|
|
namespace Blax\Shop\Tests\Feature;
|
|
|
|
use Blax\Shop\Enums\StockStatus;
|
|
use Blax\Shop\Enums\StockType;
|
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
|
use Blax\Shop\Models\Product;
|
|
use Blax\Shop\Models\ProductStock;
|
|
use Blax\Shop\Tests\TestCase;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
class ProductStockTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
/** @test */
|
|
public function it_creates_stock_record_on_increase()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
$product->increaseStock(10);
|
|
|
|
$this->assertDatabaseHas('product_stocks', [
|
|
'product_id' => $product->id,
|
|
'quantity' => 10,
|
|
'type' => 'increase',
|
|
]);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_creates_stock_record_on_decrease()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
$product->increaseStock(20);
|
|
|
|
$product->decreaseStock(5);
|
|
|
|
$this->assertDatabaseHas('product_stocks', [
|
|
'product_id' => $product->id,
|
|
'quantity' => -5,
|
|
'type' => 'decrease',
|
|
]);
|
|
}
|
|
|
|
/** @test */
|
|
public function stock_belongs_to_product()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
$product->increaseStock(10);
|
|
|
|
$stock = $product->stocks()->first();
|
|
|
|
$this->assertInstanceOf(Product::class, $stock->product);
|
|
$this->assertEquals($product->id, $stock->product->id);
|
|
}
|
|
|
|
/** @test */
|
|
public function product_has_many_stock_records()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
$product->increaseStock(10);
|
|
$product->increaseStock(5);
|
|
$product->decreaseStock(3);
|
|
|
|
$this->assertCount(3, $product->stocks);
|
|
}
|
|
|
|
/** @test */
|
|
public function available_stock_considers_all_records()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
$product->increaseStock(50);
|
|
$product->increaseStock(30);
|
|
$product->decreaseStock(20);
|
|
|
|
$this->assertEquals(60, $product->getAvailableStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function claim_reduces_available_stock()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$claim = $product->claimStock(25);
|
|
|
|
$this->assertEquals(75, $product->getAvailableStock());
|
|
$this->assertNotNull($claim);
|
|
}
|
|
|
|
/** @test */
|
|
public function releasing_claim_increases_available_stock()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$claim = $product->claimStock(25);
|
|
$this->assertEquals(75, $product->getAvailableStock());
|
|
|
|
$claim->release();
|
|
|
|
$this->assertEquals(100, $product->refresh()->getAvailableStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function permanent_claim_has_no_expiry()
|
|
{
|
|
$product = Product::factory()->withStocks(50)->create();
|
|
|
|
$claim = $product->claimStock(10);
|
|
|
|
$this->assertNull($claim->expires_at);
|
|
$this->assertTrue($claim->isPermanent());
|
|
}
|
|
|
|
/** @test */
|
|
public function temporary_claim_has_expiry()
|
|
{
|
|
$product = Product::factory()->withStocks(50)->create();
|
|
|
|
$claim = $product->claimStock(
|
|
quantity: 10,
|
|
until: now()->addHours(2)
|
|
);
|
|
|
|
$this->assertNotNull($claim->expires_at);
|
|
$this->assertTrue($claim->isTemporary());
|
|
}
|
|
|
|
/** @test */
|
|
public function claim_can_have_note()
|
|
{
|
|
$product = Product::factory()->withStocks(50)->create();
|
|
|
|
$note = 'Claimed for VIP customer';
|
|
$claim = $product->claimStock(
|
|
quantity: 10,
|
|
note: $note
|
|
);
|
|
|
|
$this->assertEquals($note, $claim->note);
|
|
}
|
|
|
|
/** @test */
|
|
public function cannot_claim_more_than_available()
|
|
{
|
|
$product = Product::factory()->withStocks(10)->create();
|
|
|
|
$this->expectException(NotEnoughStockException::class);
|
|
|
|
$product->claimStock(15);
|
|
}
|
|
|
|
/** @test */
|
|
public function pending_scope_returns_unreleased_claims()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$pending = $product->claimStock(10);
|
|
$released = $product->claimStock(5);
|
|
$released->release();
|
|
|
|
$pendingClaims = ProductStock::pending()->get();
|
|
|
|
$this->assertTrue($pendingClaims->contains($pending));
|
|
$this->assertFalse($pendingClaims->contains($released));
|
|
}
|
|
|
|
/** @test */
|
|
public function released_scope_returns_released_claims()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$pending = $product->claimStock(10);
|
|
$released = $product->claimStock(5);
|
|
$released->release();
|
|
|
|
$releasedClaims = ProductStock::released()->get();
|
|
|
|
$this->assertFalse($releasedClaims->contains($pending));
|
|
$this->assertTrue($releasedClaims->contains($released));
|
|
}
|
|
|
|
/** @test */
|
|
public function expired_claims_dont_affect_available_stock()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$product->claimStock(
|
|
quantity: 20,
|
|
until: now()->subHour()
|
|
);
|
|
|
|
// Expired claims should be counted in available stock
|
|
$available = $product->claims()->get();
|
|
|
|
$this->assertEquals(0, $available->count());
|
|
}
|
|
|
|
/** @test */
|
|
public function cannot_release_stock_twice()
|
|
{
|
|
$product = Product::factory()->withStocks(50)->create();
|
|
|
|
$claim = $product->claimStock(10);
|
|
|
|
$this->assertTrue($claim->release());
|
|
$this->assertFalse($claim->release());
|
|
}
|
|
|
|
/** @test */
|
|
public function stock_status_is_tracked()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
$product->increaseStock(10);
|
|
|
|
$stock = $product->stocks()->first();
|
|
|
|
$this->assertEquals(StockStatus::COMPLETED, $stock->status);
|
|
}
|
|
|
|
/** @test */
|
|
public function product_without_stock_management_returns_max_stock()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => false]);
|
|
|
|
$available = $product->getAvailableStock();
|
|
|
|
$this->assertEquals(PHP_INT_MAX, $available);
|
|
}
|
|
|
|
/** @test */
|
|
public function product_without_stock_management_doesnt_create_records()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => false]);
|
|
|
|
$result = $product->increaseStock(10);
|
|
|
|
$this->assertFalse($result);
|
|
$this->assertCount(0, $product->stocks);
|
|
}
|
|
|
|
/** @test */
|
|
public function claim_without_stock_management_returns_null()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => false]);
|
|
|
|
$claim = $product->claimStock(10);
|
|
|
|
$this->assertNull($claim);
|
|
}
|
|
|
|
/** @test */
|
|
public function available_stocks_attribute_accessor_works()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
$product->increaseStock(25);
|
|
$product->increaseStock(15);
|
|
|
|
$this->assertEquals(40, $product->AvailableStocks);
|
|
}
|
|
|
|
/** @test */
|
|
public function claims_method_filters_active_only()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$active = $product->claimStock(10, until: now()->addDay());
|
|
$expired = $product->claimStock(5, until: now()->subDay());
|
|
|
|
$claims = $product->claims()->get();
|
|
|
|
$this->assertCount(1, $claims);
|
|
$this->assertEquals($active->id, $claims->first()->id);
|
|
}
|
|
|
|
/** @test */
|
|
public function can_adjust_stock()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
$product->increaseStock(20);
|
|
$this->assertEquals(20, $product->getAvailableStock());
|
|
|
|
$product->adjustStock(
|
|
type: \Blax\Shop\Enums\StockType::DECREASE,
|
|
quantity: 5
|
|
);
|
|
$this->assertEquals(15, $product->getAvailableStock());
|
|
|
|
$product->adjustStock(
|
|
type: \Blax\Shop\Enums\StockType::INCREASE,
|
|
quantity: 10
|
|
);
|
|
$this->assertEquals(25, $product->getAvailableStock());
|
|
|
|
// Also with until
|
|
$product->adjustStock(
|
|
type: \Blax\Shop\Enums\StockType::DECREASE,
|
|
quantity: 5,
|
|
until: now()->addDay()
|
|
);
|
|
$this->assertEquals(20, $product->getAvailableStock());
|
|
|
|
$this->travel(23)->hours();
|
|
|
|
$this->assertEquals(20, $product->getAvailableStock());
|
|
|
|
$this->travel(2)->days();
|
|
|
|
$this->assertEquals(25, $product->getAvailableStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_claim_stock_with_claimed_from_date()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$claimedFrom = now()->addDays(5);
|
|
$until = now()->addDays(10);
|
|
|
|
$claim = $product->claimStock(
|
|
quantity: 20,
|
|
from: $claimedFrom,
|
|
until: $until
|
|
);
|
|
|
|
$this->assertNotNull($claim);
|
|
$this->assertEquals($claimedFrom->format('Y-m-d H:i:s'), $claim->claimed_from->format('Y-m-d H:i:s'));
|
|
$this->assertEquals($until->format('Y-m-d H:i:s'), $claim->expires_at->format('Y-m-d H:i:s'));
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_check_available_stock_on_a_date()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Claim stock from day 5 to day 10
|
|
$product->claimStock(
|
|
quantity: 30,
|
|
from: now()->addDays(5),
|
|
until: now()->addDays(10)
|
|
);
|
|
|
|
// Should have full stock available before claimed_from date
|
|
$availableOnDay3 = $product->availableOnDate(now()->addDays(3));
|
|
$this->assertEquals(100, $availableOnDay3);
|
|
|
|
// Should have reduced stock during claimed period
|
|
$availableOnDay7 = $product->availableOnDate(now()->addDays(7));
|
|
$this->assertEquals(70, $availableOnDay7);
|
|
|
|
// Should have full stock available after expires_at date
|
|
$availableOnDay12 = $product->availableOnDate(now()->addDays(12));
|
|
$this->assertEquals(100, $availableOnDay12);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_handle_multiple_overlapping_claims_on_date()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// First claim: days 5-10
|
|
$product->claimStock(
|
|
quantity: 20,
|
|
from: now()->addDays(5),
|
|
until: now()->addDays(10)
|
|
);
|
|
|
|
// Second claim: days 8-15
|
|
$product->claimStock(
|
|
quantity: 30,
|
|
from: now()->addDays(8),
|
|
until: now()->addDays(15)
|
|
);
|
|
|
|
// Day 6: only first claim is active
|
|
$availableOnDay6 = $product->availableOnDate(now()->addDays(6));
|
|
$this->assertEquals(80, $availableOnDay6);
|
|
|
|
// Day 9: both claims are active
|
|
$availableOnDay9 = $product->availableOnDate(now()->addDays(9));
|
|
$this->assertEquals(50, $availableOnDay9);
|
|
|
|
// Day 13: only second claim is active
|
|
$availableOnDay13 = $product->availableOnDate(now()->addDays(13));
|
|
$this->assertEquals(70, $availableOnDay13);
|
|
|
|
// Day 20: no claims active
|
|
$availableOnDay20 = $product->availableOnDate(now()->addDays(20));
|
|
$this->assertEquals(100, $availableOnDay20);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_handles_claims_without_claimed_from_as_immediately_claimed()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Claim without claimed_from (immediately claimed)
|
|
$product->claimStock(
|
|
quantity: 25,
|
|
until: now()->addDays(10)
|
|
);
|
|
|
|
// Should be claimed immediately
|
|
$availableNow = $product->availableOnDate(now());
|
|
$this->assertEquals(75, $availableNow);
|
|
|
|
// Should still be claimed on day 7
|
|
$availableOnDay7 = $product->availableOnDate(now()->addDays(7));
|
|
$this->assertEquals(75, $availableOnDay7);
|
|
|
|
// Should be released after expiry
|
|
$availableOnDay12 = $product->availableOnDate(now()->addDays(12));
|
|
$this->assertEquals(100, $availableOnDay12);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_handles_permanent_claims_without_expires_at()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Permanent claim from day 5 onwards
|
|
$product->claimStock(
|
|
quantity: 40,
|
|
from: now()->addDays(5)
|
|
);
|
|
|
|
// Before claimed_from: full stock available
|
|
$availableOnDay3 = $product->availableOnDate(now()->addDays(3));
|
|
$this->assertEquals(100, $availableOnDay3);
|
|
|
|
// After claimed_from: reduced stock
|
|
$availableOnDay10 = $product->availableOnDate(now()->addDays(10));
|
|
$this->assertEquals(60, $availableOnDay10);
|
|
|
|
// Far future: still reduced (permanent claim)
|
|
$availableOnDay100 = $product->availableOnDate(now()->addDays(100));
|
|
$this->assertEquals(60, $availableOnDay100);
|
|
}
|
|
|
|
/** @test */
|
|
public function available_on_date_scope_filters_correctly()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Create various claims
|
|
$claim1 = $product->claimStock(
|
|
quantity: 10,
|
|
from: now()->addDays(5),
|
|
until: now()->addDays(10)
|
|
);
|
|
|
|
$claim2 = $product->claimStock(
|
|
quantity: 15,
|
|
from: now()->addDays(8),
|
|
until: now()->addDays(15)
|
|
);
|
|
|
|
$claim3 = $product->claimStock(
|
|
quantity: 20,
|
|
from: now()->addDays(20),
|
|
until: now()->addDays(25)
|
|
);
|
|
|
|
// Test scope on day 7 - should only include claim1
|
|
$claimsOnDay7 = \Blax\Shop\Models\ProductStock::availableOnDate(now()->addDays(7))
|
|
->where('product_id', $product->id)
|
|
->get();
|
|
|
|
$this->assertCount(1, $claimsOnDay7);
|
|
$this->assertEquals($claim1->id, $claimsOnDay7->first()->id);
|
|
|
|
// Test scope on day 12 - should only include claim2
|
|
$claimsOnDay12 = \Blax\Shop\Models\ProductStock::availableOnDate(now()->addDays(12))
|
|
->where('product_id', $product->id)
|
|
->get();
|
|
|
|
$this->assertCount(1, $claimsOnDay12);
|
|
$this->assertEquals($claim2->id, $claimsOnDay12->first()->id);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_get_claimed_stock_amount()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Claim some stock
|
|
$product->claimStock(quantity: 25);
|
|
$product->claimStock(quantity: 15);
|
|
|
|
// Should return total claimed
|
|
$this->assertEquals(40, $product->getClaimedStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function it_checks_if_claim_is_active()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$claim = $product->claimStock(quantity: 10);
|
|
|
|
$this->assertTrue($claim->isActive());
|
|
|
|
$claim->release();
|
|
|
|
$this->assertFalse($claim->fresh()->isActive());
|
|
}
|
|
|
|
/** @test */
|
|
public function it_releases_expired_claims()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Create expired claim
|
|
$expiredClaim = $product->claimStock(
|
|
quantity: 20,
|
|
until: now()->subHour()
|
|
);
|
|
|
|
// Create active claim
|
|
$activeClaim = $product->claimStock(
|
|
quantity: 15,
|
|
until: now()->addHours(2)
|
|
);
|
|
|
|
// Release expired claims
|
|
$count = \Blax\Shop\Models\ProductStock::releaseExpired();
|
|
|
|
$this->assertEquals(1, $count);
|
|
$this->assertEquals(StockStatus::COMPLETED, $expiredClaim->fresh()->status);
|
|
$this->assertEquals(StockStatus::PENDING, $activeClaim->fresh()->status);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_has_reference_relationship()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
$user = \Workbench\App\Models\User::factory()->create();
|
|
|
|
$claim = $product->claimStock(
|
|
quantity: 10,
|
|
reference: $user,
|
|
note: 'Reserved for user'
|
|
);
|
|
|
|
$this->assertNotNull($claim->reference);
|
|
$this->assertEquals($user->id, $claim->reference->id);
|
|
$this->assertEquals(get_class($user), $claim->reference_type);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_handles_return_stock_type()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
$product->increaseStock(50);
|
|
$product->decreaseStock(10);
|
|
|
|
// Use adjustStock with RETURN type (adds stock back)
|
|
$product->adjustStock(
|
|
type: StockType::RETURN,
|
|
quantity: 5
|
|
);
|
|
|
|
// Refresh to get updated stock
|
|
$product = $product->fresh();
|
|
|
|
// Should have 45 total (50 - 10 + 5)
|
|
$this->assertEquals(45, $product->getAvailableStock());
|
|
|
|
// Verify the return entry exists
|
|
$returnEntry = $product->stocks()->where('type', StockType::RETURN->value)->first();
|
|
$this->assertNotNull($returnEntry);
|
|
$this->assertEquals(5, $returnEntry->quantity);
|
|
}
|
|
|
|
/** @test */
|
|
public function temporary_scope_filters_correctly()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$temporary = $product->claimStock(quantity: 10, until: now()->addDay());
|
|
$permanent = $product->claimStock(quantity: 20);
|
|
|
|
$temporaryStocks = \Blax\Shop\Models\ProductStock::temporary()->get();
|
|
$permanentStocks = \Blax\Shop\Models\ProductStock::permanent()->get();
|
|
|
|
$this->assertTrue($temporaryStocks->contains($temporary));
|
|
$this->assertFalse($temporaryStocks->contains($permanent));
|
|
$this->assertTrue($permanentStocks->contains($permanent));
|
|
$this->assertFalse($permanentStocks->contains($temporary));
|
|
}
|
|
|
|
/** @test */
|
|
public function it_tracks_stock_with_custom_status()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
// Add stock with PENDING status
|
|
$product->adjustStock(
|
|
type: StockType::INCREASE,
|
|
quantity: 50,
|
|
status: StockStatus::PENDING
|
|
);
|
|
|
|
// Should not be counted in available stock (only COMPLETED counts)
|
|
$this->assertEquals(0, $product->getAvailableStock());
|
|
|
|
// Mark as completed
|
|
$stockEntry = $product->stocks()->where('type', StockType::INCREASE->value)->first();
|
|
$stockEntry->status = StockStatus::COMPLETED;
|
|
$stockEntry->save();
|
|
|
|
// Now should be available
|
|
$this->assertEquals(50, $product->fresh()->getAvailableStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function backward_compatibility_accessors_work()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$claim = $product->claimStock(
|
|
quantity: 10,
|
|
until: now()->addDays(5)
|
|
);
|
|
|
|
// Test released_at accessor (should be null for pending)
|
|
$this->assertNull($claim->released_at);
|
|
|
|
// Test until_at accessor (alias for expires_at)
|
|
$this->assertEquals($claim->expires_at->format('Y-m-d'), $claim->until_at->format('Y-m-d'));
|
|
|
|
// Release the claim
|
|
$claim->release();
|
|
|
|
// Now released_at should return updated_at
|
|
$this->assertNotNull($claim->fresh()->released_at);
|
|
$this->assertEquals($claim->fresh()->updated_at->format('Y-m-d H:i:s'), $claim->fresh()->released_at->format('Y-m-d H:i:s'));
|
|
}
|
|
|
|
/** @test */
|
|
public function adjust_stock_increase_type_affects_available_stock_correctly()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
$this->assertEquals(0, $product->getAvailableStock());
|
|
|
|
// Add stock using adjustStock with INCREASE type
|
|
$product->adjustStock(
|
|
type: StockType::INCREASE,
|
|
quantity: 50
|
|
);
|
|
|
|
$this->assertEquals(50, $product->getAvailableStock());
|
|
|
|
// Add more stock
|
|
$product->adjustStock(
|
|
type: StockType::INCREASE,
|
|
quantity: 30
|
|
);
|
|
|
|
$this->assertEquals(80, $product->getAvailableStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function adjust_stock_decrease_type_affects_available_stock_correctly()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$this->assertEquals(100, $product->getAvailableStock());
|
|
|
|
// Decrease stock using adjustStock
|
|
$product->adjustStock(
|
|
type: StockType::DECREASE,
|
|
quantity: 20
|
|
);
|
|
|
|
$this->assertEquals(80, $product->getAvailableStock());
|
|
|
|
// Decrease more stock
|
|
$product->adjustStock(
|
|
type: StockType::DECREASE,
|
|
quantity: 15
|
|
);
|
|
|
|
$this->assertEquals(65, $product->getAvailableStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function adjust_stock_return_type_affects_available_stock_correctly()
|
|
{
|
|
$product = Product::factory()->withStocks(50)->create();
|
|
$product->decreaseStock(10);
|
|
|
|
$this->assertEquals(40, $product->getAvailableStock());
|
|
|
|
// Return stock using adjustStock with RETURN type
|
|
$product->adjustStock(
|
|
type: StockType::RETURN,
|
|
quantity: 8
|
|
);
|
|
|
|
$this->assertEquals(48, $product->getAvailableStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function adjust_stock_claimed_type_affects_available_and_claimed_stock_correctly()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
$this->assertEquals(100, $product->getAvailableStock());
|
|
$this->assertEquals(0, $product->getClaimedStock());
|
|
|
|
// Claim stock using adjustStock with CLAIMED type
|
|
// Note: adjustStock(CLAIMED) now delegates to claimStock() for consistency
|
|
// This creates: DECREASE (COMPLETED) + CLAIMED (PENDING)
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 25
|
|
);
|
|
|
|
// Available stock is reduced by the DECREASE entry
|
|
$this->assertEquals(75, $product->getAvailableStock());
|
|
|
|
// Claimed stock shows the pending claim (always positive now)
|
|
$this->assertEquals(25, $product->getClaimedStock());
|
|
}
|
|
/** @test */
|
|
public function adjust_stock_with_until_parameter_expires_correctly()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Add stock with expiration date in the future
|
|
$product->adjustStock(
|
|
type: StockType::INCREASE,
|
|
quantity: 50,
|
|
until: now()->addDays(5)
|
|
);
|
|
|
|
// Should be available now
|
|
$this->assertEquals(150, $product->getAvailableStock());
|
|
|
|
// Travel to after expiration
|
|
$this->travel(6)->days();
|
|
|
|
// Should no longer be counted as available
|
|
$this->assertEquals(100, $product->getAvailableStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function adjust_stock_claimed_with_from_and_until_affects_availability_by_date()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Claim stock from day 5 to day 10 using adjustStock
|
|
// Now works correctly because adjustStock(CLAIMED) uses the same pattern as claimStock
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 30,
|
|
from: now()->addDays(5),
|
|
until: now()->addDays(10)
|
|
);
|
|
|
|
// Current available stock is reduced by the DECREASE entry
|
|
$this->assertEquals(70, $product->getAvailableStock());
|
|
|
|
// Check availability on specific dates
|
|
$availableOnDay3 = $product->availableOnDate(now()->addDays(3));
|
|
$this->assertEquals(100, $availableOnDay3); // Before claim starts
|
|
|
|
$availableOnDay7 = $product->availableOnDate(now()->addDays(7));
|
|
$this->assertEquals(70, $availableOnDay7); // During claim period
|
|
|
|
$availableOnDay12 = $product->availableOnDate(now()->addDays(12));
|
|
$this->assertEquals(100, $availableOnDay12); // After claim expires
|
|
}
|
|
/** @test */
|
|
public function adjust_stock_multiple_claimed_types_accumulate_in_claimed_stock()
|
|
{
|
|
$product = Product::factory()->withStocks(200)->create();
|
|
|
|
// Make multiple claims using adjustStock
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 20
|
|
);
|
|
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 35
|
|
);
|
|
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 15
|
|
);
|
|
|
|
// Available stock is reduced by all claims
|
|
$this->assertEquals(130, $product->getAvailableStock());
|
|
|
|
// Total claimed stock (always positive)
|
|
$this->assertEquals(70, $product->getClaimedStock());
|
|
}
|
|
/** @test */
|
|
public function adjust_stock_claimed_with_completed_status_does_not_count_as_claimed()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Note: adjustStock(CLAIMED) now always delegates to claimStock() which creates PENDING claims
|
|
// This test no longer makes sense with the corrected implementation
|
|
// Manual claim with COMPLETED status can still be created directly
|
|
$product->stocks()->create([
|
|
'type' => StockType::CLAIMED,
|
|
'quantity' => 25,
|
|
'status' => StockStatus::COMPLETED,
|
|
]);
|
|
|
|
// Available stock unchanged (this is completed/released claim)
|
|
$this->assertEquals(100, $product->getAvailableStock());
|
|
|
|
// Should NOT count as claimed stock (only PENDING claims count)
|
|
$this->assertEquals(0, $product->getClaimedStock());
|
|
}
|
|
|
|
/** @test */
|
|
public function adjust_stock_with_mixed_types_calculates_correctly()
|
|
{
|
|
$product = Product::factory()->create(['manage_stock' => true]);
|
|
|
|
// Start with 100
|
|
$product->adjustStock(type: StockType::INCREASE, quantity: 100);
|
|
$this->assertEquals(100, $product->getAvailableStock());
|
|
|
|
// Claim 30 - now reduces available stock
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 30
|
|
);
|
|
$this->assertEquals(70, $product->getAvailableStock());
|
|
$this->assertEquals(30, $product->getClaimedStock());
|
|
|
|
// Decrease 20 (regular decrease with COMPLETED status)
|
|
$product->adjustStock(type: StockType::DECREASE, quantity: 20);
|
|
$this->assertEquals(50, $product->getAvailableStock());
|
|
$this->assertEquals(30, $product->getClaimedStock());
|
|
|
|
// Return 10 (adds back to stock)
|
|
$product->adjustStock(type: StockType::RETURN, quantity: 10);
|
|
$this->assertEquals(60, $product->getAvailableStock());
|
|
$this->assertEquals(30, $product->getClaimedStock());
|
|
|
|
// Increase 25
|
|
$product->adjustStock(type: StockType::INCREASE, quantity: 25);
|
|
$this->assertEquals(85, $product->getAvailableStock());
|
|
$this->assertEquals(30, $product->getClaimedStock());
|
|
}
|
|
/** @test */
|
|
public function adjust_stock_claimed_without_from_is_immediately_active()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Claim without 'from' date - should be immediately active
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 40,
|
|
until: now()->addDays(10)
|
|
);
|
|
|
|
// Should be claimed right now
|
|
$availableNow = $product->availableOnDate(now());
|
|
$this->assertEquals(60, $availableNow);
|
|
|
|
// Should still be claimed tomorrow
|
|
$availableTomorrow = $product->availableOnDate(now()->addDays(1));
|
|
$this->assertEquals(60, $availableTomorrow);
|
|
|
|
// Should be released after expiration
|
|
$availableAfter = $product->availableOnDate(now()->addDays(11));
|
|
$this->assertEquals(100, $availableAfter);
|
|
}
|
|
/** @test */
|
|
public function adjust_stock_claimed_without_until_is_permanent_claim()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Permanent claim from day 5 onwards (no until date)
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 35,
|
|
from: now()->addDays(5)
|
|
);
|
|
|
|
// Before 'from' date: full stock available
|
|
$availableOnDay3 = $product->availableOnDate(now()->addDays(3));
|
|
$this->assertEquals(100, $availableOnDay3);
|
|
|
|
// After 'from' date: permanently reduced
|
|
$availableOnDay10 = $product->availableOnDate(now()->addDays(10));
|
|
$this->assertEquals(65, $availableOnDay10);
|
|
|
|
// Far future: still reduced (permanent)
|
|
$availableOnDay100 = $product->availableOnDate(now()->addDays(100));
|
|
$this->assertEquals(65, $availableOnDay100);
|
|
}
|
|
/** @test */
|
|
public function adjust_stock_with_overlapping_claimed_periods_calculates_correctly()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// First claim: days 5-15
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 20,
|
|
from: now()->addDays(5),
|
|
until: now()->addDays(15)
|
|
);
|
|
|
|
// Second claim: days 10-20
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 30,
|
|
from: now()->addDays(10),
|
|
until: now()->addDays(20)
|
|
);
|
|
|
|
// Day 3: no claims active
|
|
$this->assertEquals(100, $product->availableOnDate(now()->addDays(3)));
|
|
|
|
// Day 7: only first claim active
|
|
$this->assertEquals(80, $product->availableOnDate(now()->addDays(7)));
|
|
|
|
// Day 12: both claims active
|
|
$this->assertEquals(50, $product->availableOnDate(now()->addDays(12)));
|
|
|
|
// Day 18: only second claim active
|
|
$this->assertEquals(70, $product->availableOnDate(now()->addDays(18)));
|
|
|
|
// Day 25: no claims active
|
|
$this->assertEquals(100, $product->availableOnDate(now()->addDays(25)));
|
|
|
|
// Current available stock (both claims reduce it)
|
|
$this->assertEquals(50, $product->getAvailableStock());
|
|
|
|
// Total claimed stock
|
|
$this->assertEquals(50, $product->getClaimedStock());
|
|
}
|
|
/** @test */
|
|
public function adjust_stock_with_note_and_reference_tracks_correctly()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
$user = \Workbench\App\Models\User::factory()->create();
|
|
|
|
// Claim with note and reference
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 25,
|
|
note: 'VIP customer reservation',
|
|
referencable: $user
|
|
);
|
|
|
|
// Available stock is reduced
|
|
$this->assertEquals(75, $product->getAvailableStock());
|
|
// Claimed stock shows positive value
|
|
$this->assertEquals(25, $product->getClaimedStock());
|
|
|
|
// Verify note and reference are stored
|
|
$claim = $product->stocks()->where('type', StockType::CLAIMED->value)->first();
|
|
$this->assertEquals('VIP customer reservation', $claim->note);
|
|
$this->assertEquals($user->id, $claim->reference_id);
|
|
$this->assertEquals(get_class($user), $claim->reference_type);
|
|
}
|
|
|
|
/** @test */
|
|
public function adjust_stock_expired_claims_dont_affect_current_availability()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Create an expired claim using adjustStock
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 30,
|
|
from: now()->subDays(5),
|
|
until: now()->subDays(1)
|
|
);
|
|
|
|
// Available stock is reduced (the DECREASE is permanent)
|
|
$this->assertEquals(70, $product->getAvailableStock());
|
|
|
|
// Claimed stock still shows the pending claim
|
|
$this->assertEquals(30, $product->getClaimedStock());
|
|
|
|
// Active claims (not expired) should be empty
|
|
$activeClaims = $product->claims()->get();
|
|
$this->assertCount(0, $activeClaims);
|
|
}
|
|
|
|
/** @test */
|
|
public function adjust_stock_releasing_claimed_updates_calculations()
|
|
{
|
|
$product = Product::factory()->withStocks(100)->create();
|
|
|
|
// Create claim using adjustStock
|
|
$product->adjustStock(
|
|
type: StockType::CLAIMED,
|
|
quantity: 40
|
|
);
|
|
|
|
$this->assertEquals(60, $product->getAvailableStock());
|
|
$this->assertEquals(40, $product->getClaimedStock());
|
|
|
|
// Find and release the claim
|
|
$claim = $product->stocks()
|
|
->where('type', StockType::CLAIMED->value)
|
|
->where('status', StockStatus::PENDING->value)
|
|
->first();
|
|
|
|
$claim->release();
|
|
|
|
// Claimed stock should drop to 0
|
|
$this->assertEquals(0, $product->getClaimedStock());
|
|
|
|
// Available stock is restored when claim is released
|
|
$this->assertEquals(100, $product->getAvailableStock());
|
|
}
|
|
}
|