fix: violation alpha sync per (siswa, tanggal) — hapus violation lama saat push koreksi tanpa alpha
Bug: $alphaDate global null saat push tanpa alpha → violation lama tak pernah dihapus (false positive); totalAlpha dihitung utk semua siswa dgn tanggal global → rawan salah siswa/tanggal. Kini iterasi per (student, date) dari push. + test AttendanceSyncViolationFixTest (4 test).
This commit is contained in:
@@ -90,8 +90,7 @@ class AttendanceSyncController extends Controller
|
|||||||
$classNameMap = $students->pluck('class_name', 'id');
|
$classNameMap = $students->pluck('class_name', 'id');
|
||||||
|
|
||||||
$alphaCounts = [];
|
$alphaCounts = [];
|
||||||
$alphaDate = null;
|
$datesByStudent = []; // student_id => [date => true] — untuk pembersihan violation per tanggal
|
||||||
$allStudentIdsInPush = []; // track all students for cleanup
|
|
||||||
|
|
||||||
foreach ($attendances as $item) {
|
foreach ($attendances as $item) {
|
||||||
$sourceId = $item['source_student_id'];
|
$sourceId = $item['source_student_id'];
|
||||||
@@ -103,7 +102,7 @@ class AttendanceSyncController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$localStudentId = $studentMap[$sourceId];
|
$localStudentId = $studentMap[$sourceId];
|
||||||
$allStudentIdsInPush[$localStudentId] = true;
|
$datesByStudent[$localStudentId][$item['date']] = true;
|
||||||
|
|
||||||
$statusMap = [
|
$statusMap = [
|
||||||
'present' => 'hadir',
|
'present' => 'hadir',
|
||||||
@@ -137,7 +136,6 @@ class AttendanceSyncController extends Controller
|
|||||||
|
|
||||||
if ($localStatus === 'alpha') {
|
if ($localStatus === 'alpha') {
|
||||||
$alphaCounts[$localStudentId] = ($alphaCounts[$localStudentId] ?? 0) + 1;
|
$alphaCounts[$localStudentId] = ($alphaCounts[$localStudentId] ?? 0) + 1;
|
||||||
$alphaDate = $item['date'];
|
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$results['errors'][] = "student_id={$sourceId} jam={$item['lesson_hour']}: " . $e->getMessage();
|
$results['errors'][] = "student_id={$sourceId} jam={$item['lesson_hour']}: " . $e->getMessage();
|
||||||
@@ -145,24 +143,29 @@ class AttendanceSyncController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-generate / update / hapus violations untuk alpha siswa di push ini
|
// Auto-generate / update / hapus violations alpha per (siswa, tanggal) dari push ini.
|
||||||
|
// FIX 2026-08-31: sebelumnya memakai $alphaDate global (null saat push tanpa alpha)
|
||||||
|
// sehingga violation lama tidak pernah dihapus → false positive; dan $totalAlpha
|
||||||
|
// dihitung untuk semua siswa memakai satu tanggal global → rawan salah tanggal/siswa.
|
||||||
|
// Sekarang iterasi per (student, date) yang benar-benar muncul di push.
|
||||||
$alphaType = ViolationType::where('slug', 'alpha')->first();
|
$alphaType = ViolationType::where('slug', 'alpha')->first();
|
||||||
|
|
||||||
if ($alphaType) {
|
if ($alphaType) {
|
||||||
$violationService = app(\App\Services\ViolationService::class);
|
$violationService = app(\App\Services\ViolationService::class);
|
||||||
|
|
||||||
foreach ($allStudentIdsInPush as $studentId => $_) {
|
foreach ($datesByStudent as $studentId => $dates) {
|
||||||
|
foreach (array_keys($dates) as $date) {
|
||||||
try {
|
try {
|
||||||
// Hitung total alpha di DB (akumulasi dari semua push, bukan cuma push ini)
|
// Hitung total alpha di DB per (siswa, tanggal) — akumulasi dari semua push
|
||||||
$totalAlpha = \App\Models\Attendance::where('student_id', $studentId)
|
$totalAlpha = \App\Models\Attendance::where('student_id', $studentId)
|
||||||
->where('date', $alphaDate)
|
->where('date', $date)
|
||||||
->where('status', 'alpha')
|
->where('status', 'alpha')
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// Cari existing system-generated violation
|
// Cari existing system-generated violation untuk (siswa, tanggal) yang sama
|
||||||
$existing = \App\Models\Violation::where('student_id', $studentId)
|
$existing = \App\Models\Violation::where('student_id', $studentId)
|
||||||
->where('violation_type_id', $alphaType->id)
|
->where('violation_type_id', $alphaType->id)
|
||||||
->where('violation_date', $alphaDate)
|
->where('violation_date', $date)
|
||||||
->whereNull('recorded_by')
|
->whereNull('recorded_by')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -181,7 +184,7 @@ class AttendanceSyncController extends Controller
|
|||||||
$violationService->recordViolation([
|
$violationService->recordViolation([
|
||||||
'student_id' => $studentId,
|
'student_id' => $studentId,
|
||||||
'violation_type_id' => $alphaType->id,
|
'violation_type_id' => $alphaType->id,
|
||||||
'violation_date' => $alphaDate,
|
'violation_date' => $date,
|
||||||
'points' => $points,
|
'points' => $points,
|
||||||
'description' => $desc,
|
'description' => $desc,
|
||||||
'notes' => 'Dibuat otomatis dari sinkron E-Jurnal.',
|
'notes' => 'Dibuat otomatis dari sinkron E-Jurnal.',
|
||||||
@@ -190,12 +193,13 @@ class AttendanceSyncController extends Controller
|
|||||||
}
|
}
|
||||||
$results['violations']++;
|
$results['violations']++;
|
||||||
} elseif ($existing) {
|
} elseif ($existing) {
|
||||||
// Tidak ada alpha tapi ada violation lama → hapus (ganti status ke hadir)
|
// Tidak ada alpha di tanggal ini tapi ada violation system lama → hapus (ganti status ke hadir)
|
||||||
$existing->delete();
|
$existing->delete();
|
||||||
$results['violations']++;
|
$results['violations']++;
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$results['errors'][] = "Violation sync failed for student={$studentId}: " . $e->getMessage();
|
$results['errors'][] = "Violation sync failed for student={$studentId} date={$date}: " . $e->getMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Attendance;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Models\Student;
|
||||||
|
use App\Models\Violation;
|
||||||
|
use App\Models\ViolationType;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIX 2026-08-31 — bug sync attendance e-jurnal → violation alpha:
|
||||||
|
* 1. violation lama tidak pernah dihapus saat push koreksi tanpa alpha ($alphaDate null)
|
||||||
|
* 2. totalAlpha dihitung untuk semua siswa memakai tanggal global → salah siswa/tanggal
|
||||||
|
*
|
||||||
|
* Kasus nyata: XII TKJ 3 26-08 HENDIK (sakit) / RAMADANI & RHEDO (hadir) dicatat bolos.
|
||||||
|
*/
|
||||||
|
class AttendanceSyncViolationFixTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Setting::setValue('ejurnal_sync_token', 'test-token');
|
||||||
|
|
||||||
|
$category = \App\Models\ViolationCategory::create([
|
||||||
|
'name' => 'Kehadiran',
|
||||||
|
'slug' => 'kehadiran',
|
||||||
|
'color' => '#EF4444',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ViolationType::create([
|
||||||
|
'category_id' => $category->id,
|
||||||
|
'name' => 'Membolos/tidak masuk sekolah tanpa keterangan',
|
||||||
|
'slug' => 'alpha',
|
||||||
|
'points' => 20, // poin per 10 JP (2 poin/JP)
|
||||||
|
'description' => 'Tidak hadir tanpa keterangan',
|
||||||
|
'is_active' => true,
|
||||||
|
'is_system' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function makeStudent(int $sourceId, string $name): Student
|
||||||
|
{
|
||||||
|
return Student::create([
|
||||||
|
'source_id' => (string) $sourceId,
|
||||||
|
'nisn' => '00' . $sourceId,
|
||||||
|
'student_number' => 'TEST/' . $sourceId,
|
||||||
|
'full_name' => $name,
|
||||||
|
'class_name' => 'XII TKJ 3',
|
||||||
|
'class_level' => 'XII',
|
||||||
|
'department_name' => 'TKJ',
|
||||||
|
'status' => 'active',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sync(array $attendances): \Illuminate\Testing\TestResponse
|
||||||
|
{
|
||||||
|
return $this->post('/api/v1/attendance/sync', [
|
||||||
|
'token' => 'test-token',
|
||||||
|
'attendances' => $attendances,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_push_alpha_membuat_violation_hanya_untuk_siswa_alpha(): void
|
||||||
|
{
|
||||||
|
$s1 = $this->makeStudent(1001, 'SISWA ALPHA');
|
||||||
|
$s2 = $this->makeStudent(1002, 'SISWA HADIR');
|
||||||
|
|
||||||
|
$this->sync([
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'alpha'],
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 2, 'status' => 'alpha'],
|
||||||
|
['source_student_id' => 1002, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'hadir'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
// Siswa alpha → violation 2 JP = 4 poin (2 poin/JP)
|
||||||
|
$this->assertDatabaseHas('violations', [
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
'points' => 4,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Siswa hadir → TIDAK boleh kena violation
|
||||||
|
$this->assertDatabaseMissing('violations', [
|
||||||
|
'student_id' => $s2->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_push_koreksi_tanpa_alpha_menghapus_violation_lama(): void
|
||||||
|
{
|
||||||
|
$s1 = $this->makeStudent(1001, 'SISWA KOREKSI');
|
||||||
|
$alphaType = ViolationType::where('slug', 'alpha')->first();
|
||||||
|
|
||||||
|
// Simulasikan violation system-generated lama (salah: siswa sebenarnya sakit)
|
||||||
|
Violation::create([
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'student_class' => 'XII TKJ 3',
|
||||||
|
'violation_type_id' => $alphaType->id,
|
||||||
|
'recorded_by' => null,
|
||||||
|
'description' => 'Alpha - Tidak hadir tanpa keterangan (4 jam pelajaran)',
|
||||||
|
'points' => 8,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
'notes' => 'Dibuat otomatis dari sinkron E-Jurnal.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Push koreksi: siswa kini SAKIT (tidak ada alpha sama sekali dalam push)
|
||||||
|
$this->sync([
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'sakit'],
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 2, 'status' => 'sakit'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
// Violation lama harus TERHAPUS (bug lama: tidak terhapus karena $alphaDate null)
|
||||||
|
$this->assertNull(
|
||||||
|
Violation::where('student_id', $s1->id)->where('violation_date', '2026-08-26')->first(),
|
||||||
|
'Violation alpha lama harus terhapus setelah push koreksi tanpa alpha.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_push_multi_tanggal_tidak_membuat_violation_salah_tanggal(): void
|
||||||
|
{
|
||||||
|
$s1 = $this->makeStudent(1001, 'SISWA CAMPURAN');
|
||||||
|
|
||||||
|
// Push berisi 2 tanggal: 26-08 hadir, 27-08 alpha.
|
||||||
|
// Bug lama: $alphaDate global = 27-08 → siswa dihitung alpha di 27-08 untuk semua siswa.
|
||||||
|
// Harusnya violation HANYA untuk 27-08.
|
||||||
|
$this->sync([
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'hadir'],
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-27', 'lesson_hour' => 1, 'status' => 'alpha'],
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-27', 'lesson_hour' => 2, 'status' => 'alpha'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('violations', [
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'violation_date' => '2026-08-27',
|
||||||
|
'points' => 4,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('violations', [
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_push_siswa_yang_bukan_alpha_tidak_kena_violation_saat_push_berisi_alpha_siswa_lain(): void
|
||||||
|
{
|
||||||
|
$s1 = $this->makeStudent(1001, 'SISWA ALPHA');
|
||||||
|
$s2 = $this->makeStudent(1002, 'SISWA HADIR');
|
||||||
|
|
||||||
|
// Bug lama: totalAlpha dihitung utk SEMUA siswa dgn tanggal global → s2 ikut kena.
|
||||||
|
$this->sync([
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'alpha'],
|
||||||
|
['source_student_id' => 1002, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'hadir'],
|
||||||
|
['source_student_id' => 1002, 'date' => '2026-08-26', 'lesson_hour' => 2, 'status' => 'hadir'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('violations', [
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('violations', [
|
||||||
|
'student_id' => $s2->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user