diff --git a/src/Models/CartItem.php b/src/Models/CartItem.php index 2eac0d4..bbffb29 100644 --- a/src/Models/CartItem.php +++ b/src/Models/CartItem.php @@ -373,6 +373,16 @@ class CartItem extends Model * Update the booking dates for this cart item. * Automatically recalculates price based on the new date range. * + * IMPORTANT: This method uses cart-aware pricing! + * For pool products, it automatically considers which price tiers are already + * used in the cart to determine the next available price based on the pricing + * strategy (LOWEST, HIGHEST, AVERAGE). + * + * The method passes the NEW dates to getCurrentPrice() to ensure accurate + * pricing calculations. Without passing dates, the pricing logic would use + * stale dates from the cart item before the update, potentially selecting + * the wrong price tier. + * * NOTE: This method allows setting any dates, even if they're not available. * Use the is_ready_to_checkout attribute to check if the dates are valid. * @@ -392,7 +402,14 @@ class CartItem extends Model if (is_string($until)) { $until = \Carbon\Carbon::parse($until); } - if ($from >= $until && $until) { + + // Validate that both dates are provided + if (!$from || !$until) { + throw new \Exception("Both 'from' and 'until' dates are required."); + } + + // Validate date order + if ($from >= $until) { throw new \Exception("The 'from' date must be before the 'until' date."); } @@ -406,8 +423,9 @@ class CartItem extends Model $days = $this->calculateBookingDays($from, $until); // Get current price per day - $pricePerDay = $product->getCurrentPrice(); - $regularPricePerDay = $product->getCurrentPrice(false) ?? $pricePerDay; + // Pass dates to ensure accurate pricing for pool products during date updates + $pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until); + $regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until) ?? $pricePerDay; // Calculate new prices $pricePerUnit = $pricePerDay * $days; @@ -439,22 +457,24 @@ class CartItem extends Model $from = \Carbon\Carbon::parse($from); } + // Refresh to get current state + $this->refresh(); + if ($this->until && $from >= $this->until) { throw new InvalidDateRangeException(); } - // Refresh to get current state before checking - $this->refresh(); + // Get the current until date before updating + $currentUntil = $this->until; - $this->update(['from' => $from]); - $this->refresh(); - - // If both dates are now set, recalculate pricing - if ($this->until) { - return $this->updateDates($this->from, $this->until); + // If both dates are set, use updateDates to recalculate pricing + if ($currentUntil) { + return $this->updateDates($from, $currentUntil); } - return $this; + // Otherwise just update the from date + $this->update(['from' => $from]); + return $this->fresh(); } /** @@ -471,21 +491,23 @@ class CartItem extends Model $until = \Carbon\Carbon::parse($until); } + // Refresh to get current state + $this->refresh(); + if ($this->from && $this->from >= $until) { throw new InvalidDateRangeException(); } - // Refresh to get current state before checking - $this->refresh(); + // Get the current from date before updating + $currentFrom = $this->from; - $this->update(['until' => $until]); - $this->refresh(); - - // If both dates are now set, recalculate pricing - if ($this->from) { - return $this->updateDates($this->from, $this->until); + // If both dates are set, use updateDates to recalculate pricing + if ($currentFrom) { + return $this->updateDates($currentFrom, $until); } - return $this; + // Otherwise just update the until date + $this->update(['until' => $until]); + return $this->fresh(); } } diff --git a/src/Models/Product.php b/src/Models/Product.php index 7f14f66..b3d88c2 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -369,10 +369,45 @@ class Product extends Model implements Purchasable, Cartable } /** - * Get the current price with pool product inheritance support + * Get the current price with pool product inheritance support and cart-aware pricing. + * + * IMPORTANT: This method handles cart-aware pricing automatically! + * + * For pool products, this method: + * - Automatically retrieves the cart from session or authenticated user if not provided + * - Considers which price tiers are already used in the cart + * - Returns the next available price based on the pricing strategy (LOWEST, HIGHEST, AVERAGE) + * + * ⚠️ COMMON MISTAKE: Do NOT call getNextAvailablePoolPriceConsideringCart() directly! + * Always use this method instead, as it handles cart resolution and edge cases properly. + * + * Example usage: + * ```php + * ✅ CORRECT: Let getCurrentPrice handle cart resolution + * $price = $product->getCurrentPrice(); + * + * ✅ CORRECT: Pass cart explicitly if you have it + * $price = $product->getCurrentPrice(null, $cart); + * + * ✅ CORRECT: Pass dates for booking calculations + * $price = $product->getCurrentPrice(null, $cart, $fromDate, $untilDate); + * + * ❌ WRONG: Bypasses cart resolution and session handling + * $price = $product->getNextAvailablePoolPriceConsideringCart($cart, null); + * ``` + * + * @param bool|null $sales_price Whether to get sale price (null = auto-detect) + * @param mixed $cart Optional cart instance (auto-resolved from session/user if not provided) + * @param \DateTimeInterface|null $from Optional start date for booking calculations + * @param \DateTimeInterface|null $until Optional end date for booking calculations + * @return float|null The current price, or null if unavailable */ - public function getCurrentPrice(bool|null $sales_price = null, mixed $cart = null): ?float - { + public function getCurrentPrice( + bool|null $sales_price = null, + mixed $cart = null, + ?\DateTimeInterface $from = null, + ?\DateTimeInterface $until = null + ): ?float { // If this is a pool product, use cart-aware pricing if cart is provided if ($this->isPool()) { // If no cart provided, try to get the cart from session first, then user's cart @@ -396,7 +431,7 @@ class Product extends Model implements Purchasable, Cartable if ($cart) { // Cart-aware: Use smarter pricing that considers which price tiers are used // This returns null if no items are available (all sold out) - return $this->getNextAvailablePoolPriceConsideringCart($cart, $sales_price); + return $this->getNextAvailablePoolPriceConsideringCart($cart, $sales_price, $from, $until); } // No cart: Get inherited price from single items diff --git a/tests/Feature/PoolPerMinutePricingTest.php b/tests/Feature/PoolPerMinutePricingTest.php index 1f49714..539d623 100644 --- a/tests/Feature/PoolPerMinutePricingTest.php +++ b/tests/Feature/PoolPerMinutePricingTest.php @@ -361,8 +361,9 @@ class PoolPerMinutePricingTest extends TestCase /** @test */ public function it_updates_pool_cart_item_from_date_recalculates_per_minute_price() { - $from = Carbon::now()->addDays(5)->setTime(12, 0, 0); - $until = Carbon::now()->addDays(6)->setTime(12, 0, 0); // 24 hours + $now = Carbon::now(); + $from = $now->copy()->addDays(5)->setTime(12, 0, 0); + $until = $now->copy()->addDays(6)->setTime(12, 0, 0); // 24 hours $cart = \Blax\Shop\Models\Cart::factory()->create([ 'customer_id' => $this->user->id, @@ -374,7 +375,7 @@ class PoolPerMinutePricingTest extends TestCase $this->assertEquals(30.00, $cartItem->price); // Update from date to make it 30 hours (1.25 days) - $newFrom = Carbon::now()->addDays(5)->setTime(6, 0, 0); + $newFrom = $now->copy()->addDays(5)->setTime(6, 0, 0); $cartItem->setFromDate($newFrom); // Price should be $30 * 1.25 = $37.50