265 lines
9.6 KiB
PHP
265 lines
9.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Invoices;
|
|
|
|
use App\Models\Enrollment;
|
|
use App\Models\EventCharges;
|
|
use App\Models\Invoice;
|
|
use App\Models\Payment;
|
|
use App\Models\Refund;
|
|
use App\Models\StudentClass;
|
|
use App\Models\User;
|
|
use DateTime;
|
|
use DateTimeZone;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class InvoiceGenerationService
|
|
{
|
|
public function __construct(
|
|
private InvoiceConfigService $config,
|
|
private InvoiceTuitionService $tuitionService
|
|
) {
|
|
}
|
|
|
|
public function generateInvoice(int $parentId, ?string $schoolYear = null): array
|
|
{
|
|
$schoolYear = $schoolYear ?: $this->config->getSchoolYear();
|
|
$semester = $this->config->getSemester();
|
|
|
|
$enrollments = Enrollment::query()
|
|
->where('parent_id', $parentId)
|
|
->where('school_year', $schoolYear)
|
|
->get()
|
|
->map(fn ($r) => (array) $r)
|
|
->all();
|
|
|
|
if (empty($enrollments)) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'No enrollment records found.',
|
|
];
|
|
}
|
|
|
|
$registeredKids = [];
|
|
$withdrawnKids = [];
|
|
|
|
foreach ($enrollments as $enrollment) {
|
|
$student = [
|
|
'student_id' => $enrollment['student_id'],
|
|
'parent_id' => $enrollment['parent_id'],
|
|
'class_section_id' => $enrollment['class_section_id'],
|
|
'enrollment_status' => $enrollment['enrollment_status'],
|
|
'school_year' => $enrollment['school_year'],
|
|
'semester' => $enrollment['semester'],
|
|
'admission_status' => $enrollment['admission_status'],
|
|
'is_withdrawn' => $enrollment['is_withdrawn'],
|
|
];
|
|
|
|
if (in_array($enrollment['enrollment_status'], ['enrolled', 'payment pending'], true)) {
|
|
$registeredKids[] = $student;
|
|
} elseif (in_array($enrollment['enrollment_status'], ['withdrawn', 'refund pending', 'withdraw under review'], true)) {
|
|
$withdrawnKids[] = $student;
|
|
}
|
|
}
|
|
|
|
$registeredKids = array_values(array_filter($registeredKids, function ($student) use ($schoolYear) {
|
|
$sid = (int) ($student['student_id'] ?? 0);
|
|
return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear);
|
|
}));
|
|
|
|
$withdrawnKids = array_values(array_filter($withdrawnKids, function ($student) use ($schoolYear) {
|
|
$sid = (int) ($student['student_id'] ?? 0);
|
|
return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear);
|
|
}));
|
|
|
|
$tuitionFee = $this->tuitionService->calculateTuitionFee(
|
|
$registeredKids,
|
|
$withdrawnKids,
|
|
$this->config->getRefundDeadline()
|
|
);
|
|
|
|
$eventsList = EventCharges::getChargesWithEventInfo($parentId, $schoolYear);
|
|
$eventChargeTotal = array_sum(array_column($eventsList, 'charged'));
|
|
|
|
$this->recalculateAndUpdateDiscount($parentId, $schoolYear, $tuitionFee, $enrollments);
|
|
|
|
$refundPaid = (new Refund())->getTotalApprovedRefundByParentIdAndSchoolYear($parentId, $schoolYear);
|
|
$totalPaid = Payment::getTotalPaidByParentId($parentId, $schoolYear);
|
|
|
|
$totalAmount = max(0.0, (float) $tuitionFee) + (float) $eventChargeTotal;
|
|
|
|
$existingInvoices = Invoice::getInvoicesByParentId($parentId, $schoolYear);
|
|
$updated = false;
|
|
$updatedIds = [];
|
|
$insertId = null;
|
|
|
|
if (!empty($existingInvoices)) {
|
|
foreach ($existingInvoices as $invoice) {
|
|
if (!isset($invoice['id'])) {
|
|
continue;
|
|
}
|
|
|
|
$extrasSum = $this->sumInvoiceAdditionalCharges((int) $invoice['id'], $schoolYear);
|
|
$newTotal = round($totalAmount + $extrasSum, 2);
|
|
|
|
$invDiscount = $this->sumInvoiceDiscounts((int) $invoice['id']);
|
|
$invRefunds = $this->sumInvoiceRefunds((int) $invoice['id'], $schoolYear);
|
|
$paidOnInv = (float) ($invoice['paid_amount'] ?? 0.0);
|
|
|
|
$newBalance = $newTotal - $invDiscount - $invRefunds - $paidOnInv;
|
|
$newStatus = $newBalance <= 0.00001
|
|
? 'Paid'
|
|
: ($paidOnInv > 0 ? 'Partially Paid' : 'Unpaid');
|
|
|
|
Invoice::query()
|
|
->whereKey($invoice['id'])
|
|
->update([
|
|
'total_amount' => $newTotal,
|
|
'paid_amount' => $paidOnInv,
|
|
'balance' => $newBalance,
|
|
'status' => $newStatus,
|
|
'updated_at' => utc_now(),
|
|
]);
|
|
|
|
$updatedIds[] = (int) $invoice['id'];
|
|
$updated = true;
|
|
}
|
|
} else {
|
|
$schoolId = User::getSchoolIdByUserId($parentId);
|
|
$invoiceNumber = $schoolId
|
|
? ('INV-' . $schoolId . '-' . uniqid())
|
|
: uniqid('INV-');
|
|
|
|
$issueUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
|
$dueUtc = null;
|
|
$dueDate = $this->config->getDueDate();
|
|
if (!empty($dueDate)) {
|
|
$dueLocal = new DateTime($dueDate . ' 19:59:59', new DateTimeZone($this->config->getTimezone()));
|
|
$dueLocal->setTimezone(new DateTimeZone('UTC'));
|
|
$dueUtc = $dueLocal->format('Y-m-d H:i:s');
|
|
}
|
|
|
|
$insertId = Invoice::query()->insertGetId([
|
|
'parent_id' => $parentId,
|
|
'invoice_number' => $invoiceNumber,
|
|
'total_amount' => $totalAmount,
|
|
'paid_amount' => 0,
|
|
'balance' => $totalAmount,
|
|
'status' => 'Unpaid',
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'issue_date' => $issueUtc,
|
|
'due_date' => $dueUtc,
|
|
'created_at' => utc_now(),
|
|
'updated_at' => utc_now(),
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'ok' => true,
|
|
'updated' => $updated,
|
|
'updated_ids' => $updatedIds,
|
|
'insert_id' => $insertId ? (int) $insertId : null,
|
|
'total_paid' => $totalPaid,
|
|
'refund_paid' => $refundPaid,
|
|
];
|
|
}
|
|
|
|
private function recalculateAndUpdateDiscount(int $parentId, string $schoolYear, float $tuitionFee, array $enrollments): void
|
|
{
|
|
$eventChargeTotal = 0.0;
|
|
try {
|
|
$eventsList = EventCharges::getChargesWithEventInfo($parentId, $schoolYear);
|
|
$eventChargeTotal = array_sum(array_column($eventsList, 'charged'));
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
$invoices = Invoice::getInvoicesByParentId($parentId, $schoolYear);
|
|
if (empty($invoices)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($invoices as $invoice) {
|
|
if (!isset($invoice['id'])) {
|
|
continue;
|
|
}
|
|
|
|
$invoiceId = (int) $invoice['id'];
|
|
$discountUsage = DB::table('discount_usages as du')
|
|
->select('du.id', 'dv.id as voucher_id', 'dv.discount_type', 'dv.discount_value')
|
|
->join('discount_vouchers as dv', 'du.voucher_id', '=', 'dv.id')
|
|
->join('invoices as i', 'du.invoice_id', '=', 'i.id')
|
|
->where('du.invoice_id', $invoiceId)
|
|
->where('i.school_year', $schoolYear)
|
|
->first();
|
|
|
|
if (!$discountUsage) {
|
|
continue;
|
|
}
|
|
|
|
$extrasSum = $this->sumInvoiceAdditionalCharges($invoiceId, $schoolYear);
|
|
$baseTotal = round($tuitionFee + $eventChargeTotal + $extrasSum, 2);
|
|
|
|
if (($discountUsage->discount_type ?? '') === 'percent') {
|
|
$discountAmount = round(($baseTotal * (float) $discountUsage->discount_value) / 100, 2);
|
|
} else {
|
|
$discountAmount = min((float) $discountUsage->discount_value, $baseTotal);
|
|
}
|
|
|
|
DB::table('discount_usages')
|
|
->where('id', $discountUsage->id)
|
|
->update([
|
|
'discount_amount' => $discountAmount,
|
|
'updated_at' => utc_now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function sumInvoiceAdditionalCharges(int $invoiceId, string $schoolYear): float
|
|
{
|
|
$extrasSum = 0.0;
|
|
$rows = DB::table('additional_charges')
|
|
->select('charge_type', 'amount')
|
|
->where('invoice_id', $invoiceId)
|
|
->where('school_year', $schoolYear)
|
|
->where('status', 'applied')
|
|
->get();
|
|
|
|
foreach ($rows as $row) {
|
|
$amount = (float) ($row->amount ?? 0);
|
|
$type = strtolower((string) ($row->charge_type ?? 'add'));
|
|
if ($type === 'deduct') {
|
|
$amount = -abs($amount);
|
|
} else {
|
|
$amount = abs($amount);
|
|
}
|
|
$extrasSum += $amount;
|
|
}
|
|
|
|
return $extrasSum;
|
|
}
|
|
|
|
private function sumInvoiceDiscounts(int $invoiceId): float
|
|
{
|
|
$row = DB::table('discount_usages')
|
|
->selectRaw('COALESCE(SUM(discount_amount),0) AS tot')
|
|
->where('invoice_id', $invoiceId)
|
|
->first();
|
|
|
|
return (float) ($row->tot ?? 0.0);
|
|
}
|
|
|
|
private function sumInvoiceRefunds(int $invoiceId, string $schoolYear): float
|
|
{
|
|
$row = DB::table('refunds')
|
|
->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
|
->where('invoice_id', $invoiceId)
|
|
->where('school_year', $schoolYear)
|
|
->whereIn('status', ['Partial', 'Paid'])
|
|
->first();
|
|
|
|
return (float) ($row->tot ?? 0.0);
|
|
}
|
|
}
|