where('parent_id', $parentId) ->when($schoolYear, fn ($q) => $q->where('school_year', $schoolYear)) ->orderByDesc('created_at') ->get(); $invoiceIds = $rows ->pluck('id') ->map(fn ($id) => (int) $id) ->filter(fn ($id) => $id > 0) ->values() ->all(); $paymentsByInvoice = $this->paymentsByInvoice($parentId, $invoiceIds); $adjustments = $this->invoiceAdjustments($invoiceIds); return $rows ->map(function (Invoice $invoice) use ($paymentsByInvoice, $adjustments): array { $invoiceRow = $invoice->toArray(); $invoiceId = (int) $invoice->id; $payments = $paymentsByInvoice[(int) $invoice->id] ?? []; $paidAmount = round(array_sum(array_map( static fn (array $payment): float => (float) ($payment['paid_amount'] ?? 0), $payments )), 2); $discountAmount = round((float) ($adjustments['discounts'][$invoiceId] ?? 0), 2); $refundAmount = round((float) ($adjustments['refunds'][$invoiceId] ?? 0), 2); $totalAmount = round((float) ($invoice->total_amount ?? 0), 2); $balance = max(0.0, round($totalAmount - $paidAmount - $discountAmount - $refundAmount, 2)); $invoiceRow['payments'] = $payments; $invoiceRow['paid_amount'] = $paidAmount; $invoiceRow['discount'] = $discountAmount; $invoiceRow['refund_amount'] = $refundAmount; $invoiceRow['balance'] = $balance; $invoiceRow['last_paid_amount'] = (float) ($payments[0]['paid_amount'] ?? 0); $invoiceRow['last_payment_date'] = $payments[0]['payment_date'] ?? null; $invoiceRow['status'] = $this->deriveInvoiceStatus($invoice->status, $paidAmount, $balance); return $invoiceRow; }) ->all(); } public function statement(int $parentId, ?string $schoolYear = null): array { $parent = User::query()->find($parentId); $account = $schoolYear ? ParentAccount::query() ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->first() : null; $columns = [ 'id', 'invoice_number', 'total_amount', 'paid_amount', 'balance', 'status', 'description', 'school_year', ]; if (Schema::hasColumn('invoices', 'invoice_type')) { $columns[] = 'invoice_type'; } if (Schema::hasColumn('invoices', 'balance_transfer_id')) { $columns[] = 'balance_transfer_id'; } $invoices = Invoice::query() ->where('parent_id', $parentId) ->when($schoolYear, fn ($q) => $q->where('school_year', $schoolYear)) ->orderBy('created_at') ->get($columns); $transferSourceYears = $this->transferSourceYears($invoices->pluck('balance_transfer_id')->filter()->all()); $invoiceIds = $invoices ->pluck('id') ->map(fn ($id) => (int) $id) ->filter(fn ($id) => $id > 0) ->values() ->all(); $adjustments = $this->invoiceAdjustments($invoiceIds); $lines = $invoices ->map(function (Invoice $invoice) use ($transferSourceYears, $adjustments): array { $transferId = (int) ($invoice->balance_transfer_id ?? 0); $invoiceId = (int) $invoice->id; $totalAmount = round((float) ($invoice->total_amount ?? 0), 2); $paidAmount = round((float) ($adjustments['payments'][$invoiceId] ?? $invoice->paid_amount ?? 0), 2); $discountAmount = round((float) ($adjustments['discounts'][$invoiceId] ?? 0), 2); $refundAmount = round((float) ($adjustments['refunds'][$invoiceId] ?? 0), 2); $balance = max(0.0, round($totalAmount - $paidAmount - $discountAmount - $refundAmount, 2)); return [ 'label' => $invoice->description ?: ($invoice->invoice_number ?: sprintf('Invoice #%d', $invoice->id)), 'amount' => $balance, 'source_school_year' => $transferId > 0 ? ($transferSourceYears[$transferId] ?? $invoice->school_year) : $invoice->school_year, 'invoice_id' => $invoice->id, ]; }) ->values() ->all(); $openingBalance = round((float) ($account?->opening_balance ?? 0), 2); $currentBalance = round($openingBalance + array_sum(array_map( static fn (array $line): float => (float) ($line['amount'] ?? 0), $lines )), 2); return [ 'parent_id' => $parentId, 'parent_name' => $this->parentName($parent), 'school_year' => $schoolYear, 'lines' => $lines, 'opening_balance' => $openingBalance, 'current_balance' => round($currentBalance, 2), ]; } private function parentName(?User $parent): ?string { if (! $parent) { return null; } $parts = array_filter([ trim((string) ($parent->firstname ?? '')), trim((string) ($parent->lastname ?? '')), ]); if ($parts !== []) { return implode(' ', $parts); } return $parent->name ?? $parent->email ?? null; } /** * @param list $invoiceIds * @return array>> */ private function paymentsByInvoice(int $parentId, array $invoiceIds): array { if ($invoiceIds === [] || ! Schema::hasTable('payments')) { return []; } $columns = [ 'id', 'parent_id', 'invoice_id', 'paid_amount', Schema::hasColumn('payments', 'balance') ? 'balance' : DB::raw('NULL AS balance'), Schema::hasColumn('payments', 'payment_method') ? 'payment_method' : DB::raw('NULL AS payment_method'), Schema::hasColumn('payments', 'payment_date') ? 'payment_date' : DB::raw('NULL AS payment_date'), Schema::hasColumn('payments', 'transaction_id') ? 'transaction_id' : DB::raw('NULL AS transaction_id'), Schema::hasColumn('payments', 'status') ? 'status' : DB::raw('NULL AS status'), Schema::hasColumn('payments', 'school_year') ? 'school_year' : DB::raw('NULL AS school_year'), Schema::hasColumn('payments', 'semester') ? 'semester' : DB::raw('NULL AS semester'), Schema::hasColumn('payments', 'created_at') ? 'created_at' : DB::raw('NULL AS created_at'), ]; $query = DB::table('payments') ->select($columns) ->where('parent_id', $parentId) ->whereIn('invoice_id', $invoiceIds); if (Schema::hasColumn('payments', 'status')) { $query->whereNotIn(DB::raw('LOWER(status)'), ['pending', 'void', 'voided', 'cancelled', 'canceled', 'failed', 'rejected']); } if (Schema::hasColumn('payments', 'payment_date')) { $query->orderByDesc('payment_date'); } if (Schema::hasColumn('payments', 'created_at')) { $query->orderByDesc('created_at'); } $rows = $query ->get() ->map(static fn ($row): array => [ 'id' => (int) $row->id, 'parent_id' => (int) $row->parent_id, 'invoice_id' => (int) $row->invoice_id, 'paid_amount' => (float) $row->paid_amount, 'balance' => (float) $row->balance, 'payment_method' => $row->payment_method, 'payment_date' => $row->payment_date, 'transaction_id' => $row->transaction_id, 'status' => $row->status, 'school_year' => $row->school_year, 'semester' => $row->semester, 'created_at' => $row->created_at, ]) ->all(); $grouped = []; foreach ($rows as $row) { $grouped[(int) $row['invoice_id']][] = $row; } return $grouped; } /** * @param list $invoiceIds * @return array{payments: array, discounts: array, refunds: array} */ private function invoiceAdjustments(array $invoiceIds): array { $invoiceIds = array_values(array_unique(array_filter(array_map('intval', $invoiceIds)))); $adjustments = [ 'payments' => [], 'discounts' => [], 'refunds' => [], ]; if ($invoiceIds === []) { return $adjustments; } if (Schema::hasTable('payments')) { $paymentQuery = DB::table('payments') ->selectRaw('invoice_id, COALESCE(SUM(paid_amount),0) AS total') ->whereIn('invoice_id', $invoiceIds); if (Schema::hasColumn('payments', 'status')) { $paymentQuery->where(function ($query) { $query->whereNotIn(DB::raw('LOWER(status)'), [ 'pending', 'void', 'voided', 'cancelled', 'canceled', 'failed', 'rejected', 'refunded', 'chargeback', 'declined', 'reversed', ])->orWhereNull('status'); }); } $adjustments['payments'] = $paymentQuery ->groupBy('invoice_id') ->pluck('total', 'invoice_id') ->map(fn ($value) => (float) $value) ->all(); } if (Schema::hasTable('discount_usages')) { $adjustments['discounts'] = DB::table('discount_usages') ->selectRaw('invoice_id, COALESCE(SUM(discount_amount),0) AS total') ->whereIn('invoice_id', $invoiceIds) ->groupBy('invoice_id') ->pluck('total', 'invoice_id') ->map(fn ($value) => (float) $value) ->all(); } if (Schema::hasTable('refunds')) { $adjustments['refunds'] = DB::table('refunds') ->selectRaw('invoice_id, COALESCE(SUM(refund_paid_amount),0) AS total') ->whereIn('invoice_id', $invoiceIds) ->whereIn('status', ['Partial', 'Paid']) ->groupBy('invoice_id') ->pluck('total', 'invoice_id') ->map(fn ($value) => (float) $value) ->all(); } return $adjustments; } private function deriveInvoiceStatus(?string $storedStatus, float $paidAmount, float $balance): string { if ($paidAmount <= 0 && $balance > 0) { return 'Unpaid'; } if ($balance <= 0) { return 'Paid'; } if ($paidAmount > 0) { return 'Partially Paid'; } return $storedStatus ?: 'Unpaid'; } /** * @param array $transferIds * @return array */ private function transferSourceYears(array $transferIds): array { $ids = array_values(array_unique(array_map('intval', $transferIds))); if ($ids === []) { return []; } return ParentBalanceTransfer::query() ->whereIn('id', $ids) ->pluck('from_school_year', 'id') ->all(); } }