fix the enrollement-carryover balance
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 49s
Tests / PHPUnit (push) Successful in 1m21s

This commit is contained in:
root
2026-09-09 21:40:01 -04:00
parent 2d5b151234
commit 36e8ffe56d
12 changed files with 1365 additions and 39 deletions
+108 -22
View File
@@ -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;