$studentIds * @return Collection> daftar academic_term_id per student_id yang sudah punya tagihan */ public function getInvoicedTermIdsByStudent(array $studentIds): Collection { return TuitionInvoice::query() ->whereIn('student_id', $studentIds) ->get(['student_id', 'academic_term_id']) ->groupBy('student_id') ->map(fn ($rows) => $rows->pluck('academic_term_id')->values()->all()); } public function paginated( int $perPage = 25, string $search = '', ?int $academicTermId = null, ?string $status = null, ?string $paymentMethod = null, ): LengthAwarePaginator { return $this->scopedQuery($search, $academicTermId) ->select(['id', 'student_id', 'academic_term_id', 'amount_due', 'due_date']) ->withSum('payments as paid_total', 'amount_paid') ->withCount('payments') ->with(['student.user.profile', 'student.department', 'academicTerm:id,name,semester,start_date,end_date']) ->when($status, fn ($q) => match ($status) { 'paid' => $q->havingRaw('COALESCE(paid_total, 0) >= amount_due'), 'partial' => $q->havingRaw('COALESCE(paid_total, 0) > 0 AND COALESCE(paid_total, 0) < amount_due'), 'unpaid' => $q->havingRaw('COALESCE(paid_total, 0) <= 0'), default => $q, }) ->when($paymentMethod, fn ($q) => $q->whereHas('payments', fn ($q) => $q->where('payment_method', $paymentMethod))) ->latest() ->paginate($perPage); } /** * @return array{unpaid_students: int, partial_students: int, paid_students: int, total_outstanding: float, total_collected: float, total_invoices: int} */ public function summary(string $search = '', ?int $academicTermId = null): array { $invoices = $this->scopedQuery($search, $academicTermId) ->select(['id', 'student_id', 'amount_due']) ->withSum('payments as paid_total', 'amount_paid') ->get(); $totalCollected = (float) $invoices->sum('paid_total'); $totalOutstanding = (float) $invoices->sum(fn (TuitionInvoice $invoice) => max(0, $invoice->amount_due - ($invoice->paid_total ?? 0))); $studentsByStatus = fn (\Closure $matches) => $invoices->filter($matches)->pluck('student_id')->unique()->count(); return [ 'unpaid_students' => $studentsByStatus(fn (TuitionInvoice $invoice) => (float) ($invoice->paid_total ?? 0) <= 0), 'partial_students' => $studentsByStatus(fn (TuitionInvoice $invoice) => (float) ($invoice->paid_total ?? 0) > 0 && (float) ($invoice->paid_total ?? 0) < (float) $invoice->amount_due), 'paid_students' => $studentsByStatus(fn (TuitionInvoice $invoice) => (float) ($invoice->paid_total ?? 0) >= (float) $invoice->amount_due), 'total_outstanding' => $totalOutstanding, 'total_collected' => $totalCollected, 'total_invoices' => $invoices->count(), ]; } private function scopedQuery(string $search = '', ?int $academicTermId = null) { return TuitionInvoice::query() ->when($search, fn ($q) => $q->whereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%") ->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")))) ->when($academicTermId, fn ($q) => $q->where('academic_term_id', $academicTermId)); } public function createMany(array $studentIds, array $data): void { DB::transaction(function () use ($studentIds, $data) { foreach ($studentIds as $studentId) { TuitionInvoice::create([ 'student_id' => $studentId, 'academic_term_id' => $data['academic_term_id'], 'amount_due' => $data['amount_due'], 'due_date' => $data['due_date'], ]); } }); } public function update(TuitionInvoice $invoice, array $data): TuitionInvoice { $invoice->student_id = $data['student_id']; $invoice->academic_term_id = $data['academic_term_id']; $invoice->amount_due = $data['amount_due']; $invoice->due_date = $data['due_date'] ?? null; $invoice->update(); return $invoice; } public function delete(TuitionInvoice $invoice): bool { return $invoice->delete(); } }