I price factory, RA product traits
This commit is contained in:
parent
5dcbf73be3
commit
2008a16a53
|
|
@ -4,7 +4,6 @@ namespace Blax\Shop\Database\Factories;
|
||||||
|
|
||||||
use Blax\Shop\Models\ProductPrice;
|
use Blax\Shop\Models\ProductPrice;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class ProductPriceFactory extends Factory
|
class ProductPriceFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
@ -12,10 +11,21 @@ class ProductPriceFactory extends Factory
|
||||||
|
|
||||||
public function definition()
|
public function definition()
|
||||||
{
|
{
|
||||||
|
$type = $this->faker->randomElement(['one_time', 'recurring']);
|
||||||
|
$unit_amount = $this->faker->randomFloat(2, 100, 40000);
|
||||||
|
$sale_unit_amount = $this->faker->randomFloat(2, $unit_amount * 0.5, $unit_amount * 0.80);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'type' => $type,
|
||||||
|
'billing_scheme' => $this->faker->randomElement(['per_unit', 'tiered']),
|
||||||
'unit_amount' => $this->faker->randomFloat(2, 1, 1000),
|
'unit_amount' => $this->faker->randomFloat(2, 1, 1000),
|
||||||
'currency' => 'EUR',
|
'currency' => 'EUR',
|
||||||
'is_default' => false,
|
'is_default' => false,
|
||||||
|
'unit_amount' => $unit_amount,
|
||||||
|
'sale_unit_amount' => $sale_unit_amount,
|
||||||
|
'interval' => $type === 'recurring' ? $this->faker->randomElement(['day', 'week', 'month', 'quarter', 'year']) : null,
|
||||||
|
'interval_count' => $type === 'recurring' ? $this->faker->numberBetween(1, 12) : null,
|
||||||
|
'trial_period_days' => $type === 'recurring' ? $this->faker->numberBetween(0, 30) : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Models;
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
use App\Services\StripeService;
|
|
||||||
use Blax\Shop\Contracts\Cartable;
|
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 Blax\Shop\Traits\HasPrices;
|
||||||
|
use Blax\Shop\Traits\HasStocks;
|
||||||
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;
|
||||||
|
|
@ -16,11 +16,10 @@ 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\Database\Eloquent\Relations\MorphMany;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class Product extends Model implements Purchasable, Cartable
|
class Product extends Model implements Purchasable, Cartable
|
||||||
{
|
{
|
||||||
use HasFactory, HasUuids, HasMetaTranslation;
|
use HasFactory, HasUuids, HasMetaTranslation, HasStocks, HasPrices;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'slug',
|
'slug',
|
||||||
|
|
@ -126,14 +125,6 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prices(): MorphMany
|
|
||||||
{
|
|
||||||
return $this->morphMany(
|
|
||||||
config('shop.models.product_price', ProductPrice::class),
|
|
||||||
'purchasable'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function parent()
|
public function parent()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(self::class, 'parent_id');
|
return $this->belongsTo(self::class, 'parent_id');
|
||||||
|
|
@ -157,27 +148,11 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
return $this->hasMany(config('shop.models.product_attribute', 'Blax\Shop\Models\ProductAttribute'));
|
return $this->hasMany(config('shop.models.product_attribute', 'Blax\Shop\Models\ProductAttribute'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stocks(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function actions(): HasMany
|
public function actions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(config('shop.models.product_action', ProductAction::class));
|
return $this->hasMany(config('shop.models.product_action', ProductAction::class));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAvailableStocksAttribute(): int
|
|
||||||
{
|
|
||||||
return $this->stocks()
|
|
||||||
->available()
|
|
||||||
->where(function ($query) {
|
|
||||||
$query->whereNull('expires_at')
|
|
||||||
->orWhere('expires_at', '>', now());
|
|
||||||
})
|
|
||||||
->sum('quantity') ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function purchases(): MorphMany
|
public function purchases(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(
|
return $this->morphMany(
|
||||||
|
|
@ -215,110 +190,6 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCurrentPrice(bool|null $sales_price = null): ?float
|
|
||||||
{
|
|
||||||
return $this->defaultPrice()->first()?->getCurrentPrice($sales_price ?? $this->isOnSale());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isInStock(): bool
|
|
||||||
{
|
|
||||||
if (!$this->manage_stock) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->getAvailableStock() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function decreaseStock(int $quantity = 1): bool
|
|
||||||
{
|
|
||||||
if (!$this->manage_stock) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->AvailableStocks < $quantity) {
|
|
||||||
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();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function increaseStock(int $quantity = 1): bool
|
|
||||||
{
|
|
||||||
if (!$this->manage_stock) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->stocks()->create([
|
|
||||||
'quantity' => $quantity,
|
|
||||||
'type' => 'increase',
|
|
||||||
'status' => 'completed',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->logStockChange($quantity, 'increase');
|
|
||||||
|
|
||||||
$this->save();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function reserveStock(
|
|
||||||
int $quantity,
|
|
||||||
$reference = null,
|
|
||||||
?\DateTimeInterface $until = null,
|
|
||||||
?string $note = null
|
|
||||||
): ?\Blax\Shop\Models\ProductStock {
|
|
||||||
|
|
||||||
if (!$this->manage_stock) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
|
|
||||||
|
|
||||||
return $stockModel::reserve(
|
|
||||||
$this,
|
|
||||||
$quantity,
|
|
||||||
$reference,
|
|
||||||
$until,
|
|
||||||
$note
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAvailableStock(): int
|
|
||||||
{
|
|
||||||
if (!$this->manage_stock) {
|
|
||||||
return PHP_INT_MAX;
|
|
||||||
}
|
|
||||||
|
|
||||||
return max(0, $this->AvailableStocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getReservedStock(): int
|
|
||||||
{
|
|
||||||
return $this->activeStocks()->sum('quantity');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function logStockChange(int $quantityChange, string $type): void
|
|
||||||
{
|
|
||||||
DB::table('product_stock_logs')->insert([
|
|
||||||
'product_id' => $this->id,
|
|
||||||
'quantity_change' => $quantityChange,
|
|
||||||
'quantity_after' => $this->stock_quantity,
|
|
||||||
'type' => $type,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getAvailableActions(): array
|
public static function getAvailableActions(): array
|
||||||
{
|
{
|
||||||
return ProductAction::getAvailableActions();
|
return ProductAction::getAvailableActions();
|
||||||
|
|
@ -354,17 +225,6 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
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)
|
||||||
|
|
@ -391,41 +251,6 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopePriceRange($query, ?float $min = null, ?float $max = null)
|
|
||||||
{
|
|
||||||
if ($min !== null) {
|
|
||||||
$query->where('price', '>=', $min);
|
|
||||||
}
|
|
||||||
if ($max !== null) {
|
|
||||||
$query->where('price', '<=', $max);
|
|
||||||
}
|
|
||||||
return $query;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scopeOrderByPrice($query, string $direction = 'asc')
|
|
||||||
{
|
|
||||||
return $query->orderBy('price', $direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scopeLowStock($query)
|
|
||||||
{
|
|
||||||
$stockTable = config('shop.tables.product_stocks', 'product_stocks');
|
|
||||||
$productTable = config('shop.tables.products', 'products');
|
|
||||||
|
|
||||||
return $query->where('manage_stock', true)
|
|
||||||
->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
|
|
||||||
{
|
|
||||||
if (!$this->manage_stock || !$this->low_stock_threshold) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->getAvailableStock() <= $this->low_stock_threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isVisible(): bool
|
public function isVisible(): bool
|
||||||
{
|
{
|
||||||
if (!$this->is_visible || $this->status !== 'published') {
|
if (!$this->is_visible || $this->status !== 'published') {
|
||||||
|
|
@ -506,31 +331,4 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
|
|
||||||
return parent::newInstance($attributes, $exists);
|
return parent::newInstance($attributes, $exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function defaultPrice()
|
|
||||||
{
|
|
||||||
return $this->prices()->where('is_default', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPriceAttribute(): ?float
|
|
||||||
{
|
|
||||||
return $this->getCurrentPrice();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function reservations()
|
|
||||||
{
|
|
||||||
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
|
|
||||||
|
|
||||||
return $stockModel::reservations()
|
|
||||||
->where(function ($query) {
|
|
||||||
$query->whereNull('expires_at')
|
|
||||||
->orWhere('expires_at', '>', now());
|
|
||||||
})
|
|
||||||
->where('product_id', $this->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasPrice(): bool
|
|
||||||
{
|
|
||||||
return $this->prices()->exists();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Traits;
|
||||||
|
|
||||||
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
trait HasPrices
|
||||||
|
{
|
||||||
|
public function prices(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(
|
||||||
|
config('shop.models.product_price', ProductPrice::class),
|
||||||
|
'purchasable'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentPrice(bool|null $sales_price = null): ?float
|
||||||
|
{
|
||||||
|
return $this->defaultPrice()->first()?->getCurrentPrice($sales_price ?? $this->isOnSale());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopePriceRange($query, ?float $min = null, ?float $max = null)
|
||||||
|
{
|
||||||
|
return $query->whereHas('prices', function ($q) use ($min, $max) {
|
||||||
|
if ($min !== null) {
|
||||||
|
$q->where('unit_amount', '>=', $min);
|
||||||
|
}
|
||||||
|
if ($max !== null) {
|
||||||
|
$q->where('unit_amount', '<=', $max);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeOrderByPrice($query, string $direction = 'asc')
|
||||||
|
{
|
||||||
|
return $query->join('product_prices', function ($join) use ($query) {
|
||||||
|
$join->on($query->getModel()->getTable() . '.id', '=', 'product_prices.purchasable_id')
|
||||||
|
->where('product_prices.purchasable_type', '=', get_class($query->getModel()))
|
||||||
|
->where('product_prices.is_default', '=', true);
|
||||||
|
})->orderBy('product_prices.unit_amount', $direction)
|
||||||
|
->select($query->getModel()->getTable() . '.*');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function defaultPrice()
|
||||||
|
{
|
||||||
|
return $this->prices()->where('is_default', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriceAttribute(): ?float
|
||||||
|
{
|
||||||
|
return $this->getCurrentPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPrice(): bool
|
||||||
|
{
|
||||||
|
return $this->prices()->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Traits;
|
||||||
|
|
||||||
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
trait HasStocks
|
||||||
|
{
|
||||||
|
public function stocks(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableStocksAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->stocks()
|
||||||
|
->available()
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->whereNull('expires_at')
|
||||||
|
->orWhere('expires_at', '>', now());
|
||||||
|
})
|
||||||
|
->sum('quantity') ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isInStock(): bool
|
||||||
|
{
|
||||||
|
if (!$this->manage_stock) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getAvailableStock() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decreaseStock(int $quantity = 1, Carbon|null $until = null): bool
|
||||||
|
{
|
||||||
|
if (!$this->manage_stock) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->AvailableStocks < $quantity) {
|
||||||
|
return throw new NotEnoughStockException("Not enough stock available for product ID {$this->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stocks()->create([
|
||||||
|
'quantity' => -$quantity,
|
||||||
|
'type' => 'decrease',
|
||||||
|
'status' => 'completed',
|
||||||
|
'expires_at' => $until,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->logStockChange(-$quantity, 'decrease');
|
||||||
|
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function increaseStock(int $quantity = 1): bool
|
||||||
|
{
|
||||||
|
if (!$this->manage_stock) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stocks()->create([
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'type' => 'increase',
|
||||||
|
'status' => 'completed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->logStockChange($quantity, 'increase');
|
||||||
|
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reserveStock(
|
||||||
|
int $quantity,
|
||||||
|
$reference = null,
|
||||||
|
?\DateTimeInterface $until = null,
|
||||||
|
?string $note = null
|
||||||
|
): ?\Blax\Shop\Models\ProductStock {
|
||||||
|
|
||||||
|
if (!$this->manage_stock) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
|
||||||
|
|
||||||
|
return $stockModel::reserve(
|
||||||
|
$this,
|
||||||
|
$quantity,
|
||||||
|
$reference,
|
||||||
|
$until,
|
||||||
|
$note
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableStock(): int
|
||||||
|
{
|
||||||
|
if (!$this->manage_stock) {
|
||||||
|
return PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(0, $this->AvailableStocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReservedStock(): int
|
||||||
|
{
|
||||||
|
return $this->activeStocks()->sum('quantity');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function logStockChange(int $quantityChange, string $type): void
|
||||||
|
{
|
||||||
|
DB::table('product_stock_logs')->insert([
|
||||||
|
'product_id' => $this->id,
|
||||||
|
'quantity_change' => $quantityChange,
|
||||||
|
'quantity_after' => $this->stock_quantity,
|
||||||
|
'type' => $type,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 scopeLowStock($query)
|
||||||
|
{
|
||||||
|
$stockTable = config('shop.tables.product_stocks', 'product_stocks');
|
||||||
|
$productTable = config('shop.tables.products', 'products');
|
||||||
|
|
||||||
|
return $query->where('manage_stock', true)
|
||||||
|
->whereNotNull('low_stock_threshold')
|
||||||
|
->whereRaw("(SELECT COALESCE(SUM(quantity), 0) FROM {$stockTable} WHERE {$stockTable}.product_id = {$productTable}.id AND {$stockTable}.status IN ('completed', 'pending') AND ({$stockTable}.expires_at IS NULL OR {$stockTable}.expires_at > ?)) <= {$productTable}.low_stock_threshold", [
|
||||||
|
now()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLowStock(): bool
|
||||||
|
{
|
||||||
|
if (!$this->manage_stock || !$this->low_stock_threshold) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getAvailableStock() <= $this->low_stock_threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reservations()
|
||||||
|
{
|
||||||
|
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
|
||||||
|
|
||||||
|
return $stockModel::reservations()
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->whereNull('expires_at')
|
||||||
|
->orWhere('expires_at', '>', now());
|
||||||
|
})
|
||||||
|
->where('product_id', $this->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -59,15 +59,83 @@ class ProductScopeTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function it_can_filter_by_price_range()
|
public function it_can_filter_by_price_range()
|
||||||
{
|
{
|
||||||
Product::factory()->create(['meta' => json_encode(['price' => 50])]);
|
$product1 = Product::factory()->withPrices(1, 50)->create();
|
||||||
Product::factory()->create(['meta' => json_encode(['price' => 100])]);
|
$product2 = Product::factory()->withPrices(1, 100)->create();
|
||||||
Product::factory()->create(['meta' => json_encode(['price' => 150])]);
|
$product3 = Product::factory()->withPrices(1, 150)->create();
|
||||||
|
|
||||||
// Note: This test assumes the scope uses a 'price' column
|
$inRange = Product::priceRange(75, 125)->get();
|
||||||
// which may need adjustment based on actual implementation
|
|
||||||
$products = Product::all();
|
|
||||||
|
|
||||||
$this->assertCount(3, $products);
|
$this->assertCount(1, $inRange);
|
||||||
|
$this->assertTrue($inRange->contains($product2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_filter_by_minimum_price_only()
|
||||||
|
{
|
||||||
|
$product1 = Product::factory()->withPrices(1, 50)->create();
|
||||||
|
$product2 = Product::factory()->withPrices(1, 100)->create();
|
||||||
|
$product3 = Product::factory()->withPrices(1, 150)->create();
|
||||||
|
|
||||||
|
$minPrice = Product::priceRange(100)->get();
|
||||||
|
|
||||||
|
$this->assertCount(2, $minPrice);
|
||||||
|
$this->assertTrue($minPrice->contains($product2));
|
||||||
|
$this->assertTrue($minPrice->contains($product3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_filter_by_maximum_price_only()
|
||||||
|
{
|
||||||
|
$product1 = Product::factory()->withPrices(1, 50)->create();
|
||||||
|
$product2 = Product::factory()->withPrices(1, 100)->create();
|
||||||
|
$product3 = Product::factory()->withPrices(1, 150)->create();
|
||||||
|
|
||||||
|
$maxPrice = Product::priceRange(null, 100)->get();
|
||||||
|
|
||||||
|
$this->assertCount(2, $maxPrice);
|
||||||
|
$this->assertTrue($maxPrice->contains($product1));
|
||||||
|
$this->assertTrue($maxPrice->contains($product2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_order_products_by_price_ascending()
|
||||||
|
{
|
||||||
|
$product1 = Product::factory()->withPrices(1, 150)->create(['name' => 'Expensive']);
|
||||||
|
$product2 = Product::factory()->withPrices(1, 50)->create(['name' => 'Cheap']);
|
||||||
|
$product3 = Product::factory()->withPrices(1, 100)->create(['name' => 'Medium']);
|
||||||
|
|
||||||
|
$ordered = Product::orderByPrice('asc')->get();
|
||||||
|
|
||||||
|
$this->assertEquals($product2->id, $ordered->first()->id);
|
||||||
|
$this->assertEquals($product1->id, $ordered->last()->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_order_products_by_price_descending()
|
||||||
|
{
|
||||||
|
$product1 = Product::factory()->withPrices(1, 150)->create(['name' => 'Expensive']);
|
||||||
|
$product2 = Product::factory()->withPrices(1, 50)->create(['name' => 'Cheap']);
|
||||||
|
$product3 = Product::factory()->withPrices(1, 100)->create(['name' => 'Medium']);
|
||||||
|
|
||||||
|
$ordered = Product::orderByPrice('desc')->get();
|
||||||
|
|
||||||
|
$this->assertEquals($product1->id, $ordered->first()->id);
|
||||||
|
$this->assertEquals($product2->id, $ordered->last()->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_combine_price_range_and_order_by_price()
|
||||||
|
{
|
||||||
|
$product1 = Product::factory()->withPrices(1, 50)->create();
|
||||||
|
$product2 = Product::factory()->withPrices(1, 100)->create();
|
||||||
|
$product3 = Product::factory()->withPrices(1, 150)->create();
|
||||||
|
$product4 = Product::factory()->withPrices(1, 200)->create();
|
||||||
|
|
||||||
|
$filtered = Product::priceRange(75, 175)->orderByPrice('asc')->get();
|
||||||
|
|
||||||
|
$this->assertCount(2, $filtered);
|
||||||
|
$this->assertEquals($product2->id, $filtered->first()->id);
|
||||||
|
$this->assertEquals($product3->id, $filtered->last()->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -151,7 +219,7 @@ class ProductScopeTest extends TestCase
|
||||||
public function in_stock_scope_includes_products_without_stock_management()
|
public function in_stock_scope_includes_products_without_stock_management()
|
||||||
{
|
{
|
||||||
Product::factory()->create(['manage_stock' => false]);
|
Product::factory()->create(['manage_stock' => false]);
|
||||||
|
|
||||||
$managedProduct = Product::factory()->create(['manage_stock' => true]);
|
$managedProduct = Product::factory()->create(['manage_stock' => true]);
|
||||||
$managedProduct->increaseStock(10);
|
$managedProduct->increaseStock(10);
|
||||||
|
|
||||||
|
|
@ -164,7 +232,7 @@ class ProductScopeTest extends TestCase
|
||||||
public function in_stock_scope_excludes_out_of_stock_products()
|
public function in_stock_scope_excludes_out_of_stock_products()
|
||||||
{
|
{
|
||||||
$outOfStock = Product::factory()->create(['manage_stock' => true]);
|
$outOfStock = Product::factory()->create(['manage_stock' => true]);
|
||||||
|
|
||||||
$inStock = Product::factory()->create(['manage_stock' => true]);
|
$inStock = Product::factory()->create(['manage_stock' => true]);
|
||||||
$inStock->increaseStock(10);
|
$inStock->increaseStock(10);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue