Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.38% covered (warning)
88.38%
175 / 198
53.85% covered (warning)
53.85%
7 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
LinkWithInvoiceItemService
88.38% covered (warning)
88.38%
175 / 198
53.85% covered (warning)
53.85%
7 / 13
42.51
0.00% covered (danger)
0.00%
0 / 1
 link
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
1 / 1
4
 validateAmountInvoiceItem
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
6.29
 validateAmountPurchaseOrderItem
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
3.04
 updateStatusPurchaseOrder
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 getPurchaseOrderInvoiceItem
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 updateStatusInvoiceItem
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 processActivetyLog
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getConversion
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 invoiceItemIsFullyLinked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 linkedInvoiceAmount
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 convertInvoiceAmountToPurchaseOrderUnit
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
10.75
 convertPurchaseOrderAmountToInvoiceUnit
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
10.75
 processSeniorDivisionIfNeeded
91.43% covered (success)
91.43%
32 / 35
0.00% covered (danger)
0.00%
0 / 1
8.04
1<?php
2
3namespace Domain\PurchaseOrder\Services;
4
5use App\Events\ServiceProcessed;
6use App\Models\Conversion\Conversion;
7use App\Models\Invoice\Invoice;
8use App\Models\Invoice\InvoiceItem;
9use App\Models\PurchaseOrder\PurchaseOrderInvoiceItem;
10use App\Models\PurchaseOrder\PurchaseOrderItem;
11use App\Models\UnitMeasurement\UnitMeasurement;
12use App\Models\User;
13use Domain\Conversion\Enums\ConversionTypeEnum;
14use Domain\Invoice\DTOs\SeniorProcessInvoiceItemDivisionDTO;
15use Domain\Invoice\DTOs\StoreInvoiceActivityLogDTO;
16use Domain\Invoice\Enums\InvoiceActivetyLogOperationEnum;
17use Domain\Invoice\Enums\InvoiceItemStatusEnum;
18use Domain\Invoice\Enums\InvoicePurchaseOrderStatusEnum;
19use Domain\Invoice\Exceptions\InvoiceItemAmountExceedException;
20use Domain\Invoice\Services\InvoiceActivityLogStoreService;
21use Domain\PurchaseOrder\DTOs\LinkPurchaseOrderItemInvoiceItemDTO;
22use Domain\PurchaseOrder\Enums\PurchaseOrderInvoiceItemStatusEnum;
23use Domain\Shared\Integrations\Senior\SeniorProcessInvoiceItemDivision;
24use Illuminate\Database\Eloquent\Model;
25use Illuminate\Support\Facades\DB;
26use Throwable;
27
28readonly class LinkWithInvoiceItemService
29{
30    /**
31     * @throws Throwable
32     */
33    public function link(LinkPurchaseOrderItemInvoiceItemDTO $linkDTO, Invoice $invoice, User $user): void
34    {
35        try {
36            DB::beginTransaction();
37
38            $purchaseOrderInvoiceItem = $linkDTO->force_create
39                ? null
40                : $this->getPurchaseOrderInvoiceItem($linkDTO->invoice_item, $linkDTO->purchase_order_item);
41            $this->validateAmountInvoiceItem(
42                $linkDTO->invoice_item,
43                $linkDTO->purchase_order_item,
44                (float) $linkDTO->amount,
45                $purchaseOrderInvoiceItem
46            );
47            $this->validateAmountPurchaseOrderItem(
48                $linkDTO->purchase_order_item,
49                (float) $linkDTO->amount,
50                $purchaseOrderInvoiceItem
51            );
52
53            if ($purchaseOrderInvoiceItem) {
54                $details = $purchaseOrderInvoiceItem->getAttributes();
55
56                $purchaseOrderInvoiceItem->setAttribute('amount', $linkDTO->amount);
57                $purchaseOrderInvoiceItem->setAttribute('status', PurchaseOrderInvoiceItemStatusEnum::VALIDATED);
58                $purchaseOrderInvoiceItem->save();
59
60                $this->processActivetyLog(
61                    $invoice,
62                    PurchaseOrderInvoiceItem::class,
63                    $purchaseOrderInvoiceItem->getKey(),
64                    InvoiceActivetyLogOperationEnum::UPDATE,
65                    $user,
66                    $details,
67                    'Atualizado vínculo do item da nota fiscal com o item da ordem de compra.'
68                );
69            } else {
70                $purchaseOrderInvoiceItem = (new PurchaseOrderInvoiceItem);
71                $purchaseOrderInvoiceItem->fill($linkDTO->toArray());
72                $purchaseOrderInvoiceItem->invoiceItem()->associate($linkDTO->invoice_item);
73                $purchaseOrderInvoiceItem->purchaseOrderItem()->associate($linkDTO->purchase_order_item);
74                $purchaseOrderInvoiceItem->setAttribute('status', PurchaseOrderInvoiceItemStatusEnum::VALIDATED);
75                $purchaseOrderInvoiceItem->save();
76
77                $this->processActivetyLog(
78                    $invoice,
79                    PurchaseOrderInvoiceItem::class,
80                    $purchaseOrderInvoiceItem->getKey(),
81                    InvoiceActivetyLogOperationEnum::INSERT,
82                    $user,
83                    $purchaseOrderInvoiceItem->getAttributes(),
84                    'Realizado vínculo do item da nota fiscal com o item da ordem de compra.'
85                );
86            }
87
88            $this->updateStatusInvoiceItem($linkDTO->invoice_item, $user);
89            $this->updateStatusPurchaseOrder($invoice, $user);
90
91            DB::commit();
92        } catch (Throwable $exception) {
93            DB::rollBack();
94            throw $exception;
95        }
96
97        $this->processSeniorDivisionIfNeeded($linkDTO->invoice_item);
98        ServiceProcessed::dispatch(self::class, $user);
99    }
100
101    /**
102     * @throws InvoiceItemAmountExceedException
103     */
104    private function validateAmountInvoiceItem(
105        InvoiceItem $invoiceItem,
106        PurchaseOrderItem $purchaseOrderItem,
107        float $amount,
108        ?PurchaseOrderInvoiceItem $existingPurchaseOrderInvoiceItem = null
109    ): void
110    {
111        if ($amount <= 0) {
112            throw new InvoiceItemAmountExceedException(__('The informed amount must be greater than zero'));
113        }
114
115        $amountItem = $this->convertInvoiceAmountToPurchaseOrderUnit($invoiceItem, $purchaseOrderItem);
116
117        if ($amountItem != 0 && $amountItem < $amount) {
118            throw new InvoiceItemAmountExceedException(__('The informed amount exceeds the amount of invoice item'));
119        }
120
121        $amountInInvoiceUnit = $this->convertPurchaseOrderAmountToInvoiceUnit($invoiceItem, $purchaseOrderItem, $amount);
122
123        if ($amountInInvoiceUnit === null) {
124            return;
125        }
126
127        $linkedAmount = $this->linkedInvoiceAmount(
128            $invoiceItem,
129            $existingPurchaseOrderInvoiceItem?->getAttribute('purchase_order_item_id')
130        );
131        $invoiceAmount = round($invoiceItem->getAmount(), 2);
132
133        if (round($linkedAmount + $amountInInvoiceUnit, 2) > $invoiceAmount) {
134            throw new InvoiceItemAmountExceedException(__('The informed amount exceeds the amount of invoice item'));
135        }
136    }
137
138    /**
139     * @throws InvoiceItemAmountExceedException
140     */
141    private function validateAmountPurchaseOrderItem(
142        PurchaseOrderItem $purchaseOrderItem,
143        float $amount,
144        ?PurchaseOrderInvoiceItem $existingPurchaseOrderInvoiceItem = null
145    ): void
146    {
147        if ($amount <= 0) {
148            throw new InvoiceItemAmountExceedException(__('The informed amount must be greater than zero'));
149        }
150
151        $linkedAmount = (float) PurchaseOrderInvoiceItem::query()
152            ->where('purchase_order_item_id', $purchaseOrderItem->getKey())
153            ->where('status', '<>', PurchaseOrderInvoiceItemStatusEnum::AUTOMATIC)
154            ->when($existingPurchaseOrderInvoiceItem !== null, function ($query) use ($existingPurchaseOrderInvoiceItem) {
155                $query->where('id', '<>', $existingPurchaseOrderInvoiceItem->getKey());
156            })
157            ->sum('amount');
158
159        $remainingBalance = (float) $purchaseOrderItem->getAttribute('balance') - $linkedAmount;
160
161        if ($remainingBalance < $amount) {
162            throw new InvoiceItemAmountExceedException(__('The informed amount exceeds the amount of purchase order item'));
163        }
164    }
165
166    private function updateStatusPurchaseOrder(Invoice $invoice, User $user): void
167    {
168        $allItemsLinked = InvoiceItem::query()
169            ->where('invoice_id', $invoice->getKey())
170            ->get()
171            ->every(fn (Model $invoiceItem) => $invoiceItem instanceof InvoiceItem && $this->invoiceItemIsFullyLinked($invoiceItem));
172
173        if ($allItemsLinked) {
174            $details = $invoice->getAttributes();
175
176            $invoice->setAttribute('purchase_order_status', InvoicePurchaseOrderStatusEnum::VALIDATED);
177            $invoice->save();
178
179            $this->processActivetyLog(
180                $invoice,
181                InvoiceItem::class,
182                $invoice->getKey(),
183                InvoiceActivetyLogOperationEnum::UPDATE,
184                $user,
185                $details,
186                'Atualizado situação da ordem de compra na nota fiscal, ao vincular todos itens da nota.'
187            );
188        } else {
189            $invoice->setAttribute('purchase_order_status', InvoicePurchaseOrderStatusEnum::PENDING);
190            $invoice->save();
191        }
192    }
193
194    private function getPurchaseOrderInvoiceItem(InvoiceItem $invoiceItem, PurchaseOrderItem $purchaseOrderItem): ?PurchaseOrderInvoiceItem
195    {
196        $purchaseOrderInvoiceItem = PurchaseOrderInvoiceItem::query()
197            ->where('invoice_item_id', $invoiceItem->getKey())
198            ->where('purchase_order_item_id', $purchaseOrderItem->getKey())
199            ->orderBy('id')
200            ->first();
201
202        return $purchaseOrderInvoiceItem instanceof PurchaseOrderInvoiceItem ? $purchaseOrderInvoiceItem : null;
203    }
204
205    private function updateStatusInvoiceItem(InvoiceItem $invoiceItem, User $user): void
206    {
207        $details = $invoiceItem->getAttributes();
208        $status = $this->invoiceItemIsFullyLinked($invoiceItem)
209            ? InvoiceItemStatusEnum::VALIDATED
210            : InvoiceItemStatusEnum::PENDING;
211
212        $invoiceItem->setAttribute('status', $status);
213        $invoiceItem->save();
214
215        $this->processActivetyLog(
216            $invoiceItem->invoice,
217            InvoiceItem::class,
218            $invoiceItem->getKey(),
219            InvoiceActivetyLogOperationEnum::UPDATE,
220            $user,
221            $details,
222            'Atualizado situação do item da nota fiscal ao confirmar vínculo com item da ordem de compra.'
223        );
224    }
225
226    private function processActivetyLog(
227        Invoice $invoice,
228        string $entityType,
229        int $entityId,
230        InvoiceActivetyLogOperationEnum $operation,
231        User $user,
232        array $details,
233        string $observation
234    ): void {
235        $invoiceActivityLogDTO = new StoreInvoiceActivityLogDTO([
236            'entity_type' => $entityType,
237            'entity_id' => $entityId,
238            'operation' => $operation,
239            'user' => $user,
240            'detail' => json_encode($details),
241            'observation' => $observation,
242        ]);
243
244        (new InvoiceActivityLogStoreService)->store($invoiceActivityLogDTO, $invoice, $user);
245    }
246
247    private function getConversion(UnitMeasurement $unitMFrom, UnitMeasurement $unitMTo): ?Conversion
248    {
249        return Conversion::where('unit_measurement_from_id', $unitMFrom->getKey())
250            ->where('unit_measurement_to_id', $unitMTo->getKey())
251            ->first();
252    }
253
254    private function invoiceItemIsFullyLinked(InvoiceItem $invoiceItem): bool
255    {
256        return round($this->linkedInvoiceAmount($invoiceItem), 2) >= round($invoiceItem->getAmount(), 2);
257    }
258
259    private function linkedInvoiceAmount(InvoiceItem $invoiceItem, ?int $excludedPurchaseOrderItemId = null): float
260    {
261        return (float) PurchaseOrderInvoiceItem::query()
262            ->with('purchaseOrderItem.product.unitMeasurement')
263            ->where('invoice_item_id', $invoiceItem->getKey())
264            ->where('status', PurchaseOrderInvoiceItemStatusEnum::VALIDATED)
265            ->when($excludedPurchaseOrderItemId !== null, function ($query) use ($excludedPurchaseOrderItemId) {
266                $query->where('purchase_order_item_id', '<>', $excludedPurchaseOrderItemId);
267            })
268            ->get()
269            ->sum(function (PurchaseOrderInvoiceItem $purchaseOrderInvoiceItem) use ($invoiceItem) {
270                return $this->convertPurchaseOrderAmountToInvoiceUnit(
271                    $invoiceItem,
272                    $purchaseOrderInvoiceItem->purchaseOrderItem,
273                    (float) $purchaseOrderInvoiceItem->getAttribute('amount')
274                ) ?? 0;
275            });
276    }
277
278    private function convertInvoiceAmountToPurchaseOrderUnit(InvoiceItem $invoiceItem, PurchaseOrderItem $purchaseOrderItem): float
279    {
280        if ($invoiceItem->unitMeasurement->getKey() === $purchaseOrderItem->product->unitMeasurement->getKey()) {
281            return $invoiceItem->getAmount();
282        }
283
284        $conversion = $this->getConversion($invoiceItem->unitMeasurement, $purchaseOrderItem->product->unitMeasurement);
285
286        if (! $conversion) {
287            return 0;
288        }
289
290        if ($conversion->getAttribute('type') == ConversionTypeEnum::MULTIPLY) {
291            return $invoiceItem->getAmount() * $conversion->getAttribute('value');
292        }
293
294        return $invoiceItem->getAmount() / $conversion->getAttribute('value');
295    }
296
297    private function convertPurchaseOrderAmountToInvoiceUnit(
298        InvoiceItem $invoiceItem,
299        PurchaseOrderItem $purchaseOrderItem,
300        float $amount
301    ): ?float {
302        if ($invoiceItem->unitMeasurement->getKey() === $purchaseOrderItem->product->unitMeasurement->getKey()) {
303            return $amount;
304        }
305
306        $conversion = $this->getConversion($invoiceItem->unitMeasurement, $purchaseOrderItem->product->unitMeasurement);
307
308        if (! $conversion) {
309            return null;
310        }
311
312        if ($conversion->getAttribute('type') == ConversionTypeEnum::MULTIPLY) {
313            return $amount / $conversion->getAttribute('value');
314        }
315
316        return $amount * $conversion->getAttribute('value');
317    }
318
319    private function processSeniorDivisionIfNeeded(InvoiceItem $invoiceItem): void
320    {
321        if (! $this->invoiceItemIsFullyLinked($invoiceItem)) {
322            return;
323        }
324
325        $purchaseOrderInvoiceItems = PurchaseOrderInvoiceItem::query()
326            ->where('invoice_item_id', $invoiceItem->getKey())
327            ->where('status', PurchaseOrderInvoiceItemStatusEnum::VALIDATED)
328            ->orderBy('id')
329            ->get();
330
331        if ($purchaseOrderInvoiceItems->count() < 2) {
332            return;
333        }
334
335        $invoice = $invoiceItem->invoice()->with('integration')->first();
336        $invoiceItem->loadMissing('integration');
337
338        $invoiceParameters = $invoice?->integration?->parameters ?? [];
339        $invoiceItemParameters = $invoiceItem->integration?->parameters ?? [];
340
341        $codEmp = $invoiceParameters['codEmp'] ?? $invoiceParameters['CODEMP'] ?? null;
342        $codFil = $invoiceParameters['codFil'] ?? $invoiceParameters['CODFIL'] ?? null;
343        $chvNel = $invoiceParameters['chvNel'] ?? $invoiceParameters['CHVNEL'] ?? $invoice?->getAttribute('key');
344        $seqIpc = $invoiceItemParameters['seqIpc'] ?? $invoiceItemParameters['SEQIPC'] ?? null;
345
346        if ($codEmp === null || $codFil === null || $chvNel === null || $seqIpc === null) {
347            return;
348        }
349
350        $divisionItems = $purchaseOrderInvoiceItems
351            ->slice(1)
352            ->map(fn (PurchaseOrderInvoiceItem $item) => ['qtdIte' => (float) $item->getAttribute('amount')])
353            ->values()
354            ->all();
355
356        if (count($divisionItems) === 0) {
357            return;
358        }
359
360        app(SeniorProcessInvoiceItemDivision::class)->execute(
361            new SeniorProcessInvoiceItemDivisionDTO([
362                'codEmp' => $codEmp,
363                'codFil' => $codFil,
364                'chvNel' => $chvNel,
365                'seqIpc' => $seqIpc,
366                'itens' => $divisionItems,
367            ])
368        );
369    }
370}