116 lines
6.7 KiB
PHP
116 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\InvestmentZone;
|
|
use App\Models\User;
|
|
use App\Services\InvestmentMapIndicators;
|
|
use App\Services\InvestmentZoneIntegralDetails;
|
|
use App\Services\InvestmentZoneOperationalDetails;
|
|
use App\Services\InvestmentZoneOperationalProfile;
|
|
use App\Support\IsolatedQaDatabase;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Tests\TestCase;
|
|
|
|
class InvestmentObservationNatureTest extends TestCase
|
|
{
|
|
use DatabaseTransactions;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
|
|
Http::preventStrayRequests();
|
|
}
|
|
|
|
private function observation(?string $kind): array
|
|
{
|
|
$item = ['id' => (string) Str::uuid(), 'label' => 'SYNTHETIC nature QA', 'description' => 'Synthetic source only.',
|
|
'indicator' => 'employment', 'value' => '0', 'unit' => 'pessoas', 'period_start' => '2025-01-01', 'period_end' => '2025-12-31',
|
|
'scope' => 'Synthetic zone', 'population' => 'Synthetic establishments', 'methodology' => 'Synthetic register',
|
|
'source_reference' => 'SYNTHETIC source; not real statistics', 'source_date' => '2026-01-15', 'knowledge' => 'documented', 'disclosure' => true];
|
|
|
|
return $kind === null ? $item : [...$item, 'measure_kind' => $kind];
|
|
}
|
|
|
|
private function zone(): InvestmentZone
|
|
{
|
|
$actor = User::factory()->create(['email' => 'measure-'.Str::uuid().'@example.test']);
|
|
|
|
return InvestmentZone::create(['code' => 'MEASURE-QA-'.Str::uuid(), 'category' => 'industrial_park', 'name' => 'SYNTHETIC nature QA',
|
|
'editor_id' => $actor->id, 'publication_status' => 'draft', 'validation_status' => 'pending']);
|
|
}
|
|
|
|
public function test_explicit_natures_persist_recover_and_project_without_inference_or_private_disclosure(): void
|
|
{
|
|
$zone = $this->zone();
|
|
$service = app(InvestmentZoneIntegralDetails::class);
|
|
$ops = app(InvestmentZoneOperationalDetails::class);
|
|
$rows = array_map(fn ($kind) => $this->observation($kind), ['observed', 'planned', 'projected', null]);
|
|
$hidden = [...$this->observation('observed'), 'disclosure' => false, 'source_reference' => 'PRIVATE-NATURE-SOURCE'];
|
|
$merged = $service->merge($zone, [], ['observations' => [...$rows, $hidden]]);
|
|
$zone->forceFill(['operational_details' => $ops->pack(['integral' => $merged])])->save();
|
|
$saved = $zone->fresh();
|
|
$before = $saved->getRawOriginal('operational_details');
|
|
$recovered = $ops->recover($saved)['integral']['observations'];
|
|
$this->assertSame(['observed', 'planned', 'projected'], array_column(array_slice($recovered, 0, 3), 'measure_kind'));
|
|
$this->assertArrayNotHasKey('measure_kind', $recovered[3]);
|
|
$this->assertSame('0', $recovered[0]['value']);
|
|
$view = app(InvestmentMapIndicators::class)->forZones(collect([$saved]));
|
|
$this->assertSame(['observed', 'planned', 'projected', 'unknown'], array_column($view['records'], 'measure_kind'));
|
|
$this->assertSame(['0', '0', '0', '0'], array_column($view['records'], 'value'));
|
|
$this->assertStringNotContainsString('PRIVATE-NATURE-SOURCE', json_encode($view, JSON_THROW_ON_ERROR));
|
|
$this->assertSame($before, $zone->fresh()->getRawOriginal('operational_details'));
|
|
$this->assertSame(1, $zone->fresh()->version_no);
|
|
}
|
|
|
|
public function test_invalid_nature_is_rejected_and_an_omitted_nature_does_not_erase_an_explicit_choice(): void
|
|
{
|
|
$zone = $this->zone();
|
|
$service = app(InvestmentZoneIntegralDetails::class);
|
|
$row = $this->observation('observed');
|
|
$before = $service->merge($zone, [], ['observations' => [$row]]);
|
|
$withoutKind = $row;
|
|
unset($withoutKind['measure_kind']);
|
|
$recovered = $service->merge($zone, $before, ['observations' => [$withoutKind]]);
|
|
$this->assertSame('observed', $recovered['observations'][0]['measure_kind']);
|
|
$changed = $service->merge($zone, $before, ['observations' => [[...$row, 'measure_kind' => 'planned']]]);
|
|
$this->assertSame('planned', $changed['observations'][0]['measure_kind']);
|
|
$this->assertSame('observed', $before['observations'][0]['measure_kind']);
|
|
try {
|
|
$service->merge($zone, $before, ['observations' => [[...$row, 'measure_kind' => 'inferred_from_description']]]);
|
|
$this->fail('Invalid nature accepted');
|
|
} catch (ValidationException $error) {
|
|
$this->assertArrayHasKey('observations.0.measure_kind', $error->errors());
|
|
}
|
|
}
|
|
|
|
public function test_nature_change_stays_in_private_preparation_with_history_until_existing_editorial_review(): void
|
|
{
|
|
$zone = $this->zone();
|
|
$ops = app(InvestmentZoneOperationalDetails::class);
|
|
$row = $this->observation('observed');
|
|
$fields = app(InvestmentZoneIntegralDetails::class)->merge($zone, [], ['observations' => [$row]]);
|
|
// Synthetic public snapshot only, inside the rolled-back QA transaction.
|
|
$zone->forceFill(['publication_status' => 'published', 'validation_status' => 'validated',
|
|
'reviewer_id' => User::factory()->create(['email' => 'nature-review-'.Str::uuid().'@example.test'])->id,
|
|
'published_at' => now(), 'physical_category' => 'industrial_park',
|
|
'source_evidence' => [['source_type' => 'promoter', 'title' => 'SYNTHETIC QA only', 'url' => 'https://example.invalid/qa', 'supports' => 'Synthetic test fixture']],
|
|
'operational_details' => $ops->pack(['integral' => $fields])])->save();
|
|
$original = $zone->fresh()->getRawOriginal('operational_details');
|
|
$profile = app(InvestmentZoneOperationalProfile::class);
|
|
$prepared = $profile->prepare($zone->fresh(), ['version_no' => 1,
|
|
'operational_details' => ['integral' => ['observations' => [[...$row, 'measure_kind' => 'planned']]]]], $zone->editor_id);
|
|
$this->assertSame($original, $prepared->getRawOriginal('operational_details'));
|
|
$this->assertSame('observed', $ops->publicData($prepared)['integral']['observations'][0]['measure_kind']);
|
|
$this->assertSame('planned', $profile->draft($prepared)['fields']['operational_details']['integral']['observations'][0]['measure_kind']);
|
|
$this->assertSame(1, $prepared->version_no);
|
|
$this->assertSame(1, DB::table('investment_zone_revisions')->where('zone_id', $zone->id)->where('action', 'profile_prepared')->count());
|
|
$this->assertSame(1, DB::table('audit_events')->where('entity_id', $zone->id)->where('action', 'profile_prepared')->count());
|
|
}
|
|
}
|