95 lines
6.8 KiB
PHP
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']]);
|
|
});
|
|
}
|
|
}
|