Files
hqland-app/app/Observers/PaymentObserver.php
2026-04-24 08:58:53 +00:00

121 lines
3.6 KiB
PHP

<?php
namespace App\Observers;
use App\Models\Payment;
use App\Models\Contract;
use App\Models\PaymentScheduleItem;
class PaymentObserver
{
private static bool $handlingSurplus = false;
/**
* Tính toán lại tài chính hợp đồng sau mỗi thay đổi payment.
*/
private function recalculateContract(Contract $contract): void
{
$totalPaid = (float) $contract->payments()->sum('amount');
$contractValue = (float) $contract->total_value;
$contract->paid_amount = $totalPaid;
if ($totalPaid > $contractValue) {
$contract->remaining_amount = 0;
$contract->excess_amount = $totalPaid - $contractValue;
} else {
$contract->remaining_amount = $contractValue - $totalPaid;
$contract->excess_amount = 0;
}
$contract->saveQuietly();
}
/**
* Tự động khấu trừ tiền dư vào đợt thanh toán tiếp theo.
*/
private function applySurplusToNextInstallment(Contract $contract): void
{
if (self::$handlingSurplus) {
return;
}
$excess = (float) $contract->excess_amount;
if ($excess <= 0) {
return;
}
// Tìm đợt tiếp theo chưa thanh toán đủ (hoặc chưa có payment nào)
$nextItem = PaymentScheduleItem::query()
->whereHas('schedule', fn ($q) => $q->where('contract_id', $contract->id))
->whereNotNull('amount')
->orderBy('installment_no')
->get()
->first(function ($item) use ($contract) {
$paidForItem = (float) $contract->payments()
->where('schedule_item_id', $item->id)
->sum('amount');
return $paidForItem < (float) $item->amount;
});
if (! $nextItem) {
return;
}
$paidForItem = (float) $contract->payments()
->where('schedule_item_id', $nextItem->id)
->sum('amount');
$remainingForItem = (float) $nextItem->amount - $paidForItem;
if ($remainingForItem <= 0) {
return;
}
$applyAmount = min($excess, $remainingForItem);
self::$handlingSurplus = true;
Payment::create([
'contract_id' => $contract->id,
'schedule_item_id' => $nextItem->id,
'amount' => $applyAmount,
'paid_date' => now(),
'method' => 'Tự động khấu trừ',
'receipt_number' => 'AUTO-SURPLUS-' . now()->format('YmdHis'),
'metadata' => ['auto_surplus' => true, 'source' => 'excess_amount'],
]);
self::$handlingSurplus = false;
}
public function created(Payment $payment): void
{
if ($payment->contract) {
$this->recalculateContract($payment->contract);
$this->applySurplusToNextInstallment($payment->contract);
}
}
public function updated(Payment $payment): void
{
if ($payment->contract) {
$this->recalculateContract($payment->contract);
$this->applySurplusToNextInstallment($payment->contract);
}
if ($payment->wasChanged('contract_id') && $payment->getOriginal('contract_id')) {
$oldContract = Contract::find($payment->getOriginal('contract_id'));
if ($oldContract) {
$this->recalculateContract($oldContract);
}
}
}
public function deleted(Payment $payment): void
{
if ($payment->contract) {
$this->recalculateContract($payment->contract);
}
}
}