Files
Investidor-V2/tests/Feature/InvestmentObservationNatureTest.php
T
2026-11-13 22:44:42 +02:00

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