allowed($actor, $record->kind, 'prepare') && app(PeopleAccess::class)->can($actor, $record, 'prepare') && ! app(OrganizationAccess::class)->maintenanceOnly($actor); } public function canRead(User $actor, PeopleProcess $record): bool { return $this->canPrepare($actor, $record) || (app(PeopleHiring::class)->allowed($actor, $record->kind, 'review') && app(PeopleAccess::class)->can($actor, $record, 'review') && app(PeopleAccess::class)->can($actor, $record, 'interviews') && ! app(OrganizationAccess::class)->maintenanceOnly($actor)); } public function pending(PeopleProcess $record): int { if (! Schema::hasTable('candidate_triage')) { return 0; } return DB::table('candidate_submissions as s')->where('s.tenant_id', $record->tenant_id)->where('s.people_process_id', $record->id) ->whereNotExists(fn ($q) => $q->selectRaw('1')->from('candidate_triage as t')->whereColumn('t.candidate_submission_id', 's.id')->whereColumn('t.tenant_id', 's.tenant_id'))->count(); } /** @return array|null */ public function publicDecision(int $tenant, int $submission): ?array { if (! Schema::hasTable('candidate_triage')) { return null; } $row = DB::table('candidate_triage')->where('tenant_id', $tenant)->where('candidate_submission_id', $submission)->first(); return $row === null ? null : ['outcome' => $row->outcome, 'message' => $row->public_message, 'decided_at' => $row->created_at]; } /** @param array $input */ public function decide(User $actor, int $processId, int $submissionId, array $input): void { DB::transaction(function () use ($actor, $processId, $submissionId, $input) { app(ConflictGuard::class)->lockCompany($actor); $actor = app(CurrentActor::class)->lock($actor); abort_unless(Schema::hasTable('candidate_triage'), 503, 'Instalação da triagem pendente.'); $record = PeopleProcess::query()->where('tenant_id', $actor->tenant_id)->where('id', $processId)->lockForUpdate()->firstOrFail(); abort_unless($this->canPrepare($actor, $record), 403); $submission = DB::table('candidate_submissions')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $processId)->where('id', $submissionId)->first(); abort_unless($submission !== null, 404); $data = Validator::make($input, ['process_version' => 'required|integer|min:1', 'operation_key' => 'required|uuid', 'outcome' => 'required|in:accepted_for_analysis,not_accepted', 'reason' => 'required|string|min:10|max:4000', 'public_message' => 'required|string|min:10|max:2000', 'no_conflict' => 'accepted'])->validate(); $hash = hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); $previous = DB::table('candidate_triage')->where('tenant_id', $actor->tenant_id)->where('candidate_submission_id', $submissionId)->first(); if ($previous !== null) { abort_unless($previous->operation_key === $data['operation_key'] && $previous->input_hash === $hash, 409, 'A triagem já foi decidida. A revisão exige um percurso próprio.'); return; } abort_if(DB::table('candidate_triage')->where('tenant_id', $actor->tenant_id)->where('operation_key', $data['operation_key'])->exists(), 409); if ((int) $data['process_version'] !== $record->version) { throw ValidationException::withMessages(['process_version' => 'O processo mudou. Actualize antes de decidir.']); } abort_unless(in_array($record->status, ['open', 'assessment'], true), 409, 'O processo já passou a fase de admissão de candidaturas.'); $snapshot = json_decode($submission->snapshot, true, 32, JSON_THROW_ON_ERROR); $applicationId = null; if ($data['outcome'] === 'accepted_for_analysis') { $hasReviews = DB::table('people_reviews as r')->join('people_applications as a', 'a.id', '=', 'r.people_application_id')->where('a.tenant_id', $actor->tenant_id)->where('a.people_process_id', $processId)->exists(); abort_if($hasReviews, 409, 'A avaliação já começou. Não é possível acrescentar candidaturas por este percurso.'); abort_if(DB::table('people_applications')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $processId)->where('reference', $submission->reference)->exists(), 409, 'Já existe esta referência no processo. Reconcilie a origem antes da triagem; não foi feita associação automática.'); $received = CarbonImmutable::parse($submission->created_at)->toDateString(); abort_unless($received <= $record->closes_on, 422, 'A submissão não respeita o prazo do processo.'); $applicationId = DB::table('people_applications')->insertGetId(['tenant_id' => $actor->tenant_id, 'people_process_id' => $processId, 'created_by' => $actor->id, 'operation_key' => (string) Str::uuid(), 'name' => $snapshot['profile']['data']['name'], 'summary' => $snapshot['profile']['data']['summary'], 'reference' => $submission->reference, 'received_on' => $received, 'created_at' => now(), 'updated_at' => now()]); } $id = DB::table('candidate_triage')->insertGetId(['tenant_id' => $actor->tenant_id, 'people_process_id' => $processId, 'candidate_submission_id' => $submissionId, 'people_application_id' => $applicationId, 'created_by' => $actor->id, 'operation_key' => $data['operation_key'], 'input_hash' => $hash, 'outcome' => $data['outcome'], 'profile_version' => $snapshot['profile']['version'], 'reason' => $data['reason'], 'public_message' => $data['public_message'], 'created_at' => now()]); app(RequisitionWorkflow::class)->audit($actor, 'candidate_triage.decided', null, null, ['triage_id' => $id, 'submission_id' => $submissionId, 'application_id' => $applicationId, ...$data]); $account = CandidateAccount::query()->where('tenant_id', $actor->tenant_id)->where('id', $submission->candidate_account_id)->firstOrFail(); app(CandidateProfiles::class)->event($account, 'submission.triaged', ['submission_id' => $submissionId, 'outcome' => $data['outcome']]); }); } }