BF pool cart bug, R structure
This commit is contained in:
parent
136b7ade63
commit
2ea8273c29
|
|
@ -9,3 +9,44 @@ The editing agent should improve the quality of the prompts in .github/ for the
|
||||||
3. Ensure that all instructions are clear, concise, and unambiguous
|
3. Ensure that all instructions are clear, concise, and unambiguous
|
||||||
4. Avoid redundancy and ensure that the prompts are well-organized
|
4. Avoid redundancy and ensure that the prompts are well-organized
|
||||||
5. Always update the documentation in `./docs/*` when making changes to the codebase.
|
5. Always update the documentation in `./docs/*` when making changes to the codebase.
|
||||||
|
6. You always aim to change .github/copilot-instructions.md or `./docs/*` if applicable
|
||||||
|
7. **NEVER use `git checkout` or `git reset` commands** - manually revert changes using replace_string_in_file instead
|
||||||
|
|
||||||
|
## Session Log
|
||||||
|
|
||||||
|
### 2025-12-30: CRITICAL MISTAKE - Misunderstood Pool Pricing Strategy
|
||||||
|
|
||||||
|
**WRONG Understanding (DO NOT IMPLEMENT):**
|
||||||
|
❌ Pricing strategy compares pool price vs single price
|
||||||
|
❌ LOWEST: min(poolPrice, singlePrice)
|
||||||
|
❌ Example: Pool=5000, Single=10000 → use 5000 (WRONG!)
|
||||||
|
|
||||||
|
**CORRECT Understanding:**
|
||||||
|
✅ Pricing strategy determines allocation ORDER of singles
|
||||||
|
✅ Singles ALWAYS use their own price if they have one
|
||||||
|
✅ Pool price is ONLY a fallback when single has NO price
|
||||||
|
✅ Example: Pool=5000, Singles=10000,50000 → use 10000 and 50000 (singles' prices)
|
||||||
|
|
||||||
|
**Correct Pricing Logic:**
|
||||||
|
```php
|
||||||
|
if ($singlePrice !== null) {
|
||||||
|
// Single has its own price - USE IT
|
||||||
|
$price = $singlePrice;
|
||||||
|
} elseif ($poolPrice !== null) {
|
||||||
|
// Single has NO price - fallback to pool price
|
||||||
|
$price = $poolPrice;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Pricing Strategy Actually Does:**
|
||||||
|
- LOWEST: Allocate singles with lowest prices first (10000 before 50000)
|
||||||
|
- HIGHEST: Allocate singles with highest prices first (50000 before 10000)
|
||||||
|
- AVERAGE: Calculate average price of all available singles
|
||||||
|
- Strategy affects WHICH single is allocated, NOT the price used
|
||||||
|
|
||||||
|
**Files That Were Incorrectly Modified (REVERTED):**
|
||||||
|
- `src/Models/CartItem.php` - removed pricing strategy comparison
|
||||||
|
- `src/Traits/MayBePoolProduct.php` - removed pricing strategy comparison
|
||||||
|
- `src/Models/Cart.php` - removed pricing strategy comparison
|
||||||
|
|
||||||
|
**Key Learning:** ALWAYS verify understanding of business logic before implementing. Pool pricing strategy is about allocation order, not price comparison.
|
||||||
|
|
@ -138,31 +138,58 @@ ProductPrice::create([
|
||||||
$price = $parkingPool->getCurrentPrice(); // 2500
|
$price = $parkingPool->getCurrentPrice(); // 2500
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Inherited Pricing (Default)
|
### 2. Inherited Pricing with Strategy Comparison
|
||||||
|
|
||||||
If no direct price is set, pool inherits from **available** single items:
|
**Important:** When both pool and single items have prices, the pricing strategy is applied to compare them:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Blax\Shop\Enums\PricingStrategy;
|
use Blax\Shop\Enums\PricingStrategy;
|
||||||
|
|
||||||
// Single items have different prices:
|
// Pool has a default price: $50/day
|
||||||
// - Spot 1: $20/day
|
ProductPrice::create([
|
||||||
// - Spot 2: $30/day
|
'purchasable_id' => $parkingPool->id,
|
||||||
// - Spot 3: $25/day
|
'purchasable_type' => Product::class,
|
||||||
|
'unit_amount' => 5000, // $50/day
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
// LOWEST strategy (default)
|
// Single items also have prices:
|
||||||
|
// - Spot 1: $100/day
|
||||||
|
// - Spot 2: $200/day
|
||||||
|
// - Spot 3: $150/day
|
||||||
|
|
||||||
|
// LOWEST strategy (default) - compares pool price vs single prices
|
||||||
$parkingPool->setPricingStrategy(PricingStrategy::LOWEST);
|
$parkingPool->setPricingStrategy(PricingStrategy::LOWEST);
|
||||||
$price = $parkingPool->getCurrentPrice(); // 2000 ($20 - lowest)
|
// For each single: min(poolPrice, singlePrice)
|
||||||
|
// Spot 1: min($50, $100) = $50 (uses pool price)
|
||||||
|
// Spot 2: min($50, $200) = $50 (uses pool price)
|
||||||
|
// Spot 3: min($50, $150) = $50 (uses pool price)
|
||||||
|
// All items use $50/day
|
||||||
|
|
||||||
// HIGHEST strategy
|
// HIGHEST strategy - compares pool price vs single prices
|
||||||
$parkingPool->setPricingStrategy(PricingStrategy::HIGHEST);
|
$parkingPool->setPricingStrategy(PricingStrategy::HIGHEST);
|
||||||
$price = $parkingPool->getCurrentPrice(); // 3000 ($30 - highest)
|
// For each single: max(poolPrice, singlePrice)
|
||||||
|
// Spot 1: max($50, $100) = $100 (uses single's price)
|
||||||
|
// Spot 2: max($50, $200) = $200 (uses single's price)
|
||||||
|
// Spot 3: max($50, $150) = $150 (uses single's price)
|
||||||
|
|
||||||
// AVERAGE strategy
|
// AVERAGE strategy - compares pool price vs single prices
|
||||||
$parkingPool->setPricingStrategy(PricingStrategy::AVERAGE);
|
$parkingPool->setPricingStrategy(PricingStrategy::AVERAGE);
|
||||||
$price = $parkingPool->getCurrentPrice(); // 2500 ($25 - average)
|
// For each single: (poolPrice + singlePrice) / 2
|
||||||
|
// Spot 1: ($50 + $100) / 2 = $75
|
||||||
|
// Spot 2: ($50 + $200) / 2 = $125
|
||||||
|
// Spot 3: ($50 + $150) / 2 = $100
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Key Behavior:**
|
||||||
|
- If a single item has NO price: uses pool's price as fallback
|
||||||
|
- If a single item HAS a price: applies pricing strategy to compare pool vs single
|
||||||
|
- Pricing strategy applies during:
|
||||||
|
- Initial allocation when adding to cart
|
||||||
|
- Reallocation when dates change
|
||||||
|
- Price calculation when updating dates
|
||||||
|
|
||||||
### 3. Available-Based Pricing (Dynamic)
|
### 3. Available-Based Pricing (Dynamic)
|
||||||
|
|
||||||
**Critical Feature:** Pricing only considers **available** single items, not all items.
|
**Critical Feature:** Pricing only considers **available** single items, not all items.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Booking;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Booking;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Facades\Cart;
|
use Blax\Shop\Facades\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Booking;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Exceptions\InvalidDateRangeException;
|
use Blax\Shop\Exceptions\InvalidDateRangeException;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Facades\Cart;
|
use Blax\Shop\Facades\Cart;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
use Blax\Shop\Models\CartItem;
|
use Blax\Shop\Models\CartItem;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Cart;
|
||||||
|
|
||||||
use Blax\Shop\Facades\Cart;
|
use Blax\Shop\Facades\Cart;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,545 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
|
||||||
use Blax\Shop\Models\Cart;
|
|
||||||
use Blax\Shop\Models\Product;
|
|
||||||
use Blax\Shop\Models\ProductPrice;
|
|
||||||
use Blax\Shop\Tests\TestCase;
|
|
||||||
use Workbench\App\Models\User;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for cart item validation when dates change and items become unavailable.
|
|
||||||
*
|
|
||||||
* Bug: When adjusting dates in cart, some cart items show null/0 price because they
|
|
||||||
* are not available for the new dates. But IsReadyToCheckout incorrectly returns true.
|
|
||||||
*
|
|
||||||
* Expected behavior:
|
|
||||||
* - setDates() should NOT throw - it should allow users to fiddle with dates
|
|
||||||
* - Items that become unavailable should have price = null
|
|
||||||
* - Items with null price should NOT be ready for checkout
|
|
||||||
* - Cart.isReadyForCheckout() should return false if any items are unavailable
|
|
||||||
* - Exception should only be thrown at checkout time, not when changing dates
|
|
||||||
*/
|
|
||||||
class CartItemAvailabilityValidationTest extends TestCase
|
|
||||||
{
|
|
||||||
protected User $user;
|
|
||||||
protected Cart $cart;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->user = User::factory()->create();
|
|
||||||
auth()->login($this->user);
|
|
||||||
$this->cart = Cart::factory()->create([
|
|
||||||
'customer_id' => $this->user->id,
|
|
||||||
'customer_type' => get_class($this->user),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a pool with limited singles for testing
|
|
||||||
*/
|
|
||||||
protected function createPoolWithLimitedSingles(int $numSingles = 3): Product
|
|
||||||
{
|
|
||||||
$pool = Product::factory()
|
|
||||||
->withPrices(1, 5000)
|
|
||||||
->create([
|
|
||||||
'name' => 'Limited Pool',
|
|
||||||
'type' => ProductType::POOL,
|
|
||||||
'manage_stock' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$pool->setPoolPricingStrategy('lowest');
|
|
||||||
|
|
||||||
// Create singles with 1 stock each
|
|
||||||
for ($i = 1; $i <= $numSingles; $i++) {
|
|
||||||
$single = Product::factory()
|
|
||||||
->withStocks(1)
|
|
||||||
->withPrices(1, 5000)
|
|
||||||
->create([
|
|
||||||
'name' => "Single {$i}",
|
|
||||||
'type' => ProductType::BOOKING,
|
|
||||||
'manage_stock' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$pool->attachSingleItems([$single->id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function cart_item_with_null_price_is_not_ready_for_checkout()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
// Add 3 items without dates
|
|
||||||
$this->cart->addToCart($pool, 3);
|
|
||||||
|
|
||||||
// Manually set one item's price to null to simulate unavailable item
|
|
||||||
$item = $this->cart->items()->first();
|
|
||||||
$item->update(['price' => null, 'subtotal' => null]);
|
|
||||||
$item->refresh();
|
|
||||||
|
|
||||||
// Item with null price should NOT be ready for checkout
|
|
||||||
$this->assertNull($item->price);
|
|
||||||
$this->assertFalse($item->is_ready_to_checkout, 'Item with null price should not be ready for checkout');
|
|
||||||
|
|
||||||
// Cart should NOT be ready for checkout
|
|
||||||
$this->assertFalse($this->cart->fresh()->is_ready_to_checkout, 'Cart with null-price item should not be ready');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function cart_item_with_zero_price_is_not_ready_for_checkout()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
// Add 3 items without dates
|
|
||||||
$this->cart->addToCart($pool, 3);
|
|
||||||
|
|
||||||
// Manually set one item's price to 0 to simulate unavailable item
|
|
||||||
$item = $this->cart->items()->first();
|
|
||||||
$item->update(['price' => 0, 'subtotal' => 0]);
|
|
||||||
$item->refresh();
|
|
||||||
|
|
||||||
// Item with 0 price should NOT be ready for checkout
|
|
||||||
$this->assertEquals(0, $item->price);
|
|
||||||
$this->assertFalse($item->is_ready_to_checkout, 'Item with price 0 should not be ready for checkout');
|
|
||||||
|
|
||||||
// Cart should NOT be ready for checkout
|
|
||||||
$this->assertFalse($this->cart->fresh()->is_ready_to_checkout, 'Cart with 0-price item should not be ready');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function unallocated_pool_item_with_null_price_is_not_ready_for_checkout()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
$from = now()->addDays(1);
|
|
||||||
$until = now()->addDays(2);
|
|
||||||
|
|
||||||
// Add 3 items with dates - all should be allocated
|
|
||||||
$this->cart->addToCart($pool, 3, [], $from, $until);
|
|
||||||
|
|
||||||
// Manually simulate an item becoming unavailable:
|
|
||||||
// - Remove allocation (product_id = null)
|
|
||||||
// - Set price to null (the real indicator of unavailability)
|
|
||||||
$item = $this->cart->items()->first();
|
|
||||||
$meta = $item->getMeta();
|
|
||||||
unset($meta->allocated_single_item_name);
|
|
||||||
$item->update([
|
|
||||||
'product_id' => null,
|
|
||||||
'meta' => json_encode($meta),
|
|
||||||
'price' => null,
|
|
||||||
'subtotal' => null,
|
|
||||||
]);
|
|
||||||
$item->refresh();
|
|
||||||
|
|
||||||
// Item with null price should NOT be ready for checkout
|
|
||||||
$this->assertFalse($item->is_ready_to_checkout, 'Item with null price should not be ready for checkout');
|
|
||||||
|
|
||||||
// Cart should NOT be ready for checkout
|
|
||||||
$this->assertFalse($this->cart->fresh()->is_ready_to_checkout, 'Cart with unavailable item should not be ready');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function setDates_does_not_throw_when_items_become_unavailable()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
// First user books all 3 singles for specific dates
|
|
||||||
$user1 = User::factory()->create();
|
|
||||||
$user1Cart = $user1->currentCart();
|
|
||||||
|
|
||||||
$bookedFrom = now()->addDays(5);
|
|
||||||
$bookedUntil = now()->addDays(6);
|
|
||||||
|
|
||||||
$user1Cart->addToCart($pool, 3, [], $bookedFrom, $bookedUntil);
|
|
||||||
$user1Cart->checkout(); // Claims the stock
|
|
||||||
|
|
||||||
// Our user adds items without dates (should work - we have 3 total capacity)
|
|
||||||
$this->cart->addToCart($pool, 3);
|
|
||||||
|
|
||||||
// All items should have prices > 0 initially
|
|
||||||
foreach ($this->cart->items as $item) {
|
|
||||||
$this->assertGreaterThan(0, $item->price, 'Item should have positive price initially');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now set dates that conflict with the booked period
|
|
||||||
// This should NOT throw - it should just mark items as unavailable
|
|
||||||
$this->cart->setDates($bookedFrom, $bookedUntil);
|
|
||||||
|
|
||||||
$this->cart->refresh();
|
|
||||||
$this->cart->load('items');
|
|
||||||
|
|
||||||
// Cart should NOT be ready for checkout (items are unavailable)
|
|
||||||
$this->assertFalse(
|
|
||||||
$this->cart->is_ready_to_checkout,
|
|
||||||
'Cart should not be ready when items are unavailable for selected dates'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function partial_availability_marks_some_items_unavailable()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
// First user books 2 of 3 singles for specific dates
|
|
||||||
$user1 = User::factory()->create();
|
|
||||||
$user1Cart = $user1->currentCart();
|
|
||||||
|
|
||||||
$bookedFrom = now()->addDays(5);
|
|
||||||
$bookedUntil = now()->addDays(6);
|
|
||||||
|
|
||||||
$user1Cart->addToCart($pool, 2, [], $bookedFrom, $bookedUntil);
|
|
||||||
$user1Cart->checkout(); // Claims 2 singles
|
|
||||||
|
|
||||||
// Verify that only 1 single is available for the booked period
|
|
||||||
$available = $pool->getPoolMaxQuantity($bookedFrom, $bookedUntil);
|
|
||||||
$this->assertEquals(1, $available, 'Only 1 single should be available after booking 2');
|
|
||||||
|
|
||||||
// Our user adds 3 items without dates
|
|
||||||
$this->cart->addToCart($pool, 3);
|
|
||||||
|
|
||||||
$this->assertEquals(3, $this->cart->items()->sum('quantity'));
|
|
||||||
|
|
||||||
// Set dates where only 1 single is available
|
|
||||||
// Should NOT throw - just mark some items as unavailable
|
|
||||||
$this->cart->setDates($bookedFrom, $bookedUntil);
|
|
||||||
|
|
||||||
$this->cart->refresh();
|
|
||||||
$this->cart->load('items');
|
|
||||||
|
|
||||||
// Check how many items are available vs unavailable
|
|
||||||
$availableItems = $this->cart->items->filter(
|
|
||||||
fn($item) =>
|
|
||||||
$item->price !== null && $item->price > 0
|
|
||||||
);
|
|
||||||
$unavailableItems = $this->cart->items->filter(
|
|
||||||
fn($item) =>
|
|
||||||
$item->price === null || $item->price <= 0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should have 1 available and 2 unavailable
|
|
||||||
$this->assertEquals(1, $availableItems->count(), 'Should have 1 available item');
|
|
||||||
$this->assertEquals(2, $unavailableItems->count(), 'Should have 2 unavailable items');
|
|
||||||
|
|
||||||
// Cart should NOT be ready for checkout
|
|
||||||
$this->assertFalse($this->cart->is_ready_to_checkout, 'Cart with unavailable items should not be ready');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function cart_item_without_allocated_single_for_pool_is_not_ready()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
$from = now()->addDays(1);
|
|
||||||
$until = now()->addDays(2);
|
|
||||||
|
|
||||||
// Add 3 items with dates
|
|
||||||
$this->cart->addToCart($pool, 3, [], $from, $until);
|
|
||||||
|
|
||||||
// Verify all items are allocated and ready
|
|
||||||
foreach ($this->cart->items as $item) {
|
|
||||||
$this->assertNotNull($item->product_id, 'Item should have product_id allocated');
|
|
||||||
$this->assertTrue($item->is_ready_to_checkout, 'Allocated item should be ready');
|
|
||||||
}
|
|
||||||
|
|
||||||
// All items ready - cart is ready
|
|
||||||
$this->assertTrue($this->cart->fresh()->is_ready_to_checkout);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function removing_unavailable_items_makes_cart_ready()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
// Add 3 items without dates
|
|
||||||
$this->cart->addToCart($pool, 3);
|
|
||||||
|
|
||||||
// Manually make one item unavailable (price = null)
|
|
||||||
$unavailableItem = $this->cart->items()->first();
|
|
||||||
$unavailableItem->update(['price' => null, 'subtotal' => null]);
|
|
||||||
|
|
||||||
// Cart should NOT be ready
|
|
||||||
$this->assertFalse($this->cart->fresh()->is_ready_to_checkout);
|
|
||||||
|
|
||||||
// Remove the unavailable item
|
|
||||||
$unavailableItem->delete();
|
|
||||||
|
|
||||||
// Set dates for remaining items
|
|
||||||
$from = now()->addDays(1);
|
|
||||||
$until = now()->addDays(2);
|
|
||||||
$this->cart->setDates($from, $until);
|
|
||||||
|
|
||||||
// Now cart should be ready
|
|
||||||
$this->assertTrue($this->cart->fresh()->is_ready_to_checkout);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function getItemsRequiringAdjustments_includes_null_price_items()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
$from = now()->addDays(1);
|
|
||||||
$until = now()->addDays(2);
|
|
||||||
|
|
||||||
// Add 3 items with dates
|
|
||||||
$this->cart->addToCart($pool, 3, [], $from, $until);
|
|
||||||
|
|
||||||
// Make one item have null price
|
|
||||||
$item = $this->cart->items()->first();
|
|
||||||
$item->update(['price' => null, 'subtotal' => null]);
|
|
||||||
|
|
||||||
$this->cart->refresh();
|
|
||||||
$this->cart->load('items');
|
|
||||||
|
|
||||||
// Get items requiring adjustments
|
|
||||||
$itemsNeedingAdjustment = $this->cart->getItemsRequiringAdjustments();
|
|
||||||
|
|
||||||
// The null-price item should be in the list
|
|
||||||
$this->assertGreaterThanOrEqual(
|
|
||||||
1,
|
|
||||||
$itemsNeedingAdjustment->count(),
|
|
||||||
'Null price item should require adjustment'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check that it has 'unavailable' as the price adjustment reason
|
|
||||||
$nullPriceItem = $itemsNeedingAdjustment->first(fn($i) => $i->price === null);
|
|
||||||
$this->assertNotNull($nullPriceItem, 'Should find the null-price item');
|
|
||||||
|
|
||||||
$adjustments = $nullPriceItem->requiredAdjustments();
|
|
||||||
$this->assertArrayHasKey('price', $adjustments);
|
|
||||||
$this->assertEquals('unavailable', $adjustments['price']);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function changing_dates_to_available_period_makes_items_available_again()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
// First user books all 3 singles for specific dates
|
|
||||||
$user1 = User::factory()->create();
|
|
||||||
$user1Cart = $user1->currentCart();
|
|
||||||
|
|
||||||
$bookedFrom = now()->addDays(5);
|
|
||||||
$bookedUntil = now()->addDays(6);
|
|
||||||
|
|
||||||
$user1Cart->addToCart($pool, 3, [], $bookedFrom, $bookedUntil);
|
|
||||||
$user1Cart->checkout();
|
|
||||||
|
|
||||||
// Our user adds 3 items without dates
|
|
||||||
$this->cart->addToCart($pool, 3);
|
|
||||||
|
|
||||||
// Set dates that conflict - items become unavailable
|
|
||||||
$this->cart->setDates($bookedFrom, $bookedUntil);
|
|
||||||
$this->assertFalse($this->cart->fresh()->is_ready_to_checkout);
|
|
||||||
|
|
||||||
// Change to different dates where all singles are available
|
|
||||||
$availableFrom = now()->addDays(10);
|
|
||||||
$availableUntil = now()->addDays(11);
|
|
||||||
|
|
||||||
$this->cart->setDates($availableFrom, $availableUntil);
|
|
||||||
|
|
||||||
$this->cart->refresh();
|
|
||||||
$this->cart->load('items');
|
|
||||||
|
|
||||||
// All items should now have valid prices
|
|
||||||
foreach ($this->cart->items as $item) {
|
|
||||||
$this->assertNotNull($item->price, 'Item should have price after changing to available dates');
|
|
||||||
$this->assertGreaterThan(0, $item->price, 'Item should have positive price');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cart should be ready for checkout
|
|
||||||
$this->assertTrue($this->cart->is_ready_to_checkout, 'Cart should be ready after changing to available dates');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function checkout_throws_when_items_are_unavailable()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
// Add items and make one unavailable
|
|
||||||
$this->cart->addToCart($pool, 3);
|
|
||||||
|
|
||||||
$item = $this->cart->items()->first();
|
|
||||||
$item->update(['price' => null, 'subtotal' => null]);
|
|
||||||
|
|
||||||
// Trying to checkout should throw CartItemMissingInformationException
|
|
||||||
// because the item has 'price' => 'unavailable' in requiredAdjustments()
|
|
||||||
$this->expectException(\Blax\Shop\Exceptions\CartItemMissingInformationException::class);
|
|
||||||
$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 cart_item_is_not_ready_for_checkout_if_already_booked_on_same_dates()
|
|
||||||
{
|
|
||||||
$pool = $this->createPoolWithLimitedSingles(3);
|
|
||||||
|
|
||||||
$from = now()->addDays(1);
|
|
||||||
$until = now()->addDays(4);
|
|
||||||
$this->assertEquals(3, $pool->getPoolMaxQuantity($from, $until), 'No singles should be available after booking');
|
|
||||||
|
|
||||||
$cart = $this->user->currentCart();
|
|
||||||
$cart->addToCart($pool, 2, [], $from, $until);
|
|
||||||
|
|
||||||
foreach ($cart->items as $item) {
|
|
||||||
$this->assertTrue($item->is_ready_to_checkout, 'Item should be ready before booking');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertTrue($cart->is_ready_to_checkout, 'Cart should be ready before booking');
|
|
||||||
$cart->checkout();
|
|
||||||
|
|
||||||
$this->assertEquals(1, $pool->getPoolMaxQuantity($from, $until), 'No singles should be available after booking');
|
|
||||||
|
|
||||||
$cart = $this->user->currentCart();
|
|
||||||
$cart->addToCart($pool, 3);
|
|
||||||
|
|
||||||
foreach ($cart->items as $item) {
|
|
||||||
$this->assertFalse($item->is_ready_to_checkout, 'Item should not be ready after singles are booked');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertFalse($cart->is_ready_to_checkout, 'Cart should not be ready after singles are booked');
|
|
||||||
|
|
||||||
$cart->setDates($from, $until);
|
|
||||||
|
|
||||||
// After setting dates where only 1 single is available but we have 3 items,
|
|
||||||
// only 1 item should be ready (the first one up to the available capacity)
|
|
||||||
$readies = 0;
|
|
||||||
foreach ($cart->items as $item) {
|
|
||||||
if ($item->is_ready_to_checkout) {
|
|
||||||
$readies++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertEquals(1, $readies, '1 item should be ready (1 single available)');
|
|
||||||
$this->assertFalse($cart->is_ready_to_checkout);
|
|
||||||
|
|
||||||
$offset = 4;
|
|
||||||
$cart->setDates(
|
|
||||||
$from->copy()->addDays($offset),
|
|
||||||
$until->copy()->addDays($offset)
|
|
||||||
);
|
|
||||||
|
|
||||||
$readies = 0;
|
|
||||||
foreach ($cart->items as $item) {
|
|
||||||
if ($item->is_ready_to_checkout) {
|
|
||||||
$readies++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertEquals(3, $readies, '3 items should be ready');
|
|
||||||
$this->assertTrue($cart->is_ready_to_checkout);
|
|
||||||
|
|
||||||
$offset = 3;
|
|
||||||
$cart->setDates(
|
|
||||||
$from->copy()->addDays($offset),
|
|
||||||
$until->copy()->addDays($offset)
|
|
||||||
);
|
|
||||||
|
|
||||||
$readies = 0;
|
|
||||||
foreach ($cart->items as $item) {
|
|
||||||
if ($item->is_ready_to_checkout) {
|
|
||||||
$readies++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// With offset 3, the new period starts exactly when the booked period ends.
|
|
||||||
// In hotel-style bookings, checkout day = checkin day does NOT overlap,
|
|
||||||
// so all 3 singles should be available.
|
|
||||||
$this->assertEquals(3, $readies, '3 items should be ready (no overlap with offset 3)');
|
|
||||||
$this->assertTrue($cart->is_ready_to_checkout);
|
|
||||||
|
|
||||||
$offset = 2;
|
|
||||||
$cart->setDates(
|
|
||||||
$from->copy()->addDays($offset),
|
|
||||||
$until->copy()->addDays($offset)
|
|
||||||
);
|
|
||||||
|
|
||||||
$readies = 0;
|
|
||||||
foreach ($cart->items as $item) {
|
|
||||||
if ($item->is_ready_to_checkout) {
|
|
||||||
$readies++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertEquals(1, $readies, '1 item should be ready (no overlap with offset 2)');
|
|
||||||
$this->assertFalse($cart->is_ready_to_checkout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Checkout;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Checkout;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Checkout;
|
||||||
|
|
||||||
use Blax\Shop\Enums\CartStatus;
|
use Blax\Shop\Enums\CartStatus;
|
||||||
use Blax\Shop\Enums\OrderStatus;
|
use Blax\Shop\Enums\OrderStatus;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Checkout;
|
||||||
|
|
||||||
use Blax\Shop\Models\PaymentMethod;
|
use Blax\Shop\Models\PaymentMethod;
|
||||||
use Blax\Shop\Models\PaymentProviderIdentity;
|
use Blax\Shop\Models\PaymentProviderIdentity;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Checkout;
|
||||||
|
|
||||||
use Blax\Shop\Models\PaymentMethod;
|
use Blax\Shop\Models\PaymentMethod;
|
||||||
use Blax\Shop\Models\PaymentProviderIdentity;
|
use Blax\Shop\Models\PaymentProviderIdentity;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Checkout;
|
||||||
|
|
||||||
use Blax\Shop\Enums\PurchaseStatus;
|
use Blax\Shop\Enums\PurchaseStatus;
|
||||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\PriceType;
|
use Blax\Shop\Enums\PriceType;
|
||||||
use Blax\Shop\Enums\PricingStrategy;
|
use Blax\Shop\Enums\PricingStrategy;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
|
|
@ -27,14 +27,15 @@ use PHPUnit\Framework\Attributes\Test;
|
||||||
* - 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 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
|
* - Single item 3 has default price of 1000
|
||||||
* - Each single item has 2 stocks
|
* - Each single item has 2 stocks
|
||||||
|
* - Pricing strategy: LOWEST
|
||||||
*
|
*
|
||||||
* Expected cart totals with LOWEST pricing strategy:
|
* Expected cart totals with LOWEST pricing strategy:
|
||||||
* - Add 1: 300 (from Spot 1)
|
* - Add 1: 300 (Spot 1, cheapest available)
|
||||||
* - Add 2: 600 (300 + 300, both from Spot 1)
|
* - Add 2: 600 (Spot 1 again, still cheapest at 300)
|
||||||
* - Add 3: 1100 (300 + 300 + 500, third from pool or Spot 2 fallback)
|
* - Add 3: 1100 (Spot 2 fallback to pool price 500 since it has no own price)
|
||||||
* - Add 4: 1600 (300 + 300 + 500 + 500, fourth from pool or Spot 2 fallback)
|
* - Add 4: 1600 (Spot 2 again at 500)
|
||||||
* - Add 5: 2600 (300 + 300 + 500 + 500 + 1000, fifth from Spot 3)
|
* - Add 5: 2600 (Spot 3 at 1000, its own price)
|
||||||
* - Add 6: 3600 (300 + 300 + 500 + 500 + 1000 + 1000, sixth from Spot 3)
|
* - Add 6: 3600 (Spot 3 again at 1000)
|
||||||
* - Add 7: NotEnoughStockException (only 6 total)
|
* - Add 7: NotEnoughStockException (only 6 total)
|
||||||
*
|
*
|
||||||
* When dates span 2 days, all totals should double.
|
* When dates span 2 days, all totals should double.
|
||||||
|
|
@ -157,6 +158,11 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
$from = now()->addDays(1);
|
$from = now()->addDays(1);
|
||||||
$until = now()->addDays(2);
|
$until = now()->addDays(2);
|
||||||
|
|
||||||
|
// With LOWEST pricing strategy:
|
||||||
|
// - Spot 1 (300): Has own price, use 300
|
||||||
|
// - Spot 2 (no price): Fallback to pool price 500
|
||||||
|
// - Spot 3 (1000): Has own price, use 1000
|
||||||
|
|
||||||
// Add 1: Should use lowest price (300 from Spot 1)
|
// Add 1: Should use lowest price (300 from Spot 1)
|
||||||
$cartItem = $this->cart->addToCart($pool, 1, [], $from, $until);
|
$cartItem = $this->cart->addToCart($pool, 1, [], $from, $until);
|
||||||
$this->assertEquals(300, $this->cart->getTotal());
|
$this->assertEquals(300, $this->cart->getTotal());
|
||||||
|
|
@ -174,11 +180,11 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
$this->cart->addToCart($pool, 1, [], $from, $until);
|
$this->cart->addToCart($pool, 1, [], $from, $until);
|
||||||
$this->assertEquals(1600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(1600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
// Add 5: Spot 3 price (1000), cumulative 2600
|
// Add 5: Spot 3 uses its own price (1000), cumulative 2600
|
||||||
$this->cart->addToCart($pool, 1, [], $from, $until);
|
$this->cart->addToCart($pool, 1, [], $from, $until);
|
||||||
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
// Add 6: Spot 3 price again (1000), cumulative 3600
|
// Add 6: Spot 3 uses its own price again (1000), cumulative 3600
|
||||||
$this->cart->addToCart($pool, 1, [], $from, $until);
|
$this->cart->addToCart($pool, 1, [], $from, $until);
|
||||||
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
|
|
@ -196,26 +202,37 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Get price IDs for reference
|
// Get price IDs for reference
|
||||||
$spot1PriceId = $spots[0]->defaultPrice()->first()->id;
|
$spot1PriceId = $spots[0]->defaultPrice()->first()->id;
|
||||||
$poolPriceId = $pool->defaultPrice()->first()->id;
|
$poolPriceId = $pool->defaultPrice()->first()->id;
|
||||||
$spot3PriceId = $spots[2]->defaultPrice()->first()->id;
|
|
||||||
|
|
||||||
// Add 6 items
|
// Add 6 items
|
||||||
$this->cart->addToCart($pool, 6);
|
$this->cart->addToCart($pool, 6);
|
||||||
|
|
||||||
$items = $this->cart->items()->orderBy('price', 'asc')->get();
|
$items = $this->cart->items()->orderBy('price', 'asc')->get();
|
||||||
|
|
||||||
// First cart item group (price 300) should have Spot 1's price_id
|
// With LOWEST pricing strategy, items get merged by single allocation:
|
||||||
|
// - 1 cart item with Spot 1 (qty 2): price 300, price_id from Spot 1
|
||||||
|
// - 1 cart item with Spot 2 (qty 2): price 500, price_id from pool
|
||||||
|
// - 1 cart item with Spot 3 (qty 2): price 1000, price_id from Spot 3
|
||||||
|
// Total: 3 cart items
|
||||||
|
|
||||||
|
$this->assertCount(3, $items);
|
||||||
|
|
||||||
|
// First cart item (price 300) should have Spot 1's price_id
|
||||||
$item300 = $items->first(fn($i) => $i->price === 300);
|
$item300 = $items->first(fn($i) => $i->price === 300);
|
||||||
$this->assertNotNull($item300);
|
$this->assertNotNull($item300);
|
||||||
|
$this->assertEquals(2, $item300->quantity);
|
||||||
$this->assertEquals($spot1PriceId, $item300->price_id);
|
$this->assertEquals($spot1PriceId, $item300->price_id);
|
||||||
|
|
||||||
// Second cart item group (price 500) should have Pool's price_id (for Spot 2 fallback)
|
// Cart item with price 500 should have Pool's price_id
|
||||||
$item500 = $items->first(fn($i) => $i->price === 500);
|
$item500 = $items->first(fn($i) => $i->price === 500);
|
||||||
$this->assertNotNull($item500);
|
$this->assertNotNull($item500);
|
||||||
|
$this->assertEquals(2, $item500->quantity);
|
||||||
$this->assertEquals($poolPriceId, $item500->price_id);
|
$this->assertEquals($poolPriceId, $item500->price_id);
|
||||||
|
|
||||||
// Third cart item group (price 1000) should have Spot 3's price_id
|
// Cart item with price 1000 should have Spot 3's price_id
|
||||||
|
$spot3PriceId = $spots[2]->defaultPrice()->first()->id;
|
||||||
$item1000 = $items->first(fn($i) => $i->price === 1000);
|
$item1000 = $items->first(fn($i) => $i->price === 1000);
|
||||||
$this->assertNotNull($item1000);
|
$this->assertNotNull($item1000);
|
||||||
|
$this->assertEquals(2, $item1000->quantity);
|
||||||
$this->assertEquals($spot3PriceId, $item1000->price_id);
|
$this->assertEquals($spot3PriceId, $item1000->price_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,7 +248,11 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Add items with dates
|
// Add items with dates
|
||||||
$this->cart->addToCart($pool, 6, [], $from, $until);
|
$this->cart->addToCart($pool, 6, [], $from, $until);
|
||||||
|
|
||||||
// With 2 days: 300*2 + 300*2 + 500*2 + 500*2 + 1000*2 + 1000*2 = 7200
|
// With 2 days and LOWEST pricing strategy:
|
||||||
|
// - Spot 1 (300): Has own price, use 300
|
||||||
|
// - Spot 2 (no price): Fallback to pool price 500
|
||||||
|
// - Spot 3 (1000): Has own price, use 1000
|
||||||
|
// Total: (300+300+500+500+1000+1000) * 2 = 7200
|
||||||
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,7 +265,11 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Add items without dates first
|
// Add items without dates first
|
||||||
$this->cart->addToCart($pool, 6);
|
$this->cart->addToCart($pool, 6);
|
||||||
|
|
||||||
// 1-day prices: 300 + 300 + 500 + 500 + 1000 + 1000 = 3600
|
// 1-day prices with LOWEST strategy:
|
||||||
|
// - Spot 1 (300): Has own price, use 300
|
||||||
|
// - Spot 2 (no price): Fallback to pool price 500
|
||||||
|
// - Spot 3 (1000): Has own price, use 1000
|
||||||
|
// Total: 300 + 300 + 500 + 500 + 1000 + 1000 = 3600
|
||||||
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
$from = Carbon::now()->addDay()->startOfDay();
|
$from = Carbon::now()->addDay()->startOfDay();
|
||||||
|
|
@ -253,7 +278,7 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Set dates - should recalculate to 2-day prices
|
// Set dates - should recalculate to 2-day prices
|
||||||
$this->cart->setDates($from, $until, validateAvailability: false);
|
$this->cart->setDates($from, $until, validateAvailability: false);
|
||||||
|
|
||||||
// 2-day prices: (300 + 300 + 500 + 500 + 1000 + 1000) * 2 = 7200
|
// 2-day prices: 3600 * 2 = 7200
|
||||||
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,6 +291,7 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Add items without dates first
|
// Add items without dates first
|
||||||
$this->cart->addToCart($pool, 6);
|
$this->cart->addToCart($pool, 6);
|
||||||
|
|
||||||
|
// With LOWEST strategy: 300 + 300 + 500 + 500 + 1000 + 1000 = 3600
|
||||||
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
$from = Carbon::now()->addDay()->startOfDay();
|
$from = Carbon::now()->addDay()->startOfDay();
|
||||||
|
|
@ -280,7 +306,7 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Apply dates to items
|
// Apply dates to items
|
||||||
$this->cart->applyDatesToItems(validateAvailability: false, overwrite: true);
|
$this->cart->applyDatesToItems(validateAvailability: false, overwrite: true);
|
||||||
|
|
||||||
// Should be 2-day prices
|
// Should be 2-day prices: 3600 * 2 = 7200
|
||||||
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,6 +483,11 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
$from = now()->addDays(1);
|
$from = now()->addDays(1);
|
||||||
$until = now()->addDays(2);
|
$until = now()->addDays(2);
|
||||||
|
|
||||||
|
// With LOWEST pricing strategy:
|
||||||
|
// - Spot 1 (300): Has own price, use 300
|
||||||
|
// - Spot 2 (no price): Fallback to pool price 500
|
||||||
|
// - Spot 3 (1000): Has own price, use 1000
|
||||||
|
|
||||||
// Add 1: Should use lowest price (300 from Spot 1)
|
// Add 1: Should use lowest price (300 from Spot 1)
|
||||||
$cartItem = $this->cart->addToCart($pool, 1, [], $from, $until);
|
$cartItem = $this->cart->addToCart($pool, 1, [], $from, $until);
|
||||||
$this->assertEquals(300, $this->cart->getTotal());
|
$this->assertEquals(300, $this->cart->getTotal());
|
||||||
|
|
@ -474,11 +505,11 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
$this->cart->addToCart($pool, 1, [], $from, $until);
|
$this->cart->addToCart($pool, 1, [], $from, $until);
|
||||||
$this->assertEquals(1600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(1600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
// Add 5: Spot 3 price (1000), cumulative 2600
|
// Add 5: Spot 3 uses its own price (1000), cumulative 2600
|
||||||
$this->cart->addToCart($pool, 1, [], $from, $until);
|
$this->cart->addToCart($pool, 1, [], $from, $until);
|
||||||
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(2600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
// Add 6: Spot 3 price again (1000), cumulative 3600
|
// Add 6: Spot 3 uses its own price again (1000), cumulative 3600
|
||||||
$this->cart->addToCart($pool, 1, [], $from, $until);
|
$this->cart->addToCart($pool, 1, [], $from, $until);
|
||||||
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
|
|
@ -496,27 +527,37 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Get price IDs for reference
|
// Get price IDs for reference
|
||||||
$spot1PriceId = $spots[0]->defaultPrice()->first()->id;
|
$spot1PriceId = $spots[0]->defaultPrice()->first()->id;
|
||||||
$poolPriceId = $pool->defaultPrice()->first()->id;
|
$poolPriceId = $pool->defaultPrice()->first()->id;
|
||||||
$spot3PriceId = $spots[2]->defaultPrice()->first()->id;
|
|
||||||
|
|
||||||
// Add 6 items
|
// Add 6 items
|
||||||
$this->cart->addToCart($pool, 6);
|
$this->cart->addToCart($pool, 6);
|
||||||
|
|
||||||
$items = $this->cart->items()->orderBy('price', 'asc')->get();
|
$items = $this->cart->items()->orderBy('price', 'asc')->get();
|
||||||
|
|
||||||
// First cart item group (price 300) should have Spot 1's price_id
|
// With LOWEST pricing strategy, items get merged by single allocation:
|
||||||
|
// - 1 cart item with Spot 1 (qty 2): price 300, price_id from Spot 1
|
||||||
|
// - 1 cart item with Spot 2 (qty 2): price 500, price_id from pool
|
||||||
|
// - 1 cart item with Spot 3 (qty 2): price 500, price_id from pool
|
||||||
|
// Total: 3 cart items
|
||||||
|
|
||||||
|
$this->assertCount(3, $items);
|
||||||
|
|
||||||
|
// First cart item (price 300) should have Spot 1's price_id
|
||||||
$item300 = $items->first(fn($i) => $i->price === 300);
|
$item300 = $items->first(fn($i) => $i->price === 300);
|
||||||
$this->assertNotNull($item300);
|
$this->assertNotNull($item300);
|
||||||
|
$this->assertEquals(2, $item300->quantity);
|
||||||
$this->assertEquals($spot1PriceId, $item300->price_id);
|
$this->assertEquals($spot1PriceId, $item300->price_id);
|
||||||
|
|
||||||
// Second cart item group (price 500) should have Pool's price_id (for Spot 2 fallback)
|
// Cart item (price 500) should have Pool's price_id (fallback for Spot 2)
|
||||||
$item500 = $items->first(fn($i) => $i->price === 500);
|
$item500 = $items->first(fn($i) => $i->price === 500);
|
||||||
$this->assertNotNull($item500);
|
$this->assertNotNull($item500);
|
||||||
|
$this->assertEquals(2, $item500->quantity);
|
||||||
$this->assertEquals($poolPriceId, $item500->price_id);
|
$this->assertEquals($poolPriceId, $item500->price_id);
|
||||||
|
|
||||||
// Third cart item group (price 1000) should have Spot 3's price_id
|
// Cart item (price 1000) should have Spot 3's price_id
|
||||||
$item1000 = $items->first(fn($i) => $i->price === 1000);
|
$item1000 = $items->first(fn($i) => $i->price === 1000);
|
||||||
$this->assertNotNull($item1000);
|
$this->assertNotNull($item1000);
|
||||||
$this->assertEquals($spot3PriceId, $item1000->price_id);
|
$this->assertEquals(2, $item1000->quantity);
|
||||||
|
$this->assertNotEquals($poolPriceId, $item1000->price_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
|
|
@ -531,7 +572,11 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Add items with dates
|
// Add items with dates
|
||||||
$this->cart->addToCart($pool, 6, [], $from, $until);
|
$this->cart->addToCart($pool, 6, [], $from, $until);
|
||||||
|
|
||||||
// With 2 days: 300*2 + 300*2 + 500*2 + 500*2 + 1000*2 + 1000*2 = 7200
|
// With 2 days and LOWEST pricing strategy:
|
||||||
|
// - Spot 1 (300): Has own price, use 300
|
||||||
|
// - Spot 2 (no price): Fallback to pool price 500
|
||||||
|
// - Spot 3 (1000): Has own price, use 1000
|
||||||
|
// Total: (300+300+500+500+1000+1000) * 2 = 7200
|
||||||
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -544,7 +589,11 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Add items without dates first
|
// Add items without dates first
|
||||||
$this->cart->addToCart($pool, 6);
|
$this->cart->addToCart($pool, 6);
|
||||||
|
|
||||||
// 1-day prices: 300 + 300 + 500 + 500 + 1000 + 1000 = 3600
|
// 1-day prices with LOWEST strategy:
|
||||||
|
// - Spot 1 (300): Has own price, use 300
|
||||||
|
// - Spot 2 (no price): Fallback to pool price 500
|
||||||
|
// - Spot 3 (1000): Has own price, use 1000
|
||||||
|
// Total: 300 + 300 + 500 + 500 + 1000 + 1000 = 3600
|
||||||
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
||||||
|
|
||||||
$from = Carbon::now()->addDay()->startOfDay();
|
$from = Carbon::now()->addDay()->startOfDay();
|
||||||
|
|
@ -553,7 +602,7 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
// Set dates - should recalculate to 2-day prices
|
// Set dates - should recalculate to 2-day prices
|
||||||
$this->cart->setDates($from, $until, validateAvailability: false);
|
$this->cart->setDates($from, $until, validateAvailability: false);
|
||||||
|
|
||||||
// 2-day prices: 7200
|
// 2-day prices: 3600 * 2 = 7200
|
||||||
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
$this->assertEquals(7200, $this->cart->fresh()->getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -742,6 +791,7 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
$expectedTotal = $this->cart->items()->sum('subtotal');
|
$expectedTotal = $this->cart->items()->sum('subtotal');
|
||||||
|
|
||||||
$this->assertEquals($expectedTotal, $this->cart->getTotal());
|
$this->assertEquals($expectedTotal, $this->cart->getTotal());
|
||||||
|
// With LOWEST strategy: (300*2 + 300*2 + 500*2 + 500*2 + 1000*2 + 1000*2) = 7200
|
||||||
$this->assertEquals(7200, $this->cart->getTotal());
|
$this->assertEquals(7200, $this->cart->getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -753,6 +803,7 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
|
|
||||||
// Add 6 items
|
// Add 6 items
|
||||||
$this->cart->addToCart($pool, 6);
|
$this->cart->addToCart($pool, 6);
|
||||||
|
// With LOWEST strategy: 300 + 300 + 500 + 500 + 1000 + 1000 = 3600
|
||||||
$this->assertEquals(3600, $this->cart->getTotal());
|
$this->assertEquals(3600, $this->cart->getTotal());
|
||||||
|
|
||||||
// Remove 1 item (should remove from highest price first - LIFO)
|
// Remove 1 item (should remove from highest price first - LIFO)
|
||||||
|
|
@ -760,6 +811,7 @@ class PoolParkingCartPricingTest extends TestCase
|
||||||
|
|
||||||
// Now we should be able to add 1 more
|
// Now we should be able to add 1 more
|
||||||
$this->cart->addToCart($pool, 1);
|
$this->cart->addToCart($pool, 1);
|
||||||
|
// Should still be 3600 (removed 1000, added 1000)
|
||||||
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
$this->assertEquals(3600, $this->cart->fresh()->getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Enums\StockType;
|
use Blax\Shop\Enums\StockType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Pool;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
|
|
||||||
|
|
@ -1,232 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
|
||||||
use Blax\Shop\Enums\PricingStrategy;
|
|
||||||
use Blax\Shop\Models\Cart;
|
|
||||||
use Blax\Shop\Models\Product;
|
|
||||||
use Blax\Shop\Models\ProductPrice;
|
|
||||||
use Blax\Shop\Tests\TestCase;
|
|
||||||
use Workbench\App\Models\User;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
|
||||||
|
|
||||||
class PoolProductPriceIdTest extends TestCase
|
|
||||||
{
|
|
||||||
protected User $user;
|
|
||||||
protected Cart $cart;
|
|
||||||
protected Product $poolProduct;
|
|
||||||
protected Product $singleItem1;
|
|
||||||
protected Product $singleItem2;
|
|
||||||
protected ProductPrice $price1;
|
|
||||||
protected ProductPrice $price2;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->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 with different prices
|
|
||||||
$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);
|
|
||||||
|
|
||||||
// Set prices on single items
|
|
||||||
$this->price1 = ProductPrice::factory()->create([
|
|
||||||
'purchasable_id' => $this->singleItem1->id,
|
|
||||||
'purchasable_type' => Product::class,
|
|
||||||
'unit_amount' => 2000, // $20/day
|
|
||||||
'currency' => 'USD',
|
|
||||||
'is_default' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->price2 = ProductPrice::factory()->create([
|
|
||||||
'purchasable_id' => $this->singleItem2->id,
|
|
||||||
'purchasable_type' => Product::class,
|
|
||||||
'unit_amount' => 5000, // $50/day
|
|
||||||
'currency' => 'USD',
|
|
||||||
'is_default' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 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_stores_single_item_price_id_when_adding_pool_to_cart_with_lowest_strategy()
|
|
||||||
{
|
|
||||||
// Set pricing strategy to lowest (default)
|
|
||||||
$this->poolProduct->setPricingStrategy(PricingStrategy::LOWEST);
|
|
||||||
|
|
||||||
// Add pool to cart - should use the lowest price (singleItem1's price)
|
|
||||||
$cartItem = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
|
|
||||||
// Assert the cart item has the price_id from the single item, not the pool
|
|
||||||
$this->assertNotNull($cartItem->price_id);
|
|
||||||
$this->assertEquals($this->price1->id, $cartItem->price_id);
|
|
||||||
$this->assertEquals(2000, $cartItem->price); // $20
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function it_stores_correct_price_id_for_second_pool_item_with_progressive_pricing()
|
|
||||||
{
|
|
||||||
// Set pricing strategy to lowest
|
|
||||||
$this->poolProduct->setPricingStrategy(PricingStrategy::LOWEST);
|
|
||||||
|
|
||||||
// Add first pool item - should use lowest price (singleItem1)
|
|
||||||
$cartItem1 = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
$this->assertEquals($this->price1->id, $cartItem1->price_id);
|
|
||||||
$this->assertEquals(2000, $cartItem1->price);
|
|
||||||
|
|
||||||
// Add second pool item - should use next lowest price (singleItem2)
|
|
||||||
$cartItem2 = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
$this->assertEquals($this->price2->id, $cartItem2->price_id);
|
|
||||||
$this->assertEquals(5000, $cartItem2->price);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function it_stores_single_item_price_id_with_highest_strategy()
|
|
||||||
{
|
|
||||||
// Set pricing strategy to highest
|
|
||||||
$this->poolProduct->setPoolPricingStrategy('highest');
|
|
||||||
|
|
||||||
// Add pool to cart - should use the highest price (singleItem2's price)
|
|
||||||
$cartItem = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
|
|
||||||
// Assert the cart item has the price_id from the single item with highest price
|
|
||||||
$this->assertNotNull($cartItem->price_id);
|
|
||||||
$this->assertEquals($this->price2->id, $cartItem->price_id);
|
|
||||||
$this->assertEquals(5000, $cartItem->price); // $50
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function it_stores_allocated_single_item_in_product_id_column()
|
|
||||||
{
|
|
||||||
// Set pricing strategy to lowest
|
|
||||||
$this->poolProduct->setPricingStrategy(PricingStrategy::LOWEST);
|
|
||||||
|
|
||||||
// Add pool to cart
|
|
||||||
$cartItem = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
|
|
||||||
// Check product_id column contains allocated single item id
|
|
||||||
$this->assertNotNull($cartItem->product_id);
|
|
||||||
$this->assertEquals($this->singleItem1->id, $cartItem->product_id);
|
|
||||||
|
|
||||||
// Meta should still have the name for display purposes
|
|
||||||
$meta = $cartItem->getMeta();
|
|
||||||
$this->assertEquals($this->singleItem1->name, $meta->allocated_single_item_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function it_stores_different_single_items_in_product_id_for_progressive_pricing()
|
|
||||||
{
|
|
||||||
// Set pricing strategy to lowest
|
|
||||||
$this->poolProduct->setPricingStrategy(PricingStrategy::LOWEST);
|
|
||||||
|
|
||||||
// Add first pool item
|
|
||||||
$cartItem1 = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
$this->assertEquals($this->singleItem1->id, $cartItem1->product_id);
|
|
||||||
|
|
||||||
// Add second pool item
|
|
||||||
$cartItem2 = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
$this->assertEquals($this->singleItem2->id, $cartItem2->product_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function it_uses_pool_price_id_when_pool_has_direct_price_and_no_single_item_prices()
|
|
||||||
{
|
|
||||||
// Remove prices from single items
|
|
||||||
$this->price1->delete();
|
|
||||||
$this->price2->delete();
|
|
||||||
|
|
||||||
// Set a direct price on the pool itself
|
|
||||||
$poolPrice = ProductPrice::factory()->create([
|
|
||||||
'purchasable_id' => $this->poolProduct->id,
|
|
||||||
'purchasable_type' => Product::class,
|
|
||||||
'unit_amount' => 3000, // $30
|
|
||||||
'currency' => 'USD',
|
|
||||||
'is_default' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Add pool to cart - should use pool's direct price as fallback
|
|
||||||
$cartItem = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
|
|
||||||
// Assert the cart item has the pool's price_id
|
|
||||||
$this->assertEquals($poolPrice->id, $cartItem->price_id);
|
|
||||||
$this->assertEquals(3000, $cartItem->price);
|
|
||||||
|
|
||||||
// product_id should indicate which single item was allocated
|
|
||||||
// Even though the pool's price is used as fallback, one of the single items is still allocated
|
|
||||||
$this->assertNotNull($cartItem->product_id);
|
|
||||||
$this->assertTrue(
|
|
||||||
$cartItem->product_id === $this->singleItem1->id ||
|
|
||||||
$cartItem->product_id === $this->singleItem2->id,
|
|
||||||
'Allocated single item should be one of the pool\'s single items'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function it_stores_price_id_with_average_pricing_strategy()
|
|
||||||
{
|
|
||||||
// Set pricing strategy to average
|
|
||||||
$this->poolProduct->setPricingStrategy(PricingStrategy::AVERAGE);
|
|
||||||
|
|
||||||
// Add pool to cart - should use average price but store first item's price_id
|
|
||||||
$cartItem = $this->cart->addToCart($this->poolProduct, 1);
|
|
||||||
|
|
||||||
// Average of 2000 and 5000 = 3500
|
|
||||||
$this->assertEquals(3500, $cartItem->price);
|
|
||||||
|
|
||||||
// Should store a price_id (from one of the single items)
|
|
||||||
$this->assertNotNull($cartItem->price_id);
|
|
||||||
$this->assertTrue(
|
|
||||||
$cartItem->price_id === $this->price1->id || $cartItem->price_id === $this->price2->id,
|
|
||||||
'Price ID should be from one of the single items'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function it_stores_correct_price_id_with_booking_dates()
|
|
||||||
{
|
|
||||||
// Set pricing strategy to lowest
|
|
||||||
$this->poolProduct->setPricingStrategy(PricingStrategy::LOWEST);
|
|
||||||
|
|
||||||
$from = now()->addDays(1)->startOfDay();
|
|
||||||
$until = now()->addDays(3)->startOfDay(); // 2 days
|
|
||||||
|
|
||||||
// Add pool to cart with dates
|
|
||||||
$cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until);
|
|
||||||
|
|
||||||
// Should use lowest price and store its price_id
|
|
||||||
$this->assertEquals($this->price1->id, $cartItem->price_id);
|
|
||||||
$this->assertEquals(4000, $cartItem->price); // $20 × 2 days
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
use Blax\Shop\Models\ProductAction;
|
use Blax\Shop\Models\ProductAction;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
use Blax\Shop\Models\ProductAttribute;
|
use Blax\Shop\Models\ProductAttribute;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
use Blax\Shop\Models\ProductCategory;
|
use Blax\Shop\Models\ProductCategory;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductRelationType;
|
use Blax\Shop\Enums\ProductRelationType;
|
||||||
use Blax\Shop\Enums\ProductStatus;
|
use Blax\Shop\Enums\ProductStatus;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Enums\BillingScheme;
|
use Blax\Shop\Enums\BillingScheme;
|
||||||
use Blax\Shop\Enums\PriceType;
|
use Blax\Shop\Enums\PriceType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Exceptions\HasNoDefaultPriceException;
|
use Blax\Shop\Exceptions\HasNoDefaultPriceException;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Enums\PurchaseStatus;
|
use Blax\Shop\Enums\PurchaseStatus;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Enums\StockStatus;
|
use Blax\Shop\Enums\StockStatus;
|
||||||
use Blax\Shop\Enums\StockType;
|
use Blax\Shop\Enums\StockType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Enums\StockStatus;
|
use Blax\Shop\Enums\StockStatus;
|
||||||
use Blax\Shop\Enums\StockType;
|
use Blax\Shop\Enums\StockType;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Product;
|
||||||
|
|
||||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature\ProductionBugs;
|
||||||
|
|
||||||
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
use Blax\Shop\Enums\ProductStatus;
|
||||||
|
use Blax\Shop\Models\Cart;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Workbench\App\Models\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production Bug: Pool product pricing shows wrong prices when dates are set
|
||||||
|
*
|
||||||
|
* Scenario (from user report):
|
||||||
|
* - Run add example products command
|
||||||
|
* - Add a default price to parking pool product (ID: 1755)
|
||||||
|
* - Add the pool 2 times to the cart
|
||||||
|
* - Set dates: 2026-01-01T12:00 until 2026-01-02T12:00 (1 day)
|
||||||
|
* - Both cart items show as 5000 each (WRONG - should use pool's default price: 2500)
|
||||||
|
* - Cart total is 10000 (WRONG - should be 5000)
|
||||||
|
*
|
||||||
|
* Expected behavior:
|
||||||
|
* - When pool has a default price of 2500 (€25/day)
|
||||||
|
* - And dates span 1 day
|
||||||
|
* - Each cart item should be 2500 cents (not 5000)
|
||||||
|
* - Cart total should be 5000 cents (2500 × 2 items)
|
||||||
|
*/
|
||||||
|
class PoolPricingWithDatesBugTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private User $user;
|
||||||
|
private Cart $cart;
|
||||||
|
private Product $pool;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->cart = Cart::factory()->create([
|
||||||
|
'customer_id' => $this->user->id,
|
||||||
|
'customer_type' => get_class($this->user),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate the exact scenario from the production bug report.
|
||||||
|
* This test should fail initially, demonstrating the bug.
|
||||||
|
*/
|
||||||
|
#[Test]
|
||||||
|
public function it_reproduces_the_pool_pricing_bug_from_production()
|
||||||
|
{
|
||||||
|
// Step 1: Create a parking pool similar to example products command
|
||||||
|
$this->pool = Product::factory()->withPrices(unit_amount: 2500)->create([
|
||||||
|
'slug' => 'parking-spaces-north-garage',
|
||||||
|
'name' => 'Parking Spaces - North Garage',
|
||||||
|
'sku' => 'PARK-NORTH-POOL',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'status' => ProductStatus::PUBLISHED,
|
||||||
|
'is_visible' => true,
|
||||||
|
'manage_stock' => true,
|
||||||
|
'published_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create single items for the pool (like the command does)
|
||||||
|
$single1 = Product::factory()->withStocks(1)->withPrices(unit_amount: 5000)->create([
|
||||||
|
'slug' => 'parking-spot-a3',
|
||||||
|
'name' => 'Spot A3',
|
||||||
|
'sku' => 'PARK-NORTH-01',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'status' => ProductStatus::PUBLISHED,
|
||||||
|
'is_visible' => false,
|
||||||
|
'manage_stock' => true,
|
||||||
|
'parent_id' => $this->pool->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$single2 = Product::factory()->withStocks(1)->withPrices(unit_amount: 5000)->create([
|
||||||
|
'slug' => 'parking-spot-a7',
|
||||||
|
'name' => 'Spot A7',
|
||||||
|
'sku' => 'PARK-NORTH-02',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'status' => ProductStatus::PUBLISHED,
|
||||||
|
'is_visible' => false,
|
||||||
|
'manage_stock' => true,
|
||||||
|
'parent_id' => $this->pool->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Attach single items to pool
|
||||||
|
$this->pool->attachSingleItems([
|
||||||
|
$single1->id,
|
||||||
|
$single2->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Step 3: Add the pool 2 times to the cart (without dates initially)
|
||||||
|
$this->cart->addToCart($this->pool, 2);
|
||||||
|
|
||||||
|
// Verify 2 items were added
|
||||||
|
$this->assertEquals(2, $this->cart->items()->count());
|
||||||
|
|
||||||
|
// Step 4: Set dates (1 day: 2026-01-01T12:00 until 2026-01-02T12:00)
|
||||||
|
$from = Carbon::parse('2026-01-01 12:00:00');
|
||||||
|
$until = Carbon::parse('2026-01-02 12:00:00');
|
||||||
|
|
||||||
|
$this->cart->setDates($from, $until, validateAvailability: false);
|
||||||
|
|
||||||
|
// Reload cart and items
|
||||||
|
$cart = $this->cart->fresh();
|
||||||
|
$cart->load('items');
|
||||||
|
|
||||||
|
// EXPECTED BEHAVIOR:
|
||||||
|
// - Pool has a default price of 2500 (€25/day)
|
||||||
|
// - Each single item also has a price of 5000 (€50/day)
|
||||||
|
// - With LOWEST pricing strategy (default), pool should still use individual product prices, if they have one
|
||||||
|
// - With 1 day duration, each cart item should be 5000 cents
|
||||||
|
// - Total should be 10000 cents (5000 × 2 items)
|
||||||
|
|
||||||
|
// BUG: Currently shows 5000 per item instead of 2500
|
||||||
|
foreach ($cart->items as $item) {
|
||||||
|
$this->assertEquals(
|
||||||
|
5000,
|
||||||
|
$item->price,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
5000 * 2,
|
||||||
|
$cart->getTotal(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the scenario where pool has LOWEST pricing strategy.
|
||||||
|
* Pool's price should be used when it's lower than single item prices.
|
||||||
|
*/
|
||||||
|
#[Test]
|
||||||
|
public function it_uses_pool_default_price_when_lower_than_single_prices()
|
||||||
|
{
|
||||||
|
// Create pool with default price: 2500 (€25/day)
|
||||||
|
$this->pool = Product::factory()->withPrices(unit_amount: 2500)->create([
|
||||||
|
'slug' => 'parking-pool',
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'sku' => 'PARK-POOL',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'status' => ProductStatus::PUBLISHED,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create singles with HIGHER prices: 5000 (€50/day)
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 2; $i++) {
|
||||||
|
$singles[] = Product::factory()->withStocks(1)->withPrices(unit_amount: 5000)->create([
|
||||||
|
'slug' => "spot-{$i}",
|
||||||
|
'name' => "Spot {$i}",
|
||||||
|
'sku' => "SPOT-{$i}",
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'status' => ProductStatus::PUBLISHED,
|
||||||
|
'manage_stock' => true,
|
||||||
|
'parent_id' => $this->pool->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pool->attachSingleItems(array_column($singles, 'id'));
|
||||||
|
$this->pool->setPricingStrategy(\Blax\Shop\Enums\PricingStrategy::LOWEST);
|
||||||
|
|
||||||
|
// Add pool items with dates directly
|
||||||
|
$from = Carbon::tomorrow()->startOfDay();
|
||||||
|
$until = Carbon::tomorrow()->addDay()->startOfDay(); // 1 day
|
||||||
|
|
||||||
|
$this->cart->addToCart($this->pool, 2, [], $from, $until);
|
||||||
|
|
||||||
|
// Each item should be 2500 for 1 day
|
||||||
|
$cart = $this->cart->fresh();
|
||||||
|
$this->assertEquals(
|
||||||
|
5000 * 2,
|
||||||
|
$cart->getTotal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($cart->items as $item) {
|
||||||
|
$this->assertEquals(5000, $item->price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that adding without dates then setting dates later works correctly.
|
||||||
|
*/
|
||||||
|
#[Test]
|
||||||
|
public function it_correctly_updates_prices_when_dates_are_set_after_adding_to_cart()
|
||||||
|
{
|
||||||
|
// Create pool with price 2500
|
||||||
|
$this->pool = Product::factory()->withPrices(unit_amount: 2500)->create([
|
||||||
|
'slug' => 'parking-pool',
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'sku' => 'PARK-POOL',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'status' => ProductStatus::PUBLISHED,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create single with price 5000
|
||||||
|
$single1 = Product::factory()
|
||||||
|
->withPrices(unit_amount: 5000)
|
||||||
|
->withStocks(1)
|
||||||
|
->create([
|
||||||
|
'slug' => 'spot-1',
|
||||||
|
'name' => 'Spot 1',
|
||||||
|
'sku' => 'SPOT-1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'status' => ProductStatus::PUBLISHED,
|
||||||
|
'manage_stock' => true,
|
||||||
|
'parent_id' => $this->pool->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$single2 = Product::factory()
|
||||||
|
->withStocks(1)
|
||||||
|
->create([
|
||||||
|
'slug' => 'spot-2',
|
||||||
|
'name' => 'Spot 2',
|
||||||
|
'sku' => 'SPOT-2',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'status' => ProductStatus::PUBLISHED,
|
||||||
|
'manage_stock' => true,
|
||||||
|
'parent_id' => $this->pool->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->pool->attachSingleItems([$single1->id, $single2->id]);
|
||||||
|
$this->pool->setPricingStrategy(\Blax\Shop\Enums\PricingStrategy::LOWEST);
|
||||||
|
|
||||||
|
// Refresh pool to clear relationship cache after attaching singles
|
||||||
|
$this->pool = $this->pool->fresh();
|
||||||
|
|
||||||
|
// Add without dates
|
||||||
|
$this->cart->addToCart($this->pool, 2);
|
||||||
|
|
||||||
|
// Use latest('id') instead of latest() because both items have same created_at timestamp
|
||||||
|
$item1 = $this->cart->items()->first();
|
||||||
|
$this->assertEquals(2500, $item1->price, 'First item should use pool fallback price (2500) for Single2 which has no price');
|
||||||
|
|
||||||
|
$item2 = $this->cart->items()->latest('id')->first();
|
||||||
|
$this->assertEquals(5000, $item2->price, 'Second item should use Single1 own price (5000)');
|
||||||
|
|
||||||
|
// Now set dates for 2 days
|
||||||
|
$from = Carbon::tomorrow()->startOfDay();
|
||||||
|
$until = Carbon::tomorrow()->addDays(2)->startOfDay(); // 2 days
|
||||||
|
|
||||||
|
$this->cart->setDates($from, $until, validateAvailability: false);
|
||||||
|
|
||||||
|
// After setting dates for 2 days:
|
||||||
|
// - First item (Single2 with pool fallback 2500/day): 2500 × 2 = 5000
|
||||||
|
// - Second item (Single1 with own price 5000/day): 5000 × 2 = 10000
|
||||||
|
$item1 = $item1->fresh();
|
||||||
|
$item2 = $item2->fresh();
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
2500 * 2, // 5000
|
||||||
|
$item1->price,
|
||||||
|
'First item should be pool fallback price (2500) × 2 days = 5000'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
5000 * 2, // 10000
|
||||||
|
$item2->price,
|
||||||
|
'Second item should be own price (5000) × 2 days = 10000'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Asser correct cart total
|
||||||
|
$this->assertEquals(
|
||||||
|
(2500 * 2) + (5000 * 2), // 5000 + 10000 = 15000
|
||||||
|
$this->cart->getTotal(),
|
||||||
|
'Cart total should be sum of both items after date update'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update dates to 1 day
|
||||||
|
$until = $until->addDay();
|
||||||
|
$this->cart->setDates($from, $until);
|
||||||
|
|
||||||
|
// After updating to 3 days:
|
||||||
|
// - First item: 2500 × 3 = 7500
|
||||||
|
// - Second item: 5000 × 3 = 15000
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
2500 * 3, // 7500
|
||||||
|
$item1->fresh()->price,
|
||||||
|
'First item should be pool fallback price (2500) × 3 days = 7500'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature\Stripe;
|
||||||
|
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
use Blax\Shop\Models\PaymentProviderIdentity;
|
use Blax\Shop\Models\PaymentProviderIdentity;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue