Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.25% covered (warning)
81.25%
130 / 160
54.55% covered (warning)
54.55%
6 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestSeparationService
81.25% covered (warning)
81.25%
130 / 160
54.55% covered (warning)
54.55%
6 / 11
40.18
0.00% covered (danger)
0.00%
0 / 1
 separation
85.37% covered (warning)
85.37%
35 / 41
0.00% covered (danger)
0.00%
0 / 1
4.05
 findAddressItem
41.18% covered (danger)
41.18%
7 / 17
0.00% covered (danger)
0.00%
0 / 1
10.09
 populateItemToTransferDTO
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 validateCanPickedItem
45.00% covered (danger)
45.00%
9 / 20
0.00% covered (danger)
0.00%
0 / 1
9.16
 storePickedItem
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
2.00
 markItemAsPicked
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
6
 setStatusRequestItem
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setPickingOrderProductAsSeparated
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPickingOrderProductAsSeparatedPartial
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 processStatusPickingOrder
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
5.00
 processRequestLog
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Domain\Request\Services;
4
5use App\Models\PickingOrder\PickingOrderProduct;
6use Exception;
7use Throwable;
8
9use Illuminate\Support\Facades\DB;
10use Illuminate\Support\ItemNotFoundException;
11
12use App\Events\ServiceProcessed;
13use App\Helper\AppHelper;
14use App\Models\PickedItem;
15use App\Models\User;
16use App\Models\Item\Item;
17use App\Models\PickingOrder\PickingOrder;
18use App\Models\Request\RequestItem;
19use App\Models\StockIO\StockIOProduct;
20use App\Models\Transferable\Transferable;
21
22use Domain\PickingOrder\DTOs\PickedItemDTO;
23use Domain\PickingOrder\DTOs\PickItemWarehouseDTO;
24use Domain\PickingOrder\Enums\PickingOrderStatusEnum;
25use Domain\PickingOrder\Exception\PickingOrderCanceledException;
26use Domain\PickingOrder\Services\PickedItemStoreService;
27use Domain\PickingOrder\Services\PickingOrderStartService;
28use Domain\Transaction\Enums\TransactionOutputReasonEnum;
29use Domain\Transaction\Enums\TransactionTypeEnum;
30use Domain\Transferable\DTOs\TransferItemDTO;
31use Domain\Transferable\Exceptions\CantTransferItemException;
32use Domain\Transferable\Services\TransferableTransferPickedItemService;
33use Domain\Request\DTOs\RequestLogStoreDTO;
34use Domain\Request\Enums\RequestItemStatusEnum;
35use Domain\Request\Enums\RequestLogOperationEnum;
36use Domain\StockIO\Enums\StockProductStatusEnum;
37use Domain\TransferTask\Enums\PickedTransferTaskProductStatusEnum;
38use Domain\Item\Exceptions\ItemPriorityValueNullException;
39readonly class RequestSeparationService
40{
41
42    /**
43     * @param PickItemWarehouseDTO $pickItemDTO
44     * @param Transferable $sender
45     * @param PickingOrder $pickingOrder
46     * @param User $user
47     * @return void
48     * @throws Throwable
49     */
50    public function separation(PickItemWarehouseDTO $pickItemDTO, Transferable $sender, PickingOrder $pickingOrder, User $user): void
51    {
52        if ($pickingOrder->isCanceled()) {
53            throw new PickingOrderCanceledException(
54                __('Picking order is canceled.')
55            );
56        }
57
58        try {
59            DB::beginTransaction();
60
61            PickingOrder::where('id', $pickingOrder->getKey())
62                ->lockForUpdate()
63                ->first();
64
65            $itemOnAddress = $this->findAddressItem($sender, $pickItemDTO);
66            $this->validateCanPickedItem($itemOnAddress, $pickItemDTO);
67
68            $itemToTransferDTO = $this->populateItemToTransferDTO(
69                $itemOnAddress,
70                $pickItemDTO->amount
71            );
72
73            if ($pickingOrder->isPaused()) {
74                (new PickingOrderStartService())->resumeCurrentPause($pickingOrder);
75
76                $pickingOrder->status = PickingOrderStatusEnum::STARTED;
77                $pickingOrder->save();
78            }
79
80            $stockIO = $pickItemDTO->stockIOProduct->stockIO;
81
82            $item = (new TransferableTransferPickedItemService())->transferOne(
83                $itemToTransferDTO,
84                $sender->transferable,
85                $stockIO,
86                TransactionTypeEnum::OUTPUT,
87                TransactionOutputReasonEnum::PRE_ORDER->value,
88                $user,
89                $pickItemDTO->observation,
90                $stockIO->sector->value
91            );
92
93            /** @var PickingOrderProduct $pickingOrderProduct */
94            $pickingOrderProduct = PickingOrderProduct::query()
95                ->where('picking_order_id', $pickingOrder->getKey())
96                ->where('stock_io_product_id', $pickItemDTO->stockIOProduct->getKey())
97                ->firstOrFail();
98
99            $this->storePickedItem($pickItemDTO, $item, $user, $pickingOrderProduct);
100            $this->markItemAsPicked($pickItemDTO->stockIOProduct, $user, $pickingOrderProduct);
101
102            $this->processRequestLog($pickItemDTO->stockIOProduct, $user);
103
104            DB::commit();
105        } catch (Throwable $throwable) {
106            DB::rollBack();
107            throw $throwable;
108        }
109
110        ServiceProcessed::dispatch(self::class, $user);
111    }
112
113    /**
114     * @param Transferable $transferable
115     * @param PickItemWarehouseDTO $pickedItemDTO
116     * @return Item
117     */
118    private function findAddressItem(Transferable $transferable, PickItemWarehouseDTO $pickedItemDTO): Item
119    {
120        $productField = AppHelper::getPriorityField($pickedItemDTO->stockIOProduct->product);
121        $priorityValue = $pickedItemDTO->integrationItemDTO->priorityValue;
122
123        if (is_null($priorityValue) && !is_null($productField)) {
124            throw new ItemPriorityValueNullException(__("The product has a priority setting, but the priority value was not specified."));
125        }
126
127        if (is_null($productField)) {
128            $itemToTransfer = $transferable->transferable->items()->where('product_id', $pickedItemDTO->stockIOProduct->product->getKey())->first();
129        } else {
130            $itemToTransfer = AppHelper::findItemByPriorityValue(
131                $transferable,
132                $pickedItemDTO->stockIOProduct->product,
133                $pickedItemDTO->integrationItemDTO->priorityValue,
134                $pickedItemDTO->integrationItemDTO->priorityField,
135            );
136        }
137
138        if (!$itemToTransfer) {
139            throw new ItemNotFoundException(
140                __("Product is not part of this address")
141            );
142        }
143
144        return $itemToTransfer;
145    }
146
147    /**
148     * @param Item $itemToTransfer
149     * @param float $amount
150     * @return TransferItemDTO
151     */
152    private function populateItemToTransferDTO(
153        Item $itemToTransfer,
154        float $amount
155    ): TransferItemDTO {
156        return new TransferItemDTO([
157            'item'         => $itemToTransfer,
158            'amount'       => $amount,
159        ]);
160    }
161
162    /**
163     * @param Item $addressItem
164     * @param PickItemWarehouseDTO $pickedItemDTO
165     * @return void
166     */
167    private function validateCanPickedItem(Item $addressItem, PickItemWarehouseDTO $pickedItemDTO): void
168    {
169        if ($addressItem->isAmountExceeded($pickedItemDTO->amount)) {
170            if($addressItem->pickTransferTaskProducts
171                ->where('status', PickedTransferTaskProductStatusEnum::TRANSFERRING)
172                ->sum('amount') > 0
173            ) {
174                throw new CantTransferItemException(
175                    __('The quantity you are trying to transfer exceeds the available amount. Part of the item is already reserved for another transfer.')
176                );
177            } else {
178                throw new CantTransferItemException(
179                    __('The amount you are trying to transfer exceeds the available amount in the item')
180                );
181            }
182        }
183
184        if ($pickedItemDTO->stockIOProduct->hasAmountExceededLimit($pickedItemDTO->amount)) {
185            throw new CantTransferItemException(
186                __('The quantity entered exceeds the quantity requested by the request')
187            );
188        }
189
190        if ($pickedItemDTO->stockIOProduct->getAttribute('status') == StockProductStatusEnum::CANCELED) {
191            throw new CantTransferItemException(
192                __('Cant pick the product: :product. Request product is canceled.',
193                    ['product' => $pickedItemDTO->stockIOProduct->product->integration->getAttribute('parameters')['integration_code']]
194                )
195            );
196        }
197    }
198
199    /**
200     * @param PickItemWarehouseDTO $pickItemDTO
201     * @param Item $item
202     * @param User $user
203     * @param PickingOrderProduct $pickingOrderProduct
204     * @return void
205     * @throws Exception
206     */
207    private function storePickedItem(PickItemWarehouseDTO $pickItemDTO, Item $item, User $user, PickingOrderProduct $pickingOrderProduct): void
208    {
209        $stockIOProduct = $pickItemDTO->stockIOProduct;
210
211        $pickedItem = PickedItem::select('*')
212            ->where('stock_io_product_id', $stockIOProduct->getKey())
213            ->where('item_id', $item->getKey())
214            ->where('picking_order_product_id', $pickingOrderProduct->getKey())
215            ->first();
216
217        if ($pickedItem) {
218            $pickedItem->setAttribute('amount', $pickedItem->getAttribute('amount') + $pickItemDTO->amount);
219            $pickedItem->save();
220        } else {
221            $pickedItemDTO = new PickedItemDTO([
222                'stock_io_product' => $stockIOProduct,
223                'item' => $item,
224                'observation' => $pickItemDTO->observation,
225                'amount' => $pickItemDTO->amount,
226                'pickingOrderProduct' => $pickingOrderProduct
227            ]);
228
229            (new PickedItemStoreService())->store(
230                $pickedItemDTO,
231                $user
232            );
233        }
234    }
235
236    /**
237     * @param StockIOProduct $stockOutputProduct
238     * @param User $user
239     * @param PickingOrderProduct $pickingOrderProduct
240     * @return void
241     */
242    private function markItemAsPicked(StockIOProduct $stockOutputProduct, User $user, PickingOrderProduct $pickingOrderProduct): void
243    {
244        $pickedAmountForTask = round((float) $pickingOrderProduct->pickedItems()->sum('amount'), 2);
245        $pickedAmountForRequestItem = round((float) $stockOutputProduct->pickedItems()->sum('amount'), 2);
246        $taskAmount = round((float) $pickingOrderProduct->getAttribute('amount'), 2);
247        $requestItemAmount = round((float) $stockOutputProduct->getAttribute('amount'), 2);
248
249        if ($pickedAmountForRequestItem >= $requestItemAmount) {
250            $stockOutputProduct->setAttribute('picked', true);
251            $stockOutputProduct->setAttribute('status', StockProductStatusEnum::SEPARATED);
252            $stockOutputProduct->save();
253        } else {
254            $stockOutputProduct->setAttribute('status', StockProductStatusEnum::SEPARATED_PARTIAL);
255            $stockOutputProduct->save();
256        }
257
258        if ($pickedAmountForTask >= $taskAmount) {
259            $this->setPickingOrderProductAsSeparated($pickingOrderProduct);
260        } else {
261            $this->setPickingOrderProductAsSeparatedPartial($pickingOrderProduct);
262        }
263
264        if ($stockOutputProduct->requestItem) {
265            $this->setStatusRequestItem(
266                $stockOutputProduct->requestItem,
267                $pickedAmountForRequestItem >= round((float) $stockOutputProduct->requestItem->getAttribute('amount'), 2)
268                    ? RequestItemStatusEnum::SEPARATED
269                    : RequestItemStatusEnum::SEPARATED_PARCIAL,
270                $user
271            );
272        }
273
274        if ($pickingOrderProduct->pickingOrder) {
275            $this->processStatusPickingOrder($pickingOrderProduct->pickingOrder);
276        }
277    }
278
279    /**
280     * @param RequestItem $requestItem
281     * @param RequestItemStatusEnum $status
282     * @param User $user
283     * @return void
284     */
285    private function setStatusRequestItem(RequestItem $requestItem, RequestItemStatusEnum $status, User $user): void
286    {
287        $requestItem->setAttribute('status', $status->value);
288        $requestItem->save();
289
290        (new ProcessStatusRequestService())->process($requestItem->request, $user);
291    }
292
293    /**
294     * @param PickingOrderProduct $pickingOrderProduct
295     * @return void
296     */
297    private function setPickingOrderProductAsSeparated(PickingOrderProduct $pickingOrderProduct): void
298    {
299        $pickingOrderProduct->setAttribute('status', PickingOrderStatusEnum::SEPARATED);
300        $pickingOrderProduct->save();
301    }
302
303    /**
304     * @param PickingOrderProduct $pickingOrderProduct
305     * @return void
306     */
307    private function setPickingOrderProductAsSeparatedPartial(PickingOrderProduct $pickingOrderProduct): void
308    {
309        $pickingOrderProduct->setAttribute('status', PickingOrderStatusEnum::SEPARATED_PARTIAL);
310        $pickingOrderProduct->save();
311    }
312
313    /**
314     * @param PickingOrder $pickingOrder
315     * @return void
316     */
317    private function processStatusPickingOrder(PickingOrder $pickingOrder): void
318    {
319        $statusCounts = $pickingOrder->pickingOrderProduct()
320            ->selectRaw('status, COUNT(*) as total')
321            ->groupBy('status')
322            ->pluck('total', 'status');
323
324        $get = fn($status) => $statusCounts[$status] ?? 0;
325
326        $countAll = $statusCounts
327            ->reject(fn($_, $status) => $status == PickingOrderStatusEnum::CANCELED->value)
328            ->sum();
329
330        $separated = $get(PickingOrderStatusEnum::SEPARATED->value);
331        $separatedPartial = $get(PickingOrderStatusEnum::SEPARATED_PARTIAL->value);
332
333        if ($countAll === 0) {
334            return;
335        }
336
337        if ($separated === $countAll) {
338            $pickingOrder->setAttribute('status', PickingOrderStatusEnum::SEPARATED->value);
339            $pickingOrder->save();
340
341            return;
342        }
343
344        if ($separatedPartial > 0 || $separated > 0) {
345            $pickingOrder->setAttribute('status', PickingOrderStatusEnum::SEPARATED_PARTIAL->value);
346            $pickingOrder->save();
347        }
348    }
349
350    /**
351     * @param StockIOProduct $stockIOProduct
352     * @param User $user
353     */
354    private function processRequestLog(
355        StockIOProduct $stockIOProduct,
356        User $user): void
357    {
358        $RequestLogStoreDTO = new RequestLogStoreDTO([
359            'request'     => $stockIOProduct->stockIO->request ? $stockIOProduct->stockIO->request : null,
360            'stockIO'     => $stockIOProduct->stockIO,
361            'operation'   => RequestLogOperationEnum::SEPARATED_REQUEST,
362            'user'        => $user,
363            'detail'      => $stockIOProduct->toJson(),
364            'observation' => "Item da requisição separado: ". $stockIOProduct->getKey(),
365        ]);
366
367        (new RequestLogStoreService())->store($RequestLogStoreDTO, $user);
368    }
369}