laravel-shop/tests/Feature/ProductStockTest.php

1578 lines
53 KiB
PHP
Raw Normal View History

2025-11-25 16:14:00 +00:00
<?php
namespace Blax\Shop\Tests\Feature;
use Blax\Shop\Enums\StockStatus;
use Blax\Shop\Enums\StockType;
2025-11-25 16:14:00 +00:00
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;
2025-12-24 18:40:10 +00:00
use PHPUnit\Framework\Attributes\Test;
2025-11-25 16:14:00 +00:00
class ProductStockTest extends TestCase
{
use RefreshDatabase;
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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',
]);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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',
]);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function claim_reduces_available_stock()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(100)->create();
2025-12-04 10:06:09 +00:00
$claim = $product->claimStock(25);
2025-11-25 16:14:00 +00:00
$this->assertEquals(75, $product->getAvailableStock());
2025-12-04 10:06:09 +00:00
$this->assertNotNull($claim);
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function releasing_claim_increases_available_stock()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(100)->create();
2025-12-04 10:06:09 +00:00
$claim = $product->claimStock(25);
2025-11-25 16:14:00 +00:00
$this->assertEquals(75, $product->getAvailableStock());
2025-12-04 10:06:09 +00:00
$claim->release();
2025-11-25 16:14:00 +00:00
$this->assertEquals(100, $product->refresh()->getAvailableStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function permanent_claim_has_no_expiry()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(50)->create();
2025-12-04 10:06:09 +00:00
$claim = $product->claimStock(10);
2025-11-25 16:14:00 +00:00
2025-12-04 10:06:09 +00:00
$this->assertNull($claim->expires_at);
$this->assertTrue($claim->isPermanent());
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function temporary_claim_has_expiry()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(50)->create();
2025-12-04 10:06:09 +00:00
$claim = $product->claimStock(
2025-11-25 16:14:00 +00:00
quantity: 10,
until: now()->addHours(2)
);
2025-12-04 10:06:09 +00:00
$this->assertNotNull($claim->expires_at);
$this->assertTrue($claim->isTemporary());
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function claim_can_have_note()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(50)->create();
2025-12-04 10:06:09 +00:00
$note = 'Claimed for VIP customer';
$claim = $product->claimStock(
2025-11-25 16:14:00 +00:00
quantity: 10,
note: $note
);
2025-12-04 10:06:09 +00:00
$this->assertEquals($note, $claim->note);
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function cannot_claim_more_than_available()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(10)->create();
$this->expectException(NotEnoughStockException::class);
2025-12-04 10:06:09 +00:00
$product->claimStock(15);
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function pending_scope_returns_unreleased_claims()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(100)->create();
2025-12-04 10:06:09 +00:00
$pending = $product->claimStock(10);
$released = $product->claimStock(5);
2025-11-25 16:14:00 +00:00
$released->release();
2025-12-04 10:06:09 +00:00
$pendingClaims = ProductStock::pending()->get();
2025-11-25 16:14:00 +00:00
2025-12-04 10:06:09 +00:00
$this->assertTrue($pendingClaims->contains($pending));
$this->assertFalse($pendingClaims->contains($released));
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function released_scope_returns_released_claims()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(100)->create();
2025-12-04 10:06:09 +00:00
$pending = $product->claimStock(10);
$released = $product->claimStock(5);
2025-11-25 16:14:00 +00:00
$released->release();
2025-12-04 10:06:09 +00:00
$releasedClaims = ProductStock::released()->get();
2025-11-25 16:14:00 +00:00
2025-12-04 10:06:09 +00:00
$this->assertFalse($releasedClaims->contains($pending));
$this->assertTrue($releasedClaims->contains($released));
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function expired_claims_dont_affect_available_stock()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(100)->create();
2025-12-04 10:06:09 +00:00
$product->claimStock(
2025-11-25 16:14:00 +00:00
quantity: 20,
until: now()->subHour()
);
2025-12-04 10:06:09 +00:00
// Expired claims should be counted in available stock
$available = $product->claims()->get();
2025-12-03 14:45:11 +00:00
2025-11-25 16:14:00 +00:00
$this->assertEquals(0, $available->count());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
public function cannot_release_stock_twice()
{
$product = Product::factory()->withStocks(50)->create();
2025-12-04 10:06:09 +00:00
$claim = $product->claimStock(10);
2025-11-25 16:14:00 +00:00
2025-12-04 10:06:09 +00:00
$this->assertTrue($claim->release());
$this->assertFalse($claim->release());
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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);
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function claim_without_stock_management_returns_null()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->create(['manage_stock' => false]);
2025-12-04 10:06:09 +00:00
$claim = $product->claimStock(10);
2025-11-25 16:14:00 +00:00
2025-12-04 10:06:09 +00:00
$this->assertNull($claim);
2025-11-25 16:14:00 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-11-25 16:14:00 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
public function claims_method_filters_active_only()
2025-11-25 16:14:00 +00:00
{
$product = Product::factory()->withStocks(100)->create();
2025-12-04 10:06:09 +00:00
$active = $product->claimStock(10, until: now()->addDay());
$expired = $product->claimStock(5, until: now()->subDay());
2025-11-25 16:14:00 +00:00
2025-12-04 10:06:09 +00:00
$claims = $product->claims()->get();
2025-11-25 16:14:00 +00:00
2025-12-04 10:06:09 +00:00
$this->assertCount(1, $claims);
$this->assertEquals($active->id, $claims->first()->id);
2025-11-25 16:14:00 +00:00
}
2025-12-03 14:45:11 +00:00
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-03 14:45:11 +00:00
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());
}
2025-12-04 10:06:09 +00:00
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
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'));
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 10:06:09 +00:00
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);
}
2025-12-24 18:40:10 +00:00
#[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
2025-12-05 09:23:47 +00:00
$this->assertEquals(40, $product->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[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());
}
2025-12-24 18:40:10 +00:00
#[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);
}
2025-12-24 18:40:10 +00:00
#[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);
}
2025-12-24 18:40:10 +00:00
#[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);
}
2025-12-24 18:40:10 +00:00
#[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));
}
2025-12-24 18:40:10 +00:00
#[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());
}
2025-12-24 18:40:10 +00:00
#[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'));
}
2025-12-24 18:40:10 +00:00
#[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());
}
2025-12-24 18:40:10 +00:00
#[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());
}
2025-12-24 18:40:10 +00:00
#[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());
}
2025-12-24 18:40:10 +00:00
#[Test]
public function adjust_stock_claimed_type_affects_available_and_claimed_stock_correctly()
{
$product = Product::factory()->withStocks(100)->create();
$this->assertEquals(100, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
// 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)
2025-12-05 09:23:47 +00:00
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[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());
}
2025-12-24 18:40:10 +00:00
#[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)
);
2025-12-05 09:23:47 +00:00
// Current available stock is unaffected until the claim starts
$this->assertEquals(100, $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
}
2025-12-24 18:40:10 +00:00
#[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)
2025-12-05 09:23:47 +00:00
$this->assertEquals(70, $product->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[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)
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[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());
2025-12-05 09:23:47 +00:00
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
// Decrease 20 (regular decrease with COMPLETED status)
$product->adjustStock(type: StockType::DECREASE, quantity: 20);
$this->assertEquals(50, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
// Return 10 (adds back to stock)
$product->adjustStock(type: StockType::RETURN, quantity: 10);
$this->assertEquals(60, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
// Increase 25
$product->adjustStock(type: StockType::INCREASE, quantity: 25);
$this->assertEquals(85, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[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);
}
2025-12-24 18:40:10 +00:00
#[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);
}
2025-12-24 18:40:10 +00:00
#[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)
);
2025-12-05 09:23:47 +00:00
$this->assertEquals(100, $product->getAvailableStock());
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
$this->assertEquals(20, $product->getActiveAndPlannedClaimedStock());
$this->assertEquals(20, $product->getFutureClaimedStock());
// Second claim: days 10-20
$product->adjustStock(
type: StockType::CLAIMED,
quantity: 30,
from: now()->addDays(10),
until: now()->addDays(20)
);
2025-12-05 09:23:47 +00:00
$this->assertEquals(100, $product->getAvailableStock());
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
$this->assertEquals(50, $product->getActiveAndPlannedClaimedStock());
$this->assertEquals(50, $product->getFutureClaimedStock());
// 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)));
2025-12-05 09:23:47 +00:00
// Current available stock (future claims do not reduce until active)
$this->assertEquals(100, $product->getAvailableStock());
$this->travel(6)->days();
2025-12-05 09:23:47 +00:00
$this->assertEquals(80, $product->getAvailableStock());
$this->assertEquals(20, $product->getCurrentlyClaimedStock());
$this->assertEquals(50, $product->getActiveAndPlannedClaimedStock());
$this->assertEquals(30, $product->getFutureClaimedStock());
$this->travel(6)->days();
$this->assertEquals(50, $product->getAvailableStock());
$this->assertEquals(50, $product->getCurrentlyClaimedStock());
$this->assertEquals(50, $product->getActiveAndPlannedClaimedStock());
$this->assertEquals(0, $product->getFutureClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[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
2025-12-05 09:23:47 +00:00
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
// 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);
}
2025-12-24 18:40:10 +00:00
#[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)
);
2025-12-04 11:58:34 +00:00
// Available stock is automatically restored (expired claims add stock back)
$this->assertEquals(100, $product->getAvailableStock());
2025-12-04 11:58:34 +00:00
// Claimed stock does NOT show expired claims (automatically excluded)
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
// Active claims (not expired) should be empty
$activeClaims = $product->claims()->get();
$this->assertCount(0, $activeClaims);
}
2025-12-24 18:40:10 +00:00
#[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());
2025-12-05 09:23:47 +00:00
$this->assertEquals(40, $product->getCurrentlyClaimedStock());
// 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
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
// Available stock is restored when claim is released
$this->assertEquals(100, $product->getAvailableStock());
}
2025-12-04 11:58:34 +00:00
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 11:58:34 +00:00
public function stock_claim_creates_correct_transactions()
{
$product = Product::factory()->withStocks(100)->create();
// Claim 30 units for 5 days
$claim = $product->claimStock(
quantity: 30,
until: now()->addDays(5)
);
$this->assertNotNull($claim);
// Should create two entries: DECREASE (COMPLETED) + CLAIMED (PENDING)
$decreaseEntry = $product->stocks()
->where('type', StockType::DECREASE->value)
->where('status', StockStatus::COMPLETED->value)
->first();
$claimedEntry = $product->stocks()
->where('type', StockType::CLAIMED->value)
->where('status', StockStatus::PENDING->value)
->first();
$this->assertNotNull($decreaseEntry, 'DECREASE entry should exist');
$this->assertEquals(-30, $decreaseEntry->quantity);
$this->assertEquals(StockStatus::COMPLETED, $decreaseEntry->status);
$this->assertNotNull($claimedEntry, 'CLAIMED entry should exist');
$this->assertEquals(30, $claimedEntry->quantity);
$this->assertEquals(StockStatus::PENDING, $claimedEntry->status);
$this->assertNotNull($claimedEntry->expires_at);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 11:58:34 +00:00
public function claimed_stock_reduces_available_and_increases_claimed()
{
$product = Product::factory()->withStocks(100)->create();
// Initial state
$this->assertEquals(100, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
// Claim 30 units
$product->claimStock(quantity: 30, until: now()->addDays(5));
// Available stock should be reduced
$this->assertEquals(70, $product->getAvailableStock());
// Claimed stock should show the claim
2025-12-05 09:23:47 +00:00
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
// Claim another 20 units
$product->claimStock(quantity: 20, until: now()->addDays(3));
// Available stock should be further reduced
$this->assertEquals(50, $product->getAvailableStock());
// Claimed stock should show both claims
2025-12-05 09:23:47 +00:00
$this->assertEquals(50, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 11:58:34 +00:00
public function expired_claims_automatically_restore_available_stock()
{
$product = Product::factory()->withStocks(100)->create();
// Claim 30 units, expires in 5 days
$product->claimStock(quantity: 30, until: now()->addDays(5));
// During claim period: available reduced, claimed shows 30
$this->assertEquals(70, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
// Travel to day 3 (still within claim period)
$this->travel(3)->days();
$this->assertEquals(70, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
// Travel to day 6 (after expiration)
$this->travel(3)->days();
// Available stock should be automatically restored
$this->assertEquals(100, $product->getAvailableStock());
// Claimed stock should be 0 (expired claims excluded)
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
// No manual release() was called!
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 11:58:34 +00:00
public function multiple_claims_with_different_expirations_restore_progressively()
{
$product = Product::factory()->withStocks(100)->create();
// Claim 1: 20 units, expires in 3 days
$product->claimStock(quantity: 20, until: now()->addDays(3));
// Claim 2: 30 units, expires in 7 days
$product->claimStock(quantity: 30, until: now()->addDays(7));
// Initial state: both claims active
$this->assertEquals(50, $product->getAvailableStock()); // 100 - 20 - 30
2025-12-05 09:23:47 +00:00
$this->assertEquals(50, $product->getCurrentlyClaimedStock()); // 20 + 30
2025-12-04 11:58:34 +00:00
// Travel to day 4 (first claim expired, second still active)
$this->travel(4)->days();
$this->assertEquals(70, $product->getAvailableStock()); // 100 - 30 (only second claim)
2025-12-05 09:23:47 +00:00
$this->assertEquals(30, $product->getCurrentlyClaimedStock()); // Only second claim
2025-12-04 11:58:34 +00:00
// Travel to day 8 (both claims expired)
$this->travel(4)->days();
$this->assertEquals(100, $product->getAvailableStock()); // All restored
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock()); // No claims
2025-12-04 11:58:34 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 11:58:34 +00:00
public function permanent_claims_without_expiration_never_auto_restore()
{
$product = Product::factory()->withStocks(100)->create();
// Permanent claim (no until date)
$product->claimStock(quantity: 25);
// Available stock reduced, claimed shows 25
$this->assertEquals(75, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
// Travel far into the future
$this->travel(100)->days();
// Permanent claim never expires
$this->assertEquals(75, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
// Must manually release permanent claims
$claim = $product->stocks()
->where('type', StockType::CLAIMED->value)
->where('status', StockStatus::PENDING->value)
->first();
$claim->release();
// Now stock is restored
$this->assertEquals(100, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 11:58:34 +00:00
public function adjust_stock_claimed_also_auto_restores_after_expiration()
{
$product = Product::factory()->withStocks(100)->create();
// Use adjustStock instead of claimStock
$product->adjustStock(
type: StockType::CLAIMED,
quantity: 40,
until: now()->addDays(5)
);
// During claim period
$this->assertEquals(60, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(40, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
// After expiration
$this->travel(6)->days();
// Stock automatically restored
$this->assertEquals(100, $product->getAvailableStock());
2025-12-05 09:23:47 +00:00
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
2025-12-04 11:58:34 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-04 11:58:34 +00:00
public function claimed_stock_transactions_maintain_data_integrity()
{
$product = Product::factory()->withStocks(100)->create();
// Claim 35 units
$claim = $product->claimStock(quantity: 35, until: now()->addDays(5));
// Verify DECREASE entry
$decrease = $product->stocks()
->where('type', StockType::DECREASE->value)
->latest()
->first();
$this->assertEquals(-35, $decrease->quantity);
$this->assertEquals(StockStatus::COMPLETED, $decrease->status);
// Verify CLAIMED entry matches the returned claim
$this->assertEquals($claim->id, $product->stocks()
->where('type', StockType::CLAIMED->value)
->latest()
->first()
->id);
$this->assertEquals(35, $claim->quantity);
$this->assertEquals(StockStatus::PENDING, $claim->status);
// Verify totals
$allCompleted = $product->stocks()
->where('status', StockStatus::COMPLETED->value)
->where('type', '!=', StockType::CLAIMED->value)
->sum('quantity');
// Should be: initial increase (100) + decrease (-35) = 65
// But getAvailableStock applies willExpire filter
$this->assertEquals(65, $product->getAvailableStock());
$allClaimed = $product->stocks()
->where('type', StockType::CLAIMED->value)
->where('status', StockStatus::PENDING->value)
->sum('quantity');
$this->assertEquals(35, $allClaimed);
}
2025-12-05 09:23:47 +00:00
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function can_get_all_stocks_including_pending_and_expired()
{
$product = Product::factory()->withStocks(100)->create();
$product->adjustStock(type: StockType::INCREASE, quantity: 50);
$this->assertEquals(150, $product->getAvailableStock());
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
$product->adjustStock(type: StockType::DECREASE, quantity: 20);
$this->assertEquals(130, $product->getAvailableStock());
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
$product->adjustStock(type: StockType::CLAIMED, quantity: 30, until: now()->addDays(5));
$this->assertEquals(100, $product->getAvailableStock());
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
$product->adjustStock(type: StockType::CLAIMED, quantity: 10, until: now()->subDays(1));
$this->assertEquals(100, $product->getAvailableStock());
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
$product->adjustStock(type: StockType::CLAIMED, quantity: 10, from: now()->addDays(1), until: now()->addDays(5));
$this->assertEquals(100, $product->getAvailableStock());
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
$allStocks = $product->allStocks()->get();
$types = $allStocks->pluck('type')->toArray();
$this->assertCount(9, $allStocks);
$this->assertContains(StockType::INCREASE, $types);
$this->assertContains(StockType::DECREASE, $types);
$this->assertEquals(3, count(array_filter($types, fn($t) => $t === StockType::CLAIMED)));
$this->travel(2)->days();
$this->assertEquals(90, $product->getAvailableStock());
$this->assertEquals(40, $product->getCurrentlyClaimedStock());
$this->travel(5)->days();
$this->assertEquals(130, $product->getAvailableStock());
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_claimed_stock_returns_active_pending_claims_only()
{
$product = Product::factory()->withStocks(100)->create();
// Create active claims (immediately claimed)
$product->claimStock(quantity: 20);
$product->claimStock(quantity: 15);
// Create future claim (starts later)
$product->claimStock(quantity: 10, from: now()->addDays(5));
// Create expired claim
$product->claimStock(quantity: 25, until: now()->subDay());
// getCurrentlyClaimedStock should only count active claims: 20 + 15 = 35
$this->assertEquals(35, $product->getCurrentlyClaimedStock());
// Verify the future claim and expired claim are not counted
$allClaims = $product->stocks()
->where('type', StockType::CLAIMED->value)
->get();
$this->assertCount(4, $allClaims);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_claimed_stock_excludes_released_claims()
{
$product = Product::factory()->withStocks(100)->create();
// Create claims
$claim1 = $product->claimStock(quantity: 20);
$claim2 = $product->claimStock(quantity: 30);
$this->assertEquals(50, $product->getCurrentlyClaimedStock());
// Release one claim
$claim1->release();
// Should only count remaining active claim
$this->assertEquals(30, $product->fresh()->getCurrentlyClaimedStock());
// Release the other claim
$claim2->release();
// Should be zero when all released
$this->assertEquals(0, $product->fresh()->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_claimed_stock_accounts_for_claim_expiration()
{
$product = Product::factory()->withStocks(200)->create();
// Create claims with different expiration dates
$product->claimStock(quantity: 50, until: now()->addDays(2));
$product->claimStock(quantity: 75, until: now()->addDays(10));
$product->claimStock(quantity: 30); // permanent claim (no expiration)
// Initially, all should be claimed
$this->assertEquals(155, $product->getCurrentlyClaimedStock());
// Travel to day 3 - first claim should have expired
$this->travel(3)->days();
$this->assertEquals(105, $product->fresh()->getCurrentlyClaimedStock());
// Travel to day 11 - second claim should have expired
$this->travel(9)->days();
$this->assertEquals(30, $product->fresh()->getCurrentlyClaimedStock());
// Permanent claim should never expire
$this->travel(100)->days();
$this->assertEquals(30, $product->fresh()->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_total_planned_claimed_stock_includes_future_claims()
{
$product = Product::factory()->withStocks(300)->create();
// Create active claims
$product->claimStock(quantity: 25);
// Create future claims (start later)
$product->claimStock(quantity: 50, from: now()->addDays(2));
$product->claimStock(quantity: 35, from: now()->addDays(7));
// Create permanent claim from specific date
$product->claimStock(quantity: 40, from: now()->addDays(15));
// getActiveAndPlannedClaimedStock should include all PENDING claims regardless of start date
// This includes: 25 + 50 + 35 + 40 = 150
$this->assertEquals(150, $product->getActiveAndPlannedClaimedStock());
// getCurrentlyClaimedStock should only include active ones (started)
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_total_planned_claimed_stock_excludes_expired_claims()
{
$product = Product::factory()->withStocks(200)->create();
// Create various claims
$product->claimStock(quantity: 30, until: now()->subDay()); // already expired
$product->claimStock(quantity: 40, until: now()->addDays(5)); // will expire
$product->claimStock(quantity: 50); // permanent
// Should only count non-expired PENDING claims: 40 + 50 = 90
$this->assertEquals(90, $product->getActiveAndPlannedClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_total_planned_claimed_stock_excludes_released_claims()
{
$product = Product::factory()->withStocks(200)->create();
// Create claims
$claim1 = $product->claimStock(quantity: 35, from: now()->addDays(3));
$claim2 = $product->claimStock(quantity: 45, from: now()->addDays(8));
$claim3 = $product->claimStock(quantity: 25);
$this->assertEquals(105, $product->getActiveAndPlannedClaimedStock());
// Release one future claim
$claim1->release();
// Should not count released claims: 45 + 25 = 70
$this->assertEquals(70, $product->fresh()->getActiveAndPlannedClaimedStock());
// Release another
$claim2->release();
// Should only count unreleased: 25
$this->assertEquals(25, $product->fresh()->getActiveAndPlannedClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_future_claimed_stock_without_from_date_parameter()
{
$product = Product::factory()->withStocks(300)->create();
// Create immediate claim (no claimed_from)
$product->claimStock(quantity: 20);
// Create future claims
$product->claimStock(quantity: 30, from: now()->addDays(2));
$product->claimStock(quantity: 40, from: now()->addDays(5));
// Without parameter, getFutureClaimedStock should only count claims with claimed_from > now()
$this->assertEquals(70, $product->getFutureClaimedStock());
// Travel forward
$this->travel(3)->days();
// Now the first future claim (day 2) is active, so only the day 5 one remains
$this->assertEquals(40, $product->fresh()->getFutureClaimedStock());
// Travel more
$this->travel(3)->days();
// All future claims have started
$this->assertEquals(0, $product->fresh()->getFutureClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_future_claimed_stock_with_from_date_parameter()
{
$product = Product::factory()->withStocks(300)->create();
// Create claims
$product->claimStock(quantity: 20); // no claimed_from
$product->claimStock(quantity: 30, from: now()->addDays(3));
$product->claimStock(quantity: 40, from: now()->addDays(8));
$product->claimStock(quantity: 25, from: now()->addDays(15));
// Get claims starting from day 5 onwards
$fromDay5 = $product->getFutureClaimedStock(now()->addDays(5));
$this->assertEquals(65, $fromDay5); // 40 + 25
// Get claims starting from day 10 onwards
$fromDay10 = $product->getFutureClaimedStock(now()->addDays(10));
$this->assertEquals(25, $fromDay10); // 25
// Get claims starting from future date with no claims
$fromDay20 = $product->getFutureClaimedStock(now()->addDays(20));
$this->assertEquals(0, $fromDay20);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function get_future_claimed_stock_excludes_expired_claims()
{
$product = Product::factory()->withStocks(300)->create();
// Future claim that expires before being used
$product->claimStock(quantity: 30, from: now()->addDays(2), until: now()->addDays(3));
// Future claim with normal expiration
$product->claimStock(quantity: 50, from: now()->addDays(5), until: now()->addDays(10));
// Without parameter, both are future claims
$this->assertEquals(80, $product->getFutureClaimedStock());
// Travel to day 4 (first claim expired on day 3, second still active)
$this->travel(4)->days();
// getFutureClaimedStock uses willExpire() which filters out expired claims
// The first claim is expired, so only the second one counts
// The second claim still starts in the future (day 5 from original, which is tomorrow)
$this->assertEquals(50, $product->fresh()->getFutureClaimedStock());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function claimed_stock_methods_work_together()
{
$product = Product::factory()->withStocks(500)->create();
// Create a mix of claims
$product->claimStock(quantity: 50); // active now
$product->claimStock(quantity: 75, from: now()->addDays(3)); // future
$product->claimStock(quantity: 40, from: now()->addDays(7), until: now()->addDays(10)); // future with expiry
$product->claimStock(quantity: 60); // another active now
// Test all three methods
$this->assertEquals(110, $product->getCurrentlyClaimedStock()); // 50 + 60 (active claims)
$this->assertEquals(225, $product->getActiveAndPlannedClaimedStock()); // all 4 claims
$this->assertEquals(115, $product->getFutureClaimedStock()); // 75 + 40 (future claims)
// Release one active claim
$activeClaimId = $product->stocks()
->where('type', StockType::CLAIMED->value)
->where('status', StockStatus::PENDING->value)
->whereNull('claimed_from')
->first()
->id;
$product->stocks()->find($activeClaimId)->release();
// Recalculate
$this->assertEquals(60, $product->fresh()->getCurrentlyClaimedStock()); // only one active now
$this->assertEquals(175, $product->fresh()->getActiveAndPlannedClaimedStock()); // 3 remaining
$this->assertEquals(115, $product->fresh()->getFutureClaimedStock()); // future ones unchanged
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-05 09:23:47 +00:00
public function claimed_stock_methods_return_zero_for_unmanaged_stock()
{
$product = Product::factory()->create(['manage_stock' => false]);
// Try to claim stock (should return null)
$claim = $product->claimStock(quantity: 50);
$this->assertNull($claim);
// Methods should still return 0
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
$this->assertEquals(0, $product->getActiveAndPlannedClaimedStock());
$this->assertEquals(0, $product->getFutureClaimedStock());
}
2025-11-25 16:14:00 +00:00
}