config->getSchoolYear(); $semester = $this->config->getSemester(); $enrollments = Enrollment::query() ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->get() ->map(fn ($r) => $r->toArray()) ->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); } }