add projet
This commit is contained in:
+724
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user