diff --git a/app/Http/Controllers/InvestmentDiscoveryController.php b/app/Http/Controllers/InvestmentDiscoveryController.php index c7a94c04..3a7439d0 100644 --- a/app/Http/Controllers/InvestmentDiscoveryController.php +++ b/app/Http/Controllers/InvestmentDiscoveryController.php @@ -199,15 +199,16 @@ class InvestmentDiscoveryController extends Controller { $records = $catalogue->query($catalogue->filters($request))->limit(1000)->get(); - return response()->streamDownload(function () use ($records) { + return response()->streamDownload(function () use ($records, $catalogue) { $stream = fopen('php://output', 'w'); if ($stream === false) { return; } fwrite($stream, "\xEF\xBB\xBF"); - fputcsv($stream, ['Referência', 'Título', 'Sector', 'Província', 'Montante', 'Moeda', 'Maturidade', 'URL'], ';', '"', ''); + fputcsv($stream, ['Referência', 'Título', 'Sector', 'Província', 'Montante', 'Moeda', 'Maturidade', 'URL', 'Localização e âmbito'], ';', '"', ''); foreach ($records as $r) { - $fields = [$r->reference, $r->title, $r->sector?->name, $r->province, $r->estimated_investment_amount, $r->estimated_investment_currency, $r->maturity_label, url('/oportunidades/'.$r->slug)]; + $location = $catalogue->locationPresentation($r); + $fields = [$r->reference, $r->title, $r->sector?->name, implode(' / ', $location['provinces']), $r->estimated_investment_amount, $r->estimated_investment_currency, $r->maturity_label, url('/oportunidades/'.$r->slug), $location['summary']]; fputcsv($stream, array_map(fn ($v) => preg_match('/^[\s]*[=+@-]/u', (string) $v) ? "'".$v : $v, $fields), ';', '"', ''); } fclose($stream); diff --git a/app/Services/InvestmentCatalogue.php b/app/Services/InvestmentCatalogue.php index 96eddf31..31ae768f 100644 --- a/app/Services/InvestmentCatalogue.php +++ b/app/Services/InvestmentCatalogue.php @@ -260,26 +260,41 @@ class InvestmentCatalogue /** Public result presentation only; no editorial or territorial data is saved. */ public function presentLocation(Opportunity $record): Opportunity + { + $record->setAttribute('territorial_summary', $this->locationPresentation($record)['summary']); + + return $record; + } + + /** @return array{summary: string, provinces: list} */ + public function locationPresentation(Opportunity $record): array { $geo = app(InvestmentGeography::class); $paths = []; + $provinceNames = []; $ids = array_filter([$record->primary_territory_id, ...$record->additionalTerritories()->pluck('geo_locations.id')->all()]); foreach (array_unique($ids) as $id) { $path = $geo->path($id); if ($path) { $paths[] = implode(' / ', array_column($path, 'name')); + foreach ($path as $node) { + if ($node->location_type === 'province') { + $provinceNames[] = (string) $node->name; + } + } } } if ($paths) { $label = implode('; ', array_unique($paths)); } elseif ($provinces = app(InvestmentCorridorContext::class)->catalogueProvinces($record)) { - $label = implode(' / ', array_column($provinces, 'name')).' — contexto provincial do corredor; distrito e posto administrativo não indicados'; + $provinceNames = array_map(fn ($province) => (string) $province->name, $provinces); + $label = implode(' / ', $provinceNames).' — contexto provincial do corredor; distrito e posto administrativo não indicados'; } else { $label = implode(' / ', array_filter([$record->province, $record->district])) ?: 'Localização não indicada'; + $provinceNames = $record->province ? [(string) $record->province] : []; } - $record->setAttribute('territorial_summary', $label); - return $record; + return ['summary' => $label, 'provinces' => array_values(array_unique($provinceNames))]; } /** @param array $territoryFilters diff --git a/docs/investmoz/consolidated-review-matrix-2026-10-03.md b/docs/investmoz/consolidated-review-matrix-2026-10-03.md index ed47cf8f..400d72ca 100644 --- a/docs/investmoz/consolidated-review-matrix-2026-10-03.md +++ b/docs/investmoz/consolidated-review-matrix-2026-10-03.md @@ -2202,3 +2202,14 @@ Dois testes frontend dirigidos passaram: contagens/proveniência desconhecida/re **ACT-02 permanece parcial:** este recorte implementa controlo das camadas já existentes e explicitação de cobertura. Não cria camadas factuais de recursos, infra-estruturas, mão-de-obra ou potencial económico, nem declara completa a cobertura provincial/distrital. Backend, geometrias, divulgação, permissões, fontes, datas e cálculo preservados; sem importações ou escrita no activo. **Visual pendente pela equipa:** na candidata, conferir os mapas da ficha de oportunidade, carteira e zona de investimento; ocultar/repor cada camada e ambas, conferir contagens de investimentos/marcadores, fonte/data ausentes e fichas sem coordenadas, e mudar filtros territoriais. Registar revisão, perfil, percurso, esperado/observado e evidência nesta matriz. O painel de contexto administrativo separado mantém o seu âmbito. Sem instalação ou avanço de main. + + +### 04/11/2026 — conferência visual da main na QA isolada e correcção do CSV provincial + +Percurso executado no Edge em `http://127.0.0.2:18644`, base exclusiva `main_visual_20261104_test` no cluster QA `127.0.0.1:55441`, com actor e conteúdos sintéticos. Autenticação Fortify normal com MFA: editar, guardar v2, recarregar e conferir os valores; aprovar v3; guardar novo rascunho v4; terminar sessão e conferir publicamente apenas v3, sem o marcador do rascunho. Histórico v1–v4 preservado. Esta prova conclui o recorte visual local de Competitividade; não equivale a execução no portal remoto ou aceitação da equipa em dados reais. + +Filtro apenas por Maputo Província, com sector Agronegócio: cinco oportunidades existentes mais quatro fixtures sintéticas nos níveis província/distrito/posto/localidade resultaram em nove na grelha, lista, mapa e CSV. Distrito de Matola apresentou três; regressar a toda a província recuperou nove e limpou distrito/posto/localidade. Nenhuma coordenada criada: mapa informa zero locais e nove sem coordenadas. Hash das oportunidades anteriores idêntico antes/depois. Limite administrativo desta província indisponível na QA, explicitamente indicado; cobertura cartográfica transversal continua pendente. + +Detectada e corrigida divergência no CSV: coluna Província vazia apesar da localização canónica/contexto de corredor disponível. Exportação reutiliza a apresentação territorial existente, conserva as oito primeiras colunas e o escape contra fórmulas, acrescentando Localização e âmbito. Contexto do corredor indica expressamente que distrito/posto não foram indicados; não grava nem inventa localização. CSV descarregado pela interface após correcção: nove linhas, todas com província e âmbito coerentes. + +Delta aprovado: 17 testes de base de dados, 609 asserções; PHPStan sem erros e Pint aprovado. Provas anteriores de 1.325 testes/28.377 asserções preservadas, sem repetição integral. Recibo com hashes e capturas: `docs/investmoz/unification/main-local-visual-qa-20261104.json`; artefactos em `outputs/main-visual-qa-20261104/`. Apenas main local e QA; sem push, escrita activa ou alteração de permissões activas. Conferência dos filtros no portal de destino e pendências gerais ACT-02/V2-07/V2-10 continuam abertas. diff --git a/docs/investmoz/unification/main-local-visual-qa-20261104.json b/docs/investmoz/unification/main-local-visual-qa-20261104.json new file mode 100644 index 00000000..f5554b6d --- /dev/null +++ b/docs/investmoz/unification/main-local-visual-qa-20261104.json @@ -0,0 +1,138 @@ +{ + "date": "2026-11-04", + "code_base": "dc0bbd74", + "scope": "main local; QA isolada; sem push ou instalação remota", + "target": { + "http": "http://127.0.0.2:18644", + "database": "main_visual_20261104_test", + "postgres": "127.0.0.1:55441" + }, + "competitiveness": { + "authentication": "Fortify normal com MFA; actor sintético; sessão terminada no final", + "saved_and_reopened_version": 2, + "approved_version": 3, + "later_saved_draft_version": 4, + "anonymous_public_version": 3, + "draft_marker_absent_public": true, + "history_versions": [ + 4, + 3, + 2, + 1 + ] + }, + "province": { + "existing_results": 5, + "synthetic_lower_level_fixtures": 4, + "province_results": 9, + "district_results": 3, + "return_to_whole_province_results": 9, + "district_post_locality_filters_empty": true, + "confirmed_coordinates": 0, + "records_without_coordinates": 9, + "existing_rows_sha256": "6b6654b620c90920927f075d2cd0d89203997cc4971de2dee4053f3872822995", + "existing_rows_unchanged": true, + "coordinates_created": false + }, + "csv": { + "before": "Província vazia apesar de localização/contexto disponível", + "after": "Província derivada da apresentação existente; coluna Localização e âmbito acrescentada após as oito colunas anteriores; contexto de corredor explicitamente qualificado", + "rows_verified": 9, + "columns": [ + "Referência", + "Título", + "Sector", + "Província", + "Montante", + "Moeda", + "Maturidade", + "URL", + "Localização e âmbito" + ], + "formula_escape_preserved": true, + "territorial_records_written": false + }, + "delta_checks": { + "ProvinceSearchCompatibilityTest": { + "tests": 4, + "assertions": 399, + "passed": true + }, + "InvestmentTerritoryTest": { + "tests": 13, + "assertions": 210, + "passed": true + }, + "total_tests": 17, + "total_assertions": 609, + "phpstan_errors": 0, + "pint": "passed" + }, + "previous_proofs": { + "receipt": "docs/investmoz/unification/main-local-database-qa-20261104.json", + "tests": 1325, + "assertions": 28377, + "preserved_not_reexecuted": true + }, + "environment_observations": [ + "Edge disponível nesta execução; sem reparação da extensão ou diagnóstico retroactivo de portas.", + "Servidor QA inicial apresentou erro RecursiveIterator; substituído apenas o processo HTTP próprio por PHP Laragon ZTS, após conferir endereço e router; configurações globais e serviços activos preservados." + ], + "limitations": [ + "Falta limite administrativo acessível desta província na QA; interface informa ausência, sem geometria inventada.", + "Não constitui conferência no portal remoto nem aceitação da equipa em dados reais.", + "ACT-20261029-02, V2-07 e V2-10 mantêm pendências transversais; nenhum encerramento global." + ], + "active_database_changed": false, + "remote_push": false, + "proofs": { + "outputs/main-visual-qa-20261104/competitiveness-reopened-v2.jpg": { + "sha256": "600be345402f49be18ae2a0d711d1b4b9f22b92888cc99c85bef447748ff1e0b", + "bytes": 117130 + }, + "outputs/main-visual-qa-20261104/competitiveness-history-v4.jpg": { + "sha256": "c5bc80fa149a47acb4c611b556f11056b65601cccbbeb894dea67d25a003162f", + "bytes": 142439 + }, + "outputs/main-visual-qa-20261104/competitiveness-approved-anonymous-v3.jpg": { + "sha256": "f7f36c9a219fb985015076819876c8f0ef7b1106528c6831b0c37a0b37726744", + "bytes": 101483 + }, + "outputs/main-visual-qa-20261104/province-maputo-baseline-five.jpg": { + "sha256": "80c9796fbe3029e810276d2663b71af33a56773662ce580d9e4797df37c07f5b", + "bytes": 112558 + }, + "outputs/main-visual-qa-20261104/province-list-nine.jpg": { + "sha256": "45c587fa001878faf0086a9184b0ecc6b10de4ad49ad3bf6d4c46f6033eaa3f1", + "bytes": 119640 + }, + "outputs/main-visual-qa-20261104/province-map-nine-unlocated.jpg": { + "sha256": "f792ef6b6b7f4676f5ae3d03e5df0d14aadcf2b3f1d6c9091a2d69475f89d9f4", + "bytes": 127224 + }, + "outputs/main-visual-qa-20261104/district-three.dom.txt": { + "sha256": "327ef9df57857f6eec6a7394cfdd182e8b0831783a8dc18973af6d3e466bf80d", + "bytes": 15267 + }, + "outputs/main-visual-qa-20261104/province-map-nine.dom.txt": { + "sha256": "be98f4dcf36ffbe2aca791942f9d4399c91972a06cf6556d1bcf814a0cfec933", + "bytes": 23379 + }, + "outputs/main-visual-qa-20261104/competitiveness-anonymous-v3.dom.txt": { + "sha256": "a2bb6274cebb75de62976afb72d1235c32db2ba9572c37c0f6e988d5114b8aec", + "bytes": 10379 + }, + "outputs/main-visual-qa-20261104/province-maputo-nine.csv": { + "sha256": "f1c2beb7c2b49d7aba481cb612535fe6c4974d1f7af8da34e4cd27ad9825d8c5", + "bytes": 1584 + }, + "outputs/main-visual-qa-20261104/province-maputo-nine-corrected.csv": { + "sha256": "70fb2a79277f4d7bce523e18bf2e32299c18029be0400f01b43132588d9a251c", + "bytes": 2640 + }, + "outputs/main-visual-qa-20261104/province-fixtures.json": { + "sha256": "7a04c79f87f2fb77595e601ba21fae598c62c4fd2cb7efa21673949c186c8ed0", + "bytes": 1426 + } + } +} diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 46db0c50..5af67f4f 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -9098,3 +9098,14 @@ Resultado final por caso distinto, substituindo o resultado inicial apenas quand Preparação e diagnósticos preservados: dois snapshots QA anteriores restaurados em bases separadas e actualizados pelas migrações locais; histórico de uma migração da oferta reconciliado só no clone, após comparar colunas, constraints, índices, políticas e RLS com o schema actual. Dois testes com traits combinados de RefreshDatabase tinham sido mal agrupados: executor da cópia de referência interrompido, logs conservados, cópia restaurada e testes separados; não foi perda de dados activos. Fixture fiscal própria executada em schema limpo, sem apagar versões legítimas do snapshot. Chave local compatível do snapshot verificada e disponibilizada apenas ao executor como chave anterior, sem expor, alterar chave ou reescrever registos. Originais editoriais existentes copiados para a fixture QA com hashes verificados. Falhas iniciais não são apagadas nem apresentadas como regressões resolvidas no produto sem causa demonstrada. Preservação: 523 JSON históricos conferidos/restaurados por SHA256, 17 saídas novas arquivadas separadamente; dumps originais QA inalterados. Matriz mantém 3.555 requisitos únicos e 49 colunas. Pint, sintaxe do teste alterado e diff-check aprovados. Sem push, publicação, migração/escrita activa ou alteração de permissões activas. Estas provas não substituem a conferência visual autenticada nem encerram requisitos transversais; pendências anteriores conservadas. Recibo: `docs/investmoz/unification/main-local-database-qa-20261104.json`; resultados por teste, tentativas iniciais, executores e JUnit em `outputs/main-db-qa-20261104/`. + + +### 04/11/2026 — conferência visual da main na QA isolada e correcção do CSV provincial + +Percurso executado no Edge em `http://127.0.0.2:18644`, base exclusiva `main_visual_20261104_test` no cluster QA `127.0.0.1:55441`, com actor e conteúdos sintéticos. Autenticação Fortify normal com MFA: editar, guardar v2, recarregar e conferir os valores; aprovar v3; guardar novo rascunho v4; terminar sessão e conferir publicamente apenas v3, sem o marcador do rascunho. Histórico v1–v4 preservado. Esta prova conclui o recorte visual local de Competitividade; não equivale a execução no portal remoto ou aceitação da equipa em dados reais. + +Filtro apenas por Maputo Província, com sector Agronegócio: cinco oportunidades existentes mais quatro fixtures sintéticas nos níveis província/distrito/posto/localidade resultaram em nove na grelha, lista, mapa e CSV. Distrito de Matola apresentou três; regressar a toda a província recuperou nove e limpou distrito/posto/localidade. Nenhuma coordenada criada: mapa informa zero locais e nove sem coordenadas. Hash das oportunidades anteriores idêntico antes/depois. Limite administrativo desta província indisponível na QA, explicitamente indicado; cobertura cartográfica transversal continua pendente. + +Detectada e corrigida divergência no CSV: coluna Província vazia apesar da localização canónica/contexto de corredor disponível. Exportação reutiliza a apresentação territorial existente, conserva as oito primeiras colunas e o escape contra fórmulas, acrescentando Localização e âmbito. Contexto do corredor indica expressamente que distrito/posto não foram indicados; não grava nem inventa localização. CSV descarregado pela interface após correcção: nove linhas, todas com província e âmbito coerentes. + +Delta aprovado: 17 testes de base de dados, 609 asserções; PHPStan sem erros e Pint aprovado. Provas anteriores de 1.325 testes/28.377 asserções preservadas, sem repetição integral. Recibo com hashes e capturas: `docs/investmoz/unification/main-local-visual-qa-20261104.json`; artefactos em `outputs/main-visual-qa-20261104/`. Apenas main local e QA; sem push, escrita activa ou alteração de permissões activas. Conferência dos filtros no portal de destino e pendências gerais ACT-02/V2-07/V2-10 continuam abertas. diff --git a/docs/investmoz/v2/auditoria-portal/backlog.md b/docs/investmoz/v2/auditoria-portal/backlog.md index 7ccebf60..df832e8e 100644 --- a/docs/investmoz/v2/auditoria-portal/backlog.md +++ b/docs/investmoz/v2/auditoria-portal/backlog.md @@ -3406,3 +3406,14 @@ A pesquisa rápida já entregue permanece demonstrada; não foi substituída por ### 03/11/2026 — AUD-COUNTRY-02 e AUD-PROFILE-25: reutilização de país com pré-visualização e confirmação repetível [ORGANIZATION-COUNTRY-REUSE-20261103] Reutilização explícita do país organizacional com consulta/pré-visualização, confirmação, recibo repetível cifrado e fonte/versões preservadas. Guarda edições e bloqueia acções concorrentes da mesma declaração enquanto o pedido não tem confirmação. Representação de leitura actual e propriedade conferidas também no replay; snapshots/submissões anteriores não são reescritos.29 Feature/364 asserções (seis novos e23 anteriores),79 frontend (dez novos e69 anteriores),PHPStan/Pint/tipos/formato/lint/build aprovados.79 tabelas QA vazias, constraint da falha removida por rollback e índice de representação intacto. Dois processos reais aguardaram o mesmo lock e receberam o mesmo resultado/v2, com uma cópia cifrada e um recibo.89 recibos/411 provas preservados;16 provas novas. Recibo organization_country_reuse_increment_20261103. Parcial local,zero escrita/migração activa. + + +### 04/11/2026 — conferência visual da main na QA isolada e correcção do CSV provincial + +Percurso executado no Edge em `http://127.0.0.2:18644`, base exclusiva `main_visual_20261104_test` no cluster QA `127.0.0.1:55441`, com actor e conteúdos sintéticos. Autenticação Fortify normal com MFA: editar, guardar v2, recarregar e conferir os valores; aprovar v3; guardar novo rascunho v4; terminar sessão e conferir publicamente apenas v3, sem o marcador do rascunho. Histórico v1–v4 preservado. Esta prova conclui o recorte visual local de Competitividade; não equivale a execução no portal remoto ou aceitação da equipa em dados reais. + +Filtro apenas por Maputo Província, com sector Agronegócio: cinco oportunidades existentes mais quatro fixtures sintéticas nos níveis província/distrito/posto/localidade resultaram em nove na grelha, lista, mapa e CSV. Distrito de Matola apresentou três; regressar a toda a província recuperou nove e limpou distrito/posto/localidade. Nenhuma coordenada criada: mapa informa zero locais e nove sem coordenadas. Hash das oportunidades anteriores idêntico antes/depois. Limite administrativo desta província indisponível na QA, explicitamente indicado; cobertura cartográfica transversal continua pendente. + +Detectada e corrigida divergência no CSV: coluna Província vazia apesar da localização canónica/contexto de corredor disponível. Exportação reutiliza a apresentação territorial existente, conserva as oito primeiras colunas e o escape contra fórmulas, acrescentando Localização e âmbito. Contexto do corredor indica expressamente que distrito/posto não foram indicados; não grava nem inventa localização. CSV descarregado pela interface após correcção: nove linhas, todas com província e âmbito coerentes. + +Delta aprovado: 17 testes de base de dados, 609 asserções; PHPStan sem erros e Pint aprovado. Provas anteriores de 1.325 testes/28.377 asserções preservadas, sem repetição integral. Recibo com hashes e capturas: `docs/investmoz/unification/main-local-visual-qa-20261104.json`; artefactos em `outputs/main-visual-qa-20261104/`. Apenas main local e QA; sem push, escrita activa ou alteração de permissões activas. Conferência dos filtros no portal de destino e pendências gerais ACT-02/V2-07/V2-10 continuam abertas. diff --git a/tests/Feature/ProvinceSearchCompatibilityTest.php b/tests/Feature/ProvinceSearchCompatibilityTest.php index 52152cc8..6820a202 100644 --- a/tests/Feature/ProvinceSearchCompatibilityTest.php +++ b/tests/Feature/ProvinceSearchCompatibilityTest.php @@ -46,6 +46,23 @@ class ProvinceSearchCompatibilityTest extends TestCase return ['/api/v1/investment/search?'.$query, '/api/v1/opportunities/search?'.$query]; } + private function csvRows(string $csv): array + { + $stream = fopen('php://memory', 'w+'); + fwrite($stream, $csv); + rewind($stream); + $headers = fgetcsv($stream, null, ';', '"', ''); + $headers[0] = ltrim($headers[0], "\xEF\xBB\xBF"); + $this->assertSame(['Referência', 'Título', 'Sector', 'Província', 'Montante', 'Moeda', 'Maturidade', 'URL', 'Localização e âmbito'], $headers); + $rows = []; + while (($row = fgetcsv($stream, null, ';', '"', '')) !== false) { + $rows[] = array_combine($headers, $row); + } + fclose($stream); + + return $rows; + } + public function test_a_single_province_includes_all_lower_levels_and_linked_locations_in_both_apis(): void { $geo = app(InvestmentGeography::class); @@ -75,7 +92,14 @@ class ProvinceSearchCompatibilityTest extends TestCase } } } - foreach ($records as $record) { + $csvRows = $this->csvRows($this->get('/api/v1/investment/export?'.http_build_query(['province_id' => $path[0]->id, 'q' => $prefix]))->assertOk()->streamedContent()); + $this->assertCount(5, $csvRows); + $byReference = array_column($csvRows, null, 'Referência'); + foreach ($records as $index => $record) { + $csvRow = $byReference[$record->reference]; + $this->assertSame($path[0]->name, $csvRow['Província']); + $this->assertSame(implode(' / ', array_column(array_slice($path, 0, min($index + 1, 4)), 'name')), $csvRow['Localização e âmbito']); + $this->assertSame($record->province, $record->fresh()->province); $this->assertNull($record->fresh()->district); $this->assertSame($record->primary_territory_id, $record->fresh()->primary_territory_id); } @@ -159,6 +183,13 @@ class ProvinceSearchCompatibilityTest extends TestCase foreach (Opportunity::whereIn('id', $expected)->get() as $row) { $this->assertStringContainsString($row->title, $csv); } + $csvRows = $this->csvRows($csv); + $this->assertCount(5, $csvRows); + foreach ($csvRows as $row) { + $this->assertStringContainsString('Maputo Província', $row['Província']); + $this->assertStringContainsString('Maputo Cidade', $row['Província']); + $this->assertStringContainsString('contexto provincial do corredor; distrito e posto administrativo não indicados', $row['Localização e âmbito']); + } foreach ($this->requests(['sector' => 'turismo', 'province_id' => $provinceId]) as $url) { $response = $this->getJson($url)->assertOk(); foreach ($response->json('data') as $row) {