fix: keep public catalogue selectors independent of opportunity results

This commit is contained in:
2026-10-22 14:28:40 +02:00
parent 7b1c35793c
commit 1d921c9d03
7 changed files with 176 additions and 22 deletions
+19 -19
View File
@@ -231,16 +231,25 @@ class InvestmentCatalogue
} elseif (in_array($relation, ['clusters', 'corridors'], true)) {
$query->whereIn('id', $this->publicStructureIds($relation, $foreign));
} else {
$query->whereIn('id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign));
$query->whereIn('id', app(OpportunityClassification::class)->targets('project')->select('id'));
}
}
if (! $management && $path !== [] && $relation !== 'locations') {
$dossiers = InvestmentPortfolioController::visible();
app(InvestmentGeography::class)->constrainDossiersToTerritories($dossiers,
app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id));
$query->whereIn('id', $dossiers->whereNotNull($foreign)->select($foreign));
$territories = app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id);
$query->where(fn ($area) => $area->whereIn('primary_territory_id', $territories)
->orWhereIn('id', $dossiers->whereNotNull($foreign)->select($foreign)));
}
$result[$relation] = $query->orderBy('name')->get(['id', 'name']);
if (! $management && $relation !== 'locations') {
$dossiers = InvestmentPortfolioController::visible()->whereIn($foreign, $result[$relation]->pluck('id'))
->orderBy('id')->get(['id', $foreign])->unique($foreign)->keyBy($foreign);
$result[$relation]->each(function ($record) use ($dossiers): void {
$record->url = isset($dossiers[$record->id]) ? '/carteira/'.$dossiers[$record->id]->id : null;
});
}
}
$zones = app(InvestmentZones::class)->visible();
@@ -250,24 +259,15 @@ class InvestmentCatalogue
['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'geo_location_id']));
}
$result['zones'] = $zones->orderBy('name')->get(['id', 'name', 'category']);
if (! $management && ($path !== [] || ! empty($territoryFilters['sector']))) {
$scope = \Illuminate\Support\Arr::only($territoryFilters, ['sector','province_id','district_id','admin_post_id','locality_id','include_national']);
$available = ['chain' => [], 'activity' => []];
$this->query($scope)->reorder()->chunkById(200, function ($records) use (&$available) {
foreach ($records as $record) {
foreach (app(OpportunityClassification::class)->publicRows($record) as $row) {
if (isset($available[$row['target_type']])) $available[$row['target_type']][] = $row['target_id'];
}
foreach (['subsector_id','economic_activity_id'] as $key) {
$id = $record->workflow_context['publication']['taxonomy'][$key] ?? null;
if ($id) $available['activity'][] = $id;
}
}
}, 'opportunities.id', 'id');
foreach ($available as $kind => $ids) {
$result['classification']['targets'][$kind] = collect($result['classification']['targets'][$kind])->whereIn('id', $ids)->values();
}
if (! $management && $path !== []) {
// A chain's documented scope comes from its public cluster, never
// from the opportunities surviving the current search.
$ids = app(OpportunityClassification::class)->targets('chain')
->whereIn('cluster_id', $result['clusters']->pluck('id'))->pluck('id');
$result['classification']['targets']['chain'] = collect($result['classification']['targets']['chain'])->whereIn('id', $ids)->values();
}
// Activities are a public classification catalogue, not evidence of
// investment availability in a territory or sector.
return $result;
}
@@ -2204,3 +2204,18 @@ Integração Git por avanço directo até `1ec31856`, incluindo `9ac623a4` e rec
Decisão expressa da Tempo: o rodapé deve apresentar **comercial@tempo.co.mz**, porque o contacto comercial com a instituição é feito por esse endereço. Actualizado o valor por omissão de `portal.institutional_contact_email`. Na homologação existia uma sobreposição explícita `PORTAL_INSTITUTIONAL_CONTACT_EMAIL=info@tempo.co.mz`: alterada exclusivamente essa chave para `comercial@tempo.co.mz`, com backup privado íntegro, propriedade/permissões preservadas e reconstrução da cache de configuração. A apresentação e o `mailto:` utilizam a mesma configuração existente; não houve envio de email.
Recibos de destino e integração: `outputs/investmoz-territory-recovery-20261022/revision-before-integration.log`, `footer-email-receipt.log` e recibo final de instalação no mesmo directório do workspace de entrega. A confirmação final deve identificar a revisão efectivamente instalada pelo processo automático, mantendo os 30 resumos e as regras fiscais existentes.
## Selectores especializados do catálogo — QA, 22/10/2026
Pedido da Tempo: explorar estruturas públicas independentemente da existência de oportunidades, sem inventar associações nem divulgar registos privados. A homologação em `7b1c3579` já devolve e apresenta **Parque Industrial de Balama** e **Parque Industrial de Montepuez** no selector de Cabo Delgado; “Todos” é o valor inicial, não uma prova de ausência das opções. A API devolve duas zonas nessa província e oito no âmbito nacional. A base QA local tem outro conjunto de dados: não continha parques públicos ligados a essa província. Esta diferença de dados não é uma regressão demonstrada do selector da homologação.
Falhas verificadas no código: actividades e cadeias eram reduzidas às oportunidades publicadas encontradas no território/sector; estruturas públicas podiam depender da existência adicional de uma ficha publicada da carteira; os selectores vazios só apresentavam “Todos”; a consulta da ficha de um parque sem oportunidades não era apresentada no resultado vazio da grelha.
Correcção na branch `codex/catalogue-specialised-selectors-QA`: opções provenientes dos cadastros públicos existentes, preservando os critérios de consulta e os vínculos territoriais. Estruturas com território principal documentado não dependem de uma oportunidade nem de uma ficha adicional da carteira. Mantêm-se os vínculos territoriais já resolvidos pelas fichas publicadas. Cadeias acompanham os clusters públicos do âmbito seleccionado. Actividades são o catálogo público de classificação, explicitamente separado da disponibilidade local de investimentos; não se inventa uma associação territorial ou sectorial. Os oito selectores apresentam contagem ou explicação de ausência. Uma selecção indisponível permanece identificada, com possibilidade de remoção, em vez de parecer “Todos”.
Quando a estrutura seleccionada não tem correspondências, o resultado informa “Sem oportunidades publicadas associadas com estes filtros” e mantém ligação à ficha pública existente, em qualquer vista. Um registo mestre sem ficha pública detalhada é identificado como tal; não se cria uma ficha ou publicação automaticamente. A pesquisa continua a combinar todos os filtros, sem alargamento automático ou atribuição de benefícios. A alteração territorial mantém a limpeza dos níveis e selecções incompatíveis já implementada.
Provas novas, sem repetir os percursos anteriormente aceites: Cabo Delgado com dois parques sintéticos identificados; selecção de Balama, zero oportunidades, abertura da ficha, regresso e recarregamento com selecção conservada; limpeza; Nampula com parque sintético Topuito; ausência de restrição territorial com ambos os conjuntos disponíveis e selecção anterior limpa. Fixtures limitadas à base `simulator_fiscal_delta_20261021_test`, porta 55441, excluídas do Git e da disponibilização. Testes `CatalogueSelectorReferencesTest`: três testes, 27 verificações, transacções revertidas, incluindo exclusão de privados/outro território, catálogo de actividades independente dos resultados, estruturas/cadeia sem oportunidades ou fichas adicionais. `npm run types:check` e build QA passaram. Provas locais: `outputs/catalogue-selectors-20261022/audit.json`, `qa-balama.png`, `qa-nampula.png`, `qa-national.png` e logs de build/tipos no mesmo directório.
Diagnóstico separado do mapa: API pública de Cabo Delgado devolve território cadastrado, zero limites e nenhuma geometria acessível. Leitura SSH em transacção **READ ONLY** confirmou zero linhas em `geo_geometries` para essa província; neste caso não é uma camada existente ocultada pelo perfil. Não foram criadas coordenadas, geometrias, vínculos ou permissões. Este diagnóstico não equivale à implementação dos limites administrativos.
Integração pendente da revisão prevista: comparar com `origin/main`, preservar alterações concorrentes e integrar pelo processo Git automático existente. Não há migrations, publicação de oportunidades, activação fiscal ou transporte das fixtures. Conferir apenas o delta e a revisão servida depois de autorizada a integração; não repetir as provas já aceites.
@@ -4681,7 +4681,7 @@ Confirmação manual declarada pelo responsável; sem captura automática nova.
Reutilização por mesmo consumidor/ação/campo/resultado e destinoQA, não por semelhança ou nome de ficheiro. Novos ramos/dados, licenças e obrigações identificados separadamente. Tentativa automáticaC135 conservada como histórico, não saldo global.","2–8 h de reconciliação/ensaio por consumidor agrupado, pressupondo dados sintéticos/perfis preparados. Alteração: estimar após reprodução concreta; variante integral pode exceder esta faixa.",,,
AUD-REL-01,Preservar relações territoriais e citar por associação,"Pedido direto 09/10/2026, auditoria §1–4; fontes específicas descritas em relatorio-executivo.md",cartografia/relações,/management/relationship-repair; fichas de projetos/clusters,"InvestmentGeography, campos TerritoryFields/ProvinceSelect, geo_locations/geo_geometries e importador autenticado. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/reused-evidence-integrity.json: 35 recibos anteriores com hashes correspondentes; o âmbito de cada recibo permanece limitado ao critério ensaiado. outputs/investmoz-audit-current-20261012/http-promoter.json; http-validator.json; http-publisher.json; http-manager.json.,Implementado parcialmente,Associações/citações por entidade e território ainda por demonstrar; não inferir Turismo→geo nem proximidade→identidade. Preservar relações existentes.,"QA408/581INE; ativo0geometrias;173correspondências por nome/edição/pai, Mandie ambíguo. Publicação é ação separada. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.",AUD-REL-01: 28 locais e 13 relações mantidos; citações e papéis conservados; conflito/idempotência/reversão,P1,AUD-REL-01: Relatórios históricos mantidos; estado ativo apurado por leitura nesta auditoria,AUD-REL-01,1,"[{""id"":""AUD-REL-01"",""pedido"":""Preservar relações territoriais e citar por associação"",""fontes"":[""Pedido direto 09/10/2026, auditoria §1–4; fontes específicas descritas em relatorio-executivo.md""],""aceitacao"":""28 locais e 13 relações mantidos; citações e papéis conservados; conflito/idempotência/reversão"",""estado"":""parcial"",""variantes"":""Relatórios históricos mantidos; estado ativo apurado por leitura nesta auditoria"",""modulo"":""cartografia/relações""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,geo_locations: 1990; geo_geometries: 0; project_locations: 28; cluster_projects: 13; opportunity_projects: 0; opportunity_clusters: 0,"Capacidades/provas específicas anteriores existem, mas o requisito agregado ainda tem os critérios restantes explicitados; fechos parciais preservados.
[RECONCILIACAO-43-20261019] 23: 1921 referências e zero geometrias no destino. Reconciliar geometrias/fontes/classificação existentes em QA e consumidores abertos; corrigir bug de2. Não transformar divergência de contagens em importação. | 32: Prova positiva no destino exige dossier real autorizado; dependência de verificação/dados, não reconstrução. Ligação documental por referência adicional em39. | 36: Integrar revisão660a25e37e1e e títulos/reutilização de hierarquia, preservando fontes/estados e conflitos. Sem migrations.","Associações/citações por entidade e território ainda por demonstrar; não inferir Turismo→geo nem proximidade→identidade. Preservar relações existentes.
[RECONCILIACAO-43-20261019] 23: 1921 referências e zero geometrias no destino. Reconciliar geometrias/fontes/classificação existentes em QA e consumidores abertos; corrigir bug de2. Não transformar divergência de contagens em importação. | 32: Prova positiva no destino exige dossier real autorizado; dependência de verificação/dados, não reconstrução. Ligação documental por referência adicional em39. | 36: Integrar revisão660a25e37e1e e títulos/reutilização de hierarquia, preservando fontes/estados e conflitos. Sem migrations. | 22/10/2026 — selectores especializados em QA: opções de cadastros públicos independentes das oportunidades; território principal documentado ou vínculos já publicados; cadeia pelo cluster e actividade como classificação pública. Vazio explicado; zero correspondências mantém ficha pública existente. Cabo Delgado/Nampula/nacional, selecção/limpeza/ficha/regresso/recarregamento; fixtures exclusivamente QA. CatalogueSelectorReferencesTest: 3 testes, 27 verificações; tipos/build passaram. Homologação já oferece Balama/Montepuez; a base local tinha dados distintos. Sem publicação/activação ou integração nesta execução. Manual: secção Selectores especializados do catálogo; provas outputs/catalogue-selectors-20261022.","Associações/citações por entidade e território ainda por demonstrar; não inferir Turismo→geo nem proximidade→identidade. Preservar relações existentes.
[UTILIZACAO-HOMOLOGACAO-20261019] 23: Implementação parcial + dados — Consumidores cartográficos restantes e correspondências geográficas por item; diferença de contagens não identifica registos em falta. | 32: Implementado no recorte; falta processo real — Recuperação no clone aceite; vínculo positivo no destino depende de dossier legítimo autorizado. | 36: Disponibilidade condicionada conforme limite registado — Harmonização territorial apenas QA, expressamente excluída da revisão autorizada.","InvestmentGeography, campos TerritoryFields/ProvinceSelect, geo_locations/geo_geometries e importador autenticado","QA408/581INE; ativo0geometrias;173correspondências por nome/edição/pai, Mandie ambíguo. Publicação é ação separada.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Completar critérios restantes sem repetir provas válidas,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: território. Esforço de código adicional só após lacuna reproduzida.,"outputs/investmoz-audit-current-20261012/reused-evidence-integrity.json: 35 recibos anteriores com hashes correspondentes; o âmbito de cada recibo permanece limitado ao critério ensaiado. outputs/investmoz-audit-current-20261012/http-promoter.json; http-validator.json; http-publisher.json; http-manager.json. | Homologação teste-investidor 19/10/2026: Cartografia e consumidores territoriais restantes=Não demonstrado; Vínculo positivo e recuperação do dossier=Não demonstrado; Onde se localiza / associações territoriais e produtivas=Ausente. Saldo derivado e limites em outputs/investmoz-disponibilizacao-integral-20261019/SALDO-E-ACTUALIZACAO-CONSOLIDADA.md. Não substitui estado/provas do activo; trecho documental encerrado, vínculo dossier aberto. | Proposta integral fdcb31f1b5b9: aplicação activa local completa + R2/DOC01/destino reconciliados; schema/build/recuperação de contexto e reversão de apresentação ensaiados no clone isolado. Não instalado remotamente. Inc. QA territoriais/Inhambane/programas separados. Limites e decisões em outputs/investmoz-disponibilizacao-integral-20261019/PROPOSTA-INTEGRAL-PARA-DECISAO.md. | Execução autorizada fdcb31f1b5b9 concluída em teste-investidor: Cartografia e consumidores territoriais restantes=Não demonstrado; Vínculo positivo e recuperação do dossier=Não demonstrado; Onde se localiza / associações territoriais e produtivas=Ausente. 1080 hashes iguais, schema171/88, custódia/scan/recuperação reais. Sem publicação ou papéis novos. Vínculo dossier legítimo pendente; incrementos QA separados. Limites em outputs/investmoz-disponibilizacao-integral-20261019/RESULTADO-DISPONIBILIZACAO-INTEGRAL.md; não encerra critérios globais não demonstrados.
@@ -4698,7 +4698,7 @@ AUD-REL-01,Preservar relações territoriais e citar por associação,"Pedido di
[Executável c11a0c74766c] P3 staging privado completo; P4 nominal destino real 27 refs/25 territórios, zero geometrias. Pré-condições fontes conferidas; pacote para decisão, não aplicado. Reversão após uso só manifesto/build, backend e dados retidos. Adaptador ensaiado só em clone: rollback/recuperação após commit e repetição sem duplicações; aplicação 56 fontes inalterada. Acesso público e diferenças visuais/login continuam abertos. Saldo remoto delimitado 4–7 h; sem alterações remotas.
[Executável 82dab3719ffc] P3 staging privado completo; P4 nominal destino real 27 refs/25 territórios, zero geometrias. Pré-condições fontes conferidas; pacote para decisão, não aplicado. Reversão após uso só manifesto/build, backend e dados retidos. Adaptador ensaiado só em clone: rollback/recuperação após commit e repetição sem duplicações; aplicação 56 fontes inalterada. Acesso público e diferenças visuais/login continuam abertos. Saldo remoto delimitado 4–7 h; sem alterações remotas.
[Executável 312965f2c653] P3 staging privado completo; P4 nominal destino real 27 refs/25 territórios, zero geometrias. Pré-condições fontes conferidas; pacote para decisão, não aplicado. Reversão após uso só manifesto/build, backend e dados retidos. Adaptador ensaiado só em clone: rollback/recuperação após commit e repetição sem duplicações; aplicação 56 fontes inalterada. Acesso público e diferenças visuais/login continuam abertos. Saldo remoto delimitado 4–7 h; sem alterações remotas.
[Homologação 312965f2c653 executada] Autorização Tempo; 30 fichas privadas/30 locais/31 suportes/11 relações/2 originais e 2 definições internas, 233 caminhos conferidos. Dados anteriores/permissões preservados; zero publicação/migrations/geometrias/P7. Saldo delimitado de execução concluído. Dossier legítimo, oito lacunas e questões de autenticação/visual separadas. RECIBO-EXECUCAO-312965.json.",território,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Pontes de rastreabilidade em falta; não inferir pares oportunidade–projeto/cluster,Acervo aceite; recuperar citação de cada par,InvestmentRelationshipRepair; InvestmentTerritorialTraceability,Leitura ativa e suite territorial; confirmação histórica V2,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"2–8 h de reconciliação/ensaio por consumidor agrupado, pressupondo dados sintéticos/perfis preparados. Alteração: estimar após reprodução concreta; variante integral pode exceder esta faixa.",,,
[Homologação 312965f2c653 executada] Autorização Tempo; 30 fichas privadas/30 locais/31 suportes/11 relações/2 originais e 2 definições internas, 233 caminhos conferidos. Dados anteriores/permissões preservados; zero publicação/migrations/geometrias/P7. Saldo delimitado de execução concluído. Dossier legítimo, oito lacunas e questões de autenticação/visual separadas. RECIBO-EXECUCAO-312965.json. | 22/10/2026 — selectores especializados em QA: opções de cadastros públicos independentes das oportunidades; território principal documentado ou vínculos já publicados; cadeia pelo cluster e actividade como classificação pública. Vazio explicado; zero correspondências mantém ficha pública existente. Cabo Delgado/Nampula/nacional, selecção/limpeza/ficha/regresso/recarregamento; fixtures exclusivamente QA. CatalogueSelectorReferencesTest: 3 testes, 27 verificações; tipos/build passaram. Homologação já oferece Balama/Montepuez; a base local tinha dados distintos. Sem publicação/activação ou integração nesta execução. Manual: secção Selectores especializados do catálogo; provas outputs/catalogue-selectors-20261022.",território,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Pontes de rastreabilidade em falta; não inferir pares oportunidade–projeto/cluster,Acervo aceite; recuperar citação de cada par,InvestmentRelationshipRepair; InvestmentTerritorialTraceability,Leitura ativa e suite territorial; confirmação histórica V2,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"2–8 h de reconciliação/ensaio por consumidor agrupado, pressupondo dados sintéticos/perfis preparados. Alteração: estimar após reprodução concreta; variante integral pode exceder esta faixa.",,,
AUD-REL-02,Correspondência das seis oportunidades com âncoras/clusters,"Pedido direto 09/10/2026, auditoria §1–4; fontes específicas descritas em relatorio-executivo.md",cartografia/relações,/management/opportunities/{id}/context,"InvestmentGeography, campos TerritoryFields/ProvinceSelect, geo_locations/geo_geometries e importador autenticado. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/reused-evidence-integrity.json: 35 recibos anteriores com hashes correspondentes; o âmbito de cada recibo permanece limitado ao critério ensaiado. outputs/investmoz-audit-current-20261012/http-promoter.json; http-validator.json; http-publisher.json; http-manager.json.,Dependente de decisão concreta da Tempo,Os pares das seis oportunidades legadas com projetos/âncoras/clusters continuam indeterminados. Indicar pares com fundamento documental; não bloquear as oportunidades independentes.,"QA408/581INE; ativo0geometrias;173correspondências por nome/edição/pai, Mandie ambíguo. Publicação é ação separada. Os pares das seis oportunidades legadas com projetos/âncoras/clusters continuam indeterminados. Indicar pares com fundamento documental; não bloquear as oportunidades independentes.",AUD-REL-02: Somente pares explicitamente identificados pelo responsável/documento; sem duplicar relações,P1,AUD-REL-02: Relatórios históricos mantidos; estado ativo apurado por leitura nesta auditoria,AUD-REL-02,1,"[{""id"":""AUD-REL-02"",""pedido"":""Correspondência das seis oportunidades com âncoras/clusters"",""fontes"":[""Pedido direto 09/10/2026, auditoria §1–4; fontes específicas descritas em relatorio-executivo.md""],""aceitacao"":""Somente pares explicitamente identificados pelo responsável/documento; sem duplicar relações"",""estado"":""dependente de decisão concreta"",""variantes"":""Relatórios históricos mantidos; estado ativo apurado por leitura nesta auditoria"",""modulo"":""cartografia/relações""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,dependente de decisão concreta,geo_locations: 1990; geo_geometries: 0; project_locations: 28; cluster_projects: 13; opportunity_projects: 0; opportunity_clusters: 0,"Decisão concreta isolada explicitada; não bloqueia as frentes independentes já autorizadas.
[RECONCILIACAO-43-20261019] 23: 1921 referências e zero geometrias no destino. Reconciliar geometrias/fontes/classificação existentes em QA e consumidores abertos; corrigir bug de2. Não transformar divergência de contagens em importação.","Os pares das seis oportunidades legadas com projetos/âncoras/clusters continuam indeterminados. Indicar pares com fundamento documental; não bloquear as oportunidades independentes.
@@ -10639,7 +10639,7 @@ AUD-FINTECH-01,"Caracterizar e acompanhar investimento Fintech no mesmo dossier/
[RECONCILIACAO-43-20261019] Código e critérios confrontados com leitura READ ONLY fresca; PROVA-RECONCILIACAO-43-LEITURA.json, RECONCILIACAO-43-QA-FONTES.json, RECONCILIACAO-43-SCHEMA-QA.json. Não encerra critério funcional por inventário.
[PLANO-P1-P6-20261019] Plano Tempo: 27–46 h disponibilização; P7 e desenvolvimento separados. Inclusão 36–39 expressa para decisão; sem publicação automática. Revisão de proposta d87970e701f0. Roteiro no manual existente; não encerra critério por instalação.",Setores e Fintech — QA 15/10/2026,15/10/2026; preserva fechos e B.,,,,,,,,,,"Cenários sintéticos, não comprovam licença institucional/atividade financeira real ou correspondência jurídica específica; 12 critérios aceites não reabertos.",,,,
AUD-SEARCH-T01,Consistência transversal da pesquisa e navegação — Oportunidades,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/oportunidades,"investment/catalogue — Filtro único, Mais filtros, preparação explícita, moeda obrigatória para montantes, pedidos canceláveis, estados e contexto únicos.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,"6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado.",QA — delta delimitado; revisão Tempo pendente,Revisão visual da Tempo; NLP separado.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Filtros duplicados, códigos internos, maturidades cruzadas, contagens concorrentes e mapa em grelha.",Revisão visual da Tempo; NLP separado.,investment/catalogue; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,"6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado.",Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Revisão visual da Tempo; NLP separado.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
AUD-SEARCH-T01,Consistência transversal da pesquisa e navegação — Oportunidades,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/oportunidades,"investment/catalogue — Filtro único, Mais filtros, preparação explícita, moeda obrigatória para montantes, pedidos canceláveis, estados e contexto únicos.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,"6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado.",QA — delta delimitado; revisão Tempo pendente,Revisão visual da Tempo; NLP separado.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Filtros duplicados, códigos internos, maturidades cruzadas, contagens concorrentes e mapa em grelha. | 22/10/2026 — selectores especializados em QA: opções de cadastros públicos independentes das oportunidades; território principal documentado ou vínculos já publicados; cadeia pelo cluster e actividade como classificação pública. Vazio explicado; zero correspondências mantém ficha pública existente. Cabo Delgado/Nampula/nacional, selecção/limpeza/ficha/regresso/recarregamento; fixtures exclusivamente QA. CatalogueSelectorReferencesTest: 3 testes, 27 verificações; tipos/build passaram. Homologação já oferece Balama/Montepuez; a base local tinha dados distintos. Sem publicação/activação ou integração nesta execução. Manual: secção Selectores especializados do catálogo; provas outputs/catalogue-selectors-20261022.",Revisão visual da Tempo; NLP separado.,investment/catalogue; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,"6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado. | 22/10/2026 — selectores especializados em QA: opções de cadastros públicos independentes das oportunidades; território principal documentado ou vínculos já publicados; cadeia pelo cluster e actividade como classificação pública. Vazio explicado; zero correspondências mantém ficha pública existente. Cabo Delgado/Nampula/nacional, selecção/limpeza/ficha/regresso/recarregamento; fixtures exclusivamente QA. CatalogueSelectorReferencesTest: 3 testes, 27 verificações; tipos/build passaram. Homologação já oferece Balama/Montepuez; a base local tinha dados distintos. Sem publicação/activação ou integração nesta execução. Manual: secção Selectores especializados do catálogo; provas outputs/catalogue-selectors-20261022.",Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Revisão visual da Tempo; NLP separado.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
AUD-SEARCH-T02,Consistência transversal da pesquisa e navegação — Negócios,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/negocios,"investment/business-marketplace — Sector básico, critérios especializados recolhidos em Mais filtros, estado de auditoria retirado do controlo público, contagem identificada e filtros removíveis.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: universo results.meta.total=0; selector partilhado e código inspeccionados.,QA — delta delimitado; revisão Tempo pendente,Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Classificação técnica visível, estados de auditoria e contagem territorial sem âmbito.",Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote.,investment/business-marketplace; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: universo results.meta.total=0; selector partilhado e código inspeccionados.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
AUD-SEARCH-T03,Consistência transversal da pesquisa e navegação — Sectores,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/sectores; /sectores/{slug},portal/services — Contagem identificada como Sectores; navegação comum preserva a entrada por sector e Fintech como especialização.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,"Inspecção de PublicPortalController, SectorOrganization e portalNavigation; testes de navegação.",QA — delta delimitado; revisão Tempo pendente,Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Navegação relacionada dispersa e contagens genéricas sem nome do universo.,Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro.,portal/services; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,"Inspecção de PublicPortalController, SectorOrganization e portalNavigation; testes de navegação.",Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
AUD-SEARCH-T04,Consistência transversal da pesquisa e navegação — Localizações,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/oportunidades?view=map,investment/catalogue — Mesmos filtros e permissões; locais com coordenadas e oportunidades sem coordenadas apresentados separadamente; fichas de contexto identificadas.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,Teste de associação/lista/exportação; mapa do serviço exclui proximidade e preserva oportunidade sem coordenadas.,QA — delta delimitado; revisão Tempo pendente,Nenhuma coordenada ou geometria criada.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mapa e contagem de oportunidades podiam usar universos diferentes sem explicação.,Nenhuma coordenada ou geometria criada.,investment/catalogue; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,Teste de associação/lista/exportação; mapa do serviço exclui proximidade e preserva oportunidade sem coordenadas.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Nenhuma coordenada ou geometria criada.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
Can't render this file because it is too large.
@@ -48,6 +48,16 @@ const labels: Record<string, string> = {
authority_id: 'Entidade',
publication_state: 'Critério herdado',
};
const specialisedEmpty: Record<string, string> = {
corridor_id: 'Não existem corredores públicos cadastrados para esta localização.',
cluster_id: 'Não existem clusters públicos cadastrados para esta localização.',
zee_id: 'Não existem ZEE públicas cadastradas para esta localização.',
zfi_id: 'Não existem ZFI públicas cadastradas para esta localização.',
park_id: 'Não existem parques industriais públicos cadastrados para esta localização.',
project_id: 'Não existem projectos âncora públicos cadastrados para esta localização.',
subsector_id: 'Não existem actividades disponíveis no catálogo público.',
chain_id: 'Não existem cadeias de valor públicas cadastradas para esta localização.',
};
export default function OpportunitySearchFilters({
data,
references,
@@ -75,6 +85,9 @@ export default function OpportunitySearchFilters({
<Field key={key} label={labels[key]}>
<select
className={input}
aria-label={labels[key]}
aria-describedby={specialisedEmpty[key] ? `filter-help-${key}` : undefined}
disabled={Boolean(specialisedEmpty[key]) && choices.length === 0 && !data[key]}
value={String(data[key] ?? '')}
onChange={(e) =>
change({
@@ -86,12 +99,23 @@ export default function OpportunitySearchFilters({
}
>
<option value="">Todos</option>
{data[key] && !choices.some((o) => o.value === String(data[key])) && (
<option value={String(data[key])}>Selecção indisponível neste contexto — remova o filtro</option>
)}
{choices.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
{specialisedEmpty[key] && (
<p id={`filter-help-${key}`} className="text-sm text-slate-600">
{choices.length === 0
? specialisedEmpty[key]
: `${choices.length} opções públicas disponíveis, mesmo sem oportunidades associadas.`}
{key === 'subsector_id' && choices.length > 0 && ' Classificação de actividades; não confirma a existência de investimentos nesta localização.'}
</p>
)}
{hint && <p className="text-sm text-slate-600">{hint}</p>}
</Field>
);
@@ -102,6 +102,16 @@ export default function Catalogue({
),
});
const view = String(filters.view ?? 'grid');
const selectedStructures = [
...(references.zones ?? []).filter((zone) =>
['park_id', 'zee_id', 'zfi_id'].some((key) => filters[key] === zone.id),
).map((zone) => ({ ...zone, url: `/zonas/${zone.id}` })),
...(['clusters', 'corridors', 'projects'] as const).flatMap((group) =>
references[group].filter((record) => filters[
{ clusters: 'cluster_id', corridors: 'corridor_id', projects: 'project_id' }[group]
] === record.id),
),
];
const [territoryPath, setTerritoryPath] = useState(territorySelection);
const [saved, setSaved] = useState(favourites);
const [selected, setSelected] = useState<number[]>([]);
@@ -362,6 +372,17 @@ export default function Catalogue({
total={results.total}
retry={() => runSearch()}
/>
{!searchLoading && !searchError && results.total === 0 && selectedStructures.length > 0 && (
<section className={card} aria-label="Estrutura seleccionada">
<p className="font-semibold">Sem oportunidades publicadas associadas com estes filtros.</p>
<p>A estrutura continua disponível para consulta. A sua selecção não confirma benefícios nem disponibilidade para investimento.</p>
{selectedStructures.map((record) => (
record.url ? <Link key={record.id} className="block underline" href={searchTerritoryHref(record.url, page.url)}>
Consultar ficha: {record.name}
</Link> : <p key={record.id}>{record.name} — sem ficha pública detalhada disponível.</p>
))}
</section>
)}
<div className="flex flex-wrap items-center gap-4">
{!searchLoading &&
!searchError &&
+1
View File
@@ -19,6 +19,7 @@ export type Option = {
export type Entity = {
id: string;
name: string;
url?: string | null;
dossiers?: { id: string; title: string; source: string }[];
geography?: StructureGeography | null;
};
@@ -0,0 +1,93 @@
<?php
namespace Tests\Feature;
use App\Services\InvestmentCatalogue;
use App\Services\InvestmentZones;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class CatalogueSelectorReferencesTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$db = DB::selectOne('select current_database() as name, inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $db->name);
$this->assertSame(55441, (int) $db->port);
$this->withoutVite();
}
public function test_public_parks_remain_selectable_with_zero_results_without_exposing_private_or_other_territories(): void
{
$province = '059b02ca-7095-577a-8705-4d4c46896747';
$template = app(InvestmentZones::class)->visible()->firstOrFail();
$ids = [];
foreach (['public', 'private', 'other'] as $case) {
$zone = $template->replicate();
$zone->id = (string) Str::uuid();
$zone->code = 'SINT-SELECTOR-'.Str::random(12);
$zone->name = 'SINTÉTICO QA '.$case;
$zone->category = 'industrial_park';
$zone->primary_territory_id = $case === 'other' ? '9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8' : $province;
$zone->publication_status = $case === 'private' ? 'draft' : 'published';
$zone->source_evidence = [['source_type' => 'government', 'url' => 'https://example.test/qa-only', 'supports' => 'Synthetic QA fixture, not a legal source']];
$zone->save();
$ids[$case] = $zone->id;
}
$scope = ['province_id' => $province, 'park_id' => $ids['public'], 'q' => 'no-matching-opportunity-'.Str::uuid()];
$this->assertSame(0, app(InvestmentCatalogue::class)->query($scope)->count());
$response = $this->getJson('/api/v1/investment/references?'.http_build_query($scope))->assertOk();
$available = array_column($response->json('zones'), 'id');
$this->assertContains($ids['public'], $available);
$this->assertNotContains($ids['private'], $available);
$this->assertNotContains($ids['other'], $available);
$this->assertContains($ids['other'], array_column($this->getJson('/api/v1/investment/references')->assertOk()->json('zones'), 'id'));
$this->get('/zonas/'.$ids['public'])->assertOk();
$this->get('/zonas/'.$ids['private'])->assertNotFound();
}
public function test_activity_catalogue_does_not_disappear_when_opportunity_search_has_no_matches(): void
{
$catalogue = app(InvestmentCatalogue::class);
$global = collect($catalogue->references()['classification']['targets']['activity'])->pluck('id')->all();
$this->assertNotEmpty($global);
foreach (['059b02ca-7095-577a-8705-4d4c46896747', '9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8'] as $province) {
$scope = ['province_id' => $province, 'q' => 'no-matching-opportunity-'.Str::uuid(), 'sector' => 'tourism'];
$this->assertSame(0, $catalogue->query($scope)->count());
$this->assertSame($global, collect($catalogue->references(false, $scope)['classification']['targets']['activity'])->pluck('id')->all());
}
}
public function test_public_master_structures_and_chain_are_available_without_opportunities_or_published_dossiers(): void
{
$province = '059b02ca-7095-577a-8705-4d4c46896747';
$clusters = [];
foreach (['public', 'internal'] as $visibility) {
$id = (string) Str::uuid();
DB::table('economic_clusters')->insert(['id' => $id, 'code' => 'SINT-'.Str::random(12), 'name' => 'SINTÉTICO QA cluster',
'sector' => 'QA', 'status' => 'active', 'data_classification' => $visibility, 'primary_territory_id' => $province]);
$clusters[$visibility] = $id;
}
$corridor = (string) Str::uuid();
DB::table('development_corridors')->insert(['id' => $corridor, 'code' => 'SINT-'.Str::random(12), 'name' => 'SINTÉTICO QA corredor',
'status' => 'active', 'data_classification' => 'public', 'primary_territory_id' => $province]);
$project = \App\Models\Project::factory()->create(['data_classification' => 'public', 'primary_territory_id' => $province]);
$chain = (string) Str::uuid();
DB::table('value_chains')->insert(['id' => $chain, 'cluster_id' => $clusters['public'], 'code' => 'SINT-'.Str::random(12),
'name' => 'SINTÉTICO QA cadeia', 'stage' => 'production', 'data_classification' => 'public']);
$refs = app(InvestmentCatalogue::class)->references(false, ['province_id' => $province, 'q' => 'no-matching-opportunity']);
$this->assertContains($clusters['public'], $refs['clusters']->pluck('id')->all());
$this->assertNotContains($clusters['internal'], $refs['clusters']->pluck('id')->all());
$this->assertContains($corridor, $refs['corridors']->pluck('id')->all());
$this->assertContains($project->id, $refs['projects']->pluck('id')->all());
$this->assertContains($chain, collect($refs['classification']['targets']['chain'])->pluck('id')->all());
$other = app(InvestmentCatalogue::class)->references(false, ['province_id' => '9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8']);
$this->assertNotContains($clusters['public'], $other['clusters']->pluck('id')->all());
$this->assertNotContains($chain, collect($other['classification']['targets']['chain'])->pluck('id')->all());
}
}