Files
alrahma_sunday_school_api/app/Services/Finance/InstallmentPlanService.php
T
2026-06-05 01:51:08 -04:00

137 lines
6.1 KiB
PHP

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