| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 | <?php
namespace App\Concerns;
use App\Enums\Accounting\AdjustmentComputation;
use App\Enums\Accounting\DocumentDiscountMethod;
use App\Models\Accounting\Bill;
use App\Models\Accounting\DocumentLineItem;
use App\Utilities\Currency\CurrencyAccessor;
use App\Utilities\Currency\CurrencyConverter;
use App\Utilities\RateCalculator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
trait ManagesLineItems
{
    protected function handleLineItems(Model $record, Collection $lineItems): void
    {
        foreach ($lineItems as $itemData) {
            $lineItem = isset($itemData['id'])
                ? $record->lineItems->find($itemData['id'])
                : $record->lineItems()->make();
            $lineItem->fill([
                'offering_id' => $itemData['offering_id'],
                'description' => $itemData['description'],
                'quantity' => $itemData['quantity'],
                'unit_price' => $itemData['unit_price'],
            ]);
            if (! $lineItem->exists) {
                $lineItem->documentable()->associate($record);
            }
            $lineItem->save();
            $this->handleLineItemAdjustments($lineItem, $itemData, $record->discount_method);
            $this->updateLineItemTotals($lineItem, $record->discount_method);
        }
    }
    protected function deleteRemovedLineItems(Model $record, Collection $lineItems): void
    {
        $existingLineItemIds = $record->lineItems->pluck('id');
        $updatedLineItemIds = $lineItems->pluck('id')->filter();
        $lineItemsToDelete = $existingLineItemIds->diff($updatedLineItemIds);
        if ($lineItemsToDelete->isNotEmpty()) {
            $record
                ->lineItems()
                ->whereIn('id', $lineItemsToDelete)
                ->each(fn (DocumentLineItem $lineItem) => $lineItem->delete());
        }
    }
    protected function handleLineItemAdjustments(DocumentLineItem $lineItem, array $itemData, DocumentDiscountMethod $discountMethod): void
    {
        $isBill = $lineItem->documentable instanceof Bill;
        $taxType = $isBill ? 'purchaseTaxes' : 'salesTaxes';
        $discountType = $isBill ? 'purchaseDiscounts' : 'salesDiscounts';
        $adjustmentIds = collect($itemData[$taxType] ?? [])
            ->merge($discountMethod->isPerLineItem() ? ($itemData[$discountType] ?? []) : [])
            ->filter()
            ->unique();
        $lineItem->adjustments()->sync($adjustmentIds);
        $lineItem->refresh();
    }
    protected function updateLineItemTotals(DocumentLineItem $lineItem, DocumentDiscountMethod $discountMethod): void
    {
        $lineItem->updateQuietly([
            'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
            'discount_total' => $discountMethod->isPerLineItem()
                ? $lineItem->calculateDiscountTotal()->getAmount()
                : 0,
        ]);
    }
    protected function updateDocumentTotals(Model $record, array $data): array
    {
        $currencyCode = $data['currency_code'] ?? $record->currency_code ?? CurrencyAccessor::getDefaultCurrency();
        $subtotalCents = $record->lineItems()->sum('subtotal');
        $taxTotalCents = $record->lineItems()->sum('tax_total');
        $discountTotalCents = $this->calculateDiscountTotal(
            DocumentDiscountMethod::parse($data['discount_method']),
            AdjustmentComputation::parse($data['discount_computation']),
            $data['discount_rate'] ?? null,
            $subtotalCents,
            $record,
            $currencyCode,
        );
        $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
        return [
            'subtotal' => CurrencyConverter::convertCentsToFormatSimple($subtotalCents, $currencyCode),
            'tax_total' => CurrencyConverter::convertCentsToFormatSimple($taxTotalCents, $currencyCode),
            'discount_total' => CurrencyConverter::convertCentsToFormatSimple($discountTotalCents, $currencyCode),
            'total' => CurrencyConverter::convertCentsToFormatSimple($grandTotalCents, $currencyCode),
        ];
    }
    protected function calculateDiscountTotal(
        DocumentDiscountMethod $discountMethod,
        ?AdjustmentComputation $discountComputation,
        ?string $discountRate,
        int $subtotalCents,
        Model $record,
        string $currencyCode
    ): int {
        if ($discountMethod->isPerLineItem()) {
            return $record->lineItems()->sum('discount_total');
        }
        if ($discountComputation?->isPercentage()) {
            $scaledRate = RateCalculator::parseLocalizedRate($discountRate);
            return RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
        }
        return CurrencyConverter::convertToCents($discountRate, $currencyCode);
    }
}
 |