Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.00% covered (warning)
84.00%
189 / 225
45.00% covered (danger)
45.00%
9 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestDevolutionService
84.00% covered (warning)
84.00%
189 / 225
45.00% covered (danger)
45.00%
9 / 20
57.44
0.00% covered (danger)
0.00%
0 / 1
 devolution
92.68% covered (success)
92.68%
38 / 41
0.00% covered (danger)
0.00%
0 / 1
3.00
 processStatusRequestItem
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 validateRequestItem
12.50% covered (danger)
12.50%
1 / 8
0.00% covered (danger)
0.00%
0 / 1
9.03
 validateAmount
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getStockIOProductDeliveredAmount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStockIOProductReturnedAmount
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getAvailableAmountToDevolution
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 populateStockIODTO
88.89% covered (warning)
88.89%
24 / 27
0.00% covered (danger)
0.00%
0 / 1
4.02
 processStockInput
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 processStockIOProduct
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
2.09
 populateStockIOProductDTO
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 processItem
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 processPickedItem
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 verifyProductPriorityIsNull
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 getStockIOProductIsReturned
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCostCenterId
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 populateExportDTO
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 integrationRequestDevolutionDTO
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 resolveGeneratedRequestSequence
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
5.12
 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 Domain\Request\DTOs\ExportDevolutionRequestSeniorDTO;
