Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.36% covered (success)
91.36%
148 / 162
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
TransferableTransferItemService
91.36% covered (success)
91.36%
148 / 162
63.64% covered (warning)
63.64%
7 / 11
37.88
0.00% covered (danger)
0.00%
0 / 1
 transferMany
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
3
 transferOne
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 transactionOne
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
3.03
 performItemTransfer
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
11
 transferItemToRecipient
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 getRecipientItem
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
1
 createRecipientItem
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 validate
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 performTransaction
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 processReadingLogs
20.00% covered (danger)
20.00%
1 / 5
0.00% covered (danger)
0.00%
0 / 1
12.19
 performItemRecalculation
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
8.12
1<?php
2
3namespace Domain\Transferable\Services;
4
5use Exception;
6use function __;
7
8use Illuminate\Database\Eloquent\Builder;
9use Illuminate\Support\Facades\DB;
10use Symfony\Component\HttpKernel\Exception\HttpException;
11
12use App\Models\User;
13use App\Models\AbstractModelDefault;
14use App\Models\Address\Address;
15use App\Models\Item\Item;
16use App\Models\Transaction\Transaction;
17use App\Models\StockIO\StockIO;
18use App\Events\ServiceProcessed;
19
20use Domain\Integration\DTOs\IntegrationItemDTO;
21use Domain\Integration\DTOs\IntegrationItemWarehouseDTO;
22use Domain\Integration\Services\IntegrationStoreService;
23use Domain\Item\Interfaces\HasItemsInterface;
24use Domain\ReadingLog\Services\ReadingLogStoreService;
25use Domain\StockIO\Enums\StockInputReasonEnum;
26use Domain\StockIO\Enums\StockStatusEnum;
27use Domain\Transaction\DTOs\TransactionDTO;
28use Domain\Transaction\Enums\TransactionTypeEnum;
29use Domain\Transaction\Services\TransactionStoreService;
30use Domain\Transferable\DTOs\TransferItemDTO;
31use Domain\Transferable\DTOs\TransferItemsDTO;
32use Domain\Transferable\Exceptions\CantRemoveItemException;
33use Domain\Transferable\Exceptions\CantStoreItemException;
34
35readonly class TransferableTransferItemService
36{
37    /**
38     * @param TransferItemsDTO $transferItemsDTO
39     * @param HasItemsInterface $sender
40     * @param HasItemsInterface $recipient
41     * @param User $user
42     * @return void
43     * @throws Exception
44     */
45    public function transferMany(
46        TransferItemsDTO $transferItemsDTO,
47        HasItemsInterface $sender,
48        HasItemsInterface $recipient,
49        User $user,
50        int $sector
51    ): void {
52        try {
53            DB::beginTransaction();
54            $this->validate($sender, $recipient);
55
56            /** @var TransferItemDTO $transferItemDTO */
57            foreach ($transferItemsDTO->items as $transferItemDTO) {
58                $this->performTransaction(
59                    $transferItemDTO,
60                    $sender,
61                    $recipient,
62                    $transferItemsDTO->transactionTypeEnum,
63                    $transferItemsDTO->reason,
64                    $user,
65                    $sector,
66                    $transferItemsDTO->observation
67                );
68
69                $this->performItemTransfer($transferItemDTO, $sender, $recipient, $user);
70            }
71
72            $this->processReadingLogs($transferItemsDTO->readingLogDTOs, $sender, $user);
73            DB::commit();
74        } catch (Exception $exception) {
75            DB::rollBack();
76            throw $exception;
77        }
78        ServiceProcessed::dispatch(self::class, $user);
79    }
80
81    /**
82     * @param TransferItemDTO $transferItemDTO
83     * @param HasItemsInterface $sender
84     * @param HasItemsInterface $recipient
85     * @param TransactionTypeEnum $transactionTypeEnum
86     * @param ?int $reason
87     * @param User $user
88     * @return Item
89     * @throws Exception
90     */
91    public function transferOne(
92        TransferItemDTO $transferItemDTO,
93        HasItemsInterface $sender,
94        HasItemsInterface $recipient,
95        TransactionTypeEnum $transactionTypeEnum,
96        ?int $reason,
97        User $user,
98        ?string $observation = null,
99        int $sector
100    ): Item {
101        try {
102            DB::beginTransaction();
103            $this->validate($sender, $recipient);
104
105            $this->performTransaction($transferItemDTO,
106                $sender,
107                $recipient,
108                $transactionTypeEnum,
109                $reason,
110                $user,
111                $sector,
112                $observation
113            );
114
115            $item = $this->performItemTransfer($transferItemDTO, $sender, $recipient, $user);
116
117            $this->processReadingLogs($transferItemDTO->readingLogDTOs, $sender, $user);
118
119            DB::commit();
120            ServiceProcessed::dispatch(self::class, $user);
121            return $item;
122        } catch (Exception $exception) {
123            DB::rollBack();
124            throw $exception;
125        }
126    }
127
128        /**
129     * @param TransferItemDTO $transferItemDTO
130     * @param HasItemsInterface $sender
131     * @param TransactionTypeEnum $transactionTypeEnum
132     * @param int $reason
133     * @param User $user
134     * @return void
135     * @throws Exception
136     */
137    public function transactionOne(
138        TransferItemDTO $transferItemDTO,
139        HasItemsInterface $sender,
140        TransactionTypeEnum $transactionTypeEnum,
141        int $reason,
142        User $user,
143        $sector,
144        ?string $observation = null,
145        $performRecalculation = true
146    ): void {
147        try {
148            DB::beginTransaction();
149            $this->validate($sender, $sender);
150
151            $this->performTransaction(
152                $transferItemDTO,
153                $sender,
154                $sender,
155                $transactionTypeEnum,
156                $reason,
157                $user,
158                $sector,
159                $observation
160            );
161
162            if ($performRecalculation)
163                $this->performItemRecalculation($transferItemDTO, $sender);
164
165            $this->processReadingLogs($transferItemDTO->readingLogDTOs, $sender, $user);
166
167            DB::commit();
168            ServiceProcessed::dispatch(self::class, $user);
169        } catch (Exception $exception) {
170            DB::rollBack();
171            throw $exception;
172        }
173    }
174
175    /**
176     * @param TransferItemDTO $transferItemDTO
177     * @param HasItemsInterface $sender
178     * @param HasItemsInterface $recipient
179     * @param User $user
180     * @return Item
181     */
182    protected function performItemTransfer(
183        TransferItemDTO $transferItemDTO,
184        HasItemsInterface $sender,
185        HasItemsInterface $recipient,
186        User $user
187    ): Item {
188        $transferItemDTO->item->decrementAmount($transferItemDTO->amount);
189
190        (new CheckItemCanTransferService())->check(
191            $sender,
192            $recipient,
193            $transferItemDTO->item
194        );
195
196        $item = $this->transferItemToRecipient($recipient, $transferItemDTO, $user);
197
198        if ($transferItemDTO->item->getAttribute('amount') == 0) {
199            if (($sender instanceof Address && $sender->isInput()) ||
200                ($sender instanceof StockIO &&
201                 $sender->isInput() &&
202                 $sender->getAttribute('reason') == StockInputReasonEnum::INVOICE->value)
203            ) {
204                $transferItemDTO->item->pickedItems()->update(['item_id' => null]);
205            } elseif ($sender instanceof Address) {
206                $transferItemDTO->item->pickedItems()->update(['item_id' => null]);
207            }
208
209            $transferItemDTO->item->integration->delete();
210            $transferItemDTO->item->delete();
211        } else {
212            $transferItemDTO->item->save();
213        }
214
215        if ($sender instanceof StockIO) {
216            if ($sender->isInput() && $sender->items()->doesntExist()) {
217                $sender->setAttribute('status', StockStatusEnum::CONCLUDED);
218                $sender->save();
219            }
220        }
221
222        return $item;
223    }
224
225    /**
226     * @param HasItemsInterface $recipient
227     * @param TransferItemDTO $transferItemDTO
228     * @param User $user
229     * @return Item
230     */
231    public function transferItemToRecipient(
232        HasItemsInterface $recipient,
233        TransferItemDTO $transferItemDTO,
234        User $user
235    ): Item {
236        /** @var ?Item $item */
237        $item = $this->getRecipientItem($recipient, $transferItemDTO);
238
239        if ($item) {
240            $item->incrementAmount($transferItemDTO->amount);
241            $item->save();
242        } else {
243            $item = $this->createRecipientItem($recipient, $transferItemDTO, $user);
244        }
245        return $item;
246    }
247
248    /**
249     * @param HasItemsInterface $recipient
250     * @param TransferItemDTO $itemToTransfer
251     * @return Item|null
252     */
253    protected function getRecipientItem(
254        HasItemsInterface $recipient,
255        TransferItemDTO $itemToTransfer
256    ): ?Item {
257        $integration = $itemToTransfer->item->integration?->getAttribute('parameters') ?? [];
258
259        /** @var Item|null $recipientItem */
260        $recipientItem = $recipient->items()
261            ->join('integrations as recipient_integrations', function ($join) {
262                $join->on('recipient_integrations.integrable_id', '=', 'items.id')
263                    ->where('recipient_integrations.integrable_type', '=', Item::class);
264            })
265            ->where('product_id', $itemToTransfer->item->product?->getKey())
266            ->Where('storage_id', $itemToTransfer->item->storage?->getKey())
267            ->when(isset($integration['production_order']), function (Builder $builder) use ($integration) {
268                $builder->whereRaw(
269                    "recipient_integrations.parameters->>'production_order' = ?",
270                    [$integration['production_order']]
271                );
272            }, function (Builder $builder) use ($integration) {
273                $builder->whereRaw(
274                    "recipient_integrations.parameters->>'priority_value' = ?",
275                    [$integration['priority_value'] ?? null]
276                )->whereRaw(
277                    "recipient_integrations.parameters->>'priority_field' = ?",
278                    [$integration['priority_field'] ?? null]
279                );
280            })
281            ->select('items.*')
282            ->orderBy('id')
283            ->first();
284
285        return $recipientItem;
286    }
287
288    /**
289     * @param HasItemsInterface $recipient
290     * @param TransferItemDTO $transferItemDTO
291     * @param User $user
292     * @return Item
293     */
294    private function createRecipientItem(
295        HasItemsInterface $recipient,
296        TransferItemDTO $transferItemDTO,
297        User $user
298    ): Item {
299        /** @var Item $itemRecipient */
300        $itemRecipient = $recipient->items()->newModelInstance();
301        $itemRecipient->product()->associate($transferItemDTO->item->product);
302        $itemRecipient->storage()->associate($transferItemDTO->item->storage);
303        $itemRecipient->incrementAmount($transferItemDTO->amount);
304        $recipient->items()->save($itemRecipient);
305
306        $integrationItemDTO = isset($transferItemDTO->item->integration->getAttribute('parameters')['production_order']) ?
307        new IntegrationItemDTO([
308            'production_order' => $transferItemDTO->item->integration->getAttribute('parameters')['production_order'],
309            'movement_date' => $transferItemDTO->item->integration->getAttribute('parameters')['movement_date'],
310        ]) :
311        new IntegrationItemWarehouseDTO ([
312            'priority_value' => $transferItemDTO->item->integration->getAttribute('parameters')['priority_value'],
313            'priority_field' => $transferItemDTO->item->integration->getAttribute('parameters')['priority_field'],
314        ]);
315
316        (new IntegrationStoreService())->store($integrationItemDTO, $itemRecipient, $user);
317
318        return $itemRecipient;
319    }
320
321    /**
322     * @param HasItemsInterface $sender
323     * @param HasItemsInterface $recipient
324     * @return void
325     */
326    private function validate(
327        HasItemsInterface $sender,
328        HasItemsInterface $recipient
329    ): void {
330        if (!$sender->canRemove()) {
331            throw new CantRemoveItemException(__('The sender cannot send items'));
332        }
333
334        if (!$recipient->canStore()) {
335            throw new CantStoreItemException(__('The recipient cannot store items'));
336        }
337    }
338
339    /**
340     * @param TransferItemDTO $transferItemDTO
341     * @param HasItemsInterface $sender
342     * @param HasItemsInterface $recipient
343     * @param TransactionTypeEnum $transactionTypeEnum
344     * @param ?int $reason
345     * @param User $user
346     * @param string|null $observation
347     * @param int $sector
348     * @return Transaction
349     */
350    private function performTransaction(
351        TransferItemDTO $transferItemDTO,
352        HasItemsInterface $sender,
353        HasItemsInterface $recipient,
354        TransactionTypeEnum $transactionTypeEnum,
355        ?int $reason,
356        User $user,
357        int $sector,
358        ?string $observation = ''
359    ): Transaction {
360        $transactionDTO = new TransactionDTO([
361            'item'        => $transferItemDTO->item->load(
362                ['product', 'storage', 'integration', 'product.integration', 'product.measure', 'storage.integration']
363            ),
364            'from'        => $sender,
365            'to'          => $recipient,
366            'amount'      => $transferItemDTO->amount,
367            'type'        => $transactionTypeEnum,
368            'reason'      => $reason,
369            'observation' => $observation,
370            'sector'      => $sector
371        ]);
372
373        return (new TransactionStoreService())->recordTransaction($transactionDTO, $user);
374    }
375
376
377    /**
378     * @param array $dtos
379     * @param AbstractModelDefault|HasItemsInterface $sender
380     * @param User $user
381     * @return void
382     */
383    private function processReadingLogs(
384        array $dtos,
385        AbstractModelDefault|HasItemsInterface $sender,
386        User $user
387    ): void {
388        if (count($dtos) > 0) {
389            foreach ($dtos as $readingLogDTO) {
390                if (!$sender instanceof AbstractModelDefault) {
391                    throw new HttpException(500, __('The sender must be an instance of AbstractModelDefault'));
392                }
393
394                (new ReadingLogStoreService())->store($readingLogDTO, $sender, $user);
395            }
396        }
397    }
398
399    /**
400     * @param TransferItemDTO $transferItemDTO
401     * @param HasItemsInterface $sender
402     * @return void
403     */
404    private function performItemRecalculation(
405        TransferItemDTO $transferItemDTO,
406        HasItemsInterface $sender
407    )  {
408        if ($transferItemDTO->amount > 0) {
409            $transferItemDTO->item->incrementAmount(abs($transferItemDTO->amount));
410        } else {
411            $transferItemDTO->item->decrementAmount(abs($transferItemDTO->amount));
412        }
413
414        if ($transferItemDTO->item->getAttribute('amount') == 0) {
415            if ($sender instanceof Address && $sender->isInput()) {
416                $transferItemDTO->item->pickedItems()->update(['item_id' => null]);
417            }
418
419            $transferItemDTO->item->integration->delete();
420            $transferItemDTO->item->pickedItems()->delete();
421            $transferItemDTO->item->delete();
422        } else {
423            $transferItemDTO->item->save();
424        }
425    }
426
427}