assertTrue(IsolatedQaDatabase::allowsSchemaChanges()); $this->assertTrue(Schema::hasColumn('investment_zones', 'editorial_draft')); $this->assertTrue(Schema::hasColumn('investment_zones', 'operational_details')); $this->withoutVite(); [$this->author,$this->reviewer] = User::factory()->count(2)->create()->all(); config(['portal.managers' => [$this->author->email, $this->reviewer->email], 'portal.profile_access_enabled' => false, 'editorial_workflow.enabled' => true, 'editorial_workflow.zone_review' => true]); $this->actingAsWithMfa($this->reviewer); $this->actingAsWithMfa($this->author); $country = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $country, 'code' => 'ZONE-'.Str::random(12), 'name' => 'Synthetic reviewed country', 'location_type' => 'country', 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$country]); $province = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $province, 'parent_id' => $country, 'code' => 'ZONE-'.Str::random(12), 'name' => 'Synthetic reviewed province', 'location_type' => 'province', 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$province]); // Previously published synthetic baseline; this is not an approval of a real source. $this->zone = InvestmentZone::create(['code' => 'ZONE-'.Str::random(10), 'name' => 'Original public zone', 'description' => 'Synthetic informational description', 'category' => 'industrial_park', 'physical_category' => 'industrial_park', 'legal_regime' => 'pending', 'primary_territory_id' => $province, 'territory_version' => 1, 'version_no' => 1, 'publication_status' => 'published', 'validation_status' => 'validated', 'editor_id' => $this->author->id, 'reviewer_id' => $this->author->id, 'published_at' => now(), 'source_evidence' => [['title' => 'Synthetic institutional report', 'url' => 'https://example.test/institutional', 'source_type' => 'institutional', 'supports' => 'Synthetic evidence of the identity and location only']]]); } private function profile(): InvestmentZoneOperationalProfile { return app(InvestmentZoneOperationalProfile::class); } private function prepare(): void { $this->actingAsWithMfa($this->author); $fields = ['version_no' => $this->profile()->workingVersion($this->zone->fresh()), 'name' => 'Private revised zone', 'operational_details' => ['schema_version' => 1, 'integral' => ['schema_version' => 1, 'observations' => [['id' => (string) Str::uuid(), 'label' => 'Synthetic investment observation', 'knowledge' => 'documented', 'disclosure' => true, 'source_reference' => 'Synthetic report page 2', 'source_date' => '2026-01-15', 'scope' => 'This synthetic zone only', 'indicator' => 'investment', 'measure_kind' => 'observed', 'value' => '9007199254740.25', 'unit' => 'MZN', 'period_start' => '2025-01-01', 'period_end' => '2025-12-31', 'population' => 'Declared establishments', 'methodology' => 'Synthetic documentary record']]]]]; app(InvestmentZones::class)->save($this->zone->id, $fields, $this->author->id); } private function decision(string $action, ?User $actor = null): array { $actor ??= $action === 'submit' ? $this->author : $this->reviewer; $this->actingAsWithMfa($actor); return $this->profile()->review($this->zone->id, $actor, $this->profile()->workingVersion($this->zone->fresh()), $action, 'Synthetic review decision with source'); } private function approve(): void { foreach (['submit', 'start_review', 'approve'] as $action) { $this->decision($action); } } public function test_indicator_preparation_and_approval_preserve_the_public_projection_until_explicit_publication(): void { $before = Arr::except($this->zone->fresh()->getAttributes(), ['editorial_draft']); $this->prepare(); $this->assertSame($before, Arr::except($this->zone->fresh()->getAttributes(), ['editorial_draft'])); $this->assertSame([], app(InvestmentZoneOperationalDetails::class)->publicData($this->zone->fresh())); $this->approve(); $this->assertSame($before, Arr::except($this->zone->fresh()->getAttributes(), ['editorial_draft'])); $this->actingAsWithMfa($this->reviewer); $saved = app(InvestmentZones::class)->publish($this->zone->id, $this->profile()->workingVersion($this->zone->fresh()), $this->reviewer->id); $this->assertSame('Private revised zone', $saved->name); $rows = app(InvestmentMapIndicators::class)->forZones(collect([$saved]))['records']; $this->assertCount(1, $rows); $this->assertSame('9007199254740.25', $rows[0]['value']); $this->assertSame('observed', $rows[0]['measure_kind']); $this->assertSame('This synthetic zone only', $rows[0]['scope']); $this->assertSame('Synthetic report page 2', $rows[0]['source_reference']); $this->assertNull($this->profile()->draft($saved)); $this->assertNotEmpty(app(PortalEditorialWorkflow::class)->notices($this->author)); } public function test_editing_after_approval_requires_a_new_decision_and_direct_publish_cannot_bypass_it(): void { $this->prepare(); $this->approve(); $this->actingAsWithMfa($this->author); app(InvestmentZones::class)->save($this->zone->id, ['version_no' => $this->profile()->workingVersion($this->zone->fresh()), 'description' => 'Deliberate later change'], $this->author->id); $this->assertSame('draft', $this->profile()->management($this->zone->fresh())['editorial_workflow']['state']); try { app(InvestmentZones::class)->publish($this->zone->id, $this->profile()->workingVersion($this->zone->fresh()), $this->author->id); $this->fail('Approval must be renewed'); } catch (HttpException $e) { $this->assertSame(409, $e->getStatusCode()); } $this->assertSame('Original public zone', $this->zone->fresh()->name); } public function test_public_suspension_preserves_the_private_review_and_approved_exact_content(): void { $this->prepare(); $this->approve(); $this->actingAsWithMfa($this->reviewer); $saved = app(InvestmentZones::class)->suspend($this->zone->id, $this->profile()->workingVersion($this->zone->fresh()), $this->reviewer->id); $working = $this->profile()->management($saved); $this->assertSame('approved', $working['editorial_workflow']['state']); $this->assertTrue($working['editorial_workflow']['fresh']); $this->assertSame('suspended', $saved->publication_status); app(InvestmentZones::class)->publish($saved->id, $working['version_no'], $this->reviewer->id); $this->assertSame('Private revised zone', $saved->fresh()->name); } public function test_history_recovers_the_exact_private_observation_without_overwriting_current_content(): void { $this->prepare(); $version = $this->profile()->workingVersion($this->zone->fresh()); $url = '/api/v2/investment/admin/zones/'.$this->zone->id.'/revisions/'.$version; $before = $this->zone->fresh()->getAttributes(); $this->getJson($url)->assertOk()->assertJsonPath('fields.name', 'Private revised zone')->assertJsonPath('observations.0.value', '9007199254740.25')->assertJsonMissingPath('fields.editorial_draft')->assertJsonMissingPath('private_ciphertext'); $this->assertSame($before, $this->zone->fresh()->getAttributes()); $this->getJson(str_replace($this->zone->id, (string) Str::uuid(), $url))->assertNotFound(); $this->actingAsWithMfa(User::factory()->create())->getJson($url)->assertForbidden(); } public function test_http_decisions_require_scope_fresh_mfa_and_preserve_source_and_history(): void { $this->prepare(); $version = $this->profile()->workingVersion($this->zone->fresh()); $url = '/api/v2/investment/admin/zones/'.$this->zone->id.'/editorial/submit'; $this->withSession(['portal.mfa_at' => time() - 1000])->postJson($url, ['version_no' => $version, 'reason' => 'Synthetic decision reason'])->assertForbidden(); $this->actingAsWithMfa($this->author)->postJson($url, ['version_no' => $version, 'reason' => 'Synthetic decision reason'])->assertOk()->assertJsonPath('editorial_workflow.state', 'submitted'); $this->assertGreaterThan(1, app(PortalEditorialNativeNotices::class)->pending($this->reviewer)['zones'] + 1); $outsider = User::factory()->create(); $this->actingAsWithMfa($outsider)->postJson($url, ['version_no' => $version, 'reason' => 'Synthetic decision reason'])->assertForbidden(); config(['editorial_workflow.zone_review' => false]); $this->actingAsWithMfa($this->author); try { app(InvestmentZones::class)->save($this->zone->id, ['version_no' => $version, 'name' => 'Forbidden legacy overwrite'], $this->author->id); $this->fail('Disabling must preserve workflow'); } catch (HttpException $e) { $this->assertSame(503, $e->getStatusCode()); } $this->assertSame('Original public zone', $this->zone->fresh()->name); } }