178 lines
16 KiB
PHP
178 lines
16 KiB
PHP
<?php
|
|
|
|
use App\Enums\PurchasingRole;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\CandidateProfiles;
|
|
use App\Services\CandidateSubmissions;
|
|
use App\Services\CandidateTriage;
|
|
use App\Services\PeopleAccess;
|
|
use App\Services\PeopleHiring;
|
|
use App\Services\RequisitionWorkflow;
|
|
use App\Services\SubmissionForms;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::query()->create(['name' => 'Empresa fictícia de submissões']);
|
|
$this->admin = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => PurchasingRole::Admin]);
|
|
$this->reviewer = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => PurchasingRole::Admin]);
|
|
$this->actingAs($this->admin);
|
|
app(CandidateProfiles::class)->configure($this->admin, ['version' => 0, 'registration_open' => true, 'notice' => 'Aviso fictício para submissões sem dados reais.', 'reason' => 'Activação fictícia para testar submissões.']);
|
|
$this->definition = ['available' => true, 'fields' => [['key' => 'full_name', 'label' => 'Nome completo', 'type' => 'text', 'required' => true], ['key' => 'availability_location', 'label' => 'Disponibilidade', 'type' => 'location', 'required' => true]], 'documents' => []];
|
|
app(SubmissionForms::class)->save($this->admin, ['audience' => 'service', 'modality' => 'competition', 'version' => 0, 'definition' => $this->definition, 'reason' => 'Formulário fictício para esta oportunidade.']);
|
|
$this->process = app(PeopleHiring::class)->execute($this->admin, null, 'create', ['operation_key' => (string) Str::uuid(), 'kind' => 'service', 'title' => 'Oportunidade fictícia', 'purpose' => 'Finalidade interna que não deve ser publicada.', 'closes_on' => today()->addWeek()->toDateString(), 'minimum_reviews' => 1, 'criteria' => [['label' => 'Competências', 'weight' => 100]]]);
|
|
app(PeopleAccess::class)->assign($this->admin, $this->process->id, ['version' => 1, 'user_id' => $this->reviewer->id, 'scopes' => ['review'], 'reason' => 'Conferência independente da abertura.']);
|
|
$this->actingAs($this->reviewer);
|
|
app(PeopleHiring::class)->execute($this->reviewer, $this->process->id, 'open', ['version' => 1, 'reason' => 'Abertura fictícia do processo.', 'no_conflict' => true]);
|
|
$this->actingAs($this->admin);
|
|
$this->publication = ['version' => 0, 'active' => true, 'description' => 'Descrição pública fictícia da oportunidade.', 'reason' => 'Publicação fictícia para candidaturas.'];
|
|
app(CandidateSubmissions::class)->publish($this->admin, $this->process->id, $this->publication);
|
|
$this->credentials = ['email' => 'submit@example.invalid', 'password' => 'Candidate-Test-123!', 'password_confirmation' => 'Candidate-Test-123!', 'portal_version' => 1, 'notice_acknowledged' => true];
|
|
$this->account = app(CandidateProfiles::class)->register($this->tenant->id, $this->credentials);
|
|
$this->account->setAttribute('email_verified_at', now())->save();
|
|
$this->profile = ['version' => 0, 'name' => 'Pessoa fictícia', 'phone' => '000000', 'summary' => 'Perfil fictício de competências.', 'availability' => ['province' => 'Província fictícia', 'district' => 'Distrito fictício', 'locality' => 'Localidade fictícia'], 'reason' => 'Preencher perfil fictício para candidatura.'];
|
|
app(CandidateProfiles::class)->save($this->account, $this->profile);
|
|
$this->base = '/candidaturas/'.$this->tenant->id;
|
|
$this->postJson($this->base.'/entrar', $this->credentials)->assertRedirect();
|
|
$this->body = ['operation_key' => (string) Str::uuid(), 'profile_version' => 1, 'publication_version' => 1,
|
|
'answers' => ['full_name' => 'Pessoa fictícia', 'availability_location' => $this->profile['availability']]];
|
|
});
|
|
|
|
test('electronic submission returns a scoped receipt and preserves the actual profile and answers', function () {
|
|
$this->get($this->base.'/oportunidades/'.$this->process->id)->assertInertia(fn (Assert $p) => $p->where('opportunity.description', 'Descrição pública fictícia da oportunidade.')->missing('opportunity.purpose')->missing('opportunity.selection'));
|
|
$url = $this->base.'/oportunidades/'.$this->process->id;
|
|
$this->postJson($url, $this->body)->assertRedirect();
|
|
$row = DB::table('candidate_submissions')->sole();
|
|
$this->get($this->base.'/submissoes/'.$row->id)->assertInertia(fn (Assert $p) => $p->where('receipt.snapshot.profile.version', 1)->where('receipt.snapshot.answers.availability_location.district', 'Distrito fictício')->where('receipt.snapshot.status', 'received')->missing('receipt.snapshot.reviews'));
|
|
app(CandidateProfiles::class)->save($this->account, [...$this->profile, 'version' => 1, 'name' => 'Nome posterior']);
|
|
$this->postJson($url, $this->body)->assertRedirect();
|
|
expect(DB::table('candidate_submissions')->count())->toBe(1)->and(json_decode($row->snapshot, true)['profile']['data']['name'])->toBe('Pessoa fictícia');
|
|
$this->postJson($url, [...$this->body, 'operation_key' => (string) Str::uuid(), 'profile_version' => 2])->assertConflict();
|
|
});
|
|
|
|
test('withdrawn opportunities stale profiles and unverified contacts cannot submit', function () {
|
|
$url = $this->base.'/oportunidades/'.$this->process->id;
|
|
$this->postJson($url, [...$this->body, 'profile_version' => 99])->assertUnprocessable();
|
|
$this->account->setAttribute('email_verified_at', null)->save();
|
|
$this->postJson($url, $this->body)->assertForbidden();
|
|
$this->account->setAttribute('email_verified_at', now())->save();
|
|
app(CandidateSubmissions::class)->publish($this->admin, $this->process->id, [...$this->publication, 'version' => 1, 'active' => false]);
|
|
$this->postJson($url, $this->body)->assertNotFound();
|
|
expect(DB::table('candidate_submissions')->count())->toBe(0);
|
|
});
|
|
|
|
test('received submissions do not become internal evaluations and only the assigned preparer reads the inbox', function () {
|
|
$this->postJson($this->base.'/oportunidades/'.$this->process->id, $this->body)->assertRedirect();
|
|
expect(DB::table('people_applications')->count())->toBe(0);
|
|
$url = '/pessoas/'.$this->process->id.'/recebidas';
|
|
$this->get($url)->assertInertia(fn (Assert $p) => $p->has('submissions.data', 1));
|
|
$this->actingAs($this->reviewer)->get($url)->assertForbidden();
|
|
$other = Tenant::query()->create(['name' => 'Outra empresa fictícia']);
|
|
$this->get('/candidaturas/'.$other->id.'/submissoes/1')->assertUnauthorized();
|
|
$this->admin->forceFill(['purchasing_role' => PurchasingRole::Requester])->save();
|
|
$this->actingAs($this->admin)->get($url)->assertForbidden();
|
|
$this->postJson('/pessoas/'.$this->process->id.'/publicacao', [...$this->publication, 'version' => 1, 'active' => false])->assertForbidden();
|
|
});
|
|
|
|
test('an opportunity requiring files cannot be published through the data only reception path', function () {
|
|
$this->actingAs($this->admin);
|
|
$definition = [...$this->definition, 'documents' => [['key' => 'cv', 'label' => 'CV', 'formats' => ['pdf'], 'max_kb' => 1024, 'required' => true]]];
|
|
app(SubmissionForms::class)->save($this->admin, ['audience' => 'service', 'modality' => 'competition', 'version' => 1, 'definition' => $definition, 'reason' => 'Exigir CV no novo formulário fictício.']);
|
|
$record = app(PeopleHiring::class)->execute($this->admin, null, 'create', ['operation_key' => (string) Str::uuid(), 'kind' => 'service', 'title' => 'Oportunidade com CV', 'purpose' => 'Teste de documento obrigatório na candidatura.', 'closes_on' => today()->addWeek()->toDateString(), 'minimum_reviews' => 1, 'criteria' => [['label' => 'Competências', 'weight' => 100]]]);
|
|
app(PeopleAccess::class)->assign($this->admin, $record->id, ['version' => 1, 'user_id' => $this->reviewer->id, 'scopes' => ['review'], 'reason' => 'Conferência independente do novo processo.']);
|
|
$this->actingAs($this->reviewer);
|
|
app(PeopleHiring::class)->execute($this->reviewer, $record->id, 'open', ['version' => 1, 'reason' => 'Abertura fictícia para testar os anexos.', 'no_conflict' => true]);
|
|
$this->actingAs($this->admin)->postJson('/pessoas/'.$record->id.'/publicacao', $this->publication)->assertUnprocessable();
|
|
expect(DB::table('candidate_opportunity_publications')->where('people_process_id', $record->id)->count())->toBe(0);
|
|
});
|
|
|
|
function electronicTriagePayload(object $test, string $outcome = 'accepted_for_analysis'): array
|
|
{
|
|
return ['operation_key' => (string) Str::uuid(), 'process_version' => $test->process->fresh()->version, 'outcome' => $outcome,
|
|
'reason' => 'MOTIVO-INTERNO-RESERVADO da conferência fictícia.', 'public_message' => 'A empresa concluiu a triagem desta candidatura.', 'no_conflict' => true];
|
|
}
|
|
|
|
test('triage reuses the submitted profile after the deadline without rewriting the receipt or admitting the triager as evaluator', function () {
|
|
$this->postJson($this->base.'/oportunidades/'.$this->process->id, $this->body)->assertRedirect();
|
|
$submission = DB::table('candidate_submissions')->sole();
|
|
$snapshot = $submission->snapshot;
|
|
app(CandidateProfiles::class)->save($this->account, [...$this->profile, 'version' => 1, 'name' => 'Nome alterado depois de submeter']);
|
|
$this->travel(8)->days();
|
|
$close = ['version' => $this->process->fresh()->version, 'reason' => 'Concluir recepção do processo fictício.'];
|
|
$this->postJson('/pessoas/'.$this->process->id.'/close', $close)->assertUnprocessable();
|
|
$this->get('/pessoas')->assertInertia(fn (Assert $p) => $p->where('workdesk.tasks.0.action', 'triage'));
|
|
$body = electronicTriagePayload($this);
|
|
$url = '/pessoas/'.$this->process->id.'/recebidas/'.$submission->id.'/triagem';
|
|
$this->postJson($url, $body)->assertRedirect();
|
|
$this->postJson($url, $body)->assertRedirect();
|
|
$application = DB::table('people_applications')->sole();
|
|
expect($application->name)->toBe('Pessoa fictícia')->and($application->reference)->toBe($submission->reference)->and($application->created_by)->toBe($this->admin->id)
|
|
->and(DB::table('candidate_submissions')->sole()->snapshot)->toBe($snapshot)->and(DB::table('candidate_triage')->count())->toBe(1);
|
|
$this->postJson('/pessoas/'.$this->process->id.'/close', $close)->assertRedirect();
|
|
$record = $this->process->fresh();
|
|
$evaluation = ['version' => $record->version, 'reason' => 'Avaliar candidatura fictícia com evidências.', 'no_conflict' => true, 'application_id' => $application->id,
|
|
'answers' => [['score' => '8', 'evidence' => 'Evidência fictícia das competências.']]];
|
|
$this->postJson('/pessoas/'.$record->id.'/evaluate', $evaluation)->assertForbidden();
|
|
$this->actingAs($this->reviewer)->postJson('/pessoas/'.$record->id.'/evaluate', $evaluation)->assertRedirect();
|
|
expect(app(PeopleHiring::class)->matrix($record)[0]['electronic_source']['profile_version'])->toBe(1);
|
|
});
|
|
|
|
test('candidate sees only the approved triage communication and refusal creates no assessment row', function () {
|
|
$this->postJson($this->base.'/oportunidades/'.$this->process->id, $this->body)->assertRedirect();
|
|
$submission = DB::table('candidate_submissions')->sole();
|
|
$body = electronicTriagePayload($this, 'not_accepted');
|
|
$url = '/pessoas/'.$this->process->id.'/recebidas/'.$submission->id.'/triagem';
|
|
$this->postJson($url, $body)->assertRedirect();
|
|
expect(DB::table('people_applications')->count())->toBe(0);
|
|
$this->get($this->base.'/submissoes/'.$submission->id)->assertInertia(fn (Assert $p) => $p->where('decision.outcome', 'not_accepted')->where('decision.message', $body['public_message'])->missing('decision.reason')->missing('decision.created_by')->missing('decision.people_application_id'))->assertDontSee('MOTIVO-INTERNO-RESERVADO');
|
|
$this->postJson($url, electronicTriagePayload($this))->assertConflict();
|
|
});
|
|
|
|
test('triage respects current assignment stale process versions and separates confidential source reading', function () {
|
|
$this->postJson($this->base.'/oportunidades/'.$this->process->id, $this->body)->assertRedirect();
|
|
$submission = DB::table('candidate_submissions')->sole();
|
|
$body = electronicTriagePayload($this);
|
|
$url = '/pessoas/'.$this->process->id.'/recebidas/'.$submission->id.'/triagem';
|
|
$this->postJson($url, [...$body, 'process_version' => 99])->assertUnprocessable();
|
|
$this->actingAs($this->reviewer)->postJson($url, $body)->assertForbidden();
|
|
$this->get('/pessoas/'.$this->process->id.'/recebidas')->assertForbidden();
|
|
$this->actingAs($this->admin);
|
|
$state = DB::table('people_access_states')->where('people_process_id', $this->process->id)->value('version');
|
|
app(PeopleAccess::class)->assign($this->admin, $this->process->id, ['version' => $state, 'user_id' => $this->reviewer->id, 'scopes' => ['review', 'interviews'], 'reason' => 'Autorizar consulta das evidências para análise.']);
|
|
$this->actingAs($this->reviewer)->get('/pessoas/'.$this->process->id.'/recebidas')->assertInertia(fn (Assert $p) => $p->where('canPrepare', false)->where('canTriage', false)->has('submissions.data', 1));
|
|
$this->postJson($url, $body)->assertForbidden();
|
|
});
|
|
|
|
test('triage audit failure rolls back the linked internal candidature decision and candidate event', function () {
|
|
$this->postJson($this->base.'/oportunidades/'.$this->process->id, $this->body)->assertRedirect();
|
|
$submission = DB::table('candidate_submissions')->sole();
|
|
$events = DB::table('candidate_events')->count();
|
|
$this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->once()->andThrow(new RuntimeException('Injected triage audit failure'));
|
|
expect(fn () => app(CandidateTriage::class)->decide($this->admin, $this->process->id, $submission->id, electronicTriagePayload($this)))->toThrow(RuntimeException::class);
|
|
expect(DB::table('candidate_triage')->count())->toBe(0)->and(DB::table('people_applications')->count())->toBe(0)->and(DB::table('candidate_events')->count())->toBe($events);
|
|
});
|
|
|
|
test('a reference already entered manually requires reconciliation instead of automatic merging', function () {
|
|
$this->postJson($this->base.'/oportunidades/'.$this->process->id, $this->body)->assertRedirect();
|
|
$submission = DB::table('candidate_submissions')->sole();
|
|
DB::table('people_applications')->insert(['tenant_id' => $this->tenant->id, 'people_process_id' => $this->process->id, 'created_by' => $this->admin->id, 'operation_key' => (string) Str::uuid(), 'name' => 'Registo manual anterior', 'summary' => 'Não fundir este registo automaticamente.', 'reference' => $submission->reference, 'received_on' => today(), 'created_at' => now(), 'updated_at' => now()]);
|
|
$this->postJson('/pessoas/'.$this->process->id.'/recebidas/'.$submission->id.'/triagem', electronicTriagePayload($this))->assertConflict();
|
|
expect(DB::table('candidate_triage')->count())->toBe(0)->and(DB::table('people_applications')->count())->toBe(1);
|
|
});
|
|
|
|
test('legacy assessment already under review cannot receive a new admitted candidature through triage', function () {
|
|
$this->postJson($this->base.'/oportunidades/'.$this->process->id, $this->body)->assertRedirect();
|
|
$submission = DB::table('candidate_submissions')->sole();
|
|
// Fixture for a process whose assessment began before triage was introduced.
|
|
$application = DB::table('people_applications')->insertGetId(['tenant_id' => $this->tenant->id, 'people_process_id' => $this->process->id, 'created_by' => $this->admin->id,
|
|
'operation_key' => (string) Str::uuid(), 'name' => 'Candidatura anterior', 'summary' => 'Registo anterior à introdução da triagem.', 'reference' => 'LEGACY-TEST', 'received_on' => today(), 'created_at' => now(), 'updated_at' => now()]);
|
|
DB::table('people_reviews')->insert(['tenant_id' => $this->tenant->id, 'people_application_id' => $application, 'reviewed_by' => $this->reviewer->id, 'answers' => '[]', 'reason' => 'Avaliação anterior da fixture.', 'score_minor' => 800, 'created_at' => now(), 'updated_at' => now()]);
|
|
DB::table('people_processes')->where('id', $this->process->id)->update(['status' => 'assessment', 'version' => $this->process->fresh()->version + 1]);
|
|
$url = '/pessoas/'.$this->process->id.'/recebidas/'.$submission->id.'/triagem';
|
|
$this->postJson($url, electronicTriagePayload($this))->assertConflict();
|
|
$this->postJson($url, electronicTriagePayload($this, 'not_accepted'))->assertRedirect();
|
|
expect(DB::table('people_applications')->count())->toBe(1);
|
|
});
|