I pool cart support, tests
This commit is contained in:
parent
06b45cc9b3
commit
f20637770f
|
|
@ -218,7 +218,8 @@ class Cart extends Model
|
||||||
public function setDates(
|
public function setDates(
|
||||||
\DateTimeInterface|string|int|float $from,
|
\DateTimeInterface|string|int|float $from,
|
||||||
\DateTimeInterface|string|int|float $until,
|
\DateTimeInterface|string|int|float $until,
|
||||||
bool $validateAvailability = true
|
bool $validateAvailability = true,
|
||||||
|
bool $overwrite_item_dates = true
|
||||||
): self {
|
): self {
|
||||||
// Parse string dates using Carbon
|
// Parse string dates using Carbon
|
||||||
if (is_string($from) || is_numeric($from)) {
|
if (is_string($from) || is_numeric($from)) {
|
||||||
|
|
@ -243,7 +244,10 @@ class Cart extends Model
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Update cart items with from/until
|
// Update cart items with from/until
|
||||||
$this->applyDatesToItems($validateAvailability);
|
$this->applyDatesToItems(
|
||||||
|
$validateAvailability,
|
||||||
|
$overwrite_item_dates
|
||||||
|
);
|
||||||
|
|
||||||
return $this->fresh();
|
return $this->fresh();
|
||||||
}
|
}
|
||||||
|
|
@ -587,12 +591,22 @@ class Cart extends Model
|
||||||
// For pool products, calculate current quantity in cart once to ensure consistency
|
// For pool products, calculate current quantity in cart once to ensure consistency
|
||||||
// Force fresh query to get latest cart state (important for recursive calls)
|
// Force fresh query to get latest cart state (important for recursive calls)
|
||||||
$currentQuantityInCart = null;
|
$currentQuantityInCart = null;
|
||||||
|
$poolSingleItem = null;
|
||||||
|
$poolPriceId = null;
|
||||||
|
|
||||||
if ($cartable instanceof Product && $cartable->isPool()) {
|
if ($cartable instanceof Product && $cartable->isPool()) {
|
||||||
$this->unsetRelation('items'); // Clear cached relationship
|
$this->unsetRelation('items'); // Clear cached relationship
|
||||||
$currentQuantityInCart = $this->items()
|
$currentQuantityInCart = $this->items()
|
||||||
->where('purchasable_id', $cartable->getKey())
|
->where('purchasable_id', $cartable->getKey())
|
||||||
->where('purchasable_type', get_class($cartable))
|
->where('purchasable_type', get_class($cartable))
|
||||||
->sum('quantity');
|
->sum('quantity');
|
||||||
|
|
||||||
|
// Pre-calculate pool pricing info for use in merge logic
|
||||||
|
$poolItemData = $cartable->getNextAvailablePoolItemWithPrice($this, null, $from, $until);
|
||||||
|
if ($poolItemData) {
|
||||||
|
$poolSingleItem = $poolItemData['item'];
|
||||||
|
$poolPriceId = $poolItemData['price_id'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if item already exists in cart with same parameters, dates, AND price
|
// Check if item already exists in cart with same parameters, dates, AND price
|
||||||
|
|
@ -600,7 +614,7 @@ class Cart extends Model
|
||||||
->where('purchasable_id', $cartable->getKey())
|
->where('purchasable_id', $cartable->getKey())
|
||||||
->where('purchasable_type', get_class($cartable))
|
->where('purchasable_type', get_class($cartable))
|
||||||
->get()
|
->get()
|
||||||
->first(function ($item) use ($parameters, $from, $until, $cartable, $currentQuantityInCart) {
|
->first(function ($item) use ($parameters, $from, $until, $cartable, $poolPriceId) {
|
||||||
$existingParams = is_array($item->parameters)
|
$existingParams = is_array($item->parameters)
|
||||||
? $item->parameters
|
? $item->parameters
|
||||||
: (array) $item->parameters;
|
: (array) $item->parameters;
|
||||||
|
|
@ -621,22 +635,19 @@ class Cart extends Model
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For pool products, check pricing strategy to determine merge behavior
|
// For pool products, check if price_id matches to allow proper merging
|
||||||
|
// Pool items with the same price_id (from the same single item) can merge
|
||||||
|
// but items from different single items (different price_id) should NOT merge
|
||||||
|
// Also check that the actual price matches (important for AVERAGE strategy where price can change)
|
||||||
$priceMatch = true;
|
$priceMatch = true;
|
||||||
if ($cartable instanceof Product && $cartable->isPool()) {
|
if ($cartable instanceof Product && $cartable->isPool()) {
|
||||||
// For pools, use smart pricing that considers which tiers are used
|
// Calculate expected price for this item
|
||||||
$currentPrice = $cartable->getNextAvailablePoolPriceConsideringCart($this, null, $from, $until);
|
$poolItemData = $cartable->getNextAvailablePoolItemWithPrice($this, null, $from, $until);
|
||||||
if (!$currentPrice) {
|
$expectedPrice = $poolItemData['price'] ?? null;
|
||||||
// Fallback to getCurrentPrice if method returns null
|
|
||||||
$currentPrice = $cartable->getCurrentPrice();
|
|
||||||
}
|
|
||||||
if ($from && $until) {
|
|
||||||
$days = $this->calculateBookingDays($from, $until);
|
|
||||||
$currentPrice *= $days;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare prices - merge if prices match
|
// Only merge if price_id matches AND the price amount matches
|
||||||
$priceMatch = abs((float)$item->price - $currentPrice) < 0.01;
|
$priceMatch = $poolPriceId && $item->price_id === $poolPriceId &&
|
||||||
|
$expectedPrice !== null && $item->unit_amount === (int) round($expectedPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $paramsMatch && $datesMatch && $priceMatch;
|
return $paramsMatch && $datesMatch && $priceMatch;
|
||||||
|
|
@ -644,8 +655,6 @@ class Cart extends Model
|
||||||
|
|
||||||
// Calculate price per day (base price)
|
// Calculate price per day (base price)
|
||||||
// For pool products, get price based on how many items are already in cart
|
// For pool products, get price based on how many items are already in cart
|
||||||
$poolSingleItem = null;
|
|
||||||
$poolPriceId = null;
|
|
||||||
if ($cartable instanceof Product && $cartable->isPool()) {
|
if ($cartable instanceof Product && $cartable->isPool()) {
|
||||||
// Use smarter pricing that considers which price tiers are used
|
// Use smarter pricing that considers which price tiers are used
|
||||||
$poolItemData = $cartable->getNextAvailablePoolItemWithPrice($this, null, $from, $until);
|
$poolItemData = $cartable->getNextAvailablePoolItemWithPrice($this, null, $from, $until);
|
||||||
|
|
|
||||||
|
|
@ -435,8 +435,9 @@ class CartItem extends Model
|
||||||
|
|
||||||
// Get current price per day
|
// Get current price per day
|
||||||
// Pass dates to ensure accurate pricing for pool products during date updates
|
// Pass dates to ensure accurate pricing for pool products during date updates
|
||||||
$pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until);
|
// Pass cart item ID to exclude this item from usage calculation
|
||||||
$regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until) ?? $pricePerDay;
|
$pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until, $this->id);
|
||||||
|
$regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until, $this->id) ?? $pricePerDay;
|
||||||
|
|
||||||
// Store the base unit_amount (price for 1 quantity, 1 day) in cents
|
// Store the base unit_amount (price for 1 quantity, 1 day) in cents
|
||||||
$unitAmount = (int) round($pricePerDay);
|
$unitAmount = (int) round($pricePerDay);
|
||||||
|
|
|
||||||
|
|
@ -401,13 +401,15 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
* @param mixed $cart Optional cart instance (auto-resolved from session/user if not provided)
|
* @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 $from Optional start date for booking calculations
|
||||||
* @param \DateTimeInterface|null $until Optional end date for booking calculations
|
* @param \DateTimeInterface|null $until Optional end date for booking calculations
|
||||||
|
* @param string|int|null $excludeCartItemId Cart item ID to exclude from usage calculation (for date updates)
|
||||||
* @return float|null The current price, or null if unavailable
|
* @return float|null The current price, or null if unavailable
|
||||||
*/
|
*/
|
||||||
public function getCurrentPrice(
|
public function getCurrentPrice(
|
||||||
bool|null $sales_price = null,
|
bool|null $sales_price = null,
|
||||||
mixed $cart = null,
|
mixed $cart = null,
|
||||||
?\DateTimeInterface $from = null,
|
?\DateTimeInterface $from = null,
|
||||||
?\DateTimeInterface $until = null
|
?\DateTimeInterface $until = null,
|
||||||
|
string|int|null $excludeCartItemId = null
|
||||||
): ?float {
|
): ?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()) {
|
||||||
|
|
@ -432,7 +434,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, $from, $until);
|
return $this->getNextAvailablePoolPriceConsideringCart($cart, $sales_price, $from, $until, $excludeCartItemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No cart: Get inherited price from single items
|
// No cart: Get inherited price from single items
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ trait MayBePoolProduct
|
||||||
$strategy = $this->getPricingStrategy();
|
$strategy = $this->getPricingStrategy();
|
||||||
|
|
||||||
// Build list of available single items with their prices
|
// Build list of available single items with their prices
|
||||||
|
// IMPORTANT: Collect ALL available items first, then sort, to ensure correct pricing strategy order
|
||||||
$availableItems = [];
|
$availableItems = [];
|
||||||
foreach ($singleItems as $item) {
|
foreach ($singleItems as $item) {
|
||||||
if ($item->isAvailableForBooking($from, $until, 1)) {
|
if ($item->isAvailableForBooking($from, $until, 1)) {
|
||||||
|
|
@ -155,11 +156,6 @@ trait MayBePoolProduct
|
||||||
'price' => $price ?? PHP_FLOAT_MAX, // Items without prices go last
|
'price' => $price ?? PHP_FLOAT_MAX, // Items without prices go last
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Early exit if we have enough
|
|
||||||
if (count($availableItems) >= $quantity) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($availableItems) < $quantity) {
|
if (count($availableItems) < $quantity) {
|
||||||
|
|
@ -684,13 +680,15 @@ trait MayBePoolProduct
|
||||||
* @param bool|null $sales_price Whether to get sale price
|
* @param bool|null $sales_price Whether to get sale price
|
||||||
* @param \DateTimeInterface|null $from Start date for availability check
|
* @param \DateTimeInterface|null $from Start date for availability check
|
||||||
* @param \DateTimeInterface|null $until End date for availability check
|
* @param \DateTimeInterface|null $until End date for availability check
|
||||||
|
* @param string|int|null $excludeCartItemId Cart item ID to exclude from usage calculation (for date updates)
|
||||||
* @return array|null ['price' => float, 'item' => Product, 'price_id' => string|null]
|
* @return array|null ['price' => float, 'item' => Product, 'price_id' => string|null]
|
||||||
*/
|
*/
|
||||||
public function getNextAvailablePoolItemWithPrice(
|
public function getNextAvailablePoolItemWithPrice(
|
||||||
\Blax\Shop\Models\Cart $cart,
|
\Blax\Shop\Models\Cart $cart,
|
||||||
bool|null $sales_price = null,
|
bool|null $sales_price = null,
|
||||||
?\DateTimeInterface $from = null,
|
?\DateTimeInterface $from = null,
|
||||||
?\DateTimeInterface $until = null
|
?\DateTimeInterface $until = null,
|
||||||
|
string|int|null $excludeCartItemId = null
|
||||||
): ?array {
|
): ?array {
|
||||||
if (!$this->isPool()) {
|
if (!$this->isPool()) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -724,12 +722,36 @@ trait MayBePoolProduct
|
||||||
$days = $this->calculateBookingDays($from, $until);
|
$days = $this->calculateBookingDays($from, $until);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build usage map: price => quantity used
|
// Build usage map: track which single items have been allocated
|
||||||
$priceUsage = [];
|
// Use allocated_single_item_id from meta to track actual single item usage
|
||||||
|
// ONLY count items that overlap with the current booking period
|
||||||
|
// Exclude the specified cart item (if updating dates on existing item)
|
||||||
|
$singleItemUsage = []; // item_id => quantity used
|
||||||
foreach ($cartItems as $item) {
|
foreach ($cartItems as $item) {
|
||||||
$pricePerDay = $item->price / $days;
|
// Skip the cart item being updated (if applicable)
|
||||||
$priceKey = round($pricePerDay, 2); // Round to avoid floating point issues
|
if ($excludeCartItemId && $item->id === $excludeCartItemId) {
|
||||||
$priceUsage[$priceKey] = ($priceUsage[$priceKey] ?? 0) + $item->quantity;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only count this cart item if it overlaps with the current booking period
|
||||||
|
$overlaps = true;
|
||||||
|
if ($from && $until && $item->from && $item->until) {
|
||||||
|
// Check if the cart item's booking period overlaps with the current period
|
||||||
|
// No overlap if: cart item ends before current starts, or cart item starts after current ends
|
||||||
|
$overlaps = !(
|
||||||
|
$item->until < $from || // Cart item ends before current booking starts
|
||||||
|
$item->from > $until // Cart item starts after current booking ends
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($overlaps) {
|
||||||
|
$meta = $item->getMeta();
|
||||||
|
$allocatedItemId = $meta->allocated_single_item_id ?? null;
|
||||||
|
|
||||||
|
if ($allocatedItemId) {
|
||||||
|
$singleItemUsage[$allocatedItemId] = ($singleItemUsage[$allocatedItemId] ?? 0) + $item->quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build available items list
|
// Build available items list
|
||||||
|
|
@ -749,16 +771,16 @@ trait MayBePoolProduct
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($price !== null) {
|
if ($price !== null) {
|
||||||
$priceRounded = round($price, 2);
|
// Subtract quantity already allocated from THIS specific single item
|
||||||
|
$usedFromThisItem = $singleItemUsage[$item->id] ?? 0;
|
||||||
|
$availableFromThisItem = $available === PHP_INT_MAX
|
||||||
|
? PHP_INT_MAX
|
||||||
|
: max(0, $available - $usedFromThisItem);
|
||||||
|
|
||||||
// Subtract quantity already used in cart at this price
|
if ($availableFromThisItem > 0) {
|
||||||
$usedAtThisPrice = $priceUsage[$priceRounded] ?? 0;
|
|
||||||
$availableAtThisPrice = $available - $usedAtThisPrice;
|
|
||||||
|
|
||||||
if ($availableAtThisPrice > 0) {
|
|
||||||
$availableItems[] = [
|
$availableItems[] = [
|
||||||
'price' => $price,
|
'price' => $price,
|
||||||
'quantity' => $availableAtThisPrice,
|
'quantity' => $availableFromThisItem,
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
'price_id' => $priceModel?->id,
|
'price_id' => $priceModel?->id,
|
||||||
];
|
];
|
||||||
|
|
@ -848,9 +870,10 @@ trait MayBePoolProduct
|
||||||
\Blax\Shop\Models\Cart $cart,
|
\Blax\Shop\Models\Cart $cart,
|
||||||
bool|null $sales_price = null,
|
bool|null $sales_price = null,
|
||||||
?\DateTimeInterface $from = null,
|
?\DateTimeInterface $from = null,
|
||||||
?\DateTimeInterface $until = null
|
?\DateTimeInterface $until = null,
|
||||||
|
string|int|null $excludeCartItemId = null
|
||||||
): ?float {
|
): ?float {
|
||||||
$result = $this->getNextAvailablePoolItemWithPrice($cart, $sales_price, $from, $until);
|
$result = $this->getNextAvailablePoolItemWithPrice($cart, $sales_price, $from, $until, $excludeCartItemId);
|
||||||
return $result['price'] ?? null;
|
return $result['price'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -945,6 +968,14 @@ trait MayBePoolProduct
|
||||||
throw InvalidPoolConfigurationException::notAPoolProduct($this->name);
|
throw InvalidPoolConfigurationException::notAPoolProduct($this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Critical: Pool products should NEVER manage stock themselves
|
||||||
|
// Stock is managed by individual single items only
|
||||||
|
if ($this->manage_stock) {
|
||||||
|
throw new InvalidPoolConfigurationException(
|
||||||
|
"Pool product '{$this->name}' has manage_stock=true. Pool products should never manage stock directly - only their single items manage stock."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$singleItems = $this->singleProducts;
|
$singleItems = $this->singleProducts;
|
||||||
|
|
||||||
// Critical: No single items
|
// Critical: No single items
|
||||||
|
|
@ -962,16 +993,14 @@ trait MayBePoolProduct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check stock management on single items
|
// Note: Single items may or may not manage stock
|
||||||
$itemsWithoutStock = $singleItems->filter(fn($item) => !$item->manage_stock);
|
// Items without stock management are treated as having unlimited availability
|
||||||
if ($itemsWithoutStock->isNotEmpty()) {
|
// This is acceptable - the pool just checks availability from each single item
|
||||||
$itemNames = $itemsWithoutStock->pluck('name')->toArray();
|
|
||||||
$errors[] = "Single items without stock management: " . implode(', ', $itemNames);
|
|
||||||
throw InvalidPoolConfigurationException::singleItemsWithoutStock($this->name, $itemNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for items with zero stock
|
// Check for items with zero stock (only for items that manage stock)
|
||||||
$itemsWithZeroStock = $singleItems->filter(fn($item) => $item->getAvailableStock() <= 0);
|
$itemsWithZeroStock = $singleItems
|
||||||
|
->filter(fn($item) => $item->manage_stock) // Only check items that manage stock
|
||||||
|
->filter(fn($item) => $item->getAvailableStock() <= 0);
|
||||||
if ($itemsWithZeroStock->isNotEmpty()) {
|
if ($itemsWithZeroStock->isNotEmpty()) {
|
||||||
$itemNames = $itemsWithZeroStock->pluck('name')->toArray();
|
$itemNames = $itemsWithZeroStock->pluck('name')->toArray();
|
||||||
$warnings[] = "Single items with zero stock: " . implode(', ', $itemNames);
|
$warnings[] = "Single items with zero stock: " . implode(', ', $itemNames);
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ class BookingTimespanValidationTest extends TestCase
|
||||||
$poolProduct = Product::factory()->create([
|
$poolProduct = Product::factory()->create([
|
||||||
'name' => 'Kayak Fleet',
|
'name' => 'Kayak Fleet',
|
||||||
'type' => ProductType::POOL,
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false, // Pool products never manage stock themselves
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$singleItem = Product::factory()->create([
|
$singleItem = Product::factory()->create([
|
||||||
|
|
@ -174,6 +175,7 @@ class BookingTimespanValidationTest extends TestCase
|
||||||
// Create a pool product with 2 single items so both bookings can succeed
|
// Create a pool product with 2 single items so both bookings can succeed
|
||||||
$poolProduct = Product::factory()->create([
|
$poolProduct = Product::factory()->create([
|
||||||
'type' => ProductType::POOL,
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ProductPrice::factory()->create([
|
ProductPrice::factory()->create([
|
||||||
|
|
@ -392,6 +394,7 @@ class BookingTimespanValidationTest extends TestCase
|
||||||
// Create pool with 2 single items
|
// Create pool with 2 single items
|
||||||
$poolProduct = Product::factory()->create([
|
$poolProduct = Product::factory()->create([
|
||||||
'type' => ProductType::POOL,
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$singleItem1 = Product::factory()->create([
|
$singleItem1 = Product::factory()->create([
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ class CartDateManagementTest extends TestCase
|
||||||
$cartFromDate = Carbon::now()->addDays(1);
|
$cartFromDate = Carbon::now()->addDays(1);
|
||||||
$cartUntilDate = Carbon::now()->addDays(3);
|
$cartUntilDate = Carbon::now()->addDays(3);
|
||||||
|
|
||||||
$cart->setDates($cartFromDate, $cartUntilDate, validateAvailability: false);
|
$cart->setDates($cartFromDate, $cartUntilDate, validateAvailability: false, overwrite_item_dates: false);
|
||||||
$cart->applyDatesToItems(validateAvailability: false, overwrite: false);
|
$cart->applyDatesToItems(validateAvailability: false, overwrite: false);
|
||||||
|
|
||||||
$item->refresh();
|
$item->refresh();
|
||||||
|
|
@ -328,7 +328,7 @@ class CartDateManagementTest extends TestCase
|
||||||
$cartFromDate = Carbon::now()->addDays(1);
|
$cartFromDate = Carbon::now()->addDays(1);
|
||||||
$cartUntilDate = Carbon::now()->addDays(3);
|
$cartUntilDate = Carbon::now()->addDays(3);
|
||||||
|
|
||||||
$cart->setDates($cartFromDate, $cartUntilDate, validateAvailability: false);
|
$cart->setDates($cartFromDate, $cartUntilDate, validateAvailability: false, overwrite_item_dates: false);
|
||||||
$cart->applyDatesToItems(validateAvailability: false, overwrite: false);
|
$cart->applyDatesToItems(validateAvailability: false, overwrite: false);
|
||||||
|
|
||||||
$item->refresh();
|
$item->refresh();
|
||||||
|
|
@ -364,7 +364,7 @@ class CartDateManagementTest extends TestCase
|
||||||
$cartFromDate = Carbon::now()->addDays(5);
|
$cartFromDate = Carbon::now()->addDays(5);
|
||||||
$cartUntilDate = Carbon::now()->addDays(7);
|
$cartUntilDate = Carbon::now()->addDays(7);
|
||||||
|
|
||||||
$cart->setDates($cartFromDate, $cartUntilDate, validateAvailability: false);
|
$cart->setDates($cartFromDate, $cartUntilDate, validateAvailability: false, overwrite_item_dates: false);
|
||||||
$cart->applyDatesToItems(validateAvailability: false, overwrite: false);
|
$cart->applyDatesToItems(validateAvailability: false, overwrite: false);
|
||||||
|
|
||||||
$item->refresh();
|
$item->refresh();
|
||||||
|
|
|
||||||
|
|
@ -220,11 +220,11 @@ class PoolPerMinutePricingTest extends TestCase
|
||||||
|
|
||||||
// First booking uses lowest pricing: 3000 cents * 0.25 = 750 cents ($7.50)
|
// First booking uses lowest pricing: 3000 cents * 0.25 = 750 cents ($7.50)
|
||||||
$this->assertEquals(750, $cartItem1->price);
|
$this->assertEquals(750, $cartItem1->price);
|
||||||
// Second booking may use next available pricing tier
|
// Second booking is on a different day (non-overlapping) so also uses lowest pricing
|
||||||
$this->assertGreaterThanOrEqual(750, (int)$cartItem2->price);
|
$this->assertEquals(750, (int)$cartItem2->price);
|
||||||
|
|
||||||
// Total should be reasonable for two 6-hour bookings
|
// Total should be 750 + 750 = 1500 for two 6-hour bookings on different days
|
||||||
$this->assertGreaterThan(1500, $cart->getTotal());
|
$this->assertEquals(1500, $cart->getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,10 @@ class PoolProductRelationsTest extends TestCase
|
||||||
public function legacy_manual_attach_still_works()
|
public function legacy_manual_attach_still_works()
|
||||||
{
|
{
|
||||||
// Test that old way of attaching still works (without reverse relation)
|
// Test that old way of attaching still works (without reverse relation)
|
||||||
$pool = Product::factory()->create(['type' => ProductType::POOL]);
|
$pool = Product::factory()->create([
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
$spot = Product::factory()->create(['type' => ProductType::BOOKING]);
|
$spot = Product::factory()->create(['type' => ProductType::BOOKING]);
|
||||||
|
|
||||||
// Old way using productRelations()->attach() directly
|
// Old way using productRelations()->attach() directly
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ class PoolSeparateCartItemsTest extends TestCase
|
||||||
$item2 = $this->cart->addToCart($this->pool, 1, [], $from, $until);
|
$item2 = $this->cart->addToCart($this->pool, 1, [], $from, $until);
|
||||||
|
|
||||||
// Should create separate cart items because price is different
|
// Should create separate cart items because price is different
|
||||||
|
// (Note: With AVERAGE strategy, price_id may be the same but price differs)
|
||||||
$this->assertNotEquals($item1->id, $item2->id);
|
$this->assertNotEquals($item1->id, $item2->id);
|
||||||
$this->assertEquals(2, $this->cart->items()->count());
|
$this->assertEquals(2, $this->cart->items()->count());
|
||||||
$this->assertNotEquals($price1, $item2->price);
|
$this->assertNotEquals($price1, $item2->price);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue