seed(DatabaseSeeder::class); $finance = User::query()->where('email', 'kewangan@prn.local')->firstOrFail(); $application = $this->assignedApplication('Finance View', '910101105551'); $this->actingAs($finance) ->get(route('kewangan.bank.index')) ->assertOk() ->assertSee($application->name) ->assertSee('Penyata ada'); } public function test_admin_kewangan_can_verify_bank_account(): void { $this->seed(DatabaseSeeder::class); $finance = User::query()->where('email', 'kewangan@prn.local')->firstOrFail(); $application = $this->assignedApplication('Finance Verify', '910101105552'); $verification = $application->bankVerification()->firstOrFail(); $this->actingAs($finance) ->patch(route('kewangan.bank.update', $verification), [ 'status' => 'verified', 'finance_note' => 'Akaun disahkan.', ]) ->assertRedirect(route('kewangan.bank.show', $verification)); $verification->refresh(); $this->assertSame('verified', $verification->status); $this->assertSame('Akaun disahkan.', $verification->finance_note); $this->assertSame($finance->id, $verification->verified_by_user_id); $this->assertNotNull($verification->verified_at); } public function test_finance_filters_missing_bank_statement_and_account_number(): void { $this->seed(DatabaseSeeder::class); $finance = User::query()->where('email', 'kewangan@prn.local')->firstOrFail(); $complete = $this->assignedApplication('Finance Complete', '910101105553'); $missing = $this->assignedApplication('Finance Missing', '910101105554', false, false); $this->actingAs($finance) ->get(route('kewangan.bank.index', [ 'missing_bank_statement' => 1, 'missing_account_number' => 1, ])) ->assertOk() ->assertSee($missing->name) ->assertDontSee($complete->name); } public function test_admin_kewangan_cannot_change_assignment(): void { $this->seed(DatabaseSeeder::class); $finance = User::query()->where('email', 'kewangan@prn.local')->firstOrFail(); $assignment = StaffAssignment::query()->firstOrFail(); $this->actingAs($finance) ->get(route('admin.management.assignments.edit', $assignment)) ->assertForbidden(); } public function test_finance_export_creates_export_log(): void { Storage::fake('local'); $this->seed(DatabaseSeeder::class); $finance = User::query()->where('email', 'kewangan@prn.local')->firstOrFail(); $this->assignedApplication('Finance Export', '910101105555'); $this->actingAs($finance) ->get(route('kewangan.bank.export')) ->assertOk(); $exportLog = ExportLog::query()->where('report_type', 'finance_verification_list')->firstOrFail(); $this->assertSame($finance->id, $exportLog->generated_by_user_id); $this->assertNotNull($exportLog->generated_at); $this->assertSame('local', $exportLog->disk); Storage::disk('local')->assertExists($exportLog->path); } private function assignedApplication(string $name, string $icNumber, bool $withBankStatement = true, bool $withAccountNumber = true): Application { $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, 'approved_position_id' => $position->id, 'status' => 'assigned', 'name' => $name, 'ic_number' => $icNumber, 'bank_name' => 'Maybank', 'bank_account_number' => $withAccountNumber ? '1234567890' : null, ]); BankVerification::query()->create([ 'application_id' => $application->id, 'status' => 'pending', ]); StaffAssignment::query()->create([ 'election_id' => $pusat->election_id, 'application_id' => $application->id, 'position_id' => $position->id, 'pusat_mengundi_id' => $pusat->id, 'status' => 'active', 'assigned_at' => now(), 'source' => 'test', ]); if ($withBankStatement) { ApplicationDocument::query()->create([ 'application_id' => $application->id, 'document_type' => 'bank_statement', 'disk' => 'local', 'path' => 'applications/'.$application->public_uuid.'/bank.pdf', 'original_name' => 'bank.pdf', 'mime_type' => 'application/pdf', 'size' => 12, ]); } return $application; } }