seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $ownPusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $otherPusat = $this->otherPusat(); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); $ownApplication = Application::factory()->create([ 'election_id' => $ownPusat->election_id, 'pusat_mengundi_id' => $ownPusat->id, 'requested_position_id' => $position->id, 'name' => 'Pemohon Pusat Sendiri', ]); Application::factory()->create([ 'election_id' => $otherPusat->election_id, 'pusat_mengundi_id' => $otherPusat->id, 'requested_position_id' => $position->id, 'name' => 'Pemohon Pusat Lain', ]); $this->actingAs($ppm) ->get(route('ppm.applications.index')) ->assertOk() ->assertSee($ownApplication->name) ->assertDontSee('Pemohon Pusat Lain'); } public function test_ppm_cannot_view_application_outside_own_pusat(): void { $this->seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); $otherPusat = $this->otherPusat(); $application = Application::factory()->create([ 'election_id' => $otherPusat->election_id, 'pusat_mengundi_id' => $otherPusat->id, 'requested_position_id' => $position->id, ]); $this->actingAs($ppm) ->get(route('ppm.applications.show', $application->public_uuid)) ->assertForbidden(); } public function test_ppm_cannot_approve_application_without_required_documents(): void { $this->seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); $application = Application::factory()->create([ 'election_id' => $pusat->election_id, 'pusat_mengundi_id' => $pusat->id, 'requested_position_id' => $position->id, ]); $this->actingAs($ppm) ->post(route('ppm.applications.approve', $application->public_uuid), [ 'approved_position_code' => 'PAPM', ]) ->assertSessionHasErrors('documents'); $this->assertDatabaseMissing('staff_assignments', [ 'application_id' => $application->id, ]); } public function test_ppm_can_approve_ktm_and_assign_saluran(): void { Storage::fake('local'); $this->seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $position = Position::query()->where('code', 'KTM')->firstOrFail(); $saluran = SaluranMengundi::query() ->where('pusat_mengundi_id', $pusat->id) ->where('number', '3') ->firstOrFail(); $application = $this->applicationWithDocuments($pusat, $position, 'Pemohon KTM'); $this->actingAs($ppm) ->post(route('ppm.applications.approve', $application->public_uuid), [ 'approved_position_code' => 'KTM', 'saluran_mengundi_id' => $saluran->id, 'note' => 'Diluluskan untuk Saluran 3.', ]) ->assertRedirect(route('ppm.applications.show', $application->public_uuid)); $application->refresh(); $this->assertSame('assigned', $application->status); $this->assertSame($position->id, $application->approved_position_id); $this->assertTrue(StaffAssignment::query() ->where('application_id', $application->id) ->where('position_id', $position->id) ->where('saluran_mengundi_id', $saluran->id) ->where('status', 'active') ->exists()); $this->assertTrue($application->statusHistories()->where('to_status', 'assigned')->exists()); } public function test_ppm_can_change_role_and_assign_kp_to_approved_ktm(): void { Storage::fake('local'); $this->seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $requestedPosition = Position::query()->where('code', 'PAPM')->firstOrFail(); $approvedPosition = Position::query()->where('code', 'KP')->firstOrFail(); $ktmAssignment = $this->ktmAssignment($pusat); $application = $this->applicationWithDocuments($pusat, $requestedPosition, 'Pemohon Ditukar KP'); $this->actingAs($ppm) ->post(route('ppm.applications.approve', $application->public_uuid), [ 'approved_position_code' => 'KP', 'ktm_assignment_id' => $ktmAssignment->id, 'note' => 'Tukar dari PAPM ke KP.', ]) ->assertRedirect(route('ppm.applications.show', $application->public_uuid)); $application->refresh(); $this->assertSame('assigned', $application->status); $this->assertSame($approvedPosition->id, $application->approved_position_id); $this->assertSame($ktmAssignment->id, $application->selected_ktm_assignment_id); $this->assertTrue(StaffAssignment::query() ->where('application_id', $application->id) ->where('position_id', $approvedPosition->id) ->where('reports_to_assignment_id', $ktmAssignment->id) ->where('status', 'active') ->exists()); } public function test_ppm_can_reject_own_pusat_application(): void { $this->seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); $application = Application::factory()->create([ 'election_id' => $pusat->election_id, 'pusat_mengundi_id' => $pusat->id, 'requested_position_id' => $position->id, ]); $this->actingAs($ppm) ->post(route('ppm.applications.reject', $application->public_uuid), [ 'reason' => 'Tidak memenuhi syarat.', ]) ->assertRedirect(route('ppm.applications.show', $application->public_uuid)); $application->refresh(); $this->assertSame('rejected', $application->status); $this->assertSame('Tidak memenuhi syarat.', $application->rejection_reason); $this->assertTrue($application->statusHistories()->where('to_status', 'rejected')->exists()); } private function otherPusat(): PusatMengundi { $election = Election::query()->where('code', 'PRN2026')->firstOrFail(); $daerah = DaerahMengundi::factory()->forElection($election)->create([ 'code' => 'D999', 'name' => 'Daerah Lain', ]); return PusatMengundi::factory()->create([ 'election_id' => $election->id, 'daerah_mengundi_id' => $daerah->id, 'code' => 'PM999', 'name' => 'Pusat Lain', ]); } private function applicationWithDocuments(PusatMengundi $pusatMengundi, Position $position, string $name): Application { $application = Application::factory()->create([ 'election_id' => $pusatMengundi->election_id, 'pusat_mengundi_id' => $pusatMengundi->id, 'requested_position_id' => $position->id, 'name' => $name, ]); foreach (['ic_document', 'bank_statement'] as $type) { $path = 'applications/'.$application->public_uuid.'/'.$type.'.pdf'; Storage::disk('local')->put($path, 'test'); ApplicationDocument::query()->create([ 'application_id' => $application->id, 'document_type' => $type, 'disk' => 'local', 'path' => $path, 'original_name' => $type.'.pdf', 'mime_type' => 'application/pdf', 'size' => 4, ]); } return $application; } private function ktmAssignment(PusatMengundi $pusatMengundi): StaffAssignment { $ktmPosition = Position::query()->where('code', 'KTM')->firstOrFail(); return StaffAssignment::query() ->where('election_id', $pusatMengundi->election_id) ->where('pusat_mengundi_id', $pusatMengundi->id) ->where('position_id', $ktmPosition->id) ->where('status', 'active') ->whereNotNull('saluran_mengundi_id') ->firstOrFail(); } }