137 lines
8.5 KiB
PHP
137 lines
8.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
use App\Services\OpportunityManagement;
|
|
use App\Services\ProactiveSupply;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Tests\Support\ProactiveInvestmentFixture as F;
|
|
use Tests\TestCase;
|
|
|
|
final class ProactiveSupplyTest extends TestCase
|
|
{
|
|
use DatabaseTransactions;
|
|
|
|
private User $actor;
|
|
|
|
private string $territory;
|
|
|
|
private string $observation;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
F::guard();
|
|
$this->withoutVite();
|
|
config(['portal.profile_access_enabled' => true, 'portal.opportunity_specialized_drafts_enabled' => true, 'portal.managers' => [], 'investor-path.enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
|
|
$this->actor = F::operator();
|
|
$this->login($this->actor);
|
|
$input = F::input('Gurué');
|
|
$this->territory = $input['district_id'];
|
|
$hash = hash('sha256', 'SYNTHETIC-QA-SUPPLY-SOURCE');
|
|
DB::table('proactive_supply_sources')->insert(['sha256' => $hash, 'name' => 'SYNTHETIC-QA-SUPPLY.xlsx', 'period' => '2020', 'limitations' => 'Synthetic, not a real market source', 'receipt' => '{}', 'imported_at' => now()]);
|
|
$this->observation = hash('sha256', 'SYNTHETIC-QA-OBSERVATION');
|
|
$row = ['id' => $this->observation, 'source_hash' => $hash, 'sheet' => 'QA', 'row_number' => 2, 'entity_key' => hash('sha256', 'QA-ENTITY'), 'name' => 'PRIVATE-UNIQUE-QA-SUPPLIER', 'province' => 'Zambézia', 'district' => 'Gurué', 'cae' => '33120', 'activity' => 'SYNTHETIC QA repair', 'sector' => 'SYNTHETIC QA manufacturing', 'source_date' => '2020', 'fingerprint' => hash('sha256', 'QA-FINGERPRINT'), 'duplicate_of' => null, 'identity_conflict' => false, 'territory_id' => $this->territory, 'territory_resolution' => 'exact_normalized_province_district'];
|
|
DB::table('proactive_supply_observations')->insert($row);
|
|
DB::table('proactive_supply_observations')->insert([...$row, 'id' => hash('sha256', 'QA-REPEATED-OBSERVATION'), 'row_number' => 3, 'duplicate_of' => $this->observation]);
|
|
}
|
|
|
|
private function login(User $u): void
|
|
{
|
|
$this->actingAs($u)->withSession(['portal.mfa_actor' => $u->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $u->portal_access_revision]);
|
|
}
|
|
|
|
private function url(string $extra = ''): string
|
|
{
|
|
return '/minhas-oportunidades/oferta-interna?territory_id='.$this->territory.$extra;
|
|
}
|
|
|
|
public function test_private_query_filters_supply_and_preserves_source_and_duplicate_count(): void
|
|
{
|
|
$r = $this->getJson($this->url('&activity=SYNTHETIC%20QA%20repair'))->assertOk()->assertJsonPath('total', 1)->assertJsonPath('duplicate_sightings', 1)->assertJsonPath('rows.0.source_name', 'SYNTHETIC-QA-SUPPLY.xlsx')->assertJsonPath('rows.0.row_number', 2)->assertJsonPath('rows.0.source_date', '2020');
|
|
$this->assertStringContainsString('no-store', $r->headers->get('Cache-Control'));
|
|
$this->assertArrayNotHasKey('contact', $r->json('rows.0'));
|
|
$this->assertArrayNotHasKey('nuit', $r->json('rows.0'));
|
|
}
|
|
|
|
public function test_no_match_means_insufficient_information_not_absent_market(): void
|
|
{
|
|
$this->getJson($this->url('&activity=NO-SUCH-SYNTHETIC-ACTIVITY'))->assertOk()->assertJsonPath('total', 0)->assertJsonPath('status', 'Informação insuficiente nesta base e nestes filtros.');
|
|
}
|
|
|
|
public function test_unassigned_user_cannot_read_internal_register(): void
|
|
{
|
|
$this->login(User::factory()->create());
|
|
$this->getJson($this->url())->assertForbidden();
|
|
$this->getJson('/minhas-oportunidades/oferta-interna?territory_id=not-a-uuid')->assertForbidden();
|
|
}
|
|
|
|
public function test_comparison_persists_source_lineage_and_rejects_forgery_and_public_reclassification(): void
|
|
{
|
|
$input = F::input('Gurué');
|
|
$a = $input['proactive'];
|
|
$id = (string) Str::uuid();
|
|
$a['evidence'][] = ['id' => $id, 'interpretation' => 'SYNTHETIC QA possible complement, not a contract', 'verified_on' => '2026-10-28', ...app(ProactiveSupply::class)->evidence($this->actor, $this->observation, null)];
|
|
$a['supply_comparisons'] = [['observation_id' => $this->observation, 'relationship' => 'complementarity', 'rationale' => 'SYNTHETIC QA compare servicing with tea-chain idea', 'limitations' => 'No current capacity or demand proved']];
|
|
$input['proactive'] = $a;
|
|
$oid = $this->postJson('/minhas-oportunidades/drafts', $input)->assertCreated()->json('id');
|
|
$o = Opportunity::findOrFail($oid);
|
|
$saved = app(OpportunityManagement::class)->values($o)['proactive'];
|
|
$this->assertSame($this->observation, $saved['supply_comparisons'][0]['observation_id']);
|
|
$this->assertSame('internal', $saved['evidence'][2]['access']);
|
|
$this->assertStringContainsString('SYNTHETIC-QA-SUPPLY.xlsx', $saved['evidence'][2]['source']);
|
|
$a['evidence'][2]['source'] = 'Forged source';
|
|
$a['evidence'][2]['statement'] = 'Forged supply fact';
|
|
$this->putJson('/minhas-oportunidades/'.$oid.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA canonical source lineage is server-resolved'])->assertOk();
|
|
$saved = app(OpportunityManagement::class)->values($o->fresh())['proactive'];
|
|
$this->assertStringContainsString('PRIVATE-UNIQUE-QA-SUPPLIER', $saved['evidence'][2]['statement']);
|
|
$a['evidence'][2]['access'] = 'public';
|
|
$this->putJson('/minhas-oportunidades/'.$oid.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA internal list cannot become public'])->assertUnprocessable();
|
|
$a['evidence'][2]['access'] = 'internal';
|
|
$a['supply_comparisons'][0]['observation_id'] = str_repeat('f', 64);
|
|
$this->putJson('/minhas-oportunidades/'.$oid.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA unknown supply observation rejected'])->assertUnprocessable();
|
|
}
|
|
|
|
public function test_sector_filter_does_not_infer_absence_and_comparison_requires_its_evidence(): void
|
|
{
|
|
$this->getJson($this->url('§or=NO-SUCH-SECTOR'))->assertOk()->assertJsonPath('total', 0);
|
|
$input = F::input('Gurué');
|
|
$input['proactive']['supply_comparisons'] = [['observation_id' => $this->observation, 'relationship' => 'partner', 'rationale' => 'SYNTHETIC QA prospective partner', 'limitations' => 'Not contacted, no agreement']];
|
|
$this->postJson('/minhas-oportunidades/drafts', $input)->assertUnprocessable();
|
|
}
|
|
|
|
public function test_database_policy_hides_internal_rows_without_actor_context(): void
|
|
{
|
|
$role = 'supply_qa_'.bin2hex(random_bytes(6));
|
|
DB::statement('CREATE ROLE '.$role.' NOLOGIN');
|
|
DB::statement('GRANT USAGE ON SCHEMA public TO '.$role);
|
|
DB::statement('GRANT SELECT ON proactive_supply_sources,proactive_supply_observations TO '.$role);
|
|
DB::select("select set_config('app.actor_user_id','',true),set_config('app.proactive_supply_read','',true)");
|
|
try {
|
|
DB::statement('SET LOCAL ROLE '.$role);
|
|
$this->assertSame(0, DB::table('proactive_supply_observations')->count());
|
|
$this->assertSame(0, DB::table('proactive_supply_sources')->count());
|
|
DB::select("select set_config('app.actor_user_id',?,true),set_config('app.proactive_supply_read','internal',true)", [(string) $this->actor->id]);
|
|
$this->assertNotNull(DB::table('proactive_supply_observations')->where('id', $this->observation)->first());
|
|
} finally {
|
|
DB::statement('RESET ROLE');
|
|
}
|
|
}
|
|
|
|
public function test_preserved_repetitions_cannot_inflate_saved_comparisons(): void
|
|
{
|
|
$this->expectException(ValidationException::class);
|
|
$this->expectExceptionMessage('Esta observação administrativa é repetida.');
|
|
$comparison = ['relationship' => 'competition', 'rationale' => 'SYNTHETIC QA duplicate must not count as another competitor', 'limitations' => 'Historical observation,not a unique company'];
|
|
app(ProactiveSupply::class)->comparisons($this->actor, [
|
|
[...$comparison, 'observation_id' => $this->observation],
|
|
[...$comparison, 'observation_id' => hash('sha256', 'QA-REPEATED-OBSERVATION')],
|
|
], null);
|
|
}
|
|
}
|