IMU cart + test

This commit is contained in:
Fabian @ Blax Software 2025-12-18 12:21:29 +01:00
parent 2303b9f703
commit d13945a731
5 changed files with 629 additions and 84 deletions

View File

@ -48,6 +48,7 @@ return new class extends Migration
$table->index(['slug', 'status']); $table->index(['slug', 'status']);
$table->index(['featured', 'is_visible', 'status']); $table->index(['featured', 'is_visible', 'status']);
$table->index(['type', 'status', 'is_visible']);
$table->index('parent_id'); $table->index('parent_id');
$table->foreign('parent_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); $table->foreign('parent_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade');
}); });
@ -130,6 +131,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
$table->index('currency'); $table->index('currency');
$table->index(['purchasable_type', 'purchasable_id', 'is_default', 'active']);
}); });
} }
@ -168,6 +170,7 @@ return new class extends Migration
$table->index(['product_id', 'status']); $table->index(['product_id', 'status']);
$table->index(['reference_type', 'reference_id']); $table->index(['reference_type', 'reference_id']);
$table->index(['claimed_from', 'expires_at']); $table->index(['claimed_from', 'expires_at']);
$table->index(['product_id', 'type', 'status', 'claimed_from', 'expires_at']);
$table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); $table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade');
}); });
} }
@ -268,6 +271,7 @@ return new class extends Migration
$table->index(['session_id', 'status']); $table->index(['session_id', 'status']);
$table->index(['customer_type', 'customer_id', 'status']); $table->index(['customer_type', 'customer_id', 'status']);
$table->index(['status', 'last_activity_at']);
}); });
} }
@ -280,9 +284,10 @@ return new class extends Migration
$table->foreignUuid('purchase_id')->nullable()->constrained(config('shop.tables.product_purchases', 'product_purchases'))->nullOnDelete(); $table->foreignUuid('purchase_id')->nullable()->constrained(config('shop.tables.product_purchases', 'product_purchases'))->nullOnDelete();
$table->foreignUuid('price_id')->nullable()->constrained(config('shop.tables.product_prices', 'product_prices'))->nullOnDelete(); $table->foreignUuid('price_id')->nullable()->constrained(config('shop.tables.product_prices', 'product_prices'))->nullOnDelete();
$table->integer('quantity')->default(1); $table->integer('quantity')->default(1);
$table->decimal('price', 10, 2)->default(0); $table->integer('price')->default(0); // Stored in cents
$table->decimal('regular_price', 10, 2)->nullable(); $table->integer('regular_price')->nullable(); // Stored in cents
$table->decimal('subtotal', 10, 2); $table->integer('unit_amount')->nullable(); // Base unit price for 1 quantity, 1 day (in cents)
$table->integer('subtotal'); // Stored in cents
$table->json('parameters')->nullable(); $table->json('parameters')->nullable();
$table->json('meta')->nullable(); $table->json('meta')->nullable();
$table->timestamp('from')->nullable(); $table->timestamp('from')->nullable();
@ -290,6 +295,8 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
$table->index(['cart_id', 'purchasable_id']); $table->index(['cart_id', 'purchasable_id']);
$table->index(['cart_id', 'purchasable_type', 'purchasable_id']);
$table->index(['from', 'until']);
$table->foreign('cart_id')->references('id')->on(config('shop.tables.carts', 'carts'))->onDelete('cascade'); $table->foreign('cart_id')->references('id')->on(config('shop.tables.carts', 'carts'))->onDelete('cascade');
}); });
} }

View File

