laravel-shop/tests/Feature/ProductionBugs/PoolPricingWithDatesBugTest...

293 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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'
);
}
}