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,
- ]);
- }
- }
|