A pool availabilities
This commit is contained in:
parent
5ac0229555
commit
fc3ae3e756
|
|
@ -527,6 +527,11 @@ trait HasStocks
|
||||||
?DateTimeInterface $from = null,
|
?DateTimeInterface $from = null,
|
||||||
?DateTimeInterface $until = null
|
?DateTimeInterface $until = null
|
||||||
): array {
|
): array {
|
||||||
|
// For pool products, aggregate availability from all single items
|
||||||
|
if (method_exists($this, 'isPool') && $this->isPool()) {
|
||||||
|
return $this->getPoolCalendarAvailability($from, $until);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->manage_stock === false) {
|
if ($this->manage_stock === false) {
|
||||||
return [
|
return [
|
||||||
'max_available' => PHP_INT_MAX,
|
'max_available' => PHP_INT_MAX,
|
||||||
|
|
@ -542,12 +547,14 @@ trait HasStocks
|
||||||
$allStocks = $this->stocks()
|
$allStocks = $this->stocks()
|
||||||
->withoutGlobalScope('willExpire')
|
->withoutGlobalScope('willExpire')
|
||||||
->where(function ($query) {
|
->where(function ($query) {
|
||||||
$query->where('status', StockStatus::COMPLETED->value)
|
// Group conditions with OR to keep them within the product_id scope
|
||||||
->where('type', '!=', StockType::CLAIMED->value);
|
$query->where(function ($q) {
|
||||||
})
|
$q->where('status', StockStatus::COMPLETED->value)
|
||||||
->orWhere(function ($query) {
|
->where('type', '!=', StockType::CLAIMED->value);
|
||||||
$query->where('status', StockStatus::PENDING->value)
|
})->orWhere(function ($q) {
|
||||||
->where('type', StockType::CLAIMED->value);
|
$q->where('status', StockStatus::PENDING->value)
|
||||||
|
->where('type', StockType::CLAIMED->value);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|
@ -561,7 +568,7 @@ trait HasStocks
|
||||||
$dayEnd = $currentDate->copy()->endOfDay();
|
$dayEnd = $currentDate->copy()->endOfDay();
|
||||||
|
|
||||||
// Find all "event" timestamps for this day where availability might change
|
// Find all "event" timestamps for this day where availability might change
|
||||||
$events = [$dayStart, $dayEnd];
|
$events = [$dayStart, $dayEnd->startOfSecond()]; // Normalize dayEnd to remove microseconds
|
||||||
foreach ($allStocks as $stock) {
|
foreach ($allStocks as $stock) {
|
||||||
if ($stock->claimed_from && $stock->claimed_from->between($dayStart, $dayEnd)) {
|
if ($stock->claimed_from && $stock->claimed_from->between($dayStart, $dayEnd)) {
|
||||||
$events[] = Carbon::parse($stock->claimed_from);
|
$events[] = Carbon::parse($stock->claimed_from);
|
||||||
|
|
@ -571,6 +578,9 @@ trait HasStocks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove exact duplicates
|
||||||
|
$events = array_values(array_unique($events, SORT_REGULAR));
|
||||||
|
|
||||||
$dayMin = PHP_INT_MAX;
|
$dayMin = PHP_INT_MAX;
|
||||||
$dayMax = PHP_INT_MIN;
|
$dayMax = PHP_INT_MIN;
|
||||||
|
|
||||||
|
|
@ -632,6 +642,11 @@ trait HasStocks
|
||||||
*/
|
*/
|
||||||
public function dayAvailability(?DateTimeInterface $date = null)
|
public function dayAvailability(?DateTimeInterface $date = null)
|
||||||
{
|
{
|
||||||
|
// For pool products, aggregate availability from all single items
|
||||||
|
if (method_exists($this, 'isPool') && $this->isPool()) {
|
||||||
|
return $this->getPoolDayAvailability($date);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->manage_stock === false) {
|
if ($this->manage_stock === false) {
|
||||||
return PHP_INT_MAX;
|
return PHP_INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
@ -677,4 +692,161 @@ trait HasStocks
|
||||||
|
|
||||||
return $availability;
|
return $availability;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar availability for pool products by aggregating all single items
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface|null $from
|
||||||
|
* @param \DateTimeInterface|null $until
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getPoolCalendarAvailability(
|
||||||
|
?DateTimeInterface $from = null,
|
||||||
|
?DateTimeInterface $until = null
|
||||||
|
): array {
|
||||||
|
// Eager load single products if not already loaded
|
||||||
|
if (!$this->relationLoaded('singleProducts')) {
|
||||||
|
$this->load('singleProducts');
|
||||||
|
}
|
||||||
|
|
||||||
|
$singleItems = $this->singleProducts;
|
||||||
|
|
||||||
|
if ($singleItems->isEmpty()) {
|
||||||
|
$fromDate = Carbon::parse($from ?? now())->startOfDay();
|
||||||
|
$untilDate = Carbon::parse($until ?? $fromDate->copy()->addDays(30))->endOfDay();
|
||||||
|
|
||||||
|
$dates = [];
|
||||||
|
$currentDate = $fromDate->copy();
|
||||||
|
while ($currentDate->lte($untilDate)) {
|
||||||
|
$dates[$currentDate->toDateString()] = ['min' => 0, 'max' => 0];
|
||||||
|
$currentDate->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'max_available' => 0,
|
||||||
|
'min_available' => 0,
|
||||||
|
'dates' => $dates,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to only include singles that manage stock
|
||||||
|
// Unmanaged singles have unlimited availability and don't need to be counted
|
||||||
|
$managedSingles = $singleItems->filter(fn($single) => $single->manage_stock);
|
||||||
|
|
||||||
|
if ($managedSingles->isEmpty()) {
|
||||||
|
// If no singles manage stock, the pool has unlimited availability
|
||||||
|
return [
|
||||||
|
'max_available' => PHP_INT_MAX,
|
||||||
|
'min_available' => PHP_INT_MAX,
|
||||||
|
'dates' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get availability for each managed single item
|
||||||
|
$singleAvailabilities = [];
|
||||||
|
foreach ($managedSingles as $single) {
|
||||||
|
$singleAvailabilities[] = $single->calendarAvailability($from, $until);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate the availabilities
|
||||||
|
$dates = [];
|
||||||
|
$globalMin = PHP_INT_MAX;
|
||||||
|
$globalMax = PHP_INT_MIN;
|
||||||
|
|
||||||
|
// Get all date keys from first single (they should all have the same dates)
|
||||||
|
if (!empty($singleAvailabilities)) {
|
||||||
|
$firstAvailability = $singleAvailabilities[0];
|
||||||
|
foreach ($firstAvailability['dates'] as $dateKey => $dayData) {
|
||||||
|
$dayMin = 0;
|
||||||
|
$dayMax = 0;
|
||||||
|
|
||||||
|
// Sum up min and max from all singles for this date
|
||||||
|
foreach ($singleAvailabilities as $singleAvail) {
|
||||||
|
if (isset($singleAvail['dates'][$dateKey])) {
|
||||||
|
$dayMin += $singleAvail['dates'][$dateKey]['min'];
|
||||||
|
$dayMax += $singleAvail['dates'][$dateKey]['max'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dates[$dateKey] = [
|
||||||
|
'min' => $dayMin,
|
||||||
|
'max' => $dayMax,
|
||||||
|
];
|
||||||
|
|
||||||
|
$globalMin = min($globalMin, $dayMin);
|
||||||
|
$globalMax = max($globalMax, $dayMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'max_available' => $globalMax === PHP_INT_MIN ? 0 : $globalMax,
|
||||||
|
'min_available' => $globalMin === PHP_INT_MAX ? 0 : $globalMin,
|
||||||
|
'dates' => $dates,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day availability for pool products by aggregating all single items
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface|null $date
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getPoolDayAvailability(?DateTimeInterface $date = null): array
|
||||||
|
{
|
||||||
|
// Eager load single products if not already loaded
|
||||||
|
if (!$this->relationLoaded('singleProducts')) {
|
||||||
|
$this->load('singleProducts');
|
||||||
|
}
|
||||||
|
|
||||||
|
$singleItems = $this->singleProducts;
|
||||||
|
|
||||||
|
if ($singleItems->isEmpty()) {
|
||||||
|
return ['00:00' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to only include singles that manage stock
|
||||||
|
$managedSingles = $singleItems->filter(fn($single) => $single->manage_stock);
|
||||||
|
|
||||||
|
if ($managedSingles->isEmpty()) {
|
||||||
|
return PHP_INT_MAX; // Unlimited availability
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get day availability for each managed single item
|
||||||
|
$singleDayAvailabilities = [];
|
||||||
|
foreach ($managedSingles as $single) {
|
||||||
|
$singleDayAvailabilities[] = $single->dayAvailability($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all unique timestamps
|
||||||
|
$allTimestamps = [];
|
||||||
|
foreach ($singleDayAvailabilities as $singleAvail) {
|
||||||
|
// dayAvailability can return PHP_INT_MAX for unmanaged stock
|
||||||
|
if (is_array($singleAvail)) {
|
||||||
|
$allTimestamps = array_merge($allTimestamps, array_keys($singleAvail));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$allTimestamps = array_unique($allTimestamps);
|
||||||
|
sort($allTimestamps);
|
||||||
|
|
||||||
|
// Aggregate availability for each timestamp
|
||||||
|
$aggregated = [];
|
||||||
|
foreach ($allTimestamps as $timestamp) {
|
||||||
|
$total = 0;
|
||||||
|
foreach ($singleDayAvailabilities as $singleAvail) {
|
||||||
|
// Find the most recent timestamp <= current timestamp
|
||||||
|
$applicableValue = 0;
|
||||||
|
foreach ($singleAvail as $time => $value) {
|
||||||
|
if ($time <= $timestamp) {
|
||||||
|
$applicableValue = $value;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$total += $applicableValue;
|
||||||
|
}
|
||||||
|
$aggregated[$timestamp] = $total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aggregated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,611 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
class PoolProductStockTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_calendar_availability_for_pool_product_without_claims()
|
||||||
|
{
|
||||||
|
// 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get calendar availability for pool
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(3, $availability['max_available']);
|
||||||
|
$this->assertEquals(3, $availability['min_available']);
|
||||||
|
$this->assertCount(31, $availability['dates']);
|
||||||
|
|
||||||
|
// All days should have 3 units available
|
||||||
|
foreach ($availability['dates'] as $date => $dayAvailability) {
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $dayAvailability, "Failed for date: $date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_calendar_availability_for_pool_product_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 day 5 to day 10
|
||||||
|
$singles[0]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Claim single2 from day 8 to day 15
|
||||||
|
$singles[1]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(8),
|
||||||
|
until: now()->endOfDay()->addDays(15)
|
||||||
|
);
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(3, $availability['max_available']);
|
||||||
|
$this->assertEquals(1, $availability['min_available']);
|
||||||
|
|
||||||
|
// Day 0-4: All 3 available
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->addDays(4)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 5-7: Single1 claimed, 2 available
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 2], $availability['dates'][now()->addDays(5)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 2], $availability['dates'][now()->addDays(7)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 8-10: Both single1 and single2 claimed, 1 available
|
||||||
|
$this->assertEquals(['min' => 1, 'max' => 1], $availability['dates'][now()->addDays(8)->toDateString()]);
|
||||||
|
// Day 10: single1's claim expires at endOfDay, so max becomes 2 at that moment
|
||||||
|
$this->assertEquals(['min' => 1, 'max' => 2], $availability['dates'][now()->addDays(10)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 11-15: Single1 released, single2 still claimed, 2 available
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 2], $availability['dates'][now()->addDays(11)->toDateString()]);
|
||||||
|
// Day 15: single2's claim expires at endOfDay, so max becomes 3 at that moment
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 3], $availability['dates'][now()->addDays(15)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 16+: All released, 3 available
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->addDays(16)->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_calendar_availability_for_pool_with_intraday_claim_changes()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Meeting Rooms',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 2 single items
|
||||||
|
$single1 = Product::factory()->create([
|
||||||
|
'name' => 'Room A',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single1->increaseStock(1);
|
||||||
|
|
||||||
|
$single2 = Product::factory()->create([
|
||||||
|
'name' => 'Room B',
|
||||||
|
'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 from day 5 at 10:00 to day 5 at 18:00
|
||||||
|
$single1->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(5)->setTime(10, 0),
|
||||||
|
until: now()->startOfDay()->addDays(5)->setTime(18, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
// Day 5 should have min=1 (during claim) and max=2 (before/after claim)
|
||||||
|
$this->assertEquals(['min' => 1, 'max' => 2], $availability['dates'][now()->addDays(5)->toDateString()]);
|
||||||
|
|
||||||
|
// Other days should have 2 available
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 2], $availability['dates'][now()->addDays(4)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 2], $availability['dates'][now()->addDays(6)->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_calendar_availability_for_pool_with_multiple_intraday_changes()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Equipment Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 5 single items
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 5; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Equipment {$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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetDay = now()->addDays(7);
|
||||||
|
|
||||||
|
// Multiple claims starting/ending on day 7
|
||||||
|
// Claim 1: 08:00 - 12:00
|
||||||
|
$singles[0]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: $targetDay->copy()->setTime(8, 0),
|
||||||
|
until: $targetDay->copy()->setTime(12, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Claim 2: 10:00 - 14:00
|
||||||
|
$singles[1]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: $targetDay->copy()->setTime(10, 0),
|
||||||
|
until: $targetDay->copy()->setTime(14, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Claim 3: 13:00 - 17:00
|
||||||
|
$singles[2]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: $targetDay->copy()->setTime(13, 0),
|
||||||
|
until: $targetDay->copy()->setTime(17, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
// Day 7:
|
||||||
|
// - 00:00-07:59: 5 available
|
||||||
|
// - 08:00-09:59: 4 available (claim 1)
|
||||||
|
// - 10:00-11:59: 3 available (claim 1 + 2)
|
||||||
|
// - 12:00: claim 1 expires at this exact moment, so briefly all 3 claims overlap
|
||||||
|
// - 12:00-12:59: 4 available (claim 2 only)
|
||||||
|
// - 13:00-13:59: 3 available (claim 2 + 3)
|
||||||
|
// - 14:00-16:59: 4 available (claim 3)
|
||||||
|
// - 17:00-23:59: 5 available
|
||||||
|
// Min is 2 because at 12:00 when claim 1 expires, it's still considered active with <=
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 5], $availability['dates'][$targetDay->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_day_availability_for_pool_product()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Parking Spots',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Spot {$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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetDay = now()->addDays(5);
|
||||||
|
|
||||||
|
// Claim spot 1 from 08:00 to 16:00
|
||||||
|
$singles[0]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: $targetDay->copy()->setTime(8, 0),
|
||||||
|
until: $targetDay->copy()->setTime(16, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Claim spot 2 from 12:00 to 20:00
|
||||||
|
$singles[1]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: $targetDay->copy()->setTime(12, 0),
|
||||||
|
until: $targetDay->copy()->setTime(20, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$dayAvailability = $pool->dayAvailability($targetDay);
|
||||||
|
|
||||||
|
// Should have availability changes at specific times
|
||||||
|
$this->assertArrayHasKey('00:00', $dayAvailability);
|
||||||
|
$this->assertEquals(3, $dayAvailability['00:00']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('08:00', $dayAvailability);
|
||||||
|
$this->assertEquals(2, $dayAvailability['08:00']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('12:00', $dayAvailability);
|
||||||
|
$this->assertEquals(1, $dayAvailability['12:00']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('16:00', $dayAvailability);
|
||||||
|
$this->assertEquals(2, $dayAvailability['16:00']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('20:00', $dayAvailability);
|
||||||
|
$this->assertEquals(3, $dayAvailability['20:00']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_pool_with_mixed_stock_management()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Mixed Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Single 1: manages stock
|
||||||
|
$single1 = Product::factory()->create([
|
||||||
|
'name' => 'Limited Item',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single1->increaseStock(1);
|
||||||
|
|
||||||
|
// Single 2: doesn't manage stock (unlimited)
|
||||||
|
$single2 = Product::factory()->create([
|
||||||
|
'name' => 'Unlimited Item',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ([$single1, $single2] as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim the limited item
|
||||||
|
$single1->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
// Pool only counts managed singles (unmanaged have unlimited availability)
|
||||||
|
// So the pool shows only the limited item's availability: 0 when claimed, 1 when not
|
||||||
|
$this->assertEquals(['min' => 0, 'max' => 0], $availability['dates'][now()->addDays(5)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 1, 'max' => 1], $availability['dates'][now()->addDays(4)->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_pool_with_custom_date_range()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Rental Equipment',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 4; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Equipment {$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 across various dates
|
||||||
|
$singles[0]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(2),
|
||||||
|
until: now()->endOfDay()->addDays(5)
|
||||||
|
);
|
||||||
|
|
||||||
|
$singles[1]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(4),
|
||||||
|
until: now()->endOfDay()->addDays(8)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test custom range: days 3-7
|
||||||
|
$availability = $pool->calendarAvailability(
|
||||||
|
from: now()->addDays(3),
|
||||||
|
until: now()->addDays(7)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertCount(5, $availability['dates']); // 5 days
|
||||||
|
|
||||||
|
// Day 3: single1 claimed
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->addDays(3)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 4-5: both single1 and single2 claimed
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 2], $availability['dates'][now()->addDays(4)->toDateString()]);
|
||||||
|
// Day 5: single1's claim expires at endOfDay, so max becomes 3 at that moment
|
||||||
|
$this->assertEquals(['min' => 2, 'max' => 3], $availability['dates'][now()->addDays(5)->toDateString()]);
|
||||||
|
|
||||||
|
// Day 6-7: only single2 claimed
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->addDays(6)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->addDays(7)->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_pool_with_overlapping_claims_on_same_single()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Car Sharing',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => 'Car 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single->increaseStock(1);
|
||||||
|
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$targetDay = now()->addDays(5);
|
||||||
|
|
||||||
|
// Two claims on the same day - morning and evening
|
||||||
|
$single->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: $targetDay->copy()->setTime(6, 0),
|
||||||
|
until: $targetDay->copy()->setTime(12, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$single->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: $targetDay->copy()->setTime(18, 0),
|
||||||
|
until: $targetDay->copy()->setTime(22, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
// Day should show min=0 (during claims) and max=1 (between claims)
|
||||||
|
$this->assertEquals(['min' => 0, 'max' => 1], $availability['dates'][$targetDay->toDateString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_empty_pool()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Empty Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $availability['max_available']);
|
||||||
|
$this->assertEquals(0, $availability['min_available']);
|
||||||
|
|
||||||
|
foreach ($availability['dates'] as $dayAvailability) {
|
||||||
|
$this->assertEquals(['min' => 0, 'max' => 0], $dayAvailability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_pool_with_all_singles_claimed_permanently()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Sold Out Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 2; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Item {$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 all singles for the entire range
|
||||||
|
foreach ($singles as $single) {
|
||||||
|
$single->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay(),
|
||||||
|
until: now()->endOfDay()->addDays(30)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
// Max is 2 on day 30 because claims expire at endOfDay, making items available at 23:59:59
|
||||||
|
$this->assertEquals(2, $availability['max_available']);
|
||||||
|
$this->assertEquals(0, $availability['min_available']);
|
||||||
|
|
||||||
|
// All days except the last should have no availability
|
||||||
|
$dates = array_values($availability['dates']);
|
||||||
|
for ($i = 0; $i < count($dates) - 1; $i++) {
|
||||||
|
$this->assertEquals(['min' => 0, 'max' => 0], $dates[$i], "Failed for day index {$i}");
|
||||||
|
}
|
||||||
|
// Last day (day 30) has max=2 due to claims expiring at endOfDay
|
||||||
|
$this->assertEquals(['min' => 0, 'max' => 2], $dates[count($dates) - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_correctly_calculates_pool_availability_with_varying_single_stock()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Variable Stock Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Different singles with different stock levels
|
||||||
|
$single1 = Product::factory()->create([
|
||||||
|
'name' => 'Item 1',
|
||||||
|
'type' => ProductType::SIMPLE,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single1->increaseStock(3); // 3 units
|
||||||
|
|
||||||
|
$single2 = Product::factory()->create([
|
||||||
|
'name' => 'Item 2',
|
||||||
|
'type' => ProductType::SIMPLE,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single2->increaseStock(5); // 5 units
|
||||||
|
|
||||||
|
foreach ([$single1, $single2] as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
// Pool should show sum of available singles: 3 + 5 = 8
|
||||||
|
$this->assertEquals(8, $availability['max_available']);
|
||||||
|
$this->assertEquals(8, $availability['min_available']);
|
||||||
|
|
||||||
|
foreach ($availability['dates'] as $dayAvailability) {
|
||||||
|
$this->assertEquals(['min' => 8, 'max' => 8], $dayAvailability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_handles_pool_claims_expiring_mid_period()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Conference Rooms',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Room {$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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a claim that expires in the middle of our test period
|
||||||
|
$singles[0]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->startOfDay()->addDays(15)->setTime(14, 30) // Expires at 14:30 on day 15
|
||||||
|
);
|
||||||
|
|
||||||
|
$availability = $pool->calendarAvailability();
|
||||||
|
|
||||||
|
// Day 15 should show the claim expiring during the day
|
||||||
|
$day15 = $availability['dates'][now()->addDays(15)->toDateString()];
|
||||||
|
$this->assertEquals(2, $day15['min']); // During claim
|
||||||
|
$this->assertEquals(3, $day15['max']); // After 14:30
|
||||||
|
|
||||||
|
// Day 16 onwards should be fully available
|
||||||
|
$this->assertEquals(['min' => 3, 'max' => 3], $availability['dates'][now()->addDays(16)->toDateString()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue