Files
Investidor-V2/app/Services/InvestmentTerritorialValidation.php
T
nala bdd766c68a
tests / ci (push) Canceled after 0s
resolvemos o problema dos ZEE e ZFI
2026-10-07 19:46:35 +02:00

133 lines
8.1 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
/** Validate only the territory explicitly proposed in the documentary plan. */
class InvestmentTerritorialValidation
{
/** @param array<string,mixed> $row */
public function rowId(array $row): string
{
return hash('sha256', json_encode($row, JSON_THROW_ON_ERROR));
}
/** @param array<string,mixed> $row
* @return array<string,mixed>
*/
public function validate(array $row): array
{
$issues = $warnings = $path = $visited = [];
$pending = false;
$project = DB::table('projects')->where('id', $row['project_id'])->whereNull('deleted_at')->first();
$dossier = DB::table('investment_dossiers')->where('id', $row['dossier_id'])->whereNull('deleted_at')->first();
if (! $project || ! $dossier || $dossier->project_id !== $row['project_id']) {
$issues[] = 'Projeto ou documento de origem indisponível ou de outra identidade.';
} elseif (($row['source_reference'] ?? '') === '' || ($row['source_quote'] ?? '') === '') {
$pending = true;
$warnings[] = 'Falta referência ou citação documental da localização.';
} elseif ($dossier->source !== $row['source_reference'] || (json_decode($dossier->content, true)['location'] ?? null) !== $row['source_quote']
|| (int) $dossier->version_no !== (int) $row['dossier_version']) {
$issues[] = 'Fonte, citação ou versão do dossier diverge da proposta.';
}
if (($row['data_classification'] ?? null) === 'public' && $dossier && $dossier->publication_status !== 'published') {
$issues[] = 'A autorização de divulgação do dossier já não corresponde à proposta pública.';
}
if (! in_array($row['role'] ?? null, ['primary', 'secondary', 'impact'], true)) {
$issues[] = 'Papel territorial inválido.';
}
$id = $row['geo_location_id'];
while ($id) {
if (isset($visited[$id])) {
$issues[] = 'Ciclo na hierarquia territorial.';
break;
}
$visited[$id] = true;
$node = DB::table('geo_locations')->where('id', $id)->first();
if (! $node || $node->deleted_at || ! in_array($node->data_classification, ['public', 'internal'], true)) {
$issues[] = 'Território ou antepassado indisponível para esta associação.';
break;
}
array_unshift($path, ['id' => $node->id, 'name' => $node->name, 'type' => $node->location_type]);
$id = $node->parent_id;
}
$levels = ['country' => 0, 'province' => 1, 'district' => 2, 'admin_post' => 3, 'locality' => 4, 'site' => 5];
$previous = null;
foreach ($path as $node) {
$level = $levels[$node['type']] ?? null;
if ($level === null || ($previous !== null && $level !== $previous + 1 && $node['type'] !== 'site')) {
$issues[] = 'Hierarquia incompatível: província → distrito → posto administrativo → localidade.';
}
$previous = $level;
}
if (! collect($path)->contains('type', 'province')) {
$pending = true;
$warnings[] = 'A hierarquia não identifica uma província.';
}
foreach (InvestmentGeography::FIELDS as $field => $type) {
if (isset($row[$field]) && ! collect($path)->contains(fn ($node) => $node['type'] === $type && $node['id'] === $row[$field])) {
$issues[] = 'O nível indicado em '.$field.' não pertence à hierarquia proposta.';
}
}
$points = DB::table('geo_geometries')->where('geo_location_id', $row['geo_location_id'])->whereNull('deleted_at')
->whereIn('data_classification', ['public', 'internal'])->where('geometry_type', 'point')
->get(['id', 'source_system', DB::raw('ST_AsGeoJSON(geom) as geometry'), DB::raw('ST_SRID(geom) as srid')]);
$coordinates = [];
foreach ($points as $point) {
$geometry = json_decode($point->geometry, true);
$coordinates[] = ['geometry_id' => $point->id, 'source' => $point->source_system, 'coordinates' => $geometry['coordinates'] ?? [], 'srid' => $point->srid];
}
if (isset($row['coordinates'])) {
$coordinates[] = ['geometry_id' => null, 'source' => $row['source_reference'], 'coordinates' => $row['coordinates'], 'srid' => 4326];
}
$checks = [];
foreach ($coordinates as $point) {
$xy = $point['coordinates'];
if (count($xy) < 2 || ! is_numeric($xy[0]) || ! is_numeric($xy[1]) || ! is_finite((float) $xy[0]) || ! is_finite((float) $xy[1])
|| abs((float) $xy[0]) > 180 || abs((float) $xy[1]) > 90 || (int) $point['srid'] !== 4326) {
$issues[] = 'Coordenadas inválidas ou sistema de referência incompatível com WGS84.';
continue;
}
$boundaries = DB::table('geo_geometries')->whereIn('geo_location_id', array_column($path, 'id'))->whereNull('deleted_at')
->whereIn('data_classification', ['public', 'internal'])->whereIn('geometry_type', ['polygon', 'multipolygon'])
->whereRaw('ST_IsValid(geom) AND NOT ST_IsEmpty(geom) AND ST_SRID(geom)=4326')
->select(['id', 'geo_location_id', 'source_system'])
->selectRaw('ST_Covers(geom, ST_SetSRID(ST_MakePoint(?, ?),4326)) as compatible', [(float) $xy[0], (float) $xy[1]])->get();
if ($boundaries->isEmpty()) {
$warnings[] = 'Coordenadas com formato válido; sem limites cartográficos documentados para confirmar inclusão territorial.';
}
foreach ($boundaries as $boundary) {
$checks[] = ['geometry_id' => $point['geometry_id'], 'territory_id' => $boundary->geo_location_id, 'source' => $boundary->source_system, 'compatible' => (bool) $boundary->compatible];
if (! $boundary->compatible) {
$issues[] = 'Coordenadas fora do território '.(collect($path)->firstWhere('id', $boundary->geo_location_id)['name'] ?? $boundary->geo_location_id).'.';
}
}
}
if (! $coordinates) {
$warnings[] = 'Sem coordenadas; a ausência não impede validar a localização documental.';
}
$current = [];
if ($project) {
$links = DB::table('project_locations')->where('project_id', $project->id)->whereNull('deleted_at')->get();
foreach ($links as $link) {
$location = DB::table('geo_locations')->where('id', $link->geo_location_id)->first();
$current[] = ['id' => $link->geo_location_id, 'name' => $location->name ?? 'Território indisponível', 'role' => $link->role,
'path' => array_map(fn ($node) => ['id' => $node->id, 'name' => $node->name, 'type' => $node->location_type], app(InvestmentGeography::class)->path($link->geo_location_id, true))];
}
if ($project->primary_territory_id && ! collect($current)->contains(fn ($item) => $item['id'] === $project->primary_territory_id && $item['role'] === 'primary')) {
$location = DB::table('geo_locations')->where('id', $project->primary_territory_id)->first();
$current[] = ['id' => $project->primary_territory_id, 'name' => $location->name ?? 'Território indisponível', 'role' => 'primary',
'path' => array_map(fn ($node) => ['id' => $node->id, 'name' => $node->name, 'type' => $node->location_type], app(InvestmentGeography::class)->path($project->primary_territory_id, true))];
}
}
return ['row_id' => $this->rowId($row), 'status' => $issues ? 'contradictory' : ($pending ? 'pending' : 'grounded'),
'issues' => array_values(array_unique($issues)), 'warnings' => array_values(array_unique($warnings)), 'current' => $current,
'proposed' => ['id' => $row['geo_location_id'], 'name' => collect($path)->last()['name'] ?? ($row['name'] ?? 'Território indisponível'), 'role' => $row['role'], 'path' => $path],
'coordinates' => $coordinates, 'coordinate_checks' => $checks];
}
}