create(['role' => 'admin']); [$fromYear, $toYear, $department, $fromClass] = $this->createPromotionContext(); Student::create([ 'student_number' => 'S-100', 'full_name' => 'Ahmad Ringkasan', 'gender' => 'L', 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'active', ]); $response = $this->actingAs($admin)->get(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, ])); $response->assertOk(); $response->assertSee('Pilih kelas asal untuk memproses siswa per rombel.', false); $response->assertSee($fromClass->name); $response->assertSee('Proses kelas ini'); $response->assertDontSee('Ahmad Ringkasan'); } public function test_admin_can_promote_student_and_create_new_academic_history(): void { $admin = User::factory()->create(['role' => 'admin']); [$fromYear, $toYear, $department, $fromClass, $targetClass] = $this->createPromotionContext(); $student = Student::create([ 'student_number' => 'S-001', 'full_name' => 'Budi Santoso', 'gender' => 'L', 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'active', ]); StudentAcademicHistory::create([ 'student_id' => $student->id, 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'active', 'start_date' => '2025-07-01', 'is_current' => true, ]); $response = $this->actingAs($admin)->post(route('student-promotions.store'), [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, 'promotions' => [ [ 'student_id' => $student->id, 'target_class_id' => $targetClass->id, 'action' => 'promote', 'notes' => 'Naik kelas otomatis hasil akhir tahun.', ], ], ]); $response->assertRedirect(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ])); $student->refresh(); $this->assertSame($toYear->id, $student->academic_year_id); $this->assertSame($targetClass->id, $student->class_id); $this->assertSame('active', $student->status); $this->assertDatabaseHas('student_academic_histories', [ 'student_id' => $student->id, 'academic_year_id' => $fromYear->id, 'class_id' => $fromClass->id, 'status' => 'promoted', 'is_current' => 0, 'notes' => 'Naik kelas otomatis hasil akhir tahun.', ]); $this->assertDatabaseHas('student_academic_histories', [ 'student_id' => $student->id, 'academic_year_id' => $toYear->id, 'class_id' => $targetClass->id, 'status' => 'active', 'is_current' => 1, 'notes' => 'Naik kelas otomatis hasil akhir tahun.', ]); } public function test_promotion_is_rejected_when_target_class_exceeds_capacity(): void { $admin = User::factory()->create(['role' => 'admin']); [$fromYear, $toYear, $department, $fromClass, $targetClass] = $this->createPromotionContext([ 'target_capacity' => 1, ]); Student::create([ 'student_number' => 'S-EXIST', 'full_name' => 'Siswa Existing', 'gender' => 'L', 'academic_year_id' => $toYear->id, 'department_id' => $department->id, 'class_id' => $targetClass->id, 'status' => 'active', ]); $student = Student::create([ 'student_number' => 'S-002', 'full_name' => 'Citra Lestari', 'gender' => 'P', 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'active', ]); StudentAcademicHistory::create([ 'student_id' => $student->id, 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'active', 'start_date' => '2025-07-01', 'is_current' => true, ]); $response = $this->from(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ]))->actingAs($admin)->post(route('student-promotions.store'), [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, 'promotions' => [ [ 'student_id' => $student->id, 'target_class_id' => $targetClass->id, 'action' => 'promote', 'notes' => 'Coba promosi ke kelas penuh.', ], ], ]); $response->assertRedirect(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ])); $response->assertSessionHas('errors'); $this->assertStringContainsString( "Kelas {$targetClass->name} melebihi kapasitas. Sisa kuota 0, tetapi alokasi promosi/repeat yang dipilih 1.", session('errors')->first() ); $student->refresh(); $this->assertSame($fromYear->id, $student->academic_year_id); $this->assertSame($fromClass->id, $student->class_id); $this->assertDatabaseMissing('student_academic_histories', [ 'student_id' => $student->id, 'academic_year_id' => $toYear->id, ]); } public function test_promotion_is_rejected_when_student_already_has_target_year_history(): void { $admin = User::factory()->create(['role' => 'admin']); [$fromYear, $toYear, $department, $fromClass, $targetClass] = $this->createPromotionContext(); $student = Student::create([ 'student_number' => 'S-003', 'full_name' => 'Dewi Anjani', 'gender' => 'P', 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'active', ]); StudentAcademicHistory::create([ 'student_id' => $student->id, 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'active', 'start_date' => '2025-07-01', 'is_current' => true, ]); StudentAcademicHistory::create([ 'student_id' => $student->id, 'academic_year_id' => $toYear->id, 'department_id' => $department->id, 'class_id' => $targetClass->id, 'status' => 'active', 'start_date' => '2026-07-01', 'is_current' => false, ]); $response = $this->from(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ]))->actingAs($admin)->post(route('student-promotions.store'), [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, 'promotions' => [ [ 'student_id' => $student->id, 'target_class_id' => $targetClass->id, 'action' => 'promote', 'notes' => 'Tidak boleh dobel histori.', ], ], ]); $response->assertRedirect(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ])); $response->assertSessionHasErrors([ 'promotions.0.student_id' => 'Siswa Dewi Anjani sudah memiliki histori akademik pada tahun ajaran tujuan.', ]); $student->refresh(); $this->assertSame($fromYear->id, $student->academic_year_id); $this->assertSame($fromClass->id, $student->class_id); $this->assertDatabaseCount('student_academic_histories', 2); } public function test_non_active_students_are_hidden_from_promotion_index_and_rejected_if_posted(): void { $admin = User::factory()->create(['role' => 'admin']); [$fromYear, $toYear, $department, $fromClass, $targetClass] = $this->createPromotionContext(); $droppedStudent = Student::create([ 'student_number' => 'S-004', 'full_name' => 'Eko Keluar', 'gender' => 'L', 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'dropped', ]); $transferredStudent = Student::create([ 'student_number' => 'S-005', 'full_name' => 'Fani Pindah', 'gender' => 'P', 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'transferred', ]); $activeStudent = Student::create([ 'student_number' => 'S-006', 'full_name' => 'Gilang Aktif', 'gender' => 'L', 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'class_id' => $fromClass->id, 'status' => 'active', ]); $response = $this->actingAs($admin)->get(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ])); $response->assertOk(); $response->assertSee('Gilang Aktif'); $response->assertDontSee('Eko Keluar'); $response->assertDontSee('Fani Pindah'); $droppedPost = $this->from(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ]))->actingAs($admin)->post(route('student-promotions.store'), [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, 'promotions' => [ [ 'student_id' => $droppedStudent->id, 'target_class_id' => $targetClass->id, 'action' => 'promote', 'notes' => 'Tidak boleh ikut promosi.', ], ], ]); $droppedPost->assertRedirect(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ])); $droppedPost->assertSessionHasErrors([ 'promotions.0.student_id' => 'Siswa Eko Keluar tidak berstatus aktif sehingga tidak bisa diproses promosi.', ]); $transferredPost = $this->from(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ]))->actingAs($admin)->post(route('student-promotions.store'), [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, 'promotions' => [ [ 'student_id' => $transferredStudent->id, 'target_class_id' => $targetClass->id, 'action' => 'promote', 'notes' => 'Tidak boleh ikut promosi.', ], ], ]); $transferredPost->assertRedirect(route('student-promotions.index', [ 'from_academic_year_id' => $fromYear->id, 'to_academic_year_id' => $toYear->id, 'class_id' => $fromClass->id, ])); $transferredPost->assertSessionHasErrors([ 'promotions.0.student_id' => 'Siswa Fani Pindah tidak berstatus aktif sehingga tidak bisa diproses promosi.', ]); $droppedStudent->refresh(); $transferredStudent->refresh(); $activeStudent->refresh(); $this->assertSame('dropped', $droppedStudent->status); $this->assertSame('transferred', $transferredStudent->status); $this->assertSame('active', $activeStudent->status); $this->assertDatabaseMissing('student_academic_histories', [ 'student_id' => $droppedStudent->id, 'academic_year_id' => $toYear->id, ]); $this->assertDatabaseMissing('student_academic_histories', [ 'student_id' => $transferredStudent->id, 'academic_year_id' => $toYear->id, ]); } protected function createPromotionContext(array $overrides = []): array { $fromYear = AcademicYear::create([ 'name' => '2025/2026', 'start_year' => 2025, 'end_year' => 2026, 'is_active' => true, ]); $toYear = AcademicYear::create([ 'name' => '2026/2027', 'start_year' => 2026, 'end_year' => 2027, 'is_active' => false, ]); $department = Department::create([ 'code' => 'RPL', 'name' => 'Rekayasa Perangkat Lunak', 'is_active' => true, ]); $fromClass = SchoolClass::create([ 'academic_year_id' => $fromYear->id, 'department_id' => $department->id, 'name' => 'X RPL 1', 'level' => 'X', 'capacity' => 36, 'is_active' => true, ]); $targetClass = SchoolClass::create([ 'academic_year_id' => $toYear->id, 'department_id' => $department->id, 'name' => 'XI RPL 1', 'level' => 'XI', 'capacity' => $overrides['target_capacity'] ?? 36, 'is_active' => true, ]); return [$fromYear, $toYear, $department, $fromClass, $targetClass]; } }