add refund and event logic
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Refunds;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Refund;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class RefundPayoutService
|
||||
{
|
||||
public function recordPayment(int $refundId, array $payload, int $actorId): array
|
||||
{
|
||||
$refund = Refund::query()->find($refundId);
|
||||
if (!$refund) {
|
||||
return ['ok' => false, 'message' => 'Refund not found.'];
|
||||
}
|
||||
|
||||
if (!in_array($refund->status, [Refund::STATUS_APPROVED, Refund::STATUS_PARTIAL], true)) {
|
||||
return ['ok' => false, 'message' => 'Refund must be Approved or Partial to pay.'];
|
||||
}
|
||||
|
||||
$newPaidAmount = (float) ($payload['paid_amount'] ?? 0);
|
||||
if ($newPaidAmount <= 0) {
|
||||
return ['ok' => false, 'message' => 'Valid paid amount is required.'];
|
||||
}
|
||||
|
||||
$oldPaid = (float) ($refund->refund_paid_amount ?? 0);
|
||||
$target = (float) ($refund->refund_amount ?? 0);
|
||||
$total = $oldPaid + $newPaidAmount;
|
||||
|
||||
if ($total > $target + 0.0001) {
|
||||
return ['ok' => false, 'message' => 'Total paid cannot exceed refund amount.'];
|
||||
}
|
||||
|
||||
$refundMethod = (string) ($payload['payment_method'] ?? '');
|
||||
$checkNumber = $payload['check_number'] ?? null;
|
||||
$checkFileName = $refund->check_file;
|
||||
|
||||
if ($refundMethod === 'Check' && ($payload['check_file'] ?? null) instanceof UploadedFile) {
|
||||
$file = $payload['check_file'];
|
||||
if ($file->isValid()) {
|
||||
$checkFileName = $file->hashName();
|
||||
$file->storeAs('checks', $checkFileName);
|
||||
}
|
||||
}
|
||||
|
||||
$newStatus = ($total < $target) ? Refund::STATUS_PARTIAL : Refund::STATUS_PAID;
|
||||
|
||||
return DB::transaction(function () use (
|
||||
$refund,
|
||||
$total,
|
||||
$newStatus,
|
||||
$refundMethod,
|
||||
$checkNumber,
|
||||
$checkFileName,
|
||||
$actorId
|
||||
) {
|
||||
$assignInvoiceId = $refund->invoice_id ?: $this->assignInvoiceForRefund($refund);
|
||||
|
||||
$refund->update([
|
||||
'refund_paid_amount' => $total,
|
||||
'status' => $newStatus,
|
||||
'refunded_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => $actorId,
|
||||
'refund_method' => $refundMethod,
|
||||
'check_nbr' => $checkNumber,
|
||||
'check_file' => $checkFileName,
|
||||
'invoice_id' => $assignInvoiceId,
|
||||
]);
|
||||
|
||||
return ['ok' => true];
|
||||
});
|
||||
}
|
||||
|
||||
private function assignInvoiceForRefund(Refund $refund): ?int
|
||||
{
|
||||
$parentId = (int) ($refund->parent_id ?? 0);
|
||||
$schoolYear = (string) ($refund->school_year ?? '');
|
||||
if ($parentId <= 0 || $schoolYear === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$invRows = Invoice::query()
|
||||
->select('id', 'total_amount')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
if (empty($invRows)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$invoiceIds = array_map(static fn ($r) => (int) $r['id'], $invRows);
|
||||
|
||||
$paidBy = $this->sumPaymentsByInvoice($invoiceIds);
|
||||
$discBy = $this->sumDiscountsByInvoice($invoiceIds);
|
||||
|
||||
$minRaw = 0.0;
|
||||
$minId = null;
|
||||
foreach ($invRows as $row) {
|
||||
$iid = (int) $row['id'];
|
||||
$raw = (float) ($row['total_amount'] ?? 0) - (float) ($discBy[$iid] ?? 0) - (float) ($paidBy[$iid] ?? 0);
|
||||
if ($raw < $minRaw) {
|
||||
$minRaw = $raw;
|
||||
$minId = $iid;
|
||||
}
|
||||
}
|
||||
|
||||
if ($minId !== null) {
|
||||
return $minId;
|
||||
}
|
||||
|
||||
$latest = Invoice::query()
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('created_at')
|
||||
->first();
|
||||
|
||||
return $latest ? (int) $latest->id : null;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Refund invoice assignment failed.', ['error' => $e->getMessage()]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function sumPaymentsByInvoice(array $invoiceIds): array
|
||||
{
|
||||
$query = DB::table('payments')
|
||||
->select('invoice_id', DB::raw('COALESCE(SUM(paid_amount),0) AS total_paid'))
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->groupBy('invoice_id');
|
||||
|
||||
if (Schema::hasColumn('payments', 'status')) {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNotIn('status', [
|
||||
'void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled',
|
||||
])->orWhereNull('status');
|
||||
});
|
||||
}
|
||||
if (Schema::hasColumn('payments', 'is_void')) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('is_void', 0)->orWhereNull('is_void');
|
||||
});
|
||||
}
|
||||
|
||||
$rows = $query->get();
|
||||
$paidBy = [];
|
||||
foreach ($rows as $row) {
|
||||
$paidBy[(int) $row->invoice_id] = (float) ($row->total_paid ?? 0);
|
||||
}
|
||||
|
||||
return $paidBy;
|
||||
}
|
||||
|
||||
private function sumDiscountsByInvoice(array $invoiceIds): array
|
||||
{
|
||||
$rows = DB::table('discount_usages')
|
||||
->select('invoice_id', DB::raw('COALESCE(SUM(discount_amount),0) AS total_disc'))
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->groupBy('invoice_id')
|
||||
->get();
|
||||
|
||||
$discBy = [];
|
||||
foreach ($rows as $row) {
|
||||
$discBy[(int) $row->invoice_id] = (float) ($row->total_disc ?? 0);
|
||||
}
|
||||
|
||||
return $discBy;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user