From 1398fd0c27b042c247aea7bc5e6cab2de9196793 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Sat, 20 Dec 2025 12:43:28 +0100 Subject: [PATCH] BF cart items --- src/Models/CartItem.php | 58 ++++++++++- .../CartItemAvailabilityValidationTest.php | 99 +++++++++++++++++++ 2 files changed, 152 insertions(+), 5 deletions(-) diff --git a/src/Models/CartItem.php b/src/Models/CartItem.php index 5ba279d..62b4909 100644 --- a/src/Models/CartItem.php +++ b/src/Models/CartItem.php @@ -451,11 +451,59 @@ class CartItem extends Model // Calculate days using per-minute precision $days = $this->calculateBookingDays($from, $until); - // Get current price per day - // Pass dates to ensure accurate pricing for pool products during date updates - // Pass cart item ID to exclude this item from usage calculation - $pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until, $this->id); - $regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until, $this->id) ?? $pricePerDay; + // For pool products with an allocated single, use the allocated single's price + // This ensures consistency when reallocatePoolItems has already assigned a specific single + $meta = $this->getMeta(); + $allocatedSingleItemId = $meta->allocated_single_item_id ?? null; + + if ($product->isPool() && $allocatedSingleItemId) { + // Get the allocated single item + $allocatedSingle = Product::find($allocatedSingleItemId); + + if ($allocatedSingle) { + // Get price from the allocated single, with fallback to pool price + $priceModel = $allocatedSingle->defaultPrice()->first(); + $pricePerDay = $priceModel?->getCurrentPrice($allocatedSingle->isOnSale()); + $regularPricePerDay = $priceModel?->getCurrentPrice(false) ?? $pricePerDay; + + // Fallback to pool price if single has no price + if ($pricePerDay === null && $product->hasPrice()) { + $poolPriceModel = $product->defaultPrice()->first(); + $pricePerDay = $poolPriceModel?->getCurrentPrice($product->isOnSale()); + $regularPricePerDay = $poolPriceModel?->getCurrentPrice(false) ?? $pricePerDay; + } + } else { + // Allocated single not found - this is an error state, mark as unavailable + $this->update([ + 'from' => $from, + 'until' => $until, + 'price' => null, + 'regular_price' => null, + 'unit_amount' => null, + 'subtotal' => null, + ]); + return $this->fresh(); + } + } else { + // Non-pool product or pool without allocation: use getCurrentPrice + // Pass dates to ensure accurate pricing for pool products during date updates + // Pass cart item ID to exclude this item from usage calculation + $pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until, $this->id); + $regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until, $this->id) ?? $pricePerDay; + } + + // If no price found, mark as unavailable + if ($pricePerDay === null) { + $this->update([ + 'from' => $from, + 'until' => $until, + 'price' => null, + 'regular_price' => null, + 'unit_amount' => null, + 'subtotal' => null, + ]); + return $this->fresh(); + } // Store the base unit_amount (price for 1 quantity, 1 day) in cents $unitAmount = (int) round($pricePerDay); diff --git a/tests/Feature/CartItemAvailabilityValidationTest.php b/tests/Feature/CartItemAvailabilityValidationTest.php index c11f0e9..6a264ae 100644 --- a/tests/Feature/CartItemAvailabilityValidationTest.php +++ b/tests/Feature/CartItemAvailabilityValidationTest.php @@ -387,4 +387,103 @@ class CartItemAvailabilityValidationTest extends TestCase $this->expectException(\Blax\Shop\Exceptions\CartItemMissingInformationException::class); $this->cart->checkout(); } + + /** @test */ + public function checkoutSessionLink_throws_when_items_have_null_price() + { + $pool = $this->createPoolWithLimitedSingles(3); + + $from = now()->addDays(1); + $until = now()->addDays(2); + + // Add items + $this->cart->addToCart($pool, 3, [], $from, $until); + + // Manually make one unavailable + $item = $this->cart->items()->first(); + $item->update(['price' => null, 'subtotal' => null]); + + // checkoutSessionLink should throw because item is unavailable + $this->expectException(\Blax\Shop\Exceptions\CartItemMissingInformationException::class); + $this->cart->checkoutSessionLink(); + } + + /** @test */ + public function checkoutSessionLink_throws_when_items_have_zero_price() + { + $pool = $this->createPoolWithLimitedSingles(3); + + $from = now()->addDays(1); + $until = now()->addDays(2); + + // Add items + $this->cart->addToCart($pool, 3, [], $from, $until); + + // Manually set price to 0 (should also be considered unavailable) + $item = $this->cart->items()->first(); + $item->update(['price' => 0, 'subtotal' => 0]); + + // checkoutSessionLink should throw because item has 0 price + $this->expectException(\Blax\Shop\Exceptions\CartItemMissingInformationException::class); + $this->cart->checkoutSessionLink(); + } + + /** @test */ + public function pool_items_maintain_consistent_pricing_after_date_changes() + { + $pool = $this->createPoolWithLimitedSingles(3); + + $from1 = now()->addDays(1); + $until1 = now()->addDays(2); + + // Add 3 items with dates + $this->cart->addToCart($pool, 3, [], $from1, $until1); + + // Get initial prices + $initialPrices = $this->cart->items->pluck('price')->sort()->values()->toArray(); + + // Change to different dates (same duration) + $from2 = now()->addDays(5); + $until2 = now()->addDays(6); + + $this->cart->setDates($from2, $until2); + $this->cart->refresh(); + $this->cart->load('items'); + + // Prices should be the same (only dates changed, not duration) + $newPrices = $this->cart->items->pluck('price')->sort()->values()->toArray(); + + $this->assertEquals( + $initialPrices, + $newPrices, + 'Prices should remain consistent when only dates change (same duration)' + ); + } + + /** @test */ + public function price_zero_is_treated_as_unavailable() + { + $pool = $this->createPoolWithLimitedSingles(3); + + $from = now()->addDays(1); + $until = now()->addDays(2); + + $this->cart->addToCart($pool, 3, [], $from, $until); + + // Set price to 0 (simulating an old bug where 0 was used instead of null) + $item = $this->cart->items()->first(); + $item->update(['price' => 0, 'subtotal' => 0]); + $item->refresh(); + + // Item should NOT be ready for checkout + $this->assertFalse($item->is_ready_to_checkout, 'Item with price 0 should not be ready'); + + // requiredAdjustments should show price as unavailable + $adjustments = $item->requiredAdjustments(); + $this->assertArrayHasKey('price', $adjustments); + $this->assertEquals('unavailable', $adjustments['price']); + + // Cart should NOT be ready + $this->assertFalse($this->cart->fresh()->is_ready_to_checkout); + } }