BFI tests fixing, prices, purchases, cart
This commit is contained in:
parent
fda536deea
commit
ab1e2468ca
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": false,
|
||||||
"[php]": {
|
"[php]": {
|
||||||
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
|
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,25 @@ class ProductFactory extends Factory
|
||||||
{
|
{
|
||||||
return $this->state(['featured' => true]);
|
return $this->state(['featured' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function withPrices(int $count = 1): static
|
||||||
|
{
|
||||||
|
return $this->afterCreating(function (Product $product) use ($count) {
|
||||||
|
$prices = \Blax\Shop\Models\ProductPrice::factory()
|
||||||
|
->count($count)
|
||||||
|
->create([
|
||||||
|
'purchasable_type' => get_class($product),
|
||||||
|
'purchasable_id' => $product->id,
|
||||||
|
'unit_amount' => $this->faker->randomFloat(2, 10, 1000),
|
||||||
|
'currency' => 'EUR',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set the first price as default
|
||||||
|
if ($prices->isNotEmpty()) {
|
||||||
|
$defaultPrice = $prices->first();
|
||||||
|
$defaultPrice->is_default = true;
|
||||||
|
$defaultPrice->save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Database\Factories;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ProductPriceFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = ProductPrice::class;
|
||||||
|
|
||||||
|
public function definition()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'unit_amount' => $this->faker->randomFloat(2, 1, 1000),
|
||||||
|
'currency' => 'EUR',
|
||||||
|
'is_default' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -116,13 +116,13 @@ return new class extends Migration
|
||||||
if (!Schema::hasTable(config('shop.tables.product_prices', 'product_prices'))) {
|
if (!Schema::hasTable(config('shop.tables.product_prices', 'product_prices'))) {
|
||||||
Schema::create(config('shop.tables.product_prices', 'product_prices'), function (Blueprint $table) {
|
Schema::create(config('shop.tables.product_prices', 'product_prices'), function (Blueprint $table) {
|
||||||
$table->uuid('id')->primary();
|
$table->uuid('id')->primary();
|
||||||
$table->uuid('product_id');
|
$table->uuidMorphs('purchasable');
|
||||||
$table->string('stripe_price_id')->nullable();
|
$table->string('stripe_price_id')->nullable();
|
||||||
$table->string('name')->nullable();
|
$table->string('name')->nullable();
|
||||||
$table->string('type')->default('one_time'); // one_time, recurring
|
$table->string('type')->default('one_time'); // one_time, recurring
|
||||||
$table->string('currency', 3)->default('USD');
|
$table->string('currency', 3)->default('EUR');
|
||||||
$table->integer('price')->default(0); // Store as smallest currency unit (cents)
|
$table->integer('unit_amount')->default(0); // Store as smallest currency unit (cents)
|
||||||
$table->integer('sale_price')->nullable();
|
$table->integer('sale_unit_amount')->nullable();
|
||||||
$table->boolean('is_default')->default(false);
|
$table->boolean('is_default')->default(false);
|
||||||
$table->boolean('active')->default(true);
|
$table->boolean('active')->default(true);
|
||||||
$table->string('billing_scheme')->nullable(); // per_unit, tiered
|
$table->string('billing_scheme')->nullable(); // per_unit, tiered
|
||||||
|
|
@ -132,10 +132,7 @@ return new class extends Migration
|
||||||
$table->json('meta')->nullable();
|
$table->json('meta')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->index(['product_id', 'currency']);
|
$table->index('currency');
|
||||||
$table->index(['product_id', 'is_default']);
|
|
||||||
$table->index(['active', 'type']);
|
|
||||||
$table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,18 +268,17 @@ return new class extends Migration
|
||||||
Schema::create(config('shop.tables.cart_items', 'cart_items'), function (Blueprint $table) {
|
Schema::create(config('shop.tables.cart_items', 'cart_items'), function (Blueprint $table) {
|
||||||
$table->uuid('id')->primary();
|
$table->uuid('id')->primary();
|
||||||
$table->uuid('cart_id');
|
$table->uuid('cart_id');
|
||||||
$table->uuid('product_id');
|
$table->uuidMorphs('purchasable');
|
||||||
$table->integer('quantity')->default(1);
|
$table->integer('quantity')->default(1);
|
||||||
$table->decimal('price', 10, 2);
|
$table->decimal('price', 10, 2)->default(0);
|
||||||
$table->decimal('regular_price', 10, 2)->nullable();
|
$table->decimal('regular_price', 10, 2)->nullable();
|
||||||
$table->decimal('subtotal', 10, 2);
|
$table->decimal('subtotal', 10, 2);
|
||||||
$table->json('attributes')->nullable();
|
$table->json('parameters')->nullable();
|
||||||
$table->json('meta')->nullable();
|
$table->json('meta')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->index(['cart_id', 'product_id']);
|
$table->index(['cart_id', 'product_id']);
|
||||||
$table->foreign('cart_id')->references('id')->on(config('shop.tables.carts', 'carts'))->onDelete('cascade');
|
$table->foreign('cart_id')->references('id')->on(config('shop.tables.carts', 'carts'))->onDelete('cascade');
|
||||||
$table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Contracts;
|
||||||
|
|
||||||
|
interface Cartable
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -10,5 +10,7 @@ interface Purchasable
|
||||||
|
|
||||||
public function decreaseStock(int $quantity = 1): bool;
|
public function decreaseStock(int $quantity = 1): bool;
|
||||||
|
|
||||||
public function increaseStock(int $quantity = 1): void;
|
public function increaseStock(int $quantity = 1): bool;
|
||||||
|
|
||||||
|
public function purchases();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class NotEnoughStockException extends Exception {}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Models;
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
|
use Blax\Shop\Contracts\Cartable;
|
||||||
use Blax\Workkit\Traits\HasExpiration;
|
use Blax\Workkit\Traits\HasExpiration;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -109,4 +110,30 @@ class Cart extends Model
|
||||||
$cart->items()->delete();
|
$cart->items()->delete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addToCart(
|
||||||
|
Model $cartable,
|
||||||
|
$quantity = 1,
|
||||||
|
$parameters = []
|
||||||
|
) : CartItem {
|
||||||
|
|
||||||
|
// $cartable must implement Cartable
|
||||||
|
if (! $cartable instanceof Cartable) {
|
||||||
|
throw new \Exception("Item must implement the Cartable interface.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$cartItem = $this->items()->create([
|
||||||
|
'purchasable_id' => $cartable->getKey(),
|
||||||
|
'purchasable_type' => get_class($cartable),
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'price' => ($cartable?->sale_unit_amount ?? $cartable?->unit_amount ?? 0),
|
||||||
|
'regular_price' => $cartable?->unit_amount,
|
||||||
|
'subtotal' => ($cartable?->sale_unit_amount ?? $cartable?->unit_amount ?? 0) * $quantity,
|
||||||
|
'parameters' => $parameters,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cartItem = $cartItem->fresh();
|
||||||
|
|
||||||
|
return $cartItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,24 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Models;
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
|
use Blax\Workkit\Traits\HasMeta;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class CartItem extends Model
|
class CartItem extends Model
|
||||||
{
|
{
|
||||||
use HasUuids;
|
use HasUuids, HasMeta;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'cart_id',
|
'cart_id',
|
||||||
'product_id',
|
'purchasable_id',
|
||||||
|
'purchasable_type',
|
||||||
'quantity',
|
'quantity',
|
||||||
'price',
|
'price',
|
||||||
'regular_price',
|
'regular_price',
|
||||||
'subtotal',
|
'subtotal',
|
||||||
'attributes',
|
'parameters',
|
||||||
'meta',
|
'meta',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -26,7 +28,7 @@ class CartItem extends Model
|
||||||
'price' => 'decimal:2',
|
'price' => 'decimal:2',
|
||||||
'regular_price' => 'decimal:2',
|
'regular_price' => 'decimal:2',
|
||||||
'subtotal' => 'decimal:2',
|
'subtotal' => 'decimal:2',
|
||||||
'attributes' => 'array',
|
'parameters' => 'array',
|
||||||
'meta' => 'array',
|
'meta' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -59,9 +61,18 @@ class CartItem extends Model
|
||||||
return $this->belongsTo(config('shop.models.cart'), 'cart_id');
|
return $this->belongsTo(config('shop.models.cart'), 'cart_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function product(): BelongsTo
|
public function purchasable()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(config('shop.models.product'), 'product_id');
|
return $this->morphTo('purchasable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function product(): BelongsTo|null
|
||||||
|
{
|
||||||
|
if ($this->purchasable_type === config('shop.models.product', Product::class)) {
|
||||||
|
return $this->belongsTo(config('shop.models.product'), 'purchasable_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSubtotal(): float
|
public function getSubtotal(): float
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,22 @@
|
||||||
namespace Blax\Shop\Models;
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
use App\Services\StripeService;
|
use App\Services\StripeService;
|
||||||
|
use Blax\Shop\Contracts\Cartable;
|
||||||
use Blax\Workkit\Traits\HasMetaTranslation;
|
use Blax\Workkit\Traits\HasMetaTranslation;
|
||||||
use Blax\Shop\Events\ProductCreated;
|
use Blax\Shop\Events\ProductCreated;
|
||||||
use Blax\Shop\Events\ProductUpdated;
|
use Blax\Shop\Events\ProductUpdated;
|
||||||
use Blax\Shop\Contracts\Purchasable;
|
use Blax\Shop\Contracts\Purchasable;
|
||||||
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class Product extends Model implements Purchasable
|
class Product extends Model implements Purchasable, Cartable
|
||||||
{
|
{
|
||||||
use HasFactory, HasUuids, HasMetaTranslation;
|
use HasFactory, HasUuids, HasMetaTranslation;
|
||||||
|
|
||||||
|
|
@ -49,7 +52,6 @@ class Product extends Model implements Purchasable
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'manage_stock' => 'boolean',
|
'manage_stock' => 'boolean',
|
||||||
'in_stock' => 'boolean',
|
|
||||||
'virtual' => 'boolean',
|
'virtual' => 'boolean',
|
||||||
'downloadable' => 'boolean',
|
'downloadable' => 'boolean',
|
||||||
'meta' => 'object',
|
'meta' => 'object',
|
||||||
|
|
@ -62,8 +64,6 @@ class Product extends Model implements Purchasable
|
||||||
'sort_order' => 'integer',
|
'sort_order' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Remove - causes issues with casting
|
|
||||||
|
|
||||||
protected $dispatchesEvents = [
|
protected $dispatchesEvents = [
|
||||||
'created' => ProductCreated::class,
|
'created' => ProductCreated::class,
|
||||||
'updated' => ProductUpdated::class,
|
'updated' => ProductUpdated::class,
|
||||||
|
|
@ -125,9 +125,12 @@ class Product extends Model implements Purchasable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prices(): HasMany
|
public function prices(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(config('shop.models.product_price', ProductPrice::class));
|
return $this->morphMany(
|
||||||
|
config('shop.models.product_price', ProductPrice::class),
|
||||||
|
'purchasable'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parent()
|
public function parent()
|
||||||
|
|
@ -163,9 +166,17 @@ class Product extends Model implements Purchasable
|
||||||
return $this->hasMany(config('shop.models.product_action', ProductAction::class));
|
return $this->hasMany(config('shop.models.product_action', ProductAction::class));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function activeStocks(): HasMany
|
public function getAvailableStocksAttribute(): int
|
||||||
{
|
{
|
||||||
return $this->stocks()->pending();
|
return $this->stocks()->available()->sum('quantity') ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purchases(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(
|
||||||
|
config('shop.models.product_purchase', ProductPurchase::class),
|
||||||
|
'purchasable'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopePublished($query)
|
public function scopePublished($query)
|
||||||
|
|
@ -180,10 +191,6 @@ class Product extends Model implements Purchasable
|
||||||
|
|
||||||
public function isOnSale(): bool
|
public function isOnSale(): bool
|
||||||
{
|
{
|
||||||
if (!$this->sale_price) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$now = now();
|
$now = now();
|
||||||
|
|
||||||
if ($this->sale_start && $now->lt($this->sale_start)) {
|
if ($this->sale_start && $now->lt($this->sale_start)) {
|
||||||
|
|
@ -207,30 +214,55 @@ class Product extends Model implements Purchasable
|
||||||
return $defaultPrice ? $defaultPrice->price : $this->regular_price;
|
return $defaultPrice ? $defaultPrice->price : $this->regular_price;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isInStock(): bool
|
||||||
|
{
|
||||||
|
if (!$this->manage_stock) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getAvailableStock() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public function decreaseStock(int $quantity = 1): bool
|
public function decreaseStock(int $quantity = 1): bool
|
||||||
{
|
{
|
||||||
if (!$this->manage_stock) {
|
if (!$this->manage_stock) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config('shop.stock.log_changes', true)) {
|
if ($this->AvailableStocks < $quantity) {
|
||||||
$this->logStockChange(-$quantity, 'decrease');
|
return throw new NotEnoughStockException("Not enough stock available for product ID {$this->id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->stocks()->create([
|
||||||
|
'quantity' => -$quantity,
|
||||||
|
'type' => 'decrease',
|
||||||
|
'status' => 'completed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->logStockChange(-$quantity, 'decrease');
|
||||||
|
|
||||||
$this->save();
|
$this->save();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function increaseStock(int $quantity = 1): void
|
public function increaseStock(int $quantity = 1): bool
|
||||||
{
|
{
|
||||||
if (!$this->manage_stock) {
|
if (!$this->manage_stock) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->stocks()->create([
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'type' => 'increase',
|
||||||
|
'status' => 'completed',
|
||||||
|
]);
|
||||||
|
|
||||||
$this->logStockChange($quantity, 'increase');
|
$this->logStockChange($quantity, 'increase');
|
||||||
|
|
||||||
$this->save();
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reserveStock(
|
public function reserveStock(
|
||||||
|
|
@ -257,7 +289,7 @@ class Product extends Model implements Purchasable
|
||||||
return PHP_INT_MAX;
|
return PHP_INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
return max(0, $this->stock_quantity);
|
return max(0, $this->AvailableStocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getReservedStock(): int
|
public function getReservedStock(): int
|
||||||
|
|
@ -315,6 +347,17 @@ class Product extends Model implements Purchasable
|
||||||
return $this->relatedProducts()->wherePivot('type', 'cross-sell');
|
return $this->relatedProducts()->wherePivot('type', 'cross-sell');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function scopeInStock($query)
|
||||||
|
{
|
||||||
|
return $query->where(function ($q) {
|
||||||
|
$q->where('manage_stock', false)
|
||||||
|
->orWhere(function ($q2) {
|
||||||
|
$q2->where('manage_stock', true)
|
||||||
|
->whereRaw("(SELECT SUM(quantity) FROM " . config('shop.tables.product_stocks', 'product_stocks') . " WHERE product_id = " . config('shop.tables.products', 'products') . ".id) > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function scopeVisible($query)
|
public function scopeVisible($query)
|
||||||
{
|
{
|
||||||
return $query->where('is_visible', true)
|
return $query->where('is_visible', true)
|
||||||
|
|
@ -399,9 +442,6 @@ class Product extends Model implements Purchasable
|
||||||
'regular_price' => $this->regular_price,
|
'regular_price' => $this->regular_price,
|
||||||
'sale_price' => $this->sale_price,
|
'sale_price' => $this->sale_price,
|
||||||
'is_on_sale' => $this->isOnSale(),
|
'is_on_sale' => $this->isOnSale(),
|
||||||
'in_stock' => $this->in_stock,
|
|
||||||
'stock_quantity' => $this->manage_stock ? $this->stock_quantity : null,
|
|
||||||
'stock_status' => $this->stock_status,
|
|
||||||
'low_stock' => $this->isLowStock(),
|
'low_stock' => $this->isLowStock(),
|
||||||
'featured' => $this->featured,
|
'featured' => $this->featured,
|
||||||
'virtual' => $this->virtual,
|
'virtual' => $this->virtual,
|
||||||
|
|
@ -456,4 +496,9 @@ class Product extends Model implements Purchasable
|
||||||
|
|
||||||
return parent::newInstance($attributes, $exists);
|
return parent::newInstance($attributes, $exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function defaultPrice()
|
||||||
|
{
|
||||||
|
return $this->prices()->where('is_default', true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,48 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Models;
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
|
use Blax\Shop\Contracts\Cartable;
|
||||||
|
use Blax\Shop\Contracts\Purchasable;
|
||||||
use Blax\Workkit\Traits\HasMetaTranslation;
|
use Blax\Workkit\Traits\HasMetaTranslation;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class ProductPrice extends Model
|
class ProductPrice extends Model implements Cartable
|
||||||
{
|
{
|
||||||
use HasUuids, HasMetaTranslation;
|
use HasFactory, HasUuids, HasMetaTranslation;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'active',
|
'purchasable_type',
|
||||||
'product_id',
|
'purchasable_id',
|
||||||
'stripe_price_id',
|
'stripe_price_id',
|
||||||
'name',
|
'name',
|
||||||
'type',
|
'type',
|
||||||
'price',
|
'currency',
|
||||||
'sale_price',
|
'unit_amount',
|
||||||
|
'sale_unit_amount',
|
||||||
'is_default',
|
'is_default',
|
||||||
|
'active',
|
||||||
'billing_scheme',
|
'billing_scheme',
|
||||||
'interval',
|
'interval',
|
||||||
'interval_count',
|
'interval_count',
|
||||||
'trial_period_days',
|
'trial_period_days',
|
||||||
'currency',
|
|
||||||
'meta',
|
'meta',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'price' => 'integer',
|
|
||||||
'sale_price' => 'integer',
|
|
||||||
'is_default' => 'boolean',
|
'is_default' => 'boolean',
|
||||||
'trial_period_days' => 'integer',
|
|
||||||
'meta' => 'object',
|
|
||||||
'active' => 'boolean',
|
'active' => 'boolean',
|
||||||
|
'meta' => 'object',
|
||||||
|
'unit_amount' => 'float',
|
||||||
|
'sale_unit_amount' => 'float',
|
||||||
|
'interval_count' => 'integer',
|
||||||
|
'trial_period_days' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function product()
|
public function purchasable()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Product::class);
|
return $this->morphTo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopeIsActive($query)
|
public function scopeIsActive($query)
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ class ProductPurchase extends Model
|
||||||
'status',
|
'status',
|
||||||
'cart_id',
|
'cart_id',
|
||||||
'price_id',
|
'price_id',
|
||||||
'purchasable',
|
'purchasable_id',
|
||||||
'purchaser',
|
'purchasable_type',
|
||||||
|
'purchaser_id',
|
||||||
|
'purchaser_type',
|
||||||
'quantity',
|
'quantity',
|
||||||
'amount',
|
'amount',
|
||||||
'amount_paid',
|
'amount_paid',
|
||||||
|
|
@ -37,12 +39,12 @@ class ProductPurchase extends Model
|
||||||
|
|
||||||
public function purchasable()
|
public function purchasable()
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo('purchasable');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function purchaser()
|
public function purchaser()
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo('purchaser');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function product()
|
public function product()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Blax\Shop\Models;
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Workkit\Traits\HasExpiration;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -11,7 +12,7 @@ use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class ProductStock extends Model
|
class ProductStock extends Model
|
||||||
{
|
{
|
||||||
use HasUuids;
|
use HasUuids, HasExpiration;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'product_id',
|
'product_id',
|
||||||
|
|
@ -70,16 +71,6 @@ class ProductStock extends Model
|
||||||
return $query->where('status', 'completed');
|
return $query->where('status', 'completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopeExpired($query)
|
|
||||||
{
|
|
||||||
return $query->where('status', 'expired')
|
|
||||||
->orWhere(function ($q) {
|
|
||||||
$q->where('status', 'pending')
|
|
||||||
->whereNotNull('expires_at')
|
|
||||||
->where('expires_at', '<=', now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scopeTemporary($query)
|
public function scopeTemporary($query)
|
||||||
{
|
{
|
||||||
return $query->whereNotNull('expires_at');
|
return $query->whereNotNull('expires_at');
|
||||||
|
|
@ -216,4 +207,9 @@ class ProductStock extends Model
|
||||||
|
|
||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function scopeAvailable($query)
|
||||||
|
{
|
||||||
|
return $query->where('status', 'completed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,43 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Traits;
|
namespace Blax\Shop\Traits;
|
||||||
|
|
||||||
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
|
use Blax\Shop\Models\CartItem;
|
||||||
use Blax\Shop\Models\ProductPurchase;
|
use Blax\Shop\Models\ProductPurchase;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
use Blax\Shop\Models\ProductPrice;
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
trait HasShoppingCapabilities
|
trait HasShoppingCapabilities
|
||||||
{
|
{
|
||||||
|
public function cart(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(
|
||||||
|
config('shop.models.cart', \Blax\Shop\Models\Cart::class),
|
||||||
|
'customer'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all purchases made by this entity
|
* Get all purchases made by this entity
|
||||||
*/
|
*/
|
||||||
public function purchases(): MorphMany
|
public function purchases(): MorphMany
|
||||||
{
|
{
|
||||||
|
// This morph represents the purchaser (e.g. User), not the product.
|
||||||
return $this->morphMany(
|
return $this->morphMany(
|
||||||
config('shop.models.product_purchase', ProductPurchase::class),
|
config('shop.models.product_purchase', ProductPurchase::class),
|
||||||
'purchasable'
|
'purchaser'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cart items (purchases with status 'cart')
|
* Get cart items (purchases with status 'cart')
|
||||||
*/
|
*/
|
||||||
public function cartItems(): MorphMany
|
public function cartItems(): HasMany
|
||||||
{
|
{
|
||||||
return $this->purchases()->where('status', 'cart');
|
return $this->cart()->latest()->firstOrCreate()->items();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -50,16 +62,21 @@ trait HasShoppingCapabilities
|
||||||
ProductPrice|string $productPrice,
|
ProductPrice|string $productPrice,
|
||||||
int $quantity = 1,
|
int $quantity = 1,
|
||||||
): ProductPurchase {
|
): ProductPurchase {
|
||||||
if ($productPrice instanceof ProductPrice) {
|
|
||||||
} else {
|
|
||||||
$productPrice = ProductPrice::findOrFail($productPrice);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$productPrice?->product?->id) {
|
$productPrice = ($productPrice instanceof ProductPrice)
|
||||||
|
? $productPrice
|
||||||
|
: ProductPrice::findOrFail($productPrice);
|
||||||
|
|
||||||
|
if (!$productPrice?->purchasable?->id) {
|
||||||
throw new \Exception("Price does not belong to the specified product");
|
throw new \Exception("Price does not belong to the specified product");
|
||||||
}
|
}
|
||||||
|
|
||||||
$product = $productPrice->product;
|
$product = $productPrice->purchasable;
|
||||||
|
|
||||||
|
// product must have interface Purchasable
|
||||||
|
if (!in_array('Blax\Shop\Contracts\Purchasable', class_implements($product))) {
|
||||||
|
throw new \Exception("The product is not purchasable");
|
||||||
|
}
|
||||||
|
|
||||||
// Validate stock availability
|
// Validate stock availability
|
||||||
if ($product->manage_stock) {
|
if ($product->manage_stock) {
|
||||||
|
|
@ -81,7 +98,10 @@ trait HasShoppingCapabilities
|
||||||
|
|
||||||
// Create purchase record
|
// Create purchase record
|
||||||
$purchase = $this->purchases()->create([
|
$purchase = $this->purchases()->create([
|
||||||
'product_id' => $product->id,
|
'purchasable_id' => $product->id,
|
||||||
|
'purchasable_type' => get_class($product),
|
||||||
|
'purchaser_id' => $this->getKey(),
|
||||||
|
'purchaser_type' => get_class($this),
|
||||||
'quantity' => $quantity,
|
'quantity' => $quantity,
|
||||||
'status' => 'unpaid',
|
'status' => 'unpaid',
|
||||||
'meta' => array_merge([
|
'meta' => array_merge([
|
||||||
|
|
@ -96,64 +116,48 @@ trait HasShoppingCapabilities
|
||||||
'purchaser' => $this,
|
'purchaser' => $this,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$purchase->fresh();
|
||||||
|
|
||||||
|
if (!$purchase) {
|
||||||
|
throw new \Exception("Unable to create purchase record");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$purchase->purchasable || $purchase->purchasable->id !== $product->id) {
|
||||||
|
throw new \Exception("Purchase record does not match the product");
|
||||||
|
}
|
||||||
|
|
||||||
return $purchase;
|
return $purchase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add product to cart
|
* Add product to cart
|
||||||
*
|
*
|
||||||
* @param Product $product
|
* @param Product|ProductPrice $price
|
||||||
* @param int $quantity
|
* @param int $quantity
|
||||||
* @param array $options
|
* @param array $options
|
||||||
* @return ProductPurchase
|
* @return CartItem
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function addToCart(Product $product, int $quantity = 1, array $options = []): ProductPurchase
|
public function addToCart(Product|ProductPrice $price, int $quantity = 1, array $parameters = []): CartItem
|
||||||
{
|
{
|
||||||
// Check if product already in cart
|
return $this->cart()->latest()->firstOrCreate()->addToCart(
|
||||||
$existingItem = $this->cartItems()
|
$price,
|
||||||
->where('product_id', $product->id)
|
$quantity,
|
||||||
->first();
|
$parameters
|
||||||
|
);
|
||||||
if ($existingItem) {
|
|
||||||
return $this->updateCartQuantity($existingItem, $existingItem->quantity + $quantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate stock
|
|
||||||
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
|
|
||||||
throw new \Exception("Insufficient stock available");
|
|
||||||
}
|
|
||||||
|
|
||||||
$priceId = $options['price_id'] ?? null;
|
|
||||||
$price = $this->determinePurchasePrice($product, $priceId);
|
|
||||||
|
|
||||||
return $this->purchases()->create([
|
|
||||||
'product_id' => $product->id,
|
|
||||||
'quantity' => $quantity,
|
|
||||||
'status' => 'cart',
|
|
||||||
'meta' => array_merge([
|
|
||||||
'price_id' => $priceId,
|
|
||||||
'price' => $price,
|
|
||||||
'amount' => $price * $quantity,
|
|
||||||
], $options['meta'] ?? []),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update cart item quantity
|
* Update cart item quantity
|
||||||
*
|
*
|
||||||
* @param ProductPurchase $cartItem
|
* @param CartItem $cartItem
|
||||||
* @param int $quantity
|
* @param int $quantity
|
||||||
* @return ProductPurchase
|
* @return CartItem
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function updateCartQuantity(ProductPurchase $cartItem, int $quantity): ProductPurchase
|
public function updateCartQuantity(CartItem $cartItem, int $quantity): CartItem
|
||||||
{
|
{
|
||||||
if ($cartItem->status !== 'cart') {
|
$product = $cartItem->purchasable;
|
||||||
throw new \Exception("Cannot update non-cart item");
|
|
||||||
}
|
|
||||||
|
|
||||||
$product = $cartItem->product;
|
|
||||||
|
|
||||||
// Validate stock
|
// Validate stock
|
||||||
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
|
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
|
||||||
|
|
@ -161,15 +165,9 @@ trait HasShoppingCapabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
$meta = (array) $cartItem->meta;
|
$meta = (array) $cartItem->meta;
|
||||||
$priceId = $meta['price_id'] ?? null;
|
|
||||||
$price = $this->determinePurchasePrice($product, $priceId);
|
|
||||||
|
|
||||||
$cartItem->update([
|
$cartItem->update([
|
||||||
'quantity' => $quantity,
|
'quantity' => $quantity,
|
||||||
'meta' => array_merge($meta, [
|
|
||||||
'price' => $price,
|
|
||||||
'amount' => $price * $quantity,
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $cartItem->fresh();
|
return $cartItem->fresh();
|
||||||
|
|
@ -178,17 +176,13 @@ trait HasShoppingCapabilities
|
||||||
/**
|
/**
|
||||||
* Remove item from cart
|
* Remove item from cart
|
||||||
*
|
*
|
||||||
* @param ProductPurchase $cartItem
|
* @param CartItem $cartItem
|
||||||
* @return bool
|
* @return bool
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function removeFromCart(ProductPurchase $cartItem): bool
|
public function removeFromCart(CartItem $cartItem): bool
|
||||||
{
|
{
|
||||||
if ($cartItem->status !== 'cart') {
|
return $cartItem->forceDelete();
|
||||||
throw new \Exception("Cannot remove non-cart item");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cartItem->delete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -237,56 +231,19 @@ trait HasShoppingCapabilities
|
||||||
*/
|
*/
|
||||||
public function checkout(?string $cartId = null, array $options = []): Collection
|
public function checkout(?string $cartId = null, array $options = []): Collection
|
||||||
{
|
{
|
||||||
$items = $this->cartItems()->with('product')->get();
|
$items = $this->cartItems()
|
||||||
|
->with('purchasable')
|
||||||
|
->get();
|
||||||
|
|
||||||
if ($items->isEmpty()) {
|
if ($items->isEmpty()) {
|
||||||
throw new \Exception("Cart is empty");
|
throw new \Exception("Cart is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate stock for all items
|
$purchases = collect();
|
||||||
foreach ($items as $item) {
|
|
||||||
$product = $item->product;
|
|
||||||
if ($product->manage_stock && $product->getAvailableStock() < $item->quantity) {
|
|
||||||
throw new \Exception("Insufficient stock for: {$product->getLocalized('name')}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process each item
|
//
|
||||||
$completedPurchases = collect();
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$product = $item->product;
|
|
||||||
|
|
||||||
// Decrease stock
|
return $purchases;
|
||||||
if (!$product->decreaseStock($item->quantity)) {
|
|
||||||
// Rollback previous purchases
|
|
||||||
foreach ($completedPurchases as $purchase) {
|
|
||||||
$purchase->product->increaseStock($purchase->quantity);
|
|
||||||
$purchase->delete();
|
|
||||||
}
|
|
||||||
throw new \Exception("Unable to process checkout");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status and store charge info in meta
|
|
||||||
$meta = array_merge((array) $item->meta, [
|
|
||||||
'charge_id' => $options['charge_id'] ?? null,
|
|
||||||
'completed_at' => now()->toISOString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$item->update([
|
|
||||||
'status' => 'completed',
|
|
||||||
'meta' => $meta,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Trigger actions
|
|
||||||
$product->callActions('purchased', $item, [
|
|
||||||
'purchaser' => $this,
|
|
||||||
...$options,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$completedPurchases->push($item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $completedPurchases;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace Blax\Shop\Tests\Feature;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
use Blax\Shop\Models\CartItem;
|
use Blax\Shop\Models\CartItem;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
use Blax\Shop\Tests\TestCase;
|
use Blax\Shop\Tests\TestCase;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Workbench\App\Models\User;
|
use Workbench\App\Models\User;
|
||||||
|
|
@ -44,15 +45,21 @@ class CartManagementTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function it_can_add_items_to_cart()
|
public function it_can_add_items_to_cart()
|
||||||
{
|
{
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$price = ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $product->id,
|
||||||
|
'purchasable_type' => get_class($product),
|
||||||
|
]);
|
||||||
|
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product = Product::factory()->create(['price' => 99.99]);
|
|
||||||
|
|
||||||
$cartItem = CartItem::create([
|
$cartItem = CartItem::create([
|
||||||
'cart_id' => $cart->id,
|
'cart_id' => $cart->id,
|
||||||
'product_id' => $product->id,
|
'purchasable_id' => $price->id,
|
||||||
|
'purchasable_type' => get_class($price),
|
||||||
'quantity' => 2,
|
'quantity' => 2,
|
||||||
'price' => $product->price,
|
'price' => $price->unit_amount,
|
||||||
'subtotal' => $product->price * 2,
|
'subtotal' => $price->unit_amount * 2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertCount(1, $cart->fresh()->items);
|
$this->assertCount(1, $cart->fresh()->items);
|
||||||
|
|
@ -64,15 +71,13 @@ class CartManagementTest extends TestCase
|
||||||
{
|
{
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product = Product::factory()->create(['price' => 50.00]);
|
$product = Product::factory()->create(['price' => 50.00]);
|
||||||
|
$price = ProductPrice::factory()->create([
|
||||||
$cartItem = CartItem::create([
|
'purchasable_id' => $product->id,
|
||||||
'cart_id' => $cart->id,
|
'purchasable_type' => get_class($product),
|
||||||
'product_id' => $product->id,
|
'unit_amount' => 50.00,
|
||||||
'quantity' => 1,
|
|
||||||
'price' => $product->price,
|
|
||||||
'subtotal' => $product->price,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$cartItem = $cart->addToCart($price, quantity: 1);
|
||||||
$cartItem->update(['quantity' => 3]);
|
$cartItem->update(['quantity' => 3]);
|
||||||
|
|
||||||
$this->assertEquals(3, $cartItem->fresh()->quantity);
|
$this->assertEquals(3, $cartItem->fresh()->quantity);
|
||||||
|
|
@ -82,46 +87,44 @@ class CartManagementTest extends TestCase
|
||||||
public function it_can_remove_items_from_cart()
|
public function it_can_remove_items_from_cart()
|
||||||
{
|
{
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product = Product::factory()->create(['price' => 75.00]);
|
$product = Product::factory()->create();
|
||||||
|
$price = ProductPrice::factory()->create([
|
||||||
$cartItem = CartItem::create([
|
'purchasable_id' => $product->id,
|
||||||
'cart_id' => $cart->id,
|
'purchasable_type' => get_class($product),
|
||||||
'product_id' => $product->id,
|
'unit_amount' => 100.00,
|
||||||
'quantity' => 1,
|
|
||||||
'price' => $product->price,
|
|
||||||
'subtotal' => $product->price,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertCount(1, $cart->fresh()->items);
|
$cartItem = $cart->addToCart($price, quantity: 1);
|
||||||
|
|
||||||
|
$this->assertCount(1, $cart->items);
|
||||||
|
|
||||||
$cartItem->delete();
|
$cartItem->delete();
|
||||||
|
|
||||||
$this->assertCount(0, $cart->fresh()->items);
|
$this->assertCount(0, $cart->refresh()->items);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function it_calculates_cart_total_correctly()
|
public function it_calculates_cart_total_correctly()
|
||||||
{
|
{
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product1 = Product::factory()->create(['price' => 50.00]);
|
$product1 = Product::factory()->create();
|
||||||
$product2 = Product::factory()->create(['price' => 30.00]);
|
$product2 = Product::factory()->create();
|
||||||
|
|
||||||
CartItem::create([
|
$productPrice1 = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product1->id,
|
||||||
'product_id' => $product1->id,
|
'purchasable_type' => get_class($product1),
|
||||||
'quantity' => 2,
|
'unit_amount' => 50.00,
|
||||||
'price' => $product1->price,
|
|
||||||
'subtotal' => $product1->price * 2,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CartItem::create([
|
$productPrice2 = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product2->id,
|
||||||
'product_id' => $product2->id,
|
'purchasable_type' => get_class($product2),
|
||||||
'quantity' => 1,
|
'unit_amount' => 30.00,
|
||||||
'price' => $product2->price,
|
|
||||||
'subtotal' => $product2->price,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$cart->addToCart($productPrice1, quantity: 2);
|
||||||
|
$cart->addToCart($productPrice2, quantity: 1);
|
||||||
|
|
||||||
$total = $cart->fresh()->getTotal();
|
$total = $cart->fresh()->getTotal();
|
||||||
|
|
||||||
$this->assertEquals(130.00, $total); // (50 * 2) + (30 * 1)
|
$this->assertEquals(130.00, $total); // (50 * 2) + (30 * 1)
|
||||||
|
|
@ -134,22 +137,21 @@ class CartManagementTest extends TestCase
|
||||||
$product1 = Product::factory()->create();
|
$product1 = Product::factory()->create();
|
||||||
$product2 = Product::factory()->create();
|
$product2 = Product::factory()->create();
|
||||||
|
|
||||||
CartItem::create([
|
$product1Price = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product1->id,
|
||||||
'product_id' => $product1->id,
|
'purchasable_type' => get_class($product1),
|
||||||
'quantity' => 3,
|
'unit_amount' => 10.00,
|
||||||
'price' => 10.00,
|
|
||||||
'subtotal' => 30.00,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CartItem::create([
|
$product2Price = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product2->id,
|
||||||
'product_id' => $product2->id,
|
'purchasable_type' => get_class($product2),
|
||||||
'quantity' => 2,
|
'unit_amount' => 20.00,
|
||||||
'price' => 20.00,
|
|
||||||
'subtotal' => 40.00,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$cart->addToCart($product1Price, quantity: 3);
|
||||||
|
$cart->addToCart($product2Price, quantity: 2);
|
||||||
|
|
||||||
$totalItems = $cart->fresh()->getTotalItems();
|
$totalItems = $cart->fresh()->getTotalItems();
|
||||||
|
|
||||||
$this->assertEquals(5, $totalItems); // 3 + 2
|
$this->assertEquals(5, $totalItems); // 3 + 2
|
||||||
|
|
@ -238,16 +240,16 @@ class CartManagementTest extends TestCase
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product = Product::factory()->create(['price' => 45.00]);
|
$product = Product::factory()->create(['price' => 45.00]);
|
||||||
|
|
||||||
$cartItem = CartItem::create([
|
$productPrice = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product->id,
|
||||||
'product_id' => $product->id,
|
'purchasable_type' => get_class($product),
|
||||||
'quantity' => 1,
|
'unit_amount' => 45.00,
|
||||||
'price' => $product->price,
|
|
||||||
'subtotal' => $product->price,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$cartItem = $cart->addToCart($productPrice, quantity: 1);
|
||||||
|
|
||||||
$this->assertEquals($cart->id, $cartItem->cart->id);
|
$this->assertEquals($cart->id, $cartItem->cart->id);
|
||||||
$this->assertEquals($product->id, $cartItem->product->id);
|
$this->assertEquals($productPrice->id, $cartItem->purchasable_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -256,14 +258,14 @@ class CartManagementTest extends TestCase
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product = Product::factory()->create(['price' => 25.00]);
|
$product = Product::factory()->create(['price' => 25.00]);
|
||||||
|
|
||||||
$cartItem = CartItem::create([
|
$productPrice = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product->id,
|
||||||
'product_id' => $product->id,
|
'purchasable_type' => get_class($product),
|
||||||
'quantity' => 4,
|
'unit_amount' => 25.00,
|
||||||
'price' => $product->price,
|
|
||||||
'subtotal' => $product->price * 4,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$cartItem = $cart->addToCart($productPrice, quantity: 4);
|
||||||
|
|
||||||
$this->assertEquals(100.00, $cartItem->getSubtotal()); // 25 * 4
|
$this->assertEquals(100.00, $cartItem->getSubtotal()); // 25 * 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,20 +275,23 @@ class CartManagementTest extends TestCase
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product = Product::factory()->create();
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
$cartItem = CartItem::create([
|
$productPrice = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product->id,
|
||||||
'product_id' => $product->id,
|
'purchasable_type' => get_class($product),
|
||||||
'quantity' => 1,
|
'unit_amount' => 50.00,
|
||||||
'price' => 50.00,
|
|
||||||
'subtotal' => 50.00,
|
|
||||||
'attributes' => [
|
|
||||||
'color' => 'blue',
|
|
||||||
'size' => 'large',
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals('blue', $cartItem->attributes['color']);
|
$cartItem = $cart->addToCart(
|
||||||
$this->assertEquals('large', $cartItem->attributes['size']);
|
$productPrice,
|
||||||
|
quantity: 1,
|
||||||
|
parameters: [
|
||||||
|
'color' => 'blue',
|
||||||
|
'size' => 'large',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals('blue', $cartItem->parameters['color']);
|
||||||
|
$this->assertEquals('large', $cartItem->parameters['size']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -295,23 +300,23 @@ class CartManagementTest extends TestCase
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product = Product::factory()->create(['price' => 30.00]);
|
$product = Product::factory()->create(['price' => 30.00]);
|
||||||
|
|
||||||
CartItem::create([
|
$productPrice = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product->id,
|
||||||
'product_id' => $product->id,
|
'purchasable_type' => get_class($product),
|
||||||
'quantity' => 1,
|
'unit_amount' => 30.00,
|
||||||
'price' => $product->price,
|
|
||||||
'subtotal' => $product->price,
|
|
||||||
'attributes' => ['size' => 'small'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CartItem::create([
|
$cart->addToCart(
|
||||||
'cart_id' => $cart->id,
|
$productPrice,
|
||||||
'product_id' => $product->id,
|
quantity: 1,
|
||||||
'quantity' => 2,
|
parameters: ['size' => 'small']
|
||||||
'price' => $product->price,
|
);
|
||||||
'subtotal' => $product->price * 2,
|
|
||||||
'attributes' => ['size' => 'large'],
|
$cart->addToCart(
|
||||||
]);
|
$productPrice,
|
||||||
|
quantity: 2,
|
||||||
|
parameters: ['size' => 'large']
|
||||||
|
);
|
||||||
|
|
||||||
$this->assertCount(2, $cart->fresh()->items);
|
$this->assertCount(2, $cart->fresh()->items);
|
||||||
}
|
}
|
||||||
|
|
@ -322,14 +327,17 @@ class CartManagementTest extends TestCase
|
||||||
$cart = Cart::create();
|
$cart = Cart::create();
|
||||||
$product = Product::factory()->create();
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
$cartItem = CartItem::create([
|
$productPrice = ProductPrice::factory()->create([
|
||||||
'cart_id' => $cart->id,
|
'purchasable_id' => $product->id,
|
||||||
'product_id' => $product->id,
|
'purchasable_type' => get_class($product),
|
||||||
'quantity' => 1,
|
'unit_amount' => 75.00,
|
||||||
'price' => 50.00,
|
|
||||||
'subtotal' => 50.00,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$cartItem = $cart->addToCart(
|
||||||
|
$productPrice,
|
||||||
|
quantity: 1,
|
||||||
|
);
|
||||||
|
|
||||||
$cartItemId = $cartItem->id;
|
$cartItemId = $cartItem->id;
|
||||||
|
|
||||||
$cart->forceDelete();
|
$cart->forceDelete();
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,11 @@ class ProductManagementTest extends TestCase
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($product->increaseStock(10));
|
$this->assertTrue($product->increaseStock(60));
|
||||||
$this->assertEquals(60, $product->fresh()->stock_quantity);
|
$this->assertEquals(60, $product->AvailableStocks);
|
||||||
|
|
||||||
$this->assertTrue($product->decreaseStock(5));
|
$this->assertTrue($product->decreaseStock(5));
|
||||||
$this->assertEquals(55, $product->fresh()->stock_quantity);
|
$this->assertEquals(55, $product->AvailableStocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -58,11 +58,11 @@ class ProductManagementTest extends TestCase
|
||||||
{
|
{
|
||||||
$product = Product::factory()->create([
|
$product = Product::factory()->create([
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
'stock_quantity' => 5,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertFalse($product->decreaseStock(10));
|
$this->assertThrows(function () use ($product) {
|
||||||
$this->assertEquals(5, $product->fresh()->stock_quantity);
|
$product->decreaseStock(10);
|
||||||
|
}, \Blax\Shop\Exceptions\NotEnoughStockException::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -70,10 +70,11 @@ class ProductManagementTest extends TestCase
|
||||||
{
|
{
|
||||||
$product = Product::factory()->create([
|
$product = Product::factory()->create([
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
'stock_quantity' => 100,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals(100, $product->getAvailableStock());
|
$this->assertEquals(0, $product->getAvailableStock());
|
||||||
|
$product->increaseStock(20);
|
||||||
|
$this->assertEquals(20, $product->getAvailableStock());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -81,14 +82,11 @@ class ProductManagementTest extends TestCase
|
||||||
{
|
{
|
||||||
$productInStock = Product::factory()->create([
|
$productInStock = Product::factory()->create([
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
'stock_quantity' => 10,
|
|
||||||
'in_stock' => true,
|
|
||||||
]);
|
]);
|
||||||
|
$productInStock->increaseStock(10);
|
||||||
|
|
||||||
$productOutOfStock = Product::factory()->create([
|
$productOutOfStock = Product::factory()->create([
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
'stock_quantity' => 0,
|
|
||||||
'in_stock' => false,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($productInStock->isInStock());
|
$this->assertTrue($productInStock->isInStock());
|
||||||
|
|
@ -128,7 +126,8 @@ class ProductManagementTest extends TestCase
|
||||||
$product = Product::factory()->create();
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
ProductPrice::create([
|
ProductPrice::create([
|
||||||
'product_id' => $product->id,
|
'purchasable_id' => $product->id,
|
||||||
|
'purchasable_type' => get_class($product),
|
||||||
'type' => 'one-time',
|
'type' => 'one-time',
|
||||||
'price' => 9999,
|
'price' => 9999,
|
||||||
'currency' => 'USD',
|
'currency' => 'USD',
|
||||||
|
|
@ -136,7 +135,8 @@ class ProductManagementTest extends TestCase
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ProductPrice::create([
|
ProductPrice::create([
|
||||||
'product_id' => $product->id,
|
'purchasable_id' => $product->id,
|
||||||
|
'purchasable_type' => get_class($product),
|
||||||
'type' => 'recurring',
|
'type' => 'recurring',
|
||||||
'price' => 1999,
|
'price' => 1999,
|
||||||
'currency' => 'USD',
|
'currency' => 'USD',
|
||||||
|
|
@ -144,7 +144,7 @@ class ProductManagementTest extends TestCase
|
||||||
'active' => true,
|
'active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertCount(2, $product->fresh()->prices);
|
$this->assertCount(2, $product->prices);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -202,21 +202,20 @@ class ProductManagementTest extends TestCase
|
||||||
public function it_can_scope_in_stock_products()
|
public function it_can_scope_in_stock_products()
|
||||||
{
|
{
|
||||||
Product::factory()->create([
|
Product::factory()->create([
|
||||||
'in_stock' => true,
|
'manage_stock' => false,
|
||||||
'manage_stock' => true,
|
|
||||||
'stock_quantity' => 10,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Product::factory()->create([
|
$productInStock = Product::factory()->create([
|
||||||
'in_stock' => false,
|
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
'stock_quantity' => 0,
|
|
||||||
]);
|
]);
|
||||||
|
$productInStock->increaseStock(10);
|
||||||
|
|
||||||
$inStock = Product::inStock()->get();
|
$inStock = Product::inStock()->get();
|
||||||
|
|
||||||
$this->assertCount(1, $inStock);
|
$this->assertCount(2, $inStock);
|
||||||
$this->assertTrue($inStock->first()->in_stock);
|
$this->assertTrue((bool) ($inStock->first()->isInStock()));
|
||||||
|
$this->assertNotEquals($inStock->reverse()->first()->id, $inStock->first()->id);
|
||||||
|
$this->assertTrue((bool) ($inStock->reverse()->first()->isInStock()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
use Blax\Shop\Models\ProductPurchase;
|
use Blax\Shop\Models\ProductPurchase;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
use Blax\Shop\Models\Cart;
|
use Blax\Shop\Models\Cart;
|
||||||
|
use Blax\Shop\Models\CartItem;
|
||||||
use Blax\Shop\Tests\TestCase;
|
use Blax\Shop\Tests\TestCase;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Workbench\App\Models\User;
|
use Workbench\App\Models\User;
|
||||||
|
|
@ -18,17 +21,23 @@ class PurchaseFlowTest extends TestCase
|
||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$product = Product::factory()->create([
|
$product = Product::factory()->create([
|
||||||
'price' => 99.99,
|
|
||||||
'manage_stock' => false,
|
'manage_stock' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$purchase = $user->purchase($product, quantity: 1);
|
$price = ProductPrice::create([
|
||||||
|
'purchasable_id' => $product->id,
|
||||||
|
'purchasable_type' => get_class($product),
|
||||||
|
'amount' => 4999, // in cents
|
||||||
|
'currency' => 'USD',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$purchase = $user->purchase($price, quantity: 1);
|
||||||
|
|
||||||
$this->assertInstanceOf(ProductPurchase::class, $purchase);
|
$this->assertInstanceOf(ProductPurchase::class, $purchase);
|
||||||
$this->assertEquals($product->id, $purchase->product_id);
|
$this->assertEquals($product->id, $purchase->purchasable_id);
|
||||||
$this->assertEquals($user->id, $purchase->user_id);
|
$this->assertEquals($user->id, $purchase->purchaser_id);
|
||||||
$this->assertEquals(1, $purchase->quantity);
|
$this->assertEquals(1, $purchase->quantity);
|
||||||
$this->assertEquals('completed', $purchase->status);
|
$this->assertEquals('unpaid', $purchase->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -36,27 +45,36 @@ class PurchaseFlowTest extends TestCase
|
||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$product = Product::factory()->create([
|
$product = Product::factory()->create([
|
||||||
'price' => 49.99,
|
|
||||||
'manage_stock' => false,
|
'manage_stock' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$cartItem = $user->addToCart($product, quantity: 2);
|
$price = ProductPrice::create([
|
||||||
|
'purchasable_id' => $product->id,
|
||||||
|
'purchasable_type' => get_class($product),
|
||||||
|
'amount' => 2999, // in cents
|
||||||
|
'currency' => 'USD',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
$this->assertInstanceOf(ProductPurchase::class, $cartItem);
|
$cartItem = $user->addToCart($price, quantity: 2);
|
||||||
$this->assertEquals('cart', $cartItem->status);
|
|
||||||
|
$this->assertInstanceOf(CartItem::class, $cartItem);
|
||||||
$this->assertEquals(2, $cartItem->quantity);
|
$this->assertEquals(2, $cartItem->quantity);
|
||||||
$this->assertEquals($product->id, $cartItem->product_id);
|
$this->assertEquals($price->id, $cartItem->purchasable_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function user_can_get_cart_items()
|
public function user_can_get_cart_items()
|
||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$product1 = Product::factory()->create(['price' => 20.00]);
|
$product1 = Product::factory()->withPrices()->create();
|
||||||
$product2 = Product::factory()->create(['price' => 30.00]);
|
$product2 = Product::factory()->withPrices(2)->create();
|
||||||
|
|
||||||
$user->addToCart($product1, quantity: 1);
|
$this->assertCount(1, $product1->prices);
|
||||||
$user->addToCart($product2, quantity: 2);
|
$this->assertCount(2, $product2->prices);
|
||||||
|
|
||||||
|
$user->addToCart($product1->prices()->first(), quantity: 1);
|
||||||
|
$user->addToCart($product2->prices()->first(), quantity: 2);
|
||||||
|
|
||||||
$cartItems = $user->cartItems;
|
$cartItems = $user->cartItems;
|
||||||
|
|
||||||
|
|
@ -67,12 +85,9 @@ class PurchaseFlowTest extends TestCase
|
||||||
public function user_can_update_cart_item_quantity()
|
public function user_can_update_cart_item_quantity()
|
||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$product = Product::factory()->create([
|
$product = Product::factory()->withPrices()->create();
|
||||||
'price' => 50.00,
|
|
||||||
'manage_stock' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$cartItem = $user->addToCart($product, quantity: 1);
|
$cartItem = $user->addToCart($product->prices()->first(), quantity: 1);
|
||||||
|
|
||||||
$user->updateCartQuantity($cartItem, quantity: 5);
|
$user->updateCartQuantity($cartItem, quantity: 5);
|
||||||
|
|
||||||
|
|
@ -83,32 +98,32 @@ class PurchaseFlowTest extends TestCase
|
||||||
public function user_can_remove_item_from_cart()
|
public function user_can_remove_item_from_cart()
|
||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$product = Product::factory()->create();
|
$product = Product::factory()->withPrices()->create();
|
||||||
|
|
||||||
$cartItem = $user->addToCart($product, quantity: 1);
|
$cartItem = $user->addToCart($product->prices()->first(), quantity: 1);
|
||||||
$this->assertCount(1, $user->fresh()->cartItems);
|
|
||||||
|
$this->assertCount(1, $user->cartItems);
|
||||||
|
|
||||||
$user->removeFromCart($cartItem);
|
$user->removeFromCart($cartItem);
|
||||||
|
|
||||||
$this->assertCount(0, $user->fresh()->cartItems);
|
$this->assertCount(0, $user->refresh()->cartItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function user_can_checkout_cart()
|
public function user_can_checkout_cart()
|
||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$product1 = Product::factory()->create([
|
$product1 = Product::factory()->withPrices()->create();
|
||||||
'price' => 25.00,
|
$product2 = Product::factory()->withPrices()->create();
|
||||||
'manage_stock' => false,
|
|
||||||
]);
|
|
||||||
$product2 = Product::factory()->create([
|
|
||||||
'price' => 35.00,
|
|
||||||
'manage_stock' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user->addToCart($product1, quantity: 2);
|
$user->addToCart($product1, quantity: 2);
|
||||||
$user->addToCart($product2, quantity: 1);
|
$user->addToCart($product2, quantity: 1);
|
||||||
|
|
||||||
|
$this->assertThrows(fn() => $user->checkout(), NotEnoughStockException::class);
|
||||||
|
|
||||||
|
$product1->update(['manage_stock' => false]);
|
||||||
|
$product2->increaseStock(5);
|
||||||
|
|
||||||
$purchases = $user->checkout();
|
$purchases = $user->checkout();
|
||||||
|
|
||||||
$this->assertCount(2, $purchases);
|
$this->assertCount(2, $purchases);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue