EscolaLMS/Cart

View on GitHub
src/Models/Product.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace EscolaLms\Cart\Models;

use EscolaLms\Cart\Contracts\Productable;
use EscolaLms\Cart\Database\Factories\ProductFactory;
use EscolaLms\Cart\Enums\ProductType;
use EscolaLms\Cart\Models\Contracts\ProductInterface;
use EscolaLms\Cart\Models\Contracts\ProductTrait;
use EscolaLms\Cart\Models\User;
use EscolaLms\Cart\QueryBuilders\ProductModelQueryBuilder;
use EscolaLms\Cart\Services\Contracts\ProductServiceContract;
use EscolaLms\Core\Models\User as CoreUser;
use EscolaLms\Tags\Models\Tag;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;

/**
 * EscolaLms\Cart\Models\Product
 *
 * @OA\Schema (
 *      schema="Product",
 *      @OA\Property(
 *          property="id",
 *          description="id",
 *          type="integer",
 *      ),
 *      @OA\Property(
 *          property="name",
 *          description="name",
 *          type="string"
 *      ),
 *      @OA\Property(
 *          property="price",
 *          description="price",
 *          type="integer"
 *      )
 * )
 * @property int $id
 * @property string $name
 * @property string $type
 * @property int $price
 * @property int|null $price_old
 * @property float $tax_rate
 * @property int $extra_fees
 * @property bool $purchasable
 * @property string|null $teaser_url
 * @property string|null $description
 * @property string|null $poster_url
 * @property string|null $duration
 * @property int|null $limit_per_user
 * @property int|null $limit_total
 * @property \Illuminate\Support\Carbon|null $created_at
 * @property \Illuminate\Support\Carbon|null $updated_at
 * @property-read Collection|\EscolaLms\Cart\Models\Category[] $categories
 * @property-read int|null $categories_count
 * @property-read Collection $authors
 * @property-read bool $buyable_by_user
 * @property-read int $calculated_duration
 * @property-read bool $owned_by_user
 * @property-read int $owned_by_user_quantity
 * @property-read string|null $poster_absolute_url
 * @property-read Collection|\EscolaLms\Cart\Models\ProductProductable[] $productables
 * @property-read int|null $productables_count
 * @property-read Collection|Product[] $relatedProducts
 * @property-read int|null $related_products_count
 * @property-read Collection|Tag[] $tags
 * @property-read int|null $tags_count
 * @property-read Collection|User[] $users
 * @property-read int|null $users_count
 * @property-read int|null $available_quantity
 * @property-read int $sold_quantity
 * @method static \EscolaLms\Cart\Database\Factories\ProductFactory factory(...$parameters)
 * @method static ProductModelQueryBuilder|Product newModelQuery()
 * @method static ProductModelQueryBuilder|Product newQuery()
 * @method static ProductModelQueryBuilder|Product query()
 * @method static ProductModelQueryBuilder|Product whereCreatedAt($value)
 * @method static ProductModelQueryBuilder|Product whereDescription($value)
 * @method static ProductModelQueryBuilder|Product whereDoesntHaveProductablesNotBuyableByUser(?\EscolaLms\Core\Models\User $user = null)
 * @method static ProductModelQueryBuilder|Product whereDoesntHaveProductablesNotOwnedByUser(?\EscolaLms\Core\Models\User $user = null)
 * @method static ProductModelQueryBuilder|Product whereDuration($value)
 * @method static ProductModelQueryBuilder|Product whereExtraFees($value)
 * @method static ProductModelQueryBuilder|Product whereHasProductable(\Illuminate\Database\Eloquent\Model|Productable $productable)
 * @method static ProductModelQueryBuilder|Product whereHasProductableClass(string $productable_type)
 * @method static ProductModelQueryBuilder|Product whereHasProductableClassAndId(string $productable_type, int $productable_id)
 * @method static ProductModelQueryBuilder|Product whereHasUser(\EscolaLms\Core\Models\User $user)
 * @method static ProductModelQueryBuilder|Product whereHasUserWithType(\EscolaLms\Core\Models\User $user, string $productType)
 * @method static ProductModelQueryBuilder|Product whereId($value)
 * @method static ProductModelQueryBuilder|Product whereLimitPerUser($value)
 * @method static ProductModelQueryBuilder|Product whereLimitTotal($value)
 * @method static ProductModelQueryBuilder|Product whereName($value)
 * @method static ProductModelQueryBuilder|Product wherePosterUrl($value)
 * @method static ProductModelQueryBuilder|Product wherePrice($value)
 * @method static ProductModelQueryBuilder|Product wherePriceOld($value)
 * @method static ProductModelQueryBuilder|Product wherePurchasable($value)
 * @method static ProductModelQueryBuilder|Product whereTaxRate($value)
 * @method static ProductModelQueryBuilder|Product whereTeaserUrl($value)
 * @method static ProductModelQueryBuilder|Product whereType($value)
 * @method static ProductModelQueryBuilder|Product whereUpdatedAt($value)
 * @property string|null $subscription_period
 * @property int|null $subscription_duration
 * @property bool $recursive
 * @property bool|null $has_trial
 * @property string|null $trial_period
 * @property int|null $trial_duration
 * @property-read string|null $poster_url_or_productable_thumbnail
 * @method static ProductModelQueryBuilder|Product whereHasTrial($value)
 * @method static ProductModelQueryBuilder|Product whereRecursive($value)
 * @method static ProductModelQueryBuilder|Product whereSubscriptionDuration($value)
 * @method static ProductModelQueryBuilder|Product whereSubscriptionPeriod($value)
 * @method static ProductModelQueryBuilder|Product whereTrialDuration($value)
 * @method static ProductModelQueryBuilder|Product whereTrialPeriod($value)
 * @property array|null $fields
 * @method static ProductModelQueryBuilder|Product whereFields($value)
 * @method static ProductModelQueryBuilder|Product whereHasUserWithProductType(\EscolaLms\Core\Models\User $user, string $productType, ?bool $active = true)
 * @mixin \Eloquent
 */
