merge([ 'ic_number' => preg_replace('/\D+/', '', (string) $this->input('ic_number')), 'phone_number' => trim((string) $this->input('phone_number')), 'bank_account_number' => preg_replace('/[\s-]+/', '', (string) $this->input('bank_account_number')), ]); } /** * @return array */ public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'ic_number' => ['required', 'digits:12'], 'phone_number' => ['required', 'string', 'max:30'], 'email' => ['nullable', 'email', 'max:255'], 'address' => ['required', 'string', 'max:1000'], 'requested_position_code' => ['required', Rule::in(['KTM', 'KP', 'KPDP', 'PAPM'])], 'selected_ktm_assignment_id' => [ Rule::requiredIf(fn (): bool => in_array($this->input('requested_position_code'), ['KP', 'KPDP'], true)), 'nullable', 'integer', ], 'bank_name' => ['required', 'string', 'max:255'], 'bank_account_number' => ['required', 'string', 'max:50'], 'ic_document' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:5120'], 'bank_statement' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:5120'], ]; } public function after(): array { return [ function (Validator $validator): void { $pusatMengundi = $this->route('pusatMengundi'); if (! $pusatMengundi instanceof PusatMengundi) { return; } /** @var Election $election */ $election = $pusatMengundi->election()->with('settings')->firstOrFail(); if (! app(RegistrationPeriodService::class)->isOpen($election)) { $validator->errors()->add('registration', app(RegistrationPeriodService::class)->closedMessage($election)); } $roleCode = (string) $this->input('requested_position_code'); $position = Position::query() ->where('code', $roleCode) ->where('is_public_applyable', true) ->first(); if (! $position instanceof Position) { $validator->errors()->add('requested_position_code', 'Jawatan yang dipilih tidak dibuka untuk permohonan awam.'); return; } $this->validateDuplicateIc($validator, $pusatMengundi); if (in_array($roleCode, ['KP', 'KPDP'], true)) { $this->validateKtmVacancy($validator, $pusatMengundi, $roleCode); } }, ]; } /** * @return array */ public function attributes(): array { return [ 'name' => 'nama', 'ic_number' => 'no. kad pengenalan', 'phone_number' => 'no. telefon', 'email' => 'emel', 'address' => 'alamat', 'requested_position_code' => 'jawatan dipohon', 'selected_ktm_assignment_id' => 'KTM', 'bank_name' => 'nama bank', 'bank_account_number' => 'no. akaun bank', 'ic_document' => 'dokumen kad pengenalan', 'bank_statement' => 'penyata bank', ]; } private function validateDuplicateIc(Validator $validator, PusatMengundi $pusatMengundi): void { // SoftDeletes scope automatically excludes deleted_at IS NOT NULL records (e.g. soft-deleted // KTM-created applicants). The whereNotIn further excludes 'cancelled' records (which are // NOT soft-deleted) and 'deleted_by_ktm' records as an explicit safety layer. $existing = Application::query() ->where('election_id', $pusatMengundi->election_id) ->where('ic_number', $this->input('ic_number')) ->whereNotIn('status', ['cancelled', 'deleted_by_ktm']) ->first(); if (! $existing instanceof Application) { return; } if ($existing->source === 'created_by_ktm') { $validator->errors()->add( 'ic_number', 'No. kad pengenalan ini telah didaftarkan oleh KTM. KTM perlu memadam rekod tersebut sebelum pemohon boleh mendaftar sendiri.', ); return; } $validator->errors()->add( 'ic_number', 'No. kad pengenalan ini telah mempunyai permohonan untuk pilihanraya semasa.', ); } private function validateKtmVacancy(Validator $validator, PusatMengundi $pusatMengundi, string $roleCode): void { $assignmentId = (int) $this->input('selected_ktm_assignment_id'); if ($assignmentId <= 0) { return; } if (! app(KtmVacancyService::class)->hasVacancyForAssignment($assignmentId, $pusatMengundi, $roleCode)) { $validator->errors()->add( 'selected_ktm_assignment_id', 'KTM yang dipilih tidak sah atau kekosongan untuk jawatan ini telah penuh.', ); } } }