add controllers, servoices
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
class FinancialChartService
|
||||
{
|
||||
public function generateBarChart(array $summary): ?string
|
||||
{
|
||||
if ($this->skipRemoteCharts()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$chartData = [
|
||||
'type' => 'bar',
|
||||
'data' => [
|
||||
'labels' => ['Charges', 'Paid', 'Unpaid', 'Discounts', 'Refunds', 'Expenses', 'Reimbursements', 'Net'],
|
||||
'datasets' => [[
|
||||
'label' => 'Amount (USD)',
|
||||
'data' => [
|
||||
$summary['totalCharges'] ?? 0,
|
||||
$summary['amountCollected'] ?? 0,
|
||||
$summary['totalUnpaid'] ?? 0,
|
||||
$summary['totalDiscounts'] ?? 0,
|
||||
$summary['totalRefunds'] ?? 0,
|
||||
$summary['totalExpenses'] ?? 0,
|
||||
$summary['totalReimbursements'] ?? 0,
|
||||
$summary['netAmount'] ?? 0,
|
||||
],
|
||||
'backgroundColor' => 'rgba(54, 162, 235, 0.6)',
|
||||
]],
|
||||
],
|
||||
];
|
||||
|
||||
return $this->downloadChart($chartData, 'bar_chart.png');
|
||||
}
|
||||
|
||||
public function generatePieChart(array $summary): ?string
|
||||
{
|
||||
if ($this->skipRemoteCharts()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$chartData = [
|
||||
'type' => 'pie',
|
||||
'data' => [
|
||||
'labels' => ['Expenses', 'Reimbursements'],
|
||||
'datasets' => [[
|
||||
'data' => [
|
||||
$summary['totalExpenses'] ?? 0,
|
||||
$summary['totalReimbursements'] ?? 0,
|
||||
],
|
||||
'backgroundColor' => ['#e74c3c', '#8e44ad'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
|
||||
return $this->downloadChart($chartData, 'pie_chart.png');
|
||||
}
|
||||
|
||||
private function downloadChart(array $chartData, string $filename): ?string
|
||||
{
|
||||
$baseUrl = 'https://quickchart.io/chart?c=';
|
||||
$url = $baseUrl . urlencode(json_encode($chartData));
|
||||
$targetDir = storage_path('app/reports');
|
||||
if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$contents = @file_get_contents($url);
|
||||
if ($contents === false) {
|
||||
return null;
|
||||
}
|
||||
$path = $targetDir . DIRECTORY_SEPARATOR . $filename;
|
||||
file_put_contents($path, $contents);
|
||||
return $path;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function skipRemoteCharts(): bool
|
||||
{
|
||||
if (app()->environment('testing')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) config('services.quickchart.disabled', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
class FinancialDonationRecipientService
|
||||
{
|
||||
public const SPECIAL_RECIPIENTS = [
|
||||
990001 => 'Masjid',
|
||||
990002 => 'Donation',
|
||||
];
|
||||
|
||||
public function recipientIds(): array
|
||||
{
|
||||
return array_map('intval', array_keys(self::SPECIAL_RECIPIENTS));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class FinancialPaymentService
|
||||
{
|
||||
public function paymentsByInvoice(?string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
$query = $this->applyPaymentFilters($this->buildPaymentQuery($schoolYear, $dateFrom, $dateTo));
|
||||
|
||||
return $query
|
||||
->selectRaw('invoice_id, SUM(paid_amount) AS paid_amount')
|
||||
->whereNotNull('invoice_id')
|
||||
->groupBy('invoice_id')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function paymentBreakdown(?string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
$query = $this->applyPaymentFilters($this->buildPaymentQuery($schoolYear, $dateFrom, $dateTo));
|
||||
|
||||
$rows = $query
|
||||
->selectRaw("
|
||||
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
|
||||
")
|
||||
->whereNotNull('invoice_id')
|
||||
->groupBy('invoice_id', 'method')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$breakdown = [];
|
||||
foreach ($rows as $row) {
|
||||
$invoiceId = (int) ($row['invoice_id'] ?? 0);
|
||||
if ($invoiceId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$method = (string) ($row['method'] ?? '');
|
||||
$amount = (float) ($row['amount'] ?? 0);
|
||||
if (!isset($breakdown[$invoiceId])) {
|
||||
$breakdown[$invoiceId] = [];
|
||||
}
|
||||
$breakdown[$invoiceId][$method] = $amount;
|
||||
}
|
||||
|
||||
return $breakdown;
|
||||
}
|
||||
|
||||
public function paymentTotals(?string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
$query = $this->applyPaymentFilters($this->buildPaymentQuery($schoolYear, $dateFrom, $dateTo));
|
||||
|
||||
$row = (array) $query
|
||||
->selectRaw("
|
||||
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
|
||||
")
|
||||
->whereNotNull('invoice_id')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total_all' => (float) ($row['total_all'] ?? 0),
|
||||
'total_cash' => (float) ($row['total_cash'] ?? 0),
|
||||
'total_check' => (float) ($row['total_check'] ?? 0),
|
||||
'total_credit' => (float) ($row['total_credit'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function buildPaymentQuery(?string $schoolYear, ?string $dateFrom, ?string $dateTo): Builder
|
||||
{
|
||||
$query = DB::table('payments');
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$query->where('school_year', $schoolYear);
|
||||
}
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(payment_date) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(payment_date) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function applyPaymentFilters(Builder $query): Builder
|
||||
{
|
||||
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
||||
|
||||
if (Schema::hasColumn('payments', 'status')) {
|
||||
$query->where(function ($q) use ($exclude) {
|
||||
$q->whereNotIn('status', $exclude)->orWhereNull('status');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('payments', 'is_void')) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('is_void', 0)->orWhereNull('is_void');
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
class FinancialPdfReportService
|
||||
{
|
||||
public function __construct(private FinancialChartService $charts)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildPdf(array $summary): string
|
||||
{
|
||||
$pdf = new \FPDF();
|
||||
$pdf->AddPage();
|
||||
$pdf->SetFont('Arial', 'B', 16);
|
||||
$pdf->Cell(0, 10, 'Financial Report for School Year: ' . ($summary['schoolYear'] ?? ''), 0, 1, 'C');
|
||||
|
||||
$pdf->Ln(10);
|
||||
$pdf->SetFont('Arial', 'B', 12);
|
||||
$pdf->Cell(130, 10, 'Description', 1);
|
||||
$pdf->Cell(60, 10, 'Amount (USD)', 1);
|
||||
$pdf->Ln();
|
||||
|
||||
$rows = [
|
||||
'Total Charges' => $summary['totalCharges'] ?? 0,
|
||||
'Total Extra Charges' => $summary['totalExtraCharges'] ?? 0,
|
||||
'Total Discounts' => $summary['totalDiscounts'] ?? 0,
|
||||
'Total Refunds' => $summary['totalRefunds'] ?? 0,
|
||||
'Total Expenses' => $summary['totalExpenses'] ?? 0,
|
||||
'Total Reimbursements' => $summary['totalReimbursements'] ?? 0,
|
||||
'Donation to School (Masjid/Donation reimbursements)' => $summary['donationToSchool'] ?? 0,
|
||||
'Net Amount (Earned Income)' => $summary['netAmount'] ?? 0,
|
||||
'Amount Collected (Paid)' => $summary['amountCollected'] ?? 0,
|
||||
'Amount Unpaid (Outstanding)' => $summary['totalUnpaid'] ?? 0,
|
||||
];
|
||||
|
||||
$pdf->SetFont('Arial', '', 11);
|
||||
foreach ($rows as $label => $amount) {
|
||||
$pdf->Cell(130, 10, $label, 1);
|
||||
$pdf->Cell(60, 10, '$' . number_format((float) $amount, 2), 1);
|
||||
$pdf->Ln();
|
||||
}
|
||||
|
||||
$barChart = $this->charts->generateBarChart($summary);
|
||||
if (!empty($barChart) && file_exists($barChart)) {
|
||||
$pdf->Ln(10);
|
||||
$pdf->SetFont('Arial', 'B', 12);
|
||||
$pdf->Cell(0, 10, 'Summary Graph', 0, 1);
|
||||
$pdf->Image($barChart, null, null, 180);
|
||||
}
|
||||
|
||||
$pieChart = $this->charts->generatePieChart($summary);
|
||||
if (!empty($pieChart) && file_exists($pieChart)) {
|
||||
$pdf->Ln(10);
|
||||
$pdf->SetFont('Arial', 'B', 12);
|
||||
$pdf->Cell(0, 10, 'Expense Breakdown', 0, 1);
|
||||
$pdf->Image($pieChart, null, null, 120);
|
||||
}
|
||||
|
||||
return $pdf->Output('S');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FinancialReportService
|
||||
{
|
||||
public function __construct(
|
||||
private FinancialPaymentService $payments,
|
||||
private FinancialSchoolYearService $schoolYears
|
||||
) {
|
||||
}
|
||||
|
||||
public function getReport(?string $dateFrom, ?string $dateTo, ?string $schoolYear): array
|
||||
{
|
||||
$invoiceQuery = DB::table('invoices')
|
||||
->selectRaw("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name")
|
||||
->join('users', 'users.id', '=', 'invoices.parent_id');
|
||||
|
||||
$refundQuery = DB::table('refunds');
|
||||
$discountQuery = DB::table('discount_usages');
|
||||
$expenseQuery = DB::table('expenses');
|
||||
$reimbQuery = DB::table('reimbursements');
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$invoiceQuery->where('invoices.school_year', $schoolYear);
|
||||
$refundQuery->where('school_year', $schoolYear);
|
||||
$discountQuery->where('school_year', $schoolYear);
|
||||
$expenseQuery->where('school_year', $schoolYear);
|
||||
$reimbQuery->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$invoiceQuery->whereRaw('DATE(COALESCE(invoices.issue_date, invoices.created_at)) >= ?', [$dateFrom]);
|
||||
$refundQuery->whereRaw('DATE(COALESCE(refunded_at, created_at)) >= ?', [$dateFrom]);
|
||||
$discountQuery->whereRaw('DATE(COALESCE(used_at, created_at)) >= ?', [$dateFrom]);
|
||||
$expenseQuery->whereRaw('DATE(created_at) >= ?', [$dateFrom]);
|
||||
$reimbQuery->whereRaw('DATE(created_at) >= ?', [$dateFrom]);
|
||||
}
|
||||
|
||||
if (!empty($dateTo)) {
|
||||
$invoiceQuery->whereRaw('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <= ?', [$dateTo]);
|
||||
$refundQuery->whereRaw('DATE(COALESCE(refunded_at, created_at)) <= ?', [$dateTo]);
|
||||
$discountQuery->whereRaw('DATE(COALESCE(used_at, created_at)) <= ?', [$dateTo]);
|
||||
$expenseQuery->whereRaw('DATE(created_at) <= ?', [$dateTo]);
|
||||
$reimbQuery->whereRaw('DATE(created_at) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$invoices = $invoiceQuery->get()->map(fn ($row) => (array) $row)->all();
|
||||
|
||||
$payments = $this->payments->paymentsByInvoice($schoolYear, $dateFrom, $dateTo);
|
||||
$paymentBreakdown = $this->payments->paymentBreakdown($schoolYear, $dateFrom, $dateTo);
|
||||
$paymentTotals = $this->payments->paymentTotals($schoolYear, $dateFrom, $dateTo);
|
||||
|
||||
$refunds = $refundQuery
|
||||
->selectRaw('invoice_id, school_year, SUM(refund_paid_amount) AS total_refunded')
|
||||
->whereNotNull('invoice_id')
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->groupBy('invoice_id', 'school_year')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$discounts = $discountQuery
|
||||
->selectRaw('invoice_id, school_year, SUM(discount_amount) AS discount_amount')
|
||||
->whereNotNull('invoice_id')
|
||||
->groupBy('invoice_id', 'school_year')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$expenses = $expenseQuery
|
||||
->selectRaw('category, SUM(amount) AS total_amount')
|
||||
->groupBy('category')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$reimbursements = $reimbQuery
|
||||
->selectRaw('status, SUM(amount) AS total_amount')
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$schoolYears = $this->schoolYears->listYears($schoolYear);
|
||||
|
||||
return [
|
||||
'selectedYear' => $schoolYear,
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo,
|
||||
'schoolYears' => $schoolYears,
|
||||
'invoices' => $invoices,
|
||||
'payments' => $payments,
|
||||
'paymentBreakdown' => $paymentBreakdown,
|
||||
'paymentTotals' => $paymentTotals,
|
||||
'refunds' => $refunds,
|
||||
'expenses' => $expenses,
|
||||
'reimbursements' => $reimbursements,
|
||||
'discounts' => $discounts,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FinancialSchoolYearService
|
||||
{
|
||||
public function listYears(?string $fallbackYear = null): array
|
||||
{
|
||||
$years = DB::table('invoices')
|
||||
->select('school_year')
|
||||
->whereNotNull('school_year')
|
||||
->distinct()
|
||||
->orderBy('school_year', 'DESC')
|
||||
->pluck('school_year')
|
||||
->map(static fn ($val) => (string) $val)
|
||||
->filter(static fn ($val) => $val !== '')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (empty($years) && !empty($fallbackYear)) {
|
||||
$years[] = (string) $fallbackYear;
|
||||
}
|
||||
|
||||
return $years;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class FinancialSummaryService
|
||||
{
|
||||
public function __construct(
|
||||
private FinancialDonationRecipientService $donationRecipients
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSummary(?string $dateFrom, ?string $dateTo, ?string $schoolYear): array
|
||||
{
|
||||
$schoolYear = $schoolYear ?: (Configuration::getConfig('school_year') ?? date('Y'));
|
||||
$derivedDateFrom = null;
|
||||
$derivedDateTo = null;
|
||||
|
||||
if (!empty($schoolYear) && empty($dateFrom) && empty($dateTo)) {
|
||||
if (preg_match('/^(\\d{4})[^\\d]?(\\d{4})$/', $schoolYear, $m)) {
|
||||
$derivedDateFrom = $m[1] . '-01-01';
|
||||
$derivedDateTo = $m[2] . '-12-31';
|
||||
}
|
||||
}
|
||||
|
||||
$invoiceDateFrom = $dateFrom ?: $derivedDateFrom;
|
||||
$invoiceDateTo = $dateTo ?: $derivedDateTo;
|
||||
|
||||
$invoiceQuery = DB::table('invoices')->where('school_year', $schoolYear);
|
||||
if (!empty($invoiceDateFrom)) {
|
||||
$invoiceQuery->whereRaw('DATE(COALESCE(issue_date, created_at)) >= ?', [$invoiceDateFrom]);
|
||||
}
|
||||
if (!empty($invoiceDateTo)) {
|
||||
$invoiceQuery->whereRaw('DATE(COALESCE(issue_date, created_at)) <= ?', [$invoiceDateTo]);
|
||||
}
|
||||
$invoices = $invoiceQuery->get()->map(fn ($row) => (array) $row)->all();
|
||||
$totalCharges = array_sum(array_map(static fn ($row) => (float) ($row['total_amount'] ?? 0), $invoices));
|
||||
|
||||
$invoiceIds = array_values(array_unique(array_filter(array_map(static function ($row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
return $id > 0 ? $id : null;
|
||||
}, $invoices))));
|
||||
|
||||
$totalExtraCharges = $this->sumAdditionalCharges($schoolYear, $dateFrom, $dateTo);
|
||||
$extraByParent = $this->extraChargesByParent($schoolYear, $dateFrom, $dateTo);
|
||||
$extraChargesUnapplied = array_sum($extraByParent);
|
||||
$totalCharges += $extraChargesUnapplied;
|
||||
|
||||
$totalPaid = $this->sumPayments($schoolYear, $invoiceDateFrom, $invoiceDateTo);
|
||||
|
||||
$paidByInvoice = [];
|
||||
$discountByInvoice = [];
|
||||
$refundByInvoice = [];
|
||||
if (!empty($invoiceIds)) {
|
||||
$paidByInvoice = $this->paymentsByInvoiceIds($invoiceIds, $schoolYear, $invoiceDateFrom, $invoiceDateTo);
|
||||
$discountByInvoice = $this->discountsByInvoiceIds($invoiceIds, $dateFrom, $dateTo);
|
||||
$refundByInvoice = $this->refundsByInvoiceIds($invoiceIds, $dateFrom, $dateTo);
|
||||
}
|
||||
|
||||
$totalExpenses = $this->sumExpenses($schoolYear, $invoiceDateFrom, $invoiceDateTo);
|
||||
$totalReimbursements = $this->sumReimbursements($schoolYear, $invoiceDateFrom, $invoiceDateTo);
|
||||
|
||||
$donationToSchool = $this->sumDonationToSchool($schoolYear, $invoiceDateFrom, $invoiceDateTo);
|
||||
$totalReimbursements = max(0.0, $totalReimbursements - $donationToSchool['donationReimb']);
|
||||
|
||||
$totalRefunds = $this->sumRefunds($schoolYear, $dateFrom, $dateTo);
|
||||
$totalDiscounts = $this->sumDiscounts($schoolYear, $dateFrom, $dateTo);
|
||||
|
||||
$totalUnpaid = $this->sumUnpaidBalances($invoices, $paidByInvoice, $discountByInvoice, $refundByInvoice, $extraByParent);
|
||||
|
||||
$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['donationToSchool'],
|
||||
'totalPaid' => $totalPaid,
|
||||
'amountCollected' => $amountCollected,
|
||||
'totalUnpaid' => $totalUnpaid,
|
||||
'netAmount' => $netAmount,
|
||||
];
|
||||
}
|
||||
|
||||
private function sumAdditionalCharges(string $schoolYear, ?string $dateFrom, ?string $dateTo): float
|
||||
{
|
||||
$query = DB::table('additional_charges as ac')
|
||||
->selectRaw('COALESCE(SUM(ac.amount), 0) AS amount')
|
||||
->where('ac.school_year', $schoolYear)
|
||||
->where('ac.status !=', 'void');
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(ac.due_date) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(ac.due_date) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$row = (array) $query->first();
|
||||
return (float) ($row['amount'] ?? 0);
|
||||
}
|
||||
|
||||
private function extraChargesByParent(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
$query = DB::table('additional_charges as ac')
|
||||
->selectRaw('ac.parent_id, COALESCE(SUM(ac.amount), 0) AS amount')
|
||||
->where('ac.school_year', $schoolYear)
|
||||
->where('ac.status !=', 'void')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('ac.invoice_id')
|
||||
->orWhere('ac.invoice_id', 0)
|
||||
->orWhere('ac.status !=', 'applied');
|
||||
})
|
||||
->groupBy('ac.parent_id');
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(ac.due_date) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(ac.due_date) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$rows = $query->get()->map(fn ($row) => (array) $row)->all();
|
||||
$extraByParent = [];
|
||||
foreach ($rows as $row) {
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
$amount = (float) ($row['amount'] ?? 0);
|
||||
if ($parentId > 0 && abs($amount) > 0.00001) {
|
||||
$extraByParent[$parentId] = $amount;
|
||||
}
|
||||
}
|
||||
|
||||
return $extraByParent;
|
||||
}
|
||||
|
||||
private function sumPayments(string $schoolYear, ?string $dateFrom, ?string $dateTo): float
|
||||
{
|
||||
$query = DB::table('payments')->where('school_year', $schoolYear);
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(payment_date) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(payment_date) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
||||
if (Schema::hasColumn('payments', 'status')) {
|
||||
$query->where(function ($q) use ($exclude) {
|
||||
$q->whereNotIn('status', $exclude)->orWhereNull('status');
|
||||
});
|
||||
}
|
||||
if (Schema::hasColumn('payments', 'is_void')) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('is_void', 0)->orWhereNull('is_void');
|
||||
});
|
||||
}
|
||||
|
||||
$row = (array) $query->selectRaw('COALESCE(SUM(paid_amount), 0) AS paid_amount')->first();
|
||||
return (float) ($row['paid_amount'] ?? 0);
|
||||
}
|
||||
|
||||
private function paymentsByInvoiceIds(array $invoiceIds, string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
$query = DB::table('payments')
|
||||
->selectRaw('invoice_id, COALESCE(SUM(paid_amount), 0) AS total_paid')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(payment_date) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(payment_date) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
||||
if (Schema::hasColumn('payments', 'status')) {
|
||||
$query->where(function ($q) use ($exclude) {
|
||||
$q->whereNotIn('status', $exclude)->orWhereNull('status');
|
||||
});
|
||||
}
|
||||
if (Schema::hasColumn('payments', 'is_void')) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('is_void', 0)->orWhereNull('is_void');
|
||||
});
|
||||
}
|
||||
|
||||
$rows = $query->groupBy('invoice_id')->get()->map(fn ($row) => (array) $row)->all();
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$invoiceId = (int) ($row['invoice_id'] ?? 0);
|
||||
if ($invoiceId > 0) {
|
||||
$map[$invoiceId] = (float) ($row['total_paid'] ?? 0);
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function discountsByInvoiceIds(array $invoiceIds, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
$query = DB::table('discount_usages')
|
||||
->selectRaw('invoice_id, COALESCE(SUM(discount_amount),0) AS total_disc')
|
||||
->whereIn('invoice_id', $invoiceIds);
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(COALESCE(used_at, created_at)) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(COALESCE(used_at, created_at)) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$rows = $query->groupBy('invoice_id')->get()->map(fn ($row) => (array) $row)->all();
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$invoiceId = (int) ($row['invoice_id'] ?? 0);
|
||||
if ($invoiceId > 0) {
|
||||
$map[$invoiceId] = (float) ($row['total_disc'] ?? 0);
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function refundsByInvoiceIds(array $invoiceIds, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
$query = DB::table('refunds')
|
||||
->selectRaw('invoice_id, COALESCE(SUM(refund_paid_amount),0) AS total_refund')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->whereIn('status', ['Partial', 'Paid']);
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(COALESCE(refunded_at, created_at)) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(COALESCE(refunded_at, created_at)) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$rows = $query->groupBy('invoice_id')->get()->map(fn ($row) => (array) $row)->all();
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$invoiceId = (int) ($row['invoice_id'] ?? 0);
|
||||
if ($invoiceId > 0) {
|
||||
$map[$invoiceId] = (float) ($row['total_refund'] ?? 0);
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function sumExpenses(string $schoolYear, ?string $dateFrom, ?string $dateTo): float
|
||||
{
|
||||
$query = DB::table('expenses')->where('school_year', $schoolYear);
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(created_at) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(created_at) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$row = (array) $query->selectRaw('COALESCE(SUM(amount), 0) AS amount')->first();
|
||||
return (float) ($row['amount'] ?? 0);
|
||||
}
|
||||
|
||||
private function sumReimbursements(string $schoolYear, ?string $dateFrom, ?string $dateTo): float
|
||||
{
|
||||
$normalizedYear = preg_replace('/[^0-9]/', '', (string) $schoolYear);
|
||||
|
||||
$query = DB::table('reimbursements as r')
|
||||
->selectRaw('COALESCE(SUM(r.amount),0) AS amount')
|
||||
->leftJoin('expenses as e', 'e.id', '=', 'r.expense_id');
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$query->where(function ($q) use ($schoolYear, $normalizedYear) {
|
||||
$q->where(function ($q2) use ($schoolYear, $normalizedYear) {
|
||||
$q2->where('r.school_year', $schoolYear)
|
||||
->orWhereRaw(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(r.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '') = ?",
|
||||
[$normalizedYear]
|
||||
);
|
||||
})->orWhere(function ($q2) use ($schoolYear, $normalizedYear) {
|
||||
$q2->whereNull('r.school_year')
|
||||
->where(function ($q3) use ($schoolYear, $normalizedYear) {
|
||||
$q3->where('e.school_year', $schoolYear)
|
||||
->orWhereRaw(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(e.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '') = ?",
|
||||
[$normalizedYear]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(COALESCE(r.created_at, e.created_at)) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(COALESCE(r.created_at, e.created_at)) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$row = (array) $query->first();
|
||||
$total = (float) ($row['amount'] ?? 0);
|
||||
|
||||
$batchFallback = DB::table('reimbursement_batch_items as bi')
|
||||
->selectRaw('COALESCE(SUM(e.amount),0) AS amount')
|
||||
->join('reimbursement_batches as b', 'b.id', '=', 'bi.batch_id')
|
||||
->join('expenses as e', 'e.id', '=', 'bi.expense_id')
|
||||
->leftJoin('reimbursements as r', 'r.expense_id', '=', 'e.id')
|
||||
->where('b.status', 'closed')
|
||||
->whereNull('bi.unassigned_at')
|
||||
->whereNull('r.id');
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$batchFallback->where(function ($q) use ($schoolYear, $normalizedYear) {
|
||||
$q->where('e.school_year', $schoolYear)
|
||||
->orWhereRaw(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(e.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '') = ?",
|
||||
[$normalizedYear]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$batchFallback->whereRaw('DATE(COALESCE(e.created_at, b.closed_at)) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$batchFallback->whereRaw('DATE(COALESCE(e.created_at, b.closed_at)) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$fallbackRow = (array) $batchFallback->first();
|
||||
$total += (float) ($fallbackRow['amount'] ?? 0);
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function sumDonationToSchool(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
$expenseQuery = DB::table('expenses')
|
||||
->where('school_year', $schoolYear)
|
||||
->where('category', 'Donation');
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$expenseQuery->whereRaw('DATE(created_at) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$expenseQuery->whereRaw('DATE(created_at) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$expenseRow = (array) $expenseQuery->selectRaw('COALESCE(SUM(amount),0) AS amount')->first();
|
||||
$donationExpense = (float) ($expenseRow['amount'] ?? 0);
|
||||
|
||||
$donationReimb = 0.0;
|
||||
$recipientIds = $this->donationRecipients->recipientIds();
|
||||
if (!empty($recipientIds)) {
|
||||
$normalizedYear = preg_replace('/[^0-9]/', '', (string) $schoolYear);
|
||||
$donationQuery = DB::table('reimbursements as r')
|
||||
->selectRaw('COALESCE(SUM(r.amount),0) AS amount')
|
||||
->leftJoin('expenses as e', 'e.id', '=', 'r.expense_id')
|
||||
->whereIn('r.reimbursed_to', $recipientIds);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$donationQuery->where(function ($q) use ($schoolYear, $normalizedYear) {
|
||||
$q->where(function ($q2) use ($schoolYear, $normalizedYear) {
|
||||
$q2->where('r.school_year', $schoolYear)
|
||||
->orWhereRaw(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(r.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '') = ?",
|
||||
[$normalizedYear]
|
||||
);
|
||||
})->orWhere(function ($q2) use ($schoolYear, $normalizedYear) {
|
||||
$q2->whereNull('r.school_year')
|
||||
->where(function ($q3) use ($schoolYear, $normalizedYear) {
|
||||
$q3->where('e.school_year', $schoolYear)
|
||||
->orWhereRaw(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(e.school_year, ' ', ''), '-', ''), '/', ''), '\\\\', '') = ?",
|
||||
[$normalizedYear]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$donationQuery->whereRaw('DATE(COALESCE(r.created_at, e.created_at)) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$donationQuery->whereRaw('DATE(COALESCE(r.created_at, e.created_at)) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$donationRow = (array) $donationQuery->first();
|
||||
$donationReimb = (float) ($donationRow['amount'] ?? 0);
|
||||
}
|
||||
|
||||
return [
|
||||
'donationToSchool' => $donationExpense + $donationReimb,
|
||||
'donationReimb' => $donationReimb,
|
||||
];
|
||||
}
|
||||
|
||||
private function sumRefunds(string $schoolYear, ?string $dateFrom, ?string $dateTo): float
|
||||
{
|
||||
$query = DB::table('refunds')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->whereNotNull('refund_paid_amount');
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(COALESCE(refunded_at, created_at)) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(COALESCE(refunded_at, created_at)) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$row = (array) $query->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS refund_paid_amount')->first();
|
||||
return (float) ($row['refund_paid_amount'] ?? 0);
|
||||
}
|
||||
|
||||
private function sumDiscounts(string $schoolYear, ?string $dateFrom, ?string $dateTo): float
|
||||
{
|
||||
$query = DB::table('discount_usages as du')
|
||||
->join('invoices as i', 'i.id', '=', 'du.invoice_id')
|
||||
->where('i.school_year', $schoolYear);
|
||||
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(COALESCE(du.used_at, du.created_at)) >= ?', [$dateFrom]);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(COALESCE(du.used_at, du.created_at)) <= ?', [$dateTo]);
|
||||
}
|
||||
|
||||
$row = (array) $query->selectRaw('COALESCE(SUM(du.discount_amount),0) AS discount_amount')->first();
|
||||
return (float) ($row['discount_amount'] ?? 0);
|
||||
}
|
||||
|
||||
private function sumUnpaidBalances(array $invoices, array $paidByInvoice, array $discountByInvoice, array $refundByInvoice, array $extraByParent): float
|
||||
{
|
||||
$parentBalances = [];
|
||||
|
||||
foreach ($invoices as $inv) {
|
||||
$invoiceId = (int) ($inv['id'] ?? 0);
|
||||
$parentId = (int) ($inv['parent_id'] ?? 0);
|
||||
if ($invoiceId <= 0 || $parentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$total = (float) ($inv['total_amount'] ?? 0);
|
||||
$paid = (float) ($paidByInvoice[$invoiceId] ?? 0);
|
||||
$disc = (float) ($discountByInvoice[$invoiceId] ?? 0);
|
||||
$ref = (float) ($refundByInvoice[$invoiceId] ?? 0);
|
||||
$balance = max(0.0, round($total - $disc - $paid - $ref, 2));
|
||||
|
||||
if (!isset($parentBalances[$parentId])) {
|
||||
$parentBalances[$parentId] = 0.0;
|
||||
}
|
||||
$parentBalances[$parentId] += $balance;
|
||||
}
|
||||
|
||||
foreach ($extraByParent as $parentId => $extra) {
|
||||
if (!isset($parentBalances[$parentId])) {
|
||||
$parentBalances[$parentId] = 0.0;
|
||||
}
|
||||
$parentBalances[$parentId] += (float) $extra;
|
||||
}
|
||||
|
||||
$totalUnpaid = 0.0;
|
||||
foreach ($parentBalances as $balance) {
|
||||
if ($balance > 0.00001) {
|
||||
$totalUnpaid += $balance;
|
||||
}
|
||||
}
|
||||
|
||||
return $totalUnpaid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Finance;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class FinancialUnpaidParentsService
|
||||
{
|
||||
public function __construct(private FinancialSchoolYearService $schoolYears)
|
||||
{
|
||||
}
|
||||
|
||||
public function getUnpaidParents(?string $schoolYear): array
|
||||
{
|
||||
$schoolYear = trim((string) ($schoolYear ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = (string) (Configuration::getConfig('school_year') ?? date('Y'));
|
||||
}
|
||||
|
||||
$schoolYears = $this->schoolYears->listYears($schoolYear);
|
||||
|
||||
$invRows = DB::table('invoices as i')
|
||||
->select('i.id', 'i.parent_id', 'i.total_amount', 'u.firstname', 'u.lastname', 'u.email')
|
||||
->join('users as u', 'u.id', '=', 'i.parent_id')
|
||||
->where('i.school_year', $schoolYear)
|
||||
->orderBy('i.parent_id', 'ASC')
|
||||
->orderBy('i.id', 'ASC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$byParent = [];
|
||||
$hasStatus = Schema::hasColumn('payments', 'status');
|
||||
$hasVoid = Schema::hasColumn('payments', 'is_void');
|
||||
|
||||
foreach ($invRows as $row) {
|
||||
$invoiceId = (int) ($row['id'] ?? 0);
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
if ($invoiceId <= 0 || $parentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$paidQuery = DB::table('payments')
|
||||
->selectRaw('COALESCE(SUM(paid_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId);
|
||||
if ($hasStatus) {
|
||||
$paidQuery->where(function ($q) {
|
||||
$q->whereNotIn('status', [
|
||||
'void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled',
|
||||
])->orWhereNull('status');
|
||||
});
|
||||
}
|
||||
if ($hasVoid) {
|
||||
$paidQuery->where(function ($q) {
|
||||
$q->where('is_void', 0)->orWhereNull('is_void');
|
||||
});
|
||||
}
|
||||
$paidRow = (array) $paidQuery->first();
|
||||
$paidSum = (float) ($paidRow['tot'] ?? 0);
|
||||
|
||||
$discRow = (array) DB::table('discount_usages')
|
||||
->selectRaw('COALESCE(SUM(discount_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->first();
|
||||
$discSum = (float) ($discRow['tot'] ?? 0);
|
||||
|
||||
$refRow = (array) DB::table('refunds')
|
||||
->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->first();
|
||||
$refSum = (float) ($refRow['tot'] ?? 0);
|
||||
|
||||
$total = (float) ($row['total_amount'] ?? 0);
|
||||
$balance = max(0.0, round($total - $discSum - $paidSum - $refSum, 2));
|
||||
|
||||
if (!isset($byParent[$parentId])) {
|
||||
$byParent[$parentId] = [
|
||||
'parent_id' => $parentId,
|
||||
'firstname' => (string) ($row['firstname'] ?? ''),
|
||||
'lastname' => (string) ($row['lastname'] ?? ''),
|
||||
'email' => (string) ($row['email'] ?? ''),
|
||||
'total_invoice' => 0.0,
|
||||
'total_balance' => 0.0,
|
||||
'total_paid' => 0.0,
|
||||
'total_discount' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
$byParent[$parentId]['total_invoice'] += $total;
|
||||
$byParent[$parentId]['total_balance'] += $balance;
|
||||
$byParent[$parentId]['total_paid'] += $paidSum;
|
||||
$byParent[$parentId]['total_discount'] += $discSum;
|
||||
}
|
||||
|
||||
$extraRows = DB::table('additional_charges as ac')
|
||||
->selectRaw(
|
||||
"ac.parent_id, u.firstname, u.lastname, u.email, COALESCE(SUM(ac.amount), 0) AS total_extra",
|
||||
false
|
||||
)
|
||||
->leftJoin('users as u', 'u.id', '=', 'ac.parent_id')
|
||||
->where('ac.school_year', $schoolYear)
|
||||
->where('ac.status !=', 'void')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('ac.invoice_id')
|
||||
->orWhere('ac.invoice_id', 0)
|
||||
->orWhere('ac.status !=', 'applied');
|
||||
})
|
||||
->groupBy('ac.parent_id')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
foreach ($extraRows as $row) {
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
if ($parentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$extra = (float) ($row['total_extra'] ?? 0);
|
||||
if (abs($extra) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($byParent[$parentId])) {
|
||||
$byParent[$parentId] = [
|
||||
'parent_id' => $parentId,
|
||||
'firstname' => (string) ($row['firstname'] ?? ''),
|
||||
'lastname' => (string) ($row['lastname'] ?? ''),
|
||||
'email' => (string) ($row['email'] ?? ''),
|
||||
'total_invoice' => 0.0,
|
||||
'total_balance' => 0.0,
|
||||
'total_paid' => 0.0,
|
||||
'total_discount' => 0.0,
|
||||
];
|
||||
}
|
||||
$byParent[$parentId]['total_invoice'] += $extra;
|
||||
$byParent[$parentId]['total_balance'] += $extra;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$installmentEndRaw = (string) (Configuration::getConfig('installment_date') ?? '');
|
||||
$tzName = function_exists('user_timezone') ? (string) user_timezone() : config('app.timezone', 'UTC');
|
||||
$tz = new \DateTimeZone($tzName);
|
||||
$today = new \DateTimeImmutable('today', $tz);
|
||||
|
||||
$endDate = null;
|
||||
if ($installmentEndRaw !== '') {
|
||||
try {
|
||||
$endDate = new \DateTimeImmutable($installmentEndRaw, $tz);
|
||||
} catch (\Throwable $e) {
|
||||
$endDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
$monthsUntil = static function (? \DateTimeImmutable $end) use ($today): int {
|
||||
if (!$end) {
|
||||
return 0;
|
||||
}
|
||||
$y = (int) $end->format('Y') - (int) $today->format('Y');
|
||||
$m = (int) $end->format('n') - (int) $today->format('n');
|
||||
$months = $y * 12 + $m;
|
||||
if ((int) $end->format('j') > (int) $today->format('j')) {
|
||||
$months += 1;
|
||||
}
|
||||
return max(0, $months);
|
||||
};
|
||||
|
||||
foreach ($byParent as $agg) {
|
||||
$balance = (float) ($agg['total_balance'] ?? 0);
|
||||
if ($balance <= 0.00001) {
|
||||
continue;
|
||||
}
|
||||
$remMonths = $monthsUntil($endDate);
|
||||
$remainingInstallments = max(1, $remMonths);
|
||||
$instAmount = round($balance / $remainingInstallments, 2);
|
||||
|
||||
$rows[] = [
|
||||
'parent_id' => (int) ($agg['parent_id'] ?? 0),
|
||||
'firstname' => (string) ($agg['firstname'] ?? ''),
|
||||
'lastname' => (string) ($agg['lastname'] ?? ''),
|
||||
'email' => (string) ($agg['email'] ?? ''),
|
||||
'total_invoice' => (float) ($agg['total_invoice'] ?? 0),
|
||||
'total_balance' => $balance,
|
||||
'total_discount' => (float) ($agg['total_discount'] ?? 0),
|
||||
'remaining_installments' => $remainingInstallments,
|
||||
'installment_amount' => $instAmount,
|
||||
];
|
||||
}
|
||||
|
||||
usort($rows, static fn ($a, $b) => $b['total_balance'] <=> $a['total_balance']);
|
||||
|
||||
$nextInstallment = (new \DateTime('now', $tz))
|
||||
->modify('first day of next month')
|
||||
->format('Y-m-d');
|
||||
|
||||
$parentIds = array_values(array_unique(array_map(static fn ($row) => (int) ($row['parent_id'] ?? 0), $rows)));
|
||||
$hasPayments = [];
|
||||
$paidTotals = [];
|
||||
$paymentCounts = [];
|
||||
if (!empty($parentIds)) {
|
||||
$pRows = DB::table('payments')
|
||||
->selectRaw('parent_id, SUM(paid_amount) AS total_paid, COUNT(*) AS payment_count')
|
||||
->whereIn('parent_id', $parentIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->groupBy('parent_id')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
foreach ($pRows as $row) {
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
if ($parentId > 0) {
|
||||
$hasPayments[$parentId] = true;
|
||||
$paidTotals[$parentId] = (float) ($row['total_paid'] ?? 0);
|
||||
$paymentCounts[$parentId] = (int) ($row['payment_count'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results = array_map(function (array $row) use ($hasPayments, $paidTotals, $paymentCounts, $nextInstallment) {
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
$name = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? ''));
|
||||
return [
|
||||
'parent_id' => $parentId,
|
||||
'parent_name' => $name,
|
||||
'email' => (string) ($row['email'] ?? ''),
|
||||
'total_invoice' => (float) ($row['total_invoice'] ?? 0),
|
||||
'total_balance' => (float) ($row['total_balance'] ?? 0),
|
||||
'total_discount' => (float) ($row['total_discount'] ?? 0),
|
||||
'remaining_installments' => (int) ($row['remaining_installments'] ?? 0),
|
||||
'installment_amount' => (float) ($row['installment_amount'] ?? 0),
|
||||
'type' => isset($hasPayments[$parentId]) ? 'installment' : 'no_payment',
|
||||
'total_paid' => isset($paidTotals[$parentId]) ? (float) $paidTotals[$parentId] : 0.0,
|
||||
'payment_count' => isset($paymentCounts[$parentId]) ? (int) $paymentCounts[$parentId] : 0,
|
||||
'has_installment' => isset($hasPayments[$parentId]) ? 1 : 0,
|
||||
'next_installment' => $nextInstallment,
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
return [
|
||||
'schoolYear' => $schoolYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
'results' => $results,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user