fix: preserve territorial scope in catalogue CSV and record local visual QA

This commit is contained in:
2026-11-04 10:49:22 +02:00
parent dc0bbd74d5
commit 6dde36b5ca
7 changed files with 225 additions and 7 deletions
@@ -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);
+18 -3
View File
@@ -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<string>} */
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<string,mixed> $territoryFilters
@@ -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.
@@ -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
}
}
}
@@ -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.
@@ -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.
@@ -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) {