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()]); } }