seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); $this->actingAs($admin) ->post(route('admin.management.applications.store'), [ 'election_id' => $pusat->election_id, 'pusat_mengundi_id' => $pusat->id, 'requested_position_id' => $position->id, 'name' => 'Manual Admin', 'ic_number' => '900101105555', 'phone_number' => '0123456789', 'email' => 'manual@example.test', 'bank_name' => 'Maybank', 'bank_account_number' => '1234567890', ]) ->assertRedirect(); $application = Application::query()->where('ic_number', '900101105555')->firstOrFail(); $this->assertSame('admin_manual', $application->source); $this->assertSame('submitted', $application->status); $this->assertTrue($application->bankVerification()->exists()); } public function test_admin_edit_after_registration_closed_requires_catatan(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@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, 'ic_number' => '900101105556', ]); $this->closeRegistration($pusat->election); $payload = [ 'pusat_mengundi_id' => $pusat->id, 'requested_position_id' => $position->id, 'status' => 'submitted', 'name' => 'Manual Edit', 'ic_number' => '900101105556', 'phone_number' => '0123456789', 'email' => 'manual-edit@example.test', ]; $this->actingAs($admin) ->patch(route('admin.management.applications.update', $application->public_uuid), $payload) ->assertSessionHasErrors('note'); $this->actingAs($admin) ->patch(route('admin.management.applications.update', $application->public_uuid), [ ...$payload, 'note' => 'Pembetulan selepas tutup pendaftaran.', ]) ->assertRedirect(); $this->assertTrue(SystemNote::query() ->where('noteable_type', Application::class) ->where('noteable_id', $application->id) ->where('note_type', 'admin_application_edit_after_close') ->exists()); } public function test_admin_can_direct_assign_application_after_close_with_catatan(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@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, 'ic_number' => '900101105557', ]); $this->closeRegistration($pusat->election); $this->actingAs($admin) ->post(route('admin.management.applications.assign', $application->public_uuid), [ 'position_id' => $position->id, 'pusat_mengundi_id' => $pusat->id, 'status' => 'active', ]) ->assertSessionHasErrors('note'); $this->actingAs($admin) ->post(route('admin.management.applications.assign', $application->public_uuid), [ 'position_id' => $position->id, 'pusat_mengundi_id' => $pusat->id, 'status' => 'active', 'note' => 'Lantikan manual selepas tutup.', ]) ->assertRedirect(); $application->refresh(); $this->assertSame('assigned', $application->status); $this->assertTrue(StaffAssignment::query() ->where('application_id', $application->id) ->where('position_id', $position->id) ->where('source', 'admin_direct') ->exists()); $this->assertTrue(SystemNote::query()->where('note_type', 'admin_assignment_after_close')->exists()); } public function test_admin_can_create_representative_record(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $this->actingAs($admin) ->post(route('admin.management.representatives.store'), [ 'type' => 'jkm', 'election_id' => $pusat->election_id, 'pusat_mengundi_id' => $pusat->id, 'name' => 'Wakil JKM', 'ic_number' => '900101105558', 'phone_number' => '0123456789', 'agency' => 'JKM', ]) ->assertRedirect(route('admin.management.representatives.index')); $this->assertTrue(JkmRepresentative::query()->where('name', 'Wakil JKM')->exists()); } public function test_admin_kewangan_cannot_access_admin_management_routes(): void { $this->seed(DatabaseSeeder::class); $finance = User::query()->where('email', 'kewangan@prn.local')->firstOrFail(); $this->actingAs($finance) ->get(route('admin.management.assignments.index')) ->assertForbidden(); } public function test_admin_create_application_after_registration_closed_requires_catatan(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); $this->closeRegistration($pusat->election); $payload = [ 'election_id' => $pusat->election_id, 'pusat_mengundi_id' => $pusat->id, 'requested_position_id' => $position->id, 'name' => 'Manual Post-Close', 'ic_number' => '900101105560', 'phone_number' => '0123456789', 'email' => 'post-close@example.test', 'bank_name' => 'Maybank', 'bank_account_number' => '1234567890', ]; // Without note — both middleware and service should reject $this->actingAs($admin) ->post(route('admin.management.applications.store'), $payload) ->assertSessionHasErrors('note'); $this->assertDatabaseMissing('applications', ['ic_number' => '900101105560']); // With note — must succeed $this->actingAs($admin) ->post(route('admin.management.applications.store'), [ ...$payload, 'note' => 'Manual entry selepas tutup pendaftaran.', ]) ->assertRedirect(); $this->assertDatabaseHas('applications', ['ic_number' => '900101105560']); } public function test_admin_update_assignment_after_registration_closed_requires_catatan(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); $assignment = StaffAssignment::query() ->where('election_id', $pusat->election_id) ->where('pusat_mengundi_id', $pusat->id) ->where('status', 'active') ->firstOrFail(); $this->closeRegistration($pusat->election); $payload = [ 'position_id' => $assignment->position_id, 'pusat_mengundi_id' => $pusat->id, 'status' => 'active', ]; // Without note $this->actingAs($admin) ->patch(route('admin.management.assignments.update', $assignment), $payload) ->assertSessionHasErrors('note'); // With note $this->actingAs($admin) ->patch(route('admin.management.assignments.update', $assignment), [ ...$payload, 'note' => 'Kemaskini tugasan selepas tutup.', ]) ->assertRedirect(); $this->assertTrue(SystemNote::query() ->where('note_type', 'admin_assignment_edit_after_close') ->exists()); } public function test_admin_create_representative_after_registration_closed_requires_catatan(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $this->closeRegistration($pusat->election); $payload = [ 'type' => 'kkm', 'election_id' => $pusat->election_id, 'pusat_mengundi_id' => $pusat->id, 'name' => 'Wakil KKM Post-Close', ]; // Without note $this->actingAs($admin) ->post(route('admin.management.representatives.store'), $payload) ->assertSessionHasErrors('note'); // With note $this->actingAs($admin) ->post(route('admin.management.representatives.store'), [ ...$payload, 'note' => 'Tambah wakil selepas tutup.', ]) ->assertRedirect(); $this->assertDatabaseHas('kkm_representatives', ['name' => 'Wakil KKM Post-Close']); } public function test_admin_delete_representative_after_registration_closed_requires_catatan(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $jkm = JkmRepresentative::query()->create([ 'election_id' => $pusat->election_id, 'pusat_mengundi_id' => $pusat->id, 'name' => 'JKM Untuk Padam', ]); $this->closeRegistration($pusat->election); // Without note — middleware blocks before service runs $this->actingAs($admin) ->delete(route('admin.management.representatives.destroy', ['type' => 'jkm', 'id' => $jkm->id])) ->assertSessionHasErrors('note'); $this->assertDatabaseHas('jkm_representatives', ['id' => $jkm->id]); // With note — service enforces its own check too $this->actingAs($admin) ->delete(route('admin.management.representatives.destroy', ['type' => 'jkm', 'id' => $jkm->id]), [ 'note' => 'Padam wakil selepas tutup.', ]) ->assertRedirect(); $this->assertSoftDeleted('jkm_representatives', ['id' => $jkm->id]); } public function test_middleware_does_not_block_when_registration_is_open(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); // Registration is open by default in seed; no note required $this->actingAs($admin) ->post(route('admin.management.applications.store'), [ 'election_id' => $pusat->election_id, 'pusat_mengundi_id' => $pusat->id, 'requested_position_id' => $position->id, 'name' => 'Admin Open', 'ic_number' => '900101105561', 'phone_number' => '0123456789', 'email' => 'admin-open@example.test', 'bank_name' => 'Maybank', 'bank_account_number' => '1234567890', ]) ->assertRedirect() ->assertSessionMissing('errors'); $this->assertDatabaseHas('applications', ['ic_number' => '900101105561']); } private function closeRegistration(Election $election): void { $election->settings()->update([ 'registration_start_date' => now()->subWeeks(4)->toDateString(), 'registration_end_date' => now()->subDay()->toDateString(), 'is_registration_open' => true, 'is_registration_open_override' => null, ]); } }