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

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->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

View File

@ -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