Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.04% covered (success)
91.04%
254 / 279
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestDeliveryService
91.04% covered (success)
91.04%
254 / 279
40.00% covered (danger)
40.00%
6 / 15
50.73
0.00% covered (danger)
0.00%
0 / 1
 delivery
94.58% covered (success)
94.58%
157 / 166
0.00% covered (danger)
0.00%
0 / 1
16.04
 sanitizeReceiverBadge
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
 formatSeniorDecimalAmount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPurchaseSolicitationQuantity
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 resolveSeniorErrorMessage
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 makeDeliveryKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 processStatusRequestItem
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
7
 processStatusPickingOrder
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
5
 validatePickingOrderStockIO
46.15% covered (danger)
46.15%
6 / 13
0.00% covered (danger)
0.00%
0 / 1
4.41
 getSeparatedAmount
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getPickingOrderProductIsDelivered
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isPickingOrderDelivered
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 getDeliveredAmountForPickingOrderProduct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 makeSeniorMovementObservation
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 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\Events\ServiceProcessed;
6use App\Models\PickingOrder\PickingOrder;
7use App\Models\PickingOrder\PickingOrderProduct;
8use App\Models\Request\RequestDelivery;
9use App\Models\StockIO\StockIO;
10use App\Models\StockIO\StockIOProduct;
11use App\Models\User;
12use Carbon\Carbon;
13use Domain\PickingOrder\Enums\PickingOrderStatusEnum;
14use Domain\PickingOrder\Exception\PickingOrderException;
15use Domain\Request\DTOs\RequestDeliveryDTO;
16use Domain\Request\DTOs\RequestLogStoreDTO;
17use Domain\Request\Enums\RequestItemStatusEnum;
18use Domain\Request\Enums\RequestLogOperationEnum;
19use Domain\Request\Support\OriginTransactionCodeResolver;
20use Domain\Shared\Integrations\Senior\Exceptions\IntegrationSoapSeniorException;
21use Domain\Shared\Integrations\Senior\SeniorRequestDeliveryExport;
22use Domain\Shared\Support\DecimalNormalizer;
23use Domain\StockIO\Enums\StockProductStatusEnum;
24use Domain\StockIO\Services\ProcessStatusStockIOService;
25use Illuminate\Support\Facades\DB;
26use Illuminate\Support\Facades\Log;
27use Illuminate\Validation\ValidationException;
28use Throwable;
29
30readonly class RequestDeliveryService
31{
32    /**
33     * @throws Throwable
34     * @throws IntegrationSoapSeniorException
35     */
36    public function delivery(RequestDeliveryDTO $requestDeliveryDTO, User $user): void
37    {
38        $receiver = $requestDeliveryDTO->receiver;
39        $receiver[0]['badge'] = $this->sanitizeReceiverBadge($receiver[0]['badge'] ?? '');
40
41        try {
42            DB::beginTransaction();
43
44            $pickingOrders = PickingOrder::query()
45                ->whereIn('id', collect($requestDeliveryDTO->picking_orders)->pluck('id'))
46                ->with([
47                    'pickingOrderProduct.stockIOProduct.requestItem',
48                    'pickingOrderProduct.pickedItems',
49                ])
50                ->get()
51                ->keyBy('id');
52
53            $deliveriesToSenior = [];
54            $deliveriesToProcess = [];
55            $errors = [];
56            foreach ($requestDeliveryDTO->picking_orders as $pickingOrder) {
57                $pickingOrderM = $pickingOrders[$pickingOrder['id']];
58
59                if ($this->isPickingOrderDelivered($pickingOrderM)) {
60                    throw new PickingOrderException(
61                        __('Picking order is already delivered. Picking Order: :pickingOrder.',
62                            ['pickingOrder' => $pickingOrderM->getKey()])
63                    );
64                }
65
66                $this->validatePickingOrderStockIO($pickingOrderM, $pickingOrder['stock_ios']);
67
68                foreach ($pickingOrder['stock_ios'] as $stockIO) {
69                    $pickingOrderProducts = $pickingOrderM
70                        ->pickingOrderProduct
71                        ->filter(fn ($pop) => $pop->stockIOProduct->stock_io_id === $stockIO['id']
72                        );
73
74                    foreach ($pickingOrderProducts as $pickingOrderProduct) {
75                        $amount = $this->getSeparatedAmount($pickingOrderProduct);
76
77                        if (! $this->getPickingOrderProductIsDelivered($pickingOrderProduct) && $amount > 0) {
78                            $numEme = $pickingOrderProduct->stockIOProduct->stockIO->integration->getAttribute('parameters')['request_number'];
79                            $seqEme = $pickingOrderProduct->stockIOProduct->integration->getAttribute('parameters')['request_sequence'];
80                            $origin = $pickingOrderProduct->stockIOProduct->product->family->origin;
81                            $transactionCode = (new OriginTransactionCodeResolver)->forRequest($origin);
82                            $receiverBadge = $receiver[0]['badge'];
83
84                            $deliveryKey = $this->makeDeliveryKey($numEme, $seqEme);
85
86                            $deliveryToSenior = [
87                                'datMov' => Carbon::now()->format('d/m/Y'),
88                                'numeme' => $numEme,
89                                'seqeme' => $seqEme,
90                                'qtdAtd' => $this->formatSeniorDecimalAmount($amount),
91                                'codtns' => $transactionCode,
92                                'nroCra' => $receiverBadge,
93                                'obsMov' => $this->makeSeniorMovementObservation($requestDeliveryDTO),
94                            ];
95
96                            $purchaseSolicitationQuantity = $this->getPurchaseSolicitationQuantity(
97                                $pickingOrderProduct->stockIOProduct
98                            );
99
100                            if ($purchaseSolicitationQuantity > 0) {
101                                $deliveryToSenior['qtdCpr'] = $this->formatSeniorDecimalAmount(
102                                    $purchaseSolicitationQuantity
103                                );
104                            }
105
106                            $deliveriesToSenior[$deliveryKey] = $deliveryToSenior;
107
108                            $deliveriesToProcess[$deliveryKey] = [
109                                'picking_order' => $pickingOrderM,
110                                'store_keeper' => $requestDeliveryDTO->store_keeper,
111                                'receiver' => $receiver,
112                                'stock_io_product' => $pickingOrderProduct->stockIOProduct,
113                                'picking_order_product' => $pickingOrderProduct,
114                                'amount' => $amount,
115                                'stock_io' => StockIO::find($stockIO['id']),
116                            ];
117                        }
118                    }
119                }
120            }
121
122            DB::commit();
123        } catch (Throwable $throwable) {
124            DB::rollBack();
125            throw $throwable;
126        }
127
128        Log::channel('integration')->info('senior.request_delivery.started', [
129            'requests' => collect($deliveriesToSenior)
130                ->map(fn (array $delivery, string $deliveryKey) => [
131                    'delivery_key' => $deliveryKey,
132                    'numeme' => $delivery['numeme'] ?? null,
133                    'seqeme' => $delivery['seqeme'] ?? null,
134                    'qtdAtd' => $delivery['qtdAtd'] ?? null,
135                    'codtns' => $delivery['codtns'] ?? null,
136                    'nroCra' => $delivery['nroCra'] ?? null,
137                    'stock_io_id' => $deliveriesToProcess[$deliveryKey]['stock_io']?->getKey() ?? null,
138                    'stock_io_product_id' => $deliveriesToProcess[$deliveryKey]['stock_io_product']?->getKey() ?? null,
139                    'picking_order_id' => $deliveriesToProcess[$deliveryKey]['picking_order']?->getKey() ?? null,
140                ])
141                ->values()
142                ->all(),
143        ]);
144
145        $seniorPayload = [
146            'codEmp' => 1,
147            'codFil' => 1,
148            'requisicoes' => array_values($deliveriesToSenior),
149        ];
150
151        $seniorReturns = (new SeniorRequestDeliveryExport)->execute($seniorPayload);
152
153        Log::channel('integration')->info('senior.request_delivery.finished', [
154            'returns' => collect($seniorReturns)
155                ->map(function ($seniorReturn) use ($deliveriesToProcess) {
156                    $seniorReturn = (object) $seniorReturn;
157                    $deliveryKey = $this->makeDeliveryKey(
158                        $seniorReturn->numEme ?? null,
159                        $seniorReturn->seqEme ?? null
160                    );
161
162                    return [
163                        'delivery_key' => $deliveryKey,
164                        'matched' => isset($deliveriesToProcess[$deliveryKey]),
165                        'statusRegistro' => $seniorReturn->statusRegistro ?? null,
166                        'numEme' => $seniorReturn->numEme ?? null,
167                        'seqEme' => $seniorReturn->seqEme ?? null,
168                        'mensagemRegistro' => $seniorReturn->mensagemRegistro ?? null,
169                    ];
170                })
171                ->values()
172                ->all(),
173        ]);
174
175        $affectedStockIOs = collect();
176
177        foreach ($seniorReturns as $seniorReturn) {
178            $seniorReturn = (object) $seniorReturn;
179            if ((int) ($seniorReturn->statusRegistro ?? 0) === 1) {
180                $deliveryKey = $this->makeDeliveryKey(
181                    $seniorReturn->numEme ?? null,
182                    $seniorReturn->seqEme ?? null
183                );
184                if (! isset($deliveriesToProcess[$deliveryKey])) {
185                    $errors[] = $this->resolveSeniorErrorMessage($seniorReturn);
186
187                    continue;
188                }
189
190                $deliveryToProcess = $deliveriesToProcess[$deliveryKey];
191                $affectedStockIOs->put(
192                    $deliveryToProcess['stock_io']->getKey(),
193                    $deliveryToProcess['stock_io']
194                );
195
196                $requestDelivery = new RequestDelivery;
197                $requestDelivery->fill($requestDeliveryDTO->toArray());
198
199                if (! is_null($deliveryToProcess['store_keeper'])) {
200                    $requestDelivery->storeKeeper()->associate($deliveryToProcess['store_keeper']);
201                } else {
202                    $requestDelivery->storeKeeper()->associate($user);
203                }
204
205                $requestDelivery->pickingOrder()->associate($deliveryToProcess['picking_order']);
206                $requestDelivery->stockIOProduct()->associate($deliveryToProcess['stock_io_product']);
207                $requestDelivery->setAttribute('amount', $deliveryToProcess['amount']);
208                $requestDelivery->setAttribute('receiver', json_encode($deliveryToProcess['receiver']));
209
210                $requestDelivery->save();
211
212                $deliveryToProcess['stock_io_product']->refresh();
213
214                $this->processStatusRequestItem(
215                    $deliveryToProcess['stock_io_product'],
216                    $deliveryToProcess['amount'],
217                    $user,
218                    $deliveryToProcess['picking_order_product']
219                );
220
221                $this->processRequestLog(
222                    $deliveryToProcess['stock_io'],
223                    $user,
224                    $requestDelivery,
225                    $deliveryToProcess['stock_io_product']
226                );
227
228            } else {
229                $errors[] = $this->resolveSeniorErrorMessage($seniorReturn);
230            }
231        }
232
233        if (! empty($errors)) {
234            Log::channel('integration')->error('senior.request_delivery.failed', [
235                'payload' => $seniorPayload,
236                'returns' => collect($seniorReturns)
237                    ->map(fn ($seniorReturn) => (array) $seniorReturn)
238                    ->values()
239                    ->all(),
240                'errors' => $errors,
241            ]);
242        }
243
244        foreach ($requestDeliveryDTO->picking_orders as $pickingOrder) {
245            $pickingOrderM = $pickingOrders[$pickingOrder['id']];
246
247            $this->processStatusPickingOrder($pickingOrderM);
248        }
249
250        $affectedStockIOs->each(function (StockIO $stockIO) use ($user) {
251            (new ProcessStatusStockIOService)->process($stockIO->refresh(), $user);
252        });
253
254        if (! empty($errors)) {
255            throw ValidationException::withMessages($errors);
256        }
257
258        ServiceProcessed::dispatch(self::class, $user);
259    }
260
261    private function sanitizeReceiverBadge(string $receiverBadge): string
262    {
263        $sanitizedBadge = preg_replace('/\D+/', '', $receiverBadge) ?? '';
264
265        if ($sanitizedBadge === '') {
266            throw ValidationException::withMessages([
267                'receiver.0.badge' => __('The receiver badge is invalid.'),
268            ]);
269        }
270
271        return $sanitizedBadge;
272    }
273
274    private function formatSeniorDecimalAmount(float $amount): string
275    {
276        return number_format(round($amount, 2), 2, '.', '');
277    }
278
279    private function getPurchaseSolicitationQuantity(StockIOProduct $stockIOProduct): float
280    {
281        $quantity = $stockIOProduct->integration?->getAttribute('parameters')['purchase_solicitation_quantity'] ?? null;
282
283        return DecimalNormalizer::toFloat($quantity);
284    }
285
286    private function resolveSeniorErrorMessage(object $seniorReturn): string
287    {
288        $message = trim((string) ($seniorReturn->mensagemRegistro ?? ''));
289        if ($message !== '') {
290            return $message;
291        }
292
293        return __('Unable to fulfill request in Senior due to an invalid response payload.');
294    }
295
296    private function makeDeliveryKey($requestNumber, $requestSequence): string
297    {
298        return (string) $requestNumber.':'.(string) $requestSequence;
299    }
300
301    private function processStatusRequestItem(
302        StockIOProduct $stockIOProduct,
303        float $amount,
304        User $user,
305        ?PickingOrderProduct $pickingOrderProduct = null
306    ): void {
307        $referenceAmount = $stockIOProduct->requestItem
308            ? (float) $stockIOProduct->requestItem->getAttribute('amount')
309            : (float) $stockIOProduct->amount;
310        $deliveredAmount = (float) $stockIOProduct->requestDeliveries()->sum('amount');
311        $isDelivered = round($deliveredAmount, 2) >= round($referenceAmount, 2);
312        $status = $isDelivered ? StockProductStatusEnum::CONCLUDED : StockProductStatusEnum::DELIVERED_PARTIAL;
313        $stockIOProduct->setAttribute(
314            'status',
315            $status
316        );
317        $stockIOProduct->save();
318
319        $currentPickingOrderProduct = $pickingOrderProduct ?? $stockIOProduct->getRelationValue('pickingOrderProduct');
320
321        if ($currentPickingOrderProduct) {
322            $deliveredPickingOrderAmount = $this->getDeliveredAmountForPickingOrderProduct($currentPickingOrderProduct);
323            $currentPickingOrderProduct->setAttribute(
324                'status',
325                round($deliveredPickingOrderAmount, 2) >= round((float) $currentPickingOrderProduct->getAttribute('amount'), 2)
326                    ? PickingOrderStatusEnum::CONCLUDED
327                    : PickingOrderStatusEnum::DELIVERED_PARTIAL
328            );
329            $currentPickingOrderProduct->save();
330        }
331
332        (new ProcessStatusStockIOService)->process($stockIOProduct->stockIO, $user);
333
334        if ($stockIOProduct->requestItem) {
335            $requestItem = $stockIOProduct->requestItem;
336
337            if (round($deliveredAmount, 2) >= round((float) $requestItem->getAttribute('amount'), 2)) {
338                $requestItem->setAttribute('status', RequestItemStatusEnum::DELIVERED);
339            } else {
340                $requestItem->setAttribute('status', RequestItemStatusEnum::DELIVERED_PARCIAL);
341            }
342
343            $requestItem->save();
344
345            (new ProcessStatusRequestService)->process($requestItem->request, $user);
346        }
347    }
348
349    private function processStatusPickingOrder(PickingOrder $pickingOrder): void
350    {
351        $statusCounts = $pickingOrder->pickingOrderProduct()
352            ->selectRaw('status, COUNT(*) as total')
353            ->groupBy('status')
354            ->pluck('total', 'status');
355
356        $get = fn ($status) => $statusCounts[$status] ?? 0;
357
358        $countAll = $statusCounts
359            ->reject(fn ($_, $status) => $status == PickingOrderStatusEnum::CANCELED->value)
360            ->sum();
361
362        $delivered = $get(PickingOrderStatusEnum::CONCLUDED->value);
363        $deliveredPartial = $get(PickingOrderStatusEnum::DELIVERED_PARTIAL->value);
364
365        if ($countAll === 0) {
366            return;
367        }
368
369        if ($delivered === $countAll) {
370            $pickingOrder->setAttribute('status', PickingOrderStatusEnum::CONCLUDED->value);
371            $pickingOrder->setAttribute('end_date', Carbon::now());
372            $pickingOrder->save();
373
374            return;
375        }
376
377        if ($deliveredPartial > 0 || $delivered > 0) {
378            $pickingOrder->setAttribute('status', PickingOrderStatusEnum::DELIVERED_PARTIAL->value);
379            $pickingOrder->save();
380        }
381    }
382
383    /**
384     * @throws PickingOrderException
385     */
386    private function validatePickingOrderStockIO(PickingOrder $pickingOrder, array $stockIOs): void
387    {
388        foreach ($stockIOs as $stockIO) {
389            $exists = $pickingOrder
390                ->pickingOrderProduct
391                ->filter(fn ($pop) => $pop->stockIOProduct->stock_io_id === $stockIO['id']
392                );
393
394            if ($exists->isEmpty()) {
395                throw new PickingOrderException(
396                    __('The informed stockIO :stockIO not belong to the picking order :pickingOrder.',
397                        [
398                            'stockIO' => $stockIO['id'],
399                            'pickingOrder' => $pickingOrder->getKey(),
400                        ])
401                );
402            }
403        }
404    }
405
406    /**
407     * @param  StockIOProduct  $stockIOProduct
408     */
409    private function getSeparatedAmount(StockIOProduct|PickingOrderProduct $stockIOProductOrPickingOrderProduct): float
410    {
411        $pickingOrderProduct = $stockIOProductOrPickingOrderProduct instanceof PickingOrderProduct
412            ? $stockIOProductOrPickingOrderProduct
413            : $stockIOProductOrPickingOrderProduct->getRelationValue('pickingOrderProduct');
414
415        if (! $pickingOrderProduct) {
416            return 0;
417        }
418
419        $separatedAmount = (float) $pickingOrderProduct->pickedItems()->sum('amount');
420        $deliveredAmount = $this->getDeliveredAmountForPickingOrderProduct($pickingOrderProduct);
421
422        return max(round($separatedAmount - $deliveredAmount, 2), 0);
423    }
424
425    private function getPickingOrderProductIsDelivered(PickingOrderProduct $pickingOrderProduct): bool
426    {
427        return round($this->getDeliveredAmountForPickingOrderProduct($pickingOrderProduct), 2) >=
428            round((float) $pickingOrderProduct->getAttribute('amount'), 2);
429    }
430
431    private function isPickingOrderDelivered(PickingOrder $pickingOrder): bool
432    {
433        $activePickingOrderProducts = $pickingOrder->pickingOrderProduct
434            ->reject(fn (PickingOrderProduct $pickingOrderProduct) => $pickingOrderProduct->getAttribute('status') == PickingOrderStatusEnum::CANCELED
435            );
436
437        if ($activePickingOrderProducts->isEmpty()) {
438            return false;
439        }
440
441        return $activePickingOrderProducts->every(
442            fn (PickingOrderProduct $pickingOrderProduct) => $this->getPickingOrderProductIsDelivered($pickingOrderProduct)
443        );
444    }
445
446    private function getDeliveredAmountForPickingOrderProduct(PickingOrderProduct $pickingOrderProduct): float
447    {
448        return (float) RequestDelivery::query()
449            ->where('stock_io_product_id', $pickingOrderProduct->getAttribute('stock_io_product_id'))
450            ->where('picking_order_id', $pickingOrderProduct->getAttribute('picking_order_id'))
451            ->sum('amount');
452    }
453
454    private function makeSeniorMovementObservation(RequestDeliveryDTO $requestDeliveryDTO): string
455    {
456        $receiver = $requestDeliveryDTO->receiver[0] ?? [];
457        $badge = $this->sanitizeReceiverBadge($receiver['badge'] ?? '');
458        $name = trim((string) ($receiver['name'] ?? ''));
459
460        if ($name === '') {
461            return 'Operador:'.$badge;
462        }
463
464        return 'Operador:'.$badge.' - '.$name;
465    }
466
467    private function processRequestLog(
468        StockIO $stockIO,
469        User $user,
470        RequestDelivery $deliverie,
471        StockIOProduct $stockIOProduct): void
472    {
473        $RequestLogStoreDTO = new RequestLogStoreDTO([
474            'request' => $stockIO->request ? $stockIO->request : null,
475            'stockIO' => $stockIO,
476            'operation' => RequestLogOperationEnum::DELIVERIED_REQUEST,
477            'user' => $user,
478            'detail' => $deliverie->toJson(),
479            'observation' => 'Entrega do item: '.$stockIOProduct->getKey(),
480        ]);
481
482        (new RequestLogStoreService)->store($RequestLogStoreDTO, $user);
483    }
484}