add projet

This commit is contained in:
root
2026-03-05 12:29:37 -05:00
parent 8d1eef8ba8
commit 23b7db1107
9109 changed files with 1106501 additions and 73 deletions
+724
View File
@@ -0,0 +1,724 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\DiscountUsage;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Reimbursement;
use App\Models\Refund;
use App\Models\Configuration;
use App\Models\AdditionalCharge;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FinancialController extends BaseApiController
{
protected Invoice $invoice;
protected Payment $payment;
protected Refund $refund;
protected Expense $expense;
protected Reimbursement $reimbursement;
protected DiscountUsage $discount;
protected Configuration $config;
protected AdditionalCharge $additionalCharge;
// Special recipients for donations (Masjid/Donation reimbursements)
protected const SPECIAL_RECIPIENTS = [
// Add recipient IDs here if needed
// Example: 1 => 'Masjid',
// 2 => 'Donation',
];
public function __construct()
{
parent::__construct();
$this->invoice = model(Invoice::class);
$this->payment = model(Payment::class);
$this->refund = model(Refund::class);
$this->expense = model(Expense::class);
$this->reimbursement = model(Reimbursement::class);
$this->discount = model(DiscountUsage::class);
$this->config = model(Configuration::class);
$this->additionalCharge = model(AdditionalCharge::class);
}
/**
* Detailed financial report with invoices, payments, payment breakdown, refunds, expenses, reimbursements, discounts
*/
public function report()
{
$dateFrom = $this->request->getGet('date_from');
$dateTo = $this->request->getGet('date_to');
$schoolYear = $this->request->getGet('school_year');
try {
$db = \Config\Database::connect();
// Build filtered models
$invoice = $this->invoice;
$refund = $this->refund;
$discount = $this->discount;
$expense = $this->expense;
$reimbursement = $this->reimbursement;
// Apply filters
if ($schoolYear) {
$invoice->where('invoices.school_year', $schoolYear);
$refund->where('school_year', $schoolYear);
$discount->where('school_year', $schoolYear);
$expense->where('school_year', $schoolYear);
$reimbursement->where('school_year', $schoolYear);
}
$hasFrom = !empty($dateFrom);
$hasTo = !empty($dateTo);
if ($hasFrom) {
$invoice->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) >=', $dateFrom);
$refund->where('DATE(COALESCE(refunded_at, created_at)) >=', $dateFrom);
$discount->where('DATE(COALESCE(used_at, created_at)) >=', $dateFrom);
$expense->where('DATE(created_at) >=', $dateFrom);
$reimbursement->where('DATE(created_at) >=', $dateFrom);
}
if ($hasTo) {
$invoice->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <=', $dateTo);
$refund->where('DATE(COALESCE(refunded_at, created_at)) <=', $dateTo);
$discount->where('DATE(COALESCE(used_at, created_at)) <=', $dateTo);
$expense->where('DATE(created_at) <=', $dateTo);
$reimbursement->where('DATE(created_at) <=', $dateTo);
}
// Get invoices
$invoices = $invoice
->select("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name")
->join('users', 'users.id = invoices.parent_id')
->findAll();
// Helper to build filtered Payment
$buildPayment = function () use ($schoolYear, $dateFrom, $dateTo) {
$pm = $this->payment;
if ($schoolYear) {
$pm->where('school_year', $schoolYear);
}
if (!empty($dateFrom)) {
$pm->where('DATE(payment_date) >=', $dateFrom);
}
if (!empty($dateTo)) {
$pm->where('DATE(payment_date) <=', $dateTo);
}
return $pm;
};
// Payments aggregated by invoice_id
$payments = $buildPayment()
->select('invoice_id, SUM(paid_amount) AS paid_amount')
->where('invoice_id IS NOT NULL')
->groupBy('invoice_id')
->findAll();
// Payment breakdown by method (cash/credit/check)
$paymentBreakdownRows = $buildPayment()
->select("
invoice_id,
CASE
WHEN LOWER(TRIM(payment_method)) = 'cash' THEN 'cash'
WHEN LOWER(TRIM(payment_method)) IN ('check','cheque') THEN 'check'
WHEN LOWER(TRIM(payment_method)) IN ('credit','card','credit card','debit','debit card','visa','mastercard') THEN 'credit'
ELSE 'other'
END AS method,
SUM(paid_amount) AS amount
")
->where('invoice_id IS NOT NULL')
->groupBy('invoice_id, method')
->findAll();
$paymentBreakdown = [];
foreach ($paymentBreakdownRows as $row) {
$iid = (int)($row['invoice_id'] ?? 0);
if ($iid <= 0) continue;
$method = (string)$row['method'];
$amount = (float)$row['amount'];
if (!isset($paymentBreakdown[$iid])) $paymentBreakdown[$iid] = [];
$paymentBreakdown[$iid][$method] = $amount;
}
// Overall payment totals by method
$paymentTotalsRow = $buildPayment()
->select("
SUM(paid_amount) AS total_all,
SUM(CASE WHEN LOWER(TRIM(payment_method)) = 'cash' THEN paid_amount ELSE 0 END) AS total_cash,
SUM(CASE WHEN LOWER(TRIM(payment_method)) IN ('check','cheque') THEN paid_amount ELSE 0 END) AS total_check,
SUM(CASE WHEN LOWER(TRIM(payment_method)) IN ('credit','card','credit card','debit','debit card','visa','mastercard') THEN paid_amount ELSE 0 END) AS total_credit
")
->where('invoice_id IS NOT NULL')
->first() ?? [];
$paymentTotals = [
'total_all' => (float)($paymentTotalsRow['total_all'] ?? 0),
'total_cash' => (float)($paymentTotalsRow['total_cash'] ?? 0),
'total_check' => (float)($paymentTotalsRow['total_check'] ?? 0),
'total_credit' => (float)($paymentTotalsRow['total_credit'] ?? 0),
];
// Refunds (grouped)
$refunds = $refund
->select('invoice_id, school_year, SUM(refund_paid_amount) AS total_refunded')
->where('invoice_id IS NOT NULL')
->groupBy(['invoice_id', 'school_year'])
->findAll();
// Discounts (grouped)
$discounts = $discount
->select('invoice_id, school_year, SUM(discount_amount) AS discount_amount')
->where('invoice_id IS NOT NULL')
->groupBy(['invoice_id', 'school_year'])
->get()
->getResultArray();
// Expenses
$expenses = $expense
->select('category, SUM(amount) AS total_amount')
->groupBy('category')
->findAll();
// Reimbursements
$reimbursements = $reimbursement
->select('status, SUM(amount) AS total_amount')
->groupBy('status')
->findAll();
// School year options
$schoolYears = [];
try {
$rows = $db->table('invoices')
->select('DISTINCT school_year', false)
->where('school_year IS NOT NULL', null, false)
->orderBy('school_year', 'DESC')
->get()->getResultArray();
foreach ($rows as $r) {
$val = (string)($r['school_year'] ?? '');
if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val;
}
} catch (\Throwable $e) {}
if (empty($schoolYears) && !empty($schoolYear)) {
$schoolYears[] = (string)$schoolYear;
}
return $this->success([
'selectedYear' => $schoolYear,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
'schoolYears' => $schoolYears,
'invoices' => $invoices,
'payments' => $payments,
'paymentBreakdown' => $paymentBreakdown,
'paymentTotals' => $paymentTotals,
'refunds' => $refunds,
'expenses' => $expenses,
'reimbursements' => $reimbursements,
'discounts' => $discounts,
], 'Financial report retrieved successfully');
} catch (\Throwable $e) {
log_message('error', 'Financial report error: ' . $e->getMessage());
return $this->respondError('Failed to retrieve financial report', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Financial report summary with aggregated totals
*/
public function reportSummary()
{
$dateFrom = $this->request->getGet('date_from');
$dateTo = $this->request->getGet('date_to');
$schoolYear = $this->request->getGet('school_year');
try {
$data = $this->getFinancialSummary($dateFrom, $dateTo, $schoolYear);
// Build school year options
$schoolYears = [];
try {
$db = \Config\Database::connect();
$rows = $db->table('invoices')
->select('DISTINCT school_year', false)
->where('school_year IS NOT NULL', null, false)
->orderBy('school_year', 'DESC')
->get()->getResultArray();
foreach ($rows as $r) {
$val = (string)($r['school_year'] ?? '');
if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val;
}
} catch (\Throwable $e) {}
if (empty($schoolYears) && !empty($data['schoolYear'])) {
$schoolYears[] = (string)$data['schoolYear'];
}
$data['schoolYears'] = $schoolYears;
return $this->success($data, 'Financial report summary retrieved successfully');
} catch (\Throwable $e) {
log_message('error', 'Financial report summary error: ' . $e->getMessage());
return $this->respondError('Failed to retrieve financial report summary', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Download financial report as CSV
*/
public function downloadCsv()
{
$dateFrom = $this->request->getGet('date_from');
$dateTo = $this->request->getGet('date_to');
$schoolYear = $this->request->getGet('school_year');
try {
$invoice = $this->invoice;
$expense = $this->expense;
$reimbursement = $this->reimbursement;
// Invoices with filters
$invBuilder = $invoice
->select("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name")
->join('users', 'users.id = invoices.parent_id');
if (!empty($schoolYear)) {
$invBuilder->where('invoices.school_year', $schoolYear);
}
if (!empty($dateFrom)) {
$invBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) >=', $dateFrom);
}
if (!empty($dateTo)) {
$invBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <=', $dateTo);
}
$invoices = $invBuilder->findAll();
// Expenses summary
$expBuilder = $expense->select('category, SUM(amount) AS total_amount');
if (!empty($schoolYear)) {
$expBuilder->where('school_year', $schoolYear);
}
if (!empty($dateFrom)) {
$expBuilder->where('DATE(created_at) >=', $dateFrom);
}
if (!empty($dateTo)) {
$expBuilder->where('DATE(created_at) <=', $dateTo);
}
$expenses = $expBuilder->groupBy('category')->findAll();
// Reimbursements summary
$reimbBuilder = $reimbursement->select('status, SUM(amount) AS total_amount');
if (!empty($schoolYear)) {
$reimbBuilder->where('school_year', $schoolYear);
}
if (!empty($dateFrom)) {
$reimbBuilder->where('DATE(created_at) >=', $dateFrom);
}
if (!empty($dateTo)) {
$reimbBuilder->where('DATE(created_at) <=', $dateTo);
}
$reimbursements = $reimbBuilder->groupBy('status')->findAll();
$filename = 'financial_report_' . date('Ymd_His') . '.csv';
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
];
return new StreamedResponse(function () use ($invoices, $expenses, $reimbursements) {
$out = fopen('php://output', 'w');
// Invoices section
fputcsv($out, ['Invoice #', 'Parent', 'School Year', 'Total', 'Paid', 'Balance', 'Refund']);
foreach ($invoices as $inv) {
fputcsv($out, [
$inv['invoice_number'] ?? '',
$inv['parent_name'] ?? '',
$inv['school_year'] ?? '',
$inv['total_amount'] ?? 0,
$inv['paid_amount'] ?? 0,
$inv['balance'] ?? 0,
$inv['refund_amount'] ?? 0,
]);
}
// Expenses section
fputcsv($out, []);
fputcsv($out, ['Expenses Summary']);
fputcsv($out, ['Category', 'Total Amount']);
foreach ($expenses as $exp) {
fputcsv($out, [
$exp['category'] ?? '',
$exp['total_amount'] ?? 0,
]);
}
// Reimbursements section
fputcsv($out, []);
fputcsv($out, ['Reimbursements Summary']);
fputcsv($out, ['Status', 'Total Amount']);
foreach ($reimbursements as $r) {
fputcsv($out, [
$r['status'] ?? '',
$r['total_amount'] ?? 0,
]);
}
fclose($out);
}, Response::HTTP_OK, $headers);
} catch (\Throwable $e) {
log_message('error', 'CSV download error: ' . $e->getMessage());
return $this->respondError('Failed to generate CSV', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Get financial summary data
*/
protected function getFinancialSummary(?string $dateFrom = null, ?string $dateTo = null, ?string $schoolYear = null): array
{
$schoolYear = $schoolYear ?: $this->config->getConfig('school_year');
$invoice = $this->invoice;
$payment = $this->payment;
$refund = $this->refund;
$expense = $this->expense;
$reimbursement = $this->reimbursement;
$discount = $this->discount;
// Invoices: Total Charges
$invoiceBuilder = $invoice->where('school_year', $schoolYear);
if (!empty($dateFrom)) {
$invoiceBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) >=', $dateFrom);
}
if (!empty($dateTo)) {
$invoiceBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <=', $dateTo);
}
$invoices = $invoiceBuilder->findAll();
$totalCharges = array_sum(array_column($invoices, 'total_amount'));
// Additional Charges
$acBuilder = $this->additionalCharge->where('school_year', $schoolYear)
->where('status !=', 'void');
if ($dateFrom && $dateTo) {
$acBuilder->where('due_date >=', $dateFrom)
->where('due_date <=', $dateTo);
}
$acResult = $acBuilder->selectSum('amount')->first();
$totalExtraCharges = isset($acResult['amount']) ? (float) $acResult['amount'] : 0.00;
$totalCharges = $totalCharges + $totalExtraCharges;
// Payments: Total Paid
$paymentBuilder = $payment->where('school_year', $schoolYear);
if (!empty($dateFrom)) {
$paymentBuilder->where('DATE(payment_date) >=', $dateFrom);
}
if (!empty($dateTo)) {
$paymentBuilder->where('DATE(payment_date) <=', $dateTo);
}
$paymentResult = $paymentBuilder->selectSum('paid_amount')->first();
$totalPaid = isset($paymentResult['paid_amount']) ? (float) $paymentResult['paid_amount'] : 0.00;
// Expenses
$expenseBuilder = $expense->where('school_year', $schoolYear);
if (!empty($dateFrom)) {
$expenseBuilder->where('DATE(created_at) >=', $dateFrom);
}
if (!empty($dateTo)) {
$expenseBuilder->where('DATE(created_at) <=', $dateTo);
}
$expenseResult = $expenseBuilder->selectSum('amount')->first();
$totalExpenses = isset($expenseResult['amount']) ? (float) $expenseResult['amount'] : 0.00;
// Reimbursements
$reimbursementBuilder = $reimbursement->where('school_year', $schoolYear);
if (!empty($dateFrom)) {
$reimbursementBuilder->where('DATE(created_at) >=', $dateFrom);
}
if (!empty($dateTo)) {
$reimbursementBuilder->where('DATE(created_at) <=', $dateTo);
}
$reimbursementResult = $reimbursementBuilder->selectSum('amount')->first();
$totalReimbursements = isset($reimbursementResult['amount']) ? (float) $reimbursementResult['amount'] : 0.00;
// Donations line: reimbursements recorded against Masjid/Donation recipients
$donationToSchool = 0.0;
$specialRecipientIds = array_map('intval', array_keys(self::SPECIAL_RECIPIENTS));
if (!empty($specialRecipientIds)) {
$donationBuilder = $reimbursement
->where('school_year', $schoolYear)
->whereIn('reimbursed_to', $specialRecipientIds);
if (!empty($dateFrom)) {
$donationBuilder->where('DATE(created_at) >=', $dateFrom);
}
if (!empty($dateTo)) {
$donationBuilder->where('DATE(created_at) <=', $dateTo);
}
$donationRow = $donationBuilder->selectSum('amount')->first();
$donationToSchool = isset($donationRow['amount']) ? (float) $donationRow['amount'] : 0.00;
}
$totalReimbursements = max(0.0, $totalReimbursements - $donationToSchool);
// Refunds
$refundBuilder = $refund
->where('school_year', $schoolYear)
->where('status', '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')->first();
$totalRefunds = isset($refundResult['refund_paid_amount']) ? (float) $refundResult['refund_paid_amount'] : 0.00;
// Discounts
$discountBuilder = $discount
->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')->first();
$totalDiscounts = isset($discountResult['discount_amount']) ? (float) $discountResult['discount_amount'] : 0.00;
// Net & Unpaid
$totalUnpaid = ($totalCharges - $totalDiscounts) - $totalPaid;
$amountCollected = $totalPaid;
$netAmount = ($totalCharges - $totalDiscounts - $totalRefunds);
return [
'schoolYear' => $schoolYear,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
'totalCharges' => $totalCharges,
'totalExtraCharges' => $totalExtraCharges,
'totalDiscounts' => $totalDiscounts,
'totalRefunds' => $totalRefunds,
'totalExpenses' => $totalExpenses,
'totalReimbursements' => $totalReimbursements,
'donationToSchool' => $donationToSchool,
'totalPaid' => $totalPaid,
'amountCollected' => $amountCollected,
'totalUnpaid' => $totalUnpaid,
'netAmount' => $netAmount,
];
}
/**
* List parents with outstanding balances (> 0) for a school year
*/
public function unpaidParents()
{
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
if ($schoolYear === '') {
$schoolYear = (string) ($this->config->getConfig('school_year') ?? date('Y'));
}
try {
$db = \Config\Database::connect();
// Build school year options
$schoolYears = [];
try {
$rows = $db->table('invoices')
->select('DISTINCT school_year', false)
->where('school_year IS NOT NULL', null, false)
->orderBy('school_year', 'DESC')
->get()->getResultArray();
foreach ($rows as $r) {
$val = (string)($r['school_year'] ?? '');
if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val;
}
} catch (\Throwable $e) {}
if (empty($schoolYears) && !empty($schoolYear)) {
$schoolYears[] = (string)$schoolYear;
}
// Aggregate balances by parent
$invRows = $db->table('invoices i')
->select('i.id, i.parent_id, i.total_amount, u.firstname, u.lastname, u.email')
->join('users u', 'u.id = i.parent_id', 'inner')
->where('i.school_year', $schoolYear)
->orderBy('i.parent_id', 'ASC')
->orderBy('i.id', 'ASC')
->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);
if ($iid <= 0 || $pid <= 0) continue;
// Sum payments for this invoice
$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
$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
$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));
if (!isset($byParent[$pid])) {
$byParent[$pid] = [
'parent_id' => $pid,
'firstname' => (string)($r['firstname'] ?? ''),
'lastname' => (string)($r['lastname'] ?? ''),
'email' => (string)($r['email'] ?? ''),
'total_invoice' => 0.0,
'total_balance' => 0.0,
'total_paid' => 0.0,
];
}
$byParent[$pid]['total_invoice'] += $total;
$byParent[$pid]['total_balance'] += $balance;
$byParent[$pid]['total_paid'] += $paidSum;
}
// Build rows list (only parents with positive balance)
$rows = [];
$installmentEndRaw = (string) ($this->config->getConfig('installment_date') ?? '');
$tzName = (string) (config('School')->attendance['timezone'] ?? 'America/New_York');
$tzNY = new \DateTimeZone($tzName);
$todayNY = new \DateTimeImmutable('today', $tzNY);
$endDate = null;
if ($installmentEndRaw) {
try { $endDate = new \DateTimeImmutable($installmentEndRaw, $tzNY); } catch (\Throwable $e) { $endDate = null; }
}
$monthsUntil = function (?\DateTimeImmutable $end) use ($todayNY): int {
if (!$end) return 0;
$y = (int)$end->format('Y') - (int)$todayNY->format('Y');
$m = (int)$end->format('n') - (int)$todayNY->format('n');
$months = $y * 12 + $m;
if ((int)$end->format('j') > (int)$todayNY->format('j')) $months += 1;
return max(0, $months);
};
foreach ($byParent as $pid => $agg) {
$balance = (float)($agg['total_balance'] ?? 0);
if ($balance > 0.00001) {
$remMonths = $monthsUntil($endDate);
$remainingInstallments = max(1, $remMonths);
$instAmount = round($balance / $remainingInstallments, 2);
$rows[] = [
'parent_id' => $pid,
'firstname' => $agg['firstname'],
'lastname' => $agg['lastname'],
'email' => $agg['email'],
'total_invoice' => (float)$agg['total_invoice'],
'total_balance' => $balance,
'remaining_installments' => $remainingInstallments,
'installment_amount' => $instAmount,
];
}
}
// Order by total_balance desc
usort($rows, static function ($a, $b) {
return ($b['total_balance'] <=> $a['total_balance']);
});
// Compute next installment date
$tz = new \DateTimeZone($tzName);
$now = new \DateTime('now', $tz);
$nextInstallmentDt = (clone $now)->modify('first day of next month');
$nextInstallmentYmd = $nextInstallmentDt->format('Y-m-d');
// Determine payment types
$parentIds = array_values(array_unique(array_map(static fn($r) => (int)($r['parent_id'] ?? 0), $rows)));
$hasPayments = [];
$paidTotals = [];
if (!empty($parentIds)) {
try {
$pRows = $db->table('payments')
->select('parent_id, SUM(paid_amount) AS total_paid')
->whereIn('parent_id', $parentIds)
->where('school_year', $schoolYear)
->groupBy('parent_id')
->get()->getResultArray();
foreach ($pRows as $pr) {
$pid = (int)($pr['parent_id'] ?? 0);
if ($pid > 0) {
$hasPayments[$pid] = true;
$paidTotals[$pid] = (float)($pr['total_paid'] ?? 0);
}
}
} catch (\Throwable $e) {}
}
$dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $nextInstallmentYmd) {
$pid = (int)($r['parent_id'] ?? 0);
$name = trim((string)($r['firstname'] ?? '') . ' ' . (string)($r['lastname'] ?? ''));
return [
'parent_id' => $pid,
'parent_name' => $name,
'email' => (string)($r['email'] ?? ''),
'total_invoice' => (float)($r['total_invoice'] ?? 0),
'total_balance' => (float)($r['total_balance'] ?? 0),
'remaining_installments' => (int)($r['remaining_installments'] ?? 0),
'installment_amount' => (float)($r['installment_amount'] ?? 0),
'type' => isset($hasPayments[$pid]) ? 'installment' : 'no_payment',
'total_paid' => isset($paidTotals[$pid]) ? (float)$paidTotals[$pid] : 0.0,
'has_installment'=> isset($hasPayments[$pid]) ? 1 : 0,
'next_installment' => $nextInstallmentYmd,
];
}, $rows);
return $this->success([
'school_year' => $schoolYear,
'schoolYears' => $schoolYears,
'results' => $dataRows,
], 'Unpaid parents retrieved successfully');
} catch (\Throwable $e) {
log_message('error', 'Unpaid parents error: ' . $e->getMessage());
return $this->respondError('Failed to retrieve unpaid parents', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}