Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
InventoryAuditService
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 2
42
0.00% covered (danger)
0.00%
0 / 1
 issues
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 repairAggregateMismatches
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Services\Stock;
4
5use Illuminate\Support\Collection;
6use Illuminate\Support\Facades\DB;
7use Illuminate\Support\Facades\Schema;
8
9class InventoryAuditService
10{
11    /** @return Collection<int, array{type:string,item:string,expected:float,actual:float}> */
12    public function issues(): Collection
13    {
14        if (! Schema::hasTable('warehouse_stocks')) {
15            return collect();
16        }
17
18        $issues = collect();
19        $baseRows = DB::select('SELECT p.id, p.name, p.stock expected, COALESCE(SUM(ws.quantity), 0) actual
20            FROM products p
21            LEFT JOIN product_variations v ON v.product_id = p.id
22            LEFT JOIN warehouse_stocks ws ON ws.product_id = p.id AND ws.variation_id IS NULL
23            WHERE v.id IS NULL AND p.deleted_at IS NULL
24            GROUP BY p.id, p.name, p.stock
25            HAVING ABS(actual - expected) > 0.0001');
26        foreach ($baseRows as $row) {
27            $issues->push(['type' => 'aggregate_mismatch', 'item' => $row->name . ' (#' . $row->id . ')', 'expected' => (float) $row->expected, 'actual' => (float) $row->actual]);
28        }
29
30        $variationRows = DB::select('SELECT v.id, p.name, v.stock_quantity expected, COALESCE(SUM(ws.quantity), 0) actual
31            FROM product_variations v
32            JOIN products p ON p.id = v.product_id
33            LEFT JOIN warehouse_stocks ws ON ws.variation_id = v.id
34            WHERE p.deleted_at IS NULL
35            GROUP BY v.id, p.name, v.stock_quantity
36            HAVING ABS(actual - expected) > 0.0001');
37        foreach ($variationRows as $row) {
38            $issues->push(['type' => 'variation_mismatch', 'item' => $row->name . ' / variation #' . $row->id, 'expected' => (float) $row->expected, 'actual' => (float) $row->actual]);
39        }
40
41        foreach (DB::table('warehouse_stocks')->where(fn ($q) => $q->where('quantity', '<', 0)->orWhere('reserved', '<', 0)->orWhereColumn('reserved', '>', 'quantity'))->get() as $row) {
42            $issues->push(['type' => 'invalid_balance', 'item' => 'warehouse stock #' . $row->id, 'expected' => max(0, (float) $row->quantity), 'actual' => (float) $row->reserved]);
43        }
44
45        return $issues;
46    }
47
48    public function repairAggregateMismatches(): int
49    {
50        $before = $this->issues()->whereIn('type', ['aggregate_mismatch', 'variation_mismatch'])->count();
51        app(LocationInventoryService::class)->reconcileAllToAggregate();
52
53        return $before;
54    }
55}