Compare commits

..
11 changed files with 4214 additions and 82 deletions
@@ -19,12 +19,21 @@ class InvestmentTerritoryController extends Controller
$data = $request->validate(['territory_id' => ['required', 'uuid']]);
$profile = $request->is('minhas-oportunidades/*');
$management = $request->is('management/*') || $profile;
$actor = $request->user();
$access = app(PortalProfileAccess::class);
// The general consumers use this same endpoint while authenticated.
// Reuse the read access already granted by the private cadastral routes.
$profileAccess = $actor && $actor->hasVerifiedEmail()
&& ($access->grants($actor) !== [] || $access->superuser($actor));
$internal = $profileAccess || ($actor?->hasVerifiedEmail() && $actor->can('manage-portal'));
if ($management) {
$access = app(PortalProfileAccess::class);
abort_unless($profile ? ($access->grants($request->user()) !== [] || $access->superuser($request->user())) : $request->user()?->can('manage-portal'), 403);
abort_unless($profile ? $profileAccess : $internal, 403);
}
return response()->json(app(TerritoryGeometry::class)->context($data['territory_id'], $management));
return response()->json([
...app(TerritoryGeometry::class)->context($data['territory_id'], $internal),
'geometry_scope' => $internal ? 'authorized_internal' : 'public',
])->header('Cache-Control', 'private, no-store');
}
private const TABLES = ['opportunities' => 'opportunities', 'projects' => 'projects', 'clusters' => 'economic_clusters', 'corridors' => 'development_corridors', 'zones' => 'investment_zones'];
@@ -14,12 +14,13 @@ final class EnsureScopedPortalAdministration
public function handle(Request $request, Closure $next): Response
{
$actor = $request->user();
abort_unless($actor, 403);
abort_unless($actor !== null, 403);
$scope = app(ScopedPortalAdministration::class);
$path = $request->path();
$interests = $request->is('management/interests', 'management/interests/*', 'api/v1/investment/admin/interests/*');
// Fiscal maintenance reuses authorised administration; unrelated gates remain unchanged.
$catalogue = $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*',
$geometryRead = $request->isMethod('GET') && $path === 'management/territory-geometries';
$catalogue = $geometryRead || $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*',
'management/economic-references/lot-01', 'management/economic-references/lot-01/*',
'management/education', 'management/education/*', 'management/investment/references', 'management/investment/references/*', 'api/v1/investment/admin/references', 'api/v1/investment/admin/references/*',
'management/investment/zones', 'management/investment/zones/*', 'api/v2/investment/admin/zones', 'api/v2/investment/admin/zones/*',
@@ -0,0 +1,42 @@
<?php
namespace App\Services;
use RuntimeException;
/** Public read projection authorized by Tempo; source rows and originals stay private. */
class ApprovedAdministrativeBoundaries
{
public const HASH = '06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc';
/** @var array<string,array<string,string>>|null */
private ?array $items = null;
/** @return array<string,array<string,string>> */
public function snapshots(): array
{
if ($this->items !== null) {
return $this->items;
}
$contents = file_get_contents(base_path('docs/investmoz/unification/approved-administrative-boundaries-20261024.json'));
if ($contents === false || ! hash_equals(self::HASH, hash('sha256', str_replace("\r\n", "\n", $contents)))) {
throw new RuntimeException('Approved administrative boundary manifest changed.');
}
$manifest = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
if (count($manifest['items']) !== 408) {
throw new RuntimeException('Exact 408-boundary scope required.');
}
$this->items = array_column($manifest['items'], null, 'id');
return $this->items;
}
public function allows(string $id, string $territoryId, string $geometryHash, string $propertiesHash): bool
{
$item = $this->snapshots()[$id] ?? null;
return $item !== null && $item['territory_id'] === $territoryId
&& hash_equals($item['geometry_sha256'], $geometryHash)
&& hash_equals($item['properties_sha256'], $propertiesHash);
}
}
+19 -4
View File
@@ -14,16 +14,31 @@ final class TerritoryGeometry
abort_unless($geography->references($management)->where('id', $id)->exists(), 404);
$path = $geography->path($id, $management);
$ids = array_map(fn ($node) => $node->id, $path);
$approved = app(ApprovedAdministrativeBoundaries::class);
$rows = DB::table('geo_geometries as g')->join('geo_locations as l', 'l.id', '=', 'g.geo_location_id')
->whereIn('g.geo_location_id', $ids)->whereNull('g.deleted_at')->whereNull('l.deleted_at')
->whereIn('g.data_classification', $management ? ['internal', 'public'] : ['public'])
->where(function ($query) use ($management, $approved) {
$query->where('g.data_classification', 'public');
$query->orWhere(function ($internal) use ($management, $approved) {
$internal->where('g.data_classification', 'internal');
if (! $management) {
$internal->whereIn('g.id', array_keys($approved->snapshots()));
}
});
})
->whereRaw("g.properties->>'purpose' = 'administrative_boundary'")
->whereIn('g.geometry_type', ['polygon', 'multipolygon'])
->whereRaw('ST_IsValid(g.geom) AND NOT ST_IsEmpty(g.geom) AND ST_SRID(g.geom)=4326')
->orderBy('g.geo_location_id')->orderBy('g.id')->limit(101)
->get(['g.id', 'g.geo_location_id', 'l.name', 'l.location_type', 'g.properties', 'g.data_classification',
DB::raw('ST_AsGeoJSON(ST_SimplifyPreserveTopology(g.geom,0.0001),6) as geometry')]);
$areas = $rows->take(100)->map(function ($row) use ($id) {
DB::raw('ST_AsGeoJSON(ST_SimplifyPreserveTopology(g.geom,0.0001),6) as geometry'),
DB::raw("encode(sha256(ST_AsEWKB(g.geom)),'hex') as geometry_sha256"),
DB::raw("encode(sha256(convert_to(g.properties::text,'UTF8')),'hex') as properties_sha256")]);
if (! $management) {
$rows = $rows->filter(fn ($row) => $row->data_classification === 'public'
|| $approved->allows($row->id, $row->geo_location_id, $row->geometry_sha256, $row->properties_sha256))->values();
}
$areas = $rows->take(100)->map(function ($row) use ($id, $management) {
$source = json_decode($row->properties, true, 512, JSON_THROW_ON_ERROR);
return ['id' => $row->id, 'territory_id' => $row->geo_location_id, 'location_type' => $row->location_type,
@@ -31,7 +46,7 @@ final class TerritoryGeometry
'title' => 'Limite administrativo: '.$row->name, 'purpose' => 'administrative_boundary',
'source_reference' => $source['authority'] ?? null, 'source_date' => $source['source_version'] ?? null,
'source_url' => $source['source_url'] ?? null, 'source_sha256' => $source['source_sha256'] ?? null,
'data_classification' => $row->data_classification, 'legal_currentness' => $source['legal_currentness'] ?? 'not_determined', 'url' => ''];
'data_classification' => $management ? $row->data_classification : 'public', 'legal_currentness' => $source['legal_currentness'] ?? 'not_determined', 'url' => ''];
})->all();
return ['territory_id' => $id, 'territory_registered' => true,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
{
"scope": "Shared administrative maps stabilization",
"source_revision": "cf2ddf21",
"candidate": "066707ec",
"owner_decision": "Tempo 2026-10-24: public consultation of the exact accepted administrative contours; originals private, no parcels or other internal layers",
"diagnosis": {
"data_loss": false,
"common_consumers": "Public-only filter excluded all 408 internal administrative boundaries",
"administration": "Legacy manage-portal gate excluded both approved superusers",
"comparison": "No change in common geometry service/controller/components between 30aed7cd and cf2ddf21",
"base_map": "Provider tiles and served assets respond; browser framing is checked after installation"
},
"qa": {
"database": "simulator_fiscal_delta_20261021_test",
"port": 55441,
"tests": 6,
"assertions": 64,
"result": "passed",
"phpstan": "passed",
"writes": "Only isolated QA transactions, rolled back"
},
"baseline": {
"published_fiches": 29,
"territorial_relations": 28,
"geometries": 408,
"geometry_rows_checksum": "7d8b71d3e65d89ffdc43c748b0572dbc"
},
"backup": {
"tables": 174,
"files_verified": 369,
"restore_tables_verified": 174,
"restore_network": "none",
"dump_sha256": "2fb5b4488799bb6072c38ee41ccf91fc2a55561363c19e280ab066363d9e0647"
},
"public_manifest_sha256": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc",
"installed": {
"status": "installed",
"commit": "066707ece631f0a7c963031c211844fbdd68916b",
"release": "/var/www/investmoz-homologacao/releases/066707ece631f0a7c963031c211844fbdd68916b-6b7bb5bd",
"previous_release": "/var/www/investmoz-homologacao/releases/cf2ddf21a1457f4852fe6aa10069ec3b854f1567-7749f82e",
"database_migrations": 0,
"content_imports": 0
},
"deferred": [
"Statistics",
"AI"
],
"accepted_tests_repeated": false,
"public_projection": {
"available": 408,
"levels": {
"admin_post": 264,
"district": 133,
"province": 11
},
"failures": [],
"manifest_sha256": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc"
},
"preservation": {
"tables": 174,
"changed_tables": {
"cache": {
"before": {
"rows": 104,
"checksum": "5d172b4d252c7b025f23ac70cf570cb6"
},
"after": {
"rows": 104,
"checksum": "a60f3e47e1ae28937404f05f45e33135"
}
},
"sessions": {
"before": {
"rows": 6,
"checksum": "dcdb7eba3f3e8f29f40ec3f7542be6ce"
},
"after": {
"rows": 7,
"checksum": "0059c0b0e16d532c6658fd420582f2e9"
}
}
},
"unchanged_tables": 172
},
"installed_file_sha256": {
"app/Services/ApprovedAdministrativeBoundaries.php": "401fd62795ca8f33106be6e07130d9207d6661695440bfc40f62f6f6b42bec64",
"app/Services/TerritoryGeometry.php": "00ce3e6dde92868b0d34b963b8e59a26e6663149cdc0398c645ef636ca382966",
"app/Http/Controllers/InvestmentTerritoryController.php": "3b184c6ea1b97f0abe86d01567d3d73444c49192c638478a6d53619e63d5d18d",
"app/Http/Middleware/EnsureScopedPortalAdministration.php": "1474db760f6bd6003a5325ae721698e90e2709a2354b51abde77f920a9dad81a",
"docs/investmoz/unification/approved-administrative-boundaries-20261024.json": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc"
},
"pipeline_summaries": [
"OK (1 test, 3106 assertions)",
"built in 24.58s",
"OK (57 tests, 309 assertions)",
"# tests 216",
"# pass 216",
"# fail 0"
],
"proofs": {
"anonymous_http": {
"status": 200,
"territory_id": "dd219b89-cc17-5944-8c7d-d2766c82b526",
"selected": "DONDO",
"ancestor": "Sofala",
"areas": 2
},
"browser": {
"province_to_district": "passed",
"framing": "selected district with ancestor context",
"reload_preserves_selection": true,
"zoom": "passed",
"visible_tiles_loaded": 36,
"visible_tiles_failed": 0,
"screenshot": "outputs/map-stabilization-20261024/dondo-installed.png",
"screenshot_sha256": "7569f3b66aceb7ef9816c31bfaa172ebcdfa10e4911961b67a129a654b5cd349"
},
"business_rows_unchanged": true,
"active_synthetic_requests_created": 0
},
"status": "installed_delta_verified",
"remaining_in_scope": []
}
@@ -35,8 +35,9 @@
"number": 3,
"component": "Navegação contextual e manifestação de interesse",
"historic_status": "Disponível",
"current_preservation_status": "Navegação/percurso preservados; carteira documental reposta em 23/10: 29 dossiers, incluindo 15 projectos âncora.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/investimentos/iniciar) · Início, contexto e navegação abrem; gravação/recuperação aceites em QA reutilizadas. Sem dossier real no destino."
"current_preservation_status": "Continuity and multi-opportunity consumers delivered: 55136b44, 8036b9b0, cf2ddf21. Accepted proofs preserved.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/investimentos/iniciar) · Início, contexto e navegação abrem; gravação/recuperação aceites em QA reutilizadas. Sem dossier real no destino.",
"status_before_stabilization_closure": "Navegação/percurso preservados; carteira documental reposta em 23/10: 29 dossiers, incluindo 15 projectos âncora."
},
{
"number": 4,
@@ -175,8 +176,9 @@
"number": 23,
"component": "Cartografia e consumidores territoriais restantes",
"historic_status": "Não demonstrado",
"current_preservation_status": "Demarcação reposta: 408 geometrias. Polígonos de localidades e correspondências já pendentes não são nova regressão.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos/mapas abrem. 1 921 referências geográficas no alvo versus 1 990 locais; diferença não reconciliada por item fora do manifesto."
"current_preservation_status": "Shared maps delivered 066707ec/5d45541a: 408 accepted contours publicly available; originals private, 29 fiches and 28 relations preserved. Missing locality polygons remain future work.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos/mapas abrem. 1 921 referências geográficas no alvo versus 1 990 locais; diferença não reconciliada por item fora do manifesto.",
"status_before_stabilization_closure": "Demarcação reposta: 408 geometrias. Polígonos de localidades e correspondências já pendentes não são nova regressão."
},
{
"number": 24,
@@ -210,8 +212,9 @@
"number": 28,
"component": "Pesquisa — estados do recorte R2",
"historic_status": "Disponível",
"current_preservation_status": "Pesquisa e selectores corrigidos em 22/10; a carteira vazia é tratada como perda separada.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos abrem; quatro estados e protecção contra respostas antigas preservados do R2, sem repetir ensaios aceites."
"current_preservation_status": "Accepted search/selectors preserved; portfolio visibility and anchor continuity restored and delivered cf2ddf21. No current portfolio-empty fault.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos abrem; quatro estados e protecção contra respostas antigas preservados do R2, sem repetir ensaios aceites.",
"status_before_stabilization_closure": "Pesquisa e selectores corrigidos em 22/10; a carteira vazia é tratada como perda separada."
},
{
"number": 29,
@@ -945,5 +948,79 @@
"new_roles_or_approvers": false,
"new_content_approval": false
},
"qa_recovery_preparation_note": "An initial QA audit-key collision rolled back atomically. Audit identity was scoped per original before the successful QA and destination recovery; no active attempt used the faulty key."
"qa_recovery_preparation_note": "An initial QA audit-key collision rolled back atomically. Audit identity was scoped per original before the successful QA and destination recovery; no active attempt used the faulty key.",
"stabilization_closure_20261024": {
"basis": "Tempo explicit sequence and technical preservation responsibility, 24/10/2026",
"installed_functional_baseline": "066707ece631f0a7c963031c211844fbdd68916b",
"map_receipt": "5d45541a",
"known_open_faults_in_delimited_stabilization_scope": [],
"does_not_close_all_43_components": true,
"prior_losses_do_not_share_a_proven_single_cause": true,
"accepted_closures": [
"6b544548/4098192a",
"d8224ba8/af926180",
"55136b44/b6a626c9",
"8036b9b0/cdf8fcfb",
"cf2ddf21/c1f4a719",
"066707ec/5d45541a"
],
"documentary_limits": [
"Six legacy opportunity/anchor-cluster pairs without documented identity remain indeterminate: AUD-REL-02",
"Two incomplete legacy originals retain their existing documentary limit"
],
"not_yet_proven_component_numbers": [
6,
18,
21,
22,
24,
25,
35
],
"future_requirement_ids": [
"AUD-MARKET-01",
"AUD-ESTABLISHMENT-01",
"AUD-FUND-01",
"AUD-COMPANY-01",
"AUD-COMPANY-02",
"AUD-CAPITAL-01",
"AUD-CAPITAL-02",
"AUD-FINTECH-01"
],
"territorial_count_reconciliation": {
"source": "outputs/preservation-inventory-20261023/local.json and homologation.json",
"public_active_local": 1921,
"public_active_target": 1921,
"local_archived_internal_groups": [
32,
32,
5
],
"individual_identity_comparison_of_archived_69_claimed": false,
"reactivation_authorized": false
},
"new_control": {
"test": "tests/Unit/ApprovedAdministrativeBoundariesTest.php",
"automatic_delivery_suite": "Unit",
"tests": 2,
"assertions": 8,
"result": "passed",
"scope": "Approved public snapshot identity; changed geometry/metadata, wrong territory and new internal identity rejected",
"http_access_contract": "tests/Feature/TerritoryGeometryConsumerAccessTest.php",
"http_contract_run_when": "Geometry/access/hierarchy changes, on isolated QA only; existing accepted result reused here"
},
"technical_preservation_owner": "Codex and programmers",
"Tempo_review": "Utility, clarity and presentation; no obligation to rediscover technical regressions",
"sequence": [
"Corrections and stabilization",
"Fiscal simulator with defined scope",
"Real estate",
"Consumer goods and franchises",
"Statistics and dashboards",
"AI"
],
"accepted_acceptance_tests_repeated": false,
"active_database_writes": 0,
"runtime_application_source_changes": 0
}
}
@@ -1,5 +1,43 @@
# Rastreabilidade, utilização e formação
## Fecho do lote de estabilização e sequência de referência — 24/10/2026
**Ordem expressa da Tempo:** (1) correcções e estabilização; (2) simulador fiscal com âmbito definido; (3) Imobiliário; (4) Bens de consumo e franquias; (5) Estatística e painéis; (6) IA. A preservação técnica é responsabilidade do Codex e dos programadores. A Tempo avalia utilidade, clareza e apresentação; não tem de redescobrir regressões para comprovar preservação.
Consolidação sobre a revisão instalada `066707ec`, recibo `5d45541a`, mantendo a mesma matriz e o inventário dos 43 componentes. Nesta passagem não foi encontrada nova falha executável nos recortes de estabilização já delimitados. A revisão de código confirma que a primeira oportunidade usada na migração do contexto comum não substitui o âmbito dos consumidores: `InvestmentConsumerScope` filtra a escolha antes do snapshot e dos relatórios. Não foram repetidas as provas aceites, impressões, submissões ou alterações de dados. A confirmação da Tempo anterior baseou-se no relatório comunicado, sem conferência do recibo local; não é registada como uma nova prova técnica ou visual.
| Situação no lote | Percursos e saldo concreto |
| --- | --- |
| Resolvido e entregue | Gravação/idioma, proveniência do relatório e Beira/sete oportunidades: `6b544548`/`4098192a`. Complementos: `d8224ba8`/`af926180`. Zonas/parques, Terra/DUAT, Balcão e manifestação por oportunidade: `55136b44`/`b6a626c9`. Selector do simulador, etapas/requisitos e pedido Terra correcto, incluindo PDF guardado: `8036b9b0`/`cdf8fcfb`. Âncoras, sector/localização e continuidade: `cf2ddf21`/`c1f4a719`. Mapas: `066707ec`/`5d45541a`. Pendências históricas destes trechos ficam superadas pelas respectivas entregas. |
| Falha conhecida ainda aberta neste lote | **Nenhuma identificada nos recortes enumerados.** Não equivale a aceitação integral dos 43 componentes ou de todas as variantes do portal. Não se inventa uma nova correcção para justificar outra ronda. |
| Pendência documental preservada | `AUD-REL-02`: os pares das seis oportunidades legadas com âncoras/clusters continuam indeterminados quando não há relação documentada. Não confundir com as 28 relações recuperadas dos projectos. Preservar a preparação independente; não associar por proximidade ou nome. Dois originais incompletos do acervo legado conservam o limite documental já registado, sem representar perda causada pelos mapas. |
| Ainda sem prova no âmbito próprio | Inventário existente: consolidação de duplicados distintos (componente 6); aplicação/editor e versões de checklists (18); partilha e variantes de sala (21/35); modalidades/SLA e fecho operacional/documental (22/25); acessibilidade além das provas aceites (24). Não foram identificadas novas regressões nesses itens; não os declarar concluídos nem exigir processos reais/sintéticos na homologação para fechar este lote. Mantêm os critérios próprios da matriz, sem atribuição de novo prazo ou prioridade. |
| Desenvolvimento futuro, separado de regressões | Negócios similares por raio (`AUD-MARKET-01`), importador de empresas/empreendimentos (`AUD-ESTABLISHMENT-01`), catálogo de fundos (`AUD-FUND-01`), comparador societário e modelos específicos (`AUD-COMPANY-01/02`), guiões de capitalização/Bolsa (`AUD-CAPITAL-01/02`), polígonos próprios ainda inexistentes e enquadramento Fintech específico. Variantes de Imobiliário/franquias, Estatística/painéis e IA seguem a ordem acima; SuiteCRM mantém a opção anterior de desactivação. Não se declara que todo o código destes módulos esteja ausente. |
**Causas independentes:** os mapas tinham filtro público e dependência de uma permissão antiga; a carteira sofreu perda de visibilidade de registos presentes; as relações de âncoras tinham omissão no transporte entre identidades documentais. As provas delimitam esses casos e não estabelecem a causa de todas as perdas anteriores. O inventário histórico territorial tinha 1 990 linhas locais contra 1 921 no destino: **1 921 públicas activas em ambos, mais 69 internas arquivadas apenas no local**, repartidas em 32+32+5. Isto explica a diferença agregada; não é prova de correspondência individual das 69 nem autoriza reactivá-las. Nenhuma reposição adicional decorre dessa contagem.
### Controlo técnico nas próximas entregas
Reutilizar o inventário/recibos e comparar apenas o delta e as dependências afectadas. O pipeline existente conserva detecção de alterações concorrentes na árvore instalada, contrato de schema, QA isolado, assets imutáveis, troca atómica e reversão por falha de saúde. A suite normal de entrega passa também a incluir `ApprovedAdministrativeBoundariesTest`: identidade aceite e recusa de novo contorno interno, território trocado, geometria ou metadados alterados. É controlo novo de âmbito público, não repetição da demonstração visual aceite. As duas provas novas passaram, com oito asserções e sem acesso à base.
Quando se alterar acesso, hierarquia ou serviço de mapas, executar o delta correspondente de `TerritoryGeometryConsumerAccessTest` em QA isolado; cobre o filtro público e os perfis aprovados sem `manage-portal`, incluindo MFA administrativo. O teste unitário isolado não substitui esse contrato HTTP. Para alterações de contexto, usar os casos afectados de `InvestmentTreatmentScopeTest`, `InvestmentAnchorIntegrationTest` e consumidores/complementos, sem repetir indiscriminadamente toda a suite de aceitação. No destino, comparar identidades/fontes/estado de publicação das 29 fichas, 28 relações e 408 geometrias com os manifestos aceites, configuração e permissões equivalentes; mudanças legítimas posteriores são reconciliadas explicitamente, não sobrescritas. Backup/restauro e acção autenticada precedem qualquer escrita de dados activos.
**Saldo de fecho:** zero falhas técnicas conhecidas abertas neste lote; limites documentais, itens sem prova e desenvolvimento futuro permanecem identificados acima e nas mesmas linhas da matriz. Não se declara a aplicação transversal integralmente concluída. O próximo lote é a delimitação do simulador fiscal; esta passagem não activa cálculos, fontes, estatísticas ou IA. A avaliação de navegação/apresentação da Tempo pode continuar pelo percurso normal e alimenta apenas falhas concretamente observadas.
## Mapas — estabilização comum e divulgação delimitada, 24/10/2026
Diagnóstico por leitura autenticada na homologação `cf2ddf21`: **29 fichas, 28 relações territoriais e 408 geometrias válidas**, sem eliminação. Os 408 limites INE/2019 conservam classificação interna, propósito administrativo e SRID 4326. O fingerprint integral das linhas é `7d8b71d3e65d89ffdc43c748b0572dbc`. Não há RLS nas tabelas comparadas. O serviço comum não mudou desde `30aed7cd`: os consumidores gerais pediam exclusivamente geometrias públicas; o cadastro usava ainda `manage-portal`, ausente nos dois superutilizadores aprovados. A prova anterior demonstrava o cadastro privado, não a divulgação pública. O mapa base, tiles e assets carregam; a resposta territorial vazia explica a ausência de contornos e de enquadramento do território escolhido.
**Decisão expressa da Tempo nesta passagem:** «Sim, disponibilizar os contornos administrativos aceites». Implementada uma projecção pública de leitura dos [408 snapshots exactos](approved-administrative-boundaries-20261024.json), conferindo identidade e hashes da geometria original e metadados. Não há reimportação, reclassificação de linhas, publicação de parcelas, acesso aos ficheiros originais ou abertura de outras camadas internas. Alterações posteriores não recebem divulgação automática. Os perfis autorizados reutilizam o acesso privado já aprovado; a leitura administrativa conserva MFA. Respostas privadas não são guardadas em cache partilhada.
QA isolado `simulator_fiscal_delta_20261021_test:55441`: seis testes dirigidos, 64 asserções; selecção, gravação/recuperação no dossier e prefill do simulador com a mesma identidade, leitura pública autorizada, bloqueio de camadas não autorizadas e de snapshots alterados, permissões/MFA e fingerprint original preservado. PHPStan dos quatro ficheiros funcionais passou. Backup fresco: 174 tabelas, 369 ficheiros conferidos; restauro isolado sem rede reproduziu as 174 tabelas.
**Entregue na homologação em `066707ece631f0a7c963031c211844fbdd68916b`**, por avanço normal do Git, preservando `cf2ddf21` e as alterações concorrentes. Instalador: contrato 1/3106, PHP 57/309, frontend 216/216 e build aprovados; zero migrações e zero importações. Os cinco ficheiros funcionais instalados coincidem com o Git. Consulta pública pelo serviço instalado: **408/408 disponíveis**, 11 províncias, 133 distritos e 264 postos, zero falhas. HTTP anónimo recupera Dondo e Sofala pela identidade canónica, com Dondo seleccionado e Sofala como contexto ancestral. Conferência visual: mudança de província para distrito actualiza o contorno e enquadramento; escolha preservada após recarregar, zoom operacional e 36 tiles carregados sem falha na observação final. Houve falhas transitórias de tiles na janela anterior; os mesmos URLs responderam 200 e o recarregamento recuperou a base, sem alteração de configuração ou fornecedor.
Comparação das 174 tabelas: **172 iguais por contagem/hash**, diferenças apenas em `cache` e `sessions`; tabelas de negócio, geometrias, permissões e fontes intactas. Conservados 29 fichas, 28 relações e o fingerprint integral das 408 geometrias. Nenhum dossier/pedido sintético criado na homologação. [Recibo da estabilização](map-stabilization-receipt-20261024.json); prova visual em `outputs/map-stabilization-20261024/dondo-installed.png`. O saldo técnico desta reposição está encerrado; a avaliação de navegação e apresentação pela Tempo permanece o próximo trabalho.
Preservados os fechos `d8224ba8`, `55136b44`, `8036b9b0` e `cf2ddf21`/`c1f4a719`, sem repetição das provas aceites. Localidades sem polígono próprio conservam apenas o contexto ancestral explicitamente identificado; não são inventadas coordenadas ou parcelas. Estatísticas e IA permanecem adiadas. Esta estabilização não declara concluída a aplicação transversal nem substitui a avaliação de navegação e apresentação pela Tempo.
## Projectos âncora — entregues na homologação em cf2ddf21, 23/10/2026
Reconciliado o saldo de `6985f82e` com as provas territoriais de 7 de Outubro e os recibos de reposição. A leitura autenticada da homologação, com o papel da aplicação e transacção apenas de leitura, confirmou **29 fichas publicadas, 13 relações de clusters e zero ligações em `project_locations`**. As cinco tabelas comparadas não usam RLS. Os projectos transportados têm identificadores diferentes dos originais e perderam as 28 ligações territoriais e os respectivos metadados de citação; não desapareceram as 29 fichas. A reposição de visibilidade anterior não abrangia estas relações. A comparação delimita a omissão no transporte; não identifica por si só o instante em que ocorreu.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,118 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\ApprovedAdministrativeBoundaries;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class TerritoryGeometryConsumerAccessTest extends TestCase
{
use DatabaseTransactions;
private User $actor;
private string $territory;
protected function setUp(): void
{
parent::setUp();
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
$this->withoutVite();
$this->withoutMiddleware(ThrottleRequests::class);
config(['portal.profile_access_enabled' => true, 'portal.managers' => [], 'investor-path.enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
$this->actor = User::factory()->create(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()]);
$other = User::factory()->create();
DB::table('portal_superuser_pair')->updateOrInsert(['id' => 1], ['first_user_id' => $this->actor->id, 'second_user_id' => $other->id, 'approved_by' => $other->id]);
$this->territory = DB::table('geo_geometries as g')->join('geo_locations as l', 'l.id', '=', 'g.geo_location_id')
->where('g.source_system', 'ine-cod-ab')->where('g.data_classification', 'internal')->where('l.location_type', 'province')->value('l.id');
$this->assertNotEmpty($this->territory);
}
public function test_general_consumer_uses_existing_authenticated_read_access_without_the_legacy_gate(): void
{
$this->assertFalse($this->actor->can('manage-portal'));
$before = $this->fingerprint();
$this->actingAs($this->actor)->getJson('/api/v1/investment/territory-geometries?territory_id='.$this->territory)
->assertOk()->assertJsonPath('selected_geometry_available', true)->assertJsonPath('geometry_scope', 'authorized_internal')
->assertJsonPath('areas.0.territory_id', $this->territory)->assertHeader('Cache-Control', 'no-store, private');
$this->assertSame($before, $this->fingerprint());
}
public function test_existing_administration_reaches_the_same_geometry_with_its_completed_mfa_session(): void
{
$this->actingAs($this->actor)->withSession(['portal.mfa_actor' => $this->actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $this->actor->portal_access_revision]);
$this->getJson('/management/territory-geometries?territory_id='.$this->territory)->assertOk()->assertJsonPath('selected_geometry_available', true);
session()->forget(['portal.mfa_actor', 'portal.mfa_at']);
$this->getJson('/management/territory-geometries?territory_id='.$this->territory)->assertForbidden();
}
public function test_anonymous_and_ordinary_accounts_do_not_receive_or_publish_internal_layers(): void
{
$before = $this->fingerprint();
$url = '/api/v1/investment/territory-geometries?territory_id='.$this->territory;
$this->getJson($url)->assertOk()->assertJsonPath('geometry_scope', 'public')->assertJsonPath('selected_geometry_available', false)->assertJsonCount(0, 'areas');
$this->actingAs(User::factory()->create())->getJson($url)->assertOk()->assertJsonPath('geometry_scope', 'public')->assertJsonCount(0, 'areas');
$this->getJson('/management/territory-geometries?territory_id='.$this->territory)->assertForbidden();
$this->assertSame($before, $this->fingerprint());
}
public function test_saved_territory_is_recovered_in_dossier_and_simulator_and_the_map_uses_that_identity(): void
{
$this->actingAs($this->actor);
$id = $this->postJson('/investimentos', ['title' => 'SYNTHETIC QA - mapa recuperado', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
$body = ['version' => $this->getJson('/investimentos/'.$id.'/contexto')->json('case.version'), 'selection' => ['territory' => 'territory:'.$this->territory]];
$preview = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json();
$this->postJson('/investimentos/'.$id.'/contexto/confirmar', [...$body, 'preview_hash' => $preview['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk();
$view = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json();
$this->assertSame($this->territory, $view['context']['selection']['territory']['id']);
$this->assertSame($this->territory, $view['prefill']['simulator']['province_id']);
$this->getJson('/api/v1/investment/territory-geometries?territory_id='.$view['prefill']['simulator']['province_id'])
->assertOk()->assertJsonPath('selected_geometry_available', true)->assertJsonPath('areas.0.selected_territory', true);
$this->assertSame($this->territory, $this->getJson('/investimentos/'.$id.'/contexto')->json('context.selection.territory.id'));
}
public function test_only_the_approved_unchanged_snapshot_is_public_without_reclassifying_the_original(): void
{
$before = $this->fingerprint();
$row = DB::table('geo_geometries as g')->where('geo_location_id', $this->territory)->first([
'g.id', 'g.geo_location_id', DB::raw("encode(sha256(ST_AsEWKB(g.geom)),'hex') as geometry_sha256"),
DB::raw("encode(sha256(convert_to(g.properties::text,'UTF8')),'hex') as properties_sha256"),
]);
// The QA import has its own identities; never copy or replace production rows.
$approved = $this->partialMock(ApprovedAdministrativeBoundaries::class);
$approved->shouldReceive('snapshots')->andReturn([$row->id => [
'territory_id' => $row->geo_location_id, 'geometry_sha256' => $row->geometry_sha256,
'properties_sha256' => $row->properties_sha256,
]]);
$url = '/api/v1/investment/territory-geometries?territory_id='.$this->territory;
$this->getJson($url)->assertOk()->assertJsonPath('selected_geometry_available', true)
->assertJsonPath('areas.0.data_classification', 'public')->assertJsonPath('investment_site', false);
$this->actingAs(User::factory()->create())->getJson($url)->assertOk()->assertJsonPath('selected_geometry_available', true);
$this->assertSame($before, $this->fingerprint());
$this->assertSame('internal', DB::table('geo_geometries')->where('id', $row->id)->value('data_classification'));
DB::table('geo_geometries')->where('id', $row->id)->update(['properties' => DB::raw("properties || '{\"qa_changed\":true}'::jsonb")]);
$this->getJson($url)->assertOk()->assertJsonPath('selected_geometry_available', false)->assertJsonCount(0, 'areas');
$this->actingAs($this->actor)->getJson($url)->assertOk()->assertJsonPath('selected_geometry_available', true);
}
public function test_public_manifest_has_the_exact_approved_scope_and_immutable_hash(): void
{
$items = app(ApprovedAdministrativeBoundaries::class)->snapshots();
$this->assertCount(408, $items);
$this->assertCount(408, array_unique(array_column($items, 'territory_id')));
$this->assertSame(['2019-06-07'], array_values(array_unique(array_column($items, 'source_version'))));
}
private function fingerprint(): string
{
return DB::selectOne("select md5(string_agg(row_to_json(g)::text,E'\\n' order by id)) as hash from geo_geometries g")->hash;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Tests\Unit;
use App\Services\ApprovedAdministrativeBoundaries;
use Tests\TestCase;
/** Runs in the normal delivery unit suite, without database or external services. */
final class ApprovedAdministrativeBoundariesTest extends TestCase
{
public function test_public_scope_keeps_the_accepted_identity_and_refuses_changed_or_new_internal_shapes(): void
{
$approved = new ApprovedAdministrativeBoundaries;
$items = $approved->snapshots();
$this->assertCount(408, $items);
$item = reset($items);
$this->assertTrue($approved->allows($item['id'], $item['territory_id'], $item['geometry_sha256'], $item['properties_sha256']));
$this->assertFalse($approved->allows('00000000-0000-0000-0000-000000000000', $item['territory_id'], $item['geometry_sha256'], $item['properties_sha256']));
$this->assertFalse($approved->allows($item['id'], '00000000-0000-0000-0000-000000000000', $item['geometry_sha256'], $item['properties_sha256']));
$this->assertFalse($approved->allows($item['id'], $item['territory_id'], str_repeat('0', 64), $item['properties_sha256']));
$this->assertFalse($approved->allows($item['id'], $item['territory_id'], $item['geometry_sha256'], str_repeat('0', 64)));
}
public function test_the_public_scope_keeps_the_accepted_edition_and_unique_territories(): void
{
$items = (new ApprovedAdministrativeBoundaries)->snapshots();
$this->assertCount(408, array_unique(array_column($items, 'territory_id')));
$this->assertSame(['2019-06-07'], array_values(array_unique(array_column($items, 'source_version'))));
}
}