2025-11-25 16:14:00 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Blax\Shop\Tests\Unit;
|
|
|
|
|
|
2025-12-03 12:59:01 +00:00
|
|
|
use Blax\Shop\Enums\CartStatus;
|
2025-11-25 16:14:00 +00:00
|
|
|
use Blax\Shop\Models\Cart;
|
|
|
|
|
use Blax\Shop\Models\Product;
|
|
|
|
|
use Blax\Shop\Models\ProductPrice;
|
|
|
|
|
use Blax\Shop\Tests\TestCase;
|
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
2025-12-24 18:40:10 +00:00
|
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
|
|
|
|
2025-11-25 16:14:00 +00:00
|
|
|
|
|
|
|
|
class CartTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use RefreshDatabase;
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_can_add_product_price_directly()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create();
|
2025-12-15 11:28:15 +00:00
|
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create();
|
|
|
|
|
$price = $product->defaultPrice()->first();
|
2025-11-25 16:14:00 +00:00
|
|
|
|
|
|
|
|
$cartItem = $cart->addToCart($price, quantity: 2);
|
|
|
|
|
|
|
|
|
|
$this->assertNotNull($cartItem);
|
|
|
|
|
$this->assertEquals(2, $cartItem->quantity);
|
|
|
|
|
$this->assertEquals(100.00, $cartItem->price);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_calculates_subtotal_automatically()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create();
|
2025-12-15 11:28:15 +00:00
|
|
|
$product = Product::factory()->withPrices(unit_amount: 50.00)->create();
|
|
|
|
|
$price = $product->defaultPrice()->first();
|
2025-11-25 16:14:00 +00:00
|
|
|
|
|
|
|
|
$cartItem = $cart->addToCart($price, quantity: 3);
|
|
|
|
|
|
|
|
|
|
$this->assertEquals(150.00, $cartItem->subtotal);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_respects_sale_prices()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create();
|
2025-12-03 12:59:01 +00:00
|
|
|
$product = Product::factory()->withPrices(1, 50)->create([
|
|
|
|
|
'sale_start' => now()->subDay(),
|
2025-11-29 07:32:58 +00:00
|
|
|
'sale_end' => now()->addDay(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$product->prices()->first()->update([
|
|
|
|
|
'is_default' => false,
|
|
|
|
|
]);
|
|
|
|
|
|
2025-12-15 11:28:15 +00:00
|
|
|
// Create a second price using factory
|
|
|
|
|
$price = ProductPrice::factory()->create([
|
2025-11-25 16:14:00 +00:00
|
|
|
'purchasable_id' => $product->id,
|
|
|
|
|
'purchasable_type' => get_class($product),
|
|
|
|
|
'unit_amount' => 100.00,
|
|
|
|
|
'sale_unit_amount' => 80.00,
|
|
|
|
|
'currency' => 'USD',
|
2025-11-29 07:32:58 +00:00
|
|
|
'is_default' => true,
|
2025-11-25 16:14:00 +00:00
|
|
|
]);
|
|
|
|
|
|
2025-11-29 07:32:58 +00:00
|
|
|
// Assert product has price
|
|
|
|
|
$this->assertTrue($product->hasPrice());
|
|
|
|
|
$this->assertEquals(2, $product->prices()->count());
|
|
|
|
|
|
|
|
|
|
$cartItem = $cart->addToCart($product, quantity: 1);
|
2025-11-25 16:14:00 +00:00
|
|
|
|
2025-11-29 07:32:58 +00:00
|
|
|
$this->assertEquals(80.00, $cartItem->getSubtotal());
|
2025-11-25 16:14:00 +00:00
|
|
|
$this->assertEquals(100.00, $cartItem->regular_price);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_can_add_items_with_custom_parameters()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create();
|
2025-12-15 11:28:15 +00:00
|
|
|
$product = Product::factory()->withPrices(unit_amount: 50.00)->create();
|
|
|
|
|
$price = $product->defaultPrice()->first();
|
2025-11-25 16:14:00 +00:00
|
|
|
|
|
|
|
|
$parameters = [
|
|
|
|
|
'color' => 'red',
|
|
|
|
|
'size' => 'medium',
|
|
|
|
|
'engraving' => 'Custom text',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$cartItem = $cart->addToCart($price, quantity: 1, parameters: $parameters);
|
|
|
|
|
|
|
|
|
|
$this->assertEquals('red', $cartItem->parameters['color']);
|
|
|
|
|
$this->assertEquals('medium', $cartItem->parameters['size']);
|
|
|
|
|
$this->assertEquals('Custom text', $cartItem->parameters['engraving']);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_total_sums_all_items()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create();
|
2025-12-03 12:59:01 +00:00
|
|
|
|
2025-12-15 11:28:15 +00:00
|
|
|
$product1 = Product::factory()->withPrices(unit_amount: 25.00)->create();
|
|
|
|
|
$price1 = $product1->defaultPrice()->first();
|
2025-11-25 16:14:00 +00:00
|
|
|
|
2025-12-15 11:28:15 +00:00
|
|
|
$product2 = Product::factory()->withPrices(unit_amount: 50.00)->create();
|
|
|
|
|
$price2 = $product2->defaultPrice()->first();
|
2025-11-25 16:14:00 +00:00
|
|
|
|
|
|
|
|
$cart->addToCart($price1, quantity: 2); // 50
|
|
|
|
|
$cart->addToCart($price2, quantity: 3); // 150
|
|
|
|
|
|
|
|
|
|
$total = $cart->fresh()->getTotal();
|
|
|
|
|
|
|
|
|
|
$this->assertEquals(200.00, $total);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_tracks_last_activity()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create([
|
|
|
|
|
'last_activity_at' => now()->subHours(2),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertNotNull($cart->last_activity_at);
|
|
|
|
|
$this->assertTrue($cart->last_activity_at->isPast());
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_can_be_converted()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create();
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($cart->isConverted());
|
|
|
|
|
|
|
|
|
|
$cart->update(['converted_at' => now()]);
|
|
|
|
|
|
|
|
|
|
$this->assertTrue($cart->fresh()->isConverted());
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function active_scope_filters_correctly()
|
|
|
|
|
{
|
|
|
|
|
// Active cart (not expired, not converted)
|
|
|
|
|
Cart::create([
|
|
|
|
|
'expires_at' => now()->addDay(),
|
|
|
|
|
'converted_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Expired cart
|
|
|
|
|
Cart::create([
|
|
|
|
|
'expires_at' => now()->subDay(),
|
|
|
|
|
'converted_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Converted cart
|
|
|
|
|
Cart::create([
|
|
|
|
|
'expires_at' => now()->addDay(),
|
|
|
|
|
'converted_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Permanent cart (no expiry)
|
|
|
|
|
Cart::create([
|
|
|
|
|
'expires_at' => null,
|
|
|
|
|
'converted_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$active = Cart::active()->get();
|
|
|
|
|
|
|
|
|
|
$this->assertCount(2, $active);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_deletes_items_on_deletion()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create();
|
2025-12-15 11:28:15 +00:00
|
|
|
$product = Product::factory()->withPrices(unit_amount: 50.00)->create();
|
|
|
|
|
$price = $product->defaultPrice()->first();
|
2025-11-25 16:14:00 +00:00
|
|
|
|
|
|
|
|
$cartItem = $cart->addToCart($price);
|
|
|
|
|
$cartItemId = $cartItem->id;
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('cart_items', ['id' => $cartItemId]);
|
|
|
|
|
|
|
|
|
|
$cart->delete();
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseMissing('cart_items', ['id' => $cartItemId]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_can_have_currency()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create([
|
|
|
|
|
'currency' => 'EUR',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertEquals('EUR', $cart->currency);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_can_have_status()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create([
|
2025-12-03 12:59:01 +00:00
|
|
|
'status' => CartStatus::ACTIVE,
|
2025-11-25 16:14:00 +00:00
|
|
|
]);
|
|
|
|
|
|
2025-12-03 12:59:01 +00:00
|
|
|
$this->assertEquals(CartStatus::ACTIVE, $cart->status);
|
2025-11-25 16:14:00 +00:00
|
|
|
|
2025-12-03 12:59:01 +00:00
|
|
|
$cart->update(['status' => CartStatus::CONVERTED]);
|
2025-11-25 16:14:00 +00:00
|
|
|
|
2025-12-03 12:59:01 +00:00
|
|
|
$this->assertEquals(CartStatus::CONVERTED, $cart->fresh()->status);
|
2025-11-25 16:14:00 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_can_store_metadata()
|
|
|
|
|
{
|
|
|
|
|
$cart = Cart::create([
|
|
|
|
|
'meta' => [
|
|
|
|
|
'coupon_code' => 'SAVE10',
|
|
|
|
|
'notes' => 'Gift wrapped',
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertEquals('SAVE10', $cart->meta->coupon_code);
|
|
|
|
|
$this->assertEquals('Gift wrapped', $cart->meta->notes);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function cart_can_have_session_id()
|
|
|
|
|
{
|
|
|
|
|
$sessionId = 'sess_' . str()->random(40);
|
|
|
|
|
|
|
|
|
|
$cart = Cart::create([
|
|
|
|
|
'session_id' => $sessionId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertEquals($sessionId, $cart->session_id);
|
|
|
|
|
}
|
2025-12-18 11:21:29 +00:00
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-12-20 11:19:34 +00:00
|
|
|
public function checkout_session_link_throws_when_stripe_disabled()
|
2025-12-18 11:21:29 +00:00
|
|
|
{
|
|
|
|
|
config(['shop.stripe.enabled' => false]);
|
|
|
|
|
|
|
|
|
|
$cart = Cart::create();
|
|
|
|
|
|
2025-12-20 11:19:34 +00:00
|
|
|
// Now throws CartEmptyException (validation happens before stripe check)
|
|
|
|
|
$this->expectException(\Blax\Shop\Exceptions\CartEmptyException::class);
|
|
|
|
|
$cart->checkoutSessionLink();
|
2025-12-18 11:21:29 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-12-20 11:19:34 +00:00
|
|
|
public function checkout_session_link_throws_when_cart_empty()
|
2025-12-18 11:21:29 +00:00
|
|
|
{
|
|
|
|
|
config(['shop.stripe.enabled' => true]);
|
|
|
|
|
|
|
|
|
|
$cart = Cart::create();
|
|
|
|
|
|
2025-12-20 11:19:34 +00:00
|
|
|
// Now throws CartEmptyException instead of returning null
|
|
|
|
|
$this->expectException(\Blax\Shop\Exceptions\CartEmptyException::class);
|
|
|
|
|
$cart->checkoutSessionLink();
|
2025-12-18 11:21:29 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-12-20 11:19:34 +00:00
|
|
|
public function checkout_session_link_throws_when_cart_empty_even_with_meta()
|
2025-12-18 11:21:29 +00:00
|
|
|
{
|
|
|
|
|
config(['shop.stripe.enabled' => true]);
|
|
|
|
|
|
|
|
|
|
$cart = Cart::create([
|
|
|
|
|
'meta' => ['other_data' => 'value'],
|
|
|
|
|
]);
|
|
|
|
|
|
2025-12-20 11:19:34 +00:00
|
|
|
// Now throws CartEmptyException instead of returning null
|
|
|
|
|
$this->expectException(\Blax\Shop\Exceptions\CartEmptyException::class);
|
|
|
|
|
$cart->checkoutSessionLink();
|
2025-12-18 11:21:29 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-12-18 11:21:29 +00:00
|
|
|
public function checkout_session_link_returns_false_on_stripe_error()
|
|
|
|
|
{
|
|
|
|
|
config(['shop.stripe.enabled' => true]);
|
|
|
|
|
config(['services.stripe.secret' => 'sk_test_invalid']);
|
|
|
|
|
|
|
|
|
|
$cart = Cart::create([
|
|
|
|
|
'meta' => ['stripe_session_id' => 'cs_test_invalid'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Note: This test would require a real Stripe API call or advanced mocking
|
|
|
|
|
// to properly test the error scenario. For now, we verify the method exists
|
|
|
|
|
// and has the correct signature. Integration tests should cover actual Stripe errors.
|
|
|
|
|
$this->assertTrue(method_exists($cart, 'checkoutSessionLink'));
|
|
|
|
|
|
|
|
|
|
// The method signature should return string|null|false
|
|
|
|
|
$reflection = new \ReflectionMethod($cart, 'checkoutSessionLink');
|
|
|
|
|
$returnType = $reflection->getReturnType();
|
|
|
|
|
$this->assertNotNull($returnType);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-12-18 11:21:29 +00:00
|
|
|
public function checkout_session_link_returns_null_when_session_not_found()
|
|
|
|
|
{
|
|
|
|
|
config(['shop.stripe.enabled' => true]);
|
|
|
|
|
config(['services.stripe.secret' => 'sk_test_invalid']);
|
|
|
|
|
|
|
|
|
|
$cart = Cart::create([
|
|
|
|
|
'meta' => ['stripe_session_id' => 'cs_test_nonexistent'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// This test requires mocking Stripe, which would be done in integration tests
|
|
|
|
|
// For unit tests, we verify the method exists and handles the scenario
|
|
|
|
|
$this->assertTrue(method_exists($cart, 'checkoutSessionLink'));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-12-18 11:21:29 +00:00
|
|
|
public function checkout_session_link_handles_meta_as_array()
|
|
|
|
|
{
|
|
|
|
|
config(['shop.stripe.enabled' => true]);
|
|
|
|
|
|
|
|
|
|
$cart = Cart::create([
|
|
|
|
|
'meta' => ['stripe_session_id' => 'cs_test_123'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Verify meta is accessible
|
|
|
|
|
$this->assertEquals('cs_test_123', $cart->meta->stripe_session_id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-12-18 11:21:29 +00:00
|
|
|
public function checkout_session_link_handles_meta_as_object()
|
|
|
|
|
{
|
|
|
|
|
config(['shop.stripe.enabled' => true]);
|
|
|
|
|
|
|
|
|
|
$cart = Cart::create();
|
|
|
|
|
$cart->meta = (object)['stripe_session_id' => 'cs_test_456'];
|
|
|
|
|
$cart->save();
|
|
|
|
|
|
|
|
|
|
$cart = $cart->fresh();
|
|
|
|
|
|
|
|
|
|
// Verify meta is accessible
|
|
|
|
|
$this->assertEquals('cs_test_456', $cart->meta->stripe_session_id);
|
|
|
|
|
}
|
2025-11-25 16:14:00 +00:00
|
|
|
}
|