A cart->calendarAvailability, product->has_more
This commit is contained in:
parent
fc3ae3e756
commit
37b3e6bdc0
|
|
@ -1284,6 +1284,201 @@ class Cart extends Model
|
||||||
return $item ?? true;
|
return $item ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar availability for all items in the cart.
|
||||||
|
*
|
||||||
|
* This method aggregates availability across all cart items and returns
|
||||||
|
* the minimum availability for each date. This is useful for booking systems
|
||||||
|
* where you need to know when ALL items in a cart can be booked together.
|
||||||
|
*
|
||||||
|
* For each date, it calculates the minimum number of complete cart "sets"
|
||||||
|
* that could be fulfilled. A set is fulfilled when all items have at least
|
||||||
|
* one unit available.
|
||||||
|
*
|
||||||
|
* Returns associative array with keys:
|
||||||
|
* - 'max_available' => Shows the peak available "sets" in the date range
|
||||||
|
* - 'min_available' => Shows the lowest available "sets" in the date range
|
||||||
|
* - 'dates' => An array of dates with their respective min/max availability
|
||||||
|
* - 'items' => Individual item availability data (for debugging)
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface|null $from Start date of the range (optional, defaults to today)
|
||||||
|
* @param \DateTimeInterface|null $until End date of the range (optional, defaults to 30 days)
|
||||||
|
* @return array Associative array with 'max_available', 'min_available', 'dates', and 'items'
|
||||||
|
*/
|
||||||
|
public function calendarAvailability(
|
||||||
|
?\DateTimeInterface $from = null,
|
||||||
|
?\DateTimeInterface $until = null
|
||||||
|
): array {
|
||||||
|
$fromDate = Carbon::parse($from ?? now())->startOfDay();
|
||||||
|
$untilDate = Carbon::parse($until ?? $fromDate->copy()->addDays(30))->endOfDay();
|
||||||
|
|
||||||
|
// Load items with their purchasable products
|
||||||
|
if (!$this->relationLoaded('items')) {
|
||||||
|
$this->load('items.purchasable');
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $this->items;
|
||||||
|
|
||||||
|
if ($items->isEmpty()) {
|
||||||
|
return [
|
||||||
|
'max_available' => PHP_INT_MAX,
|
||||||
|
'min_available' => PHP_INT_MAX,
|
||||||
|
'dates' => [],
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect availability data for each unique product in the cart
|
||||||
|
$productAvailabilities = [];
|
||||||
|
$itemDetails = [];
|
||||||
|
|
||||||
|
// Group items by product to handle multiple quantities of the same product
|
||||||
|
$productQuantities = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$product = $item->purchasable;
|
||||||
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$productKey = get_class($product) . '|' . $product->id;
|
||||||
|
if (!isset($productQuantities[$productKey])) {
|
||||||
|
$productQuantities[$productKey] = [
|
||||||
|
'product' => $product,
|
||||||
|
'quantity' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$productQuantities[$productKey]['quantity'] += $item->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get calendar availability for each unique product
|
||||||
|
foreach ($productQuantities as $productKey => $data) {
|
||||||
|
$product = $data['product'];
|
||||||
|
$requiredQuantity = $data['quantity'];
|
||||||
|
|
||||||
|
// Check if product has the calendarAvailability method (uses HasStocks trait)
|
||||||
|
if (method_exists($product, 'calendarAvailability')) {
|
||||||
|
$availability = $product->calendarAvailability($from, $until);
|
||||||
|
$productAvailabilities[$productKey] = [
|
||||||
|
'availability' => $availability,
|
||||||
|
'required_quantity' => $requiredQuantity,
|
||||||
|
];
|
||||||
|
$itemDetails[$productKey] = [
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'product_name' => $product->name ?? 'Unknown',
|
||||||
|
'required_quantity' => $requiredQuantity,
|
||||||
|
'availability' => $availability,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Product doesn't have stock management - treat as unlimited
|
||||||
|
$productAvailabilities[$productKey] = [
|
||||||
|
'availability' => [
|
||||||
|
'max_available' => PHP_INT_MAX,
|
||||||
|
'min_available' => PHP_INT_MAX,
|
||||||
|
'dates' => [],
|
||||||
|
],
|
||||||
|
'required_quantity' => $requiredQuantity,
|
||||||
|
];
|
||||||
|
$itemDetails[$productKey] = [
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'product_name' => $product->name ?? 'Unknown',
|
||||||
|
'required_quantity' => $requiredQuantity,
|
||||||
|
'availability' => [
|
||||||
|
'max_available' => PHP_INT_MAX,
|
||||||
|
'min_available' => PHP_INT_MAX,
|
||||||
|
'dates' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no products have availability data, return unlimited
|
||||||
|
if (empty($productAvailabilities)) {
|
||||||
|
return [
|
||||||
|
'max_available' => PHP_INT_MAX,
|
||||||
|
'min_available' => PHP_INT_MAX,
|
||||||
|
'dates' => [],
|
||||||
|
'items' => $itemDetails,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the combined calendar
|
||||||
|
$dates = [];
|
||||||
|
$globalMin = PHP_INT_MAX;
|
||||||
|
$globalMax = PHP_INT_MIN;
|
||||||
|
|
||||||
|
$currentDate = $fromDate->copy();
|
||||||
|
while ($currentDate->lte($untilDate)) {
|
||||||
|
$dateKey = $currentDate->toDateString();
|
||||||
|
$dayMin = PHP_INT_MAX;
|
||||||
|
$dayMax = PHP_INT_MAX;
|
||||||
|
|
||||||
|
foreach ($productAvailabilities as $productKey => $data) {
|
||||||
|
$availability = $data['availability'];
|
||||||
|
$requiredQuantity = $data['required_quantity'];
|
||||||
|
|
||||||
|
// Get the availability for this date
|
||||||
|
if (isset($availability['dates'][$dateKey])) {
|
||||||
|
$productDayData = $availability['dates'][$dateKey];
|
||||||
|
$productDayMin = $productDayData['min'] ?? 0;
|
||||||
|
$productDayMax = $productDayData['max'] ?? 0;
|
||||||
|
} else {
|
||||||
|
// No specific date data - use overall availability
|
||||||
|
$productDayMin = $availability['min_available'] ?? 0;
|
||||||
|
$productDayMax = $availability['max_available'] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate how many "sets" of the required quantity are available
|
||||||
|
if ($productDayMin === PHP_INT_MAX) {
|
||||||
|
$setsMin = PHP_INT_MAX;
|
||||||
|
} else {
|
||||||
|
$setsMin = $requiredQuantity > 0 ? intdiv($productDayMin, $requiredQuantity) : PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($productDayMax === PHP_INT_MAX) {
|
||||||
|
$setsMax = PHP_INT_MAX;
|
||||||
|
} else {
|
||||||
|
$setsMax = $requiredQuantity > 0 ? intdiv($productDayMax, $requiredQuantity) : PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cart availability is limited by the product with the least availability
|
||||||
|
$dayMin = min($dayMin, $setsMin);
|
||||||
|
$dayMax = min($dayMax, $setsMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle PHP_INT_MAX edge case
|
||||||
|
if ($dayMin === PHP_INT_MAX) {
|
||||||
|
$dayMin = PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
if ($dayMax === PHP_INT_MAX) {
|
||||||
|
$dayMax = PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dates[$dateKey] = [
|
||||||
|
'min' => $dayMin,
|
||||||
|
'max' => $dayMax,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($dayMin !== PHP_INT_MAX) {
|
||||||
|
$globalMin = min($globalMin, $dayMin);
|
||||||
|
}
|
||||||
|
if ($dayMax !== PHP_INT_MAX && $dayMax !== PHP_INT_MIN) {
|
||||||
|
$globalMax = max($globalMax, $dayMax);
|
||||||
|
} elseif ($dayMax === PHP_INT_MAX && $globalMax === PHP_INT_MIN) {
|
||||||
|
// All products have unlimited availability
|
||||||
|
$globalMax = PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentDate->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'max_available' => $globalMax === PHP_INT_MIN ? 0 : $globalMax,
|
||||||
|
'min_available' => $globalMin === PHP_INT_MAX ? PHP_INT_MAX : $globalMin,
|
||||||
|
'dates' => $dates,
|
||||||
|
'items' => $itemDetails,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate cart for checkout without converting it
|
* Validate cart for checkout without converting it
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -178,8 +178,8 @@ trait HasStocks
|
||||||
public function adjustStock(
|
public function adjustStock(
|
||||||
StockType $type,
|
StockType $type,
|
||||||
int $quantity,
|
int $quantity,
|
||||||
\DateTimeInterface|null $until = null,
|
DateTimeInterface|null $until = null,
|
||||||
\DateTimeInterface|null $from = null,
|
DateTimeInterface|null $from = null,
|
||||||
?StockStatus $status = null,
|
?StockStatus $status = null,
|
||||||
string|null $note = null,
|
string|null $note = null,
|
||||||
Model|null $referencable = null
|
Model|null $referencable = null
|
||||||
|
|
@ -252,8 +252,8 @@ trait HasStocks
|
||||||
public function claimStock(
|
public function claimStock(
|
||||||
int $quantity,
|
int $quantity,
|
||||||
$reference = null,
|
$reference = null,
|
||||||
?\DateTimeInterface $from = null,
|
?DateTimeInterface $from = null,
|
||||||
?\DateTimeInterface $until = null,
|
?DateTimeInterface $until = null,
|
||||||
?string $note = null
|
?string $note = null
|
||||||
): ?\Blax\Shop\Models\ProductStock {
|
): ?\Blax\Shop\Models\ProductStock {
|
||||||
|
|
||||||
|
|
@ -285,7 +285,7 @@ trait HasStocks
|
||||||
*
|
*
|
||||||
* @return int Available quantity (PHP_INT_MAX if stock management disabled)
|
* @return int Available quantity (PHP_INT_MAX if stock management disabled)
|
||||||
*/
|
*/
|
||||||
public function getAvailableStock(?\DateTimeInterface $date = null): int
|
public function getAvailableStock(?DateTimeInterface $date = null): int
|
||||||
{
|
{
|
||||||
if (!$this->manage_stock) {
|
if (!$this->manage_stock) {
|
||||||
return PHP_INT_MAX;
|
return PHP_INT_MAX;
|
||||||
|
|
@ -369,7 +369,7 @@ trait HasStocks
|
||||||
* @param \DateTimeInterface|null $from Optional start date to filter claims
|
* @param \DateTimeInterface|null $from Optional start date to filter claims
|
||||||
* @return int Total future claimed quantity (always positive)
|
* @return int Total future claimed quantity (always positive)
|
||||||
*/
|
*/
|
||||||
public function getFutureClaimedStock(?\DateTimeInterface $from = null): int
|
public function getFutureClaimedStock(?DateTimeInterface $from = null): int
|
||||||
{
|
{
|
||||||
$query = $this->stocks()
|
$query = $this->stocks()
|
||||||
->where('type', StockType::CLAIMED->value)
|
->where('type', StockType::CLAIMED->value)
|
||||||
|
|
@ -503,7 +503,7 @@ trait HasStocks
|
||||||
* @param \DateTimeInterface $date The date to check availability for
|
* @param \DateTimeInterface $date The date to check availability for
|
||||||
* @return int Available stock on that date (PHP_INT_MAX if stock management disabled)
|
* @return int Available stock on that date (PHP_INT_MAX if stock management disabled)
|
||||||
*/
|
*/
|
||||||
public function availableOnDate(\DateTimeInterface $date): int
|
public function availableOnDate(DateTimeInterface $date): int
|
||||||
{
|
{
|
||||||
if (!$this->manage_stock) {
|
if (!$this->manage_stock) {
|
||||||
return PHP_INT_MAX;
|
return PHP_INT_MAX;
|
||||||
|
|
@ -849,4 +849,43 @@ trait HasStocks
|
||||||
|
|
||||||
return $aggregated;
|
return $aggregated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accounts the current cart, from/until and also for pool products
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getHasMoreAttribute(): int
|
||||||
|
{
|
||||||
|
if (method_exists($this, 'isPool') && $this->isPool()) {
|
||||||
|
// For pool products, check availability across all single items
|
||||||
|
if (!$this->relationLoaded('singleProducts')) {
|
||||||
|
$this->load('singleProducts');
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalAvailable = 0;
|
||||||
|
foreach ($this->singleProducts as $single) {
|
||||||
|
$singleAvailable = $single->getHasMoreAttribute();
|
||||||
|
|
||||||
|
// If any single has unlimited availability, the pool effectively has unlimited
|
||||||
|
if ($singleAvailable === PHP_INT_MAX) {
|
||||||
|
return PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalAvailable += $singleAvailable;
|
||||||
|
|
||||||
|
// Prevent overflow - cap at PHP_INT_MAX
|
||||||
|
if ($totalAvailable >= PHP_INT_MAX || $totalAvailable < 0) {
|
||||||
|
return PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $totalAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->manage_stock === false) {
|
||||||
|
return PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getAvailableStock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,512 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
use Blax\Shop\Models\Cart;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Workbench\App\Models\User;
|
||||||
|
|
||||||
|
class CartCalendarAvailabilityTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function createUserWithCart(): array
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'id' => \Illuminate\Support\Str::uuid(),
|
||||||
|
'name' => 'Test User',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cart = Cart::factory()->forCustomer($user)->create();
|
||||||
|
|
||||||
|
return [$user, $cart];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_unlimited_availability_for_empty_cart()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(PHP_INT_MAX, $availability['max_available']);
|
||||||
|
$this->assertEquals(PHP_INT_MAX, $availability['min_available']);
|
||||||
|
$this->assertEmpty($availability['dates']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_availability_for_single_product_in_cart()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->withStocks(50)->withPrices(1, 1000)->create();
|
||||||
|
$cart->addToCart($product, 1);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(50, $availability['max_available']);
|
||||||
|
$this->assertEquals(50, $availability['min_available']);
|
||||||
|
$this->assertCount(31, $availability['dates']);
|
||||||
|
|
||||||
|
// All dates should have 50 available
|
||||||
|
foreach ($availability['dates'] as $dateKey => $dayData) {
|
||||||
|
$this->assertEquals(['min' => 50, 'max' => 50], $dayData, "Failed for date: $dateKey");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_minimum_availability_across_multiple_products()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product1 = Product::factory()->withStocks(100)->withPrices(1, 1000)->create();
|
||||||
|
$product2 = Product::factory()->withStocks(30)->withPrices(1, 500)->create();
|
||||||
|
|
||||||
|
$cart->addToCart($product1, 1);
|
||||||
|
$cart->addToCart($product2, 1);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// The cart availability should be limited by the product with less stock
|
||||||
|
$this->assertEquals(30, $availability['max_available']);
|
||||||
|
$this->assertEquals(30, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_considers_required_quantity_when_calculating_sets()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Product with 10 units in stock
|
||||||
|
$product = Product::factory()->withStocks(10)->withPrices(1, 1000)->create();
|
||||||
|
|
||||||
|
// Add 3 of this product to cart
|
||||||
|
$cart->addToCart($product, 3);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// With 10 in stock and 3 required per cart, we can fulfill 3 complete sets (10 / 3 = 3)
|
||||||
|
$this->assertEquals(3, $availability['max_available']);
|
||||||
|
$this->assertEquals(3, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_availability_for_cart_with_booking_products()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$product->increaseStock(5);
|
||||||
|
|
||||||
|
// Create a price for the product
|
||||||
|
$product->prices()->create([
|
||||||
|
'unit_amount' => 10000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cart->addToCart($product, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(5, $availability['max_available']);
|
||||||
|
$this->assertEquals(5, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_reduced_availability_when_stock_is_claimed()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$product->increaseStock(10);
|
||||||
|
|
||||||
|
// Create a price for the product
|
||||||
|
$product->prices()->create([
|
||||||
|
'unit_amount' => 10000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Claim 3 units for days 5-10
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 3,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
$cart->addToCart($product, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// Before claim period (days 0-4): 10 available
|
||||||
|
$this->assertEquals(['min' => 10, 'max' => 10], $availability['dates'][now()->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 10, 'max' => 10], $availability['dates'][now()->addDays(4)->toDateString()]);
|
||||||
|
|
||||||
|
// During claim period (days 5-10): 7 available (10 - 3)
|
||||||
|
// Day 5: claim starts at startOfDay, so min=max=7 for the whole day
|
||||||
|
$this->assertEquals(['min' => 7, 'max' => 7], $availability['dates'][now()->addDays(5)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 7, 'max' => 7], $availability['dates'][now()->addDays(7)->toDateString()]);
|
||||||
|
|
||||||
|
// After claim period: 10 available
|
||||||
|
$this->assertEquals(['min' => 10, 'max' => 10], $availability['dates'][now()->addDays(15)->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_availability_for_cart_with_pool_products()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Hotel Rooms',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a price for the pool
|
||||||
|
$pool->prices()->create([
|
||||||
|
'unit_amount' => 15000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 3 single items with stock
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Room 10{$i}",
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single->increaseStock(1);
|
||||||
|
|
||||||
|
// Create a price for each single
|
||||||
|
$single->prices()->create([
|
||||||
|
'unit_amount' => 15000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$singles[] = $single;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($singles as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart->addToCart($pool, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(3, $availability['max_available']);
|
||||||
|
$this->assertEquals(3, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_reduced_pool_availability_with_claims()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Hotel Rooms',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a price for the pool
|
||||||
|
$pool->prices()->create([
|
||||||
|
'unit_amount' => 15000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 3 single items with stock
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Room 10{$i}",
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single->increaseStock(1);
|
||||||
|
|
||||||
|
$single->prices()->create([
|
||||||
|
'unit_amount' => 15000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$singles[] = $single;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($singles as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim single1 from day 5 to day 10
|
||||||
|
$singles[0]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
$cart->addToCart($pool, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// Before claim: 3 available
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->addDays(4)->toDateString()]);
|
||||||
|
|
||||||
|
// During claim: 2 available
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 2], $availability['dates'][now()->addDays(7)->toDateString()]);
|
||||||
|
|
||||||
|
// After claim: 3 available
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->addDays(15)->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_custom_date_range()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->withStocks(25)->withPrices(1, 1000)->create();
|
||||||
|
$cart->addToCart($product, 1);
|
||||||
|
|
||||||
|
$from = now()->addDays(10);
|
||||||
|
$until = now()->addDays(20);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability($from, $until);
|
||||||
|
|
||||||
|
$this->assertCount(11, $availability['dates']); // 10 to 20 inclusive
|
||||||
|
$this->assertEquals(25, $availability['max_available']);
|
||||||
|
$this->assertEquals(25, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_minimum_sets_across_multiple_products_with_different_quantities()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Product 1: 20 in stock, need 4 = 5 sets available
|
||||||
|
$product1 = Product::factory()->withStocks(20)->withPrices(1, 1000)->create();
|
||||||
|
|
||||||
|
// Product 2: 15 in stock, need 5 = 3 sets available
|
||||||
|
$product2 = Product::factory()->withStocks(15)->withPrices(1, 500)->create();
|
||||||
|
|
||||||
|
$cart->addToCart($product1, 4);
|
||||||
|
$cart->addToCart($product2, 5);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// Cart can only be fulfilled 3 times (limited by product2: 15/5 = 3)
|
||||||
|
$this->assertEquals(3, $availability['max_available']);
|
||||||
|
$this->assertEquals(3, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_products_without_stock_management()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Product without stock management (unlimited)
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
$product->prices()->create([
|
||||||
|
'unit_amount' => 1000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cart->addToCart($product, 1);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// Unlimited availability
|
||||||
|
$this->assertEquals(PHP_INT_MAX, $availability['max_available']);
|
||||||
|
$this->assertEquals(PHP_INT_MAX, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_combines_limited_and_unlimited_products()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Limited product
|
||||||
|
$limitedProduct = Product::factory()->withStocks(10)->withPrices(1, 1000)->create();
|
||||||
|
|
||||||
|
// Unlimited product
|
||||||
|
$unlimitedProduct = Product::factory()->create([
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
$unlimitedProduct->prices()->create([
|
||||||
|
'unit_amount' => 500,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cart->addToCart($limitedProduct, 2);
|
||||||
|
$cart->addToCart($unlimitedProduct, 1);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// Limited by the limited product: 10 / 2 = 5 sets
|
||||||
|
$this->assertEquals(5, $availability['max_available']);
|
||||||
|
$this->assertEquals(5, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_item_details_for_debugging()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product1 = Product::factory()->withStocks(50)->withPrices(1, 1000)->create([
|
||||||
|
'name' => 'Product One',
|
||||||
|
]);
|
||||||
|
$product2 = Product::factory()->withStocks(30)->withPrices(1, 500)->create([
|
||||||
|
'name' => 'Product Two',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cart->addToCart($product1, 2);
|
||||||
|
$cart->addToCart($product2, 1);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('items', $availability);
|
||||||
|
$this->assertCount(2, $availability['items']);
|
||||||
|
|
||||||
|
// Verify item details are included
|
||||||
|
$itemKeys = array_keys($availability['items']);
|
||||||
|
foreach ($itemKeys as $key) {
|
||||||
|
$item = $availability['items'][$key];
|
||||||
|
$this->assertArrayHasKey('product_id', $item);
|
||||||
|
$this->assertArrayHasKey('product_name', $item);
|
||||||
|
$this->assertArrayHasKey('required_quantity', $item);
|
||||||
|
$this->assertArrayHasKey('availability', $item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_overlapping_claims_for_multiple_products()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Product 1: 100 stock, claim 30 on days 5-10
|
||||||
|
$product1 = Product::factory()->create([
|
||||||
|
'name' => 'Product 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$product1->increaseStock(100);
|
||||||
|
$product1->prices()->create([
|
||||||
|
'unit_amount' => 1000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$product1->claimStock(
|
||||||
|
quantity: 30,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Product 2: 50 stock, claim 20 on days 8-15
|
||||||
|
$product2 = Product::factory()->create([
|
||||||
|
'name' => 'Product 2',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$product2->increaseStock(50);
|
||||||
|
$product2->prices()->create([
|
||||||
|
'unit_amount' => 500,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$product2->claimStock(
|
||||||
|
quantity: 20,
|
||||||
|
from: now()->startOfDay()->addDays(8),
|
||||||
|
until: now()->endOfDay()->addDays(15)
|
||||||
|
);
|
||||||
|
|
||||||
|
$cart->addToCart($product1, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
$cart->addToCart($product2, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// Day 0-4: product1=100, product2=50 -> min(100, 50) = 50
|
||||||
|
$this->assertEquals(['min' => 50, 'max' => 50], $availability['dates'][now()->toDateString()]);
|
||||||
|
|
||||||
|
// Day 6-7: product1=70, product2=50 -> min(70, 50) = 50
|
||||||
|
$this->assertEquals(['min' => 50, 'max' => 50], $availability['dates'][now()->addDays(6)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 9: product1=70, product2=30 -> min(70, 30) = 30
|
||||||
|
$this->assertEquals(['min' => 30, 'max' => 30], $availability['dates'][now()->addDays(9)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 12: product1=100, product2=30 -> min(100, 30) = 30
|
||||||
|
$this->assertEquals(['min' => 30, 'max' => 30], $availability['dates'][now()->addDays(12)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 20: product1=100, product2=50 -> min(100, 50) = 50
|
||||||
|
$this->assertEquals(['min' => 50, 'max' => 50], $availability['dates'][now()->addDays(20)->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_same_product_added_multiple_times()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->withStocks(15)->withPrices(1, 1000)->create();
|
||||||
|
|
||||||
|
// Add the same product twice
|
||||||
|
$cart->addToCart($product, 2);
|
||||||
|
$cart->addToCart($product, 3);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
// Total required: 5, available: 15 -> 3 sets (15 / 5 = 3)
|
||||||
|
$this->assertEquals(3, $availability['max_available']);
|
||||||
|
$this->assertEquals(3, $availability['min_available']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_zero_when_no_stock_available()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
// No stock added
|
||||||
|
$product->prices()->create([
|
||||||
|
'unit_amount' => 1000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cart->addToCart($product, 1);
|
||||||
|
|
||||||
|
$availability = $cart->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $availability['max_available']);
|
||||||
|
$this->assertEquals(0, $availability['min_available']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
class GetHasMoreAttributeTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_php_int_max_when_stock_management_is_disabled()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(PHP_INT_MAX, $product->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_available_stock_for_simple_product()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->withStocks(50)->create();
|
||||||
|
|
||||||
|
$this->assertEquals(50, $product->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_zero_when_no_stock_available()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(0, $product->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_remaining_stock_after_claims()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->withStocks(100)->create();
|
||||||
|
|
||||||
|
// Claim 30 units
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 30,
|
||||||
|
from: now(),
|
||||||
|
until: now()->addDays(5)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(70, $product->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_available_stock_for_booking_product()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$product->increaseStock(10);
|
||||||
|
|
||||||
|
// Claim 3 units for a future period
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 3,
|
||||||
|
from: now()->addDays(5),
|
||||||
|
until: now()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Has more should reflect available stock at current time
|
||||||
|
$this->assertEquals(10, $product->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_aggregated_availability_for_pool_product()
|
||||||
|
{
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Hotel Rooms',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 3 single items with stock
|
||||||
|
$single1 = Product::factory()->create([
|
||||||
|
'name' => 'Room 101',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single1->increaseStock(1);
|
||||||
|
|
||||||
|
$single2 = Product::factory()->create([
|
||||||
|
'name' => 'Room 102',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single2->increaseStock(1);
|
||||||
|
|
||||||
|
$single3 = Product::factory()->create([
|
||||||
|
'name' => 'Room 103',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single3->increaseStock(1);
|
||||||
|
|
||||||
|
// Attach singles to pool
|
||||||
|
foreach ([$single1, $single2, $single3] as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool should aggregate availability from all singles: 1 + 1 + 1 = 3
|
||||||
|
$this->assertEquals(3, $pool->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_aggregated_availability_for_pool_with_claims()
|
||||||
|
{
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Hotel Rooms',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 3 single items with stock
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Room 10{$i}",
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single->increaseStock(1);
|
||||||
|
$singles[] = $single;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($singles as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim single1 from now until next week (active claim)
|
||||||
|
$singles[0]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now(),
|
||||||
|
until: now()->addDays(7)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pool should show 2 available (singles 2 and 3)
|
||||||
|
$this->assertEquals(2, $pool->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_aggregated_availability_for_pool_with_future_claims()
|
||||||
|
{
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Rental Cars',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 2 single items with stock
|
||||||
|
$single1 = Product::factory()->create([
|
||||||
|
'name' => 'Car 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single1->increaseStock(1);
|
||||||
|
|
||||||
|
$single2 = Product::factory()->create([
|
||||||
|
'name' => 'Car 2',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single2->increaseStock(1);
|
||||||
|
|
||||||
|
foreach ([$single1, $single2] as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim single1 for a FUTURE period (not yet active)
|
||||||
|
$single1->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->addDays(5),
|
||||||
|
until: now()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pool should show 2 available (both cars available NOW, claim starts in future)
|
||||||
|
$this->assertEquals(2, $pool->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_php_int_max_for_pool_with_unmanaged_singles()
|
||||||
|
{
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Digital Products Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create single items WITHOUT stock management (unlimited)
|
||||||
|
$single1 = Product::factory()->create([
|
||||||
|
'name' => 'Digital Item 1',
|
||||||
|
'type' => ProductType::SIMPLE,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$single2 = Product::factory()->create([
|
||||||
|
'name' => 'Digital Item 2',
|
||||||
|
'type' => ProductType::SIMPLE,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ([$single1, $single2] as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool with all unlimited singles should return a very large number
|
||||||
|
// (sum of PHP_INT_MAX values, which indicates unlimited availability)
|
||||||
|
$this->assertGreaterThanOrEqual(PHP_INT_MAX, $pool->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_zero_for_empty_pool()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Empty Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(0, $pool->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_zero_when_all_stock_is_claimed()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->withStocks(10)->create();
|
||||||
|
|
||||||
|
// Claim all 10 units
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 10,
|
||||||
|
from: now(),
|
||||||
|
until: now()->addDays(5)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(0, $product->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_correctly_handles_mixed_managed_and_unmanaged_pool_singles()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Mixed Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Single 1: managed stock
|
||||||
|
$single1 = Product::factory()->create([
|
||||||
|
'name' => 'Limited Item',
|
||||||
|
'type' => ProductType::SIMPLE,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single1->increaseStock(5);
|
||||||
|
|
||||||
|
// Single 2: unmanaged stock (unlimited)
|
||||||
|
$single2 = Product::factory()->create([
|
||||||
|
'name' => 'Unlimited Item',
|
||||||
|
'type' => ProductType::SIMPLE,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ([$single1, $single2] as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool should sum: 5 (limited) + PHP_INT_MAX (unlimited)
|
||||||
|
// Result will be very large, indicating effectively unlimited availability
|
||||||
|
$this->assertGreaterThanOrEqual(PHP_INT_MAX, $pool->has_more);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_returns_correct_stock_after_multiple_increases_and_decreases()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$product->increaseStock(100);
|
||||||
|
$product->decreaseStock(20);
|
||||||
|
$product->increaseStock(50);
|
||||||
|
$product->decreaseStock(30);
|
||||||
|
|
||||||
|
// 100 - 20 + 50 - 30 = 100
|
||||||
|
$this->assertEquals(100, $product->has_more);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue