Files
database-kesiswaan/tests/Feature/StudentPromotionTest.php
T

426 lines
15 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\AcademicYear;
use App\Models\Department;
use App\Models\SchoolClass;
use App\Models\Student;
use App\Models\StudentAcademicHistory;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class StudentPromotionTest extends TestCase
{
use RefreshDatabase;
public function test_index_shows_class_summary_instead_of_loading_all_students_when_class_is_not_selected(): void
{
$admin = User::factory()->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 <span class="font-semibold">kelas asal</span> 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];
}
}