class Product extends Model implements ProductInterface
{
    use ProductTrait;
    use HasFactory;

    protected $guarded = [
        'id'
    ];

    protected $casts = [
        'purchasable' => 'bool',
        'has_trial' => 'bool',
        'recursive' => 'bool',
        'fields' => 'array',
    ];

    public function productables(): HasMany
    {
        return $this->hasMany(ProductProductable::class);
    }

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'products_users')
            ->using(ProductUser::class)
            ->withPivot(['quantity', 'end_date', 'status']);
    }

    public function tags(): MorphMany
    {
        return $this->morphMany(Tag::class, 'morphable');
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class, 'products_categories');
    }

    public function relatedProducts()
    {
        return $this->belongsToMany(Product::class, 'related_product', 'product_id', 'related_product_id');
    }

    public function getBuyableDescription(): string
    {
        return $this->description ?? '';
    }

    // @phpstan-ignore-next-line
    public function getBuyablePrice(?array $options = null): int
    {
        return $this->price;
    }

    public function getExtraFees(?array $options = null): int
    {
        return $this->extra_fees ?? 0;
    }

    public function getTaxRate(): float
    {
        return $this->tax_rate ?? 0;
    }

    public function getBuyableByUserAttribute(?CoreUser $user = null, ?int $quantity = null): bool
    {
        /** @var CoreUser $user */
        $user = $user ?? Auth::user();
        return app(ProductServiceContract::class)->productIsBuyableByUser($this, $user, false, $quantity);
    }

    public function getOwnedByUserAttribute(?CoreUser $user = null): bool
    {
        /** @var CoreUser $user */
        $user = $user ?? Auth::user();
        return app(ProductServiceContract::class)->productIsOwnedByUser($this, $user);
    }

    public function getOwnedByUserQuantityAttribute(?CoreUser $user = null): int
    {
        $user = $user ?? Auth::user();
        return optional(optional($this->users->where('id', '=', $user->getKey())->first())->pivot)->quantity ?? 0;
    }

    public function newEloquentBuilder($query): ProductModelQueryBuilder
    {
        return new ProductModelQueryBuilder($query);
    }

    protected static function newFactory(): ProductFactory
    {
        return ProductFactory::new();
    }

    public function getPosterUrlOrProductableThumbnailAttribute(): ?string
    {
        $path = $this->getRawOriginal('poster_url');

        if (!empty($path)) {
            return $path;
        }

        if ($this->type === ProductType::SINGLE) {
            /** @var ProductProductable|null $first */
            $first = $this->productables->first();
            if ($first) {
                $productable = $first->getCanonicalProductableAttribute();
                if ($productable) {
                    return $productable->getThumbnail();
                }
            }
        }

        return null;
    }

    public function getPosterAbsoluteUrlAttribute(): ?string
    {
        $path = $this->getPosterUrlOrProductableThumbnailAttribute();
        if (!empty($path)) {
            return preg_match('/^(http|https):.*$/', $path) ? $path : url(Storage::url($path));
        }
        return null;
    }

    public function getAuthorsAttribute(): Collection
    {
        $authors = new Collection();
        foreach ($this->productables as $productable) {
            $productableModel = $productable->getCanonicalProductableAttribute();
            if ($productableModel instanceof Productable) {
                $authors = $authors->merge($productableModel->getProductableAuthors());
            }
        }
        return $authors->unique('id');
    }

    public function getCalculatedDurationAttribute(): int
    {
        $duration = 0;
        foreach ($this->productables as $productable) {
            $productableModel = $productable->productable;
            if ($productableModel instanceof Productable) {
                $duration += $productableModel->getProductableDuration();
            }
        }
        return $duration;
    }

    public function getAvailableQuantityAttribute(): ?int
    {
        return is_null($this->limit_total) ? null : $this->limit_total - $this->sold_quantity;
    }

    public function getSoldQuantityAttribute(): int
    {
        return $this->users()->sum('quantity');
    }

    public function getGrossPrice(): int
    {
        return $this->getBuyablePrice() + $this->getTax();
    }

    public function getTax(): int
    {
        return (int)round($this->getBuyablePrice() * $this->getTaxRate() / 100, 0);
    }

    public function getSubscriptionParameters(): array
    {
        return [
            'subscription_period' => $this->subscription_period,
            'subscription_duration' => $this->subscription_duration,
            'recursive' => $this->recursive
        ];
    }

    public function getTrailParameters(): array
    {
        return [
            'has_trial' => $this->has_trial,
            'trial_period' => $this->trial_period,
            'trial_duration' => $this->trial_duration,
        ];
    }
}