ultimatepos/Modules/Accounting/Services/InstallmentService.php

149 lines
5.3 KiB
PHP

<?php
namespace Modules\Accounting\Services;
use Carbon\Carbon;
use Modules\Accounting\Domain\FinancialPostingRequest;
use Modules\Accounting\Entities\AccountingInstallmentPlan;
use Modules\Accounting\Entities\AccountingInstallmentSchedule;
class InstallmentService
{
public function __construct(protected FinancialPostingService $postingService) {}
public function createPlan(array $data, int $businessId, int $userId): AccountingInstallmentPlan
{
$count = (int) $data['installment_count'];
$principal = (float) $data['principal_amount'];
$installmentAmount = round($principal / max($count, 1), 4);
$plan = AccountingInstallmentPlan::create([
'business_id' => $businessId,
'transaction_id' => $data['transaction_id'],
'contact_id' => $data['contact_id'] ?? null,
'principal_amount' => $principal,
'installment_count' => $count,
'installment_amount' => $installmentAmount,
'interest_rate' => $data['interest_rate'] ?? 0,
'start_date' => $data['start_date'],
'frequency' => $data['frequency'] ?? 'monthly',
'status' => 'active',
'created_by' => $userId,
]);
$dueDate = Carbon::parse($data['start_date']);
for ($i = 1; $i <= $count; $i++) {
AccountingInstallmentSchedule::create([
'installment_plan_id' => $plan->id,
'installment_number' => $i,
'due_date' => $dueDate->copy()->toDateString(),
'amount' => $installmentAmount,
'status' => 'pending',
]);
$dueDate = match ($plan->frequency) {
'weekly' => $dueDate->addWeek(),
'quarterly' => $dueDate->addMonths(3),
default => $dueDate->addMonth(),
};
}
return $plan->load('schedule');
}
public function recordPayment(
AccountingInstallmentSchedule $schedule,
float $amount,
int $userId,
int $debitAccountId,
int $creditAccountId
): AccountingInstallmentSchedule {
$mapping = $this->postingService->post(new FinancialPostingRequest(
businessId: $schedule->plan->business_id,
userId: $userId,
sourceType: 'installment_payment',
sourceId: $schedule->id,
note: 'Installment #'.$schedule->installment_number,
lines: [
['accounting_account_id' => $debitAccountId, 'amount' => $amount, 'type' => 'debit'],
['accounting_account_id' => $creditAccountId, 'amount' => $amount, 'type' => 'credit'],
],
operationDate: now()->toDateString(),
));
$schedule->paid_amount = (float) $schedule->paid_amount + $amount;
$schedule->journal_mapping_id = $mapping?->id;
$totalDue = (float) $schedule->amount + (float) $schedule->penalty_amount;
$schedule->status = $schedule->paid_amount + 0.01 >= $totalDue ? 'paid' : 'partial';
$schedule->save();
$plan = $schedule->plan;
if ($plan->schedule()->whereNotIn('status', ['paid'])->count() === 0) {
$plan->update(['status' => 'completed']);
}
return $schedule;
}
public function markOverdue(?int $businessId = null): int
{
$query = AccountingInstallmentSchedule::query()
->whereIn('status', ['pending', 'partial'])
->whereDate('due_date', '<', now()->toDateString())
->whereHas('plan', function ($q) use ($businessId) {
$q->where('status', 'active');
if ($businessId) {
$q->where('business_id', $businessId);
}
});
$count = 0;
foreach ($query->with('plan')->get() as $schedule) {
$this->applyOverduePenalty($schedule);
if ($schedule->status !== 'paid') {
$schedule->status = $schedule->paid_amount > 0 ? 'partial' : 'overdue';
}
$schedule->save();
$count++;
}
return $count;
}
public function calculatePenalty(AccountingInstallmentSchedule $schedule): float
{
$plan = $schedule->plan;
$rate = (float) ($plan->interest_rate ?: config('accounting.installments.default_penalty_rate', 0));
if ($rate <= 0) {
return 0.0;
}
$outstanding = max(0, (float) $schedule->amount - (float) $schedule->paid_amount);
if ($outstanding < 0.01) {
return 0.0;
}
$daysOverdue = Carbon::parse($schedule->due_date)->diffInDays(now());
if ($daysOverdue < 1) {
return 0.0;
}
$months = max(1, (int) ceil($daysOverdue / 30));
return round($outstanding * ($rate / 100) * $months, 4);
}
protected function applyOverduePenalty(AccountingInstallmentSchedule $schedule): void
{
$penalty = $this->calculatePenalty($schedule);
if ($penalty > (float) $schedule->penalty_amount) {
$schedule->penalty_amount = $penalty;
}
}
public function totalDue(AccountingInstallmentSchedule $schedule): float
{
return round((float) $schedule->amount + (float) $schedule->penalty_amount - (float) $schedule->paid_amount, 4);
}
}