Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.34% covered (warning)
75.34%
55 / 73
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
UnlinkWithInvoiceItemService
75.34% covered (warning)
75.34%
55 / 73
50.00% covered (danger)
50.00%
4 / 8
22.86
0.00% covered (danger)
0.00%
0 / 1
 unlink
70.00% covered (warning)
70.00%
14 / 20
0.00% covered (danger)
0.00%
0 / 1
3.24
 processActivetyLog
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 updateStatusInvoiceItem
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 updateStatusPurchaseOrder
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 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%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 convertPurchaseOrderAmountToInvoiceUnit
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
7.46
 getConversion
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
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\StoreInvoiceActivityLogDTO;
15use Domain\Invoice\Enums\InvoiceActivetyLogOperationEnum;
16use Domain\Invoice\Enums\InvoiceItemStatusEnum;
17use Domain\Invoice\Enums\InvoicePurchaseOrderStatusEnum;
18use Domain\Invoice\Exceptions\PurchaseOrderInvoiceItemNotFoundException;
19use Domain\Invoice\Services\InvoiceActivityLogStoreService;
20use Domain\PurchaseOrder\DTOs\UnlinkPurchaseOrderItemInvoiceItemDTO;
21use Domain\PurchaseOrder\Enums\PurchaseOrderInvoiceItemStatusEnum;
22use Illuminate\Database\Eloquent\Model;
23use Illuminate\Support\Facades\DB;
24use Throwable;
25
26readonly class UnlinkWithInvoiceItemService
27{
28    /**
29     * @throws Throwable
30     */
31    public function unlink(UnlinkPurchaseOrderItemInvoiceItemDTO $unlinkDTO, User $user): void
32    {
33        try {
34            DB::beginTransaction();
35
36            $purchaseOrderInvoiceItem = PurchaseOrderInvoiceItem::query()
37                ->where('invoice_item_id', $unlinkDTO->invoice_item->getKey())
38                ->where('purchase_order_item_id', $unlinkDTO->purchase_order_item->getKey())
39                ->first();
40
41            if (! $purchaseOrderInvoiceItem) {
42                throw new PurchaseOrderInvoiceItemNotFoundException(
43                    __('Link of purchase order item and invoice item not found')
44                );
45            }
46
47            // Invoice to check later if it still has items
48            $invoice = $purchaseOrderInvoiceItem->invoiceItem?->invoice;
49
50            $purchaseOrderInvoiceItemOld = $purchaseOrderInvoiceItem->getAttributes();
51
52            $purchaseOrderInvoiceItem->delete();
53
54            $this->processActivetyLog($unlinkDTO->invoice_item->invoice, $user, $purchaseOrderInvoiceItemOld);
55            $this->updateStatusInvoiceItem($unlinkDTO->invoice_item);
56            $this->updateStatusPurchaseOrder($invoice);
57
58            DB::commit();
59        } catch (Throwable $exception) {
60            DB::rollBack();
61            throw $exception;
62        }
63
64        ServiceProcessed::dispatch(self::class, $user);
65    }
66
67    private function processActivetyLog(Invoice $invoice, User $user, array $details)
68    {
69        $invoiceActivityLogDTO = new StoreInvoiceActivityLogDTO([
70            'entity_type' => PurchaseOrderInvoiceItem::class,
71            'entity_id' => $details['id'],
72            'operation' => InvoiceActivetyLogOperationEnum::DELETE,
73            'user' => $user,
74            'detail' => json_encode($details),
75            'observation' => 'Removido o vĂ­nculo do item da nota fiscal, com o item da ordem de compra.',
76        ]);
77
78        (new InvoiceActivityLogStoreService)->store($invoiceActivityLogDTO, $invoice, $user);
79    }
80
81    private function updateStatusInvoiceItem(InvoiceItem $invoiceItem): void
82    {
83        $invoiceItem->setAttribute(
84            'status',
85            $this->invoiceItemIsFullyLinked($invoiceItem) ? InvoiceItemStatusEnum::VALIDATED : InvoiceItemStatusEnum::PENDING
86        );
87        $invoiceItem->save();
88    }
89
90    private function updateStatusPurchaseOrder(?Invoice $invoice): void
91    {
92        if (! $invoice) {
93            return;
94        }
95
96        $allItemsLinked = InvoiceItem::query()
97            ->where('invoice_id', $invoice->getKey())
98            ->get()
99            ->every(fn (Model $invoiceItem) => $invoiceItem instanceof InvoiceItem && $this->invoiceItemIsFullyLinked($invoiceItem));
100
101        $invoice->setAttribute(
102            'purchase_order_status',
103            $allItemsLinked ? InvoicePurchaseOrderStatusEnum::VALIDATED : InvoicePurchaseOrderStatusEnum::PENDING
104        );
105        $invoice->save();
106    }
107
108    private function invoiceItemIsFullyLinked(InvoiceItem $invoiceItem): bool
109    {
110        return round($this->linkedInvoiceAmount($invoiceItem), 2) >= round($invoiceItem->getAmount(), 2);
111    }
112
113    private function linkedInvoiceAmount(InvoiceItem $invoiceItem): float
114    {
115        return (float) PurchaseOrderInvoiceItem::query()
116            ->with('purchaseOrderItem.product.unitMeasurement')
117            ->where('invoice_item_id', $invoiceItem->getKey())
118            ->where('status', PurchaseOrderInvoiceItemStatusEnum::VALIDATED)
119            ->get()
120            ->sum(function (PurchaseOrderInvoiceItem $purchaseOrderInvoiceItem) use ($invoiceItem) {
121                return $this->convertPurchaseOrderAmountToInvoiceUnit(
122                    $invoiceItem,
123                    $purchaseOrderInvoiceItem->purchaseOrderItem,
124                    (float) $purchaseOrderInvoiceItem->getAttribute('amount')
125                ) ?? 0;
126            });
127    }
128
129    private function convertPurchaseOrderAmountToInvoiceUnit(
130        InvoiceItem $invoiceItem,
131        PurchaseOrderItem $purchaseOrderItem,
132        float $amount
133    ): ?float {
134        $invoiceItem->loadMissing('unitMeasurement');
135        $purchaseOrderItem->loadMissing('product.unitMeasurement');
136
137        if ($invoiceItem->unitMeasurement->getKey() === $purchaseOrderItem->product->unitMeasurement->getKey()) {
138            return $amount;
139        }
140
141        $conversion = $this->getConversion($invoiceItem->unitMeasurement, $purchaseOrderItem->product->unitMeasurement);
142
143        if (! $conversion) {
144            return null;
145        }
146
147        if ($conversion->getAttribute('type') == ConversionTypeEnum::MULTIPLY) {
148            return $amount / $conversion->getAttribute('value');
149        }
150
151        return $amount * $conversion->getAttribute('value');
152    }
153
154    private function getConversion(UnitMeasurement $unitMFrom, UnitMeasurement $unitMTo): ?Conversion
155    {
156        $conversion = Conversion::query()
157            ->where('unit_measurement_from_id', $unitMFrom->getKey())
158            ->where('unit_measurement_to_id', $unitMTo->getKey())
159            ->first();
160
161        return $conversion instanceof Conversion ? $conversion : null;
162    }
163}