I basic tests
This commit is contained in:
parent
5fb31d0c4f
commit
3593a462a1
|
|
@ -122,6 +122,7 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
|
|
||||||
static::deleted(function ($model) {
|
static::deleted(function ($model) {
|
||||||
$model->actions()->delete();
|
$model->actions()->delete();
|
||||||
|
$model->attributes()->delete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,7 +390,7 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
return $query->where(function ($q) use ($search) {
|
return $query->where(function ($q) use ($search) {
|
||||||
$q->where('slug', 'like', "%{$search}%")
|
$q->where('slug', 'like', "%{$search}%")
|
||||||
->orWhere('sku', '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)
|
public function scopeLowStock($query)
|
||||||
{
|
{
|
||||||
|
$stockTable = config('shop.tables.product_stocks', 'product_stocks');
|
||||||
|
$productTable = config('shop.tables.products', 'products');
|
||||||
|
|
||||||
return $query->where('manage_stock', true)
|
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
|
public function isLowStock(): bool
|
||||||
|
|
|
||||||
|
|
@ -114,4 +114,9 @@ class ProductAction extends Model
|
||||||
|
|
||||||
dispatch(new $action_job(...$params));
|
dispatch(new $action_job(...$params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getAvailableActions(): array
|
||||||
|
{
|
||||||
|
return config('shop.actions.available', []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
||||||
|
use Blax\Shop\Exceptions\NotPurchasable;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Workbench\App\Models\User;
|
||||||
|
|
||||||
|
class HasShoppingCapabilitiesTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function user_has_cart_relationship()
|
||||||
|
{
|
||||||
|
$user = User::factory()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductAttribute;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class ProductAttributeTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_create_a_product_attribute()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class ProductPriceTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_create_a_product_price()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPurchase;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Workbench\App\Models\User;
|
||||||
|
|
||||||
|
class ProductPurchaseTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_create_a_purchase()
|
||||||
|
{
|
||||||
|
$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->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductCategory;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class ProductScopeTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_scope_by_category()
|
||||||
|
{
|
||||||
|
$category1 = ProductCategory::factory()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductStock;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class ProductStockTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_creates_stock_record_on_increase()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Unit;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class CartTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function cart_can_add_product_price_directly()
|
||||||
|
{
|
||||||
|
$cart = Cart::create();
|
||||||
|
$product = Product::factory()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue