Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.12% covered (warning)
68.12%
47 / 69
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
StockService
68.12% covered (warning)
68.12%
47 / 69
25.00% covered (danger)
25.00%
1 / 4
40.15
0.00% covered (danger)
0.00%
0 / 1
 syncStockForProduct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 syncLockedProduct
97.50% covered (success)
97.50%
39 / 40
0.00% covered (danger)
0.00%
0 / 1
16
 createStock
25.00% covered (danger)
25.00%
4 / 16
0.00% covered (danger)
0.00%
0 / 1
10.75
 createStockVariant
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace App\Service;
4
5use App\Models\admin\Stock;
6
7class StockService
8{
9    /** POS availability projection; quantities live on products and variations. */
10    const UNLIMITED_STOCK = 999999;
11
12    public static function syncStockForProduct($product)
13    {
14        return \Illuminate\Support\Facades\DB::transaction(function () use ($product) {
15            $current = \App\Models\admin\Product::lockForUpdate()->findOrFail($product->id);
16            return self::syncLockedProduct($current);
17        });
18    }
19
20    private static function syncLockedProduct($product)
21    {
22        $product->loadMissing('variations');
23
24        // Both global and product flags are required for quantity tracking.
25        $globallyTracked = (bool) feature('inventory_tracking');
26
27        if ($product->variations->count() > 0) {
28            if ($globallyTracked && $product->manage_stock) {
29                $total = (int) $product->variations->sum('stock_quantity');
30                if ((int) $product->stock !== $total) {
31                    $product->stock = $total;
32                    $product->save();
33                }
34            }
35            $liveMarkers = [];
36            foreach ($product->variations as $variation) {
37                $marker = 'VAR-' . $variation->id;
38                $liveMarkers[] = $marker;
39                $stock = Stock::firstOrNew([
40                    'product_id' => $product->id,
41                    'sku' => $marker,
42                ]);
43                $stock->selling_price = $variation->price ?: ($product->selling_price ?: $product->price);
44                $stock->product_price = $product->price;
45                // Zero is unavailable when tracked; manual mode uses status only.
46                $available = ($globallyTracked && $product->manage_stock)
47                    ? max(0, (int) $variation->stock_quantity)
48                    : (($product->stock_status === 'in_stock' && $variation->stock_status === 'in_stock') ? self::UNLIMITED_STOCK : 0);
49                $stock->stock_in = (int) $stock->stock_out + $available;
50                $stock->save();
51            }
52
53            // Retire obsolete projections without deleting historical stock identities.
54            Stock::where('product_id', $product->id)
55                ->where('sku', 'like', 'VAR-%')
56                ->whereNotIn('sku', $liveMarkers)
57                ->update(['stock_in' => \Illuminate\Support\Facades\DB::raw('stock_out')]);
58            Stock::where('product_id', $product->id)->where('sku', 'BASE-' . $product->id)->update(['stock_in' => \Illuminate\Support\Facades\DB::raw('stock_out')]);
59        } else {
60            $stock = Stock::firstOrNew([
61                'product_id' => $product->id,
62                'sku' => 'BASE-' . $product->id,
63            ]);
64            $stock->selling_price = $product->selling_price ?: $product->price;
65            $stock->product_price = $product->price;
66            $available = ($globallyTracked && $product->manage_stock)
67                ? max(0, (int) $product->stock)
68                : ($product->stock_status === 'in_stock' ? self::UNLIMITED_STOCK : 0);
69            $stock->stock_in = (int) $stock->stock_out + $available;
70            $stock->save();
71
72            // Retire stale variation rows if this product no longer has variations (same reasoning as above).
73            Stock::where('product_id', $product->id)->where('sku', 'like', 'VAR-%')->update(['stock_in' => \Illuminate\Support\Facades\DB::raw('stock_out')]);
74        }
75    }
76
77    public static function createStock($product)
78    {
79        $request = request();
80        $data = [];
81        if ($request->has('color_id') && is_array($request->color_id)) {
82            foreach ($request->color_id as $index => $color) {
83                $data = Stock::query()->create([
84                    'product_id' => $product->id,
85                    'color_id' => $color,
86                    'size_id' => $request->size[$index] ?? null,
87                    'stock_in' => $request->stock_in[$index] ?? 0,
88                    'product_price' => $request->product_price[$index] ?? 0,
89                    'selling_price' => $request->selling_price[$index] ?? 0,
90                    'discount' => $request->discount[$index] ?? 0,
91                    'created_at' => now(),
92                    'updated_at' => now(),
93                ]);
94            }
95        }
96
97        return true;
98
99    }
100
101    public static function createStockVariant($product)
102    {
103        request()->validate([
104            'color_id' => 'required|array', 'stock_in' => 'required|array',
105            'stock_in.*' => 'required|integer|min:1',
106        ]);
107        foreach (request()->color_id as $index => $color) {
108            $stock = Stock::where('product_id', $product->id)->where('color_id', $color)
109                ->where('size_id', request()->size[$index] ?? null)->firstOrFail();
110            app(\App\Services\Stock\InventoryService::class)->adjust($stock->id, (int) request()->stock_in[$index]);
111        }
112        return true;
113    }
114}