Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
112 / 119
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
PickingOrderFinishService
94.12% covered (success)
94.12%
112 / 119
28.57% covered (danger)
28.57%
2 / 7
31.20
0.00% covered (danger)
0.00%
0 / 1
 finish
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
4.02
 notifyRequester
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
7
 markPendingProductsAsSeparatedPartial
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 markStockIOProductAsSeparatedPartial
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
5.06
 markRequestItemAsSeparatedPartial
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 processLinkedRequests
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
3
 processStatusPickingOrder
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
5.01
1<?php
2
3namespace Domain\PickingOrder\Services\V2;
4
5use App\Models\PickingOrder\PickingOrderProduct;
6use App\Models\Request\Request;
7use App\Models\Request\RequestItem;
8use App\Models\StockIO\StockIOProduct;
9use App\Models\User;
10use Carbon\Carbon;
11
12use App\Models\PickingOrder\PickingOrder;
13use App\Notifications\ExpoPushNotification;
14use App\Notifications\RequestSeparatedNotification;
15
16use Domain\PickingOrder\Enums\PickingOrderStatusEnum;
17use Domain\PickingOrder\Enums\PickingOrderTypeEnumV2;
18use Domain\PickingOrder\Exception\PickingOrderCanceledException;
19use Domain\Request\Enums\RequestItemStatusEnum;
20use Domain\Request\Enums\RequestStatusEnum;
21use Domain\StockIO\Enums\StockProductStatusEnum;
22use Domain\StockIO\Enums\StockOutputReasonEnum;
23use Domain\StockIO\Enums\StockTypeEnum;
24use Domain\StockIO\Services\V2\StockOutputFinishService;
25use Exception;
26
27readonly class PickingOrderFinishService
28{
29    /**
30     * @throws Exception
31     */
32    public function finish(PickingOrder $pickingOrder, User $user): void
33    {
34        if ($pickingOrder->isCanceled()) {
35            throw new PickingOrderCanceledException(__('Picking order is canceled.'));
36        }
37
38        if ($pickingOrder->isPaused()) {
39            (new PickingOrderStartService())->resumeCurrentPause($pickingOrder);
40        }
41
42        if($pickingOrder->type->value == PickingOrderTypeEnumV2::INPUT->value) {
43            $pickingOrder->setAttribute('status', PickingOrderStatusEnum::CONCLUDED->value);
44        } else {
45            $pickingOrder->load([
46                'pickingOrderProduct.stockIOProduct.requestItem.request',
47                'pickingOrderProduct.stockIOProduct.stockIO',
48                'stockIOProducts.stockIO.integration',
49                'stockIO.stockIOProducts',
50            ]);
51
52            $this->markPendingProductsAsSeparatedPartial($pickingOrder);
53            $this->processLinkedRequests($pickingOrder);
54
55            $this->processStatusPickingOrder($pickingOrder);
56        }
57
58        $pickingOrder->setAttribute('end_date', Carbon::now());
59        $pickingOrder->save();
60
61        $pickingOrder->load(['stockIOProducts.stockIO.integration', 'stockIO.stockIOProducts']);
62
63        $this->notifyRequester($pickingOrder);
64        (new StockOutputFinishService)->processOutputFinish($pickingOrder, $user);
65    }
66
67    /**
68     * @param PickingOrder $pickingOrder
69     */
70    private function notifyRequester(PickingOrder $pickingOrder): void
71    {
72        $stockIos = $pickingOrder->stockIOProducts
73            ->map(fn ($product) => $product->stockIO)
74            ->filter(fn ($stockIo) =>
75                $stockIo &&
76                $stockIo->type === StockTypeEnum::OUTPUT &&
77                $stockIo->reason === StockOutputReasonEnum::REQUEST->value
78            )
79            ->unique('id');
80
81        foreach ($stockIos as $stockIo) {
82            $requester = $stockIo->request->requester ?? null;
83
84            if ($requester) {
85                if ($stockIo->integration) {
86                    $requester->notify(
87                        new RequestSeparatedNotification($stockIo->integration->parameters['request_number'])
88                    );
89
90                    if ($requester->expo_token) {
91                        $requester->notify(
92                            new ExpoPushNotification(
93                                __('Your request has been processed. Request :request', [
94                                    'request' => $stockIo->integration->parameters['request_number']
95                                ]),
96                                __('Request processed.')
97                            )
98                        );
99                    }
100                }
101            }
102        }
103    }
104
105    /**
106     * @param PickingOrder $pickingOrder
107     * @return void
108     */
109    private function markPendingProductsAsSeparatedPartial(PickingOrder $pickingOrder): void
110    {
111        $pickingOrder->pickingOrderProduct->each(function ($pickingOrderProduct) {
112            if ($pickingOrderProduct->status !== PickingOrderStatusEnum::PENDING) {
113                return;
114            }
115
116            $isRequestOutput = $pickingOrderProduct->stockIOProduct?->stockIO?->isRequest() ?? false;
117
118            $pickingOrderProduct->setAttribute(
119                'status',
120                $isRequestOutput
121                    ? PickingOrderStatusEnum::SEPARATED_PARTIAL
122                    : PickingOrderStatusEnum::SEPARATED
123            );
124            $pickingOrderProduct->save();
125
126            $this->markStockIOProductAsSeparatedPartial($pickingOrderProduct->stockIOProduct, $isRequestOutput);
127
128            if ($isRequestOutput) {
129                $this->markRequestItemAsSeparatedPartial($pickingOrderProduct->stockIOProduct?->requestItem);
130            }
131        });
132    }
133
134    private function markStockIOProductAsSeparatedPartial(?StockIOProduct $stockIOProduct, bool $isRequestOutput): void
135    {
136        if (!$stockIOProduct || $stockIOProduct->status === StockProductStatusEnum::CANCELED) {
137            return;
138        }
139
140        if (in_array($stockIOProduct->status, [
141            StockProductStatusEnum::SEPARATED,
142            StockProductStatusEnum::CONCLUDED,
143            StockProductStatusEnum::DELIVERED_PARTIAL,
144        ], true)) {
145            return;
146        }
147
148        $stockIOProduct->setAttribute(
149            'status',
150            $isRequestOutput
151                ? StockProductStatusEnum::SEPARATED_PARTIAL
152                : StockProductStatusEnum::SEPARATED
153        );
154        $stockIOProduct->save();
155    }
156
157    private function markRequestItemAsSeparatedPartial(?RequestItem $requestItem): void
158    {
159        if (!$requestItem || $requestItem->status !== RequestItemStatusEnum::PENDING) {
160            return;
161        }
162
163        $requestItem->setAttribute('status', RequestItemStatusEnum::SEPARATED_PARCIAL);
164        $requestItem->save();
165    }
166
167    private function processLinkedRequests(PickingOrder $pickingOrder): void
168    {
169        $pickingOrder->pickingOrderProduct
170            ->pluck('stockIOProduct.requestItem.request')
171            ->filter()
172            ->unique('id')
173            ->each(function (Request $request) {
174                $statusCounts = $request->requestItems()
175                    ->selectRaw('status, COUNT(*) as total')
176                    ->groupBy('status')
177                    ->pluck('total', 'status');
178
179                $get = fn ($status) => $statusCounts[$status] ?? 0;
180
181                $countAll = $statusCounts
182                    ->reject(fn ($_, $status) => $status == RequestItemStatusEnum::CANCELED->value)
183                    ->sum();
184
185                if ($countAll === 0) {
186                    return;
187                }
188
189                $separated = $get(RequestItemStatusEnum::SEPARATED->value);
190
191                $request->setAttribute(
192                    'status',
193                    $separated === $countAll
194                        ? RequestStatusEnum::SEPARATED
195                        : RequestStatusEnum::SEPARATED_PARTIAL
196                );
197                $request->save();
198            });
199    }
200
201    private function processStatusPickingOrder(PickingOrder $pickingOrder): void
202    {
203        $statusCounts = $pickingOrder->pickingOrderProduct()
204            ->selectRaw('status, COUNT(*) as total')
205            ->groupBy('status')
206            ->pluck('total', 'status');
207
208        $get = fn($status) => $statusCounts[$status] ?? 0;
209
210        $countAll = $statusCounts
211            ->reject(fn($_, $status) => $status == PickingOrderStatusEnum::CANCELED->value)
212            ->sum();
213
214        $separated = $get(PickingOrderStatusEnum::SEPARATED->value);
215        $separatedPartial = $get(PickingOrderStatusEnum::SEPARATED_PARTIAL->value);
216
217        if ($countAll === 0) {
218            return;
219        }
220
221        if ($separated === $countAll) {
222            $pickingOrder->setAttribute('status', PickingOrderStatusEnum::SEPARATED->value);
223
224            return;
225        }
226
227        if ($separatedPartial > 0 || $separated > 0) {
228            $pickingOrder->setAttribute('status', PickingOrderStatusEnum::SEPARATED_PARTIAL->value);
229        }
230    }
231}