From 2f3c0dc61f6a0e62895fd4e0bc6b2cda73248eb0 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Sun, 28 Dec 2025 12:05:05 +0100 Subject: [PATCH] IA stock attributes --- src/Traits/HasStocks.php | 24 ++ tests/Feature/StockAttributesTest.php | 322 ++++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 tests/Feature/StockAttributesTest.php diff --git a/src/Traits/HasStocks.php b/src/Traits/HasStocks.php index 69e4717..b7e6969 100644 --- a/src/Traits/HasStocks.php +++ b/src/Traits/HasStocks.php @@ -72,6 +72,30 @@ trait HasStocks ->sum('quantity') ?? 0; } + /** + * Get max stock (the ceiling - total capacity as if no claims existed) + * + * This shows the maximum possible stock by summing: + * - INCREASE entries (stock added) + * - RETURN entries (stock returned) + * And ignoring DECREASE and CLAIMED entries entirely. + * + * @return int Maximum capacity (PHP_INT_MAX if stock management disabled) + */ + public function getMaxStocksAttribute(): int + { + if ($this->manage_stock === false) { + return PHP_INT_MAX; + } + + // Sum only INCREASE and RETURN entries to get the "ceiling" + return (int) $this->stocks() + ->withoutGlobalScope('willExpire') + ->where('status', StockStatus::COMPLETED->value) + ->whereIn('type', [StockType::INCREASE->value, StockType::RETURN->value]) + ->sum('quantity'); + } + /** * Check if product is in stock * diff --git a/tests/Feature/StockAttributesTest.php b/tests/Feature/StockAttributesTest.php new file mode 100644 index 0000000..c1a4192 --- /dev/null +++ b/tests/Feature/StockAttributesTest.php @@ -0,0 +1,322 @@ +create(['manage_stock' => true]); + + // Add some stock increases + $product->increaseStock(50); + $product->increaseStock(30); + + $this->assertEquals(80, $product->available_stocks); + } + + #[Test] + public function available_stocks_accounts_for_decreases() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + $product->decreaseStock(25); + + $this->assertEquals(75, $product->available_stocks); + } + + #[Test] + public function available_stocks_returns_zero_when_no_stock_entries() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $this->assertEquals(0, $product->available_stocks); + } + + #[Test] + public function available_stocks_reduced_by_claims() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + + // Create a claim - this reduces available stock + $product->claimStock( + quantity: 20, + reference: null, + from: now(), + until: now()->addDays(5) + ); + + // available_stocks should show 80 (100 - 20 claimed) + $this->assertEquals(80, $product->available_stocks); + } + + #[Test] + public function available_stocks_excludes_expired_stock_entries() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + + // Create a decrease that has already expired (should not count) + $product->stocks()->create([ + 'quantity' => -30, + 'type' => StockType::DECREASE, + 'status' => StockStatus::COMPLETED, + 'expires_at' => now()->subDay(), // Expired yesterday + ]); + + // The expired decrease should not be counted + $this->assertEquals(100, $product->available_stocks); + } + + #[Test] + public function available_stocks_includes_non_expired_stock_entries() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + + // Create a decrease that will expire in the future (should count) + $product->stocks()->create([ + 'quantity' => -30, + 'type' => StockType::DECREASE, + 'status' => StockStatus::COMPLETED, + 'expires_at' => now()->addDays(5), + ]); + + $this->assertEquals(70, $product->available_stocks); + } + + #[Test] + public function available_stocks_handles_multiple_increases_and_decreases() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + $product->increaseStock(50); + $product->decreaseStock(20); + $product->increaseStock(30); + $product->decreaseStock(10); + + // 100 + 50 - 20 + 30 - 10 = 150 + $this->assertEquals(150, $product->available_stocks); + } + + #[Test] + public function available_stocks_excludes_pending_status_entries() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + + // Create a PENDING entry (should not be counted in available_stocks) + $product->stocks()->create([ + 'quantity' => 50, + 'type' => StockType::INCREASE, + 'status' => StockStatus::PENDING, + ]); + + // Only the COMPLETED entry should count + $this->assertEquals(100, $product->available_stocks); + } + + #[Test] + public function available_stocks_includes_return_type_entries() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + $product->decreaseStock(30); + + // Create a return entry + $product->stocks()->create([ + 'quantity' => 15, + 'type' => StockType::RETURN, + 'status' => StockStatus::COMPLETED, + ]); + + // 100 - 30 + 15 = 85 + $this->assertEquals(85, $product->available_stocks); + } + + // ======================================== + // getMaxStocksAttribute Tests + // ======================================== + + #[Test] + public function max_stocks_returns_php_int_max_when_stock_management_disabled() + { + $product = Product::factory()->create(['manage_stock' => false]); + + $this->assertEquals(PHP_INT_MAX, $product->max_stocks); + } + + #[Test] + public function max_stocks_shows_total_capacity_ignoring_claims() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + + // Create a claim - this should NOT reduce max_stocks + $product->claimStock( + quantity: 30, + reference: null, + from: now(), + until: now()->addDays(5) + ); + + // max_stocks should still show 100 (as if no claims existed) + $this->assertEquals(100, $product->max_stocks); + } + + #[Test] + public function max_stocks_returns_zero_when_no_stock_entries() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $this->assertEquals(0, $product->max_stocks); + } + + #[Test] + public function max_stocks_sums_only_increases_and_returns() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + $product->increaseStock(50); + $product->decreaseStock(20); // This is ignored by max_stocks + + // max_stocks ignores DECREASE, so 100 + 50 = 150 + $this->assertEquals(150, $product->max_stocks); + } + + #[Test] + public function max_stocks_with_multiple_claims_still_shows_full_capacity() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(200); + + // Create multiple claims - these should NOT reduce max_stocks + $product->claimStock(20, null, now(), now()->addDays(3)); + $product->claimStock(30, null, now()->addDays(5), now()->addDays(10)); + $product->claimStock(10, null, now()->addDays(1), now()->addDays(2)); + + // max_stocks should still be 200 (as if no claims existed) + $this->assertEquals(200, $product->max_stocks); + } + + #[Test] + public function max_stocks_includes_return_type_entries() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + $product->decreaseStock(50); // Ignored by max_stocks + + $product->stocks()->create([ + 'quantity' => 25, + 'type' => StockType::RETURN, + 'status' => StockStatus::COMPLETED, + ]); + + // max_stocks = INCREASE + RETURN, ignoring DECREASE + // 100 + 25 = 125 + $this->assertEquals(125, $product->max_stocks); + } + + // ======================================== + // Comparison Tests (available_stocks vs max_stocks) + // ======================================== + + #[Test] + public function available_stocks_less_than_max_stocks_when_decreases_exist() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + $product->decreaseStock(20); + + // available_stocks accounts for DECREASE + $this->assertEquals(80, $product->available_stocks); + + // max_stocks ignores DECREASE (shows ceiling/capacity) + $this->assertEquals(100, $product->max_stocks); + } + + #[Test] + public function available_stocks_less_than_max_stocks_when_claims_exist() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + + // Claim 30 units + $product->claimStock(30, null, now(), now()->addDays(5)); + + // available_stocks should be reduced by claims + $this->assertEquals(70, $product->available_stocks); + + // max_stocks should show full capacity as if no claims + $this->assertEquals(100, $product->max_stocks); + + // The difference is the claimed amount + $this->assertEquals(30, $product->max_stocks - $product->available_stocks); + } + + #[Test] + public function available_stocks_restored_when_claims_released() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(100); + + // max_stocks starts at 100 + $this->assertEquals(100, $product->max_stocks); + + // Create a claim + $claim = $product->claimStock( + quantity: 25, + reference: null, + from: now(), + until: now()->addDays(5) + ); + + // available_stocks is reduced by the claim + $this->assertEquals(75, $product->available_stocks); + // max_stocks stays at 100 (ignores claims) + $this->assertEquals(100, $product->max_stocks); + + // Release the claim - this creates a RETURN entry + if ($claim) { + $claim->release(); + } + $product->refresh(); + + // After release: + // - available_stocks is restored (the DECREASE from claim is offset by RETURN) + $this->assertEquals(100, $product->available_stocks); + // - max_stocks increases by the RETURN amount (100 + 25 = 125) + // This is expected because RETURN entries are counted as capacity additions + $this->assertEquals(125, $product->max_stocks); + } +}