@@ -18,6 +18,10 @@ use App\Models\InvoiceEventModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\DiscountUsageModel;
|
||||
use App\Models\RefundModel;
|
||||
use App\Libraries\FinancialStatus;
|
||||
use App\Libraries\IssueInvoiceCommand;
|
||||
use App\Libraries\InvoiceIssuanceService;
|
||||
use App\Libraries\InvoiceLedgerService;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
|
||||
@@ -50,6 +54,8 @@ class InvoiceController extends ResourceController
|
||||
protected $request;
|
||||
protected $gradeFee;
|
||||
protected $classSectionModel;
|
||||
protected $invoiceLedgerService;
|
||||
protected InvoiceIssuanceService $invoiceIssuanceService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -67,7 +73,9 @@ class InvoiceController extends ResourceController
|
||||
$this->chargesModel = new EventChargesModel();
|
||||
$this->discountUsageModel = new DiscountUsageModel();
|
||||
$this->refundModel = new RefundModel();
|
||||
$this->invoiceLedgerService = new InvoiceLedgerService();
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->invoiceIssuanceService = new InvoiceIssuanceService($this->db, $this->invoiceModel, null, $this->invoiceLedgerService);
|
||||
$this->request = \Config\Services::request();
|
||||
|
||||
$this->gradeFee = $this->configModel->getConfig('grade_fee');
|
||||
@@ -391,7 +399,7 @@ class InvoiceController extends ResourceController
|
||||
}
|
||||
|
||||
public function generateInvoice(
|
||||
string $parentId = null,
|
||||
?string $parentId = null,
|
||||
?string $schoolYearOverride = null,
|
||||
?string $semesterOverride = null,
|
||||
bool $recalculateDiscounts = true
|
||||
@@ -472,20 +480,9 @@ class InvoiceController extends ResourceController
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Refunds PAID to the parent for this year (Partial/Paid)
|
||||
$refundPaid = (float) $this->refundModel->getTotalApprovedRefundByParentIdAndSchoolYear($parentId, $schoolYear);
|
||||
|
||||
$totalPaid = $this->paymentModel->getTotalPaidByParentId($parentId, $schoolYear);
|
||||
|
||||
$discountedTuition = max(0, $tuitionFee);
|
||||
$totalAmount = $discountedTuition + $eventchargeTotal;
|
||||
|
||||
// Parent-level balance (informational); we will recalc per invoice below
|
||||
$parentBalance = $totalAmount // original charges (tuition + events)
|
||||
- $totalDiscount // any applied discounts/vouchers
|
||||
- $refundPaid // approved refunds paid to parent
|
||||
- $totalPaid; // payments received
|
||||
|
||||
// Business rule: single invoice per parent per school year.
|
||||
// If legacy duplicates exist, prefer the invoice that already has a discount applied,
|
||||
// otherwise use the latest invoice for the parent/year.
|
||||
@@ -494,106 +491,10 @@ class InvoiceController extends ResourceController
|
||||
$updated = false;
|
||||
$updatedIds = [];
|
||||
if (!empty($invoice) && isset($invoice['id'])) {
|
||||
$paymentExclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
||||
$paymentsHasStatus = false;
|
||||
$paymentsHasVoid = false;
|
||||
try {
|
||||
$paymentsHasStatus = $this->db->fieldExists('status', 'payments');
|
||||
$paymentsHasVoid = $this->db->fieldExists('is_void', 'payments');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
// Preserve applied additional charges and recalc this invoice only
|
||||
$extrasSum = 0.0;
|
||||
try {
|
||||
$rows = $this->db->table('additional_charges')
|
||||
->select('charge_type, amount')
|
||||
->where('invoice_id', (int)$invoice['id'])
|
||||
->where('school_year', $schoolYear)
|
||||
->where('status', 'applied')
|
||||
->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$amt = (float)($r['amount'] ?? 0);
|
||||
$typ = strtolower((string)($r['charge_type'] ?? 'add'));
|
||||
if ($typ === 'deduct') $amt = -abs($amt); else $amt = abs($amt);
|
||||
$extrasSum += $amt;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'additional_charges sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$newTotal = round($totalAmount + $extrasSum, 2);
|
||||
|
||||
// Per-invoice discount
|
||||
$invDiscount = 0.0;
|
||||
try {
|
||||
$d = $this->db->table('discount_usages')
|
||||
->select('COALESCE(SUM(discount_amount),0) AS tot')
|
||||
->where('invoice_id', (int)$invoice['id'])
|
||||
->get()->getRowArray();
|
||||
$invDiscount = (float)($d['tot'] ?? 0.0);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'discount sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Per-invoice refunds paid
|
||||
$invRefunds = 0.0;
|
||||
try {
|
||||
$r = $this->db->table('refunds')
|
||||
->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
||||
->where('invoice_id', (int)$invoice['id'])
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['Partial','Paid'])
|
||||
->get()->getRowArray();
|
||||
$invRefunds = (float)($r['tot'] ?? 0.0);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'refund sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Payments recorded on this invoice (sum payments to avoid stale invoice.paid_amount)
|
||||
$paidOnInv = 0.0;
|
||||
try {
|
||||
$qb = $this->db->table('payments')
|
||||
->select('COALESCE(SUM(paid_amount),0) AS tot')
|
||||
->where('invoice_id', (int)$invoice['id'])
|
||||
->where('paid_amount >', 0);
|
||||
|
||||
if ($paymentsHasStatus) {
|
||||
$qb->groupStart()
|
||||
->whereNotIn('status', $paymentExclude)
|
||||
->orWhere('status IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
if ($paymentsHasVoid) {
|
||||
$qb->groupStart()
|
||||
->where('is_void', 0)
|
||||
->orWhere('is_void IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$row = $qb->get()->getRowArray();
|
||||
$paidOnInv = (float)($row['tot'] ?? 0.0);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'payment sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
|
||||
$paidOnInv = (float)($invoice['paid_amount'] ?? 0.0);
|
||||
}
|
||||
|
||||
$newBalance = $newTotal - $invDiscount - $invRefunds - $paidOnInv;
|
||||
$newStatus = ($newBalance <= 0.00001)
|
||||
? 'Paid'
|
||||
: (($paidOnInv > 0) ? 'Partially Paid' : 'Unpaid');
|
||||
|
||||
$this->invoiceModel->update($invoice['id'], [
|
||||
'total_amount' => $newTotal,
|
||||
'paid_amount' => $paidOnInv,
|
||||
'balance' => $newBalance,
|
||||
'status' => $newStatus,
|
||||
'updated_at' => utc_now()
|
||||
]);
|
||||
|
||||
$updatedIds[] = (int)$invoice['id'];
|
||||
log_message('info', "Updated invoice ID {$invoice['id']} for parent ID {$parentId}.");
|
||||
$updated = true;
|
||||
$ledger = $this->invoiceLedgerService->recalculate((int) $invoice['id']);
|
||||
$updatedIds[] = (int) $ledger['invoice_id'];
|
||||
log_message('info', "Updated invoice ID {$invoice['id']} for parent ID {$parentId}.");
|
||||
$updated = true;
|
||||
} else {
|
||||
// Generate invoice number
|
||||
$schoolId = $this->userModel->getSchoolIdByUserId($parentId);
|
||||
@@ -616,30 +517,35 @@ class InvoiceController extends ResourceController
|
||||
$dueUtc = $dueLocal->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$insertId = $this->invoiceModel->insert([
|
||||
'parent_id' => $parentId,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'total_amount' => $totalAmount,
|
||||
'paid_amount' => 0,
|
||||
// Initial balance equals the created total; discounts/refunds/payments will adjust later
|
||||
'balance' => $totalAmount,
|
||||
'status' => 'Unpaid',
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'issue_date' => $issueUtc,
|
||||
'due_date' => $dueUtc,
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now()
|
||||
]);
|
||||
|
||||
if (!$insertId) {
|
||||
log_message('error', 'Invoice insert failed: ' . json_encode($this->invoiceModel->errors()));
|
||||
try {
|
||||
$issueResult = $this->invoiceIssuanceService->issueInvoice(new IssueInvoiceCommand([
|
||||
'parent_id' => $parentId,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'total_amount' => $totalAmount,
|
||||
'paid_amount' => 0,
|
||||
'balance' => $totalAmount,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'issue_date' => $issueUtc,
|
||||
'due_date' => $dueUtc,
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now()
|
||||
], (float) $tuitionFee, (float) $eventchargeTotal, [
|
||||
'parent_id' => (int) $parentId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'registered_student_count' => count($registeredKids),
|
||||
'withdrawn_student_count' => count($withdrawnKids),
|
||||
]));
|
||||
$insertId = $issueResult->invoiceId;
|
||||
$ledger = $issueResult->ledger;
|
||||
log_message('info', "Invoice created successfully. Insert ID: {$insertId}");
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Invoice issuance failed: ' . $e->getMessage() . ' errors=' . json_encode($this->invoiceModel->errors()));
|
||||
if ($isAjax) {
|
||||
return $this->response->setJSON(['ok' => false, 'message' => 'Failed to create invoice.']);
|
||||
}
|
||||
return redirect()->back()->with('error', 'Failed to create invoice. Please check input values.');
|
||||
} else {
|
||||
log_message('info', "Invoice created successfully. Insert ID: {$insertId}");
|
||||
}
|
||||
$updated = false;
|
||||
}
|
||||
@@ -708,98 +614,12 @@ class InvoiceController extends ResourceController
|
||||
float $tuitionFee,
|
||||
array $enrollments
|
||||
): float {
|
||||
$totalDiscount = 0.00;
|
||||
$eventchargeTotal = 0.0;
|
||||
try {
|
||||
$eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear);
|
||||
$eventchargeTotal = array_sum(array_column($eventsList, 'charged'));
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to load event charges for discount recalculation: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Get all invoices for this parent and school year
|
||||
$invoices = $this->invoiceModel->getInvoicesByParentId($parentId, $schoolYear);
|
||||
|
||||
if (empty($invoices)) {
|
||||
log_message('info', "No invoices found for parent ID $parentId in school year $schoolYear.");
|
||||
return 0.00;
|
||||
}
|
||||
$totalDiscount = 0.0;
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
if (!isset($invoice['id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$invoiceId = $invoice['id'];
|
||||
|
||||
// Get discount usage + voucher details
|
||||
$discountUsage = $this->db->table('discount_usages du')
|
||||
->select('du.id, dv.id as voucher_id, dv.discount_type, dv.discount_value')
|
||||
->join('discount_vouchers dv', 'du.voucher_id = dv.id')
|
||||
->join('invoices i', 'du.invoice_id = i.id')
|
||||
->where('du.invoice_id', $invoiceId)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$discountUsage) {
|
||||
log_message('info', "No discount applied to invoice ID $invoiceId.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$extrasSum = 0.0;
|
||||
try {
|
||||
$rows = $this->db->table('additional_charges')
|
||||
->select('charge_type, amount')
|
||||
->where('invoice_id', (int)$invoice['id'])
|
||||
->where('school_year', $schoolYear)
|
||||
->where('status', 'applied')
|
||||
->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$amt = (float)($r['amount'] ?? 0);
|
||||
$typ = strtolower((string)($r['charge_type'] ?? 'add'));
|
||||
if ($typ === 'deduct') $amt = -abs($amt); else $amt = abs($amt);
|
||||
$extrasSum += $amt;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'additional_charges sum failed for discount recalculation invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$baseTotal = round($tuitionFee + $extrasSum, 2);
|
||||
|
||||
// Recalculate discount
|
||||
if ($discountUsage['discount_type'] === 'percent') {
|
||||
$discountAmount = round(($baseTotal * $discountUsage['discount_value']) / 100, 2);
|
||||
} else {
|
||||
$discountAmount = min($discountUsage['discount_value'], $baseTotal);
|
||||
}
|
||||
|
||||
// Update discount usage
|
||||
$this->db->table('discount_usages')
|
||||
->where('id', $discountUsage['id'])
|
||||
->update([
|
||||
'discount_amount' => $discountAmount,
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id')
|
||||
]);
|
||||
|
||||
$totalDiscount += $discountAmount;
|
||||
|
||||
// Log enrollment summary
|
||||
$added = [];
|
||||
$withdrawn = [];
|
||||
foreach ($enrollments as $e) {
|
||||
if (
|
||||
in_array($e['enrollment_status'], ['enrolled', 'payment pending']) &&
|
||||
$e['admission_status'] === 'accepted'
|
||||
) {
|
||||
$added[] = $e['student_id'];
|
||||
} elseif (in_array($e['enrollment_status'], ['withdrawn', 'refund pending', 'withdraw under review'])) {
|
||||
$withdrawn[] = $e['student_id'];
|
||||
}
|
||||
}
|
||||
|
||||
log_message('info', "Recalculated discount for invoice ID $invoiceId: Added students [" . implode(',', $added) . "], Withdrawn students [" . implode(',', $withdrawn) . "]. Discount updated to $discountAmount.");
|
||||
$ledger = $this->invoiceLedgerService->recalculateInvoice((int)$invoice['id']);
|
||||
$totalDiscount += (float)($ledger['discount_total'] ?? 0.0);
|
||||
}
|
||||
|
||||
return $totalDiscount;
|
||||
@@ -964,6 +784,15 @@ class InvoiceController extends ResourceController
|
||||
return ['error' => "Parent associated with the invoice was not found."];
|
||||
}
|
||||
|
||||
$ledger = $this->invoiceLedgerService->calculateInvoice((int) $invoiceId);
|
||||
$invoiceLines = $this->db->table('invoice_lines')
|
||||
->select('description, quantity, unit_amount_cents, line_amount_cents, line_type, source_type, source_id, created_at, metadata_json')
|
||||
->where('invoice_id', (int)$invoiceId)
|
||||
->where('voided_at IS NULL', null, false)
|
||||
->orderBy('id', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$enrollments = $this->enrollmentModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
@@ -1091,19 +920,7 @@ class InvoiceController extends ResourceController
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
// Refunds PAID for this specific invoice (money returned to the parent)
|
||||
$refundsPaidTotal = 0.0;
|
||||
try {
|
||||
$r = $this->db->table('refunds')
|
||||
->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['Partial','Paid'])
|
||||
->get()->getRowArray();
|
||||
$refundsPaidTotal = (float)($r['tot'] ?? 0.0);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to sum refunds for invoice ' . (int)$invoiceId . ': ' . $e->getMessage());
|
||||
}
|
||||
$refundsPaidTotal = (float) ($ledger['refund_paid_total'] ?? 0.0);
|
||||
|
||||
/* ============================================================
|
||||
* ADDITIONAL CHARGES (itemized) for this invoice
|
||||
@@ -1172,7 +989,9 @@ class InvoiceController extends ResourceController
|
||||
'discounts' => $discounts,
|
||||
'additionalChargesTotal' => $additionalChargesTotal,
|
||||
'additionalChargeLines' => $additionalChargeLines,
|
||||
'invoiceLines' => $invoiceLines,
|
||||
'refundsPaidTotal' => $refundsPaidTotal,
|
||||
'ledger' => $ledger,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1326,56 +1145,14 @@ class InvoiceController extends ResourceController
|
||||
];
|
||||
};
|
||||
|
||||
// --- Tuition/registration lines (charges) ---
|
||||
foreach ($registeredKids as $student) {
|
||||
$id = $student['student_id'];
|
||||
$unit = (float)($studentCharges[$id]['unit_fee'] ?? 0.0);
|
||||
$name = $student['student_firstname'] . ' ' . $student['student_lastname'];
|
||||
|
||||
$classSectionName = $this->classSectionModel->getClassSectionNameByClassId($student['grade']);
|
||||
$lowerCaseName = strtolower((string)$classSectionName);
|
||||
$gradeName = ($lowerCaseName === 'kg')
|
||||
? 'in Kindergarten '
|
||||
: (($lowerCaseName === 'youth') ? 'in Youth ' : ('in Grade ' . $classSectionName));
|
||||
|
||||
$dt = $toLocal($invoice['created_at'] ?? null, false);
|
||||
$push($dt, 'Registration of student "' . $name . '" ' . $gradeName, $unit, 'registration');
|
||||
}
|
||||
|
||||
// --- Event charges (charges) ---
|
||||
foreach ($events as $event) {
|
||||
$studentName = 'N/A';
|
||||
if (!empty($event['student_id'])) {
|
||||
foreach ($students as $st) {
|
||||
if (($st['student_id'] ?? null) == $event['student_id']) {
|
||||
$studentName = $st['student_firstname'] . ' ' . $st['student_lastname'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($studentName === 'N/A') {
|
||||
$externalName = trim((string)($event['external_firstname'] ?? '') . ' ' . (string)($event['external_lastname'] ?? ''));
|
||||
if ($externalName !== '') {
|
||||
$studentName = $externalName . ' (external)';
|
||||
}
|
||||
}
|
||||
|
||||
$dt = $toLocal($event['created_at'] ?? null, false);
|
||||
$amount = (float)($event['charged'] ?? 0.0);
|
||||
$eventName = !empty($event['event_name']) ? $event['event_name'] : 'with no name';
|
||||
|
||||
$push($dt, 'Event ' . $eventName . ' charge for "' . $studentName . '"', $amount, 'event');
|
||||
}
|
||||
|
||||
// --- Withdrawn refunds (negative) — only if a non-zero refund figure exists
|
||||
foreach ($withdrawnKids as $student) {
|
||||
$id = $student['student_id'];
|
||||
$ref = (float)($studentCharges[$id]['refund'] ?? 0.0);
|
||||
if ($ref <= 0) { continue; }
|
||||
$name = $student['student_firstname'] . ' ' . $student['student_lastname'];
|
||||
|
||||
$dt = $toLocal($invoice['created_at'] ?? null, false);
|
||||
$push($dt, 'Refund for student "' . $name . '"', -1 * $ref, 'refund');
|
||||
// --- Frozen invoice charge lines. Do not rebuild issued charges from current enrollment/events.
|
||||
foreach (($data['invoiceLines'] ?? []) as $line) {
|
||||
$dt = $toLocal($line['created_at'] ?? ($invoice['created_at'] ?? null), true);
|
||||
$amount = ((int)($line['line_amount_cents'] ?? 0)) / 100;
|
||||
$type = (string)($line['line_type'] ?? 'other');
|
||||
$category = str_contains($type, 'event') ? 'event'
|
||||
: (str_contains($type, 'additional') ? 'additional' : 'registration');
|
||||
$push($dt, (string)($line['description'] ?? 'Invoice line'), $amount, $category);
|
||||
}
|
||||
|
||||
// --- Payments (negative) — stored in local time
|
||||
@@ -1386,17 +1163,11 @@ class InvoiceController extends ResourceController
|
||||
$push($dt, 'Payment (' . ($payment['payment_method'] ?? 'Payment') . ')', -1 * $amount, 'payment');
|
||||
}
|
||||
|
||||
// --- Additional charges (already signed: deduct < 0, add > 0)
|
||||
foreach ($additionalChargeLines as $l) {
|
||||
$dt = $toLocal($l['date'] ?? null, false);
|
||||
$desc = (string)($l['description'] ?? 'Additional Charge');
|
||||
$amt = (float)($l['amount'] ?? 0.0);
|
||||
$push($dt, $desc, $amt, 'additional');
|
||||
}
|
||||
|
||||
// --- Discounts (negative) integrated into the timeline
|
||||
foreach (($discounts ?? []) as $discount) {
|
||||
$amt = (float)($discount['discount_amount'] ?? 0.0);
|
||||
$amt = isset($discount['applied_discount_cents']) && $discount['applied_discount_cents'] !== null
|
||||
? ((int)$discount['applied_discount_cents']) / 100
|
||||
: (float)($discount['discount_amount'] ?? 0.0);
|
||||
$totalDiscount += $amt;
|
||||
|
||||
$dt = $toLocal($discount['used_at'] ?? ($invoice['created_at'] ?? null), false);
|
||||
@@ -1445,33 +1216,13 @@ class InvoiceController extends ResourceController
|
||||
}
|
||||
|
||||
// ======== SUMMARY (bottom) ========
|
||||
// Compute total charges from components (tuition + event charges + additional charges)
|
||||
// to ensure the PDF always reflects all elements accurately.
|
||||
$tuitionSubtotal = 0.0;
|
||||
foreach (($studentCharges ?? []) as $sc) {
|
||||
$tuitionSubtotal += (float)($sc['unit_fee'] ?? 0.0);
|
||||
}
|
||||
|
||||
$eventSubtotal = 0.0;
|
||||
foreach (($events ?? []) as $ev) {
|
||||
$eventSubtotal += (float)($ev['charged'] ?? 0.0);
|
||||
}
|
||||
|
||||
$additionalSubtotal = (float)($additionalChargesTotal ?? 0.0);
|
||||
|
||||
$totalRefund = (float)($data['refundsPaidTotal'] ?? 0.0);
|
||||
$totalAmount = round($tuitionSubtotal + $eventSubtotal + $additionalSubtotal, 2);
|
||||
$calcBalance = $totalAmount - $totalPaid - $totalDiscount - $totalRefund;
|
||||
// Prefer computed balance in PDF to avoid stale DB values
|
||||
$totalBalance = $calcBalance;
|
||||
|
||||
// Display rule: if negative, show as Credit (Overpayment) and clamp balance due to 0.00
|
||||
$displayBalance = $totalBalance;
|
||||
$creditOverpay = 0.0;
|
||||
if ($displayBalance < -0.00001) {
|
||||
$creditOverpay = abs($displayBalance);
|
||||
$displayBalance = 0.00;
|
||||
}
|
||||
$ledger = $data['ledger'] ?? [];
|
||||
$totalAmount = (float) ($ledger['total_amount'] ?? 0.0);
|
||||
$totalDiscount = (float) ($ledger['discount_total'] ?? $totalDiscount);
|
||||
$totalPaid = (float) ($ledger['paid_amount'] ?? $totalPaid);
|
||||
$totalRefund = (float) ($ledger['refund_paid_total'] ?? 0.0);
|
||||
$displayBalance = (float) ($ledger['balance'] ?? 0.0);
|
||||
$creditOverpay = (float) ($ledger['customer_credit'] ?? 0.0);
|
||||
|
||||
$pdf->Ln(5);
|
||||
$labelWidth = 165;
|
||||
@@ -1724,7 +1475,7 @@ private function getGradeLevel($grade): array
|
||||
'issue_date' => $this->request->getPost('issue_date'),
|
||||
'refund_issue_date' => $this->request->getPost('refund_issue_date'),
|
||||
'due_date' => $this->request->getPost('due_date'),
|
||||
'status' => 'Unpaid',
|
||||
'status' => FinancialStatus::INVOICE_UNPAID,
|
||||
'description' => $this->request->getPost('description'),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user