| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 | <?php
namespace App\Observers;
use App\Enums\Accounting\BillStatus;
use App\Enums\Accounting\InvoiceStatus;
use App\Models\Accounting\Bill;
use App\Models\Accounting\Invoice;
use App\Models\Accounting\Transaction;
use App\Models\Common\Client;
use App\Models\Common\Vendor;
use App\Services\TransactionService;
use App\Utilities\Currency\CurrencyConverter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
class TransactionObserver
{
    public function __construct(
        protected TransactionService $transactionService,
    ) {}
    /**
     * Handle the Transaction "saving" event.
     */
    public function saving(Transaction $transaction): void
    {
        if ($transaction->type->isTransfer() && $transaction->description === null) {
            $transaction->description = 'Account Transfer';
        }
        if ($transaction->transactionable && ! $transaction->payeeable_id) {
            $document = $transaction->transactionable;
            if ($document instanceof Invoice) {
                $transaction->payeeable_id = $document->client_id;
                $transaction->payeeable_type = Client::class;
            } elseif ($document instanceof Bill) {
                $transaction->payeeable_id = $document->vendor_id;
                $transaction->payeeable_type = Vendor::class;
            }
        }
    }
    /**
     * Handle the Transaction "created" event.
     */
    public function created(Transaction $transaction): void
    {
        $this->transactionService->createJournalEntries($transaction);
        if (! $transaction->transactionable) {
            return;
        }
        $document = $transaction->transactionable;
        if ($document instanceof Invoice) {
            $this->updateInvoiceTotals($document);
        } elseif ($document instanceof Bill) {
            $this->updateBillTotals($document);
        }
    }
    /**
     * Handle the Transaction "updated" event.
     */
    public function updated(Transaction $transaction): void
    {
        $transaction->refresh(); // DO NOT REMOVE
        $this->transactionService->updateJournalEntries($transaction);
        if (! $transaction->transactionable) {
            return;
        }
        $document = $transaction->transactionable;
        if ($document instanceof Invoice) {
            $this->updateInvoiceTotals($document);
        } elseif ($document instanceof Bill) {
            $this->updateBillTotals($document);
        }
    }
    /**
     * Handle the Transaction "deleting" event.
     */
    public function deleting(Transaction $transaction): void
    {
        DB::transaction(function () use ($transaction) {
            $this->transactionService->deleteJournalEntries($transaction);
            if (! $transaction->transactionable) {
                return;
            }
            $document = $transaction->transactionable;
            if (($document instanceof Invoice || $document instanceof Bill) && ! $document->exists) {
                return;
            }
            if ($document instanceof Invoice) {
                $this->updateInvoiceTotals($document, $transaction);
            } elseif ($document instanceof Bill) {
                $this->updateBillTotals($document, $transaction);
            }
        });
    }
    public function deleted(Transaction $transaction): void
    {
        //
    }
    protected function updateInvoiceTotals(Invoice $invoice, ?Transaction $excludedTransaction = null): void
    {
        if (! $invoice->hasPayments()) {
            return;
        }
        $invoiceCurrency = $invoice->currency_code;
        $depositTotalInInvoiceCurrencyCents = (int) $invoice->deposits()
            ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
            ->get()
            ->sum(function (Transaction $transaction) use ($invoiceCurrency) {
                // If the transaction has stored the original invoice amount in metadata, use that
                if (! empty($transaction->meta) &&
                    isset($transaction->meta['original_document_currency']) &&
                    $transaction->meta['original_document_currency'] === $invoiceCurrency &&
                    isset($transaction->meta['amount_in_document_currency_cents'])) {
                    return (int) $transaction->meta['amount_in_document_currency_cents'];
                }
                // Fall back to conversion if metadata is not available
                $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
                $amountCents = (int) $transaction->amount;
                return CurrencyConverter::convertBalance($amountCents, $bankAccountCurrency, $invoiceCurrency);
            });
        $withdrawalTotalInInvoiceCurrencyCents = (int) $invoice->withdrawals()
            ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
            ->get()
            ->sum(function (Transaction $transaction) use ($invoiceCurrency) {
                // If the transaction has stored the original invoice amount in metadata, use that
                if (! empty($transaction->meta) &&
                    isset($transaction->meta['original_document_currency']) &&
                    $transaction->meta['original_document_currency'] === $invoiceCurrency &&
                    isset($transaction->meta['amount_in_document_currency_cents'])) {
                    return (int) $transaction->meta['amount_in_document_currency_cents'];
                }
                // Fall back to conversion if metadata is not available
                $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
                $amountCents = (int) $transaction->amount;
                return CurrencyConverter::convertBalance($amountCents, $bankAccountCurrency, $invoiceCurrency);
            });
        $totalPaidInInvoiceCurrencyCents = $depositTotalInInvoiceCurrencyCents - $withdrawalTotalInInvoiceCurrencyCents;
        $invoiceTotalInInvoiceCurrencyCents = (int) $invoice->total;
        $newStatus = match (true) {
            $totalPaidInInvoiceCurrencyCents > $invoiceTotalInInvoiceCurrencyCents => InvoiceStatus::Overpaid,
            $totalPaidInInvoiceCurrencyCents === $invoiceTotalInInvoiceCurrencyCents => InvoiceStatus::Paid,
            $totalPaidInInvoiceCurrencyCents === 0 => $invoice->last_sent_at ? InvoiceStatus::Sent : InvoiceStatus::Unsent,
            default => InvoiceStatus::Partial,
        };
        $paidAt = $invoice->paid_at;
        if (in_array($newStatus, [InvoiceStatus::Paid, InvoiceStatus::Overpaid]) && ! $paidAt) {
            $paidAt = $invoice->deposits()
                ->latest('posted_at')
                ->value('posted_at');
        }
        $invoice->update([
            'amount_paid' => $totalPaidInInvoiceCurrencyCents,
            'status' => $newStatus,
            'paid_at' => $paidAt,
        ]);
    }
    protected function updateBillTotals(Bill $bill, ?Transaction $excludedTransaction = null): void
    {
        if (! $bill->hasPayments()) {
            return;
        }
        $billCurrency = $bill->currency_code;
        $withdrawalTotalInBillCurrencyCents = (int) $bill->withdrawals()
            ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
            ->get()
            ->sum(function (Transaction $transaction) use ($billCurrency) {
                // If the transaction has stored the original bill amount in metadata, use that
                if (! empty($transaction->meta) &&
                    isset($transaction->meta['original_document_currency']) &&
                    $transaction->meta['original_document_currency'] === $billCurrency &&
                    isset($transaction->meta['amount_in_document_currency_cents'])) {
                    return (int) $transaction->meta['amount_in_document_currency_cents'];
                }
                // Fall back to conversion if metadata is not available
                $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
                $amountCents = (int) $transaction->amount;
                return CurrencyConverter::convertBalance($amountCents, $bankAccountCurrency, $billCurrency);
            });
        $totalPaidInBillCurrencyCents = $withdrawalTotalInBillCurrencyCents;
        $billTotalInBillCurrencyCents = (int) $bill->total;
        $newStatus = match (true) {
            $totalPaidInBillCurrencyCents >= $billTotalInBillCurrencyCents => BillStatus::Paid,
            $totalPaidInBillCurrencyCents === 0 => BillStatus::Open,
            default => BillStatus::Partial,
        };
        $paidAt = $bill->paid_at;
        if ($newStatus === BillStatus::Paid && ! $paidAt) {
            $paidAt = $bill->withdrawals()
                ->latest('posted_at')
                ->value('posted_at');
        }
        $bill->update([
            'amount_paid' => $totalPaidInBillCurrencyCents,
            'status' => $newStatus,
            'paid_at' => $paidAt,
        ]);
    }
}
 |