BFI stocks
This commit is contained in:
parent
4a303c0f3f
commit
7c3facc3f5
|
|
@ -38,13 +38,23 @@ use Illuminate\Support\Facades\DB;
|
|||
trait HasStocks
|
||||
{
|
||||
/**
|
||||
* Get all stock entries for this product
|
||||
* Get all available stock entries for this product
|
||||
*/
|
||||
public function stocks(): HasMany
|
||||
{
|
||||
return $this->hasMany(config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all stock entries for this product including unavailable ones
|
||||
*/
|
||||
public function allStocks(): HasMany
|
||||
{
|
||||
return $this->hasMany(config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock'))
|
||||
->withExpired()
|
||||
->where('status', 'LIKE', '%');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute accessor: Get available physical stock
|
||||
*
|
||||
|
|
@ -274,28 +284,44 @@ trait HasStocks
|
|||
*
|
||||
* @return int Available quantity (PHP_INT_MAX if stock management disabled)
|
||||
*/
|
||||
public function getAvailableStock(): int
|
||||
public function getAvailableStock(?\DateTimeInterface $date = null): int
|
||||
{
|
||||
if (!$this->manage_stock) {
|
||||
return PHP_INT_MAX;
|
||||
}
|
||||
|
||||
// Base stock: all COMPLETED entries except CLAIMED, filtered by expiration
|
||||
$date = $date ?? now();
|
||||
|
||||
// Base stock: all COMPLETED entries except CLAIMED, filtered using the provided date
|
||||
$baseStock = $this->stocks()
|
||||
->withoutGlobalScope('willExpire')
|
||||
->where('status', StockStatus::COMPLETED->value)
|
||||
->where('type', '!=', StockType::CLAIMED->value)
|
||||
->willExpire()
|
||||
->where(function ($query) use ($date) {
|
||||
$query->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', $date);
|
||||
})
|
||||
->sum('quantity');
|
||||
|
||||
// Add back expired claims (these had DECREASE entries that should no longer reduce stock)
|
||||
$expiredClaims = $this->stocks()
|
||||
// Add back claims that should not reduce availability at the given date
|
||||
$inactiveClaims = $this->stocks()
|
||||
->withoutGlobalScope('willExpire')
|
||||
->where('type', StockType::CLAIMED->value)
|
||||
->where('status', StockStatus::PENDING->value)
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<=', now())
|
||||
->where(function ($query) use ($date) {
|
||||
$query->where(function ($q) use ($date) {
|
||||
// Claim has not started yet
|
||||
$q->whereNotNull('claimed_from')
|
||||
->where('claimed_from', '>', $date);
|
||||
})->orWhere(function ($q) use ($date) {
|
||||
// Claim expired before the date
|
||||
$q->whereNotNull('expires_at')
|
||||
->where('expires_at', '<=', $date);
|
||||
});
|
||||
})
|
||||
->sum('quantity');
|
||||
|
||||
return max(0, $baseStock + $expiredClaims);
|
||||
return max(0, $baseStock + $inactiveClaims);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -307,7 +333,27 @@ trait HasStocks
|
|||
*
|
||||
* @return int Total claimed quantity (always positive)
|
||||
*/
|
||||
public function getClaimedStock(): int
|
||||
public function getCurrentlyClaimedStock(): int
|
||||
{
|
||||
return abs($this->stocks()
|
||||
->where('type', StockType::CLAIMED->value)
|
||||
->where('status', StockStatus::PENDING->value)
|
||||
->willExpire()
|
||||
->where(function ($query) {
|
||||
$query->whereNull('claimed_from')
|
||||
->orWhere('claimed_from', '<=', now());
|
||||
})
|
||||
->sum('quantity'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total current and planned claimed stock
|
||||
*
|
||||
* Includes all PENDING claims, regardless of start date.
|
||||
* Useful for understanding total reservations including future bookings.
|
||||
* @return int Total current and future claimed quantity (always positive)
|
||||
*/
|
||||
public function getActiveAndPlannedClaimedStock(): int
|
||||
{
|
||||
return abs($this->stocks()
|
||||
->where('type', StockType::CLAIMED->value)
|
||||
|
|
@ -316,6 +362,32 @@ trait HasStocks
|
|||
->sum('quantity'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get future claimed stock starting from a specific date or all where claimed_at is future
|
||||
*
|
||||
* @param \DateTimeInterface|null $from Optional start date to filter claims
|
||||
* @return int Total future claimed quantity (always positive)
|
||||
*/
|
||||
public function getFutureClaimedStock(?\DateTimeInterface $from = null): int
|
||||
{
|
||||
$query = $this->stocks()
|
||||
->where('type', StockType::CLAIMED->value)
|
||||
->where('status', StockStatus::PENDING->value)
|
||||
->willExpire();
|
||||
|
||||
if ($from) {
|
||||
$query->where('claimed_from', '>=', $from);
|
||||
} else {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNotNull('claimed_from')
|
||||
->where('claimed_from', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
return abs($query->sum('quantity'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a stock change to the audit log
|
||||
*
|
||||
|
|
@ -436,23 +508,6 @@ trait HasStocks
|
|||
return PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
|
||||
|
||||
// Get current available stock (includes all completed stocks minus all currently pending claims)
|
||||
$currentAvailable = $this->getAvailableStock();
|
||||
|
||||
// Get all currently pending claimed stocks (not date-filtered)
|
||||
$allClaimedStocks = $this->stocks()
|
||||
->where('type', StockType::CLAIMED->value)
|
||||
->where('status', StockStatus::PENDING->value)
|
||||
->sum('quantity');
|
||||
|
||||
// Get stocks claimed on this specific date
|
||||
$claimedOnDate = $stockModel::availableOnDate($date)
|
||||
->where('product_id', $this->id)
|
||||
->sum('quantity');
|
||||
|
||||
// Available on date = current available + all claims - claims active on date
|
||||
return max(0, $currentAvailable + abs($allClaimedStocks) - abs($claimedOnDate));
|
||||
return $this->getAvailableStock($date);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -493,7 +493,7 @@ class ProductStockTest extends TestCase
|
|||
$product->claimStock(quantity: 15);
|
||||
|
||||
// Should return total claimed
|
||||
$this->assertEquals(40, $product->getClaimedStock());
|
||||
$this->assertEquals(40, $product->getCurrentlyClaimedStock());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -714,7 +714,7 @@ class ProductStockTest extends TestCase
|
|||
$product = Product::factory()->withStocks(100)->create();
|
||||
|
||||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
$this->assertEquals(0, $product->getClaimedStock());
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Claim stock using adjustStock with CLAIMED type
|
||||
// Note: adjustStock(CLAIMED) now delegates to claimStock() for consistency
|
||||
|
|
@ -728,7 +728,7 @@ class ProductStockTest extends TestCase
|
|||
$this->assertEquals(75, $product->getAvailableStock());
|
||||
|
||||
// Claimed stock shows the pending claim (always positive now)
|
||||
$this->assertEquals(25, $product->getClaimedStock());
|
||||
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
|
||||
}
|
||||
/** @test */
|
||||
public function adjust_stock_with_until_parameter_expires_correctly()
|
||||
|
|
@ -766,8 +766,8 @@ class ProductStockTest extends TestCase
|
|||
until: now()->addDays(10)
|
||||
);
|
||||
|
||||
// Current available stock is reduced by the DECREASE entry
|
||||
$this->assertEquals(70, $product->getAvailableStock());
|
||||
// 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));
|
||||
|
|
@ -804,7 +804,7 @@ class ProductStockTest extends TestCase
|
|||
$this->assertEquals(130, $product->getAvailableStock());
|
||||
|
||||
// Total claimed stock (always positive)
|
||||
$this->assertEquals(70, $product->getClaimedStock());
|
||||
$this->assertEquals(70, $product->getCurrentlyClaimedStock());
|
||||
}
|
||||
/** @test */
|
||||
public function adjust_stock_claimed_with_completed_status_does_not_count_as_claimed()
|
||||
|
|
@ -824,7 +824,7 @@ class ProductStockTest extends TestCase
|
|||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
|
||||
// Should NOT count as claimed stock (only PENDING claims count)
|
||||
$this->assertEquals(0, $product->getClaimedStock());
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -842,22 +842,22 @@ class ProductStockTest extends TestCase
|
|||
quantity: 30
|
||||
);
|
||||
$this->assertEquals(70, $product->getAvailableStock());
|
||||
$this->assertEquals(30, $product->getClaimedStock());
|
||||
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Decrease 20 (regular decrease with COMPLETED status)
|
||||
$product->adjustStock(type: StockType::DECREASE, quantity: 20);
|
||||
$this->assertEquals(50, $product->getAvailableStock());
|
||||
$this->assertEquals(30, $product->getClaimedStock());
|
||||
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Return 10 (adds back to stock)
|
||||
$product->adjustStock(type: StockType::RETURN, quantity: 10);
|
||||
$this->assertEquals(60, $product->getAvailableStock());
|
||||
$this->assertEquals(30, $product->getClaimedStock());
|
||||
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Increase 25
|
||||
$product->adjustStock(type: StockType::INCREASE, quantity: 25);
|
||||
$this->assertEquals(85, $product->getAvailableStock());
|
||||
$this->assertEquals(30, $product->getClaimedStock());
|
||||
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
|
||||
}
|
||||
/** @test */
|
||||
public function adjust_stock_claimed_without_from_is_immediately_active()
|
||||
|
|
@ -920,6 +920,11 @@ class ProductStockTest extends TestCase
|
|||
until: now()->addDays(15)
|
||||
);
|
||||
|
||||
$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,
|
||||
|
|
@ -928,6 +933,11 @@ class ProductStockTest extends TestCase
|
|||
until: now()->addDays(20)
|
||||
);
|
||||
|
||||
$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)));
|
||||
|
||||
|
|
@ -943,11 +953,22 @@ class ProductStockTest extends TestCase
|
|||
// 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());
|
||||
// Current available stock (future claims do not reduce until active)
|
||||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
|
||||
// Total claimed stock
|
||||
$this->assertEquals(50, $product->getClaimedStock());
|
||||
$this->travel(6)->days();
|
||||
|
||||
$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());
|
||||
}
|
||||
/** @test */
|
||||
public function adjust_stock_with_note_and_reference_tracks_correctly()
|
||||
|
|
@ -966,7 +987,7 @@ class ProductStockTest extends TestCase
|
|||
// Available stock is reduced
|
||||
$this->assertEquals(75, $product->getAvailableStock());
|
||||
// Claimed stock shows positive value
|
||||
$this->assertEquals(25, $product->getClaimedStock());
|
||||
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Verify note and reference are stored
|
||||
$claim = $product->stocks()->where('type', StockType::CLAIMED->value)->first();
|
||||
|
|
@ -992,7 +1013,7 @@ class ProductStockTest extends TestCase
|
|||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
|
||||
// Claimed stock does NOT show expired claims (automatically excluded)
|
||||
$this->assertEquals(0, $product->getClaimedStock());
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Active claims (not expired) should be empty
|
||||
$activeClaims = $product->claims()->get();
|
||||
|
|
@ -1011,7 +1032,7 @@ class ProductStockTest extends TestCase
|
|||
);
|
||||
|
||||
$this->assertEquals(60, $product->getAvailableStock());
|
||||
$this->assertEquals(40, $product->getClaimedStock());
|
||||
$this->assertEquals(40, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Find and release the claim
|
||||
$claim = $product->stocks()
|
||||
|
|
@ -1022,7 +1043,7 @@ class ProductStockTest extends TestCase
|
|||
$claim->release();
|
||||
|
||||
// Claimed stock should drop to 0
|
||||
$this->assertEquals(0, $product->getClaimedStock());
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Available stock is restored when claim is released
|
||||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
|
|
@ -1069,7 +1090,7 @@ class ProductStockTest extends TestCase
|
|||
|
||||
// Initial state
|
||||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
$this->assertEquals(0, $product->getClaimedStock());
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Claim 30 units
|
||||
$product->claimStock(quantity: 30, until: now()->addDays(5));
|
||||
|
|
@ -1078,7 +1099,7 @@ class ProductStockTest extends TestCase
|
|||
$this->assertEquals(70, $product->getAvailableStock());
|
||||
|
||||
// Claimed stock should show the claim
|
||||
$this->assertEquals(30, $product->getClaimedStock());
|
||||
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Claim another 20 units
|
||||
$product->claimStock(quantity: 20, until: now()->addDays(3));
|
||||
|
|
@ -1087,7 +1108,7 @@ class ProductStockTest extends TestCase
|
|||
$this->assertEquals(50, $product->getAvailableStock());
|
||||
|
||||
// Claimed stock should show both claims
|
||||
$this->assertEquals(50, $product->getClaimedStock());
|
||||
$this->assertEquals(50, $product->getCurrentlyClaimedStock());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -1100,13 +1121,13 @@ class ProductStockTest extends TestCase
|
|||
|
||||
// During claim period: available reduced, claimed shows 30
|
||||
$this->assertEquals(70, $product->getAvailableStock());
|
||||
$this->assertEquals(30, $product->getClaimedStock());
|
||||
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Travel to day 3 (still within claim period)
|
||||
$this->travel(3)->days();
|
||||
|
||||
$this->assertEquals(70, $product->getAvailableStock());
|
||||
$this->assertEquals(30, $product->getClaimedStock());
|
||||
$this->assertEquals(30, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Travel to day 6 (after expiration)
|
||||
$this->travel(3)->days();
|
||||
|
|
@ -1115,7 +1136,7 @@ class ProductStockTest extends TestCase
|
|||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
|
||||
// Claimed stock should be 0 (expired claims excluded)
|
||||
$this->assertEquals(0, $product->getClaimedStock());
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// No manual release() was called!
|
||||
}
|
||||
|
|
@ -1133,19 +1154,19 @@ class ProductStockTest extends TestCase
|
|||
|
||||
// Initial state: both claims active
|
||||
$this->assertEquals(50, $product->getAvailableStock()); // 100 - 20 - 30
|
||||
$this->assertEquals(50, $product->getClaimedStock()); // 20 + 30
|
||||
$this->assertEquals(50, $product->getCurrentlyClaimedStock()); // 20 + 30
|
||||
|
||||
// Travel to day 4 (first claim expired, second still active)
|
||||
$this->travel(4)->days();
|
||||
|
||||
$this->assertEquals(70, $product->getAvailableStock()); // 100 - 30 (only second claim)
|
||||
$this->assertEquals(30, $product->getClaimedStock()); // Only second claim
|
||||
$this->assertEquals(30, $product->getCurrentlyClaimedStock()); // Only second claim
|
||||
|
||||
// Travel to day 8 (both claims expired)
|
||||
$this->travel(4)->days();
|
||||
|
||||
$this->assertEquals(100, $product->getAvailableStock()); // All restored
|
||||
$this->assertEquals(0, $product->getClaimedStock()); // No claims
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock()); // No claims
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -1158,14 +1179,14 @@ class ProductStockTest extends TestCase
|
|||
|
||||
// Available stock reduced, claimed shows 25
|
||||
$this->assertEquals(75, $product->getAvailableStock());
|
||||
$this->assertEquals(25, $product->getClaimedStock());
|
||||
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Travel far into the future
|
||||
$this->travel(100)->days();
|
||||
|
||||
// Permanent claim never expires
|
||||
$this->assertEquals(75, $product->getAvailableStock());
|
||||
$this->assertEquals(25, $product->getClaimedStock());
|
||||
$this->assertEquals(25, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// Must manually release permanent claims
|
||||
$claim = $product->stocks()
|
||||
|
|
@ -1177,7 +1198,7 @@ class ProductStockTest extends TestCase
|
|||
|
||||
// Now stock is restored
|
||||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
$this->assertEquals(0, $product->getClaimedStock());
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -1194,14 +1215,14 @@ class ProductStockTest extends TestCase
|
|||
|
||||
// During claim period
|
||||
$this->assertEquals(60, $product->getAvailableStock());
|
||||
$this->assertEquals(40, $product->getClaimedStock());
|
||||
$this->assertEquals(40, $product->getCurrentlyClaimedStock());
|
||||
|
||||
// After expiration
|
||||
$this->travel(6)->days();
|
||||
|
||||
// Stock automatically restored
|
||||
$this->assertEquals(100, $product->getAvailableStock());
|
||||
$this->assertEquals(0, $product->getClaimedStock());
|
||||
$this->assertEquals(0, $product->getCurrentlyClaimedStock());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -1248,4 +1269,308 @@ class ProductStockTest extends TestCase
|
|||
|
||||
$this->assertEquals(35, $allClaimed);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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
|
||||
}
|
||||
|
||||
/** @test */
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue