$row */ public function rowId(array $row): string { return hash('sha256', json_encode($row, JSON_THROW_ON_ERROR)); } /** @param array $row * @return array */ 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]; } }