fix financial and certificates
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
use App\Models\InvoiceInstallment;
|
||||
use App\Models\InvoiceInstallmentPlan;
|
||||
use App\Models\PaymentInstallmentAllocation;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class InstallmentPlanService
|
||||
{
|
||||
public function list(array $filters): array
|
||||
{
|
||||
$q = InvoiceInstallmentPlan::query();
|
||||
foreach (['invoice_id','parent_id','school_year','status'] as $field) {
|
||||
if (!empty($filters[$field])) $q->where($field, $filters[$field]);
|
||||
}
|
||||
return ['rows' => $q->orderByDesc('id')->paginate((int)($filters['per_page'] ?? 25))];
|
||||
}
|
||||
|
||||
public function createForInvoice(int $invoiceId, array $payload, ?int $actorId): array
|
||||
{
|
||||
return DB::transaction(function () use ($invoiceId, $payload, $actorId) {
|
||||
$invoice = DB::table('invoices')->where('id', $invoiceId)->first();
|
||||
abort_if(!$invoice, 404, 'Invoice not found.');
|
||||
$total = (float) ($payload['total_amount'] ?? ($invoice->balance ?? $invoice->total_amount ?? 0));
|
||||
$count = (int) $payload['number_of_installments'];
|
||||
$plan = InvoiceInstallmentPlan::query()->create([
|
||||
'invoice_id' => $invoiceId,
|
||||
'parent_id' => $invoice->parent_id,
|
||||
'school_year' => $invoice->school_year ?? null,
|
||||
'plan_name' => $payload['plan_name'] ?? ('Invoice #' . $invoiceId . ' installment plan'),
|
||||
'total_amount' => $total,
|
||||
'number_of_installments' => $count,
|
||||
'status' => 'draft',
|
||||
'created_by' => $actorId,
|
||||
]);
|
||||
|
||||
$firstDue = Carbon::parse($payload['first_due_date']);
|
||||
$interval = (int) ($payload['interval_months'] ?? 1);
|
||||
$base = floor(($total / $count) * 100) / 100;
|
||||
$allocated = 0.0;
|
||||
for ($i = 1; $i <= $count; $i++) {
|
||||
$amount = $i === $count ? round($total - $allocated, 2) : $base;
|
||||
$allocated += $amount;
|
||||
InvoiceInstallment::query()->create([
|
||||
'installment_plan_id' => $plan->id,
|
||||
'invoice_id' => $invoiceId,
|
||||
'sequence' => $i,
|
||||
'due_date' => $firstDue->copy()->addMonths(($i - 1) * $interval)->toDateString(),
|
||||
'amount_due' => $amount,
|
||||
'amount_paid' => 0,
|
||||
'balance' => $amount,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->show($plan->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function show(int $id): array
|
||||
{
|
||||
$plan = InvoiceInstallmentPlan::query()->findOrFail($id)->toArray();
|
||||
$plan['installments'] = InvoiceInstallment::query()->where('installment_plan_id', $id)->orderBy('sequence')->get()->toArray();
|
||||
return $plan;
|
||||
}
|
||||
|
||||
public function activate(int $id, ?int $actorId): array
|
||||
{
|
||||
$plan = InvoiceInstallmentPlan::query()->findOrFail($id);
|
||||
$plan->fill(['status' => 'active', 'approved_by' => $actorId])->save();
|
||||
InvoiceInstallment::query()->where('installment_plan_id', $id)->where('status', 'pending')->update(['status' => 'due']);
|
||||
return $this->show($id);
|
||||
}
|
||||
|
||||
public function cancel(int $id): array
|
||||
{
|
||||
InvoiceInstallmentPlan::query()->whereKey($id)->update(['status' => 'cancelled']);
|
||||
InvoiceInstallment::query()->where('installment_plan_id', $id)->whereNotIn('status', ['paid'])->update(['status' => 'cancelled']);
|
||||
return $this->show($id);
|
||||
}
|
||||
|
||||
public function allocatePayment(int $paymentId, array $payload): array
|
||||
{
|
||||
return DB::transaction(function () use ($paymentId, $payload) {
|
||||
$payment = DB::table('payments')->where('id', $paymentId)->first();
|
||||
abort_if(!$payment, 404, 'Payment not found.');
|
||||
$allocations = $payload['installments'] ?? null;
|
||||
if (!$allocations) {
|
||||
$remaining = (float) ($payment->paid_amount ?? 0);
|
||||
$installments = InvoiceInstallment::query()->where('invoice_id', $payment->invoice_id)->where('balance', '>', 0)->orderBy('due_date')->lockForUpdate()->get();
|
||||
$allocations = [];
|
||||
foreach ($installments as $inst) {
|
||||
if ($remaining <= 0) break;
|
||||
$amount = min($remaining, (float) $inst->balance);
|
||||
$allocations[] = ['installment_id' => $inst->id, 'amount' => $amount];
|
||||
$remaining -= $amount;
|
||||
}
|
||||
}
|
||||
|
||||
$created = [];
|
||||
foreach ($allocations as $a) {
|
||||
$inst = InvoiceInstallment::query()->lockForUpdate()->findOrFail($a['installment_id']);
|
||||
$amount = min((float) $a['amount'], (float) $inst->balance);
|
||||
if ($amount <= 0) continue;
|
||||
$created[] = PaymentInstallmentAllocation::query()->create([
|
||||
'payment_id' => $paymentId,
|
||||
'invoice_id' => $inst->invoice_id,
|
||||
'installment_id' => $inst->id,
|
||||
'amount' => $amount,
|
||||
])->toArray();
|
||||
$inst->amount_paid = (float) $inst->amount_paid + $amount;
|
||||
$inst->balance = max(0, (float) $inst->amount_due - (float) $inst->amount_paid);
|
||||
$inst->status = $inst->balance <= 0 ? 'paid' : 'partially_paid';
|
||||
$inst->paid_at = $inst->balance <= 0 ? now() : null;
|
||||
$inst->save();
|
||||
}
|
||||
return ['allocations' => $created];
|
||||
});
|
||||
}
|
||||
|
||||
public function due(bool $overdue = false): array
|
||||
{
|
||||
$q = InvoiceInstallment::query()->where('balance', '>', 0);
|
||||
$overdue ? $q->whereDate('due_date', '<', now()->toDateString()) : $q->whereDate('due_date', '>=', now()->toDateString());
|
||||
return ['rows' => $q->orderBy('due_date')->get()->toArray()];
|
||||
}
|
||||
|
||||
public function markOverdue(): int
|
||||
{
|
||||
return InvoiceInstallment::query()->where('balance', '>', 0)->whereDate('due_date', '<', now()->toDateString())->whereNotIn('status', ['paid','cancelled','waived'])->update(['status' => 'overdue']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user