withoutVite(); $this->owner = User::factory()->create(); config(['education.enabled' => true, 'portal.managers' => [$this->owner->email]]); $this->actingAs($this->owner); } private function valid(string $kind = 'demands'): array { $province = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $province, 'name' => 'SYNTHETIC QA province', 'code' => $province, 'location_type' => 'province']); $data = ['occupation_id' => Occupation::create(['code' => (string) Str::uuid(), 'name' => 'Synthetic occupation'])->id, 'skill_id' => Skill::create(['code' => (string) Str::uuid(), 'name' => 'Synthetic skill'])->id, 'geo_location_id' => $province, 'qualification' => 'Synthetic qualification', 'quantity' => 20, 'period_start' => today()->addMonth()->toDateString(), 'period_end' => today()->addMonths(2)->toDateString(), 'source' => 'SENSITIVE SYNTHETIC QA SOURCE TEXT', 'reference_date' => today()->toDateString(), 'valid_until' => today()->addYear()->toDateString()]; if ($kind === 'demands') { return [...$data, 'employer' => 'SENSITIVE SYNTHETIC QA EMPLOYER', 'project_id' => Project::create(['code' => (string) Str::uuid(), 'name' => 'Synthetic project', 'status' => 'draft'])->id]; } $provider = TrainingProvider::create(['name' => 'Synthetic provider', 'status' => 'candidate']); return [...$data, 'course_id' => Course::create(['provider_id' => $provider->id, 'code' => (string) Str::uuid(), 'title' => 'Synthetic course', 'status' => 'draft'])->id]; } private function create(string $kind, array $payload = []): array { $input = ['client_key' => (string) Str::uuid(), 'version_no' => 0, 'payload' => $payload]; $response = $this->postJson('/management/education/drafts/'.$kind, $input)->assertOk()->assertHeader('Cache-Control', 'no-store, private'); return [$response->json('id'), $input]; } public function test_incomplete_drafts_are_private_encrypted_recoverable_and_repeated_requests_do_not_duplicate(): void { [$id, $input] = $this->create('demands', ['employer' => 'PRIVATE SYNTHETIC EMPLOYER']); $this->postJson('/management/education/drafts/demands', $input)->assertOk()->assertJsonPath('version_no', 1)->assertJsonPath('id', $id); $this->assertDatabaseCount('education_declaration_drafts', 1); $this->assertDatabaseCount('labour_demands', 0); $this->assertStringNotContainsString('PRIVATE SYNTHETIC', DB::table('education_declaration_drafts')->sole()->payload); $this->assertStringNotContainsString('PRIVATE SYNTHETIC', DB::table('audit_events')->where('source_system', 'education-drafts')->first()->after_data); $other = User::factory()->create(); config(['portal.managers' => [$this->owner->email, $other->email]]); $this->actingAs($other)->getJson('/management/education/drafts/demands/'.$id)->assertNotFound(); $this->get('/management/education?draft='.$id)->assertNotFound(); $this->get('/management/education')->assertOk()->assertInertia(fn ($page) => $page->has('ownDrafts.data', 0)->where('selectedDraft', null)); $this->getJson('/management/education/drafts/demands')->assertOk()->assertJsonCount(0, 'data'); $this->putJson('/management/education/drafts/demands/'.$id, ['version_no' => 1, 'payload' => []])->assertNotFound(); $this->actingAs(User::findOrFail($this->owner->id))->getJson('/management/education/drafts/demands/'.$id)->assertOk()->assertJsonPath('payload.employer', 'PRIVATE SYNTHETIC EMPLOYER'); $this->get('/management/education?draft='.$id)->assertOk()->assertHeader('Cache-Control', 'no-store, private') ->assertInertia(fn ($page) => $page->component('education/index')->where('selectedDraft.id', $id) ->where('selectedDraft.payload.employer', 'PRIVATE SYNTHETIC EMPLOYER')->has('ownDrafts.data', 1)->missing('ownDrafts.data.0.payload')); $this->actingAs(User::factory()->create())->postJson('/management/education/drafts/demands', $input)->assertForbidden(); } public function test_stale_edits_and_wrong_kind_are_rejected_while_incomplete_submission_creates_nothing(): void { [$id, $input] = $this->create('demands'); $url = '/management/education/drafts/demands/'.$id; $this->putJson($url, ['version_no' => 1, 'payload' => ['quantity' => 0]])->assertOk()->assertJsonPath('version_no', 2); $this->putJson($url, ['version_no' => 1, 'payload' => ['quantity' => 0]])->assertOk()->assertJsonPath('version_no', 2); $this->putJson($url, ['version_no' => 1, 'payload' => ['quantity' => 5]])->assertConflict(); $this->postJson('/management/education/drafts/capacities', $input)->assertConflict(); $this->getJson('/management/education/drafts/capacities/'.$id)->assertNotFound(); $this->putJson($url, ['version_no' => 2, 'payload' => ['status' => 'reviewed']])->assertUnprocessable(); $this->postJson($url.'/submit', ['version_no' => 2, 'payload' => ['quantity' => 0]])->assertUnprocessable(); $this->assertDatabaseCount('labour_demands', 0); $this->getJson($url)->assertJsonPath('state', 'draft'); } public function test_distinct_numeric_text_is_not_collapsed_and_rollback_preserves_drafts(): void { [$id] = $this->create('demands', ['employer' => '001']); $this->putJson('/management/education/drafts/demands/'.$id, ['version_no' => 1, 'payload' => ['employer' => '1']])->assertOk() ->assertJsonPath('version_no', 2)->assertJsonPath('payload.employer', '1'); $migration = require database_path('migrations/2026_11_02_230000_education_declaration_drafts.php'); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Preserve education drafts'); $migration->down(); } public function test_submission_is_single_preserves_snapshot_and_still_requires_independent_review(): void { $payload = $this->valid(); [$id] = $this->create('demands', ['employer' => $payload['employer']]); $url = '/management/education/drafts/demands/'.$id; $input = ['version_no' => 1, 'payload' => $payload]; $record = $this->postJson($url.'/submit', $input)->assertOk()->assertJsonPath('state', 'submitted')->json('record_id'); $this->postJson($url.'/submit', $input)->assertOk()->assertJsonPath('record_id', $record); $this->assertDatabaseCount('labour_demands', 1); $this->assertSame('submitted', DB::table('labour_demands')->sole()->status); $this->postJson('/management/education/demands/'.$record.'/review', ['version_no' => 1, 'status' => 'reviewed', 'review_reason' => 'Synthetic self-review must be refused.'])->assertForbidden(); $this->putJson($url, ['version_no' => 2, 'payload' => $payload])->assertConflict(); $this->postJson($url.'/submit', [...$input, 'payload' => [...$payload, 'quantity' => 50]])->assertConflict(); Project::whereKey($payload['project_id'])->update(['name' => 'Changed synthetic project']); $this->getJson($url)->assertJsonPath('references.project_id.name', 'Synthetic project')->assertJsonPath('payload.quantity', 20); $this->get('/management/education?draft='.$id)->assertOk()->assertInertia(fn ($page) => $page->where('selectedDraft.state', 'submitted') ->where('selectedDraft.references.project_id.name', 'Synthetic project')->where('ownDrafts.data.0.label', 'Synthetic qualification')); } public function test_capacity_reuses_final_validation_and_does_not_accept_employer_or_project_fields(): void { $payload = $this->valid('capacities'); [$id] = $this->create('capacities', [...$payload, 'employer' => 'NOT APPLICABLE', 'project_id' => 'not-applicable']); $url = '/management/education/drafts/capacities/'.$id; $this->getJson($url)->assertJsonMissingPath('payload.employer')->assertJsonMissingPath('payload.project_id'); Course::whereKey($payload['course_id'])->update(['status' => 'retired']); $this->postJson($url.'/submit', ['version_no' => 1, 'payload' => $payload])->assertUnprocessable(); Course::whereKey($payload['course_id'])->update(['status' => 'draft']); $this->postJson($url.'/submit', ['version_no' => 1, 'payload' => $payload])->assertOk()->assertJsonPath('state', 'submitted'); $this->assertDatabaseCount('training_capacities', 1); $this->assertDatabaseCount('labour_demands', 0); } }