276 lines
16 KiB
PHP
276 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Course;
|
|
use App\Models\EducationInvestmentProposal;
|
|
use App\Models\LabourDemand;
|
|
use App\Models\Occupation;
|
|
use App\Models\Opportunity;
|
|
use App\Models\Project;
|
|
use App\Models\Sector;
|
|
use App\Models\Skill;
|
|
use App\Models\TrainingCapacity;
|
|
use App\Models\TrainingProvider;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia;
|
|
use Tests\TestCase;
|
|
|
|
class EducationTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
// No fresh migrations or destructive operations: require a prepared isolated DB.
|
|
$this->assertSame('testing', app()->environment());
|
|
$this->assertSame('pgsql', DB::connection()->getDriverName());
|
|
$this->assertStringEndsWith('_test', DB::connection()->getDatabaseName());
|
|
$this->assertSame(DB::connection()->getDatabaseName(), DB::selectOne('select current_database() as name')->name);
|
|
DB::beginTransaction();
|
|
$this->beforeApplicationDestroyed(fn () => DB::rollBack());
|
|
$this->withoutVite();
|
|
config(['education.enabled' => true]);
|
|
}
|
|
|
|
private function manager(): User
|
|
{
|
|
$user = User::factory()->create();
|
|
config(['portal.managers' => [...config('portal.managers', []), $user->email]]);
|
|
$this->actingAs($user);
|
|
|
|
return $user;
|
|
}
|
|
|
|
private function declaration(): array
|
|
{
|
|
$country = (string) Str::uuid();
|
|
$province = (string) Str::uuid();
|
|
$territory = (string) Str::uuid();
|
|
DB::table('geo_locations')->insert(['id' => $country, 'name' => 'País sintético', 'code' => $country, 'location_type' => 'country']);
|
|
DB::table('geo_locations')->insert(['id' => $province, 'name' => 'Província sintética', 'code' => $province,
|
|
'location_type' => 'province', 'parent_id' => $country]);
|
|
DB::table('geo_locations')->insert(['id' => $territory, 'name' => 'Distrito sintético', 'code' => $territory,
|
|
'location_type' => 'district', 'parent_id' => $province]);
|
|
|
|
return [
|
|
'project_id' => Project::create(['code' => (string) Str::uuid(), 'name' => 'Projecto sintético', 'status' => 'draft'])->id,
|
|
'occupation_id' => Occupation::create(['code' => (string) Str::uuid(), 'name' => 'Ocupação sintética'])->id,
|
|
'skill_id' => Skill::create(['code' => (string) Str::uuid(), 'name' => 'Competência sintética'])->id,
|
|
'geo_location_id' => $territory, 'employer' => 'Empregador sintético', 'qualification' => 'Nível de teste',
|
|
'quantity' => 100, 'period_start' => now()->addMonths(3)->toDateString(),
|
|
'period_end' => now()->addMonths(6)->toDateString(), 'reference_date' => now()->toDateString(),
|
|
'valid_until' => now()->addYear()->toDateString(), 'source' => 'Fonte exclusivamente sintética para teste.',
|
|
];
|
|
}
|
|
|
|
private function course(): Course
|
|
{
|
|
$provider = TrainingProvider::create(['name' => 'Instituição sintética', 'status' => 'candidate']);
|
|
|
|
return Course::create(['provider_id' => $provider->id, 'code' => (string) Str::uuid(), 'title' => 'Curso sintético', 'status' => 'draft']);
|
|
}
|
|
|
|
public function test_educational_proposal_requires_review_and_is_a_single_unpublished_canonical_opportunity(): void
|
|
{
|
|
$manager = $this->manager();
|
|
$demandData = $this->declaration();
|
|
$this->post('/management/education/demands', $demandData)->assertSessionHasNoErrors();
|
|
$demand = LabourDemand::where('project_id', $demandData['project_id'])->firstOrFail();
|
|
$sector = Sector::create(['name' => 'Sector sintético', 'slug' => (string) Str::uuid(), 'is_active' => true]);
|
|
$data = ['demand_version' => 1, 'sector_id' => $sector->id, 'title' => 'Oficina sintética',
|
|
'intervention' => 'Resposta formativa sintética para verificação do fluxo.', 'risks' => 'Custos e viabilidade por confirmar.',
|
|
'source' => 'Fonte sintética identificada no teste.', 'currency' => 'MZN'];
|
|
$data['district_id'] = $demandData['geo_location_id'];
|
|
$data['province_id'] = DB::table('geo_locations')->where('id', $demandData['geo_location_id'])->value('parent_id');
|
|
$url = '/management/education/demands/'.$demand->id.'/investment';
|
|
$this->post($url, $data)->assertUnprocessable();
|
|
$this->manager();
|
|
$this->post('/management/education/demands/'.$demand->id.'/review', ['status' => 'reviewed', 'version_no' => 1,
|
|
'review_reason' => 'Revisão independente sintética.'])->assertSessionHasNoErrors();
|
|
$this->post($url, $data)->assertConflict();
|
|
$data['demand_version'] = 2;
|
|
$this->post($url, [...$data, 'status' => 'published'])->assertRedirect()->assertSessionHasNoErrors();
|
|
$this->post($url, $data)->assertRedirect()->assertSessionHasNoErrors();
|
|
$this->assertSame(1, EducationInvestmentProposal::where('labour_demand_id', $demand->id)->count());
|
|
$proposal = EducationInvestmentProposal::where('labour_demand_id', $demand->id)->firstOrFail();
|
|
$opportunity = Opportunity::findOrFail($proposal->opportunity_id);
|
|
$this->assertSame('draft', $opportunity->status);
|
|
$this->assertNull($opportunity->published_at);
|
|
$this->assertNull($opportunity->estimated_investment_amount);
|
|
$this->assertFalse(Opportunity::published()->whereKey($opportunity->id)->exists());
|
|
$this->assertSame(1, DB::table('audit_events')->where('entity_id', $proposal->id)->count());
|
|
$this->get('/management/education/demands/'.$demand->id.'/analysis')->assertInertia(fn (AssertableInertia $page) => $page
|
|
->where('proposal.opportunity_id', $opportunity->id)->where('proposal.demand_version', 2));
|
|
$this->put('/management/opportunities/'.$opportunity->id, [
|
|
'sector_id' => $sector->id, 'reference' => $opportunity->reference, 'title' => $opportunity->title,
|
|
'slug' => $opportunity->slug, 'status' => 'published', 'is_featured' => false, 'estimated_investment_currency' => 'MZN',
|
|
])->assertSessionHasErrors('status');
|
|
$this->assertSame('draft', $opportunity->fresh()->status);
|
|
$this->travel(2)->years();
|
|
$this->post($url, $data)->assertUnprocessable();
|
|
$this->travelBack();
|
|
}
|
|
|
|
public function test_education_filters_and_record_indicators_do_not_treat_expired_records_as_current(): void
|
|
{
|
|
$this->manager();
|
|
$data = $this->declaration();
|
|
$this->post('/management/education/demands', $data)->assertSessionHasNoErrors();
|
|
$this->get('/management/education?q=Nível&status=submitted&geo_location_id='.$data['geo_location_id'])
|
|
->assertInertia(fn (AssertableInertia $page) => $page->has('demands.data', 1)
|
|
->where('indicators.pending_demands', 1)->where('indicators.expired_demands', 0));
|
|
$this->get('/management/education?q=inexistente')->assertInertia(fn (AssertableInertia $page) => $page->has('demands.data', 0));
|
|
$this->travel(2)->years();
|
|
$this->get('/management/education?status=submitted')->assertInertia(fn (AssertableInertia $page) => $page
|
|
->has('demands.data', 0)->where('indicators.pending_demands', 0)->where('indicators.expired_demands', 1));
|
|
$this->get('/management/education?status=expired')->assertInertia(fn (AssertableInertia $page) => $page->has('demands.data', 1));
|
|
$this->travelBack();
|
|
}
|
|
|
|
public function test_provider_and_course_catalogues_follow_documented_capacity_territory(): void
|
|
{
|
|
$manager = $this->manager();
|
|
$first = $this->declaration();
|
|
$firstCourse = $this->course();
|
|
$capacity = $first;
|
|
unset($capacity['project_id'], $capacity['employer']);
|
|
TrainingCapacity::create([...$capacity, 'course_id' => $firstCourse->id, 'submitted_by' => $manager->id]);
|
|
|
|
$otherProvince = (string) Str::uuid();
|
|
$otherDistrict = (string) Str::uuid();
|
|
$country = DB::table('geo_locations')->where('id', $first['geo_location_id'])->value('parent_id');
|
|
$country = DB::table('geo_locations')->where('id', $country)->value('parent_id');
|
|
DB::table('geo_locations')->insert(['id' => $otherProvince, 'name' => 'Província de teste B', 'code' => $otherProvince,
|
|
'location_type' => 'province', 'parent_id' => $country]);
|
|
DB::table('geo_locations')->insert(['id' => $otherDistrict, 'name' => 'Distrito de teste B', 'code' => $otherDistrict,
|
|
'location_type' => 'district', 'parent_id' => $otherProvince]);
|
|
$otherCourse = $this->course();
|
|
TrainingCapacity::create([...$capacity, 'geo_location_id' => $otherDistrict, 'course_id' => $otherCourse->id, 'submitted_by' => $manager->id]);
|
|
|
|
$this->get('/management/education?geo_location_id='.$first['geo_location_id'])
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->where('filters.geo_location_id', $first['geo_location_id'])
|
|
->has('providers', 1)->where('providers.0.id', $firstCourse->provider_id)
|
|
->has('courses', 1)->where('courses.0.id', $firstCourse->id));
|
|
}
|
|
|
|
public function test_access_requires_verified_manager_and_enabled_feature(): void
|
|
{
|
|
$this->get('/management/education')->assertRedirect('/login');
|
|
$this->actingAs(User::factory()->create())->get('/management/education')->assertForbidden();
|
|
$this->manager();
|
|
config(['education.enabled' => false]);
|
|
$this->get('/management/education')->assertNotFound();
|
|
$this->post('/management/education/catalog/skills', ['name' => 'Teste', 'code' => 'TEST'])->assertNotFound();
|
|
config(['education.enabled' => true]);
|
|
$this->get('/management/education')->assertOk()->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('education/index')->where('educationEnabled', true));
|
|
$this->actingAs(User::factory()->unverified()->create())->get('/management/education')->assertRedirect('/email/verify');
|
|
}
|
|
|
|
public function test_intake_does_not_accept_official_status_or_caller_supplied_reviewer(): void
|
|
{
|
|
$user = $this->manager();
|
|
$data = $this->declaration();
|
|
$this->post('/management/education/demands', [...$data, 'status' => 'certified', 'reviewed_by' => $user->id])
|
|
->assertSessionHasNoErrors()->assertRedirect();
|
|
$demand = LabourDemand::where('project_id', $data['project_id'])->firstOrFail();
|
|
$this->assertSame('submitted', $demand->status);
|
|
$this->assertNull($demand->reviewed_by);
|
|
$this->assertSame($user->id, $demand->submitted_by);
|
|
$audit = DB::table('audit_events')->where('entity_id', $demand->id)->first();
|
|
$this->assertNotNull($audit);
|
|
$this->assertStringNotContainsString($data['source'], $audit->after_data);
|
|
$this->post('/management/education/demands', [...$data, 'quantity' => -1])->assertSessionHasErrors('quantity');
|
|
$this->post('/management/education/demands', [...$data, 'period_end' => now()->toDateString()])->assertSessionHasErrors('period_end');
|
|
$country = (string) Str::uuid();
|
|
DB::table('geo_locations')->insert([
|
|
'id' => $country, 'name' => 'País fora do escopo', 'code' => $country, 'location_type' => 'country',
|
|
]);
|
|
$this->post('/management/education/demands', [...$data, 'geo_location_id' => $country])
|
|
->assertSessionHasErrors('geo_location_id');
|
|
Skill::findOrFail($data['skill_id'])->delete();
|
|
$this->post('/management/education/demands', $data)->assertSessionHasErrors('skill_id');
|
|
}
|
|
|
|
public function test_review_requires_another_person_current_evidence_and_matching_version(): void
|
|
{
|
|
$this->manager();
|
|
$data = $this->declaration();
|
|
$this->post('/management/education/demands', $data)->assertSessionHasNoErrors();
|
|
$demand = LabourDemand::where('project_id', $data['project_id'])->firstOrFail();
|
|
$url = '/management/education/demands/'.$demand->id.'/review';
|
|
$decision = ['status' => 'reviewed', 'version_no' => 1, 'review_reason' => 'Revisão independente com evidência sintética.'];
|
|
$this->post($url, $decision)->assertForbidden();
|
|
$reviewer = $this->manager();
|
|
$this->post($url, [...$decision, 'status' => 'accredited'])->assertSessionHasErrors('status');
|
|
$this->post($url, $decision)->assertSessionHasNoErrors()->assertRedirect();
|
|
$this->assertSame($reviewer->id, $demand->fresh()->reviewed_by);
|
|
$this->assertSame(2, $demand->fresh()->version_no);
|
|
$this->post($url, $decision)->assertConflict();
|
|
$this->travel(2)->years();
|
|
$this->post($url, [...$decision, 'version_no' => 2])->assertUnprocessable();
|
|
$this->travelBack();
|
|
}
|
|
|
|
public function test_retired_courses_and_inactive_providers_cannot_receive_capacity(): void
|
|
{
|
|
$this->manager();
|
|
$data = $this->declaration();
|
|
$course = $this->course();
|
|
$data['course_id'] = $course->id;
|
|
$this->post('/management/education/capacities', $data)->assertRedirect()->assertSessionHasNoErrors();
|
|
$this->assertDatabaseHas('training_capacities', ['course_id' => $course->id, 'status' => 'submitted']);
|
|
$course->update(['status' => 'retired']);
|
|
$this->post('/management/education/capacities', $data)->assertSessionHasErrors('course_id');
|
|
$course->update(['status' => 'draft']);
|
|
TrainingProvider::findOrFail($course->provider_id)->update(['status' => 'inactive']);
|
|
$this->post('/management/education/capacities', $data)->assertSessionHasErrors('course_id');
|
|
}
|
|
|
|
public function test_catalogue_keeps_institutions_and_courses_nonofficial(): void
|
|
{
|
|
$this->manager();
|
|
$name = 'Instituição sintética '.Str::uuid();
|
|
$this->post('/management/education/catalog/providers', ['name' => $name, 'status' => 'approved'])
|
|
->assertRedirect()->assertSessionHasNoErrors();
|
|
$provider = TrainingProvider::where('name', $name)->firstOrFail();
|
|
$this->assertSame('candidate', $provider->status);
|
|
$data = ['provider_id' => $provider->id, 'title' => 'Curso sintético', 'code' => (string) Str::uuid(), 'status' => 'published'];
|
|
$this->post('/management/education/catalog/courses', $data)->assertRedirect()->assertSessionHasNoErrors();
|
|
$this->assertDatabaseHas('courses', ['code' => $data['code'], 'status' => 'draft']);
|
|
$this->post('/management/education/catalog/courses', $data)->assertSessionHasErrors('code');
|
|
$provider->update(['status' => 'inactive']);
|
|
$this->post('/management/education/catalog/courses', [...$data, 'code' => (string) Str::uuid()])
|
|
->assertSessionHasErrors('provider_id');
|
|
}
|
|
|
|
public function test_matching_never_subtracts_training_places_from_workers_and_excludes_unavailable_courses(): void
|
|
{
|
|
$submitter = $this->manager();
|
|
$data = $this->declaration();
|
|
$this->post('/management/education/demands', $data)->assertSessionHasNoErrors();
|
|
$demand = LabourDemand::where('project_id', $data['project_id'])->firstOrFail();
|
|
$course = $this->course();
|
|
$capacity = $data;
|
|
unset($capacity['project_id'], $capacity['employer']);
|
|
$reviewer = $this->manager();
|
|
TrainingCapacity::create([...$capacity, 'course_id' => $course->id, 'submitted_by' => $submitter->id,
|
|
'reviewed_by' => $reviewer->id, 'status' => 'reviewed', 'review_reason' => 'Evidência sintética revista.',
|
|
'period_start' => now()->toDateString(), 'period_end' => $data['period_start']]);
|
|
$url = '/management/education/demands/'.$demand->id.'/analysis';
|
|
$this->get($url)->assertInertia(fn (AssertableInertia $page) => $page->where('eligible', false)->has('candidates', 0));
|
|
$this->post('/management/education/demands/'.$demand->id.'/review', ['status' => 'reviewed', 'version_no' => 1,
|
|
'review_reason' => 'Revisão independente sintética.'])->assertSessionHasNoErrors();
|
|
$this->get($url)->assertInertia(fn (AssertableInertia $page) => $page->component('education/analysis')
|
|
->where('eligible', true)->has('candidates', 1)->where('gap.people', null)->where('gap.state', 'insufficient_data'));
|
|
$course->delete();
|
|
$this->get($url)->assertInertia(fn (AssertableInertia $page) => $page->has('candidates', 0)->where('gap.people', null));
|
|
$this->travel(2)->years();
|
|
$this->get($url)->assertInertia(fn (AssertableInertia $page) => $page->where('eligible', false)->where('gap.state', 'expired'));
|
|
$this->travelBack();
|
|
}
|
|
}
|