@ -56,6 +56,13 @@ class Cart extends Model
$this->table = config('shop.tables.carts', 'carts'); $this->table = config('shop.tables.carts', 'carts');
} }
protected static function booted()
{
static::deleting(function ($cart) {
$cart->items()->delete();
});
}
public function customer(): MorphTo public function customer(): MorphTo
{ {
return $this->morphTo(); return $this->morphTo();
@ -354,6 +361,16 @@ class Cart extends Model
} }
} }
/**
* Scope to find abandoned carts
* Carts that are active but haven't been updated recently
*/
public function scopeAbandoned($query, $inactiveMinutes = 60)
{
return $query->where('status', CartStatus::ACTIVE)
->where('last_activity_at', '<', now()->subMinutes($inactiveMinutes));
}
public function getUnpaidAmount(): float public function getUnpaidAmount(): float
{ {
$paidAmount = $this->purchases() $paidAmount = $this->purchases()
@ -409,13 +426,6 @@ class Cart extends Model
}); });
} }
protected static function booted()
{
static::deleting(function ($cart) {
$cart->items()->delete();
});
}
/** /**
* Store the cart ID in the session for retrieval across requests * Store the cart ID in the session for retrieval across requests
* *
@ -657,6 +667,9 @@ class Cart extends Model
throw new \Exception("Cart item price calculation resulted in null for '{$cartable->name}' (pricePerDay: {$pricePerDay}, days: {$days})"); throw new \Exception("Cart item price calculation resulted in null for '{$cartable->name}' (pricePerDay: {$pricePerDay}, days: {$days})");
} }
// Store the base unit_amount (price for 1 quantity, 1 day) in cents
$unitAmount = (int) round($pricePerDay);
// Calculate total price // Calculate total price
$totalPrice = $pricePerUnit * $quantity; $totalPrice = $pricePerUnit * $quantity;
@ -695,6 +708,7 @@ class Cart extends Model
'quantity' => $quantity, 'quantity' => $quantity,
'price' => $pricePerUnit, // Price per unit for the period 'price' => $pricePerUnit, // Price per unit for the period
'regular_price' => $regularPricePerUnit, 'regular_price' => $regularPricePerUnit,
'unit_amount' => $unitAmount, // Base price for 1 quantity, 1 day (in cents)
'subtotal' => $totalPrice, // Total for all units 'subtotal' => $totalPrice, // Total for all units
'parameters' => $parameters, 'parameters' => $parameters,
'from' => $from, 'from' => $from,
@ -802,6 +816,10 @@ class Cart extends Model
public function checkout(): static public function checkout(): static
{ {
return \DB::transaction(function () {
// Lock the cart to prevent concurrent checkouts
$this->lockForUpdate();
// Validate cart before proceeding // Validate cart before proceeding
$this->validateForCheckout(); $this->validateForCheckout();
@ -812,6 +830,12 @@ class Cart extends Model
// Create ProductPurchase for each cart item // Create ProductPurchase for each cart item
foreach ($items as $item) { foreach ($items as $item) {
$product = $item->purchasable; $product = $item->purchasable;
// Lock the product to prevent race conditions on stock
if ($product instanceof Product && method_exists($product, 'lockForUpdate')) {
$product = $product->lockForUpdate()->find($product->id);
}
$quantity = $item->quantity; $quantity = $item->quantity;
// Get booking dates from cart item directly (preferred) or from parameters (legacy) // Get booking dates from cart item directly (preferred) or from parameters (legacy)
@ -889,6 +913,7 @@ class Cart extends Model
]); ]);
return $this; return $this;
});
} }
/** /**
@ -1002,4 +1027,67 @@ class Cart extends Model
throw $e; throw $e;
} }
} }
/**
* Get the checkout session link for this cart.
*
* This method returns:
* - string: The checkout session URL if a session exists and is valid
* - null: If no session exists or Stripe is not enabled
* - false: If an error occurred while retrieving the session
*
* @return string|null|false
*/
public function checkoutSessionLink(): string|null|false
{
// Check if Stripe is enabled
if (!config('shop.stripe.enabled')) {
return null;
}
// Check if cart has a stored session ID in meta
$meta = $this->meta;
if (is_array($meta)) {
$meta = (object)$meta;
}
if (!isset($meta->stripe_session_id)) {
return null;
}
$sessionId = $meta->stripe_session_id;
try {
// Ensure Stripe is initialized
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
// Retrieve the session from Stripe
$session = \Stripe\Checkout\Session::retrieve($sessionId);
// Check if session is still valid (not expired)
// Stripe sessions expire after 24 hours
if (isset($session->expires_at) && $session->expires_at < time()) {
return null;
}
// Return the session URL
return $session->url ?? null;
} catch (\Stripe\Exception\InvalidRequestException $e) {
// Session not found or invalid
\Illuminate\Support\Facades\Log::warning('Stripe session not found', [
'cart_id' => $this->id,
'session_id' => $sessionId,
'error' => $e->getMessage(),
]);
return null;
} catch (\Exception $e) {
// Other errors
\Illuminate\Support\Facades\Log::error('Error retrieving Stripe checkout session', [
'cart_id' => $this->id,
'session_id' => $sessionId,
'error' => $e->getMessage(),
]);
return false;
}
}
} }

View File

