laravel-shop/tests/Feature/Pool/PoolParkingCartPricingTest.php

812 lines
31 KiB
PHP
Raw Normal View History

2025-12-19 11:25:59 +00:00
<?php
namespace Blax\Shop\Tests\Feature;
use Blax\Shop\Enums\ProductType;
use Blax\Shop\Exceptions\NotEnoughStockException;
use Blax\Shop\Models\Cart;
use Blax\Shop\Models\Product;
use Blax\Shop\Models\ProductPrice;
use Blax\Shop\Tests\TestCase;
use Carbon\Carbon;
use Workbench\App\Models\User;
2025-12-24 18:40:10 +00:00
use PHPUnit\Framework\Attributes\Test;
2025-12-19 11:25:59 +00:00
/**
* Test pool product cart pricing with comprehensive scenarios.
*
* This test covers four configurations:
* - A: Pool HAS default price (500), Pool does NOT manage stock
* - B: Pool does NOT have default price, Pool does NOT manage stock
* - C: Pool HAS default price (500), Pool MANAGES stock
* - D: Pool does NOT have default price, Pool MANAGES stock
*
* In all cases:
* - Pool "Parkings" with 3 single items ("Parking Spot 1-3")
* - Single item 1 has default price of 300
* - Single item 2 does NOT have a default price (should fallback to pool price in A/C, or throw exception in B/D)
* - Single item 3 has default price of 1000
* - Each single item has 2 stocks
*
* Expected cart totals with LOWEST pricing strategy:
* - Add 1: 300 (from Spot 1)
* - Add 2: 600 (300 + 300, both from Spot 1)
* - Add 3: 1100 (300 + 300 + 500, third from pool or Spot 2 fallback)
* - Add 4: 1600 (300 + 300 + 500 + 500, fourth from pool or Spot 2 fallback)
* - Add 5: 2600 (300 + 300 + 500 + 500 + 1000, fifth from Spot 3)
* - Add 6: 3600 (300 + 300 + 500 + 500 + 1000 + 1000, sixth from Spot 3)
* - Add 7: NotEnoughStockException (only 6 total)
*
* When dates span 2 days, all totals should double.
*/
class PoolParkingCartPricingTest extends TestCase
{
protected User $user;
protected Cart $cart;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
auth()->login($this->user);
}
/**
* Create the pool product with specified configuration
*
* @param bool $hasPoolPrice Whether pool has its own price
* @param bool $poolManagesStock Whether pool manages stock
* @return array{pool: Product, spots: array<Product>}
*/
protected function createParkingPool(bool $hasPoolPrice, bool $poolManagesStock): array
{
// Create pool product
$pool = Product::factory()->create([
'name' => 'Parkings',
'type' => ProductType::POOL,
'manage_stock' => $poolManagesStock,
]);
// Set pricing strategy to lowest
$pool->setPoolPricingStrategy('lowest');
// Pool price (500) - only if hasPoolPrice
if ($hasPoolPrice) {
ProductPrice::factory()->create([
'purchasable_id' => $pool->id,
'purchasable_type' => Product::class,
'unit_amount' => 500,
'currency' => 'USD',
'is_default' => true,
]);
}
// Create single items
$spot1 = Product::factory()->create([
'name' => 'Parking Spot 1',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$spot1->increaseStock(2);
// Spot 1 has default price of 300
ProductPrice::factory()->create([
'purchasable_id' => $spot1->id,
'purchasable_type' => Product::class,
'unit_amount' => 300,
'currency' => 'USD',
'is_default' => true,
]);
$spot2 = Product::factory()->create([
'name' => 'Parking Spot 2',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$spot2->increaseStock(2);
// Spot 2 does NOT have a default price - should fallback to pool price
$spot3 = Product::factory()->create([
'name' => 'Parking Spot 3',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$spot3->increaseStock(2);
// Spot 3 has default price of 1000
ProductPrice::factory()->create([
'purchasable_id' => $spot3->id,
'purchasable_type' => Product::class,
'unit_amount' => 1000,
'currency' => 'USD',
'is_default' => true,
]);
// Attach single items to pool
$pool->attachSingleItems([$spot1->id, $spot2->id, $spot3->id]);
return [
'pool' => $pool,
'spots' => [$spot1, $spot2, $spot3],
];
}
/**
* Create a fresh cart for testing
*/
protected function createCart(): Cart
{
return Cart::factory()->create([
'customer_id' => $this->user->id,
'customer_type' => get_class($this->user),
]);
}
// ==========================================
// Configuration A: Pool HAS price, does NOT manage stock
// ==========================================
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_a_progressive_pricing_step_by_step()
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
2025-12-20 10:22:04 +00:00
// Set dates for validation
$from = now()->addDays(1);
$until = now()->addDays(2);
2025-12-19 11:25:59 +00:00
// Add 1: Should use lowest price (300 from Spot 1)
2025-12-20 10:22:04 +00:00
$cartItem = $this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(300, $this->cart->getTotal());
$this->assertEquals(300, $cartItem->price);
// Add 2: Still lowest price (300), cumulative 600
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(600, $this->cart->fresh()->getTotal());
// Add 3: Next lowest is pool price (500), cumulative 1100
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(1100, $this->cart->fresh()->getTotal());
// Add 4: Pool price again (500), cumulative 1600
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(1600, $this->cart->fresh()->getTotal());
// Add 5: Spot 3 price (1000), cumulative 2600
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
// Add 6: Spot 3 price again (1000), cumulative 3600
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
2025-12-20 10:22:04 +00:00
// Add 7: Should throw exception - no more stock (with dates for validation)
2025-12-19 11:25:59 +00:00
$this->expectException(NotEnoughStockException::class);
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_a_cart_items_have_correct_price_ids()
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
// Get price IDs for reference
$spot1PriceId = $spots[0]->defaultPrice()->first()->id;
$poolPriceId = $pool->defaultPrice()->first()->id;
$spot3PriceId = $spots[2]->defaultPrice()->first()->id;
// Add 6 items
$this->cart->addToCart($pool, 6);
$items = $this->cart->items()->orderBy('price', 'asc')->get();
// First cart item group (price 300) should have Spot 1's price_id
$item300 = $items->first(fn($i) => $i->price === 300);
$this->assertNotNull($item300);
$this->assertEquals($spot1PriceId, $item300->price_id);
// Second cart item group (price 500) should have Pool's price_id (for Spot 2 fallback)
$item500 = $items->first(fn($i) => $i->price === 500);
$this->assertNotNull($item500);
$this->assertEquals($poolPriceId, $item500->price_id);
// Third cart item group (price 1000) should have Spot 3's price_id
$item1000 = $items->first(fn($i) => $i->price === 1000);
$this->assertNotNull($item1000);
$this->assertEquals($spot3PriceId, $item1000->price_id);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_a_set_dates_doubles_cart_total()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Add items with dates
$this->cart->addToCart($pool, 6, [], $from, $until);
// With 2 days: 300*2 + 300*2 + 500*2 + 500*2 + 1000*2 + 1000*2 = 7200
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_a_set_dates_after_adding_recalculates_prices()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
// Add items without dates first
$this->cart->addToCart($pool, 6);
// 1-day prices: 300 + 300 + 500 + 500 + 1000 + 1000 = 3600
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Set dates - should recalculate to 2-day prices
$this->cart->setDates($from, $until, validateAvailability: false);
// 2-day prices: (300 + 300 + 500 + 500 + 1000 + 1000) * 2 = 7200
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_a_set_from_date_and_until_date_separately()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
// Add items without dates first
$this->cart->addToCart($pool, 6);
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Set from date first
$this->cart->setFromDate($from, validateAvailability: false);
// Then set until date - this should trigger recalculation
$this->cart->setUntilDate($until, validateAvailability: false);
// Apply dates to items
$this->cart->applyDatesToItems(validateAvailability: false, overwrite: true);
// Should be 2-day prices
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_a_set_dates_overwrites_cart_item_dates()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
$from1 = Carbon::now()->addDay()->startOfDay();
$until1 = Carbon::now()->addDays(2)->startOfDay(); // 1 day
// Add items with 1-day dates
$this->cart->addToCart($pool, 2, [], $from1, $until1);
$this->assertEquals(600, $this->cart->fresh()->getTotal()); // 300 * 1 * 2
$from2 = Carbon::now()->addDay()->startOfDay();
$until2 = Carbon::now()->addDays(4)->startOfDay(); // 3 days
// Set new cart dates - should overwrite item dates
$this->cart->setDates($from2, $until2, validateAvailability: false, overwrite_item_dates: true);
// Should be 3-day prices: 300*3 + 300*3 = 1800
$this->assertEquals(1800, $this->cart->fresh()->getTotal());
// Verify item dates were overwritten
foreach ($this->cart->items as $item) {
$this->assertEquals($from2->format('Y-m-d'), $item->from->format('Y-m-d'));
$this->assertEquals($until2->format('Y-m-d'), $item->until->format('Y-m-d'));
}
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-20 11:19:34 +00:00
public function config_a_marks_items_unavailable_when_setting_dates_to_unavailable_period()
2025-12-19 11:25:59 +00:00
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(2)->startOfDay();
// Add items WITH dates first (so they become booking items)
$from2 = Carbon::now()->addDays(5)->startOfDay();
$until2 = Carbon::now()->addDays(6)->startOfDay();
$this->cart->addToCart($pool, 1, [], $from2, $until2);
// Claim all stock for the NEW period we want to set
$spots[0]->claimStock(2, null, $from, $until);
$spots[1]->claimStock(2, null, $from, $until);
$spots[2]->claimStock(2, null, $from, $until);
// Try to set dates for period when no stock is available
2025-12-20 11:19:34 +00:00
// Should NOT throw, but mark items as unavailable
2025-12-19 11:25:59 +00:00
$this->cart->setDates($from, $until, validateAvailability: true);
2025-12-20 11:19:34 +00:00
// Item should be marked as unavailable (null price)
$item = $this->cart->items()->first();
$this->assertNull($item->price, 'Unavailable item should have null price');
$this->assertFalse($item->is_ready_to_checkout, 'Unavailable item should not be ready for checkout');
2025-12-19 11:25:59 +00:00
}
// ==========================================
// Configuration B: Pool does NOT have price, does NOT manage stock
// ==========================================
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_b_progressive_pricing_step_by_step()
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: false, poolManagesStock: false);
// Add 1: Should use lowest price (300 from Spot 1)
$cartItem = $this->cart->addToCart($pool, 1);
$this->assertEquals(300, $this->cart->getTotal());
$this->assertEquals(300, $cartItem->price);
// Add 2: Still lowest price (300), cumulative 600
$this->cart->addToCart($pool, 1);
$this->assertEquals(600, $this->cart->fresh()->getTotal());
// Add 3: Spot 2 has no price and pool has no price, so next is Spot 3 (1000)
// Wait - Spot 2 should be skipped since it has no price and no pool fallback
// Expected: 300 + 300 + 1000 = 1600
$this->cart->addToCart($pool, 1);
$this->assertEquals(1600, $this->cart->fresh()->getTotal());
// Add 4: Still Spot 3 (1000), cumulative 2600
$this->cart->addToCart($pool, 1);
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
// Add 5: No more available (Spot 1 has 2, Spot 3 has 2, Spot 2 has 0 available due to no price)
// Total available: 4, should throw exception on 5th
// Note: Throws HasNoPriceException because all PRICED items are exhausted
// (Spot 2 has stock but no price, so it's not available for sale)
$this->expectException(\Blax\Shop\Exceptions\HasNoPriceException::class);
$this->cart->addToCart($pool, 1);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_b_cart_items_have_correct_price_ids()
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: false, poolManagesStock: false);
// Get price IDs for reference
$spot1PriceId = $spots[0]->defaultPrice()->first()->id;
$spot3PriceId = $spots[2]->defaultPrice()->first()->id;
// Add 4 items (max available when Spot 2 has no price)
$this->cart->addToCart($pool, 4);
$items = $this->cart->items()->orderBy('price', 'asc')->get();
// Items with price 300 should have Spot 1's price_id
$item300 = $items->first(fn($i) => $i->price === 300);
$this->assertNotNull($item300);
$this->assertEquals($spot1PriceId, $item300->price_id);
// Items with price 1000 should have Spot 3's price_id
$item1000 = $items->first(fn($i) => $i->price === 1000);
$this->assertNotNull($item1000);
$this->assertEquals($spot3PriceId, $item1000->price_id);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_b_set_dates_doubles_cart_total()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: false, poolManagesStock: false);
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Add 4 items (max available)
$this->cart->addToCart($pool, 4, [], $from, $until);
// With 2 days: (300*2 + 300*2 + 1000*2 + 1000*2) = 5200
$this->assertEquals(5200, $this->cart->fresh()->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_b_set_dates_after_adding_recalculates_prices()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: false, poolManagesStock: false);
// Add items without dates first
$this->cart->addToCart($pool, 4);
// 1-day prices: 300 + 300 + 1000 + 1000 = 2600
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Set dates - should recalculate to 2-day prices
$this->cart->setDates($from, $until, validateAvailability: false);
// 2-day prices: 2600 * 2 = 5200
$this->assertEquals(5200, $this->cart->fresh()->getTotal());
}
// ==========================================
// Configuration C: Pool HAS price, MANAGES stock
// ==========================================
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_c_progressive_pricing_step_by_step()
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: true);
2025-12-20 10:22:04 +00:00
// Set dates for validation
$from = now()->addDays(1);
$until = now()->addDays(2);
2025-12-19 11:25:59 +00:00
// Add 1: Should use lowest price (300 from Spot 1)
2025-12-20 10:22:04 +00:00
$cartItem = $this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(300, $this->cart->getTotal());
$this->assertEquals(300, $cartItem->price);
// Add 2: Still lowest price (300), cumulative 600
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(600, $this->cart->fresh()->getTotal());
// Add 3: Next lowest is pool price (500) for Spot 2, cumulative 1100
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(1100, $this->cart->fresh()->getTotal());
// Add 4: Pool price again (500), cumulative 1600
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(1600, $this->cart->fresh()->getTotal());
// Add 5: Spot 3 price (1000), cumulative 2600
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
// Add 6: Spot 3 price again (1000), cumulative 3600
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
2025-12-20 10:22:04 +00:00
// Add 7: Should throw exception - no more stock (with dates for validation)
2025-12-19 11:25:59 +00:00
$this->expectException(NotEnoughStockException::class);
2025-12-20 10:22:04 +00:00
$this->cart->addToCart($pool, 1, [], $from, $until);
2025-12-19 11:25:59 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_c_cart_items_have_correct_price_ids()
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: true);
// Get price IDs for reference
$spot1PriceId = $spots[0]->defaultPrice()->first()->id;
$poolPriceId = $pool->defaultPrice()->first()->id;
$spot3PriceId = $spots[2]->defaultPrice()->first()->id;
// Add 6 items
$this->cart->addToCart($pool, 6);
$items = $this->cart->items()->orderBy('price', 'asc')->get();
// First cart item group (price 300) should have Spot 1's price_id
$item300 = $items->first(fn($i) => $i->price === 300);
$this->assertNotNull($item300);
$this->assertEquals($spot1PriceId, $item300->price_id);
// Second cart item group (price 500) should have Pool's price_id (for Spot 2 fallback)
$item500 = $items->first(fn($i) => $i->price === 500);
$this->assertNotNull($item500);
$this->assertEquals($poolPriceId, $item500->price_id);
// Third cart item group (price 1000) should have Spot 3's price_id
$item1000 = $items->first(fn($i) => $i->price === 1000);
$this->assertNotNull($item1000);
$this->assertEquals($spot3PriceId, $item1000->price_id);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_c_set_dates_doubles_cart_total()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: true);
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Add items with dates
$this->cart->addToCart($pool, 6, [], $from, $until);
// With 2 days: 300*2 + 300*2 + 500*2 + 500*2 + 1000*2 + 1000*2 = 7200
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_c_set_dates_after_adding_recalculates_prices()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: true);
// Add items without dates first
$this->cart->addToCart($pool, 6);
// 1-day prices: 300 + 300 + 500 + 500 + 1000 + 1000 = 3600
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Set dates - should recalculate to 2-day prices
$this->cart->setDates($from, $until, validateAvailability: false);
// 2-day prices: 7200
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_c_pool_stock_is_ignored_when_single_items_manage_stock()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: true);
// Pool manages stock but has no stock of its own
// Availability should still come from single items
$this->assertEquals(6, $pool->getAvailableQuantity());
}
// ==========================================
// Configuration D: Pool does NOT have price, MANAGES stock
// ==========================================
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_d_progressive_pricing_step_by_step()
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: false, poolManagesStock: true);
// Add 1: Should use lowest price (300 from Spot 1)
$cartItem = $this->cart->addToCart($pool, 1);
$this->assertEquals(300, $this->cart->getTotal());
$this->assertEquals(300, $cartItem->price);
// Add 2: Still lowest price (300), cumulative 600
$this->cart->addToCart($pool, 1);
$this->assertEquals(600, $this->cart->fresh()->getTotal());
// Add 3: Spot 2 has no price and pool has no price, so next is Spot 3 (1000)
$this->cart->addToCart($pool, 1);
$this->assertEquals(1600, $this->cart->fresh()->getTotal());
// Add 4: Still Spot 3 (1000), cumulative 2600
$this->cart->addToCart($pool, 1);
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
// Add 5: Should throw exception - only 4 available (Spot 1:2 + Spot 3:2)
// Note: Throws HasNoPriceException because all PRICED items are exhausted
// (Spot 2 has stock but no price, so it's not available for sale)
$this->expectException(\Blax\Shop\Exceptions\HasNoPriceException::class);
$this->cart->addToCart($pool, 1);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_d_cart_items_have_correct_price_ids()
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: false, poolManagesStock: true);
// Get price IDs for reference
$spot1PriceId = $spots[0]->defaultPrice()->first()->id;
$spot3PriceId = $spots[2]->defaultPrice()->first()->id;
// Add 4 items (max available when Spot 2 has no price)
$this->cart->addToCart($pool, 4);
$items = $this->cart->items()->orderBy('price', 'asc')->get();
// Items with price 300 should have Spot 1's price_id
$item300 = $items->first(fn($i) => $i->price === 300);
$this->assertNotNull($item300);
$this->assertEquals($spot1PriceId, $item300->price_id);
// Items with price 1000 should have Spot 3's price_id
$item1000 = $items->first(fn($i) => $i->price === 1000);
$this->assertNotNull($item1000);
$this->assertEquals($spot3PriceId, $item1000->price_id);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_d_set_dates_doubles_cart_total()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: false, poolManagesStock: true);
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Add 4 items (max available)
$this->cart->addToCart($pool, 4, [], $from, $until);
// With 2 days: (300*2 + 300*2 + 1000*2 + 1000*2) = 5200
$this->assertEquals(5200, $this->cart->fresh()->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function config_d_set_dates_after_adding_recalculates_prices()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: false, poolManagesStock: true);
// Add items without dates first
$this->cart->addToCart($pool, 4);
// 1-day prices: 300 + 300 + 1000 + 1000 = 2600
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Set dates - should recalculate to 2-day prices
$this->cart->setDates($from, $until, validateAvailability: false);
// 2-day prices: 2600 * 2 = 5200
$this->assertEquals(5200, $this->cart->fresh()->getTotal());
}
// ==========================================
// Additional tests for date management
// ==========================================
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-20 11:19:34 +00:00
public function set_dates_marks_items_unavailable_when_all_claimed()
2025-12-19 11:25:59 +00:00
{
$this->cart = $this->createCart();
['pool' => $pool, 'spots' => $spots] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(2)->startOfDay();
// Add items WITH dates (so they become booking items that get validated)
$from2 = Carbon::now()->addDays(5)->startOfDay();
$until2 = Carbon::now()->addDays(6)->startOfDay();
$this->cart->addToCart($pool, 5, [], $from2, $until2);
// Claim ALL stock for the NEW period we're about to set
// This leaves 0 available for the new period
$spots[0]->claimStock(2, null, $from, $until);
$spots[1]->claimStock(2, null, $from, $until);
$spots[2]->claimStock(2, null, $from, $until);
2025-12-20 11:19:34 +00:00
// Setting dates should NOT throw, but mark items as unavailable
2025-12-19 11:25:59 +00:00
$this->cart->setDates($from, $until, validateAvailability: true);
2025-12-20 11:19:34 +00:00
// All items should be marked as unavailable (null price)
$this->cart->refresh();
$this->cart->load('items');
foreach ($this->cart->items as $item) {
$this->assertNull($item->price, 'Unavailable item should have null price');
$this->assertFalse($item->is_ready_to_checkout, 'Unavailable item should not be ready');
}
$this->assertFalse($this->cart->is_ready_to_checkout, 'Cart should not be ready');
2025-12-19 11:25:59 +00:00
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function cart_item_subtotal_updates_when_dates_change()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
// Add 2 items without dates (same price tier, should merge)
$this->cart->addToCart($pool, 2);
$item = $this->cart->items()->first();
$this->assertEquals(600, $item->subtotal); // 300 * 2
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(4)->startOfDay(); // 3 days
// Update dates via cart
$this->cart->setDates($from, $until, validateAvailability: false);
$item->refresh();
// Should be 300 * 3 days * 2 quantity = 1800
$this->assertEquals(1800, $item->subtotal);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function cart_total_and_item_subtotals_match()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(3)->startOfDay(); // 2 days
// Add 6 items
$this->cart->addToCart($pool, 6, [], $from, $until);
// Calculate expected total from item subtotals
$expectedTotal = $this->cart->items()->sum('subtotal');
$this->assertEquals($expectedTotal, $this->cart->getTotal());
$this->assertEquals(7200, $this->cart->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function removing_items_updates_pool_availability()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
// Add 6 items
$this->cart->addToCart($pool, 6);
$this->assertEquals(3600, $this->cart->getTotal());
// Remove 1 item (should remove from highest price first - LIFO)
$this->cart->removeFromCart($pool, 1);
// Now we should be able to add 1 more
$this->cart->addToCart($pool, 1);
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function adding_quantity_greater_than_one_respects_availability()
{
$this->cart = $this->createCart();
['pool' => $pool] = $this->createParkingPool(hasPoolPrice: true, poolManagesStock: false);
$from = Carbon::now()->addDay()->startOfDay();
$until = Carbon::now()->addDays(2)->startOfDay();
// Try to add 7 at once - should fail immediately
$this->expectException(NotEnoughStockException::class);
$this->cart->addToCart($pool, 7, [], $from, $until);
}
2025-12-24 18:40:10 +00:00
#[Test]
2025-12-19 11:25:59 +00:00
public function pool_with_all_single_items_without_prices_throws_exception()
{
$pool = Product::factory()->create([
'name' => 'No Price Pool',
'type' => ProductType::POOL,
'manage_stock' => false,
]);
$spot1 = Product::factory()->create([
'name' => 'No Price Spot 1',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$spot1->increaseStock(2);
$spot2 = Product::factory()->create([
'name' => 'No Price Spot 2',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$spot2->increaseStock(2);
$pool->attachSingleItems([$spot1->id, $spot2->id]);
$pool->setPoolPricingStrategy('lowest');
$this->cart = $this->createCart();
$this->expectException(\Blax\Shop\Exceptions\HasNoPriceException::class);
$this->cart->addToCart($pool, 1);
}
}