add test batches
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
namespace App\Services\Finance;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -22,7 +21,7 @@ class StakeholderFinancialAnalysisService
|
||||
|
||||
$current = $this->periodMetrics($schoolYear, $rangeFrom, $rangeTo);
|
||||
$comparison = null;
|
||||
if (! empty($compareSchoolYear)) {
|
||||
if (!empty($compareSchoolYear)) {
|
||||
[$compareFrom, $compareTo] = $this->resolveDateRange($compareSchoolYear, null, null);
|
||||
$comparison = $this->periodMetrics($compareSchoolYear, $compareFrom, $compareTo);
|
||||
}
|
||||
@@ -71,7 +70,6 @@ class StakeholderFinancialAnalysisService
|
||||
foreach ($value as $bucket => $amount) {
|
||||
$rows[] = ['Receivables Aging', $bucket, $amount];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
$rows[] = ['Receivables', $key, is_scalar($value) ? $value : json_encode($value)];
|
||||
@@ -83,15 +81,14 @@ class StakeholderFinancialAnalysisService
|
||||
$rows[] = ['Payment Method Mix', $method, $amount];
|
||||
}
|
||||
foreach (($analysis['monthlyTrend'] ?? []) as $month => $data) {
|
||||
$rows[] = ['Monthly Trend', $month.' charges', $data['charges'] ?? 0];
|
||||
$rows[] = ['Monthly Trend', $month.' collections', $data['collections'] ?? 0];
|
||||
$rows[] = ['Monthly Trend', $month.' expenses', $data['expenses'] ?? 0];
|
||||
$rows[] = ['Monthly Trend', $month.' netCash', $data['netCash'] ?? 0];
|
||||
$rows[] = ['Monthly Trend', $month . ' charges', $data['charges'] ?? 0];
|
||||
$rows[] = ['Monthly Trend', $month . ' collections', $data['collections'] ?? 0];
|
||||
$rows[] = ['Monthly Trend', $month . ' expenses', $data['expenses'] ?? 0];
|
||||
$rows[] = ['Monthly Trend', $month . ' netCash', $data['netCash'] ?? 0];
|
||||
}
|
||||
foreach (($analysis['riskFlags'] ?? []) as $flag) {
|
||||
$rows[] = ['Risk Flag', $flag['level'] ?? '', $flag['message'] ?? ''];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
@@ -172,19 +169,18 @@ class StakeholderFinancialAnalysisService
|
||||
|
||||
private function invoiceTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
if (! Schema::hasTable('invoices')) {
|
||||
if (!Schema::hasTable('invoices')) {
|
||||
return ['total' => 0.0, 'count' => 0];
|
||||
}
|
||||
$q = DB::table('invoices')->where('school_year', $schoolYear);
|
||||
$this->applyDateRange($q, $this->dateColumn('invoices', ['issue_date', 'created_at']), $dateFrom, $dateTo);
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM(total_amount),0) as total, COUNT(*) as count')->first();
|
||||
|
||||
return ['total' => (float) ($row['total'] ?? 0), 'count' => (int) ($row['count'] ?? 0)];
|
||||
}
|
||||
|
||||
private function paymentTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
if (! Schema::hasTable('payments')) {
|
||||
if (!Schema::hasTable('payments')) {
|
||||
return ['total' => 0.0, 'familyCount' => 0, 'byMethod' => []];
|
||||
}
|
||||
$base = DB::table('payments')->where('school_year', $schoolYear);
|
||||
@@ -202,13 +198,12 @@ class StakeholderFinancialAnalysisService
|
||||
$arr = (array) $row;
|
||||
$byMethod[(string) ($arr['method'] ?? 'unknown')] = (float) ($arr['total'] ?? 0);
|
||||
}
|
||||
|
||||
return ['total' => $total, 'familyCount' => $familyCount, 'byMethod' => $byMethod];
|
||||
}
|
||||
|
||||
private function refundTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
if (! Schema::hasTable('refunds')) {
|
||||
if (!Schema::hasTable('refunds')) {
|
||||
return ['total' => 0.0];
|
||||
}
|
||||
$q = DB::table('refunds')->where('school_year', $schoolYear);
|
||||
@@ -217,14 +212,13 @@ class StakeholderFinancialAnalysisService
|
||||
}
|
||||
$this->applyDateRange($q, $this->dateColumn('refunds', ['refunded_at', 'approved_at', 'created_at']), $dateFrom, $dateTo);
|
||||
$amountColumn = Schema::hasColumn('refunds', 'refund_paid_amount') ? 'refund_paid_amount' : 'refund_amount';
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM('.$amountColumn.'),0) as total')->first();
|
||||
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM(' . $amountColumn . '),0) as total')->first();
|
||||
return ['total' => (float) ($row['total'] ?? 0)];
|
||||
}
|
||||
|
||||
private function discountTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
if (! Schema::hasTable('discount_usages')) {
|
||||
if (!Schema::hasTable('discount_usages')) {
|
||||
return ['total' => 0.0];
|
||||
}
|
||||
$q = DB::table('discount_usages as du');
|
||||
@@ -234,21 +228,19 @@ class StakeholderFinancialAnalysisService
|
||||
$this->excludeStatuses($q, 'discount_usages', $this->excludedChargeStatuses, 'du.status');
|
||||
$this->applyDateRange($q, Schema::hasColumn('discount_usages', 'created_at') ? 'du.created_at' : null, $dateFrom, $dateTo);
|
||||
$amountColumn = Schema::hasColumn('discount_usages', 'discount_amount') ? 'discount_amount' : 'amount';
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM(du.'.$amountColumn.'),0) as total')->first();
|
||||
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM(du.' . $amountColumn . '),0) as total')->first();
|
||||
return ['total' => (float) ($row['total'] ?? 0)];
|
||||
}
|
||||
|
||||
private function additionalChargeTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
if (! Schema::hasTable('additional_charges')) {
|
||||
if (!Schema::hasTable('additional_charges')) {
|
||||
return ['total' => 0.0];
|
||||
}
|
||||
$q = DB::table('additional_charges')->where('school_year', $schoolYear);
|
||||
$this->excludeStatuses($q, 'additional_charges', $this->excludedChargeStatuses);
|
||||
$this->applyDateRange($q, $this->dateColumn('additional_charges', ['due_date', 'created_at']), $dateFrom, $dateTo);
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM(amount),0) as total')->first();
|
||||
|
||||
return ['total' => (float) ($row['total'] ?? 0)];
|
||||
}
|
||||
|
||||
@@ -268,21 +260,20 @@ class StakeholderFinancialAnalysisService
|
||||
return ['total' => 0.0];
|
||||
}
|
||||
$this->applyDateRange($q, $this->dateColumn($table, ['charge_date', 'created_at']), $dateFrom, $dateTo);
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM('.$amountColumn.'),0) as total')->first();
|
||||
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM(' . $amountColumn . '),0) as total')->first();
|
||||
return ['total' => (float) ($row['total'] ?? 0)];
|
||||
}
|
||||
|
||||
private function expenseTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
if (! Schema::hasTable('expenses')) {
|
||||
if (!Schema::hasTable('expenses')) {
|
||||
return ['total' => 0.0, 'byCategory' => []];
|
||||
}
|
||||
$q = DB::table('expenses')->where('school_year', $schoolYear);
|
||||
$this->excludeStatuses($q, 'expenses', ['void', 'voided', 'rejected', 'cancelled', 'canceled']);
|
||||
$this->applyDateRange($q, $this->dateColumn('expenses', ['expense_date', 'date', 'created_at']), $dateFrom, $dateTo);
|
||||
$amountColumn = Schema::hasColumn('expenses', 'amount') ? 'amount' : 'total_amount';
|
||||
$total = (float) ((array) (clone $q)->selectRaw('COALESCE(SUM('.$amountColumn.'),0) as total')->first())['total'];
|
||||
$total = (float) ((array) (clone $q)->selectRaw('COALESCE(SUM(' . $amountColumn . '),0) as total')->first())['total'];
|
||||
$categoryColumn = Schema::hasColumn('expenses', 'category') ? 'category' : (Schema::hasColumn('expenses', 'expense_category') ? 'expense_category' : null);
|
||||
$byCategory = [];
|
||||
if ($categoryColumn) {
|
||||
@@ -291,13 +282,12 @@ class StakeholderFinancialAnalysisService
|
||||
$byCategory[(string) ($arr['category'] ?? 'uncategorized')] = (float) ($arr['total'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
return ['total' => $total, 'byCategory' => $byCategory];
|
||||
}
|
||||
|
||||
private function reimbursementTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
if (! Schema::hasTable('reimbursements')) {
|
||||
if (!Schema::hasTable('reimbursements')) {
|
||||
return ['total' => 0.0];
|
||||
}
|
||||
$q = DB::table('reimbursements')->where('school_year', $schoolYear);
|
||||
@@ -306,8 +296,7 @@ class StakeholderFinancialAnalysisService
|
||||
}
|
||||
$this->applyDateRange($q, $this->dateColumn('reimbursements', ['reimbursed_at', 'created_at']), $dateFrom, $dateTo);
|
||||
$amountColumn = Schema::hasColumn('reimbursements', 'amount') ? 'amount' : (Schema::hasColumn('reimbursements', 'total_amount') ? 'total_amount' : 'reimbursement_amount');
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM('.$amountColumn.'),0) as total')->first();
|
||||
|
||||
$row = (array) $q->selectRaw('COALESCE(SUM(' . $amountColumn . '),0) as total')->first();
|
||||
return ['total' => (float) ($row['total'] ?? 0)];
|
||||
}
|
||||
|
||||
@@ -315,7 +304,7 @@ class StakeholderFinancialAnalysisService
|
||||
{
|
||||
$buckets = ['current' => 0.0, '1_30_days' => 0.0, '31_60_days' => 0.0, '61_90_days' => 0.0, 'over_90_days' => 0.0];
|
||||
$largest = [];
|
||||
if (! Schema::hasTable('invoices')) {
|
||||
if (!Schema::hasTable('invoices')) {
|
||||
return ['total' => 0.0, 'buckets' => $buckets, 'largestOpenInvoices' => []];
|
||||
}
|
||||
$q = DB::table('invoices')->where('school_year', $schoolYear);
|
||||
@@ -338,8 +327,8 @@ class StakeholderFinancialAnalysisService
|
||||
$dueRaw = $arr['due_date'] ?? $arr['created_at'] ?? null;
|
||||
$days = 0;
|
||||
try {
|
||||
if (! empty($dueRaw)) {
|
||||
$days = max(0, (int) Carbon::parse($dueRaw)->startOfDay()->diffInDays($today, false));
|
||||
if (!empty($dueRaw)) {
|
||||
$days = max(0, (int) \Illuminate\Support\Carbon::parse($dueRaw)->startOfDay()->diffInDays($today, false));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$days = 0;
|
||||
@@ -355,7 +344,6 @@ class StakeholderFinancialAnalysisService
|
||||
];
|
||||
}
|
||||
usort($largest, fn ($a, $b) => (float) $b['balance'] <=> (float) $a['balance']);
|
||||
|
||||
return ['total' => $total, 'buckets' => $buckets, 'largestOpenInvoices' => array_slice($largest, 0, 10)];
|
||||
}
|
||||
|
||||
@@ -379,13 +367,12 @@ class StakeholderFinancialAnalysisService
|
||||
];
|
||||
}
|
||||
ksort($months);
|
||||
|
||||
return $months;
|
||||
}
|
||||
|
||||
private function monthlySum(string $table, string $amountColumn, string $schoolYear, ?string $dateColumn, ?string $dateFrom, ?string $dateTo, array $excludedStatuses = []): array
|
||||
{
|
||||
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $amountColumn) || $dateColumn === null) {
|
||||
if (!Schema::hasTable($table) || !Schema::hasColumn($table, $amountColumn) || $dateColumn === null) {
|
||||
return [];
|
||||
}
|
||||
$q = DB::table($table);
|
||||
@@ -398,18 +385,17 @@ class StakeholderFinancialAnalysisService
|
||||
$monthExpr = $driver === 'sqlite'
|
||||
? "strftime('%Y-%m', $dateColumn)"
|
||||
: "DATE_FORMAT($dateColumn, '%Y-%m')";
|
||||
$rows = $q->selectRaw($monthExpr.' as month, COALESCE(SUM('.$amountColumn.'),0) as total')
|
||||
$rows = $q->selectRaw($monthExpr . ' as month, COALESCE(SUM(' . $amountColumn . '),0) as total')
|
||||
->groupBy('month')
|
||||
->orderBy('month')
|
||||
->get();
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
$arr = (array) $row;
|
||||
if (! empty($arr['month'])) {
|
||||
if (!empty($arr['month'])) {
|
||||
$out[(string) $arr['month']] = (float) ($arr['total'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
@@ -424,7 +410,6 @@ class StakeholderFinancialAnalysisService
|
||||
if ($comparison) {
|
||||
$summary['comparison'] = $this->deltas($current['keyMetrics'], $comparison['keyMetrics']);
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
@@ -446,7 +431,6 @@ class StakeholderFinancialAnalysisService
|
||||
if (empty($flags)) {
|
||||
$flags[] = ['level' => 'ok', 'message' => 'No automatic risk flags were triggered for the selected period.'];
|
||||
}
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
@@ -464,17 +448,15 @@ class StakeholderFinancialAnalysisService
|
||||
'changePct' => $prev != 0.0 ? $this->percent((($cur - $prev) / abs($prev)) * 100) : null,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function resolveDateRange(string $schoolYear, ?string $dateFrom, ?string $dateTo): array
|
||||
{
|
||||
if ((! $dateFrom || ! $dateTo) && preg_match('/^(\d{4})\D?(\d{4})$/', $schoolYear, $m)) {
|
||||
$dateFrom = $dateFrom ?: $m[1].'-01-01';
|
||||
$dateTo = $dateTo ?: $m[2].'-12-31';
|
||||
if ((!$dateFrom || !$dateTo) && preg_match('/^(\d{4})\D?(\d{4})$/', $schoolYear, $m)) {
|
||||
$dateFrom = $dateFrom ?: $m[1] . '-01-01';
|
||||
$dateTo = $dateTo ?: $m[2] . '-12-31';
|
||||
}
|
||||
|
||||
return [$dateFrom, $dateTo];
|
||||
}
|
||||
|
||||
@@ -483,17 +465,17 @@ class StakeholderFinancialAnalysisService
|
||||
if ($column === null) {
|
||||
return;
|
||||
}
|
||||
if (! empty($dateFrom)) {
|
||||
$query->whereRaw('DATE('.$column.') >= ?', [$dateFrom]);
|
||||
if (!empty($dateFrom)) {
|
||||
$query->whereRaw('DATE(' . $column . ') >= ?', [$dateFrom]);
|
||||
}
|
||||
if (! empty($dateTo)) {
|
||||
$query->whereRaw('DATE('.$column.') <= ?', [$dateTo]);
|
||||
if (!empty($dateTo)) {
|
||||
$query->whereRaw('DATE(' . $column . ') <= ?', [$dateTo]);
|
||||
}
|
||||
}
|
||||
|
||||
private function excludeStatuses($query, string $table, array $statuses, ?string $qualifiedColumn = null): void
|
||||
{
|
||||
if (empty($statuses) || ! Schema::hasColumn($table, 'status')) {
|
||||
if (empty($statuses) || !Schema::hasColumn($table, 'status')) {
|
||||
return;
|
||||
}
|
||||
$column = $qualifiedColumn ?: 'status';
|
||||
@@ -504,7 +486,7 @@ class StakeholderFinancialAnalysisService
|
||||
|
||||
private function dateColumn(string $table, array $candidates): ?string
|
||||
{
|
||||
if (! Schema::hasTable($table)) {
|
||||
if (!Schema::hasTable($table)) {
|
||||
return null;
|
||||
}
|
||||
foreach ($candidates as $column) {
|
||||
@@ -512,7 +494,6 @@ class StakeholderFinancialAnalysisService
|
||||
return $column;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user