feat: show immediate administrative subdivisions and map coverage gaps
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Services\TerritoryGeometry;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TerritoryGeometrySubdivisionTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$target = DB::selectOne('SELECT current_database() AS name, inet_server_port() AS port');
|
||||
$this->assertStringEndsWith('_test', $target->name);
|
||||
$this->assertSame(55441, (int) $target->port);
|
||||
$this->assertSame('127.0.0.1', config('database.connections.pgsql.host'));
|
||||
$this->withoutMiddleware(ThrottleRequests::class);
|
||||
}
|
||||
|
||||
private function location(string $type, ?string $parent = null, string $classification = 'public'): string
|
||||
{
|
||||
if ($type === 'province' && $parent === null) {
|
||||
$parent = $this->location('country');
|
||||
}
|
||||
$id = (string) Str::uuid();
|
||||
DB::table('geo_locations')->insert(['id' => $id, 'name' => 'Synthetic '.$type.' '.$id, 'code' => $id,
|
||||
'location_type' => $type, 'parent_id' => $parent, 'data_classification' => $classification,
|
||||
'source_system' => 'cartografia-workbook',
|
||||
'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id]);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function geometry(string $territory, string $classification = 'public'): string
|
||||
{
|
||||
$id = (string) Str::uuid();
|
||||
DB::insert("INSERT INTO geo_geometries(id,geo_location_id,geometry_type,geom,properties,data_classification) VALUES (?,?,'polygon',ST_GeomFromText('POLYGON((32 -20,33 -20,33 -19,32 -19,32 -20))',4326),?::jsonb,?)",
|
||||
[$id, $territory, json_encode(['purpose' => 'administrative_boundary', 'authority' => 'Synthetic QA only', 'source_version' => '2026-11-04']), $classification]);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function test_optional_children_preserve_default_and_sources_without_grandchildren_or_other_provinces(): void
|
||||
{
|
||||
$province = $this->location('province');
|
||||
$district = $this->location('district', $province);
|
||||
$post = $this->location('admin_post', $district);
|
||||
$this->geometry($province);
|
||||
$this->geometry($district);
|
||||
$this->geometry($post);
|
||||
$this->geometry($this->location('district', $this->location('province')));
|
||||
$url = '/api/v1/investment/territory-geometries?territory_id='.$province;
|
||||
$this->getJson($url)->assertOk()->assertJsonCount(1, 'areas')->assertJsonPath('child_coverage', null);
|
||||
$result = $this->getJson($url.'&include_children=1')->assertOk()->assertJsonCount(2, 'areas')
|
||||
->assertJsonPath('areas.0.territory_id', $province)->assertJsonPath('areas.0.selected_territory', true)
|
||||
->assertJsonPath('areas.1.territory_id', $district)->assertJsonPath('areas.1.child_territory', true)
|
||||
->assertJsonPath('areas.1.source_reference', 'Synthetic QA only')->assertJsonPath('areas.1.source_date', '2026-11-04')
|
||||
->assertJsonPath('child_coverage.registered_count', 1)->assertJsonPath('child_coverage.represented_count', 1)
|
||||
->assertJsonPath('investment_site', false)->assertJsonPath('truncated', false)->json();
|
||||
$this->assertFalse($result['areas'][1]['selected_territory']);
|
||||
$this->getJson('/api/v1/investment/territory-geometries?territory_id='.$district.'&include_children=1')
|
||||
->assertOk()->assertJsonCount(3, 'areas')->assertJsonPath('child_coverage.location_type', 'admin_post');
|
||||
}
|
||||
|
||||
public function test_coverage_counts_only_accessible_references_and_never_publishes_private_boundaries(): void
|
||||
{
|
||||
$province = $this->location('province');
|
||||
$public = $this->location('district', $province);
|
||||
$missing = $this->location('district', $province);
|
||||
$hidden = $this->location('district', $province, 'internal');
|
||||
$privateGeometry = $this->geometry($public, 'internal');
|
||||
$this->geometry($hidden);
|
||||
$before = DB::table('geo_geometries')->where('id', $privateGeometry)->value('data_classification');
|
||||
$this->getJson('/api/v1/investment/territory-geometries?territory_id='.$province.'&include_children=1')
|
||||
->assertOk()->assertJsonCount(0, 'areas')->assertJsonPath('selected_geometry_available', false)
|
||||
->assertJsonPath('child_coverage.registered_count', 2)->assertJsonPath('child_coverage.represented_count', 0);
|
||||
$internal = app(TerritoryGeometry::class)->context($province, true, true);
|
||||
$this->assertSame(3, $internal['child_coverage']['registered_count']);
|
||||
$this->assertSame(2, $internal['child_coverage']['represented_count']);
|
||||
$this->assertSame($before, DB::table('geo_geometries')->where('id', $privateGeometry)->value('data_classification'));
|
||||
DB::table('geo_locations')->where('id', $missing)->update(['deleted_at' => now()]);
|
||||
$this->getJson('/api/v1/investment/territory-geometries?territory_id='.$province.'&include_children=1')
|
||||
->assertOk()->assertJsonPath('child_coverage.registered_count', 1);
|
||||
}
|
||||
|
||||
public function test_limits_are_explicit_and_selected_boundary_is_retained_ahead_of_children(): void
|
||||
{
|
||||
$province = $this->location('province');
|
||||
$selected = $this->geometry($province);
|
||||
for ($i = 0; $i < 101; $i++) {
|
||||
$this->geometry($this->location('district', $province));
|
||||
}
|
||||
$this->getJson('/api/v1/investment/territory-geometries?territory_id='.$province.'&include_children=1')
|
||||
->assertOk()->assertJsonCount(100, 'areas')->assertJsonPath('areas.0.id', $selected)
|
||||
->assertJsonPath('selected_geometry_available', true)->assertJsonPath('truncated', true)
|
||||
->assertJsonPath('child_coverage.registered_count', 101)->assertJsonPath('child_coverage.represented_count', 99);
|
||||
}
|
||||
|
||||
public function test_reference_limit_reports_unrepresented_subdivisions_without_claiming_they_do_not_exist(): void
|
||||
{
|
||||
$province = $this->location('province');
|
||||
for ($i = 0; $i < 501; $i++) {
|
||||
$this->location('district', $province);
|
||||
}
|
||||
$this->getJson('/api/v1/investment/territory-geometries?territory_id='.$province.'&include_children=1')
|
||||
->assertOk()->assertJsonCount(0, 'areas')->assertJsonPath('truncated', true)
|
||||
->assertJsonPath('child_coverage.registered_count', 501)->assertJsonPath('child_coverage.represented_count', 0);
|
||||
}
|
||||
|
||||
public function test_invalid_child_option_is_rejected_and_locality_has_no_lower_level(): void
|
||||
{
|
||||
$province = $this->location('province');
|
||||
$district = $this->location('district', $province);
|
||||
$post = $this->location('admin_post', $district);
|
||||
$locality = $this->location('locality', $post);
|
||||
$url = '/api/v1/investment/territory-geometries?territory_id='.$locality;
|
||||
$this->getJson($url.'&include_children=all')->assertUnprocessable();
|
||||
$this->getJson($url.'&include_children=1')->assertOk()->assertJsonPath('child_coverage.location_type', null)
|
||||
->assertJsonPath('child_coverage.registered_count', 0)->assertJsonPath('truncated', false);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,9 @@ await test('catalogue composes one map with the same opportunity data and separa
|
||||
'./interactive-investment-map': { default: Map },
|
||||
'./territory-geometry-preview': { TerritoryGeometryDetails: Info },
|
||||
'./use-territory-geometry': {
|
||||
useTerritoryGeometry: (id) => {
|
||||
useTerritoryGeometry: (id, endpoint, children) => {
|
||||
assert.equal(endpoint, undefined);
|
||||
assert.equal(children, true);
|
||||
requests.push(id);
|
||||
return { context: { areas: [boundary] }, error: '' };
|
||||
},
|
||||
@@ -125,9 +127,9 @@ await test('territorial request changes discard old boundaries immediately and i
|
||||
const { useTerritoryGeometry } = load('use-territory-geometry.ts', {
|
||||
react: hooks,
|
||||
});
|
||||
const render = (id) => {
|
||||
const render = (id, children = false) => {
|
||||
cursor = 0;
|
||||
return useTerritoryGeometry(id);
|
||||
return useTerritoryGeometry(id, undefined, children);
|
||||
};
|
||||
render('a');
|
||||
const cleanup = effect();
|
||||
@@ -150,6 +152,22 @@ await test('territorial request changes discard old boundaries immediately and i
|
||||
assert.equal(render(undefined).context, null);
|
||||
assert.equal(pending[0].options.signal.aborted, true);
|
||||
assert.equal(pending.length, 2);
|
||||
assert.equal(render('b', true).context, null);
|
||||
effect();
|
||||
assert.match(pending[2].url, /include_children=1/);
|
||||
pending[2].resolve({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
areas: [boundary],
|
||||
child_coverage: { registered_count: 2 },
|
||||
}),
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(
|
||||
render('b', true).context.child_coverage.registered_count,
|
||||
2,
|
||||
);
|
||||
assert.equal(render('b', false).context, null);
|
||||
} finally {
|
||||
globalThis.fetch = previousFetch;
|
||||
}
|
||||
@@ -452,3 +470,37 @@ await test('partnership catalogue has one contextual map and keeps the applied t
|
||||
assert.equal(maps()[0].props.territoryId, 'a');
|
||||
assert.equal(maps()[0].props.points, props.mapPoints);
|
||||
});
|
||||
|
||||
await test('administrative coverage identifies lower-level subdivisions and distinguishes absent geometry from absent territory', () => {
|
||||
const Details = load('territory-geometry-preview.tsx', {
|
||||
'./interactive-investment-map': { default: () => null },
|
||||
'./use-territory-geometry': {},
|
||||
}).TerritoryGeometryDetails;
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(Details, {
|
||||
error: '',
|
||||
context: {
|
||||
selected_geometry_available: false,
|
||||
context_geometry_count: 1,
|
||||
truncated: true,
|
||||
child_coverage: {
|
||||
location_type: 'district',
|
||||
registered_count: 3,
|
||||
represented_count: 1,
|
||||
},
|
||||
areas: [
|
||||
{
|
||||
...boundary,
|
||||
selected_territory: false,
|
||||
child_territory: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.match(html, /1 de 3 referências acessíveis/);
|
||||
assert.match(html, /Subdivisão do território seleccionado/);
|
||||
assert.match(html, /não significa inexistência do território/);
|
||||
assert.match(html, /100 geometrias e 500 subdivisões/);
|
||||
assert.doesNotMatch(html, /apenas limites dos níveis superiores/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user