Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.89% covered (warning)
57.89%
22 / 38
45.00% covered (danger)
45.00%
9 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Product
57.89% covered (warning)
57.89%
22 / 38
45.00% covered (danger)
45.00%
9 / 20
58.13
0.00% covered (danger)
0.00%
0 / 1
 auditExcept
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 variations
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 specs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 boms
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 usedAsMaterialIn
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isRawMaterial
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isManufacturable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isStockTracked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stockDisplayMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 category
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 categories
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 collections
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 stocks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 boot
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 purchases_details
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 purchase_detail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 images
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAll
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 findById
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findBySkuNo
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Models\admin;
4
5use App\Models\Bom;
6use App\Models\BomItem;
7use App\Models\Category;
8use App\Models\Concerns\OwnedByVendor;
9use App\Models\ProductSpec;
10use App\Models\ProductCollection;
11use App\Models\ProductVariation;
12use App\Service\CacheService;
13use App\Services\Stock\StockResolver;
14use App\Support\Auditable;
15use Illuminate\Database\Eloquent\Factories\HasFactory;
16use Illuminate\Database\Eloquent\Model;
17
18class Product extends Model
19{
20    use Auditable, HasFactory, OwnedByVendor;
21
22    /** Storefront-noise columns we don't want cluttering the activity log. */
23    protected function auditExcept(): array
24    {
25        return ['position', 'slug', 'content', 'gallery', 'og_image', 'keywords', 'meta_title', 'meta_description'];
26    }
27
28    public const TABLE = 'products';
29
30    protected $table = self::TABLE;
31
32    protected $fillable = [
33        'name',
34        'vendor_id',
35        'category_id',
36        'type',
37        'unit',
38        'sku_no',
39        'barcode',
40        'slug',
41        'product_code',
42        'thumbnail',
43        'details',
44        'short_details',
45        'price',
46        'cost_price',
47        'selling_price',
48        'discount',
49        'section_id',
50        'brand_id',
51        'stock',
52        'alert_quantity',
53        'created_by',
54        'position',
55        'content',
56        'manage_stock',
57        'stock_status',
58        'is_featured',
59        'has_warranty',
60        'video_url',
61        'meta_title',
62        'meta_description',
63        'keywords',
64        'og_image',
65        'gallery',
66    ];
67
68    protected $casts = [
69        'manage_stock' => 'boolean',
70        'is_featured' => 'boolean',
71        'has_warranty' => 'boolean',
72        'alert_quantity' => 'integer',
73        'gallery' => 'array',
74    ];
75
76    public function variations()
77    {
78        return $this->hasMany(ProductVariation::class);
79    }
80
81    public function specs()
82    {
83        return $this->hasMany(ProductSpec::class)->orderBy('sort_order');
84    }
85
86    /* -------------------------------------------------- Manufacturing (Phase 9) */
87
88    /** BOMs that make this product (it's the finished/semi-finished output). */
89    public function boms()
90    {
91        return $this->hasMany(Bom::class, 'product_id');
92    }
93
94    /** BOM lines where this product is consumed as a raw material. */
95    public function usedAsMaterialIn()
96    {
97        return $this->hasMany(BomItem::class, 'material_id');
98    }
99
100    public function isRawMaterial(): bool
101    {
102        return $this->type === 'raw_material';
103    }
104
105    public function isManufacturable(): bool
106    {
107        return in_array($this->type, ['finished', 'semi_finished'], true);
108    }
109
110    /** Â§5.4 â€” is this product's quantity actually tracked? (needs the
111     *  `inventory_tracking` feature ON *and* this product's manage_stock ON). */
112    public function isStockTracked(): bool
113    {
114        return app(StockResolver::class)->isTracked($this);
115    }
116
117    public function stockDisplayMode(): string
118    {
119        return app(StockResolver::class)->displayMode($this);
120    }
121
122    public function category()
123    {
124        return $this->belongsTo(Category::class, 'category_id');
125    }
126
127    public function categories()
128    {
129        return $this->belongsToMany(Category::class, 'category_product', 'product_id', 'category_id');
130    }
131
132    public function collections()
133    {
134        return $this->belongsToMany(ProductCollection::class, 'product_collection_product')
135            ->withPivot('sort_order')->orderByPivot('sort_order');
136    }
137
138    public function stocks()
139    {
140        return $this->hasMany(Stock::class, 'product_id')->orderBy('color_id', 'DESC')->orderBy('stock_in', 'ASC');
141    }
142
143    /**
144     * Automatic created_by value assigned from logged in user
145     * */
146    protected static function boot()
147    {
148        parent::boot();
149
150        static::creating(function ($query) {
151            $query->created_by = auth()->id();
152        });
153
154        // CACHING: the storefront caches product/category listings and detail
155        // pages (see app/Service/CacheService.php + MhHelper.php's products()
156        // helper). Whenever a product is created, updated, or deleted, bust
157        // exactly the cache keys that could now be stale â€” so changes show up
158        // on the storefront immediately instead of waiting for the TTL.
159        //
160        // getOriginal('category_id') inside saved() still returns the
161        // PRE-update category (Eloquent only calls syncOriginal() â€” which
162        // would overwrite it with the new value â€” right after the 'saved'
163        // event fires), so this also correctly refreshes the OLD category's
164        // listing page if a product is moved to a different category.
165        static::saved(function ($product) {
166            CacheService::forgetProduct(
167                $product->id,
168                $product->category_id,
169                $product->getOriginal('category_id')
170            );
171        });
172
173        static::deleted(function ($product) {
174            CacheService::forgetProduct($product->id, $product->category_id);
175        });
176    }
177
178    public function purchases_details()
179    {
180        return $this->hasMany(Purchase::class);
181    }
182
183    public function purchase_detail()
184    {
185        return $this->hasOne(PurchaseDetails::class);
186    }
187
188    public function images()
189    {
190        return $this->hasMany(ProductImage::class, 'product_id');
191    }
192
193    /**
194     * Repository Methods
195     * */
196    public static function getAll($is_pluck = false, $order_by_id_desc = true)
197    {
198        $query = self::query();
199
200        ($order_by_id_desc ? $query->latest() : $query->orderBy('id'));
201
202        return $is_pluck ? $query->pluck('name', 'id') : $query->get();
203    }
204
205    public static function findById($id)
206    {
207
208        return self::query()->findOrFail($id);
209    }
210
211    public static function findBySkuNo($sku_no)
212    {
213
214        return self::query()
215            ->where('sku_no', '=', $sku_no)
216            ->first();
217    }
218}