@ -21,6 +21,7 @@ class CartItem extends Model
'quantity', 'quantity',
'price', 'price',
'regular_price', 'regular_price',
'unit_amount',
'subtotal', 'subtotal',
'parameters', 'parameters',
'purchase_id', 'purchase_id',
@ -33,6 +34,7 @@ class CartItem extends Model
'quantity' => 'integer', 'quantity' => 'integer',
'price' => 'integer', 'price' => 'integer',
'regular_price' => 'integer', 'regular_price' => 'integer',
'unit_amount' => 'integer',
'subtotal' => 'integer', 'subtotal' => 'integer',
'parameters' => 'array', 'parameters' => 'array',
'meta' => 'array', 'meta' => 'array',
@ -427,6 +429,9 @@ class CartItem extends Model
$pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until); $pricePerDay = $product->getCurrentPrice(null, $this->cart, $from, $until);
$regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until) ?? $pricePerDay; $regularPricePerDay = $product->getCurrentPrice(false, $this->cart, $from, $until) ?? $pricePerDay;
// Store the base unit_amount (price for 1 quantity, 1 day) in cents
$unitAmount = (int) round($pricePerDay);
// Calculate new prices and round to nearest cent for consistency // Calculate new prices and round to nearest cent for consistency
$pricePerUnit = (int) round($pricePerDay * $days); $pricePerUnit = (int) round($pricePerDay * $days);
$regularPricePerUnit = (int) round($regularPricePerDay * $days); $regularPricePerUnit = (int) round($regularPricePerDay * $days);
@ -436,6 +441,7 @@ class CartItem extends Model
'until' => $until, 'until' => $until,
'price' => $pricePerUnit, 'price' => $pricePerUnit,
'regular_price' => $regularPricePerUnit, 'regular_price' => $regularPricePerUnit,
'unit_amount' => $unitAmount,
'subtotal' => $pricePerUnit * $this->quantity, 'subtotal' => $pricePerUnit * $this->quantity,
]); ]);

342
tests/Unit/CartItemTest.php Normal file
View File

