laravel-shop/tests/Feature/PoolProductCheckoutTest.php

433 lines
13 KiB
PHP
Raw Normal View History

<?php
namespace Blax\Shop\Tests\Feature;
use Blax\Shop\Enums\ProductRelationType;
use Blax\Shop\Enums\ProductType;
use Blax\Shop\Models\Product;
use Blax\Shop\Models\ProductPrice;
use Blax\Shop\Models\ProductPurchase;
use Blax\Shop\Tests\TestCase;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Workbench\App\Models\User;
class PoolProductCheckoutTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Product $hotelRoom;
protected Product $parkingPool;
protected Product $parkingSpot1;
protected Product $parkingSpot2;
protected Product $parkingSpot3;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
// Create hotel room
$this->hotelRoom = Product::factory()->create([
'name' => 'Hotel Room',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$this->hotelRoom->increaseStock(5);
ProductPrice::factory()->create([
'purchasable_id' => $this->hotelRoom->id,
'purchasable_type' => Product::class,
'unit_amount' => 10000,
'is_default' => true,
]);
// Create parking pool
$this->parkingPool = Product::factory()->create([
'name' => 'Parking Spaces',
'type' => ProductType::POOL,
'manage_stock' => false,
]);
ProductPrice::factory()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'unit_amount' => 2000,
'is_default' => true,
]);
// Create parking spots
$this->parkingSpot1 = Product::factory()->create([
'name' => 'Spot 1',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$this->parkingSpot1->increaseStock(1);
$this->parkingSpot2 = Product::factory()->create([
'name' => 'Spot 2',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$this->parkingSpot2->increaseStock(1);
$this->parkingSpot3 = Product::factory()->create([
'name' => 'Spot 3',
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$this->parkingSpot3->increaseStock(1);
// Link spots to pool
foreach ([$this->parkingSpot1, $this->parkingSpot2, $this->parkingSpot3] as $spot) {
$this->parkingPool->productRelations()->attach($spot->id, [
'type' => ProductRelationType::SINGLE->value,
]);
}
}
/** @test */
public function checkout_cart_with_pool_product_claims_correct_single_items()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$cart = $this->user->currentCart();
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 2,
'price' => 20.00,
'from' => $from,
'until' => $until,
]);
$cart->checkout();
// Count claimed spots
$claimedCount = 0;
foreach ([$this->parkingSpot1, $this->parkingSpot2, $this->parkingSpot3] as $spot) {
if (!$spot->isAvailableForBooking($from, $until, 1)) {
$claimedCount++;
}
}
$this->assertEquals(2, $claimedCount);
}
/** @test */
public function checkout_cart_with_pool_product_without_timespan_throws_exception_when_single_items_are_bookings()
{
$cart = $this->user->currentCart();
// Add pool product without timespan
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 1,
'price' => 20.00,
]);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('is missing required information: from, until');
$cart->checkout();
}
/** @test */
public function checkout_cart_with_pool_product_and_timespan_succeeds()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$cart = $this->user->currentCart();
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 2,
'price' => 20.00,
'from' => $from,
'until' => $until,
]);
$cart->checkout();
$this->assertTrue($cart->isConverted());
$this->assertCount(1, $cart->purchases);
}
/** @test */
public function checkout_cart_with_pool_product_stores_claimed_items_in_cart_item_meta()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$cart = $this->user->currentCart();
$cartItem = $cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 2,
'price' => 20.00,
'from' => $from,
'until' => $until,
]);
$cart->checkout();
$cartItem->refresh();
$meta = $cartItem->getMeta();
$claimedItems = $meta->claimed_single_items ?? null;
$this->assertNotNull($claimedItems);
$this->assertIsArray($claimedItems);
$this->assertCount(2, $claimedItems);
// Verify claimed items are valid product IDs
foreach ($claimedItems as $itemId) {
$this->assertNotNull(Product::find($itemId));
}
}
/** @test */
public function checkout_cart_with_multiple_pool_products_claims_from_each_independently()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
// Create second pool
$bikePool = Product::factory()->create([
'name' => 'Bike Rentals',
'type' => ProductType::POOL,
'manage_stock' => false,
]);
ProductPrice::factory()->create([
'purchasable_id' => $bikePool->id,
'purchasable_type' => Product::class,
'unit_amount' => 1500,
'is_default' => true,
]);
$bike1 = Product::factory()->create([
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$bike1->increaseStock(1);
$bike2 = Product::factory()->create([
'type' => ProductType::BOOKING,
'manage_stock' => true,
]);
$bike2->increaseStock(1);
$bikePool->productRelations()->attach($bike1->id, [
'type' => ProductRelationType::SINGLE->value,
]);
$bikePool->productRelations()->attach($bike2->id, [
'type' => ProductRelationType::SINGLE->value,
]);
$cart = $this->user->currentCart();
// Add parking
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 2,
'price' => 20.00,
'from' => $from,
'until' => $until,
]);
// Add bikes
$cart->items()->create([
'purchasable_id' => $bikePool->id,
'purchasable_type' => Product::class,
'quantity' => 1,
'price' => 15.00,
'from' => $from,
'until' => $until,
]);
$cart->checkout();
// Verify parking claims
$this->assertEquals(1, $this->parkingPool->getPoolMaxQuantity($from, $until));
// Verify bike claims
$this->assertEquals(1, $bikePool->getPoolMaxQuantity($from, $until));
}
/** @test */
public function checkout_cart_with_pool_product_and_regular_booking_product_succeeds()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$cart = $this->user->currentCart();
// Add hotel room
$cart->items()->create([
'purchasable_id' => $this->hotelRoom->id,
'purchasable_type' => Product::class,
'quantity' => 1,
'price' => 100.00,
'from' => $from,
'until' => $until,
]);
// Add parking
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 1,
'price' => 20.00,
'from' => $from,
'until' => $until,
]);
$cart->checkout();
$this->assertTrue($cart->isConverted());
$this->assertCount(2, $cart->purchases);
}
/** @test */
public function checkout_cart_with_pool_product_fails_when_single_item_becomes_unavailable_during_checkout()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$cart = $this->user->currentCart();
// Add 3 parking spots (all available)
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 3,
'price' => 20.00,
'from' => $from,
'until' => $until,
]);
// Simulate another user booking spots before checkout
$this->parkingSpot1->claimStock(1, null, $from, $until);
$this->parkingSpot2->claimStock(1, null, $from, $until);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Failed to checkout pool product');
$cart->checkout();
}
/** @test */
public function checkout_cart_validates_timespan_before_claiming_stock()
{
$cart = $this->user->currentCart();
// Add pool product without timespan
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 1,
'price' => 20.00,
// No from/until
]);
$this->expectException(\Exception::class);
$cart->checkout();
// Verify no stock was claimed if validation failed
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$this->assertEquals(3, $this->parkingPool->getPoolMaxQuantity($from, $until));
}
/** @test */
public function checkout_creates_purchase_with_correct_timespan()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$cart = $this->user->currentCart();
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 1,
'price' => 20.00,
'from' => $from,
'until' => $until,
]);
$cart->checkout();
$purchase = ProductPurchase::where('cart_id', $cart->id)->first();
$this->assertNotNull($purchase);
$this->assertEquals($from->format('Y-m-d H:i:s'), $purchase->from->format('Y-m-d H:i:s'));
$this->assertEquals($until->format('Y-m-d H:i:s'), $purchase->until->format('Y-m-d H:i:s'));
}
/** @test */
public function checkout_with_pool_product_using_legacy_parameters()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$cart = $this->user->currentCart();
// Use legacy parameters instead of from/until fields
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 1,
'price' => 20.00,
'from' => $from,
'until' => $until,
'parameters' => [
'from' => $from->toDateTimeString(),
'until' => $until->toDateTimeString(),
],
]);
$cart->checkout();
$this->assertTrue($cart->isConverted());
}
/** @test */
public function checkout_pool_product_claims_stock_with_cart_reference()
{
$from = Carbon::now()->addDays(1);
$until = Carbon::now()->addDays(3);
$cart = $this->user->currentCart();
$cart->items()->create([
'purchasable_id' => $this->parkingPool->id,
'purchasable_type' => Product::class,
'quantity' => 2,
'price' => 20.00,
'from' => $from,
'until' => $until,
]);
$cart->checkout();
// Verify claims have cart as reference
$spot1Claim = $this->parkingSpot1->stocks()
->where('reference_type', get_class($cart))
->where('reference_id', $cart->id)
->first();
// At least one spot should have the cart as reference
$this->assertTrue(
$spot1Claim !== null ||
$this->parkingSpot2->stocks()->where('reference_type', get_class($cart))->exists() ||
$this->parkingSpot3->stocks()->where('reference_type', get_class($cart))->exists()
);
}
}