loadMissing(['election.settings', 'pusatMengundi', 'user']); if (! app(KtmScopeService::class)->ownsAssignment($actor, $ktmAssignment)) { throw ValidationException::withMessages([ 'assignment' => 'Anda tidak dibenarkan mendaftar KP untuk saluran ini.', ]); } $election = $this->assignmentElection($ktmAssignment); $pusatMengundi = $this->assignmentPusat($ktmAssignment); if (! app(RegistrationPeriodService::class)->isOpen($election)) { throw ValidationException::withMessages([ 'registration' => app(RegistrationPeriodService::class)->closedMessage($election), ]); } $kpPosition = Position::query()->where('code', 'KP')->firstOrFail(); if (! app(KtmVacancyService::class)->hasVacancyForAssignment($ktmAssignment->id, $pusatMengundi, 'KP')) { throw ValidationException::withMessages([ 'ktm_assignment_id' => 'Kekosongan KP untuk saluran ini telah penuh.', ]); } // SoftDeletes scope excludes soft-deleted records. whereNotIn covers 'cancelled' records // (not soft-deleted) and 'deleted_by_ktm' as an explicit redundant safety layer. $existing = Application::query() ->where('election_id', $ktmAssignment->election_id) ->where('ic_number', $data['ic_number']) ->whereNotIn('status', ['cancelled', 'deleted_by_ktm']) ->first(); if ($existing instanceof Application) { throw ValidationException::withMessages([ 'ic_number' => 'No. kad pengenalan ini telah mempunyai rekod permohonan aktif untuk pilihanraya semasa.', ]); } $application = Application::query()->create([ 'election_id' => $ktmAssignment->election_id, 'pusat_mengundi_id' => $ktmAssignment->pusat_mengundi_id, 'selected_ktm_assignment_id' => $ktmAssignment->id, 'public_uuid' => (string) Str::uuid(), 'source' => 'created_by_ktm', 'status' => 'submitted', 'name' => $data['name'], 'ic_number' => $data['ic_number'], 'phone_number' => $data['phone_number'], 'email' => $data['email'], 'address' => $data['address'] ?? null, 'requested_position_id' => $kpPosition->id, 'created_by_user_id' => $actor->id, ]); $application->statusHistories()->create([ 'to_status' => 'submitted', 'changed_by_user_id' => $actor->id, 'note' => 'KP applicant created by KTM.', 'metadata' => [ 'source' => 'created_by_ktm', 'ktm_assignment_id' => $ktmAssignment->id, ], ]); Mail::to($application->email)->queue(new KtmCreatedApplicantMail($application->load('pusatMengundi'), $ktmAssignment)); activity('ktm_flow') ->performedOn($application) ->causedBy($actor) ->withProperties([ 'ktm_assignment_id' => $ktmAssignment->id, 'position_code' => 'KP', ]) ->log('kp_applicant_created_by_ktm'); return $application; }); } public function approveKp(Application $application, User $actor): StaffAssignment { return DB::transaction(function () use ($application, $actor): StaffAssignment { $application->loadMissing(['selectedKtmAssignment.pusatMengundi', 'selectedKtmAssignment.election.settings', 'requestedPosition']); $ktmAssignment = $application->selectedKtmAssignment; if (! $ktmAssignment instanceof StaffAssignment || ! app(KtmScopeService::class)->ownsAssignment($actor, $ktmAssignment)) { throw ValidationException::withMessages([ 'application' => 'Anda tidak dibenarkan meluluskan permohonan ini.', ]); } $election = $this->assignmentElection($ktmAssignment); $pusatMengundi = $this->assignmentPusat($ktmAssignment); if (! app(RegistrationPeriodService::class)->isOpen($election)) { throw ValidationException::withMessages([ 'registration' => app(RegistrationPeriodService::class)->closedMessage($election), ]); } $kpPosition = Position::query()->where('code', 'KP')->firstOrFail(); if ((int) $application->requested_position_id !== (int) $kpPosition->id) { throw ValidationException::withMessages([ 'requested_position_id' => 'KTM hanya boleh meluluskan permohonan KP.', ]); } if (! in_array($application->status, ['submitted', 'under_ppm_review', 'approved'], true)) { throw ValidationException::withMessages([ 'status' => 'Status permohonan tidak membenarkan kelulusan KTM.', ]); } if (! app(KtmVacancyService::class)->hasVacancyForAssignment($ktmAssignment->id, $pusatMengundi, 'KP', $application->id)) { throw ValidationException::withMessages([ 'ktm_assignment_id' => 'Kekosongan KP untuk saluran ini telah penuh.', ]); } $previousStatus = $application->status; $application->forceFill([ 'status' => 'assigned', 'approved_position_id' => $kpPosition->id, 'approved_by_user_id' => $actor->id, 'approved_at' => now(), ])->save(); $assignment = StaffAssignment::query()->updateOrCreate( ['application_id' => $application->id], [ 'election_id' => $application->election_id, 'user_id' => $application->user_id, 'position_id' => $kpPosition->id, 'pusat_mengundi_id' => $application->pusat_mengundi_id, 'saluran_mengundi_id' => $ktmAssignment->saluran_mengundi_id, 'reports_to_assignment_id' => $ktmAssignment->id, 'status' => 'active', 'assigned_by_user_id' => $actor->id, 'assigned_at' => now(), 'source' => 'ktm_approval', ], ); $application->statusHistories()->create([ 'from_status' => $previousStatus, 'to_status' => 'assigned', 'changed_by_user_id' => $actor->id, 'note' => 'KP application approved by KTM.', 'metadata' => [ 'ktm_assignment_id' => $ktmAssignment->id, 'position_code' => 'KP', ], ]); $assignment->histories()->create([ 'to_position_id' => $kpPosition->id, 'to_pusat_mengundi_id' => $application->pusat_mengundi_id, 'to_saluran_mengundi_id' => $ktmAssignment->saluran_mengundi_id, 'to_reports_to_assignment_id' => $ktmAssignment->id, 'changed_by_user_id' => $actor->id, 'note' => 'KP assigned by KTM approval.', ]); activity('ktm_flow') ->performedOn($application) ->causedBy($actor) ->withProperties([ 'assignment_id' => $assignment->id, 'ktm_assignment_id' => $ktmAssignment->id, ]) ->log('kp_application_approved_by_ktm'); return $assignment; }); } public function deleteCreatedApplication(Application $application, User $actor): Application { return DB::transaction(function () use ($application, $actor): Application { if (! app(KtmScopeService::class)->canAccessApplication($actor, $application)) { throw ValidationException::withMessages([ 'application' => 'Anda tidak dibenarkan memadam rekod ini.', ]); } if ($application->source !== 'created_by_ktm') { throw ValidationException::withMessages([ 'application' => 'Hanya rekod yang dicipta oleh KTM boleh dipadam oleh KTM.', ]); } if ($application->status === 'assigned') { throw ValidationException::withMessages([ 'application' => 'Rekod yang telah diassign tidak boleh dipadam oleh KTM.', ]); } $previousStatus = $application->status; $application->forceFill([ 'status' => 'deleted_by_ktm', ])->save(); $application->statusHistories()->create([ 'from_status' => $previousStatus, 'to_status' => 'deleted_by_ktm', 'changed_by_user_id' => $actor->id, 'note' => 'KTM deleted pre-created applicant record.', ]); activity('ktm_flow') ->performedOn($application) ->causedBy($actor) ->withProperties([ 'from_status' => $previousStatus, 'to_status' => 'deleted_by_ktm', ]) ->log('ktm_created_applicant_deleted'); $application->delete(); return $application; }); } private function assignmentElection(StaffAssignment $assignment): Election { /** @var Election $election */ $election = $assignment->election()->with('settings')->firstOrFail(); return $election; } private function assignmentPusat(StaffAssignment $assignment): PusatMengundi { /** @var PusatMengundi $pusatMengundi */ $pusatMengundi = $assignment->pusatMengundi()->firstOrFail(); return $pusatMengundi; } }