fix the enrollement-carryover balance
This commit is contained in:
@@ -1064,13 +1064,66 @@ class InvoiceController extends ResourceController
|
||||
$description = $this->invoiceLedgerService->carryForwardDisplayDescription($invoice);
|
||||
}
|
||||
|
||||
$ledger = null;
|
||||
if ($invoiceId !== null && $invoiceId > 0) {
|
||||
try {
|
||||
$ledger = $isCarryForward
|
||||
? $this->invoiceLedgerService->storedInvoiceLedger($invoiceId)
|
||||
: $this->invoiceLedgerService->calculateInvoice($invoiceId);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Unable to calculate invoice management projection for invoice {id}: {message}', [
|
||||
'id' => $invoiceId,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$invoiceAmount = $ledger !== null
|
||||
? (float) ($ledger['total_amount'] ?? 0)
|
||||
: ($invoice !== null ? (float) ($invoice['total_amount'] ?? 0) : 0.0);
|
||||
|
||||
if (! $isCarryForward && $invoice !== null) {
|
||||
$snapshotTuition = array_reduce(
|
||||
array_merge($enrolledKids, $withdrawnKids),
|
||||
static fn (float $sum, array $kid): float => $sum + (float)($kid['tuition_fee'] ?? 0.0),
|
||||
0.0
|
||||
);
|
||||
|
||||
if (abs($snapshotTuition) > 0.00001) {
|
||||
$eventTotal = array_reduce(
|
||||
$this->eventChargesForInvoice($invoice),
|
||||
static fn (float $sum, array $charge): float => $sum + (float)($charge['charged'] ?? 0.0),
|
||||
0.0
|
||||
);
|
||||
$additionalRows = $this->additionalChargeModel
|
||||
->select('charge_type, amount')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->where('status', FinancialStatus::ADDITIONAL_CHARGE_APPLIED)
|
||||
->findAll();
|
||||
$additionalTotal = array_reduce(
|
||||
$additionalRows,
|
||||
static function (float $sum, array $charge): float {
|
||||
$signedAmount = InvoiceLedgerService::signedAdditionalChargeAmount($charge);
|
||||
// The management column is gross charges. Deductions are
|
||||
// applied to Balance Due but are not themselves charges.
|
||||
return $signedAmount > 0 ? $sum + $signedAmount : $sum;
|
||||
},
|
||||
0.0
|
||||
);
|
||||
|
||||
$invoiceAmount = round($snapshotTuition + $eventTotal + $additionalTotal, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'parent_name' => trim((string) ($parent['firstname'] ?? '') . ' ' . (string) ($parent['lastname'] ?? '')),
|
||||
'parent_id' => $parentId,
|
||||
'enrolledKids' => $enrolledKids,
|
||||
'withdrawnKids' => $withdrawnKids,
|
||||
'invoice_amount' => $invoice !== null ? (float) ($invoice['total_amount'] ?? 0) : 0.0,
|
||||
'invoice_balance' => $invoice !== null ? (float) ($invoice['balance'] ?? 0) : 0.0,
|
||||
'invoice_amount' => $invoiceAmount,
|
||||
'invoice_balance' => $ledger !== null
|
||||
? (float) ($ledger['balance'] ?? 0)
|
||||
: ($invoice !== null ? (float) ($invoice['balance'] ?? 0) : 0.0),
|
||||
'refund_amount' => (float) ($refundSummary['amount'] ?? 0.0),
|
||||
'refund_details' => $refundSummary['details'] ?? [],
|
||||
'last_updated' => $invoice['updated_at'] ?? null,
|
||||
@@ -1078,7 +1131,9 @@ class InvoiceController extends ResourceController
|
||||
'invoice_id' => $invoiceId,
|
||||
'invoice_number' => $invoice !== null ? (string) ($invoice['invoice_number'] ?? '') : '',
|
||||
'invoice_description' => $description,
|
||||
'invoice_status' => $invoice !== null ? (string) ($invoice['status'] ?? '') : '',
|
||||
'invoice_status' => $ledger !== null
|
||||
? (string) ($ledger['status'] ?? '')
|
||||
: ($invoice !== null ? (string) ($invoice['status'] ?? '') : ''),
|
||||
'is_carry_forward' => $isCarryForward,
|
||||
];
|
||||
}
|
||||
@@ -1352,7 +1407,10 @@ class InvoiceController extends ResourceController
|
||||
return ['error' => "Parent associated with the invoice was not found."];
|
||||
}
|
||||
|
||||
$ledger = $this->invoiceLedgerService->storedInvoiceLedger((int) $invoiceId);
|
||||
// Build the PDF from the canonical calculation so its summary matches the
|
||||
// itemized tuition, event, additional-charge, and payment rows. Stored
|
||||
// projections can be stale until the next write-side recalculation.
|
||||
$ledger = $this->invoiceLedgerService->calculateInvoice((int) $invoiceId);
|
||||
$invoiceLines = [];
|
||||
|
||||
$registeredKids = [];
|
||||
@@ -1409,12 +1467,12 @@ class InvoiceController extends ResourceController
|
||||
/* ============================================================
|
||||
* ADDITIONAL CHARGES (itemized) for this invoice
|
||||
* - uses the additional_charges table for line items
|
||||
* - uses invoice.additional_charge as the authoritative total (Strategy B)
|
||||
* - includes only applied rows, matching InvoiceLedgerService
|
||||
* ============================================================ */
|
||||
$acRows = $this->additionalChargeModel
|
||||
->select('id, charge_type, title, description, amount, due_date, status, created_at')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->where('status !=', 'void')
|
||||
->where('status', FinancialStatus::ADDITIONAL_CHARGE_APPLIED)
|
||||
->orderBy('created_at', 'ASC')
|
||||
->orderBy('id', 'ASC')
|
||||
->findAll();
|
||||
@@ -1423,26 +1481,21 @@ class InvoiceController extends ResourceController
|
||||
$additionalChargesTotal = 0.0;
|
||||
|
||||
foreach ($acRows as $ac) {
|
||||
$signed = (float)($ac['amount'] ?? 0);
|
||||
$signed = InvoiceLedgerService::signedAdditionalChargeAmount($ac);
|
||||
$ctype = strtolower((string)($ac['charge_type'] ?? ''));
|
||||
|
||||
if (in_array($ctype, ['deduct'], true) && $signed > 0) {
|
||||
$signed = -$signed;
|
||||
} elseif (in_array($ctype, ['add'], true) && $signed < 0) {
|
||||
$signed = abs($signed);
|
||||
}
|
||||
|
||||
$lineDate = !empty($ac['created_at'])
|
||||
? date('Y-m-d', strtotime($ac['created_at']))
|
||||
: (!empty($invoice['created_at']) ? local_date($invoice['created_at'], 'Y-m-d') : local_date(utc_now(), 'Y-m-d'));
|
||||
|
||||
$typeLabel = in_array($ctype, ['deduct'], true) ? 'Deduct' : 'Add';
|
||||
$title = ''; //trim((string)($ac['title'] ?? 'Additional Charge'));
|
||||
$desc = $typeLabel . ': ';
|
||||
|
||||
if (!empty($ac['description'])) {
|
||||
$desc = $ac['description'];
|
||||
$typeLabel = $signed < 0 ? 'Deduct' : 'Add';
|
||||
$title = trim((string)($ac['title'] ?? ''));
|
||||
$description = trim((string)($ac['description'] ?? ''));
|
||||
$desc = $title !== '' ? $title : 'Additional charge';
|
||||
if ($description !== '' && $description !== $title) {
|
||||
$desc .= ' - ' . $description;
|
||||
}
|
||||
$desc = $typeLabel . ': ' . $desc;
|
||||
|
||||
$additionalChargesTotal += $signed;
|
||||
|
||||
@@ -1912,6 +1965,20 @@ class InvoiceController extends ResourceController
|
||||
}
|
||||
}
|
||||
|
||||
// Additional charges are stored separately from the base invoice rows. They
|
||||
// must be added explicitly to the PDF timeline; previously they were only
|
||||
// used to calculate the fallback tuition amount and summary subtotal.
|
||||
foreach ($additionalChargeLines as $line) {
|
||||
$amount = (float)($line['amount'] ?? 0.0);
|
||||
if (abs($amount) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dt = $toLocal($line['date'] ?? ($invoice['created_at'] ?? null), false);
|
||||
$description = trim((string)($line['description'] ?? 'Additional charge'));
|
||||
$push($dt, $description !== '' ? $description : 'Additional charge', $amount, 'additional');
|
||||
}
|
||||
|
||||
// --- Payments (negative) — stored in local time
|
||||
foreach ($payments as $payment) {
|
||||
$dt = $toLocal($payment['payment_date'] ?? null, false /* local */);
|
||||
@@ -1996,12 +2063,31 @@ class InvoiceController extends ResourceController
|
||||
|
||||
// ======== SUMMARY (bottom) ========
|
||||
$ledger = $data['ledger'] ?? [];
|
||||
$totalAmount = (float) ($ledger['total_amount'] ?? 0.0);
|
||||
$chargeCategories = ['registration', 'event', 'additional', 'other'];
|
||||
$totalAmount = round(array_reduce(
|
||||
$transactions,
|
||||
static function (float $sum, array $transaction) use ($chargeCategories): float {
|
||||
$amount = (float)($transaction['amount'] ?? 0.0);
|
||||
$isCharge = in_array((string)($transaction['cat'] ?? 'other'), $chargeCategories, true);
|
||||
|
||||
// Total Charges is gross: only positive charge rows belong here.
|
||||
// Negative adjustments remain visible and reduce Balance Due.
|
||||
return $isCharge && $amount > 0 ? $sum + $amount : $sum;
|
||||
},
|
||||
0.0
|
||||
), 2);
|
||||
$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);
|
||||
// The PDF balance must reconcile exactly to its visible rows: positive
|
||||
// amounts add to the balance and negative amounts deduct from it.
|
||||
$signedRowBalance = round(array_reduce(
|
||||
$transactions,
|
||||
static fn (float $sum, array $transaction): float => $sum + (float)($transaction['amount'] ?? 0.0),
|
||||
0.0
|
||||
), 2);
|
||||
$displayBalance = max(0.0, $signedRowBalance);
|
||||
$creditOverpay = max(0.0, -$signedRowBalance);
|
||||
|
||||
$pdf->Ln(5);
|
||||
$labelWidth = 165;
|
||||
|
||||
Reference in New Issue
Block a user