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)->assertOk()->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(); } }