133 lines
8.1 KiB
PHP
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];
|
|
}
|
|
}
|