BF race condition

This commit is contained in:
Fabian @ Blax Software 2025-12-18 09:57:33 +01:00
parent c43910b927
commit a12738db1c
3 changed files with 86 additions and 28 deletions

View File

@ -373,6 +373,16 @@ class CartItem extends Model
* Update the booking dates for this cart item. * Update the booking dates for this cart item.
* Automatically recalculates price based on the new date range. * 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. * 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. * 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)) { if (is_string($until)) {
$until = \Carbon\Carbon::parse($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."); 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); $days = $this->calculateBookingDays($from, $until);
// Get current price per day // Get current price per day
$pricePerDay = $product->getCurrentPrice(); // Pass dates to ensure accurate pricing for pool products during date updates
$regularPricePerDay = $product->getCurrentPrice(false) ?? $pricePerDay; $pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until);
$regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until) ?? $pricePerDay;
// Calculate new prices // Calculate new prices
$pricePerUnit = $pricePerDay * $days; $pricePerUnit = $pricePerDay * $days;
@ -439,22 +457,24 @@ class CartItem extends Model
$from = \Carbon\Carbon::parse($from); $from = \Carbon\Carbon::parse($from);
} }
// Refresh to get current state
$this->refresh();
if ($this->until && $from >= $this->until) { if ($this->until && $from >= $this->until) {
throw new InvalidDateRangeException(); throw new InvalidDateRangeException();
} }
// Refresh to get current state before checking // Get the current until date before updating
$this->refresh(); $currentUntil = $this->until;
$this->update(['from' => $from]); // If both dates are set, use updateDates to recalculate pricing
$this->refresh(); if ($currentUntil) {
return $this->updateDates($from, $currentUntil);
// If both dates are now set, recalculate pricing
if ($this->until) {
return $this->updateDates($this->from, $this->until);
} }
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); $until = \Carbon\Carbon::parse($until);
} }
// Refresh to get current state
$this->refresh();
if ($this->from && $this->from >= $until) { if ($this->from && $this->from >= $until) {
throw new InvalidDateRangeException(); throw new InvalidDateRangeException();
} }
// Refresh to get current state before checking // Get the current from date before updating
$this->refresh(); $currentFrom = $this->from;
$this->update(['until' => $until]); // If both dates are set, use updateDates to recalculate pricing
$this->refresh(); if ($currentFrom) {
return $this->updateDates($currentFrom, $until);
// If both dates are now set, recalculate pricing
if ($this->from) {
return $this->updateDates($this->from, $this->until);
} }
return $this; // Otherwise just update the until date
$this->update(['until' => $until]);
return $this->fresh();
} }
} }

View File

@ -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 is a pool product, use cart-aware pricing if cart is provided
if ($this->isPool()) { if ($this->isPool()) {
// If no cart provided, try to get the cart from session first, then user's cart // 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) { if ($cart) {
// Cart-aware: Use smarter pricing that considers which price tiers are used // Cart-aware: Use smarter pricing that considers which price tiers are used
// This returns null if no items are available (all sold out) // 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 // No cart: Get inherited price from single items

View File

@ -361,8 +361,9 @@ class PoolPerMinutePricingTest extends TestCase
/** @test */ /** @test */
public function it_updates_pool_cart_item_from_date_recalculates_per_minute_price() public function it_updates_pool_cart_item_from_date_recalculates_per_minute_price()
{ {
$from = Carbon::now()->addDays(5)->setTime(12, 0, 0); $now = Carbon::now();
$until = Carbon::now()->addDays(6)->setTime(12, 0, 0); // 24 hours $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([ $cart = \Blax\Shop\Models\Cart::factory()->create([
'customer_id' => $this->user->id, 'customer_id' => $this->user->id,
@ -374,7 +375,7 @@ class PoolPerMinutePricingTest extends TestCase
$this->assertEquals(30.00, $cartItem->price); $this->assertEquals(30.00, $cartItem->price);
// Update from date to make it 30 hours (1.25 days) // 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); $cartItem->setFromDate($newFrom);
// Price should be $30 * 1.25 = $37.50 // Price should be $30 * 1.25 = $37.50