Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
276d833a4b | ||
|
|
5d45541a11 | ||
|
|
066707ece6 |
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user