Files
compras/app/Services/CandidateTriage.php
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

95 lines
6.8 KiB
PHP

<?php
namespace App\Services;
use App\Models\CandidateAccount;
use App\Models\PeopleProcess;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class CandidateTriage
{
public function canPrepare(User $actor, PeopleProcess $record): bool
{
return app(PeopleHiring::class)->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<string,mixed>|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<string,mixed> $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']]);
});
}
}