426 lines
15 KiB
PHP
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];
|
|
}
|
|
}
|