178 lines
5.8 KiB
PHP
178 lines
5.8 KiB
PHP
<?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;
|
|
}
|
|
}
|