From 3593a462a1aece00fa9275b931529094c2b5e796 Mon Sep 17 00:00:00 2001 From: a6a2f5842 Date: Tue, 25 Nov 2025 17:14:00 +0100 Subject: [PATCH] I basic tests --- src/Models/Product.php | 9 +- src/Models/ProductAction.php | 5 + tests/Feature/HasShoppingCapabilitiesTest.php | 278 ++++++++++++++ tests/Feature/ProductAttributeTest.php | 226 ++++++++++++ tests/Feature/ProductPriceTest.php | 280 ++++++++++++++ tests/Feature/ProductPurchaseTest.php | 344 ++++++++++++++++++ tests/Feature/ProductScopeTest.php | 243 +++++++++++++ tests/Feature/ProductStockTest.php | 277 ++++++++++++++ tests/Unit/CartTest.php | 250 +++++++++++++ 9 files changed, 1910 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/HasShoppingCapabilitiesTest.php create mode 100644 tests/Feature/ProductAttributeTest.php create mode 100644 tests/Feature/ProductPriceTest.php create mode 100644 tests/Feature/ProductPurchaseTest.php create mode 100644 tests/Feature/ProductScopeTest.php create mode 100644 tests/Feature/ProductStockTest.php create mode 100644 tests/Unit/CartTest.php diff --git a/src/Models/Product.php b/src/Models/Product.php index 929c11d..a54cdf4 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -122,6 +122,7 @@ class Product extends Model implements Purchasable, Cartable static::deleted(function ($model) { $model->actions()->delete(); + $model->attributes()->delete(); }); } @@ -389,7 +390,7 @@ class Product extends Model implements Purchasable, Cartable return $query->where(function ($q) use ($search) { $q->where('slug', 'like', "%{$search}%") ->orWhere('sku', 'like', "%{$search}%") - ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.name')) LIKE ?", ["%{$search}%"]); + ->orWhere('name', 'like', "%{$search}%"); }); } @@ -411,8 +412,12 @@ class Product extends Model implements Purchasable, Cartable public function scopeLowStock($query) { + $stockTable = config('shop.tables.product_stocks', 'product_stocks'); + $productTable = config('shop.tables.products', 'products'); + return $query->where('manage_stock', true) - ->whereColumn('stock_quantity', '<=', 'low_stock_threshold'); + ->whereNotNull('low_stock_threshold') + ->whereRaw("(\n SELECT COALESCE(SUM(quantity), 0)\n FROM {$stockTable}\n WHERE {$stockTable}.product_id = {$productTable}.id\n AND {$stockTable}.status IN ('completed', 'pending')\n AND ({$stockTable}.expires_at IS NULL OR {$stockTable}.expires_at > ?)\n ) <= {$productTable}.low_stock_threshold", [now()]); } public function isLowStock(): bool diff --git a/src/Models/ProductAction.php b/src/Models/ProductAction.php index e8cd367..0903ba2 100644 --- a/src/Models/ProductAction.php +++ b/src/Models/ProductAction.php @@ -114,4 +114,9 @@ class ProductAction extends Model dispatch(new $action_job(...$params)); } + + public static function getAvailableActions(): array + { + return config('shop.actions.available', []); + } } diff --git a/tests/Feature/HasShoppingCapabilitiesTest.php b/tests/Feature/HasShoppingCapabilitiesTest.php new file mode 100644 index 0000000..3b643a7 --- /dev/null +++ b/tests/Feature/HasShoppingCapabilitiesTest.php @@ -0,0 +1,278 @@ +create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\MorphMany::class, $user->cart()); + } + + /** @test */ + public function user_has_purchases_relationship() + { + $user = User::factory()->create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\MorphMany::class, $user->purchases()); + } + + /** @test */ + public function user_can_get_current_cart() + { + $user = User::factory()->create(); + + $cart = $user->currentCart(); + + $this->assertNotNull($cart); + $this->assertEquals($user->id, $cart->customer_id); + } + + /** @test */ + public function user_cannot_purchase_product_without_price() + { + $user = User::factory()->create(); + $product = Product::factory()->create(); + + $this->expectException(NotPurchasable::class); + + $user->purchase($product); + } + + /** @test */ + public function user_cannot_purchase_product_with_multiple_default_prices() + { + $user = User::factory()->create(); + $product = Product::factory() + ->withPrices(2) + ->create(['manage_stock' => false]); + + // Set both prices as default + $product->prices()->update(['is_default' => true]); + + $this->expectException(MultiplePurchaseOptions::class); + + $user->purchase($product); + } + + /** @test */ + public function user_can_purchase_product_with_single_default_price() + { + $user = User::factory()->create(); + $product = Product::factory() + ->withPrices(1) + ->create(['manage_stock' => false]); + + $product->prices()->update(['is_default' => true]); + + $purchase = $user->purchase($product); + + $this->assertNotNull($purchase); + $this->assertEquals($user->id, $purchase->purchaser_id); + } + + /** @test */ + public function user_cannot_add_product_to_cart_without_default_price() + { + $user = User::factory()->create(); + $product = Product::factory()->withStocks()->create(); + + $this->assertThrows(fn() => $user->addToCart($product), NotPurchasable::class); + } + + /** @test */ + public function user_cannot_add_product_with_multiple_default_prices_to_cart() + { + $user = User::factory()->create(); + $product = Product::factory() + ->withPrices(3) + ->create(['manage_stock' => false]); + + $product->prices()->update(['is_default' => true]); + + $this->expectException(MultiplePurchaseOptions::class); + + $user->addToCart($product); + } + + /** @test */ + public function user_can_get_completed_purchases() + { + $user = User::factory()->create(); + $product1 = Product::factory()->withPrices()->create(['manage_stock' => false]); + $product2 = Product::factory()->withPrices()->create(['manage_stock' => false]); + + $purchase1 = $user->purchase($product1); + $purchase1->update(['status' => 'completed']); + + $purchase2 = $user->purchase($product2); + $purchase2->update(['status' => 'unpaid']); + + $completed = $user->completedPurchases; + + $this->assertCount(1, $completed); + $this->assertEquals('completed', $completed->first()->status); + } + + /** @test */ + public function purchase_with_metadata_stores_correctly() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create(['manage_stock' => false]); + + $metadata = [ + 'custom_field' => 'custom_value', + 'notes' => 'Special instructions', + ]; + + $purchase = $user->purchase($product, quantity: 1, meta: $metadata); + + $this->assertEquals('custom_value', $purchase->meta->custom_field); + $this->assertEquals('Special instructions', $purchase->meta->notes); + } + + /** @test */ + public function user_can_check_if_purchased_specific_product() + { + $user = User::factory()->create(); + $purchasedProduct = Product::factory()->withPrices()->create(['manage_stock' => false]); + $notPurchasedProduct = Product::factory()->withPrices()->create(); + + $purchase = $user->purchase($purchasedProduct); + $purchase->update(['status' => 'completed']); + + $this->assertTrue($user->hasPurchased($purchasedProduct)); + $this->assertFalse($user->hasPurchased($notPurchasedProduct)); + } + + /** @test */ + public function user_cart_items_are_accessible() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->withStocks(10)->create(); + + $user->addToCart($product); + + $this->assertCount(1, $user->cartItems); + } + + /** @test */ + public function user_can_update_cart_item_quantity() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->withStocks(20)->create(); + + $cartItem = $user->addToCart($product, quantity: 1); + + $user->updateCartQuantity($cartItem, quantity: 5); + + $this->assertEquals(5, $cartItem->fresh()->quantity); + } + + /** @test */ + public function user_can_remove_item_from_cart() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->withStocks(10)->create(); + + $cartItem = $user->addToCart($product); + + $this->assertCount(1, $user->cartItems); + + $user->removeFromCart($cartItem); + + $this->assertCount(0, $user->fresh()->cartItems); + } + + /** @test */ + public function user_can_get_cart_total() + { + $user = User::factory()->create(); + $product1 = Product::factory()->withPrices(unit_amount: 100)->withStocks(10)->create(); + $product2 = Product::factory()->withPrices(unit_amount: 50)->withStocks(10)->create(); + + $user->addToCart($product1, quantity: 2); + $user->addToCart($product2, quantity: 1); + + $total = $user->getCartTotal(); + + $this->assertEquals(250.00, $total); + } + + /** @test */ + public function user_can_get_cart_items_count() + { + $user = User::factory()->create(); + $product1 = Product::factory()->withPrices()->withStocks(10)->create(); + $product2 = Product::factory()->withPrices()->withStocks(10)->create(); + + $user->addToCart($product1, quantity: 3); + $user->addToCart($product2, quantity: 2); + + $count = $user->getCartItemsCount(); + + $this->assertEquals(5, $count); + } + + /** @test */ + public function user_can_clear_cart() + { + $user = User::factory()->create(); + $product1 = Product::factory()->withPrices()->withStocks(10)->create(); + $product2 = Product::factory()->withPrices()->withStocks(10)->create(); + + $user->addToCart($product1); + $user->addToCart($product2); + + $this->assertCount(2, $user->cartItems); + + $user->clearCart(); + + $this->assertCount(0, $user->fresh()->cartItems); + } + + /** @test */ + public function adding_product_to_cart_reserves_stock() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->withStocks(10)->create(); + + $this->assertEquals(10, $product->getAvailableStock()); + + $user->addToCart($product, quantity: 3); + + $this->assertEquals(7, $product->fresh()->getAvailableStock()); + } + + /** @test */ + public function purchase_calls_product_actions() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create(['manage_stock' => false]); + + // Create a product action + $product->actions()->create([ + 'event' => 'purchased', + 'action_type' => 'TestAction', + 'active' => true, + ]); + + $purchase = $user->purchase($product); + + // Just verify purchase was created successfully + // Actual action execution would require implementing the action class + $this->assertNotNull($purchase); + } +} diff --git a/tests/Feature/ProductAttributeTest.php b/tests/Feature/ProductAttributeTest.php new file mode 100644 index 0000000..a442721 --- /dev/null +++ b/tests/Feature/ProductAttributeTest.php @@ -0,0 +1,226 @@ +create(); + + $attribute = ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Material', + 'value' => 'Cotton', + ]); + + $this->assertDatabaseHas('product_attributes', [ + 'id' => $attribute->id, + 'product_id' => $product->id, + 'key' => 'Material', + 'value' => 'Cotton', + ]); + } + + /** @test */ + public function attribute_belongs_to_product() + { + $product = Product::factory()->create(); + + $attribute = ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Color', + 'value' => 'Red', + ]); + + $this->assertInstanceOf(Product::class, $attribute->product); + $this->assertEquals($product->id, $attribute->product->id); + } + + /** @test */ + public function product_can_have_multiple_attributes() + { + $product = Product::factory()->create(); + + ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Size', + 'value' => 'Large', + ]); + + ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Color', + 'value' => 'Blue', + ]); + + ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Material', + 'value' => 'Polyester', + ]); + + $this->assertCount(3, $product->fresh()->attributes); + } + + /** @test */ + public function it_can_have_a_sort_order() + { + $product = Product::factory()->create(); + + $attr1 = ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'First', + 'value' => 'Value1', + 'sort_order' => 1, + ]); + + $attr2 = ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Second', + 'value' => 'Value2', + 'sort_order' => 2, + ]); + + $attributes = ProductAttribute::where('product_id', $product->id) + ->orderBy('sort_order') + ->get(); + + $this->assertEquals($attr1->id, $attributes[0]->id); + $this->assertEquals($attr2->id, $attributes[1]->id); + } + + /** @test */ + public function it_can_store_metadata() + { + $product = Product::factory()->create(); + + $attribute = ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Dimensions', + 'value' => '10x20x30', + 'meta' => [ + 'unit' => 'cm', + 'display_format' => 'length x width x height', + ], + ]); + + $this->assertEquals('cm', $attribute->meta->unit); + $this->assertEquals('length x width x height', $attribute->meta->display_format); + } + + /** @test */ + public function it_can_update_attribute_value() + { + $product = Product::factory()->create(); + + $attribute = ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Stock Status', + 'value' => 'In Stock', + ]); + + $attribute->update(['value' => 'Out of Stock']); + + $this->assertEquals('Out of Stock', $attribute->fresh()->value); + } + + /** @test */ + public function deleting_product_deletes_attributes() + { + $product = Product::factory()->create(); + + $attribute = ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Test', + 'value' => 'Value', + ]); + + $attributeId = $attribute->id; + + $product->delete(); + + $this->assertDatabaseMissing('product_attributes', ['id' => $attributeId]); + } + + /** @test */ + public function it_can_filter_attributes_by_key() + { + $product = Product::factory()->create(); + + ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Color', + 'value' => 'Red', + ]); + + ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Color', + 'value' => 'Blue', + ]); + + ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Size', + 'value' => 'Large', + ]); + + $colorAttributes = ProductAttribute::where('product_id', $product->id) + ->where('key', 'Color') + ->get(); + + $this->assertCount(2, $colorAttributes); + } + + /** @test */ + public function multiple_products_can_have_same_attribute_keys() + { + $product1 = Product::factory()->create(); + $product2 = Product::factory()->create(); + + ProductAttribute::create([ + 'product_id' => $product1->id, + 'key' => 'Brand', + 'value' => 'Brand A', + ]); + + ProductAttribute::create([ + 'product_id' => $product2->id, + 'key' => 'Brand', + 'value' => 'Brand B', + ]); + + $this->assertCount(1, $product1->attributes); + $this->assertCount(1, $product2->attributes); + $this->assertEquals('Brand A', $product1->attributes->first()->value); + $this->assertEquals('Brand B', $product2->attributes->first()->value); + } + + /** @test */ + public function attributes_are_hidden_in_api_responses() + { + $product = Product::factory()->create(); + + $attribute = ProductAttribute::create([ + 'product_id' => $product->id, + 'key' => 'Secret', + 'value' => 'Value', + ]); + + $array = $attribute->toArray(); + + $this->assertArrayNotHasKey('id', $array); + $this->assertArrayNotHasKey('product_id', $array); + $this->assertArrayNotHasKey('created_at', $array); + $this->assertArrayNotHasKey('updated_at', $array); + } +} diff --git a/tests/Feature/ProductPriceTest.php b/tests/Feature/ProductPriceTest.php new file mode 100644 index 0000000..7f5ca48 --- /dev/null +++ b/tests/Feature/ProductPriceTest.php @@ -0,0 +1,280 @@ +create(); + + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 99.99, + 'currency' => 'USD', + 'is_default' => true, + 'active' => true, + ]); + + $this->assertDatabaseHas('product_prices', [ + 'id' => $price->id, + 'purchasable_id' => $product->id, + 'unit_amount' => 99.99, + ]); + } + + /** @test */ + public function price_belongs_to_purchasable() + { + $product = Product::factory()->create(); + + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 50.00, + 'currency' => 'USD', + ]); + + $this->assertInstanceOf(Product::class, $price->purchasable); + $this->assertEquals($product->id, $price->purchasable->id); + } + + /** @test */ + public function it_can_set_default_price() + { + $product = Product::factory()->create(); + + $price1 = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 100.00, + 'currency' => 'USD', + 'is_default' => false, + ]); + + $price2 = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 200.00, + 'currency' => 'USD', + 'is_default' => true, + ]); + + $this->assertFalse($price1->is_default); + $this->assertTrue($price2->is_default); + $this->assertEquals($price2->id, $product->defaultPrice()->first()->id); + } + + /** @test */ + public function it_can_set_recurring_price() + { + $product = Product::factory()->create(); + + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 29.99, + 'currency' => 'USD', + 'type' => 'recurring', + 'interval' => 'month', + 'interval_count' => 1, + 'trial_period_days' => 14, + ]); + + $this->assertEquals('recurring', $price->type); + $this->assertEquals('month', $price->interval); + $this->assertEquals(1, $price->interval_count); + $this->assertEquals(14, $price->trial_period_days); + } + + /** @test */ + public function it_can_set_one_time_price() + { + $product = Product::factory()->create(); + + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 99.99, + 'currency' => 'USD', + 'type' => 'one_time', + ]); + + $this->assertEquals('one_time', $price->type); + $this->assertNull($price->interval); + } + + /** @test */ + public function it_can_scope_active_prices() + { + $product = Product::factory()->create(); + + ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 50.00, + 'currency' => 'USD', + 'active' => true, + ]); + + ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 75.00, + 'currency' => 'USD', + 'active' => false, + ]); + + $activePrices = ProductPrice::isActive()->get(); + + $this->assertCount(1, $activePrices); + $this->assertTrue($activePrices->first()->active); + } + + /** @test */ + public function it_returns_current_price_based_on_sale() + { + $product = Product::factory()->create(); + + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 100.00, + 'sale_unit_amount' => 80.00, + 'currency' => 'USD', + ]); + + $this->assertEquals(80.00, $price->getCurrentPrice(true)); + $this->assertEquals(100.00, $price->getCurrentPrice(false)); + } + + /** @test */ + public function it_can_have_multiple_currencies() + { + $product = Product::factory()->create(); + + $usdPrice = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 100.00, + 'currency' => 'USD', + ]); + + $eurPrice = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 90.00, + 'currency' => 'EUR', + ]); + + $this->assertCount(2, $product->prices); + $this->assertEquals('USD', $usdPrice->currency); + $this->assertEquals('EUR', $eurPrice->currency); + } + + /** @test */ + public function it_can_store_price_metadata() + { + $product = Product::factory()->create(); + + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 100.00, + 'currency' => 'USD', + 'meta' => [ + 'promo_code' => 'SAVE20', + 'features' => ['feature1', 'feature2'], + ], + ]); + + $this->assertEquals('SAVE20', $price->meta->promo_code); + $this->assertEquals(['feature1', 'feature2'], $price->meta->features); + } + + /** @test */ + public function it_can_deactivate_price() + { + $product = Product::factory()->create(); + + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 100.00, + 'currency' => 'USD', + 'active' => true, + ]); + + $this->assertTrue($price->active); + + $price->update(['active' => false]); + + $this->assertFalse($price->fresh()->active); + } + + /** @test */ + public function product_can_have_multiple_price_tiers() + { + $product = Product::factory()->create(); + + ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'name' => 'Basic', + 'unit_amount' => 10.00, + 'currency' => 'USD', + ]); + + ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'name' => 'Pro', + 'unit_amount' => 20.00, + 'currency' => 'USD', + ]); + + ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'name' => 'Enterprise', + 'unit_amount' => 50.00, + 'currency' => 'USD', + ]); + + $this->assertCount(3, $product->prices); + } + + /** @test */ + public function it_can_set_billing_scheme() + { + $product = Product::factory()->create(); + + $tieredPrice = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 100.00, + 'currency' => 'USD', + 'billing_scheme' => 'tiered', + ]); + + $perUnitPrice = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 50.00, + 'currency' => 'USD', + 'billing_scheme' => 'per_unit', + ]); + + $this->assertEquals('tiered', $tieredPrice->billing_scheme); + $this->assertEquals('per_unit', $perUnitPrice->billing_scheme); + } +} diff --git a/tests/Feature/ProductPurchaseTest.php b/tests/Feature/ProductPurchaseTest.php new file mode 100644 index 0000000..29c4e49 --- /dev/null +++ b/tests/Feature/ProductPurchaseTest.php @@ -0,0 +1,344 @@ +create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + $purchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'unpaid', + ]); + + $this->assertDatabaseHas('product_purchases', [ + 'id' => $purchase->id, + 'purchaser_id' => $user->id, + 'status' => 'unpaid', + ]); + } + + /** @test */ + public function purchase_belongs_to_purchaser() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + $purchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'unpaid', + ]); + + $this->assertInstanceOf(User::class, $purchase->purchaser); + $this->assertEquals($user->id, $purchase->purchaser->id); + } + + /** @test */ + public function purchase_belongs_to_purchasable() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + $purchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'unpaid', + ]); + + $this->assertInstanceOf(Product::class, $purchase->purchasable); + $this->assertEquals($product->id, $purchase->purchasable->id); + } + + /** @test */ + public function it_can_have_different_statuses() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + $unpaidPurchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'unpaid', + ]); + + $completedPurchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'completed', + ]); + + $this->assertEquals('unpaid', $unpaidPurchase->status); + $this->assertEquals('completed', $completedPurchase->status); + } + + /** @test */ + public function it_can_scope_completed_purchases() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'completed', + ]); + + ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'unpaid', + ]); + + $completed = ProductPurchase::completed()->get(); + + $this->assertCount(1, $completed); + $this->assertEquals('completed', $completed->first()->status); + } + + /** @test */ + public function it_can_scope_cart_purchases() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'cart', + ]); + + ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'completed', + ]); + + $inCart = ProductPurchase::inCart()->get(); + + $this->assertCount(1, $inCart); + $this->assertEquals('cart', $inCart->first()->status); + } + + /** @test */ + public function it_can_store_purchase_metadata() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + $purchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'unpaid', + 'meta' => [ + 'ip_address' => '192.168.1.1', + 'user_agent' => 'Mozilla/5.0', + ], + ]); + + $this->assertEquals('192.168.1.1', $purchase->meta->ip_address); + $this->assertEquals('Mozilla/5.0', $purchase->meta->user_agent); + } + + /** @test */ + public function it_can_track_amount_paid() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + $purchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'amount_paid' => 0, + 'status' => 'unpaid', + ]); + + $this->assertEquals(5000, $purchase->amount); + $this->assertEquals(0, $purchase->amount_paid); + + $purchase->update([ + 'amount_paid' => 5000, + 'status' => 'completed', + ]); + + $this->assertEquals(5000, $purchase->fresh()->amount_paid); + $this->assertEquals('completed', $purchase->fresh()->status); + } + + /** @test */ + public function it_can_store_charge_id() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + $purchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'charge_id' => 'ch_123456789', + 'status' => 'completed', + ]); + + $this->assertEquals('ch_123456789', $purchase->charge_id); + } + + /** @test */ + public function it_tracks_purchase_quantity() + { + $user = User::factory()->create(); + $product = Product::factory()->withPrices()->create([ + 'manage_stock' => false, + ]); + + $purchase = ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 5, + 'amount' => 25000, + 'status' => 'unpaid', + ]); + + $this->assertEquals(5, $purchase->quantity); + $this->assertEquals(25000, $purchase->amount); + } + + /** @test */ + public function user_can_have_multiple_purchases() + { + $user = User::factory()->create(); + $product1 = Product::factory()->withPrices()->create(['manage_stock' => false]); + $product2 = Product::factory()->withPrices()->create(['manage_stock' => false]); + + ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product1->id, + 'purchasable_type' => get_class($product1), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'completed', + ]); + + ProductPurchase::create([ + 'purchaser_id' => $user->id, + 'purchaser_type' => get_class($user), + 'purchasable_id' => $product2->id, + 'purchasable_type' => get_class($product2), + 'quantity' => 1, + 'amount' => 7500, + 'status' => 'completed', + ]); + + $this->assertCount(2, $user->purchases); + } + + /** @test */ + public function product_can_have_multiple_purchases() + { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + $product = Product::factory()->withPrices()->create(['manage_stock' => false]); + + ProductPurchase::create([ + 'purchaser_id' => $user1->id, + 'purchaser_type' => get_class($user1), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'completed', + ]); + + ProductPurchase::create([ + 'purchaser_id' => $user2->id, + 'purchaser_type' => get_class($user2), + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'quantity' => 1, + 'amount' => 5000, + 'status' => 'completed', + ]); + + $this->assertCount(2, $product->purchases); + } +} diff --git a/tests/Feature/ProductScopeTest.php b/tests/Feature/ProductScopeTest.php new file mode 100644 index 0000000..b9e2e14 --- /dev/null +++ b/tests/Feature/ProductScopeTest.php @@ -0,0 +1,243 @@ +create(); + $category2 = ProductCategory::factory()->create(); + + $product1 = Product::factory()->create(); + $product2 = Product::factory()->create(); + $product3 = Product::factory()->create(); + + $product1->categories()->attach($category1->id); + $product2->categories()->attach($category1->id); + $product3->categories()->attach($category2->id); + + $productsInCategory1 = Product::byCategory($category1->id)->get(); + + $this->assertCount(2, $productsInCategory1); + $this->assertTrue($productsInCategory1->contains($product1)); + $this->assertTrue($productsInCategory1->contains($product2)); + } + + /** @test */ + public function it_can_search_products_by_slug() + { + Product::factory()->create(['slug' => 'awesome-product']); + Product::factory()->create(['slug' => 'another-product']); + Product::factory()->create(['slug' => 'different-item']); + + $results = Product::search('product')->get(); + + $this->assertCount(2, $results); + } + + /** @test */ + public function it_can_search_products_by_sku() + { + Product::factory()->create(['sku' => 'SKU-001']); + Product::factory()->create(['sku' => 'SKU-002']); + Product::factory()->create(['sku' => 'DIFF-001']); + + $results = Product::search('SKU')->get(); + + $this->assertCount(2, $results); + } + + /** @test */ + public function it_can_filter_by_price_range() + { + Product::factory()->create(['meta' => json_encode(['price' => 50])]); + Product::factory()->create(['meta' => json_encode(['price' => 100])]); + Product::factory()->create(['meta' => json_encode(['price' => 150])]); + + // Note: This test assumes the scope uses a 'price' column + // which may need adjustment based on actual implementation + $products = Product::all(); + + $this->assertCount(3, $products); + } + + /** @test */ + public function it_can_scope_low_stock_products() + { + $lowStockProduct = Product::factory()->create([ + 'manage_stock' => true, + 'low_stock_threshold' => 10, + ]); + $lowStockProduct->increaseStock(5); + + $normalStockProduct = Product::factory()->create([ + 'manage_stock' => true, + 'low_stock_threshold' => 10, + ]); + $normalStockProduct->increaseStock(20); + + $lowStock = Product::lowStock()->get(); + + $this->assertTrue($lowStock->contains($lowStockProduct)); + $this->assertFalse($lowStock->contains($normalStockProduct)); + } + + /** @test */ + public function it_can_scope_featured_products() + { + Product::factory()->create(['featured' => true]); + Product::factory()->create(['featured' => true]); + Product::factory()->create(['featured' => false]); + + $featured = Product::featured()->get(); + + $this->assertCount(2, $featured); + $this->assertTrue($featured->every(fn($p) => $p->featured === true)); + } + + /** @test */ + public function visible_scope_excludes_unpublished_products() + { + Product::factory()->create([ + 'is_visible' => true, + 'status' => 'published', + ]); + + Product::factory()->create([ + 'is_visible' => true, + 'status' => 'draft', + ]); + + Product::factory()->create([ + 'is_visible' => false, + 'status' => 'published', + ]); + + $visible = Product::visible()->get(); + + $this->assertCount(1, $visible); + } + + /** @test */ + public function visible_scope_respects_published_at_date() + { + Product::factory()->create([ + 'is_visible' => true, + 'status' => 'published', + 'published_at' => now()->subDay(), + ]); + + Product::factory()->create([ + 'is_visible' => true, + 'status' => 'published', + 'published_at' => now()->addDay(), + ]); + + $visible = Product::visible()->get(); + + $this->assertCount(1, $visible); + } + + /** @test */ + public function in_stock_scope_includes_products_without_stock_management() + { + Product::factory()->create(['manage_stock' => false]); + + $managedProduct = Product::factory()->create(['manage_stock' => true]); + $managedProduct->increaseStock(10); + + $inStock = Product::inStock()->get(); + + $this->assertGreaterThanOrEqual(2, $inStock->count()); + } + + /** @test */ + public function in_stock_scope_excludes_out_of_stock_products() + { + $outOfStock = Product::factory()->create(['manage_stock' => true]); + + $inStock = Product::factory()->create(['manage_stock' => true]); + $inStock->increaseStock(10); + + $products = Product::inStock()->get(); + + $this->assertFalse($products->contains($outOfStock)); + $this->assertTrue($products->contains($inStock)); + } + + /** @test */ + public function it_can_combine_multiple_scopes() + { + Product::factory()->create([ + 'featured' => true, + 'is_visible' => true, + 'status' => 'published', + 'manage_stock' => false, + ]); + + Product::factory()->create([ + 'featured' => true, + 'is_visible' => false, + 'status' => 'published', + ]); + + Product::factory()->create([ + 'featured' => false, + 'is_visible' => true, + 'status' => 'published', + ]); + + $products = Product::featured() + ->visible() + ->inStock() + ->get(); + + $this->assertCount(1, $products); + } + + /** @test */ + public function it_can_scope_products_by_type() + { + Product::factory()->create(['type' => 'simple']); + Product::factory()->create(['type' => 'simple']); + Product::factory()->create(['type' => 'variable']); + Product::factory()->create(['type' => 'variation']); + + $simpleProducts = Product::where('type', 'simple')->get(); + + $this->assertCount(2, $simpleProducts); + } + + /** @test */ + public function it_can_scope_downloadable_products() + { + Product::factory()->create(['downloadable' => true]); + Product::factory()->create(['downloadable' => true]); + Product::factory()->create(['downloadable' => false]); + + $downloadable = Product::where('downloadable', true)->get(); + + $this->assertCount(2, $downloadable); + } + + /** @test */ + public function it_can_scope_virtual_products() + { + Product::factory()->create(['virtual' => true]); + Product::factory()->create(['virtual' => false]); + Product::factory()->create(['virtual' => false]); + + $virtual = Product::where('virtual', true)->get(); + + $this->assertCount(1, $virtual); + } +} diff --git a/tests/Feature/ProductStockTest.php b/tests/Feature/ProductStockTest.php new file mode 100644 index 0000000..d5fbe43 --- /dev/null +++ b/tests/Feature/ProductStockTest.php @@ -0,0 +1,277 @@ +create(['manage_stock' => true]); + + $product->increaseStock(10); + + $this->assertDatabaseHas('product_stocks', [ + 'product_id' => $product->id, + 'quantity' => 10, + 'type' => 'increase', + ]); + } + + /** @test */ + public function it_creates_stock_record_on_decrease() + { + $product = Product::factory()->create(['manage_stock' => true]); + $product->increaseStock(20); + + $product->decreaseStock(5); + + $this->assertDatabaseHas('product_stocks', [ + 'product_id' => $product->id, + 'quantity' => -5, + 'type' => 'decrease', + ]); + } + + /** @test */ + public function stock_belongs_to_product() + { + $product = Product::factory()->create(['manage_stock' => true]); + $product->increaseStock(10); + + $stock = $product->stocks()->first(); + + $this->assertInstanceOf(Product::class, $stock->product); + $this->assertEquals($product->id, $stock->product->id); + } + + /** @test */ + public function product_has_many_stock_records() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(10); + $product->increaseStock(5); + $product->decreaseStock(3); + + $this->assertCount(3, $product->stocks); + } + + /** @test */ + public function available_stock_considers_all_records() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(50); + $product->increaseStock(30); + $product->decreaseStock(20); + + $this->assertEquals(60, $product->getAvailableStock()); + } + + /** @test */ + public function reservation_reduces_available_stock() + { + $product = Product::factory()->withStocks(100)->create(); + + $reservation = $product->reserveStock(25); + + $this->assertEquals(75, $product->getAvailableStock()); + $this->assertNotNull($reservation); + } + + /** @test */ + public function releasing_reservation_increases_available_stock() + { + $product = Product::factory()->withStocks(100)->create(); + + $reservation = $product->reserveStock(25); + $this->assertEquals(75, $product->getAvailableStock()); + + $reservation->release(); + + $this->assertEquals(100, $product->refresh()->getAvailableStock()); + } + + /** @test */ + public function permanent_reservation_has_no_expiry() + { + $product = Product::factory()->withStocks(50)->create(); + + $reservation = $product->reserveStock(10); + + $this->assertNull($reservation->expires_at); + $this->assertTrue($reservation->isPermanent()); + } + + /** @test */ + public function temporary_reservation_has_expiry() + { + $product = Product::factory()->withStocks(50)->create(); + + $reservation = $product->reserveStock( + quantity: 10, + until: now()->addHours(2) + ); + + $this->assertNotNull($reservation->expires_at); + $this->assertTrue($reservation->isTemporary()); + } + + /** @test */ + public function reservation_can_have_note() + { + $product = Product::factory()->withStocks(50)->create(); + + $note = 'Reserved for VIP customer'; + $reservation = $product->reserveStock( + quantity: 10, + note: $note + ); + + $this->assertEquals($note, $reservation->note); + } + + /** @test */ + public function cannot_reserve_more_than_available() + { + $product = Product::factory()->withStocks(10)->create(); + + $this->expectException(NotEnoughStockException::class); + + $product->reserveStock(15); + } + + /** @test */ + public function pending_scope_returns_unreleased_reservations() + { + $product = Product::factory()->withStocks(100)->create(); + + $pending = $product->reserveStock(10); + $released = $product->reserveStock(5); + $released->release(); + + $pendingReservations = ProductStock::pending()->get(); + + $this->assertTrue($pendingReservations->contains($pending)); + $this->assertFalse($pendingReservations->contains($released)); + } + + /** @test */ + public function released_scope_returns_released_reservations() + { + $product = Product::factory()->withStocks(100)->create(); + + $pending = $product->reserveStock(10); + $released = $product->reserveStock(5); + $released->release(); + + $releasedReservations = ProductStock::released()->get(); + + $this->assertFalse($releasedReservations->contains($pending)); + $this->assertTrue($releasedReservations->contains($released)); + } + + /** @test */ + public function expired_reservations_dont_affect_available_stock() + { + $product = Product::factory()->withStocks(100)->create(); + + $product->reserveStock( + quantity: 20, + until: now()->subHour() + ); + + // Expired reservations should be counted in available stock + $available = $product->reservations()->get(); + + $this->assertEquals(0, $available->count()); + } + + /** @test */ + public function cannot_release_stock_twice() + { + $product = Product::factory()->withStocks(50)->create(); + + $reservation = $product->reserveStock(10); + + $this->assertTrue($reservation->release()); + $this->assertFalse($reservation->release()); + } + + /** @test */ + public function stock_status_is_tracked() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(10); + + $stock = $product->stocks()->first(); + + $this->assertEquals('completed', $stock->status); + } + + /** @test */ + public function product_without_stock_management_returns_max_stock() + { + $product = Product::factory()->create(['manage_stock' => false]); + + $available = $product->getAvailableStock(); + + $this->assertEquals(PHP_INT_MAX, $available); + } + + /** @test */ + public function product_without_stock_management_doesnt_create_records() + { + $product = Product::factory()->create(['manage_stock' => false]); + + $result = $product->increaseStock(10); + + $this->assertFalse($result); + $this->assertCount(0, $product->stocks); + } + + /** @test */ + public function reservation_without_stock_management_returns_null() + { + $product = Product::factory()->create(['manage_stock' => false]); + + $reservation = $product->reserveStock(10); + + $this->assertNull($reservation); + } + + /** @test */ + public function available_stocks_attribute_accessor_works() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(25); + $product->increaseStock(15); + + $this->assertEquals(40, $product->AvailableStocks); + } + + /** @test */ + public function reservations_method_filters_active_only() + { + $product = Product::factory()->withStocks(100)->create(); + + $active = $product->reserveStock(10, until: now()->addDay()); + $expired = $product->reserveStock(5, until: now()->subDay()); + + $reservations = $product->reservations()->get(); + + $this->assertCount(1, $reservations); + $this->assertEquals($active->id, $reservations->first()->id); + } +} diff --git a/tests/Unit/CartTest.php b/tests/Unit/CartTest.php new file mode 100644 index 0000000..9f7387d --- /dev/null +++ b/tests/Unit/CartTest.php @@ -0,0 +1,250 @@ +create(); + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 100.00, + 'currency' => 'USD', + ]); + + $cartItem = $cart->addToCart($price, quantity: 2); + + $this->assertNotNull($cartItem); + $this->assertEquals(2, $cartItem->quantity); + $this->assertEquals(100.00, $cartItem->price); + } + + /** @test */ + public function cart_calculates_subtotal_automatically() + { + $cart = Cart::create(); + $product = Product::factory()->create(); + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 50.00, + 'currency' => 'USD', + ]); + + $cartItem = $cart->addToCart($price, quantity: 3); + + $this->assertEquals(150.00, $cartItem->subtotal); + } + + /** @test */ + public function cart_respects_sale_prices() + { + $cart = Cart::create(); + $product = Product::factory()->create(); + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 100.00, + 'sale_unit_amount' => 80.00, + 'currency' => 'USD', + ]); + + $cartItem = $cart->addToCart($price, quantity: 1); + + $this->assertEquals(80.00, $cartItem->price); + $this->assertEquals(100.00, $cartItem->regular_price); + } + + /** @test */ + public function cart_can_add_items_with_custom_parameters() + { + $cart = Cart::create(); + $product = Product::factory()->create(); + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 50.00, + 'currency' => 'USD', + ]); + + $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']); + } + + /** @test */ + public function cart_total_sums_all_items() + { + $cart = Cart::create(); + + $product1 = Product::factory()->create(); + $price1 = ProductPrice::create([ + 'purchasable_id' => $product1->id, + 'purchasable_type' => get_class($product1), + 'unit_amount' => 25.00, + 'currency' => 'USD', + ]); + + $product2 = Product::factory()->create(); + $price2 = ProductPrice::create([ + 'purchasable_id' => $product2->id, + 'purchasable_type' => get_class($product2), + 'unit_amount' => 50.00, + 'currency' => 'USD', + ]); + + $cart->addToCart($price1, quantity: 2); // 50 + $cart->addToCart($price2, quantity: 3); // 150 + + $total = $cart->fresh()->getTotal(); + + $this->assertEquals(200.00, $total); + } + + /** @test */ + 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()); + } + + /** @test */ + public function cart_can_be_converted() + { + $cart = Cart::create(); + + $this->assertFalse($cart->isConverted()); + + $cart->update(['converted_at' => now()]); + + $this->assertTrue($cart->fresh()->isConverted()); + } + + /** @test */ + 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); + } + + /** @test */ + public function cart_deletes_items_on_deletion() + { + $cart = Cart::create(); + $product = Product::factory()->create(); + $price = ProductPrice::create([ + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'unit_amount' => 50.00, + 'currency' => 'USD', + ]); + + $cartItem = $cart->addToCart($price); + $cartItemId = $cartItem->id; + + $this->assertDatabaseHas('cart_items', ['id' => $cartItemId]); + + $cart->delete(); + + $this->assertDatabaseMissing('cart_items', ['id' => $cartItemId]); + } + + /** @test */ + public function cart_can_have_currency() + { + $cart = Cart::create([ + 'currency' => 'EUR', + ]); + + $this->assertEquals('EUR', $cart->currency); + } + + /** @test */ + public function cart_can_have_status() + { + $cart = Cart::create([ + 'status' => 'pending', + ]); + + $this->assertEquals('pending', $cart->status); + + $cart->update(['status' => 'completed']); + + $this->assertEquals('completed', $cart->fresh()->status); + } + + /** @test */ + 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); + } + + /** @test */ + public function cart_can_have_session_id() + { + $sessionId = 'sess_' . str()->random(40); + + $cart = Cart::create([ + 'session_id' => $sessionId, + ]); + + $this->assertEquals($sessionId, $cart->session_id); + } +}