@ -0,0 +1,342 @@
<?php
namespace Blax\Shop\Tests\Unit;
use Blax\Shop\Enums\ProductType;
use Blax\Shop\Models\Cart;
use Blax\Shop\Models\CartItem;
use Blax\Shop\Models\Product;
use Blax\Shop\Models\ProductPrice;
use Blax\Shop\Tests\TestCase;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
class CartItemTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function cart_item_stores_prices_as_integers_in_cents()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 1550)->create(); // $15.50 in cents
$price = $product->defaultPrice()->first();
$cartItem = $cart->addToCart($price, quantity: 2);
// Prices should be stored as integers (cents)
$this->assertIsInt($cartItem->price);
$this->assertIsInt($cartItem->regular_price);
$this->assertIsInt($cartItem->subtotal);
$this->assertIsInt($cartItem->unit_amount);
// Verify values
$this->assertEquals(1550, $cartItem->price);
$this->assertEquals(1550, $cartItem->regular_price);
$this->assertEquals(1550, $cartItem->unit_amount);
$this->assertEquals(3100, $cartItem->subtotal); // 1550 * 2
}
/** @test */
public function cart_item_unit_amount_represents_base_price_per_day()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 5000)->create(); // $50.00 per day
$price = $product->defaultPrice()->first();
$cartItem = $cart->addToCart($price, quantity: 1);
// unit_amount should be the base price for 1 quantity, 1 day
$this->assertEquals(5000, $cartItem->unit_amount);
$this->assertEquals(5000, $cartItem->price); // Same as unit_amount for 1 day
}
/** @test */
public function cart_item_calculates_price_correctly_for_booking_timespan()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 2000)->create(['type' => ProductType::BOOKING]); // $20.00 per day
$price = $product->defaultPrice()->first();
$from = Carbon::parse('2025-01-01 00:00:00');
$until = Carbon::parse('2025-01-04 00:00:00'); // 3 days
$cartItem = $cart->addToCart($price, quantity: 1, from: $from, until: $until);
// unit_amount should still be the daily rate
$this->assertEquals(2000, $cartItem->unit_amount);
// price should be unit_amount * days (3 days)
$this->assertEquals(6000, $cartItem->price); // 2000 * 3
// subtotal should be price * quantity
$this->assertEquals(6000, $cartItem->subtotal);
}
/** @test */
public function cart_item_calculates_price_with_partial_days()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 4800)->create(['type' => ProductType::BOOKING]); // $48.00 per day
$price = $product->defaultPrice()->first();
// 12 hours = 0.5 days
$from = Carbon::parse('2025-01-01 00:00:00');
$until = Carbon::parse('2025-01-01 12:00:00');
$cartItem = $cart->addToCart($price, quantity: 1, from: $from, until: $until);
$this->assertEquals(4800, $cartItem->unit_amount);
// Price should be approximately 2400 (4800 * 0.5 days)
// Allow small rounding differences
$this->assertEqualsWithDelta(2400, $cartItem->price, 1);
}
/** @test */
public function cart_item_handles_multiple_quantities_correctly()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 1000)->create();
$price = $product->defaultPrice()->first();
$cartItem = $cart->addToCart($price, quantity: 5);
$this->assertEquals(1000, $cartItem->unit_amount);
$this->assertEquals(1000, $cartItem->price); // Price per unit
$this->assertEquals(5000, $cartItem->subtotal); // 1000 * 5
}
/** @test */
public function cart_item_updates_prices_when_dates_change()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 3000)->withStocks(10)->create(['type' => ProductType::BOOKING]); // $30.00 per day
$from = Carbon::parse('2025-01-01 00:00:00');
$until = Carbon::parse('2025-01-02 00:00:00'); // 1 day
$cartItem = $cart->addToCart($product, quantity: 1, from: $from, until: $until);
// Initial state: 1 day
$this->assertEquals(3000, $cartItem->unit_amount);
$this->assertEquals(3000, $cartItem->price);
$this->assertEquals(3000, $cartItem->subtotal);
// Update to 5 days
$newUntil = Carbon::parse('2025-01-06 00:00:00');
$cartItem->updateDates($from, $newUntil);
// Refresh to get updated values
$cartItem = $cartItem->fresh();
// unit_amount should remain the same (daily rate)
$this->assertEquals(3000, $cartItem->unit_amount);
// price should now be for 5 days
$this->assertEquals(15000, $cartItem->price); // 3000 * 5
// subtotal should update accordingly
$this->assertEquals(15000, $cartItem->subtotal);
}
/** @test */
public function cart_item_handles_fractional_days_with_multiple_quantities()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 2400)->create(['type' => ProductType::BOOKING]); // $24.00 per day
$price = $product->defaultPrice()->first();
// 1.5 days
$from = Carbon::parse('2025-01-01 00:00:00');
$until = Carbon::parse('2025-01-02 12:00:00');
$cartItem = $cart->addToCart($price, quantity: 3, from: $from, until: $until);
$this->assertEquals(2400, $cartItem->unit_amount);
// Price per unit should be 2400 * 1.5 = 3600
$this->assertEqualsWithDelta(3600, $cartItem->price, 1);
// Subtotal should be 3600 * 3 = 10800
$this->assertEqualsWithDelta(10800, $cartItem->subtotal, 3);
}
/** @test */
public function cart_item_subtotal_recalculates_on_quantity_change()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 1500)->create();
$price = $product->defaultPrice()->first();
$cartItem = $cart->addToCart($price, quantity: 2);
$this->assertEquals(3000, $cartItem->subtotal); // 1500 * 2
// Update quantity
$cartItem->update(['quantity' => 5]);
$this->assertEquals(7500, $cartItem->fresh()->subtotal); // 1500 * 5
}
/** @test */
public function cart_item_stores_unit_amount_for_non_booking_products()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 9999)->create(['type' => ProductType::SIMPLE]);
$price = $product->defaultPrice()->first();
$cartItem = $cart->addToCart($price, quantity: 1);
// Even for non-booking products, unit_amount should be set
$this->assertEquals(9999, $cartItem->unit_amount);
$this->assertEquals(9999, $cartItem->price);
$this->assertEquals(9999, $cartItem->subtotal);
}
/** @test */
public function cart_item_handles_sale_prices_in_cents()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(1, 50)->create([
'sale_start' => now()->subDay(),
'sale_end' => now()->addDay(),
]);
$product->prices()->first()->update([
'is_default' => false,
]);
// Create a sale price
$price = ProductPrice::factory()->create([
'purchasable_id' => $product->id,
'purchasable_type' => get_class($product),
'unit_amount' => 10000, // $100.00 regular
'sale_unit_amount' => 7500, // $75.00 sale
'currency' => 'USD',
'is_default' => true,
]);
$cartItem = $cart->addToCart($product, quantity: 1);
// Should use sale price
$this->assertEquals(7500, $cartItem->price);
$this->assertEquals(7500, $cartItem->unit_amount);
// Regular price should be stored too
$this->assertEquals(10000, $cartItem->regular_price);
$this->assertEquals(7500, $cartItem->subtotal);
}
/** @test */
public function cart_item_rounds_prices_consistently()
{
$cart = Cart::create();
// Create a product with a price that will result in fractional cents when multiplied
$product = Product::factory()->withPrices(unit_amount: 3333)->create(['type' => ProductType::BOOKING]); // $33.33
$price = $product->defaultPrice()->first();
// 1.5 days should give 3333 * 1.5 = 4999.5 cents
$from = Carbon::parse('2025-01-01 00:00:00');
$until = Carbon::parse('2025-01-02 12:00:00');
$cartItem = $cart->addToCart($price, quantity: 1, from: $from, until: $until);
// Should round to nearest cent
$this->assertIsInt($cartItem->price);
$this->assertEquals(5000, $cartItem->price); // Rounded to 5000
}
/** @test */
public function cart_item_unit_amount_remains_constant_across_date_updates()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 4500)->withStocks(10)->create(['type' => ProductType::BOOKING]);
$from = Carbon::parse('2025-01-01');
$until = Carbon::parse('2025-01-02'); // 1 day
$cartItem = $cart->addToCart($product, quantity: 1, from: $from, until: $until);
$originalUnitAmount = $cartItem->unit_amount;
$this->assertEquals(4500, $originalUnitAmount);
// Update to different date range (7 days)
$cartItem->updateDates($from, Carbon::parse('2025-01-08'));
$cartItem = $cartItem->fresh();
// unit_amount should stay the same
$this->assertEquals($originalUnitAmount, $cartItem->unit_amount);
// But price should change
$this->assertEquals(31500, $cartItem->price); // 4500 * 7
}
/** @test */
public function cart_item_validates_database_storage_as_integer()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 2550)->create();
$price = $product->defaultPrice()->first();
$cartItem = $cart->addToCart($price, quantity: 2);
// Fetch directly from database to verify storage type
$dbItem = \DB::table('cart_items')->where('id', $cartItem->id)->first();
// Database should store as integer, not decimal
$this->assertIsInt($dbItem->price);
$this->assertIsInt($dbItem->regular_price);
$this->assertIsInt($dbItem->subtotal);
$this->assertIsInt($dbItem->unit_amount);
}
/** @test */
public function cart_item_getSubtotal_method_returns_correct_value()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 1234)->create();
$price = $product->defaultPrice()->first();
$cartItem = $cart->addToCart($price, quantity: 3);
// getSubtotal() method should return the same as subtotal property
$this->assertEquals($cartItem->subtotal, $cartItem->getSubtotal());
$this->assertEquals(3702, $cartItem->getSubtotal()); // 1234 * 3
}
/** @test */
public function cart_item_handles_zero_prices()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 0)->create(); // Free product
$price = $product->defaultPrice()->first();
$cartItem = $cart->addToCart($price, quantity: 5);
$this->assertEquals(0, $cartItem->unit_amount);
$this->assertEquals(0, $cartItem->price);
$this->assertEquals(0, $cartItem->regular_price);
$this->assertEquals(0, $cartItem->subtotal);
}
/** @test */
public function cart_item_handles_very_long_booking_periods()
{
$cart = Cart::create();
$product = Product::factory()->withPrices(unit_amount: 500)->create(['type' => ProductType::BOOKING]); // $5.00 per day
$price = $product->defaultPrice()->first();
// 365 days (1 year)
$from = Carbon::parse('2025-01-01');
$until = Carbon::parse('2026-01-01');
$cartItem = $cart->addToCart($price, quantity: 1, from: $from, until: $until);
$this->assertEquals(500, $cartItem->unit_amount);
$this->assertEquals(182500, $cartItem->price); // 500 * 365
$this->assertEquals(182500, $cartItem->subtotal);
}
}

View File

@ -232,4 +232,106 @@ class CartTest extends TestCase
$this->assertEquals($sessionId, $cart->session_id); $this->assertEquals($sessionId, $cart->session_id);
} }
/** @test */
public function checkout_session_link_returns_null_when_stripe_disabled()
{
config(['shop.stripe.enabled' => false]);
$cart = Cart::create();
$link = $cart->checkoutSessionLink();
$this->assertNull($link);
}
/** @test */
public function checkout_session_link_returns_null_when_no_session_exists()
{
config(['shop.stripe.enabled' => true]);
$cart = Cart::create();
$link = $cart->checkoutSessionLink();
$this->assertNull($link);
}
/** @test */
public function checkout_session_link_returns_null_when_session_id_empty()
{
config(['shop.stripe.enabled' => true]);
$cart = Cart::create([
'meta' => ['other_data' => 'value'],
]);
$link = $cart->checkoutSessionLink();
$this->assertNull($link);
}
/** @test */
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);
}
/** @test */
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'));
}
/** @test */
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);
}
/** @test */
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);
}
} }