Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
57.89% |
22 / 38 |
|
45.00% |
9 / 20 |
CRAP | |
0.00% |
0 / 1 |
| Product | |
57.89% |
22 / 38 |
|
45.00% |
9 / 20 |
58.13 | |
0.00% |
0 / 1 |
| auditExcept | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| variations | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| specs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| boms | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| usedAsMaterialIn | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| isRawMaterial | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| isManufacturable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| isStockTracked | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| stockDisplayMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| category | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| categories | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| collections | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| stocks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| boot | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
| purchases_details | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| purchase_detail | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| images | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getAll | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
| findById | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| findBySkuNo | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models\admin; |
| 4 | |
| 5 | use App\Models\Bom; |
| 6 | use App\Models\BomItem; |
| 7 | use App\Models\Category; |
| 8 | use App\Models\Concerns\OwnedByVendor; |
| 9 | use App\Models\ProductSpec; |
| 10 | use App\Models\ProductCollection; |
| 11 | use App\Models\ProductVariation; |
| 12 | use App\Service\CacheService; |
| 13 | use App\Services\Stock\StockResolver; |
| 14 | use App\Support\Auditable; |
| 15 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 16 | use Illuminate\Database\Eloquent\Model; |
| 17 | |
| 18 | class 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 | } |