seed(RoleUserSeeder::class); $this->service = app(DrawService::class); $this->session = DrawSession::active(); $this->petugas = User::where('email', 'cabutan@koipb.test')->first(); } private function attendedMember(array $attrs = []): Member { $member = Member::factory()->create($attrs); AttendanceRecord::create(['member_id' => $member->id, 'attended_at' => now()]); return $member; } public function test_only_attended_members_are_eligible(): void { $hadir = $this->attendedMember(); $tidakHadir = Member::factory()->create(); $pool = $this->service->eligibleMembers($this->session); $this->assertTrue($pool->contains('id', $hadir->id)); $this->assertFalse($pool->contains('id', $tidakHadir->id)); } public function test_spin_creates_pending_result_and_marks_prize(): void { $this->attendedMember(); $prize = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]); $result = $this->service->spin($prize, $this->session, $this->petugas); $this->assertEquals(DrawResult::STATUS_PENDING, $result->status); $this->assertEquals(Prize::STATUS_SEDANG, $prize->fresh()->status); } public function test_confirmed_winner_cannot_win_again(): void { $member = $this->attendedMember(); $prize1 = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]); $prize2 = Prize::create(['nama_hadiah' => 'Radio', 'draw_order' => 2]); $r1 = $this->service->spin($prize1, $this->session, $this->petugas); $this->service->confirm($r1, $this->petugas); // Member sudah menang -> tidak lagi dalam pool $this->assertFalse($this->service->eligibleMembers($this->session)->contains('id', $member->id)); // Tiada peserta layak lain -> spin prize2 patut gagal $this->expectException(\RuntimeException::class); $this->service->spin($prize2, $this->session, $this->petugas); } public function test_db_blocks_duplicate_confirmed_winner(): void { $member = $this->attendedMember(); $prize1 = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]); $prize2 = Prize::create(['nama_hadiah' => 'Radio', 'draw_order' => 2]); $r1 = $this->service->spin($prize1, $this->session, $this->petugas); $this->service->confirm($r1, $this->petugas); // Cuba paksa member sama menang prize2 (langkau pool) -> unique index DB halang $r2 = DrawResult::create([ 'draw_session_id' => $this->session->id, 'prize_id' => $prize2->id, 'member_id' => $member->id, 'status' => DrawResult::STATUS_PENDING, 'spun_at' => now(), ]); $this->expectException(\Illuminate\Database\UniqueConstraintViolationException::class); $r2->update([ 'status' => DrawResult::STATUS_CONFIRMED, 'confirmed_member_id' => $member->id, 'confirmed_prize_id' => $prize2->id, ]); } public function test_cancelled_draw_allows_redraw(): void { $member1 = $this->attendedMember(); $member2 = $this->attendedMember(); $prize = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]); $r1 = $this->service->spin($prize, $this->session, $this->petugas); $this->service->cancel($r1, $this->petugas, 'Tiada di dewan'); // Hadiah jadi redraw_required tetapi masih available $prize->refresh(); $this->assertEquals(Prize::STATUS_REDRAW, $prize->status); $this->assertTrue($prize->isAvailable()); $this->assertEquals(1, $prize->redraw_count); // Boleh spin semula $r2 = $this->service->spin($prize, $this->session, $this->petugas); $this->assertEquals(DrawResult::STATUS_PENDING, $r2->status); $this->assertEquals(2, $r2->attempt_number); // Default setting (return_cancelled_to_pool = true): peserta dibatalkan kekal dalam pool $this->assertTrue($this->service->eligibleMembers($this->session)->contains('id', $member1->id)); } public function test_setting_can_remove_cancelled_member_from_pool(): void { // Tetapan admin: keluarkan peserta dibatalkan dari pool (tiada peluang kedua) $this->session->update(['return_cancelled_to_pool' => false]); $member1 = $this->attendedMember(); $member2 = $this->attendedMember(); $prize = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]); $r1 = $this->service->spin($prize, $this->session, $this->petugas); $cancelledId = $r1->member_id; $this->service->cancel($r1, $this->petugas, 'Tiada di dewan'); $pool = $this->service->eligibleMembers($this->session->fresh()); // Peserta yang dibatalkan TIDAK lagi dalam pool; peserta lain masih ada $this->assertFalse($pool->contains('id', $cancelledId)); $remainingId = $cancelledId === $member1->id ? $member2->id : $member1->id; $this->assertTrue($pool->contains('id', $remainingId)); } public function test_cannot_spin_already_confirmed_prize(): void { $this->attendedMember(); $this->attendedMember(); $prize = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]); $r1 = $this->service->spin($prize, $this->session, $this->petugas); $this->service->confirm($r1, $this->petugas); $this->expectException(\RuntimeException::class); $this->service->spin($prize, $this->session, $this->petugas); } }