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
+140 -340
View File
@@ -9,12 +9,55 @@ use App\Models\RefundModel;
use App\Models\ExpenseModel;
use App\Models\ReimbursementModel;
use App\Models\UserModel;
use App\Libraries\FinancialStatus;
use App\Libraries\InvoiceLedgerService;
require_once APPPATH . 'ThirdParty/fpdf/fpdf.php';
use FPDF;
class FinancialController extends BaseController
{
private ?InvoiceLedgerService $invoiceLedgerService = null;
private function invoiceLedger(): InvoiceLedgerService
{
if ($this->invoiceLedgerService === null) {
$this->invoiceLedgerService = new InvoiceLedgerService();
}
return $this->invoiceLedgerService;
}
private function ledgerProjectionForInvoice(int $invoiceId): array
{
try {
return $this->invoiceLedger()->calculateInvoice($invoiceId);
} catch (\Throwable $e) {
log_message('error', 'Invoice ledger projection failed for invoice ' . $invoiceId . ': ' . $e->getMessage());
return [
'total_amount' => '0.00',
'paid_amount' => '0.00',
'discount_total' => '0.00',
'refund_paid_total' => '0.00',
'balance' => '0.00',
'customer_credit' => '0.00',
'status' => FinancialStatus::INVOICE_UNPAID,
];
}
}
private function ledgerProjectionMap(array $invoiceIds): array
{
$map = [];
foreach (array_values(array_unique(array_map('intval', $invoiceIds))) as $invoiceId) {
if ($invoiceId > 0) {
$map[$invoiceId] = $this->ledgerProjectionForInvoice($invoiceId);
}
}
return $map;
}
private function wantsJson(): bool
{
$accept = strtolower((string)($this->request->getHeaderLine('Accept') ?? ''));
@@ -157,6 +200,7 @@ public function financialReport()
$id = (int)($inv['id'] ?? 0);
return $id > 0 ? $id : null;
}, $invoices))));
$ledgerByInvoice = $this->ledgerProjectionMap($invoiceIds);
// Helper to build a fresh, filtered PaymentModel each time (so filters don't get lost between queries)
$buildPaymentModel = function () use ($schoolYear, $dateFrom, $dateTo) {
@@ -195,18 +239,14 @@ public function financialReport()
return $qb;
};
// === Payments aggregated by invoice_id (for the "Paid" column) ===
$paymentsQuery = $applyPaymentFilters($buildPaymentModel());
if (!empty($invoiceIds)) {
$paymentsQuery->whereIn('invoice_id', $invoiceIds);
} else {
$paymentsQuery->where('invoice_id', -1);
// === Ledger-derived invoice totals (for Paid/Refund/Discount/Balance/Status columns) ===
$payments = [];
foreach ($ledgerByInvoice as $invoiceId => $ledger) {
$payments[] = [
'invoice_id' => $invoiceId,
'paid_amount' => (float) ($ledger['paid_amount'] ?? 0),
];
}
$payments = $paymentsQuery
->select('invoice_id, SUM(paid_amount) AS paid_amount')
->where('invoice_id IS NOT NULL')
->groupBy('invoice_id')
->findAll();
// === Per-invoice breakdown by normalized method (cash/credit/check) ===
// Normalization rules:
@@ -260,21 +300,44 @@ public function financialReport()
'total_credit' => (float)($paymentTotalsRow['total_credit'] ?? 0),
];
// === Refunds (grouped) ===
$refunds = $refundModel
->select('invoice_id, school_year, SUM(refund_paid_amount) AS total_refunded')
->where('invoice_id IS NOT NULL')
->whereIn('status', ['Partial', 'Paid'])
->groupBy(['invoice_id', 'school_year'])
->findAll();
// === Ledger-derived refunds and discounts ===
$refunds = [];
$discounts = [];
foreach ($ledgerByInvoice as $invoiceId => $ledger) {
$invoiceSchoolYear = '';
foreach ($invoices as $invoiceRow) {
if ((int)($invoiceRow['id'] ?? 0) === (int)$invoiceId) {
$invoiceSchoolYear = (string)($invoiceRow['school_year'] ?? '');
break;
}
}
$refunds[] = [
'invoice_id' => $invoiceId,
'school_year' => $invoiceSchoolYear,
'total_refunded' => (float) ($ledger['refund_paid_total'] ?? 0),
];
$discounts[] = [
'invoice_id' => $invoiceId,
'school_year' => $invoiceSchoolYear,
'discount_amount' => (float) ($ledger['discount_total'] ?? 0),
];
}
// === Discounts (grouped) ===
$discounts = $discountModel
->select('invoice_id, school_year, SUM(discount_amount) AS discount_amount')
->where('invoice_id IS NOT NULL')
->groupBy(['invoice_id', 'school_year'])
->get()
->getResultArray();
foreach ($invoices as &$invoiceRow) {
$invoiceId = (int)($invoiceRow['id'] ?? 0);
$ledger = $ledgerByInvoice[$invoiceId] ?? null;
if ($ledger === null) {
continue;
}
$invoiceRow['total_amount'] = (float)($ledger['total_amount'] ?? 0);
$invoiceRow['paid_amount'] = (float)($ledger['paid_amount'] ?? 0);
$invoiceRow['discount'] = (float)($ledger['discount_total'] ?? 0);
$invoiceRow['refund_paid'] = (float)($ledger['refund_paid_total'] ?? 0);
$invoiceRow['balance'] = (float)($ledger['balance'] ?? 0);
$invoiceRow['customer_credit'] = (float)($ledger['customer_credit'] ?? 0);
$invoiceRow['status'] = (string)($ledger['status'] ?? ($invoiceRow['status'] ?? ''));
}
unset($invoiceRow);
// === Expenses ===
$expenses = $expenseModel
@@ -583,77 +646,27 @@ public function financialReport()
$invoiceRows = $invoiceRows->orderBy('invoices.id', 'ASC')->get()->getResultArray();
$invoiceIds = array_values(array_filter(array_map(static fn($row) => (int)($row['id'] ?? 0), $invoiceRows)));
$paidByInvoice = [];
$discountByInvoice = [];
$refundByInvoice = [];
if (!empty($invoiceIds)) {
$paidRows = $db->table('payments')
->select('invoice_id, COALESCE(SUM(paid_amount),0) AS total_paid')
->whereIn('invoice_id', $invoiceIds)
->groupStart()
->whereNotIn('status', $paymentExclude)
->orWhere('status IS NULL', null, false)
->groupEnd();
if (!empty($schoolYear)) {
$paidRows->where('school_year', $schoolYear);
}
if (!empty($invoiceDateFrom)) {
$paidRows->where('DATE(payment_date) >=', $invoiceDateFrom);
}
if (!empty($invoiceDateTo)) {
$paidRows->where('DATE(payment_date) <=', $invoiceDateTo);
}
foreach ($paidRows->groupBy('invoice_id')->get()->getResultArray() as $row) {
$paidByInvoice[(int)($row['invoice_id'] ?? 0)] = (float)($row['total_paid'] ?? 0);
}
$discountRows = $db->table('discount_usages')
->select('invoice_id, COALESCE(SUM(discount_amount),0) AS total_discount')
->whereIn('invoice_id', $invoiceIds);
if (!empty($dateFrom)) {
$discountRows->where('DATE(COALESCE(used_at, created_at)) >=', $dateFrom);
}
if (!empty($dateTo)) {
$discountRows->where('DATE(COALESCE(used_at, created_at)) <=', $dateTo);
}
foreach ($discountRows->groupBy('invoice_id')->get()->getResultArray() as $row) {
$discountByInvoice[(int)($row['invoice_id'] ?? 0)] = (float)($row['total_discount'] ?? 0);
}
$refundRows = $db->table('refunds')
->select('invoice_id, COALESCE(SUM(refund_paid_amount),0) AS total_refund')
->whereIn('invoice_id', $invoiceIds)
->whereIn('status', ['Partial', 'Paid']);
if (!empty($dateFrom)) {
$refundRows->where('DATE(COALESCE(refunded_at, created_at)) >=', $dateFrom);
}
if (!empty($dateTo)) {
$refundRows->where('DATE(COALESCE(refunded_at, created_at)) <=', $dateTo);
}
foreach ($refundRows->groupBy('invoice_id')->get()->getResultArray() as $row) {
$refundByInvoice[(int)($row['invoice_id'] ?? 0)] = (float)($row['total_refund'] ?? 0);
}
}
$ledgerByInvoice = $this->ledgerProjectionMap($invoiceIds);
$invoices = [];
foreach ($invoiceRows as $row) {
$iid = (int)($row['id'] ?? 0);
$paid = (float)($paidByInvoice[$iid] ?? 0);
$disc = (float)($discountByInvoice[$iid] ?? 0);
$refund = (float)($refundByInvoice[$iid] ?? 0);
$balance = round((float)($row['total_amount'] ?? 0) - $disc - $refund - $paid, 2);
$ledger = $ledgerByInvoice[$iid] ?? [];
$paid = (float)($ledger['paid_amount'] ?? 0);
$disc = (float)($ledger['discount_total'] ?? 0);
$refund = (float)($ledger['refund_paid_total'] ?? 0);
$balance = (float)($ledger['balance'] ?? 0);
$invoices[] = [
'Invoice #' => (string)($row['invoice_number'] ?? ''),
'Parent' => trim((string)($row['parent_name'] ?? '')),
'Issue Date' => (string)($row['issue_date'] ?? ''),
'Due Date' => (string)($row['due_date'] ?? ''),
'Gross Charges' => (float)($row['total_amount'] ?? 0),
'Gross Charges' => (float)($ledger['total_amount'] ?? 0),
'Discounts' => $disc,
'Refunds' => $refund,
'Paid' => $paid,
'Balance' => $balance,
'Status' => (string)($row['status'] ?? ''),
'Status' => (string)($ledger['status'] ?? ($row['status'] ?? '')),
];
}
@@ -992,10 +1005,6 @@ public function financialReport()
->get()->getResultArray();
$byParent = [];
$paymentsTbl = 'payments';
$hasStatus = $db->fieldExists('status', $paymentsTbl);
$hasVoid = $db->fieldExists('is_void', $paymentsTbl);
foreach ($invRows as $r) {
$iid = (int)($r['id'] ?? 0);
$pid = (int)($r['parent_id'] ?? 0);
@@ -1003,37 +1012,11 @@ public function financialReport()
continue;
}
$qb = $db->table('payments')->select('COALESCE(SUM(paid_amount),0) AS tot')->where('invoice_id', $iid);
if ($hasStatus) {
$qb->groupStart()
->whereNotIn('status', ['void','voided','refunded','failed','chargeback','declined','reversed','canceled','cancelled'])
->orWhere('status IS NULL', null, false)
->groupEnd();
}
if ($hasVoid) {
$qb->groupStart()
->where('is_void', 0)
->orWhere('is_void IS NULL', null, false)
->groupEnd();
}
$paidRow = $qb->get()->getRowArray();
$paidSum = (float)($paidRow['tot'] ?? 0);
$discRow = $db->table('discount_usages')
->select('COALESCE(SUM(discount_amount),0) AS tot')
->where('invoice_id', $iid)
->get()->getRowArray();
$discSum = (float)($discRow['tot'] ?? 0);
$refRow = $db->table('refunds')
->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
->where('invoice_id', $iid)
->whereIn('status', ['Partial','Paid'])
->get()->getRowArray();
$refSum = (float)($refRow['tot'] ?? 0);
$total = (float)($r['total_amount'] ?? 0);
$balance = max(0.0, round($total - $discSum - $paidSum - $refSum, 2));
$ledger = $this->ledgerProjectionForInvoice($iid);
$paidSum = (float)($ledger['paid_amount'] ?? 0);
$discSum = (float)($ledger['discount_total'] ?? 0);
$total = (float)($ledger['total_amount'] ?? 0);
$balance = (float)($ledger['balance'] ?? 0);
if (!isset($byParent[$pid])) {
$byParent[$pid] = [
@@ -1293,8 +1276,10 @@ public function financialReport()
->findAll();
$paymentsMap = [];
foreach ($payments as $payment) {
$paymentsMap[(int)($payment['invoice_id'] ?? 0)] = (float)($payment['paid_amount'] ?? 0);
$invoiceIdsForExport = array_values(array_filter(array_map(static fn($row) => (int)($row['id'] ?? 0), $invoices)));
$ledgerByInvoice = $this->ledgerProjectionMap($invoiceIdsForExport);
foreach ($ledgerByInvoice as $invoiceId => $ledger) {
$paymentsMap[(int)$invoiceId] = (float)($ledger['paid_amount'] ?? 0);
}
$paymentBreakdownRows = $applyPaymentFilters($buildPaymentModel())
@@ -1350,47 +1335,11 @@ public function financialReport()
}
$reimbursements = $reimbBuilder->groupBy('status')->findAll();
// Refunds (grouped)
if (!empty($schoolYear)) {
$refundModel->where('school_year', $schoolYear);
}
if ($hasFrom) {
$refundModel->where('DATE(COALESCE(refunded_at, created_at)) >=', $dateFrom);
}
if ($hasTo) {
$refundModel->where('DATE(COALESCE(refunded_at, created_at)) <=', $dateTo);
}
$refundsData = $refundModel
->select('invoice_id, SUM(refund_paid_amount) AS total_refunded')
->where('invoice_id IS NOT NULL')
->whereIn('status', ['Partial', 'Paid'])
->groupBy('invoice_id')
->findAll();
$refunds = [];
foreach ($refundsData as $refund) {
$refunds[$refund['invoice_id']] = $refund['total_refunded'];
}
$discountBuilder = $discountModel;
if (!empty($schoolYear)) {
$discountBuilder->where('school_year', $schoolYear);
}
if ($hasFrom) {
$discountBuilder->where('DATE(COALESCE(used_at, created_at)) >=', $dateFrom);
}
if ($hasTo) {
$discountBuilder->where('DATE(COALESCE(used_at, created_at)) <=', $dateTo);
}
$discountsData = $discountBuilder
->select('invoice_id, SUM(discount_amount) AS discount_amount')
->where('invoice_id IS NOT NULL')
->groupBy('invoice_id')
->get()
->getResultArray();
$discounts = [];
foreach ($discountsData as $disc) {
$discounts[$disc['invoice_id']] = $disc['discount_amount'];
foreach ($ledgerByInvoice as $invoiceId => $ledger) {
$refunds[(int)$invoiceId] = (float)($ledger['refund_paid_total'] ?? 0);
$discounts[(int)$invoiceId] = (float)($ledger['discount_total'] ?? 0);
}
$filename = 'financial_report_' . date('Ymd_His') . '.csv';
@@ -1419,10 +1368,10 @@ public function financialReport()
$check = (float)($bd['check'] ?? 0);
$refunded = (float)($refunds[$invoiceId] ?? 0);
$discount = (float)($discounts[$invoiceId] ?? 0);
$total = (float)($inv['total_amount'] ?? 0);
$balance = $total - $paid - $discount - $refunded;
if ($balance < 0) $balance = 0.0;
$status = ($balance === 0.0) ? 'Paid' : 'Unpaid';
$ledger = $ledgerByInvoice[$invoiceId] ?? [];
$total = (float)($ledger['total_amount'] ?? 0);
$balance = (float)($ledger['balance'] ?? 0);
$status = (string)($ledger['status'] ?? ($inv['status'] ?? ''));
fputcsv($out, [
$inv['invoice_number'],
@@ -1660,11 +1609,21 @@ public function financialReport()
$invoiceBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <=', $invoiceDateTo);
}
$invoices = $invoiceBuilder->findAll();
$totalCharges = array_sum(array_column($invoices, 'total_amount'));
$invoiceIds = array_values(array_unique(array_filter(array_map(static function ($inv) {
$id = (int)($inv['id'] ?? 0);
return $id > 0 ? $id : null;
}, $invoices))));
$ledgerByInvoice = $this->ledgerProjectionMap($invoiceIds);
$totalCharges = 0.0;
$totalPaid = 0.0;
$totalDiscounts = 0.0;
$totalRefunds = 0.0;
foreach ($ledgerByInvoice as $ledger) {
$totalCharges += (float)($ledger['total_amount'] ?? 0);
$totalPaid += (float)($ledger['paid_amount'] ?? 0);
$totalDiscounts += (float)($ledger['discount_total'] ?? 0);
$totalRefunds += (float)($ledger['refund_paid_total'] ?? 0);
}
// === Additional Charges ===
$hasExplicitDates = !empty($dateFrom) || !empty($dateTo);
@@ -1714,98 +1673,6 @@ public function financialReport()
$totalCharges += $extraChargesUnapplied;
// === Payments: Total Paid ===
$paymentBuilder = $paymentModel->where('school_year', $schoolYear);
if (!empty($invoiceDateFrom)) {
$paymentBuilder->where('DATE(payment_date) >=', $invoiceDateFrom);
}
if (!empty($invoiceDateTo)) {
$paymentBuilder->where('DATE(payment_date) <=', $invoiceDateTo);
}
$payHasStatus = $db->fieldExists('status', 'payments');
$payHasVoid = $db->fieldExists('is_void', 'payments');
if ($payHasStatus) {
$paymentBuilder->groupStart()
->whereNotIn('status', ['void','voided','refunded','failed','chargeback','declined','reversed','canceled','cancelled'])
->orWhere('status IS NULL', null, false)
->groupEnd();
}
if ($payHasVoid) {
$paymentBuilder->groupStart()
->where('is_void', 0)
->orWhere('is_void IS NULL', null, false)
->groupEnd();
}
$paymentResult = $paymentBuilder->selectSum('paid_amount')->get()->getRowArray();
$totalPaid = isset($paymentResult['paid_amount']) ? (float) $paymentResult['paid_amount'] : 0.00;
// === Per-invoice paid/discount/refund totals for outstanding balance ===
$paidByInvoice = [];
$discountByInvoice = [];
$refundByInvoice = [];
if (!empty($invoiceIds)) {
$paidRows = $db->table('payments')
->select('invoice_id, COALESCE(SUM(paid_amount),0) AS total_paid')
->whereIn('invoice_id', $invoiceIds);
if (!empty($schoolYear)) {
$paidRows->where('school_year', $schoolYear);
}
if (!empty($invoiceDateFrom)) {
$paidRows->where('DATE(payment_date) >=', $invoiceDateFrom);
}
if (!empty($invoiceDateTo)) {
$paidRows->where('DATE(payment_date) <=', $invoiceDateTo);
}
if ($payHasStatus) {
$paidRows->groupStart()
->whereNotIn('status', ['void','voided','refunded','failed','chargeback','declined','reversed','canceled','cancelled'])
->orWhere('status IS NULL', null, false)
->groupEnd();
}
if ($payHasVoid) {
$paidRows->groupStart()
->where('is_void', 0)
->orWhere('is_void IS NULL', null, false)
->groupEnd();
}
$paidRows = $paidRows->groupBy('invoice_id')->get()->getResultArray();
foreach ($paidRows as $r) {
$iid = (int)($r['invoice_id'] ?? 0);
if ($iid > 0) $paidByInvoice[$iid] = (float)($r['total_paid'] ?? 0);
}
$discRows = $db->table('discount_usages')
->select('invoice_id, COALESCE(SUM(discount_amount),0) AS total_disc')
->whereIn('invoice_id', $invoiceIds);
if (!empty($dateFrom)) {
$discRows->where('DATE(COALESCE(used_at, created_at)) >=', $dateFrom);
}
if (!empty($dateTo)) {
$discRows->where('DATE(COALESCE(used_at, created_at)) <=', $dateTo);
}
$discRows = $discRows->groupBy('invoice_id')->get()->getResultArray();
foreach ($discRows as $r) {
$iid = (int)($r['invoice_id'] ?? 0);
if ($iid > 0) $discountByInvoice[$iid] = (float)($r['total_disc'] ?? 0);
}
$refRows = $db->table('refunds')
->select('invoice_id, COALESCE(SUM(refund_paid_amount),0) AS total_refund')
->whereIn('invoice_id', $invoiceIds)
->whereIn('status', ['Partial', 'Paid']);
if (!empty($dateFrom)) {
$refRows->where('DATE(COALESCE(refunded_at, created_at)) >=', $dateFrom);
}
if (!empty($dateTo)) {
$refRows->where('DATE(COALESCE(refunded_at, created_at)) <=', $dateTo);
}
$refRows = $refRows->groupBy('invoice_id')->get()->getResultArray();
foreach ($refRows as $r) {
$iid = (int)($r['invoice_id'] ?? 0);
if ($iid > 0) $refundByInvoice[$iid] = (float)($r['total_refund'] ?? 0);
}
}
// === Expenses ===
$expenseBuilder = $expenseModel->where('school_year', $schoolYear);
if (!empty($invoiceDateFrom)) {
@@ -1943,39 +1810,6 @@ public function financialReport()
$donationToSchool = $donationExpense + $donationReimb;
$totalReimbursements = max(0.0, $totalReimbursements - $donationReimb);
// === Refunds ===
$refundBuilder = $refundModel
->where('school_year', $schoolYear)
->whereIn('status', ['Partial', 'Paid'])
->where('refund_paid_amount IS NOT NULL');
if (!empty($dateFrom)) {
$refundBuilder->where('DATE(COALESCE(refunded_at, created_at)) >=', $dateFrom);
}
if (!empty($dateTo)) {
$refundBuilder->where('DATE(COALESCE(refunded_at, created_at)) <=', $dateTo);
}
$refundResult = $refundBuilder
->selectSum('refund_paid_amount')
->get()
->getRowArray();
$totalRefunds = isset($refundResult['refund_paid_amount']) ? (float) $refundResult['refund_paid_amount'] : 0.00;
// === Discounts ===
$discountBuilder = $discountModel
->join('invoices', 'invoices.id = discount_usages.invoice_id')
->where('invoices.school_year', $schoolYear);
if (!empty($dateFrom)) {
$discountBuilder->where('DATE(COALESCE(discount_usages.used_at, discount_usages.created_at)) >=', $dateFrom);
}
if (!empty($dateTo)) {
$discountBuilder->where('DATE(COALESCE(discount_usages.used_at, discount_usages.created_at)) <=', $dateTo);
}
$discountResult = $discountBuilder->selectSum('discount_amount')->get()->getRowArray();
$totalDiscounts = isset($discountResult['discount_amount']) ? (float) $discountResult['discount_amount'] : 0.00;
// === Net, Outstanding & Overpayments ===
$overpaymentDetails = [];
$totalUnpaid = 0.0;
@@ -1984,15 +1818,16 @@ public function financialReport()
$iid = (int)($inv['id'] ?? 0);
$pid = (int)($inv['parent_id'] ?? 0);
if ($iid <= 0 || $pid <= 0) continue;
$total = (float)($inv['total_amount'] ?? 0);
$paid = (float)($paidByInvoice[$iid] ?? 0);
$disc = (float)($discountByInvoice[$iid] ?? 0);
$ref = (float)($refundByInvoice[$iid] ?? 0);
$rawBal = round($total - $disc - $paid - $ref, 2);
if ($rawBal > 0.00001) {
$totalUnpaid += $rawBal;
} elseif ($rawBal < -0.00001) {
$credit = abs($rawBal);
$ledger = $ledgerByInvoice[$iid] ?? [];
$total = (float)($ledger['total_amount'] ?? 0);
$paid = (float)($ledger['paid_amount'] ?? 0);
$disc = (float)($ledger['discount_total'] ?? 0);
$ref = (float)($ledger['refund_paid_total'] ?? 0);
$balance = (float)($ledger['balance'] ?? 0);
$credit = (float)($ledger['customer_credit'] ?? 0);
if ($balance > 0.00001) {
$totalUnpaid += $balance;
} elseif ($credit > 0.00001) {
$totalOverpaid += $credit;
$overpaymentDetails[] = [
'type' => 'invoice',
@@ -2005,7 +1840,7 @@ public function financialReport()
'discount_amount' => $disc,
'refund_amount' => $ref,
'paid_amount' => $paid,
'note' => 'Invoice payments/discounts exceed net invoice charges.',
'note' => 'Invoice ledger shows customer credit.',
];
}
}
@@ -2111,9 +1946,7 @@ public function financialReport()
$schoolYears[] = (string)$schoolYear;
}
// Aggregate balances by parent for selected school year
// IMPORTANT: Compute current balance = total - payments - discounts - refundsPaid
// rather than trusting invoices.balance which may become stale.
// Aggregate balances by parent for selected school year from the canonical invoice ledger.
$db = \Config\Database::connect();
$invRows = $db->table('invoices i')
->select('i.id, i.parent_id, i.total_amount, u.firstname, u.lastname, u.email')
@@ -2123,51 +1956,18 @@ public function financialReport()
->orderBy('i.id', 'ASC')
->get()->getResultArray();
// Group by parent and compute balances dynamically
$byParent = [];
$paymentsTbl = 'payments';
$hasStatus = $db->fieldExists('status', $paymentsTbl);
$hasVoid = $db->fieldExists('is_void', $paymentsTbl);
foreach ($invRows as $r) {
$iid = (int)($r['id'] ?? 0);
$pid = (int)($r['parent_id'] ?? 0);
if ($iid <= 0 || $pid <= 0) continue;
// Sum payments for this invoice (exclude void/failed if such columns exist)
$qb = $db->table('payments')->select('COALESCE(SUM(paid_amount),0) AS tot')->where('invoice_id', $iid);
if ($hasStatus) {
$qb->groupStart()
->whereNotIn('status', ['void','voided','refunded','failed','chargeback','declined','reversed','canceled','cancelled'])
->orWhere('status IS NULL', null, false)
->groupEnd();
}
if ($hasVoid) {
$qb->groupStart()
->where('is_void', 0)
->orWhere('is_void IS NULL', null, false)
->groupEnd();
}
$paidRow = $qb->get()->getRowArray();
$paidSum = (float)($paidRow['tot'] ?? 0);
// Sum discounts for this invoice
$discRow = $db->table('discount_usages')
->select('COALESCE(SUM(discount_amount),0) AS tot')
->where('invoice_id', $iid)
->get()->getRowArray();
$discSum = (float)($discRow['tot'] ?? 0);
// Sum refunds PAID for this invoice (Partial/Paid only)
$refRow = $db->table('refunds')
->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
->where('invoice_id', $iid)
->whereIn('status', ['Partial','Paid'])
->get()->getRowArray();
$refSum = (float)($refRow['tot'] ?? 0);
$total = (float)($r['total_amount'] ?? 0);
$balance = max(0.0, round($total - $discSum - $paidSum - $refSum, 2));
$ledger = $this->ledgerProjectionForInvoice($iid);
$paidSum = (float)($ledger['paid_amount'] ?? 0);
$discSum = (float)($ledger['discount_total'] ?? 0);
$total = (float)($ledger['total_amount'] ?? 0);
$balance = (float)($ledger['balance'] ?? 0);
if (!isset($byParent[$pid])) {
$byParent[$pid] = [