BF cart items

This commit is contained in:
Fabian @ Blax Software 2025-12-20 12:43:28 +01:00
parent 0e6b420297
commit 1398fd0c27
2 changed files with 152 additions and 5 deletions

View File

@ -451,11 +451,59 @@ class CartItem extends Model
// Calculate days using per-minute precision // Calculate days using per-minute precision
$days = $this->calculateBookingDays($from, $until); $days = $this->calculateBookingDays($from, $until);
// Get current price per day // For pool products with an allocated single, use the allocated single's price
// Pass dates to ensure accurate pricing for pool products during date updates // This ensures consistency when reallocatePoolItems has already assigned a specific single
// Pass cart item ID to exclude this item from usage calculation $meta = $this->getMeta();
$pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until, $this->id); $allocatedSingleItemId = $meta->allocated_single_item_id ?? null;
$regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until, $this->id) ?? $pricePerDay;
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 // Store the base unit_amount (price for 1 quantity, 1 day) in cents
$unitAmount = (int) round($pricePerDay); $unitAmount = (int) round($pricePerDay);

View File

@ -387,4 +387,103 @@ class CartItemAvailabilityValidationTest extends TestCase
$this->expectException(\Blax\Shop\Exceptions\CartItemMissingInformationException::class); $this->expectException(\Blax\Shop\Exceptions\CartItemMissingInformationException::class);
$this->cart->checkout(); $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);
}
} }