¨4.0.1¨

This commit is contained in:
¨NW¨
2023-12-03 14:07:47 +00:00
parent c08b36d1b6
commit f35052522d
1112 changed files with 43019 additions and 24987 deletions

View File

@@ -0,0 +1,94 @@
<?php
namespace Modules\Product\Entities\Concerns;
use Modules\Tag\Entities\Tag;
use Modules\Brand\Entities\Brand;
use Modules\Tax\Entities\TaxClass;
use Modules\Option\Entities\Option;
use Modules\Review\Entities\Review;
use Modules\Category\Entities\Category;
use Modules\Variation\Entities\Variation;
use Modules\Product\Entities\ProductVariant;
use Modules\Attribute\Entities\ProductAttribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
trait EloquentRelations
{
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class)->withDefault();
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class, 'product_tags');
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'product_categories');
}
public function taxClass(): BelongsTo
{
return $this->belongsTo(TaxClass::class)->withDefault();
}
public function reviews(): HasMany
{
return $this->hasMany(Review::class);
}
public function attributes(): HasMany
{
return $this->hasMany(ProductAttribute::class);
}
public function variations(): BelongsToMany
{
return $this->belongsToMany(Variation::class, 'product_variations')
->orderBy('position')
->withTrashed();
}
public function variants(): HasMany
{
return $this->hasMany(ProductVariant::class, 'product_id');
}
public function options(): BelongsToMany
{
return $this->belongsToMany(Option::class, 'product_options')
->orderBy('position')
->withTrashed();
}
public function relatedProducts(): BelongsToMany
{
return $this->belongsToMany(static::class, 'related_products', 'product_id', 'related_product_id');
}
public function upSellProducts(): BelongsToMany
{
return $this->belongsToMany(static::class, 'up_sell_products', 'product_id', 'up_sell_product_id');
}
public function crossSellProducts(): BelongsToMany
{
return $this->belongsToMany(static::class, 'cross_sell_products', 'product_id', 'cross_sell_product_id');
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Modules\Product\Entities\Concerns;
trait Filterable
{
public function filter($filter)
{
return $filter->apply($this);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Modules\Product\Entities\Concerns;
use Modules\Support\Money;
trait HasSpecialPrice
{
public function getSpecialPrice(): Money
{
$specialPrice = $this->attributes['special_price'];
if ($this->special_price_type === 'percent') {
$discountedPrice = ($specialPrice / 100) * $this->attributes['price'];
$specialPrice = $this->attributes['price'] - $discountedPrice;
}
if ($specialPrice < 0) {
$specialPrice = 0;
}
return Money::inDefaultCurrency($specialPrice);
}
public function hasSpecialPrice(): bool
{
if (is_null($this->special_price)) {
return false;
}
if ($this->hasSpecialPriceStartDate() && $this->hasSpecialPriceEndDate()) {
return $this->specialPriceStartDateIsValid() && $this->specialPriceEndDateIsValid();
}
if ($this->hasSpecialPriceStartDate()) {
return $this->specialPriceStartDateIsValid();
}
if ($this->hasSpecialPriceEndDate()) {
return $this->specialPriceEndDateIsValid();
}
return true;
}
public function hasPercentageSpecialPrice(): bool
{
return $this->hasSpecialPrice() && $this->special_price_type === 'percent';
}
private function hasSpecialPriceStartDate(): bool
{
return !is_null($this->special_price_start);
}
private function hasSpecialPriceEndDate(): bool
{
return !is_null($this->special_price_end);
}
private function specialPriceStartDateIsValid(): bool
{
return today() >= $this->special_price_start;
}
private function specialPriceEndDateIsValid(): bool
{
return today() <= $this->special_price_end;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Modules\Product\Entities\Concerns;
use Modules\FlashSale\Entities\FlashSale;
trait HasStock
{
public function isOutOfStock(): bool
{
return !$this->isInStock();
}
public function isInStock()
{
if (FlashSale::contains($this)) {
return FlashSale::remainingQty($this) > 0;
}
if ($this->hasAnyVariants()) {
$productWithStock = $this->variants()
->where(function ($query) {
$query->where(
[
['manage_stock', true],
['qty', '>', 0],
]);
$query->orWhere('manage_stock', false);
})
->where('in_stock', true)
->first();
return (bool)$productWithStock;
} else {
if ($this->manage_stock && $this->qty === 0) {
return false;
}
return $this->in_stock;
}
}
public function markAsInStock(): void
{
$this->withoutEvents(function () {
$this->update(['in_stock' => true]);
});
}
public function markAsOutOfStock(): void
{
$this->withoutEvents(function () {
$this->update(['in_stock' => false]);
});
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Modules\Product\Entities\Concerns;
trait IsNew
{
public function isNew(): bool
{
if ($this->hasNewFromDate() && $this->hasNewToDate()) {
return $this->newFromDateIsValid() && $this->newToDateIsValid();
}
if ($this->hasNewFromDate()) {
return $this->newFromDateIsValid();
}
if ($this->hasNewToDate()) {
return $this->newToDateIsValid();
}
return false;
}
private function hasNewFromDate(): bool
{
return !is_null($this->new_from);
}
private function hasNewToDate(): bool
{
return !is_null($this->new_to);
}
private function newFromDateIsValid(): bool
{
return today() >= $this->new_from;
}
private function newToDateIsValid(): bool
{
return today() <= $this->new_to;
}
}

View File

@@ -0,0 +1,190 @@
<?php
namespace Modules\Product\Entities\Concerns;
use Modules\Support\Money;
use Modules\Media\Entities\File;
use Modules\FlashSale\Entities\FlashSale;
use Illuminate\Database\Eloquent\Collection;
use Modules\FlashSale\Entities\FlashSaleProduct;
trait ModelAccessors
{
public function getVariantAttribute()
{
if (request()->query('variant')) {
return $this->variants()->where('uid', request()->query('variant'))->first();
}
return $this->variants()->default()->first();
}
public function getIsInFlashSaleAttribute()
{
return FlashSale::contains($this);
}
public function getFlashSaleEndDateAttribute()
{
if (FlashSale::contains($this)) {
return FlashSaleProduct::where('product_id', $this->id)->first()?->end_date;
}
}
public function getPriceAttribute($price): Money
{
return Money::inDefaultCurrency($price);
}
public function getFormattedPriceAttribute(): string
{
return product_price_formatted($this);
}
public function getFormattedPriceRangeAttribute(): ?string
{
if ($this->variants()->exists()) {
$minPrice = $this->variants()->min('price');
$maxPrice = $this->variants()->max('price');
if ($minPrice !== $maxPrice) {
$formattedMinPriceInCurrentCurrency = Money::inDefaultCurrency($minPrice)->convertToCurrentCurrency()->format();
$formattedMaxPriceInCurrentCurrency = Money::inDefaultCurrency($maxPrice)->convertToCurrentCurrency()->format();
return "$formattedMinPriceInCurrentCurrency - $formattedMaxPriceInCurrentCurrency";
}
}
return null;
}
public function getSpecialPriceAttribute($specialPrice)
{
if (!is_null($specialPrice)) {
return Money::inDefaultCurrency($specialPrice);
}
}
public function getHasPercentageSpecialPriceAttribute(): bool
{
return $this->hasPercentageSpecialPrice();
}
public function getSpecialPricePercentAttribute()
{
if ($this->hasPercentageSpecialPrice()) {
return round($this->special_price->amount(), 2);
}
}
public function getSellingPriceAttribute($sellingPrice): Money
{
if (FlashSale::contains($this)) {
$sellingPrice = FlashSale::pivot($this)->price->amount();
}
return Money::inDefaultCurrency($sellingPrice);
}
public function getTotalAttribute($total): Money
{
return Money::inDefaultCurrency($total);
}
/**
* Get the product's base image.
*
* @return File
*/
public function getBaseImageAttribute(): File
{
return $this->files->where('pivot.zone', 'base_image')->first() ?: new File();
}
/**
* Get product's additional images.
*
* @return Collection
*/
public function getAdditionalImagesAttribute(): Collection
{
return $this->files->where('pivot.zone', 'additional_images')
->sortBy('pivot.id');
}
public function getMediaAttribute()
{
return $this->filterFiles(['base_image', 'additional_images'])->get();
}
/**
* Get product's downloadable files.
*
* @return Collection
*/
public function getDownloadsAttribute()
{
return $this->files
->where('pivot.zone', 'downloads')
->sortBy('pivot.id')
->flatten();
}
public function getDoesManageStockAttribute(): bool
{
return (bool)$this->manage_stock;
}
public function getQtyAttribute($qty)
{
return $qty;
}
public function getIsInStockAttribute(): bool
{
return (bool)$this->isInStock();
}
public function getIsOutOfStockAttribute(): bool
{
return $this->isOutOfStock();
}
public function getIsNewAttribute(): bool
{
return $this->isNew();
}
public function getAttributeSetsAttribute()
{
return $this->getAttribute('attributes')->groupBy('attributeSet');
}
public function getRatingPercentAttribute()
{
if ($this->relationLoaded('reviews')) {
return ($this->reviews->avg->rating / 5) * 100;
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Modules\Product\Entities\Concerns;
trait Predicates
{
/**
* Is this Product purchased by the current user?
*
* @return bool
*/
public function purchasedByUser(): bool
{
return true;
}
public function hasAnyVariation()
{
return $this->getAttribute('variations')->isNotEmpty();
}
public function hasAnyVariants(): bool
{
return $this->getAttribute('variants')->isNotEmpty();
}
public function hasAnyOption(): bool
{
return $this->getAttribute('options')->isNotEmpty();
}
public function hasAnyAttribute(): bool
{
return $this->getAttribute('attributes')->isNotEmpty();
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Modules\Product\Entities\Concerns;
trait QueryScopes
{
public function scopeForCard($query): void
{
$query
->withName()
->withBaseImage()
->withPrice()
->withCount('options')
->with('reviews')
->withStock()
->withNew()
->addSelect(
[
'products.id',
'products.slug',
]
);
}
public function scopeWithName($query): void
{
$query->with('translations:id,product_id,locale,name');
}
public function scopeWithStock($query): void
{
$query->addSelect(
[
'products.in_stock',
'products.manage_stock',
'products.qty',
]
);
}
public function scopeWithNew($query): void
{
$query->addSelect(
[
'products.new_from',
'products.new_to',
]
);
}
public function scopeWithPrice($query): void
{
$query->addSelect(
[
'products.price',
'products.special_price',
'products.special_price_type',
'products.selling_price',
'products.special_price_start',
'products.special_price_end',
]
);
}
public function scopeWithBaseImage($query): void
{
$query->with([
'files' => function ($q) {
$q->wherePivot('zone', 'base_image');
},
]);
}
}