fix financials
Tests / PHPUnit (push) Failing after 1m21s

This commit is contained in:
root
2026-07-18 22:57:40 -04:00
parent 068e739408
commit a30c1398a1
61 changed files with 10908 additions and 1775 deletions
+13 -26
View File
@@ -89,20 +89,12 @@
$disc = $discMatches ? reset($discMatches) : null;
$discounted = (float)($disc['discount_amount'] ?? 0);
$totalAmount = (float)($inv['total_amount'] ?? 0);
// Recompute balance for display: total - discount - paid - refunded
$balanceCalc = (float)$totalAmount - (float)$discounted - (float)$paid - (float)$refunded;
if ($balanceCalc < 0) $balanceCalc = 0.0;
$balance = $balanceCalc;
if ($balanceCalc <= 0.00001) {
$status = 'Paid';
$statusClass = 'bg-success';
} elseif ($paid > 0.00001) {
$status = 'Partially Paid';
$statusClass = 'bg-warning text-dark';
} else {
$status = 'Unpaid';
$statusClass = 'bg-danger';
}
$balance = (float)($inv['balance'] ?? 0);
$status = (string)($inv['status'] ?? 'unpaid');
$statusClass = $status === 'paid'
? 'bg-success'
: ($status === 'partially_paid' ? 'bg-warning text-dark' : 'bg-danger');
$statusLabel = str_replace('_', ' ', $status);
?>
<tr>
<td><?= esc($inv['invoice_number']) ?></td>
@@ -125,7 +117,7 @@
<td data-order="<?= $balance ?>">$<?= number_format($balance, 2) ?></td>
<td data-order="<?= $refunded ?>">$<?= number_format($refunded, 2) ?></td>
<td data-order="<?= $discounted ?>">$<?= number_format($discounted, 2) ?></td>
<td data-order="<?= esc($status) ?>"><span class="badge <?= esc($statusClass) ?>"><?= esc($status) ?></span></td>
<td data-order="<?= esc($status) ?>"><span class="badge <?= esc($statusClass) ?>"><?= esc($statusLabel) ?></span></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
@@ -375,17 +367,12 @@
const refunded = refundsMap[inv.id] || 0;
const discounted = discountsMap[inv.id] || 0;
const total = Number(inv.total_amount || 0);
let balance = total - paid - discounted - refunded;
if (!Number.isFinite(balance) || balance < 0) balance = 0;
let status = 'Unpaid';
const balance = Number(inv.balance || 0);
const status = String(inv.status || 'unpaid');
let statusClass = 'bg-danger';
if (balance <= 0.00001) {
status = 'Paid';
statusClass = 'bg-success';
} else if (paid > 0.00001) {
status = 'Partially Paid';
statusClass = 'bg-warning text-dark';
}
if (status === 'paid') statusClass = 'bg-success';
if (status === 'partially_paid') statusClass = 'bg-warning text-dark';
const statusLabel = status.replace(/_/g, ' ');
const tr = document.createElement('tr');
const parentHtml = (inv.parent_id && Number(inv.parent_id) > 0) ?
`<a href="<?= site_url('family') ?>?guardian_id=${encodeURIComponent(inv.parent_id)}" class="text-decoration-none" data-family-guardian-id="${inv.parent_id}">${inv.parent_name||''}</a>` :
@@ -402,7 +389,7 @@
<td data-order="${balance}">${fmt(balance)}</td>
<td data-order="${refunded}">${fmt(refunded)}</td>
<td data-order="${discounted}">${fmt(discounted)}</td>
<td data-order="${status}"><span class="badge ${statusClass}">${status}</span></td>`;
<td data-order="${status}"><span class="badge ${statusClass}">${statusLabel}</span></td>`;
tbody.appendChild(tr);
});
+16 -53
View File
@@ -371,8 +371,7 @@
<td>$<?= number_format((float)$invoice['paid_amount'], 2) ?></td>
<td>$<?= number_format((float)($invoice['discount'] ?? 0), 2) ?></td>
<td>$<?= number_format((float)($invoice['refund_paid'] ?? 0), 2) ?></td>
<?php $tblBal = max(0, (float)($invoice['total_amount'] ?? 0) - (float)($invoice['paid_amount'] ?? 0) - (float)($invoice['discount'] ?? 0) - (float)($invoice['refund_paid'] ?? 0)); ?>
<td>$<?= number_format($tblBal, 2) ?></td>
<td>$<?= number_format((float)($invoice['balance'] ?? 0), 2) ?></td>
<td><?= esc($invoice['status']) ?></td>
<td><?= esc(!empty($invoice['due_date']) ? local_date($invoice['due_date'], 'm-d-Y') : '') ?></td>
</tr>
@@ -397,6 +396,7 @@
<form method="post" action="<?= base_url('payment/manual_pay_update') ?>" id="addPaymentForm" enctype="multipart/form-data">
<?= csrf_field() ?>
<input type="hidden" name="search_term" value="<?= esc($searchTermUsedInSearch ?? '') ?>">
<input type="hidden" name="idempotency_key" value="<?= esc(bin2hex(random_bytes(16))) ?>">
<div class="modal-header">
<h5 class="modal-title" id="addPaymentModalLabel">Add Manual Payment</h5>
@@ -419,17 +419,13 @@
<option
value="<?= esc($invoice['id']) ?>"
data-balance="<?= esc($invoice['balance']) ?>"
data-total="<?= esc($invoice['total_amount'] ?? $invoice['total'] ?? '') ?>"
data-paid="<?= esc($invoice['paid_amount'] ?? 0) ?>"
data-discount="<?= esc($invoice['discount'] ?? 0) ?>"
data-refund="<?= esc($invoice['refund_paid'] ?? 0) ?>"
data-display-total="<?= esc($invoice['display_total'] ?? $invoice['total_amount'] ?? '') ?>"
data-balance-due-cents="<?= (int)($invoice['balance_due_cents'] ?? 0) ?>"
data-customer-credit-cents="<?= (int)($invoice['customer_credit_cents'] ?? 0) ?>"
data-next-installment="<?= (int)($invoice['next_installment'] ?? 1) ?>">
<?php
$uiTotal = (float)($invoice['total_amount'] ?? 0);
$uiPaid = (float)($invoice['paid_amount'] ?? 0);
$uiDisc = (float)($invoice['discount'] ?? 0);
$uiRef = (float)($invoice['refund_paid'] ?? 0);
$uiBal = max(0, $uiTotal - $uiPaid - $uiDisc - $uiRef);
$uiBal = (float)($invoice['balance'] ?? 0);
?>
Invoice <?= esc($invoice['invoice_number']) ?> |
Balance: $<?= number_format($uiBal, 2) ?> |
@@ -615,20 +611,11 @@
function getBalance() {
const opt = currentOpt();
if (!opt) return 0;
// Compute from total - paid FIRST (more reliable if DB balance is stale)
const totRaw = opt.getAttribute('data-total');
const paidRaw = opt.getAttribute('data-paid');
const discRaw = opt.getAttribute('data-discount');
const refRaw = opt.getAttribute('data-refund');
const tot = (totRaw !== null && totRaw !== '') ? parseFloat(totRaw) : NaN;
const paid = (paidRaw !== null && paidRaw !== '') ? parseFloat(paidRaw) : NaN;
const disc = (discRaw !== null && discRaw !== '') ? parseFloat(discRaw) : 0;
const ref = (refRaw !== null && refRaw !== '') ? parseFloat(refRaw) : 0;
if (isFinite(tot) && isFinite(paid)) {
const computed = tot - paid - (isFinite(disc) ? disc : 0) - (isFinite(ref) ? ref : 0);
if (isFinite(computed)) return Math.max(0, computed);
const centsRaw = opt.getAttribute('data-balance-due-cents');
const cents = (centsRaw !== null && centsRaw !== '') ? parseInt(centsRaw, 10) : NaN;
if (isFinite(cents)) {
return Math.max(0, cents / 100);
}
// Fallback to provided balance
const raw = opt.getAttribute('data-balance');
const bal = (raw !== null && raw !== '') ? parseFloat(raw) : NaN;
return isFinite(bal) ? Math.max(0, bal) : 0;
@@ -636,7 +623,7 @@
function getTotal() {
const opt = currentOpt();
const v = opt ? parseFloat(opt.getAttribute('data-total') || '0') : 0;
const v = opt ? parseFloat(opt.getAttribute('data-display-total') || '0') : 0;
return isFinite(v) ? v : 0;
}
@@ -926,25 +913,7 @@
}
const invoiceText = invOpt.text || ('Invoice #' + invOpt.value);
// Derive balance: prefer total - paid - discount - refunds, else fallback to data-balance
let balance = NaN;
if (invOpt) {
const rt = invOpt.getAttribute('data-total');
const rp = invOpt.getAttribute('data-paid');
const rd = invOpt.getAttribute('data-discount');
const rr = invOpt.getAttribute('data-refund');
const t = (rt !== null && rt !== '') ? parseFloat(rt) : NaN;
const p = (rp !== null && rp !== '') ? parseFloat(rp) : NaN;
const d = (rd !== null && rd !== '') ? parseFloat(rd) : 0;
const r = (rr !== null && rr !== '') ? parseFloat(rr) : 0;
if (isFinite(t) && isFinite(p)) {
balance = t - p - (isFinite(d) ? d : 0) - (isFinite(r) ? r : 0);
}
if (!isFinite(balance)) {
const rb = invOpt.getAttribute('data-balance');
balance = (rb !== null && rb !== '') ? parseFloat(rb) : NaN;
}
}
const balance = getBalance();
const nextInst = invOpt.dataset?.nextInstallment || '';
const instCount = $instCount() ? ($instCount().value || '') : '';
const instAmt = $instAmt() ? ($instAmt().value || '') : '';
@@ -1131,19 +1100,13 @@
// Normalize initial state
const inv = $invoice();
if (inv && inv.options.length > 0) {
// Prefer the first invoice whose (total - paid) > 0
// Prefer the first invoice with a backend-provided amount due.
let chosen = inv.selectedIndex;
for (let i = 0; i < inv.options.length; i++) {
const o = inv.options[i];
const rt = o.getAttribute('data-total');
const rp = o.getAttribute('data-paid');
const rd = o.getAttribute('data-discount');
const rr = o.getAttribute('data-refund');
const t = (rt !== null && rt !== '') ? parseFloat(rt) : NaN;
const p = (rp !== null && rp !== '') ? parseFloat(rp) : NaN;
const d = (rd !== null && rd !== '') ? parseFloat(rd) : 0;
const r = (rr !== null && rr !== '') ? parseFloat(rr) : 0;
let b = (isFinite(t) && isFinite(p)) ? (t - p - (isFinite(d) ? d : 0) - (isFinite(r) ? r : 0)) : NaN;
const centsRaw = o.getAttribute('data-balance-due-cents');
const cents = (centsRaw !== null && centsRaw !== '') ? parseInt(centsRaw, 10) : NaN;
let b = isFinite(cents) ? cents / 100 : NaN;
if (!isFinite(b)) {
const rb = o.getAttribute('data-balance');
b = (rb !== null && rb !== '') ? parseFloat(rb) : NaN;