user = User::factory()->create(); $this->cart = Cart::factory()->create([ 'customer_id' => $this->user->id, 'customer_type' => get_class($this->user), ]); // Create pool product $this->poolProduct = Product::factory()->create([ 'name' => 'Parking Pool', 'type' => ProductType::POOL, 'manage_stock' => false, ]); // Create single items $this->singleItem1 = Product::factory()->create([ 'name' => 'Parking Spot 1', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $this->singleItem1->increaseStock(1); $this->singleItem2 = Product::factory()->create([ 'name' => 'Parking Spot 2', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $this->singleItem2->increaseStock(1); // Link single items to pool $this->poolProduct->productRelations()->attach($this->singleItem1->id, [ 'type' => ProductRelationType::SINGLE->value, ]); $this->poolProduct->productRelations()->attach($this->singleItem2->id, [ 'type' => ProductRelationType::SINGLE->value, ]); } /** @test */ public function it_adds_pool_with_direct_price_to_cart_without_dates() { // Set direct price on pool ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, // 30.00 per day 'currency' => 'USD', 'is_default' => true, ]); $cartItem = $this->cart->addToCart($this->poolProduct, 1); $this->assertNotNull($cartItem); $this->assertEquals($this->poolProduct->id, $cartItem->purchasable_id); $this->assertEquals(1, $cartItem->quantity); $this->assertEquals(3000, $cartItem->price); // 30.00 per day × 1 day $this->assertEquals(3000, $cartItem->subtotal); // 30.00 × 1 quantity $this->assertNull($cartItem->from); $this->assertNull($cartItem->until); } /** @test */ public function it_adds_pool_with_inherited_price_to_cart_without_dates() { // Set prices on single items (20€ and 50€) ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem1->id, 'purchasable_type' => Product::class, 'unit_amount' => 2000, // 20.00 'currency' => 'USD', 'is_default' => true, ]); ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem2->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, // 50.00 'currency' => 'USD', 'is_default' => true, ]); // Set pricing strategy to average: (2000 + 5000) / 2 = 3500 $this->poolProduct->setPricingStrategy(PricingStrategy::AVERAGE); // Pool should inherit average: (2000 + 5000) / 2 = 3500 $cartItem = $this->cart->addToCart($this->poolProduct, 1); $this->assertNotNull($cartItem); $this->assertEquals($this->poolProduct->id, $cartItem->purchasable_id); $this->assertEquals(1, $cartItem->quantity); $this->assertEquals(3500, $cartItem->price); // Average: 35.00 $this->assertEquals(3500, $cartItem->subtotal); } /** @test */ public function it_adds_pool_with_direct_price_to_cart_with_booking_dates() { // Set direct price on pool ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, // 30.00 per day 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(4)->startOfDay(); // 3 days $days = $from->diffInDays($until); $cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); $this->assertNotNull($cartItem); $this->assertEquals($this->poolProduct->id, $cartItem->purchasable_id); $this->assertEquals(1, $cartItem->quantity); $this->assertEquals(9000, $cartItem->price); // 30.00 × 3 days $this->assertEquals(9000, $cartItem->subtotal); // 90.00 $this->assertEquals($from->format('Y-m-d H:i:s'), $cartItem->from->format('Y-m-d H:i:s')); $this->assertEquals($until->format('Y-m-d H:i:s'), $cartItem->until->format('Y-m-d H:i:s')); } /** @test */ public function it_adds_pool_with_inherited_price_to_cart_with_booking_dates() { // Set prices on single items (20€ and 50€) ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem1->id, 'purchasable_type' => Product::class, 'unit_amount' => 2000, // 20.00 per day 'currency' => 'USD', 'is_default' => true, ]); ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem2->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, // 50.00 per day 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(3)->startOfDay(); // 2 days $days = $from->diffInDays($until); // Set pricing strategy to average: (2000 + 5000) / 2 = 3500 per day $this->poolProduct->setPricingStrategy(PricingStrategy::AVERAGE); // Pool inherits average: (2000 + 5000) / 2 = 3500 per day $cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); $this->assertNotNull($cartItem); $this->assertEquals($this->poolProduct->id, $cartItem->purchasable_id); $this->assertEquals(1, $cartItem->quantity); $this->assertEquals(7000, $cartItem->price); // 35.00 × 2 days $this->assertEquals(7000, $cartItem->subtotal); $this->assertEquals($from->format('Y-m-d H:i:s'), $cartItem->from->format('Y-m-d H:i:s')); $this->assertEquals($until->format('Y-m-d H:i:s'), $cartItem->until->format('Y-m-d H:i:s')); } /** @test */ public function it_calculates_price_for_multiple_pool_items_with_booking_dates() { // Set direct price on pool ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 2500, // 25.00 per day 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(6)->startOfDay(); // 5 days $cartItem = $this->cart->addToCart($this->poolProduct, 2, [], $from, $until); $this->assertEquals(2, $cartItem->quantity); $this->assertEquals(12500, $cartItem->price); // 25.00 × 5 days per unit $this->assertEquals(25000, $cartItem->subtotal); // 125.00 × 2 units = 250.00 } /** @test */ public function it_uses_lowest_pricing_strategy_with_mixed_single_item_prices() { // Set prices on single items (20€ and 50€) ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem1->id, 'purchasable_type' => Product::class, 'unit_amount' => 2000, // 20.00 'currency' => 'USD', 'is_default' => true, ]); ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem2->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, // 50.00 'currency' => 'USD', 'is_default' => true, ]); // Set pricing strategy to lowest $this->poolProduct->setPoolPricingStrategy('lowest'); $cartItem = $this->cart->addToCart($this->poolProduct, 1); $this->assertEquals(2000, $cartItem->price); // Lowest: 20.00 $this->assertEquals(2000, $cartItem->subtotal); } /** @test */ public function it_uses_highest_pricing_strategy_with_mixed_single_item_prices() { // Set prices on single items (20€ and 50€) ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem1->id, 'purchasable_type' => Product::class, 'unit_amount' => 2000, // 20.00 'currency' => 'USD', 'is_default' => true, ]); ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem2->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, // 50.00 'currency' => 'USD', 'is_default' => true, ]); // Set pricing strategy to highest $this->poolProduct->setPoolPricingStrategy('highest'); $cartItem = $this->cart->addToCart($this->poolProduct, 1); $this->assertEquals(5000, $cartItem->price); // Highest: 50.00 $this->assertEquals(5000, $cartItem->subtotal); } /** @test */ public function it_uses_lowest_pricing_strategy_with_booking_dates() { // Set prices on single items (20€ and 50€) ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem1->id, 'purchasable_type' => Product::class, 'unit_amount' => 2000, // 20.00 per day 'currency' => 'USD', 'is_default' => true, ]); ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem2->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, // 50.00 per day 'currency' => 'USD', 'is_default' => true, ]); // Set pricing strategy to lowest $this->poolProduct->setPoolPricingStrategy('lowest'); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(4)->startOfDay(); // 3 days $cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); $this->assertEquals(6000, $cartItem->price); // 20.00 × 3 days $this->assertEquals(6000, $cartItem->subtotal); } /** @test */ public function it_adds_regular_product_to_cart_without_dates() { $regularProduct = Product::factory()->create([ 'name' => 'Regular Product', 'type' => ProductType::SIMPLE, 'manage_stock' => true, ]); $regularProduct->increaseStock(10); ProductPrice::factory()->create([ 'purchasable_id' => $regularProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 1500, // 15.00 'currency' => 'USD', 'is_default' => true, ]); $cartItem = $this->cart->addToCart($regularProduct, 2); $this->assertEquals(2, $cartItem->quantity); $this->assertEquals(1500, $cartItem->price); $this->assertEquals(3000, $cartItem->subtotal); $this->assertNull($cartItem->from); $this->assertNull($cartItem->until); } /** @test */ public function it_increases_quantity_when_adding_same_pool_product_with_same_dates() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(3)->startOfDay(); $cartItem1 = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); $cartItem2 = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); $this->assertEquals($cartItem1->id, $cartItem2->id); $this->assertEquals(2, $cartItem2->quantity); $this->assertEquals(12000, $cartItem2->subtotal); // 3000 × 2 days × 2 units } /** @test */ public function it_creates_separate_cart_items_for_same_pool_with_different_dates() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $from1 = Carbon::now()->addDays(1)->startOfDay(); $until1 = Carbon::now()->addDays(3)->startOfDay(); $from2 = Carbon::now()->addDays(5)->startOfDay(); $until2 = Carbon::now()->addDays(7)->startOfDay(); $cartItem1 = $this->cart->addToCart($this->poolProduct, 1, [], $from1, $until1); $cartItem2 = $this->cart->addToCart($this->poolProduct, 1, [], $from2, $until2); $this->assertNotEquals($cartItem1->id, $cartItem2->id); $this->assertEquals(1, $cartItem1->quantity); $this->assertEquals(1, $cartItem2->quantity); $this->assertEquals(2, $this->cart->items()->count()); } /** @test */ public function it_calculates_correct_total_for_cart_with_multiple_pool_items() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 2500, // 25.00 per day 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(4)->startOfDay(); // 3 days // Add 2 units $this->cart->addToCart($this->poolProduct, 2, [], $from, $until); $total = $this->cart->getTotal(); // 25.00 × 3 days × 2 units = 150.00 $this->assertEquals(15000, $total); } /** @test */ public function it_handles_pool_with_sale_price() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, // 50.00 'sale_unit_amount' => 3000, // 30.00 (sale) 'currency' => 'USD', 'is_default' => true, ]); // Set sale period $this->poolProduct->update([ 'sale_start' => now()->subDay(), 'sale_end' => now()->addDay(), ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(3)->startOfDay(); // 2 days $cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); $this->assertEquals(6000, $cartItem->price); // 30.00 × 2 days (sale price) $this->assertEquals(10000, $cartItem->regular_price); // 50.00 × 2 days (regular price) } /** @test */ public function it_handles_pool_with_inherited_sale_price() { ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem1->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, 'sale_unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); ProductPrice::factory()->create([ 'purchasable_id' => $this->singleItem2->id, 'purchasable_type' => Product::class, 'unit_amount' => 7000, 'sale_unit_amount' => 5000, 'currency' => 'USD', 'is_default' => true, ]); // Set sale period on single items $this->singleItem1->update([ 'sale_start' => now()->subDay(), 'sale_end' => now()->addDay(), ]); $this->singleItem2->update([ 'sale_start' => now()->subDay(), 'sale_end' => now()->addDay(), ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(2)->startOfDay(); // 1 day // Set pricing strategy to average $this->poolProduct->setPricingStrategy(PricingStrategy::AVERAGE); $cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); // Average sale price: (3000 + 5000) / 2 = 4000 per day $this->assertEquals(4000, $cartItem->price); // Average regular price: (5000 + 7000) / 2 = 6000 per day $this->assertEquals(6000, $cartItem->regular_price); } /** @test */ public function it_handles_zero_days_as_one_day_minimum() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); // Same day booking (0 days diff) $from = Carbon::now()->addDays(1)->setTime(10, 0); $until = Carbon::now()->addDays(1)->setTime(14, 0); $cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); // Should treat as minimum 1 day $this->assertEquals(3000, $cartItem->price); // 30.00 × 1 day } /** @test */ public function it_throws_exception_when_adding_pool_without_any_pricing() { // Pool with no direct price and single items with no prices $pool = Product::factory()->create([ 'name' => 'Pool Without Pricing', 'type' => ProductType::POOL, 'manage_stock' => false, ]); $spot = Product::factory()->create([ 'name' => 'Spot Without Price', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $spot->increaseStock(1); $pool->productRelations()->attach($spot->id, [ 'type' => ProductRelationType::SINGLE->value, ]); $this->expectException(\Blax\Shop\Exceptions\HasNoPriceException::class); $this->cart->addToCart($pool, 1); } /** @test */ public function it_throws_exception_when_pool_not_available_for_booking_period() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(3)->startOfDay(); // Claim all single items for the period $this->singleItem1->claimStock(1, null, $from, $until); $this->singleItem2->claimStock(1, null, $from, $until); // Try to add pool for same period $this->expectException(\Blax\Shop\Exceptions\NotEnoughStockException::class); $this->expectExceptionMessage('has only 0 items available'); $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); } /** @test */ public function it_throws_exception_when_booking_product_not_available_for_period() { $bookingProduct = Product::factory()->create([ 'name' => 'Meeting Room', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $bookingProduct->increaseStock(1); ProductPrice::factory()->create([ 'purchasable_id' => $bookingProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(3)->startOfDay(); // Claim the booking product for the period $bookingProduct->claimStock(1, null, $from, $until); // Try to add for overlapping period $this->expectException(\Blax\Shop\Exceptions\NotEnoughStockException::class); $this->expectExceptionMessage('not available for the requested period'); $this->cart->addToCart($bookingProduct, 1, [], $from, $until); } /** @test */ public function it_throws_exception_when_only_from_date_provided() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1); $this->expectException(\Exception::class); $this->expectExceptionMessage("Both 'from' and 'until' dates must be provided together"); $this->cart->addToCart($this->poolProduct, 1, [], $from, null); } /** @test */ public function it_throws_exception_when_only_until_date_provided() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $until = Carbon::now()->addDays(3); $this->expectException(\Exception::class); $this->expectExceptionMessage("Both 'from' and 'until' dates must be provided together"); $this->cart->addToCart($this->poolProduct, 1, [], null, $until); } /** @test */ public function it_throws_exception_when_from_is_after_until() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(5); $until = Carbon::now()->addDays(2); // Before from $this->expectException(\Exception::class); $this->expectExceptionMessage("'from' date must be before the 'until' date"); $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); } /** @test */ public function it_throws_exception_when_from_equals_until() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $date = Carbon::now()->addDays(1); $this->expectException(\Exception::class); $this->expectExceptionMessage("'from' date must be before the 'until' date"); $this->cart->addToCart($this->poolProduct, 1, [], $date, $date); } /** @test */ public function it_creates_separate_items_for_same_product_same_dates_different_parameters() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1); $until = Carbon::now()->addDays(3); $cartItem1 = $this->cart->addToCart($this->poolProduct, 1, ['zone' => 'A'], $from, $until); $cartItem2 = $this->cart->addToCart($this->poolProduct, 1, ['zone' => 'B'], $from, $until); $this->assertNotEquals($cartItem1->id, $cartItem2->id); $this->assertEquals(2, $this->cart->items()->count()); $this->assertEquals(['zone' => 'A'], $cartItem1->parameters); $this->assertEquals(['zone' => 'B'], $cartItem2->parameters); } /** @test */ public function it_throws_exception_when_pool_quantity_exceeds_available_items() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1); $until = Carbon::now()->addDays(3); // Pool has 2 single items, try to add 3 (with dates to check availability) $this->expectException(\Blax\Shop\Exceptions\NotEnoughStockException::class); $this->expectExceptionMessage('has only 2 items available'); $this->cart->addToCart($this->poolProduct, 3, [], $from, $until); } /** @test */ public function it_handles_partial_pool_availability() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1); $until = Carbon::now()->addDays(3); // Claim one of the two single items $this->singleItem1->claimStock(1, null, $from, $until); // Should be able to add 1 (one spot still available) $cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until); $this->assertNotNull($cartItem); // But not 2 $this->expectException(\Blax\Shop\Exceptions\NotEnoughStockException::class); $this->cart->addToCart($this->poolProduct, 2, [], $from, $until); } /** @test */ public function it_throws_exception_for_regular_product_without_price() { $regularProduct = Product::factory()->create([ 'name' => 'Product Without Price', 'type' => ProductType::SIMPLE, 'manage_stock' => true, ]); $regularProduct->increaseStock(10); $this->expectException(\Blax\Shop\Exceptions\HasNoPriceException::class); $this->cart->addToCart($regularProduct, 1); } /** @test */ public function it_allows_adding_booking_product_without_dates() { $bookingProduct = Product::factory()->create([ 'name' => 'Meeting Room', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $bookingProduct->increaseStock(5); ProductPrice::factory()->create([ 'purchasable_id' => $bookingProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, 'currency' => 'USD', 'is_default' => true, ]); // Should be able to add without dates $cartItem = $this->cart->addToCart($bookingProduct, 1); $this->assertNotNull($cartItem); $this->assertEquals($bookingProduct->id, $cartItem->purchasable_id); $this->assertNull($cartItem->from); $this->assertNull($cartItem->until); $this->assertEquals(5000, $cartItem->price); // 1 day default } /** @test */ public function it_allows_adding_pool_product_without_dates() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); // Should be able to add without dates $cartItem = $this->cart->addToCart($this->poolProduct, 1); $this->assertNotNull($cartItem); $this->assertEquals($this->poolProduct->id, $cartItem->purchasable_id); $this->assertNull($cartItem->from); $this->assertNull($cartItem->until); $this->assertEquals(3000, $cartItem->price); // 1 day default } /** @test */ public function it_allows_updating_cart_item_dates_later() { ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); // Add without dates $cartItem = $this->cart->addToCart($this->poolProduct, 1); $this->assertNull($cartItem->from); $this->assertNull($cartItem->until); // Update with dates $from = Carbon::now()->addDays(1); $until = Carbon::now()->addDays(3); $cartItem->update([ 'from' => $from, 'until' => $until, 'price' => 3000 * 2, // 2 days 'subtotal' => 3000 * 2 * 1, // 2 days × 1 quantity ]); $cartItem->refresh(); $this->assertEquals($from->format('Y-m-d H:i:s'), $cartItem->from->format('Y-m-d H:i:s')); $this->assertEquals($until->format('Y-m-d H:i:s'), $cartItem->until->format('Y-m-d H:i:s')); $this->assertEquals(6000, $cartItem->price); } /** @test */ public function it_limits_pool_in_cart_quantity_by_single_products() { // Set price on pool ProductPrice::factory()->create([ 'purchasable_id' => $this->poolProduct->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); // Assert cart is empty $this->assertEquals(0, $this->cart->items()->count()); // Assert poolProduct has quantity availability of 2 (based on 2 single items) $availableQuantity = $this->poolProduct->getAvailableQuantity(); $this->assertEquals(2, $availableQuantity); // Adding 2 pool items should succeed (without dates) $cartItem = $this->cart->addToCart($this->poolProduct, 2); $this->assertNotNull($cartItem); $this->assertEquals(2, $cartItem->quantity); // Try to add 1 more (total would be 3, but only 2 available) $this->expectException(\Blax\Shop\Exceptions\NotEnoughStockException::class); $this->expectExceptionMessage('has only 2 items available'); $this->cart->addToCart($this->poolProduct, 1); } /** @test */ public function it_counts_single_item_stock_quantities_in_pool_availability() { // Create a pool with multiple single items having different stock quantities $pool = Product::factory()->create([ 'name' => 'Large Parking Pool', 'type' => ProductType::POOL, 'manage_stock' => false, ]); // Create single items with different stock quantities $spot1 = Product::factory()->create([ 'name' => 'Standard Spot', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $spot1->increaseStock(5); // 5 units available $spot2 = Product::factory()->create([ 'name' => 'Premium Spot', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $spot2->increaseStock(3); // 3 units available $spot3 = Product::factory()->create([ 'name' => 'VIP Spot', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $spot3->increaseStock(2); // 2 units available // Attach single items to pool $pool->attachSingleItems([$spot1->id, $spot2->id, $spot3->id]); // Pool should have availability = 5 + 3 + 2 = 10 $availableQuantity = $pool->getAvailableQuantity(); $this->assertEquals(10, $availableQuantity); // Set price on pool ProductPrice::factory()->create([ 'purchasable_id' => $pool->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); $cart = Cart::factory()->create([ 'customer_id' => $this->user->id, 'customer_type' => get_class($this->user), ]); // Should be able to add 10 pool items $cartItem = $cart->addToCart($pool, 10); $this->assertNotNull($cartItem); $this->assertEquals(10, $cartItem->quantity); // But not 11 $this->expectException(\Blax\Shop\Exceptions\NotEnoughStockException::class); $this->expectExceptionMessage('has only 10 items available'); $cart->addToCart($pool, 1); } /** @test */ public function it_counts_available_stock_with_booking_dates() { // Create pool with single items having stock $pool = Product::factory()->create([ 'name' => 'Conference Room Pool', 'type' => ProductType::POOL, 'manage_stock' => false, ]); $room1 = Product::factory()->create([ 'name' => 'Room A', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $room1->increaseStock(3); $room2 = Product::factory()->create([ 'name' => 'Room B', 'type' => ProductType::BOOKING, 'manage_stock' => true, ]); $room2->increaseStock(2); $pool->attachSingleItems([$room1->id, $room2->id]); ProductPrice::factory()->create([ 'purchasable_id' => $pool->id, 'purchasable_type' => Product::class, 'unit_amount' => 5000, 'currency' => 'USD', 'is_default' => true, ]); $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(2)->startOfDay(); // Total availability should be 3 + 2 = 5 $this->assertEquals(5, $pool->getAvailableQuantity($from, $until)); $cart = Cart::factory()->create([ 'customer_id' => $this->user->id, 'customer_type' => get_class($this->user), ]); // Should be able to book 5 pool items for the period $cartItem = $cart->addToCart($pool, 5, [], $from, $until); $this->assertNotNull($cartItem); $this->assertEquals(5, $cartItem->quantity); } /** @test */ public function it_allows_unlimited_pool_when_single_items_dont_manage_stock() { // Create pool with single items that don't manage stock $pool = Product::factory()->create([ 'name' => 'Unlimited Parking Pool', 'type' => ProductType::POOL, 'manage_stock' => false, ]); $spot1 = Product::factory()->create([ 'name' => 'Unlimited Spot 1', 'type' => ProductType::BOOKING, 'manage_stock' => false, // No stock management ]); $spot2 = Product::factory()->create([ 'name' => 'Unlimited Spot 2', 'type' => ProductType::BOOKING, 'manage_stock' => false, // No stock management ]); $pool->attachSingleItems([$spot1->id, $spot2->id]); ProductPrice::factory()->create([ 'purchasable_id' => $pool->id, 'purchasable_type' => Product::class, 'unit_amount' => 3000, 'currency' => 'USD', 'is_default' => true, ]); // Pool should have unlimited availability $this->assertEquals(PHP_INT_MAX, $pool->getAvailableQuantity()); $cart = Cart::factory()->create([ 'customer_id' => $this->user->id, 'customer_type' => get_class($this->user), ]); // Should be able to add any quantity without dates $cartItem = $cart->addToCart($pool, 1000); $this->assertNotNull($cartItem); $this->assertEquals(1000, $cartItem->quantity); // And with dates $from = Carbon::now()->addDays(1)->startOfDay(); $until = Carbon::now()->addDays(2)->startOfDay(); $cartItem2 = $cart->addToCart($pool, 500, [], $from, $until); $this->assertNotNull($cartItem2); $this->assertEquals(500, $cartItem2->quantity); } /** @test */ public function it_picks_correct_price_for_pool_and_items_and_respects_stocks() { $this->actingAs($this->user); $pool = Product::factory() ->withPrices(1, 5000) // 50€ ->create([ 'name' => 'Parking Pool', 'type' => ProductType::POOL ]); $spot1 = Product::factory() ->withStocks(2) ->withPrices(1, 2000) // 20€ ->create([ 'name' => 'Spot 1', 'type' => ProductType::BOOKING, ]); $spot2 = Product::factory() ->withStocks(2) ->create([ 'name' => 'Spot 2', 'type' => ProductType::BOOKING, ]); $spot3 = Product::factory() ->withStocks(2) ->withPrices(1, 8000) // 80€ ->create([ 'name' => 'Spot 3', 'type' => ProductType::BOOKING, ]); $pool->attachSingleItems([ $spot1->id, $spot2->id, $spot3->id ]); // Pool should have unlimited availability $this->assertEquals(6, $pool->getAvailableQuantity()); $pool->setPoolPricingStrategy('lowest'); $cart = $this->user->currentCart(); $this->assertEquals(0, $cart->items()->count()); $this->assertThrows( fn() => $cartItem = $cart->addToCart($pool, 1000), \Blax\Shop\Exceptions\NotEnoughStockException::class ); // 1. Addition $this->assertEquals(2000, $pool->getCurrentPrice(cart: $cart)); // 20.00 $this->assertEquals(2000, $pool->getLowestAvailablePoolPrice(cart: $cart)); // 20.00 $this->assertEquals(8000, $pool->getHighestAvailablePoolPrice(cart: $cart)); // 80.00 $cartItem = $cart->addToCart($pool, 1); $this->assertNotNull($cartItem); // 2. Addition $this->assertEquals(2000, $pool->getCurrentPrice()); // 20.00 $this->assertEquals(2000, $pool->getLowestAvailablePoolPrice()); // 20.00 $this->assertEquals(8000, $pool->getHighestAvailablePoolPrice()); // 80.00 $cartItem = $cart->addToCart($pool, 1); $this->assertNotNull($cartItem); $this->assertEquals(4000, $cartItem->subtotal); // 20.00 × 2 // 3. Addition $this->assertEquals(5000, $pool->getCurrentPrice(cart: $cart)); // 50.00 $this->assertEquals(5000, $pool->getLowestAvailablePoolPrice(cart: $cart)); // 50.00 $this->assertEquals(8000, $pool->getHighestAvailablePoolPrice(cart: $cart)); // 80.00 $cartItem = $cart->addToCart($pool, 1); $this->assertNotNull($cartItem); $this->assertEquals(5000, $cartItem->price); // Next lowest (inherited from pool): 50.00 $this->assertEquals(5000, $cartItem->subtotal); // 50.00 (not cumulative) // 4. Addition $this->assertEquals(5000, $pool->getCurrentPrice()); // 50.00 $this->assertEquals(5000, $pool->getLowestAvailablePoolPrice()); // 50.00 $this->assertEquals(8000, $pool->getHighestAvailablePoolPrice()); // 80.00 $cartItem = $cart->addToCart($pool, 1); $this->assertNotNull($cartItem); $this->assertEquals(5000, $cartItem->price); // Next lowest (inherited from pool): 50.00 $this->assertEquals(10000, $cartItem->subtotal); // 50.00 × 2 (merged) // 5. Addition $this->assertEquals(8000, $pool->getCurrentPrice(cart: $cart)); // 80.00 $this->assertEquals(8000, $pool->getLowestAvailablePoolPrice(cart: $cart)); // 80.00 $this->assertEquals(8000, $pool->getHighestAvailablePoolPrice(cart: $cart)); // 80.00 $cartItem = $cart->addToCart($pool, 1); $this->assertNotNull($cartItem); $this->assertEquals(8000, $cartItem->price); // Next lowest: 80.00 $this->assertEquals(8000, $cartItem->subtotal); // 80.00 // 6. Addition $this->assertEquals(8000, $pool->getCurrentPrice()); // 80.00 $this->assertEquals(8000, $pool->getLowestAvailablePoolPrice()); // 80.00 $this->assertEquals(8000, $pool->getHighestAvailablePoolPrice()); // 80.00 $cartItem = $cart->addToCart($pool, 1); $this->assertNotNull($cartItem); $this->assertEquals(8000, $cartItem->price); // Next lowest: 80.00 $this->assertEquals(16000, $cartItem->subtotal); // 80.00 × 2 (merged) $this->assertEquals(3, $cart->items()->count()); $this->assertNull($pool->getCurrentPrice()); $this->assertNull($pool->getLowestAvailablePoolPrice()); $this->assertNull($pool->getHighestAvailablePoolPrice()); $this->assertNull($pool->getCurrentPrice(cart: $cart)); $this->assertNull($pool->getLowestAvailablePoolPrice(cart: $cart)); $this->assertNull($pool->getHighestAvailablePoolPrice(cart: $cart)); // 7. Addition $this->assertThrows( fn() => $cart->addToCart($pool, 1), \Blax\Shop\Exceptions\NotEnoughStockException::class ); } /** @test */ public function it_picks_correct_price_respects_stocks_respects_timespan_for_price() { $this->actingAs($this->user); $pool = Product::factory() ->withPrices(1, 5000) // 50€ ->create([ 'name' => 'Parking Pool', 'type' => ProductType::POOL ]); $spot1 = Product::factory() ->withStocks(2) ->withPrices(1, 2000) // 20€ ->create([ 'name' => 'Spot 1', 'type' => ProductType::BOOKING, ]); $spot2 = Product::factory() ->withStocks(2) ->create([ 'name' => 'Spot 2', 'type' => ProductType::BOOKING, ]); $spot3 = Product::factory() ->withStocks(2) ->withPrices(1, 8000) // 80€ ->create([ 'name' => 'Spot 3', 'type' => ProductType::BOOKING, ]); $pool->attachSingleItems([ $spot1->id, $spot2->id, $spot3->id ]); // Pool should have unlimited availability $this->assertEquals(6, $pool->getAvailableQuantity()); $pool->setPoolPricingStrategy('lowest'); $cart = $this->user->currentCart(); $this->assertEquals(0, $cart->items()->count()); $this->assertThrows( fn() => $cartItem = $cart->addToCart($pool, 1000), \Blax\Shop\Exceptions\NotEnoughStockException::class ); $cart->addToCart( $pool, 3, [], now()->addWeek(), now()->addWeek()->addDays(5) ); $this->assertEquals( (2000 * 2 * 5) + (5000 * 1 * 5), $cart->getTotal() ); $cart->addToCart( $pool, 3, [], now()->addWeek(), now()->addWeek()->addDays(5) ); $this->assertEquals( (2000 * 2 * 5) + (5000 * 2 * 5) + (8000 * 2 * 5), $cart->getTotal() ); $this->assertEquals(3, $cart->items()->count()); // Clear cart $cart->items()->delete(); $this->assertEquals(0, $cart->items()->count()); $this->assertEquals(0, $cart->getTotal()); // Make one spot unavailable for part of the period $spot2->adjustStock( StockType::CLAIMED, 1, from: now()->addWeek()->addDays(2), until: now()->addWeek()->addDays(3) ); $this->assertThrows( fn() => $cart->addToCart( $pool, 6, [], now()->addWeek(), now()->addWeek()->addDays(5) ), \Blax\Shop\Exceptions\NotEnoughStockException::class ); $cart->addToCart( $pool, 5, [], now()->addWeek(), now()->addWeek()->addDays(5) ); $this->assertEquals( (2000 * 2 * 5) + (5000 * 1 * 5) + (8000 * 2 * 5), $cart->getTotal() ); } }