6use Domain\Shared\Integrations\Senior\SeniorRequestDevolutionExport;
7use Throwable;
8
9use Carbon\Carbon;
10
11use Illuminate\Support\Facades\DB;
12use Illuminate\Database\Eloquent\Builder;
13
14use App\Models\User;
15use App\Events\ServiceProcessed;
16use App\Models\CostCenter\CostCenter;
17use App\Models\Item\Item;
18use App\Models\Product\Product;
19use App\Models\Request\RequestItemDevolution;
20use App\Models\StockIO\StockIO;
21use App\Models\StockIO\StockIOProduct;
22
23use Domain\Integration\DTOs\IntegrationItemWarehouseDTO;
24use Domain\Integration\DTOs\IntegrationRequestDevolutionDTO;
25use Domain\Integration\DTOs\IntegrationStockInputDevolutionProductDTO;
26use Domain\Integration\Services\IntegrationStoreService;
27use Domain\Item\DTOs\ItemWarehouseDTO;
28use Domain\Item\Services\ItemStoreService;
29use Domain\PickingOrder\DTOs\PickedItemDTO;
30use Domain\PickingOrder\Services\PickedItemStoreService;
31use Domain\Request\DTOs\RequestDevolutionDTO;
32use Domain\Request\DTOs\RequestLogStoreDTO;
33use Domain\Request\Enums\RequestItemStatusEnum;
34use Domain\Request\Enums\RequestLogOperationEnum;
35use Domain\Request\Support\OriginTransactionCodeResolver;
36use Domain\Request\Exceptions\RequestItemExcedeedAmountException;
37use Domain\Request\Exceptions\RequestItemNotCannotBeZeroException;
38use Domain\Request\Exceptions\RequestNotSeparatedException;
39use Domain\Shared\Enums\SectorEnum;
40use Domain\Shared\Integrations\Senior\Exceptions\IntegrationSoapSeniorException;
41use Domain\Shared\Integrations\Senior\SeniorRequestExport;
42use Domain\StockIO\DTOs\StockIODTO;
43use Domain\StockIO\DTOs\StockIOProductDTO;
44use Domain\StockIO\Enums\StockInputReasonEnum;
45use Domain\StockIO\Enums\StockStatusEnum;
46use Domain\StockIO\Enums\StockTypeEnum;
47use Domain\StockIO\Services\StockIOProductStoreService;
48use Domain\StockIO\Services\StockIOProductUpdateService;
49use Domain\StockIO\Services\StockIOStoreService;
50use Domain\StockIO\Services\StockIOUpdateService;
51
52readonly class RequestDevolutionService
53{
54    /**
55     * @param RequestDevolutionDTO $requestDevolutionDTO
56     * @param User $user
57     * @return void
58     * @throws Throwable
59     */
60    public function devolution(RequestDevolutionDTO $requestDevolutionDTO, User $user): void
61    {
62        $stockIOProducts = StockIOProduct::query()
63            ->whereIn('id', collect($requestDevolutionDTO->stock_io_products)->pluck('id'))
64            ->get()
65            ->keyBy('id');
66
67        foreach ($requestDevolutionDTO->stock_io_products as $stockIOProduct) {
68            $stockIOProductM = $stockIOProducts[$stockIOProduct['id']];
69            $origin = $stockIOProductM->product->family->origin;
70            $transactionCode = (new OriginTransactionCodeResolver())->forDevolution($origin);
71
72            $this->validateAmount($stockIOProductM, $stockIOProduct['amount']);
73            $this->validateRequestItem($stockIOProductM);
74
75            $requestDevolutionSenior = app(SeniorRequestDevolutionExport::class)->execute(
76                $this->populateExportDTO($stockIOProductM, $stockIOProduct['amount'], $transactionCode)
77            );
78
79            if (!$requestDevolutionSenior?->numEme) {
80                throw new IntegrationSoapSeniorException(
81                    __("Senior devolution export did not return a request number.")
82                );
83            }
84
85            DB::transaction(function () use ($requestDevolutionDTO, $user, $stockIOProductM, $stockIOProduct, $requestDevolutionSenior) {
86                $requestDevolution = new RequestItemDevolution();
87                $requestDevolution->fill($requestDevolutionDTO->toArray());
88                $requestDevolution->stockIOProduct()->associate($stockIOProductM);
89                $requestDevolution->setAttribute('amount', $stockIOProduct['amount']);
90                $requestDevolution->save();
91
92                $this->processStatusRequestItem($stockIOProductM, $stockIOProduct['amount'], $user);
93
94                $this->processStockInput(
95                    $stockIOProductM,
96                    $stockIOProduct['amount'],
97                    $user,
98                    $user
99                );
100
101                $integrationDTO = $this->integrationRequestDevolutionDTO((int) $requestDevolutionSenior->numEme);
102                (new IntegrationStoreService())->store($integrationDTO, $requestDevolution, $user);
103
104                $this->processRequestLog($stockIOProductM->stockIO, $user, $requestDevolution, $stockIOProductM);
105            });
106
107            (new SeniorDevolutionFulfillmentService())->fulfill(
108                (string) $requestDevolutionSenior->numEme,
109                $transactionCode,
110                (float) $stockIOProduct['amount'],
111                $this->resolveGeneratedRequestSequence($requestDevolutionSenior)
112            );
113        }
114
115        ServiceProcessed::dispatch(self::class, $user);
116    }
117
118    /**
119     * @param StockIOProduct $stockIOProduct
120     * @param float $amount
121     * @param User $user
122     * @return void
123     */
124    private function processStatusRequestItem(StockIOProduct $stockIOProduct, float $amount, User $user): void
125    {
126        if ($stockIOProduct->requestItem) {
127            $requestItem = $stockIOProduct->requestItem;
128
129            if ($requestItem->getAttribute('amount') == $amount) {
130                $requestItem->setAttribute('status', RequestItemStatusEnum::RETURNED);
131            } else {
132                $requestItem->setAttribute('status', RequestItemStatusEnum::RETURNED_PARCIAL);
133            }
134
135            $requestItem->save();
136
137            (new ProcessStatusRequestService())->process($requestItem->request, $user);
138        }
139    }
140
141    /**
142     * @param StockIOProduct $stockIOProduct
143     * @throws RequestNotSeparatedException
144     */
145    private function validateRequestItem(StockIOProduct $stockIOProduct): void
146    {
147        if ($this->getAvailableAmountToDevolution($stockIOProduct) <= 0) {
148            if ($this->getStockIOProductIsReturned($stockIOProduct)) {
149                throw new RequestNotSeparatedException(
150                    __('The request item is returned.')
151                );
152            }
153
154            throw new RequestNotSeparatedException(
155                __('The request item is not separated.')
156            );
157        }
158    }
159
160    /**
161     * @param StockIOProduct $stockIOProduct
162     * @param float $amount
163     * @throws RequestItemNotCannotBeZeroException|RequestItemExcedeedAmountException
164     */
165    private function validateAmount(StockIOProduct $stockIOProduct, float $amount): void
166    {
167        if ($amount == 0) {
168            throw new RequestItemNotCannotBeZeroException(
169                __('The devolution amount cannot be zero.')
170            );
171        }
172
173        $availableAmount = $this->getAvailableAmountToDevolution($stockIOProduct);
174
175        if ($availableAmount > 0 && round($amount, 2) > $availableAmount) {
176            throw new RequestItemExcedeedAmountException(
177                __('The devolution amount excedeed request item amount.')
178            );
179        }
180    }
181
182    private function getStockIOProductDeliveredAmount(StockIOProduct $stockIOProduct): float
183    {
184        return round((float) $stockIOProduct->requestDeliveries()->sum('amount'), 2);
185    }
186
187    private function getStockIOProductReturnedAmount(StockIOProduct $stockIOProduct): float
188    {
189        return round((float) RequestItemDevolution::query()
190            ->where('stock_io_product_id', $stockIOProduct->getKey())
191            ->sum('amount'), 2);
192    }
193
194    private function getAvailableAmountToDevolution(StockIOProduct $stockIOProduct): float
195    {
196        return max(
197            round($this->getStockIOProductDeliveredAmount($stockIOProduct) - $this->getStockIOProductReturnedAmount($stockIOProduct), 2),
198            0
199        );
200    }
201
202    /**
203     * @param StockIOProduct $stockIOProduct
204     * @param User $responsible
205     * @return StockIODTO
206     */
207    private function populateStockIODTO(StockIOProduct $stockIOProduct, User $responsible): StockIODTO
208    {
209        $request_id = $stockIOProduct->requestItem ?
210            $stockIOProduct->requestItem->request->getKey() :
211            null;
212
213        $costCenterId = $request_id ?
214            $stockIOProduct->requestItem->request->costCenter->getKey() :
215            $this->getCostCenterId($stockIOProduct->stockIO);
216
217        $resourceCenterId = $request_id ?
218            $stockIOProduct->requestItem->request->resourceCenter->getKey() :
219            null;
220        $requestNumber = $stockIOProduct->stockIO->integration?->parameters['request_number'] ?? null;
221
222        return new StockIODTO([
223            'detail'         => null,
224            'status'         => StockStatusEnum::PENDING->value,
225            'reason'         => StockInputReasonEnum::DEVOLUTION->value,
226            'urgent'         => false,
227            'responsible_id' => null,
228            'type'           => StockTypeEnum::INPUT->value,
229            'sector'         => SectorEnum::WAREHOUSE,
230            'integration'    => [
231                'responsible_id'     => $responsible->getKey(),
232                'request_id'         => $request_id,
233                'request_number'     => $requestNumber,
234                'stock_io_id'        => $stockIOProduct->stockIO->getKey(),
235                'cost_center_id'     => $costCenterId,
236                'resource_center_id' => $resourceCenterId
237            ],
238        ]);
239    }
240
241    /**
242     * @param StockIOProduct $stockIOProduct
243     * @param float $amount
244     * @param User $user
245     * @param User $responsible
246     * @throws Throwable
247     */
248    private function processStockInput(StockIOProduct $stockIOProduct, float $amount, User $user, User $responsible): void
249    {
250        $stockIODTO = $this->populateStockIODTO($stockIOProduct, $responsible);
251
252        /** @var StockIO|null $input */
253        $input = StockIO::query()
254            ->where('type', StockTypeEnum::INPUT->value)
255            ->where('reason', StockInputReasonEnum::DEVOLUTION->value)
256            ->whereHas('integration', function (Builder $builder) use ($stockIODTO) {
257                $builder->where('parameters->request_id', $stockIODTO->integrationDTO->request_id);
258            })
259            ->first();
260
261        $stockInput = $input ?
262            (new StockIOUpdateService())->update($stockIODTO, $input, $user) :
263            (new StockIOStoreService())->store($stockIODTO, $user);
264
265        $this->processStockIOProduct($stockInput, $stockIOProduct, $amount, $user);
266    }
267
268    /**
269     * @param StockIO $input
270     * @param StockIOProduct $stockIOProductD
271     * @param float $amount
272     * @param User $user
273     * @throws Throwable
274     */
275    private function processStockIOProduct(StockIO $input, StockIOProduct $stockIOProductD, float $amount, User $user): void
276    {
277        $stockIOProduct = StockIOProduct::query()
278            ->where('stock_io_id', $input->getKey())
279            ->whereHas('integration', function (Builder $builder) use ($stockIOProductD){
280                $builder->where('parameters->request_item_id', $stockIOProductD->getKey());
281            })
282            ->first();
283
284        if($stockIOProduct) {
285            (new StockIOProductUpdateService())->update(
286                $this->populateStockIOProductDTO($stockIOProductD, $stockIOProductD->getAttribute('amount') + $amount),
287                $stockIOProduct,
288                $user
289            );
290        } else {
291            $stockIOProduct = (new StockIOProductStoreService())->store(
292                $this->populateStockIOProductDTO($stockIOProductD, $amount),
293                $input,
294                $user
295            );
296
297            $this->processItem($stockIOProduct, $user);
298        }
299    }
300
301    /**
302     * @param StockIOProduct $stockIOProduct
303     * @param float $amount
304     * @return StockIOProductDTO
305     */
306    private function populateStockIOProductDTO(StockIOProduct $stockIOProduct, float $amount): StockIOProductDTO
307    {
308        $request_item_id = $stockIOProduct->requestItem ?
309            $stockIOProduct->requestItem->getKey() :
310            $stockIOProduct->getKey();
311
312        return new StockIOProductDTO([
313            'product' => $stockIOProduct->product,
314            'amount'  => $amount,
315            'status'  => $status ?? null,
316            'integration_dto' => new IntegrationStockInputDevolutionProductDTO([
317                'request_item_id' => $request_item_id,
318            ]),
319        ]);
320    }
321
322    /**
323     * @param StockIOProduct $stockIOProduct
324     * @param User $user
325     * @throws Throwable
326     */
327    private function processItem(StockIOProduct $stockIOProduct, User $user): void
328    {
329        if ($this->verifyProductPriorityIsNull($stockIOProduct->product)) {
330            $itemDTO = new ItemWarehouseDTO([
331                'target' => $stockIOProduct->product,
332                'amount' => $stockIOProduct->getAttribute('amount'),
333                'itemIntegrationDTO' => new IntegrationItemWarehouseDTO([
334                    'priority_field' => null,
335                    'priority_value' => null,
336                ]),
337            ]);
338
339            $item = (new ItemStoreService())->store($itemDTO, $stockIOProduct->stockIO, $user);
340
341            $this->processPickedItem($stockIOProduct, $item, $user);
342        }
343    }
344
345    /**
346     * @param StockIOProduct $stockIOProduct
347     * @param Item $item
348     * @param User $user
349     * @throws Throwable
350     */
351    private function processPickedItem(StockIOProduct $stockIOProduct, Item $item, User $user): void
352    {
353        $pickedItemDTO = new PickedItemDTO([
354            'stock_io_product' => $stockIOProduct,
355            'item' => $item,
356            'amount' => $stockIOProduct->getAttribute('amount'),
357        ]);
358
359        (new PickedItemStoreService())->store($pickedItemDTO, $user);
360    }
361
362    /**
363     * @param Product $product
364     * @return bool
365     */
366    private function verifyProductPriorityIsNull(Product $product): bool
367    {
368        $family = $product->family;
369        $origin = $product->family->origin;
370
371        $productPriorityNull = is_null($product->getAttribute('priority_field')) && is_null($product->getAttribute('priority_order'));
372        $familyPriorityNull = is_null($family->getAttribute('priority_field')) && is_null($family->getAttribute('priority_order'));
373        $originPriorityNull = is_null($origin->getAttribute('priority_field')) && is_null($origin->getAttribute('priority_order'));
374
375        return $productPriorityNull &&
376               $familyPriorityNull &&
377               $originPriorityNull;
378    }
379
380    /**
381     * @param StockIOProduct $stockIOProduct
382     * @return bool
383     */
384    private function getStockIOProductIsReturned(StockIOProduct $stockIOProduct): bool
385    {
386        $amountDelivered = $this->getStockIOProductDeliveredAmount($stockIOProduct);
387
388        if ($amountDelivered <= 0) {
389            return false;
390        }
391
392        return $this->getStockIOProductReturnedAmount($stockIOProduct) >= $amountDelivered;
393    }
394
395    /**
396     * @param StockIO $stockIO
397     * @return int
398     */
399    private function getCostCenterId(StockIO $stockIO): int
400    {
401        $costCenter = CostCenter::query()
402            ->join('integrations', function ($join) {
403                $join->on('integrations.integrable_type', '=', CostCenter::class)
404                    ->where('integrations.integrable_id', 'cost_centers.id');
405            })
406            ->where('integrations.parameters->cost_center_code', $stockIO->integration->parameters['cost_center'])
407            ->first('cost_centers.*');
408
409        return $costCenter->getKey();
410    }
411
412    /**
413     * @param StockIOProduct $stockIOProduct
414     * @param float $amount
415     * @return ExportDevolutionRequestSeniorDTO
416     */
417    private function populateExportDTO(
418        StockIOProduct $stockIOProduct,
419        float $amount,
420        string $transactionCode
421    ): ExportDevolutionRequestSeniorDTO
422    {
423        $items = [];
424
425        $ccuRes = $stockIOProduct->requestItem ?
426            $stockIOProduct->requestItem->request->costCenter->integration->getAttribute('parameters')['cost_center_code'] :
427            $stockIOProduct->stockIO->integration->getAttribute('parameters')['cost_center'];
428
429        $identificadorExt = $stockIOProduct->stockIO->getKey();
430
431        $items[] = [
432            'codPro' => $stockIOProduct->product->integration->getAttribute('parameters')['integration_product'],
433            'codDer' => $stockIOProduct->product->integration->getAttribute('parameters')['integration_derivation'],
434            'qtdEme' => $amount,
435            'datEme' => Carbon::now()->format('d/m/Y'),
436            'ccuRes' => $ccuRes,
437            'obsEme' => '',
438            'codTns' => $transactionCode,
439        ];
440
441        return new ExportDevolutionRequestSeniorDTO([
442            'identificadorSistema' => 'SOELX',
443            'codEmp'               => 1,
444            'codFil'               => 1,
445            'tipoProcessamento'    => 1,
446            'identificadorExterno' => $identificadorExt,
447            'itens'                => $items,
448            'tipoOperacao'         => 'D'
449        ]);
450    }
451
452    /**
453     * @param int $requestNumber
454     * @return IntegrationRequestDevolutionDTO
455     */
456    private function integrationRequestDevolutionDTO(int $requestNumber): IntegrationRequestDevolutionDTO
457    {
458        return new IntegrationRequestDevolutionDTO([
459            'request_number' => $requestNumber,
460        ]);
461    }
462
463    private function resolveGeneratedRequestSequence(object $requestDevolutionSenior): int
464    {
465        $itemReturn = $requestDevolutionSenior->itensRetorno
466            ?? $requestDevolutionSenior->itemRetorno
467            ?? null;
468
469        if (is_array($itemReturn)) {
470            $itemReturn = $itemReturn[0] ?? null;
471        }
472
473        foreach ([$requestDevolutionSenior, $itemReturn] as $source) {
474            if (!is_object($source)) {
475                continue;
476            }
477
478            $sequence = $source->seqEme ?? $source->seqeme ?? null;
479            if (!is_null($sequence)) {
480                return (int) $sequence;
481            }
482        }
483
484        return 1;
485    }
486
487    /**
488     * @param StockIO $stockIO
489     * @param User $user
490     * @param RequestItemDevolution $requestItemDevolution
491     * @param StockIOProduct $stockIOProduct
492     */
493    private function processRequestLog(
494        StockIO $stockIO,
495        User $user,
496        RequestItemDevolution $requestItemDevolution,
497        StockIOProduct $stockIOProduct)
498    {
499        $RequestLogStoreDTO = new RequestLogStoreDTO([
500            'request'     => $stockIO->request ? $stockIO->request : null,
501            'stockIO'     => $stockIO,
502            'operation'   => RequestLogOperationEnum::RETURNED_REQUEST,
503            'user'        => $user,
504            'detail'      => $requestItemDevolution->toJson(),
505            'observation' => 'Devolução do item: '. $stockIOProduct->getKey(),
506        ]);
507
508        (new RequestLogStoreService())->store($RequestLogStoreDTO, $user);
509    }
510}