loadMissing(['documents', 'pusatMengundi', 'requestedPosition']); if (! $this->hasRequiredDocuments($application)) { throw ValidationException::withMessages([ 'documents' => 'Permohonan hanya boleh diluluskan jika dokumen IC dan penyata bank telah dimuat naik.', ]); } if (! in_array($application->status, ['submitted', 'under_ppm_review', 'approved'], true)) { throw ValidationException::withMessages([ 'status' => 'Status permohonan tidak membenarkan kelulusan.', ]); } $position = Position::query() ->where('code', $data['approved_position_code']) ->whereIn('code', ['KTM', 'KP', 'KPDP', 'PAPM']) ->firstOrFail(); $assignmentContext = $this->assignmentContext($application, $position, $data); $previousStatus = $application->status; $previousPositionId = $application->approved_position_id ?: $application->requested_position_id; $application->forceFill([ 'status' => 'assigned', 'approved_position_id' => $position->id, 'approved_by_user_id' => $actor->id, 'approved_at' => now(), 'selected_ktm_assignment_id' => $assignmentContext['reports_to_assignment_id'], ])->save(); $assignment = StaffAssignment::query()->updateOrCreate( ['application_id' => $application->id], [ 'election_id' => $application->election_id, 'user_id' => $application->user_id, 'position_id' => $position->id, 'pusat_mengundi_id' => $application->pusat_mengundi_id, 'saluran_mengundi_id' => $assignmentContext['saluran_mengundi_id'], 'reports_to_assignment_id' => $assignmentContext['reports_to_assignment_id'], 'status' => 'active', 'assigned_by_user_id' => $actor->id, 'assigned_at' => now(), 'source' => 'ppm_approval', ], ); $application->statusHistories()->create([ 'from_status' => $previousStatus, 'to_status' => 'assigned', 'changed_by_user_id' => $actor->id, 'note' => $data['note'] ?? null, 'metadata' => [ 'approved_position_code' => $position->code, 'previous_position_id' => $previousPositionId, 'saluran_mengundi_id' => $assignmentContext['saluran_mengundi_id'], 'reports_to_assignment_id' => $assignmentContext['reports_to_assignment_id'], ], ]); $assignment->histories()->create([ 'from_position_id' => $previousPositionId, 'to_position_id' => $position->id, 'to_pusat_mengundi_id' => $application->pusat_mengundi_id, 'to_saluran_mengundi_id' => $assignmentContext['saluran_mengundi_id'], 'to_reports_to_assignment_id' => $assignmentContext['reports_to_assignment_id'], 'changed_by_user_id' => $actor->id, 'note' => $data['note'] ?? 'Application approved by PPM.', ]); if ($previousPositionId !== $position->id) { activity('ppm_review') ->performedOn($application) ->causedBy($actor) ->withProperties([ 'from_position_id' => $previousPositionId, 'to_position_id' => $position->id, ]) ->log('application_role_changed_by_ppm'); } activity('ppm_review') ->performedOn($application) ->causedBy($actor) ->withProperties([ 'assignment_id' => $assignment->id, 'approved_position_code' => $position->code, 'saluran_mengundi_id' => $assignmentContext['saluran_mengundi_id'], 'reports_to_assignment_id' => $assignmentContext['reports_to_assignment_id'], ]) ->log('application_approved_assigned_by_ppm'); return $assignment; }); } public function reject(Application $application, User $actor, string $reason): Application { return DB::transaction(function () use ($application, $actor, $reason): Application { if (! in_array($application->status, ['submitted', 'under_ppm_review', 'approved'], true)) { throw ValidationException::withMessages([ 'status' => 'Status permohonan tidak membenarkan penolakan.', ]); } $previousStatus = $application->status; $application->forceFill([ 'status' => 'rejected', 'rejected_by_user_id' => $actor->id, 'rejected_at' => now(), 'rejection_reason' => $reason, ])->save(); $application->statusHistories()->create([ 'from_status' => $previousStatus, 'to_status' => 'rejected', 'changed_by_user_id' => $actor->id, 'note' => $reason, ]); activity('ppm_review') ->performedOn($application) ->causedBy($actor) ->withProperties([ 'reason' => $reason, ]) ->log('application_rejected_by_ppm'); return $application; }); } private function hasRequiredDocuments(Application $application): bool { $types = $application->documents() ->whereIn('document_type', ['ic_document', 'bank_statement']) ->pluck('document_type') ->unique(); return $types->contains('ic_document') && $types->contains('bank_statement'); } /** * @param array{approved_position_code: string, saluran_mengundi_id?: int|null, ktm_assignment_id?: int|null, note?: string|null} $data * @return array{saluran_mengundi_id: int|null, reports_to_assignment_id: int|null} */ private function assignmentContext(Application $application, Position $position, array $data): array { $pusatMengundi = $application->pusatMengundi; if (! $pusatMengundi instanceof PusatMengundi) { throw ValidationException::withMessages([ 'pusat_mengundi_id' => 'Pusat Mengundi permohonan tidak sah.', ]); } if ($position->code === 'PAPM') { if (! app(AssignmentVacancyService::class)->hasPusatVacancy($pusatMengundi, 'PAPM')) { throw ValidationException::withMessages([ 'approved_position_code' => 'Kekosongan PAPM untuk Pusat Mengundi ini telah penuh.', ]); } return [ 'saluran_mengundi_id' => null, 'reports_to_assignment_id' => null, ]; } if ($position->code === 'KTM') { $saluranId = (int) ($data['saluran_mengundi_id'] ?? 0); if (! app(AssignmentVacancyService::class)->hasSaluranVacancy($pusatMengundi, $saluranId, 'KTM')) { throw ValidationException::withMessages([ 'saluran_mengundi_id' => 'Saluran Mengundi yang dipilih tidak sah atau kekosongan KTM telah penuh.', ]); } return [ 'saluran_mengundi_id' => $saluranId, 'reports_to_assignment_id' => null, ]; } if (in_array($position->code, ['KP', 'KPDP'], true)) { $ktmAssignmentId = (int) ($data['ktm_assignment_id'] ?? 0); if (! app(KtmVacancyService::class)->hasVacancyForAssignment($ktmAssignmentId, $pusatMengundi, $position->code, $application->id)) { throw ValidationException::withMessages([ 'ktm_assignment_id' => 'KTM yang dipilih tidak sah atau kekosongan jawatan ini telah penuh.', ]); } $ktmAssignment = StaffAssignment::query()->findOrFail($ktmAssignmentId); return [ 'saluran_mengundi_id' => $ktmAssignment->saluran_mengundi_id, 'reports_to_assignment_id' => $ktmAssignment->id, ]; } throw ValidationException::withMessages([ 'approved_position_code' => 'Jawatan yang dipilih tidak sah untuk kelulusan PPM.', ]); } }