164 lines
10 KiB
PHP
164 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\InvestmentZone;
|
|
use App\Models\User;
|
|
use App\Services\InvestmentMapIndicators;
|
|
use App\Services\InvestmentZoneOperationalDetails;
|
|
use App\Services\InvestmentZoneOperationalProfile;
|
|
use App\Services\InvestmentZones;
|
|
use App\Services\PortalEditorialNativeNotices;
|
|
use App\Services\PortalEditorialWorkflow;
|
|
use App\Support\IsolatedQaDatabase;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
use Tests\Support\AuthenticatesQaActors;
|
|
use Tests\TestCase;
|
|
|
|
final class ZoneStatisticsEditorialTest extends TestCase
|
|
{
|
|
use AuthenticatesQaActors,DatabaseTransactions;
|
|
|
|
private User $author;
|
|
|
|
private User $reviewer;
|
|
|
|
private InvestmentZone $zone;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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);
|
|
}
|
|
}
|