116 lines
9.4 KiB
PHP
116 lines
9.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Sector;
|
|
use App\Models\User;
|
|
use App\Services\FundingProgrammes;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Tests\TestCase;
|
|
|
|
final class FundingCriteriaCoherenceTest extends TestCase
|
|
{
|
|
use DatabaseTransactions;
|
|
private User $editor;
|
|
private array $geo = [];
|
|
private int $sector;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp(); $this->withoutVite();
|
|
$db = DB::selectOne('select current_database() as name, inet_server_port() as port');
|
|
$this->assertSame('investor_intent_20261115_test', $db->name); $this->assertSame(55441, (int) $db->port);
|
|
$this->editor = User::factory()->create(); config(['portal.managers' => [$this->editor->email]]);
|
|
$parent = null;
|
|
foreach (['country', 'province', 'district'] as $type) {
|
|
$id = (string) Str::uuid();
|
|
DB::table('geo_locations')->insert(['id' => $id, 'parent_id' => $parent, 'name' => 'SYNTHETIC '.$type, 'code' => $id, 'location_type' => $type, 'source_system' => 'cartografia-workbook', 'data_classification' => 'public', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$id]);
|
|
$this->geo[$type] = $id; $parent = $id;
|
|
}
|
|
$this->sector = Sector::create(['slug' => 'synthetic-funding-qa', 'name' => 'SYNTHETIC sector', 'is_active' => true, 'published_at' => now()])->id;
|
|
}
|
|
|
|
private function publish(string $code, string $kind = 'institution', array $overrides = []): string
|
|
{
|
|
$source = ['source_url' => 'https://example.invalid/synthetic-qa', 'source_reference' => 'SYNTHETIC QA ONLY', 'checked_on' => today()->toDateString()];
|
|
$draft = ['title' => 'SYNTHETIC '.$code, 'kind' => $kind, 'manager_name' => 'SYNTHETIC manager', 'summary' => 'Synthetic isolated record, not real content.', 'source_url' => $source['source_url'], 'source_reference' => $source['source_reference'], 'source_checked_on' => $source['checked_on'], 'source_status' => 'identified', 'coverage' => 'national', 'sectors_scope' => 'all', 'funding_needs' => ['investment'], 'windows' => []];
|
|
if ($kind === 'institution') $draft += ['institution_key' => $code, 'institution_nature' => 'bank', 'institution_categories' => ['banking'], 'institution_types' => ['commercial_bank'], 'institution_status' => 'reported',
|
|
'locations' => [['role' => 'headquarters', 'label' => 'Synthetic headquarters', 'territory_id' => $this->geo['district'], 'evidence_status' => 'reported', ...$source]],
|
|
'products' => [['code' => 'investment', 'title' => 'Synthetic investment product', 'needs' => ['investment'], 'coverage' => 'national', 'availability' => 'documented', ...$source]]];
|
|
$service = app(FundingProgrammes::class);
|
|
$id = $service->save($this->editor, null, ['version' => 0, 'code' => $code, 'reason' => 'Synthetic QA fixture', 'draft' => array_replace($draft, $overrides)]);
|
|
$plan = $service->preview($this->editor, $id);
|
|
$service->publish($this->editor, $id, ['version' => $plan['version'], 'hash' => $plan['hash'], 'confirm' => true, 'reason' => 'Synthetic QA only, rolled back']);
|
|
return $id;
|
|
}
|
|
|
|
public function test_bank_filters_apply_to_the_shared_query_and_never_return_brokers_or_microinsurers(): void
|
|
{
|
|
$bank = $this->publish('qa-bank');
|
|
$this->publish('qa-broker', overrides: ['institution_categories' => ['intermediation'], 'institution_types' => ['broker']]);
|
|
$this->publish('qa-microinsurance', overrides: ['institution_categories' => ['insurance'], 'institution_types' => ['microinsurer']]);
|
|
$fund = $this->publish('qa-fund', 'fund');
|
|
$service = app(FundingProgrammes::class);
|
|
$filters = ['category' => 'banking', 'institution_type' => 'commercial_bank', 'kind' => 'institution', 'certainty' => 'documented', 'location_mode' => 'physical', 'territory' => $this->geo['district'], 'sector' => $this->sector, 'need' => 'investment'];
|
|
$this->assertSame([$bank], $service->catalogueQuery($filters)->pluck('p.id')->all());
|
|
$this->assertSame([$fund], $service->catalogueQuery(['kind' => 'fund'])->pluck('p.id')->all());
|
|
$this->assertSame(0, $service->catalogueQuery(['q' => 'no-such-record'])->count());
|
|
$this->assertSame(0, $service->catalogueQuery(['category' => 'banking', 'institution_type' => 'investment_bank'])->count(), 'A valid combination without records is an empty result, not an invalid combination.');
|
|
$this->get('/fundos?'.http_build_query($filters))->assertOk()->assertInertia(fn ($page) => $page->has('items.data', 1)->where('items.data.0.id', $bank)->where('territorySelection.0.id', $this->geo['province'])->where('territorySelection.1.id', $this->geo['district']));
|
|
$returnFilters = [...$filters, 'q' => 'qa', 'state' => 'closed', 'kind' => 'institution', 'include_historical' => '0', 'province_id' => $this->geo['province'], 'district_id' => $this->geo['district'], 'page' => '2'];
|
|
$this->get('/fundos/'.$bank.'?'.http_build_query($returnFilters))->assertOk()->assertInertia(function ($page) use ($returnFilters) {
|
|
foreach ($returnFilters as $key => $value) $page->where('selection.'.$key, (string) $value);
|
|
});
|
|
$this->getJson('/api/v1/investment/territory-options?type=district')->assertOk()->assertJsonCount(0, 'data');
|
|
$this->getJson('/api/v1/investment/territory-options?type=district&parent_id='.$this->geo['province'])->assertOk()->assertJsonCount(1, 'data');
|
|
}
|
|
|
|
public function test_physical_search_defaults_to_documented_presence_but_preserves_explicit_unknown_choice(): void
|
|
{
|
|
$local = $this->publish('qa-local');
|
|
$this->publish('qa-unlocated', overrides: ['locations' => []]);
|
|
$filters = ['q' => 'SYNTHETIC qa-', 'location_mode' => 'physical', 'territory' => $this->geo['province']];
|
|
$this->get('/fundos?'.http_build_query($filters))->assertOk()->assertInertia(fn ($page) => $page
|
|
->where('filters.certainty', 'documented')->has('items.data', 1)->where('items.data.0.id', $local));
|
|
$this->get('/fundos?'.http_build_query([...$filters, 'certainty' => 'include_unknown']))->assertOk()->assertInertia(fn ($page) => $page
|
|
->where('filters.certainty', 'include_unknown')->has('items.data', 2));
|
|
}
|
|
|
|
public function test_contradictory_type_or_record_kind_and_incomplete_hierarchy_are_rejected(): void
|
|
{
|
|
foreach ([['category' => 'banking', 'institution_type' => 'broker'], ['category' => 'banking', 'institution_type' => 'commercial_bank', 'kind' => 'fund', 'location_mode' => 'physical'], ['district_id' => $this->geo['district']]] as $filters) {
|
|
try { app(FundingProgrammes::class)->catalogueQuery($filters)->count(); $this->fail('Contradictory criteria must not silently broaden a query.'); }
|
|
catch (ValidationException $error) { $this->assertNotEmpty($error->errors()); }
|
|
$this->getJson('/fundos?'.http_build_query($filters))->assertUnprocessable();
|
|
$this->get('/fundos?'.http_build_query($filters))->assertOk()->assertInertia(fn ($page) => $page->has('criteriaErrors')->has('items.data', 0));
|
|
}
|
|
}
|
|
|
|
public function test_unknown_scope_is_not_a_documented_match_and_physical_presence_does_not_prove_coverage(): void
|
|
{
|
|
$this->publish('qa-unknown', overrides: ['sectors_scope' => 'unknown', 'products' => []]);
|
|
$service = app(FundingProgrammes::class);
|
|
$this->assertSame(0, $service->catalogueQuery(['need' => 'investment', 'certainty' => 'documented'])->count());
|
|
$this->assertSame(0, $service->catalogueQuery(['sector' => $this->sector, 'certainty' => 'documented'])->count());
|
|
$this->assertSame(1, $service->catalogueQuery(['territory' => $this->geo['district'], 'location_mode' => 'physical', 'certainty' => 'documented'])->count());
|
|
$this->assertSame(0, $service->catalogueQuery(['territory' => $this->geo['district'], 'location_mode' => 'coverage', 'certainty' => 'documented'])->count());
|
|
$this->assertSame(1, $service->catalogueQuery(['need' => 'investment', 'certainty' => 'include_unknown'])->count());
|
|
$this->assertSame(0, $service->catalogueQuery(['state' => 'closed'])->count(), 'An institution without a window is not a closed funding window.');
|
|
}
|
|
|
|
public function test_window_state_and_scope_are_matched_on_the_same_window_without_inventing_a_local_window(): void
|
|
{
|
|
$source = ['source_url' => 'https://example.invalid/synthetic-qa', 'source_reference' => 'Synthetic window', 'checked_on' => today()->toDateString()];
|
|
$id = $this->publish('qa-closed-window', overrides: ['windows' => [['code' => 'closed', 'title' => 'Synthetic closed window', 'mode' => 'closed', 'coverage' => 'unknown', ...$source]]]);
|
|
$service = app(FundingProgrammes::class);
|
|
$criteria = ['territory' => $this->geo['district'], 'certainty' => 'documented', 'state' => 'closed', 'location_mode' => 'coverage'];
|
|
$this->assertSame(0, $service->catalogueQuery($criteria)->count());
|
|
$this->assertSame([$id], $service->catalogueQuery([...$criteria, 'certainty' => 'include_unknown'])->pluck('p.id')->all());
|
|
$this->assertSame([$id], $service->catalogueQuery([...$criteria, 'location_mode' => 'physical'])->pluck('p.id')->all());
|
|
$this->assertSame(0, $service->catalogueQuery([...$criteria, 'state' => 'open'])->count());
|
|
}
|
|
}
|