Compare commits

...
8 changed files with 732 additions and 33 deletions
@@ -4,6 +4,9 @@ namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Opportunity;
use App\Services\InvestmentCatalogue;
use App\Services\OpportunityMaturity;
use App\Services\SectorOrganization;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -15,6 +18,11 @@ class OpportunityController extends Controller
'q' => ['nullable', 'string', 'max:120'],
'sector' => ['nullable', 'string', 'max:80'],
'province' => ['nullable', 'string', 'max:80'],
'province_id' => ['nullable', 'uuid'],
'district_id' => ['nullable', 'uuid'],
'admin_post_id' => ['nullable', 'uuid'],
'locality_id' => ['nullable', 'uuid'],
'geo_location_id' => ['nullable', 'uuid'],
'maturity_level' => ['nullable', 'integer', 'between:1,6'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:50'],
'min_amount' => ['nullable', 'numeric', 'min:0', 'max:999999999999'],
@@ -47,22 +55,24 @@ class OpportunityController extends Controller
if (! empty($validated['sector'])) {
$sector = $validated['sector'];
\App\Services\SectorOrganization::constrain($query, $sector);
SectorOrganization::constrain($query, $sector);
}
if (! empty($validated['province'])) {
$query->where('province', $validated['province']);
$territory = array_intersect_key($validated, array_flip(['province', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'geo_location_id']));
if (array_filter($territory, fn ($value) => $value !== null && $value !== '') !== []) {
// Keep the legacy search's sorting and non-territorial filters while sharing the public territorial contract.
$query->whereIn('opportunities.id', app(InvestmentCatalogue::class)->query($territory)->reorder()->select('opportunities.id'));
}
if (! empty($validated['maturity_level'])) {
\App\Services\OpportunityMaturity::constrainSearch($query, 'historical:'.$validated['maturity_level']);
OpportunityMaturity::constrainSearch($query, 'historical:'.$validated['maturity_level']);
}
$results = $query->orderByDesc('is_featured')
->orderByDesc('published_at')
->paginate($validated['per_page'] ?? 12);
return response()->json($results);
return response()->json($results->withQueryString()->through(fn ($record) => app(InvestmentCatalogue::class)->presentLocation($record)));
}
public function show(string $slug): JsonResponse
+80 -25
View File
@@ -5,9 +5,14 @@ namespace App\Services;
use App\Http\Controllers\InvestmentPortfolioController;
use App\Models\InvestmentDossier;
use App\Models\Opportunity;
use App\Models\Sector;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class InvestmentCatalogue
{
@@ -17,8 +22,8 @@ class InvestmentCatalogue
/** @return array<string,mixed> */
public function filters(Request $request): array
{
$filters = $request->validate(['perspective' => ['nullable', 'in:exists,needs,ideas,opportunities'],'q' => ['nullable', 'string', 'max:120'], 'sector' => ['nullable', 'string', 'max:80'],
'preparation' => ['nullable', \Illuminate\Validation\Rule::in(array_column(OpportunityMaturity::searchOptions(), 'value'))],
$filters = $request->validate(['perspective' => ['nullable', 'in:exists,needs,ideas,opportunities'], 'q' => ['nullable', 'string', 'max:120'], 'sector' => ['nullable', 'string', 'max:80'],
'preparation' => ['nullable', Rule::in(array_column(OpportunityMaturity::searchOptions(), 'value'))],
'opportunity_id' => ['nullable', 'integer', 'min:1'],
'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'], 'admin_post_id' => ['nullable', 'uuid'], 'locality_id' => ['nullable', 'uuid'],
'include_national' => ['nullable', 'boolean'], 'territory_basis' => ['nullable', 'in:all,direct,context'],
@@ -40,14 +45,15 @@ class InvestmentCatalogue
'category' => ['nullable', 'in:'.implode(',', OpportunityManagement::CATEGORIES)],
'publication_state' => ['nullable', 'in:published'],
'per_page' => ['nullable', 'integer', 'between:1,50'], 'page' => ['nullable', 'integer', 'min:1']]);
$filters = app(InvestmentGeography::class)->provinceSearch($filters);
app(InvestmentGeography::class)->selection($filters);
$aliases = ['park' => 'park_id', 'cluster' => 'cluster_id', 'corridor' => 'corridor_id', 'project' => 'project_id'];
$alias = $aliases[$filters['association_type'] ?? ''] ?? null;
if ($alias && ! empty($filters['association_target']) && empty($filters['association_purpose']) && empty($filters['association_state'])) {
if (! empty($filters[$alias]) && $filters[$alias] !== $filters['association_target']) {
throw \Illuminate\Validation\ValidationException::withMessages([$alias => 'Foram indicadas duas selecções diferentes para o mesmo filtro. Escolha apenas uma.']);
throw ValidationException::withMessages([$alias => 'Foram indicadas duas selecções diferentes para o mesmo filtro. Escolha apenas uma.']);
}
if (\Illuminate\Support\Str::isUuid($filters['association_target'])) {
if (Str::isUuid($filters['association_target'])) {
$filters[$alias] = $filters['association_target'];
unset($filters['association_type'], $filters['association_target']);
}
@@ -62,9 +68,13 @@ class InvestmentCatalogue
*/
public function query(array $filters): Builder
{
$filters = app(InvestmentGeography::class)->provinceSearch($filters);
$query = Opportunity::published()->with('sector:id,name,slug');
if (in_array($filters['perspective'] ?? '', ['exists','needs','ideas'], true)) $query->where('workflow_context->promotional_summary->projection->proactive->group', $filters['perspective']);
elseif (($filters['perspective'] ?? '') === 'opportunities') $query->whereNull('workflow_context->promotional_summary->projection->proactive');
if (in_array($filters['perspective'] ?? '', ['exists', 'needs', 'ideas'], true)) {
$query->where('workflow_context->promotional_summary->projection->proactive->group', $filters['perspective']);
} elseif (($filters['perspective'] ?? '') === 'opportunities') {
$query->whereNull('workflow_context->promotional_summary->projection->proactive');
}
$associated = $this->publicAssociatedIds($filters);
if (($filters['territory_basis'] ?? 'all') === 'direct') {
$query->where(fn ($q) => $q->whereNotNull('opportunities.primary_territory_id')->orWhereExists(fn ($l) => $l->selectRaw('1')->from('opportunity_locations')->whereColumn('opportunity_id', 'opportunities.id')));
@@ -86,9 +96,14 @@ class InvestmentCatalogue
$territory->whereIn('opportunities.primary_territory_id', $ids)
->orWhereExists(fn ($locations) => $locations->selectRaw('1')->from('opportunity_locations as territory_link')
->whereColumn('territory_link.opportunity_id', 'opportunities.id')->whereIn('territory_link.geo_location_id', $ids));
if (count($path) === 1) {
$this->declaredProvince($territory, $path[0]->name);
}
if (($filters['territory_basis'] ?? 'all') !== 'direct') {
app(InvestmentRelations::class)->opportunityTerritories($territory, $ids);
if (count($path) === 1) $this->approvedCorridorProvince($territory, $path[0]->id);
if (count($path) === 1) {
$this->approvedCorridorProvince($territory, $path[0]->id);
}
}
if ($includeNational) {
$territory->orWhere('opportunities.geography_scope', 'national');
@@ -163,6 +178,10 @@ class InvestmentCatalogue
->join('geo_locations as target', 'target.id', '=', 'link.geo_location_id')
->whereColumn('link.opportunity_id', 'opportunities.id')->whereIn('link.geo_location_id', $locationIds ?? [])
->whereNull('target.deleted_at')->where('target.data_classification', 'public'));
$province = app(InvestmentGeography::class)->publicReferences()->where('id', $filters['geo_location_id'])->where('location_type', 'province')->first(['name']);
if ($province) {
$this->declaredProvince($matches, $province->name);
}
if (($filters['territory_basis'] ?? 'all') !== 'direct') {
app(InvestmentRelations::class)->opportunityTerritories($matches, $locationIds ?? []);
if (app(InvestmentGeography::class)->publicReferences()->where('id', $filters['geo_location_id'])->where('location_type', 'province')->exists()) {
@@ -206,7 +225,9 @@ class InvestmentCatalogue
if (! empty($filters['before'])) {
$query->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_profiles')->whereColumn('opportunity_id', 'opportunities.id')->where('target_date', '<=', $filters['before']));
}
if (! empty($filters['category'])) $query->where('opportunity_category', $filters['category']);
if (! empty($filters['category'])) {
$query->where('opportunity_category', $filters['category']);
}
app(OpportunityClassification::class)->constrain($query, $filters);
$sort = $filters['sort'] ?? 'recent';
@@ -218,14 +239,28 @@ class InvestmentCatalogue
};
}
private function declaredProvince($query, string $name): void
{
// Read the published province declaration only when no more precise territory contradicts it.
// This does not assign any canonical ID, district, post, locality or coordinates to the record.
$query->orWhere(fn ($declared) => $declared->whereNull('opportunities.primary_territory_id')
->whereNotExists(fn ($links) => $links->selectRaw('1')->from('opportunity_locations')->whereColumn('opportunity_id', 'opportunities.id'))
->where('opportunities.geography_scope', '!=', 'national')
->whereRaw('lower(btrim(opportunities.province)) = ?', [mb_strtolower(trim($name))]));
}
private function approvedCorridorProvince($query, string $provinceId): void
{
$code = app(InvestmentGeography::class)->publicReferences()->where('id', $provinceId)->value('source_code');
$names = [];
foreach (config('investment_corridors', []) as $entry) {
if (in_array($code, $entry['provinces'], true)) $names[] = $entry['name'];
if (in_array($code, $entry['provinces'], true)) {
$names[] = $entry['name'];
}
}
if (! $names) {
return;
}
if (!$names) return;
$query->orWhere(fn ($context) => $context->whereNull('opportunities.primary_territory_id')
->whereNotExists(fn ($link) => $link->selectRaw('1')->from('opportunity_locations')->whereColumn('opportunity_id', 'opportunities.id'))
->where('workflow_context->corridor_agribusiness->source_sha256', CorridorAgribusinessOpportunities::SOURCE_HASH)
@@ -240,13 +275,19 @@ class InvestmentCatalogue
$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'));
if ($path) {
$paths[] = implode(' / ', array_column($path, 'name'));
}
}
if ($paths) $label = implode('; ', array_unique($paths));
elseif ($provinces = app(InvestmentCorridorContext::class)->catalogueProvinces($record)) {
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';
} else $label = implode(' / ', array_filter([$record->province, $record->district])) ?: 'Localização não indicada';
} else {
$label = implode(' / ', array_filter([$record->province, $record->district])) ?: 'Localização não indicada';
}
$record->setAttribute('territorial_summary', $label);
return $record;
}
@@ -260,9 +301,9 @@ class InvestmentCatalogue
'authorities' => DB::table('authorities')->where('status', 'published')->get(['id', 'label_pt_mz']),
'categories' => OpportunityManagement::CATEGORIES,
'options' => DB::table('investment_reference_options')->when(! $management, fn ($q) => $q->where('active', true))->orderBy('sort_order')->orderBy('label')->get(),
'sectors' => \App\Models\Sector::choices()->whereNotNull('published_at')->orderBy('name')->get(['id', 'name', 'slug']),
'maturityCatalogue' => ['version'=>OpportunityMaturity::VERSION, 'stages'=>OpportunityMaturity::STAGES,
'legacyVersion'=>OpportunityMaturity::LEGACY, 'legacyLabels'=>OpportunityMaturity::LEGACY_LABELS]];
'sectors' => Sector::choices()->whereNotNull('published_at')->orderBy('name')->get(['id', 'name', 'slug']),
'maturityCatalogue' => ['version' => OpportunityMaturity::VERSION, 'stages' => OpportunityMaturity::STAGES,
'legacyVersion' => OpportunityMaturity::LEGACY, 'legacyLabels' => OpportunityMaturity::LEGACY_LABELS]];
foreach (self::RELATIONS as $relation => [$foreign, $table]) {
$query = DB::table($table)->whereNull('deleted_at');
if (! $management) {
@@ -295,7 +336,7 @@ class InvestmentCatalogue
$zones = app(InvestmentZones::class)->visible();
if (! $management && $territoryFilters !== []) {
// Opportunity words/modalities are not names/categories of zones.
$zones = app(InvestmentZones::class)->search(\Illuminate\Support\Arr::only($territoryFilters,
$zones = app(InvestmentZones::class)->search(Arr::only($territoryFilters,
['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'geo_location_id']));
}
$result['zones'] = $zones->orderBy('name')->get(['id', 'name', 'category']);
@@ -318,24 +359,35 @@ class InvestmentCatalogue
'cluster_id' => ['cluster'], 'corridor_id' => ['corridor'], 'project_id' => ['project']];
$selected = array_filter(array_intersect_key($filters, $kinds));
foreach (['zee_id' => 'zee', 'zfi_id' => 'zfi', 'park_id' => 'industrial_park'] as $key => $category) {
if (isset($selected[$key]) && ! app(InvestmentZones::class)->visible()->whereKey($selected[$key])->where('category', $category)->exists()) unset($selected[$key]);
if (isset($selected[$key]) && ! app(InvestmentZones::class)->visible()->whereKey($selected[$key])->where('category', $category)->exists()) {
unset($selected[$key]);
}
}
if ($selected === []) {
return [];
}
if ($selected === []) return [];
$ids = [];
Opportunity::published()->select('opportunities.*')->chunkById(200, function ($records) use ($selected, $kinds, &$ids) {
foreach ($records as $record) {
foreach (app(OpportunityClassification::class)->publicRows($record) as $row) {
if (in_array($row['purpose'], ['proximity', 'administrative_boundary'], true) || $row['state'] === 'rejected') continue;
if (in_array($row['purpose'], ['proximity', 'administrative_boundary'], true) || $row['state'] === 'rejected') {
continue;
}
foreach ($selected as $field => $target) {
if ($row['target_id'] === $target && in_array($row['target_type'], $kinds[$field], true)) $ids[$field][] = $record->id;
if ($row['target_id'] === $target && in_array($row['target_type'], $kinds[$field], true)) {
$ids[$field][] = $record->id;
}
}
}
}
});
if (!empty($selected['corridor_id'])) {
if (! empty($selected['corridor_id'])) {
$dossier = InvestmentPortfolioController::visible()->where('corridor_id', $selected['corridor_id'])->first();
if ($dossier) $ids['corridor_id'] = array_values(array_unique([...($ids['corridor_id'] ?? []), ...app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossier)]));
if ($dossier) {
$ids['corridor_id'] = array_values(array_unique([...($ids['corridor_id'] ?? []), ...app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossier)]));
}
}
return $ids;
}
@@ -359,9 +411,12 @@ class InvestmentCatalogue
$public = ['profile' => null, 'facets' => collect(), 'zones' => collect(), 'classification' => [], 'source_evidence' => [],
'primary_territory_path' => app(InvestmentGeography::class)->path($opportunity->primary_territory_id, true),
'opportunity_locations' => app(InvestmentRelations::class)->opportunityLocations($id)];
foreach (self::RELATIONS as $relation => $_) $public[$relation] = collect();
foreach (self::RELATIONS as $relation => $_) {
$public[$relation] = collect();
}
$public['locations'] = DB::table('opportunity_locations as l')->join('geo_locations as g', 'g.id', '=', 'l.geo_location_id')
->where('l.opportunity_id', $id)->where('g.data_classification', 'public')->whereNull('g.deleted_at')->get(['g.id', 'g.name']);
return $public;
}
$result = ['profile' => DB::table('opportunity_profiles')->where('opportunity_id', $id)->first(),
+15
View File
@@ -12,6 +12,21 @@ final class InvestmentGeography
{
public const FIELDS = ['province_id' => 'province', 'district_id' => 'district', 'admin_post_id' => 'admin_post', 'locality_id' => 'locality'];
/** Resolve an old province-name search only when the public register has exactly one match. */
public function provinceSearch(array $filters): array
{
if (empty($filters['province_id']) && is_string($filters['province'] ?? null) && trim($filters['province']) !== '') {
$matches = $this->publicReferences()->where('location_type', 'province')
->whereRaw('lower(btrim(name)) = ?', [mb_strtolower(trim($filters['province']))])->limit(2)->get(['id']);
if ($matches->count() === 1) {
$filters['province_id'] = $matches[0]->id;
unset($filters['province']);
}
}
return $filters;
}
/**
* Restrict published dossier searches using direct territories and public, validated structure links.
*
@@ -1863,4 +1863,41 @@ Instalado pelo avanço normal de main e pipeline existente, preservando 23ecccfa
**Filtros provinciais: separado e aberto.** Esta entrega não alterou a lógica dos filtros nem demonstra a correcção relatada de Agronegócio/Maputo Província no portal. Preservam-se as provas e os estados históricos; não encerrar V2-07/V2-10 ou ACT-02 por causa da persistência de Competitividade. Restantes alterações permanecem na sequência posterior.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
### 29/10/2026 — V2-07/V2-10 / ACT-02, diagnóstico dos filtros provinciais em curso
Separação confirmada pela Tempo: a equipa executará e registará a conferência visual autenticada de Competitividade (continua pendente); o Codex prossegue no código/API dos filtros. Provas aceites preservadas, sem repetição das suites fiscais ou de Competitividade. Nenhuma perda de dados confirmada nesta passagem.
Reprodução pública em b81f477b: Agronegócio/Maputo Província por province_id na API /api/v1/investment/search devolve5 oportunidades (IDs60–64). O mesmo nome em province devolve0, tanto nessa API como em /api/v1/opportunities/search. A API antiga ignora province_id e devolve34, incluindo outras províncias. São lacunas de normalização/compatibilidade entre entradas, não prova de eliminação nem falha demonstrada da expansão hierárquica já existente. Corrigir apenas os deltas: resolver nomes provinciais públicos inequívocos para a mesma hierarquia e aplicar o filtro territorial na API antiga, preservando outros filtros, versões públicas, localização real e dados. Demonstrar todos os níveis inferiores por pedidos HTTP em QA isolada; província apenas contextual ou declarada não deve ganhar distrito. O problema permanece aberto até às provas específicas e à conferência da API instalada; não encerrar a conferência visual da equipa por testes técnicos.
### 29/10/2026 — duas frentes e diagnóstico local Edge
A equipa executa directamente e regista a conferência visual autenticada de Competitividade (editar → guardar → reabrir → conferir versão aprovada); estado pendente até execução. O Codex prossegue com código/API dos filtros provinciais, independentemente do navegador. Provas aceites preservadas.
Ligação identificada: com.openai.codexextension, manifesto stdio válido, extensão Edge instalada e activa no perfil detectado, host executado como filho do Edge; named pipes do browser-use presentes e servidor registado nos logs do cliente. Inspeccionados os listeners/processos sem libertar portas. Não foi identificado conflito TCP nem um serviço de extensão parado. Não foi encontrado erro de permissões no recorte de logs consultado, sem excluir outros pontos. A descoberta desta sessão continua a indicar access host indisponível; o runtime exige caminho do pipe e identidade verificadora, ausentes na shell. Falha de passagem de contexto é uma hipótese delimitada, não conclusão de porta ocupada. O registo de erros do service worker não ficou acessível pela ligação de navegador indisponível. A recuperação suportada, limitada à abertura do perfil Edge detectado, não restabeleceu a leitura do URL. Não foram terminados processos, reinstalados hosts nem alteradas permissões/políticas. InvesteMoz e SGF continuam em execução. Relatório local: outputs/edge-bridge-diagnostic-20261029/report.json. Não declarar recuperada a ligação nem concluída a conferência visual.
Correcção provincial preparada no ramo codex/province-search-20261029; validação QA/instalação pendentes. A sessão SSH anterior expirou. O lançamento automático de nova consola foi recusado pela revisão automática com indicação genérica blocked by policy, sem razão adicional; foi pedido arranque/autenticação manual no helper existente. Nenhuma escrita no activo ou novo ensaio remoto ocorreu por essa tentativa.
### 29/10/2026 — filtros provinciais: correcção candidata, pendente de QA
Reproduzido no portal instalado b81f477b: /api/v1/investment/search por province_id de Maputo Província devolve5; por province textual devolve0 nas duas APIs; a API antiga com province_id ignora o parâmetro e devolve34 de Agronegócio. Preparada correcção no ramo codex/province-search-20261029, preservando a expansão de descendentes existente e aplicando a mesma selecção territorial à API antiga. Nomes provinciais só são resolvidos quando a correspondência pública exacta é inequívoca. Declarações provinciais antigas são consideradas sem atribuir distrito, posto, localidade ou coordenadas, e não prevalecem sobre territórios explícitos contraditórios. Nenhum dado cadastral é actualizado por esta correcção.
Quatro testes HTTP preparados para província/distrito/posto/localidade e localizações adicionais, onze províncias e exclusão de vizinhas/privados/eliminados, preservação das localizações e consistência de catálogo/mapa/exportação. Sintaxe PHP e diff conferidos; testes ainda não executados e candidato ainda não instalado. Recibo: docs/investmoz/unification/province-search-qa-20261029.json. A sessão SSH expirou e a nova consola automática foi recusada pela revisão de aprovação com blocked by policy (sem razão específica adicional); pedida autenticação manual no helper outputs/province-search-20261029/ssh-session.py. Não substituir ausência de ensaio por declaração de correcção concluída.
A conferência visual de Competitividade fica a cargo da equipa e permanece pendente até execução/registo; provas técnicas aceites preservadas. Ligação Edge diagnosticada separadamente: extensão/manifesto/host activos, stdio/named pipes, leitura do URL não restabelecida pela recuperação suportada do perfil. Nenhum processo terminado nem porta libertada/reconfigurada. InvesteMoz e SGF preservados. Relatório local: outputs/edge-bridge-diagnostic-20261029/report.json. Os filtros mantêm-se abertos até QA e confirmação da API instalada.
### 29/10/2026 — filtros provinciais, prova HTTP específica em QA
Quatro testes Feature/364 asserções passaram na base restaurada province_search_20261029_test, porta55441, candidato e0987afb. Seleccionar só a província, por nome público inequívoco ou ID, inclui registos provinciais, distritais, de posto e localidade e localizações adicionais, nas duas APIs; confirmada cobertura das11 províncias e exclusão de vizinhas. Privados/eliminados não entram; distrito seleccionado não recebe oportunidades apenas provinciais. Declaração provincial antiga não supera localização explícita noutro território. Maputo Província por nome/ID, página do catálogo, mapa e exportação concordam nas5 oportunidades do contexto aprovado; sem coordenadas ou distrito inventados. Campos e relações guardados permanecem inalterados. Zero escritas no activo; nenhuma migração nem alteração de permissões. O conjunto inclui provas novas da hierarquia HTTP, sem reabrir as provas aceites de Competitividade/fiscalidade.
Recibo: docs/investmoz/unification/province-search-qa-20261029.json. Instalação e confirmação da API no portal continuam pendentes; não declarar encerrado o problema por QA isolada. A conferência visual autenticada de Competitividade permanece separada, pendente de execução e registo pela equipa. A sessão SSH foi reaberta a pedido expresso da Tempo e ficou autenticada; a recusa anterior da abertura automática não constitui bloqueio actual.
### 29/10/2026 — filtros provinciais corrigidos e confirmados por API no portal: fde73438
**Estado deste recorte V2-07/V2-10 / ACT-02:** correcção de código/API implementada, instalada e confirmada em https://teste-investidor.tempo.co.mz/. Avanço normal de main e pipeline existente, versão anterior b81f477b preservada. A selecção provincial por nome público inequívoco ou identificador usa a mesma hierarquia nas APIs /api/v1/investment/search e /api/v1/opportunities/search. Inclui localizações dos níveis inferiores e localizações adicionais; preserva publicação/sector e exclui registos privados, eliminados e outras províncias. Declaração provincial legada não ganha localização distrital nem prevalece sobre território explícito contraditório. Nenhum cadastro foi alterado por esta correcção.
**Prova HTTP específica:** quatro testes/364 asserções na QA restaurada province_search_20261029_test, porta55441: província/distrito/posto/localidade, localizações adicionais, onze províncias, exclusões, invariância dos dados, catálogo/mapa/exportação. Candidato QA e0987afb; código instalado coincide por SHA-256 com o testado (delta posterior documental). Não substituir esta prova pelos 1.180 testes frontend do pipeline. Pipeline habitual passou schema1/3106, Unit59/320, frontend1180, tipos e compilação.
**Confirmação no instalado:** comparadas as quatro entradas (duas APIs × nome/ID) para cada uma das11 províncias, com resultados idênticos por província. Agronegócio/Maputo Província: todas devolvem agora5 oportunidades, IDs60–64, corrigindo0 por nome e34 na API antiga que ignorava o ID. Continuam identificadas como contexto provincial do corredor, com district e primary_territory_id nulos; mapa conserva5 sem ponto utilizável e não gera coordenadas. O export respondeu. A inclusão efectiva de níveis inferiores foi demonstrada com fixtures sintéticas exclusivamente na QA, não com dados fictícios no activo. Inhambane devolve0 neste recorte cadastral Agronegócio, de forma consistente nas quatro entradas; isto não é conclusão de ausência de potencial económico.
**Preservação:** backup fresco/restauro isolado176; comparação posterior de176 tabelas sem alterações de domínio, apenas diferenças operacionais de cache/sessões;369 ficheiros/configuração idênticos. Os4 ficheiros do incremento e10 ficheiros das provas aceites de Competitividade foram conferidos por hash no instalado; C132 passou e catálogo privado anónimo permanece HTTP401. Zero migrações/importações, novas divulgações, mudanças de permissões ou escritas sintéticas no activo. Recibo completo: docs/investmoz/unification/province-search-qa-20261029.json. Autenticação SSH renovada após reabertura expressamente pedida pela Tempo; a recusa anterior do lançamento automático não é bloqueio corrente.
**Separação mantida:** este resultado encerra a lacuna técnica de compatibilidade/expansão provincial descrita acima, sem declarar conferência visual dos controlos e sem encerrar os requisitos amplos ACT-02/V2-07/V2-10. A conferência visual autenticada de Competitividade continua **pendente — execução e registo pela equipa**. As provas aceites dessa funcionalidade não foram reabertas nem substituídas pela correcção dos filtros. Diagnóstico da ligação Edge continua separado; leitura de URL não declarada recuperada. Nenhum processo local foi terminado nem porta libertada; InvesteMoz e SGF preservados.
Para o registo da equipa: no cluster autorizado, editar conteúdo legítimo → Guardar rascunho no servidor → sair e reabrir → comparar campos/versão recuperados → conferir na ficha pública exclusivamente a versão aprovada. Registar utilizador/âmbito autorizado, URL/cluster, versão instalada, versões guardada/aprovada, resultado e divergências. Uma nova gravação não deve substituir a aprovação pública existente; nova aprovação depende da decisão de conteúdo aplicável. Sem exemplar aprovado disponível, manter essa parte da conferência pendente em vez de publicar dados sintéticos.
@@ -0,0 +1,339 @@
{
"public_baseline": [
{
"first_page_ids": [
64,
63,
62,
61,
60
],
"path": "/api/v1/investment/search?sector=agronegocio\u0026province_id=bfde2651-2b46-5d9c-a500-733fa55a8c5c",
"total": 5
},
{
"first_page_ids": [
],
"path": "/api/v1/investment/search?sector=agronegocio\u0026province=Maputo%20Prov%C3%ADncia",
"total": 0
},
{
"first_page_ids": [
],
"path": "/api/v1/opportunities/search?sector=agronegocio\u0026province=Maputo%20Prov%C3%ADncia",
"total": 0
},
{
"first_page_ids": [
62,
63,
64,
60,
61,
49,
50,
55,
58,
56,
52,
48
],
"path": "/api/v1/opportunities/search?sector=agronegocio\u0026province_id=bfde2651-2b46-5d9c-a500-733fa55a8c5c",
"total": 34
}
],
"branch": "codex/province-search-20261029",
"data_writes": 0,
"authentication": {
"automatic_console_launch": "rejected by automatic approval review: blocked by policy; no specific reason supplied",
"manual_helper": "outputs/province-search-20261029/ssh-session.py",
"session": "renewed_connected_for_delivery",
"resolution": "Console reopened on explicit user request, authenticated; prior automatic rejection no longer blocks delivery"
},
"installed": "fde7343894964043fa633e3d4d07385a6cb7e039",
"cause": "Legacy text province uses exact opportunity field comparison; legacy search ignores canonical province_id; canonical province filtering already expands descendants",
"remaining": [
"team execution and record of authenticated visual Competitiveness workflow",
"broader V2-07/V2-10 and ACT-02 functionality outside this provincial search compatibility correction"
],
"visual_competitiveness": "pending direct execution and record by team; accepted proofs preserved",
"schema_changes": 0,
"status": "installed_specific_HTTP_QA_and_public_API_confirmed_preservation_verified",
"change": "Resolve a public, unique exact province name to the existing hierarchy; constrain legacy API by the same published catalogue territorial query; include declared province-only legacy records only without contradictory primary/additional territories",
"candidate": "fde7343894964043fa633e3d4d07385a6cb7e039",
"browser_bridge": "diagnosed separately; URL read not restored; outputs/edge-bridge-diagnostic-20261029/report.json",
"checks": {
"specific_HTTP_tests_run": 4,
"diff": "passed",
"specific_HTTP_tests_planned": 4,
"PHP_syntax": "passed",
"specific_HTTP_assertions": 364
},
"source_sha256": {
"app/Services/InvestmentCatalogue.php": "731091c7c754aaf40a7235c405869134523de1fef431e686a94a1d1b3d1e9fd9",
"app/Services/InvestmentGeography.php": "9815835078fe625031d795e3fda83b4af0fc1a0d18af6e8ce9fd2d3f8e535c72",
"tests/Feature/ProvinceSearchCompatibilityTest.php": "b0164154b6f08c624dcd81425f145c2fc2770a595f04c7917858c30b26049279",
"app/Http/Controllers/Api/V1/OpportunityController.php": "69a873a429e3510d5fd6691b58d825cf877274f6cd7d1303aa8029555680bb92"
},
"tested_candidate": "e0987afb0e2722300eeb0d50e8797b07fb53853a",
"QA": {
"candidate": "e0987afb0e2722300eeb0d50e8797b07fb53853a",
"status": "passed",
"database": "province_search_20261029_test",
"port": 55441,
"snapshot_restored": true,
"active_database_writes": 0,
"schema_changes": 0,
"log": "/var/lib/investmoz-deploy/province-search-qa-20261029/qa-e0987afb0e2722300eeb0d50e8797b07fb53853a.log",
"summary": "PHPUnit 12.5.34 by Sebastian Bergmann and contributors.\n\nRuntime: PHP 8.4.27\nConfiguration: /var/lib/investmoz-deploy/province-search-qa-20261029/e0987afb0e2722300eeb0d50e8797b07fb53853a-1793279039249452598/phpunit.xml\n\n.... 4 / 4 (100%)\n\nTime: 00:10.928, Memory: 60.50 MB\n\n\u001b[30;42mOK (4 tests, 364 assertions)\u001b[0m\n"
},
"baseline": "b81f477bf2385227ed3099868e80b1e3c7bcb46b",
"backup": {
"stage": "/var/backups/investmoz-province-search-20261029/backup",
"created_at": "2026-10-29T13:06:32Z",
"target": {
"database": "teste_investidor",
"port": 5432
},
"tables": 176,
"files_verified": 369,
"dump_sha256": "7140e88c56f76835a4dc321b62ef13ce08050b0d59b9a69795a98c8eb3cb9337",
"files_sha256": "ab3badc10980171b1e65b4286ee0b3e26aab33354de7fb6f76826d09ea141ffb",
"restore_verified": false
},
"restore": {
"restore_verified": true,
"tables_verified": 176,
"container_network": "none",
"database": "province_search_restore_20261029_test",
"port": 55441,
"source_dump_sha256": "7140e88c56f76835a4dc321b62ef13ce08050b0d59b9a69795a98c8eb3cb9337",
"active_database_writes": 0
},
"api": {
"candidate": "fde7343894964043fa633e3d4d07385a6cb7e039",
"public_revision_confirmed": true,
"provincial_comparisons": [
{
"province": "Cabo Delgado",
"id": "059b02ca-7095-577a-8705-4d4c46896747",
"matching_results": 5,
"all_four_entry_points_agree": true
},
{
"province": "Gaza",
"id": "8ed9aa8a-8815-5863-97ec-494e40a7ea81",
"matching_results": 5,
"all_four_entry_points_agree": true
},
{
"province": "Inhambane",
"id": "9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8",
"matching_results": 0,
"all_four_entry_points_agree": true
},
{
"province": "Manica",
"id": "cf5e81f1-ffbc-535d-b1b6-b44b445c4421",
"matching_results": 7,
"all_four_entry_points_agree": true
},
{
"province": "Maputo Cidade",
"id": "4aa31470-997e-5c63-8fe0-ff2f62586f56",
"matching_results": 5,
"all_four_entry_points_agree": true
},
{
"province": "Maputo Província",
"id": "bfde2651-2b46-5d9c-a500-733fa55a8c5c",
"matching_results": 5,
"all_four_entry_points_agree": true
},
{
"province": "Nampula",
"id": "33a98ebf-f98c-53d1-a047-32ccd9c4cd98",
"matching_results": 6,
"all_four_entry_points_agree": true
},
{
"province": "Niassa",
"id": "7d908cf8-6e52-5141-8832-b1193985e39f",
"matching_results": 5,
"all_four_entry_points_agree": true
},
{
"province": "Sofala",
"id": "f57ff041-6275-57b9-8800-c35e46e0d475",
"matching_results": 7,
"all_four_entry_points_agree": true
},
{
"province": "Tete",
"id": "c0a78e45-ebaa-590e-847f-8ba68e4b2fbe",
"matching_results": 6,
"all_four_entry_points_agree": true
},
{
"province": "Zambezia",
"id": "c20efcce-8af8-588d-84b2-b4b138b6e066",
"matching_results": 6,
"all_four_entry_points_agree": true
}
],
"Maputo": [
{
"endpoint": "/api/v1/investment/search",
"selection": {
"province_id": "bfde2651-2b46-5d9c-a500-733fa55a8c5c"
},
"ids": [
60,
61,
62,
63,
64
],
"district_not_assigned": true
},
{
"endpoint": "/api/v1/investment/search",
"selection": {
"province": "Maputo Província"
},
"ids": [
60,
61,
62,
63,
64
],
"district_not_assigned": true
},
{
"endpoint": "/api/v1/opportunities/search",
"selection": {
"province_id": "bfde2651-2b46-5d9c-a500-733fa55a8c5c"
},
"ids": [
60,
61,
62,
63,
64
],
"district_not_assigned": true
},
{
"endpoint": "/api/v1/opportunities/search",
"selection": {
"province": "Maputo Província"
},
"ids": [
60,
61,
62,
63,
64
],
"district_not_assigned": true
}
],
"map_unlocated": 5,
"map_coordinates_invented": false,
"export_responded": true,
"active_domain_writes": 0,
"scope": "Installed public API confirmed; deeper-level synthetic fixtures proved separately in isolated QA (4 tests/364 assertions)",
"competitiveness_visual_conference": "pending team execution and record; accepted technical proofs preserved",
"browser_URL_read": "not recovered; independent of this API conference"
},
"code": {
"candidate": "fde7343894964043fa633e3d4d07385a6cb7e039",
"source_files_verified": 14,
"diagnostics_C132": {
"result": "passed",
"case": "C132",
"source_explicit_constant_OFF": true,
"effective_configuration_OFF": true,
"configuration_cached": false,
"database_connections_used": 0,
"active_writes": 0
},
"active_writes": 0
},
"preservation": {
"navigation_asset_and_shared_props_verified": true,
"at": "2026-10-29T13:17:10.135835+00:00",
"served_commit": "fde7343894964043fa633e3d4d07385a6cb7e039",
"backup_dump_sha256": "7140e88c56f76835a4dc321b62ef13ce08050b0d59b9a69795a98c8eb3cb9337",
"prior_public_tables_checked": 176,
"prior_domain_rows_preserved": true,
"new_migration_registry_rows": 0,
"operational_delta": {
"cache": {
"before": {
"rows": 120,
"checksum": "55ffaf3e179865765ecf047ad31a2c35"
},
"after": {
"rows": 126,
"checksum": "013841da1ac8a6ddd713bf8d12672fac"
}
},
"sessions": {
"before": {
"rows": 18,
"checksum": "8c7f1af4213371d0022a6b2e0cc62bb6"
},
"after": {
"rows": 37,
"checksum": "b9a7610497fee6005c2bebc69a84d99c"
}
}
},
"persistent_files_and_configuration_identical": 369,
"private_catalogue": {
"observations": 361464,
"distinct_administrative_observations": 214299,
"preserved_repetitions": 147165,
"unmapped_territory": 205608
},
"preservation_identity": "postgres, matching backup reader; no actor impersonation",
"anonymous_query_status": 401,
"public_revision_confirmed": true,
"actual_team_UI_journey": "pending authenticated browser conference;accepted QA is not repeated",
"unique_company_count": null,
"publication": false,
"national_coverage": "partial",
"investment_conclusion": "hypothesis,not proved demand or viability",
"L8": "unchanged,partial"
},
"pipeline": {
"unit_tests": 59,
"summaries": [
"\u001b[30;42mOK (1 test, 3106 assertions)\u001b[0m",
"✓ built in 33.95s",
"\u001b[30;42mOK (59 tests, 320 assertions)\u001b[0m",
"# tests 1180",
"# pass 1180",
"# fail 0"
],
"schema_assertions": 3106,
"frontend_tests": 1180,
"result": {
"status": "installed",
"commit": "fde7343894964043fa633e3d4d07385a6cb7e039",
"release": "/var/www/investmoz-homologacao/releases/fde7343894964043fa633e3d4d07385a6cb7e039-224377ca",
"previous_release": "/var/www/investmoz-homologacao/releases/b81f477bf2385227ed3099868e80b1e3c7bcb46b-5eed771e",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"fde7343894964043fa633e3d4d07385a6cb7e039"
],
"finished_at": "2026-10-29T13:13:03.813678+00:00"
},
"unit_assertions": 320
}
}
@@ -7421,4 +7421,41 @@ Instalado pelo avanço normal de main e pipeline existente, preservando 23ecccfa
**Filtros provinciais: separado e aberto.** Esta entrega não alterou a lógica dos filtros nem demonstra a correcção relatada de Agronegócio/Maputo Província no portal. Preservam-se as provas e os estados históricos; não encerrar V2-07/V2-10 ou ACT-02 por causa da persistência de Competitividade. Restantes alterações permanecem na sequência posterior.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
### 29/10/2026 — V2-07/V2-10 / ACT-02, diagnóstico dos filtros provinciais em curso
Separação confirmada pela Tempo: a equipa executará e registará a conferência visual autenticada de Competitividade (continua pendente); o Codex prossegue no código/API dos filtros. Provas aceites preservadas, sem repetição das suites fiscais ou de Competitividade. Nenhuma perda de dados confirmada nesta passagem.
Reprodução pública em b81f477b: Agronegócio/Maputo Província por province_id na API /api/v1/investment/search devolve5 oportunidades (IDs60–64). O mesmo nome em province devolve0, tanto nessa API como em /api/v1/opportunities/search. A API antiga ignora province_id e devolve34, incluindo outras províncias. São lacunas de normalização/compatibilidade entre entradas, não prova de eliminação nem falha demonstrada da expansão hierárquica já existente. Corrigir apenas os deltas: resolver nomes provinciais públicos inequívocos para a mesma hierarquia e aplicar o filtro territorial na API antiga, preservando outros filtros, versões públicas, localização real e dados. Demonstrar todos os níveis inferiores por pedidos HTTP em QA isolada; província apenas contextual ou declarada não deve ganhar distrito. O problema permanece aberto até às provas específicas e à conferência da API instalada; não encerrar a conferência visual da equipa por testes técnicos.
### 29/10/2026 — duas frentes e diagnóstico local Edge
A equipa executa directamente e regista a conferência visual autenticada de Competitividade (editar → guardar → reabrir → conferir versão aprovada); estado pendente até execução. O Codex prossegue com código/API dos filtros provinciais, independentemente do navegador. Provas aceites preservadas.
Ligação identificada: com.openai.codexextension, manifesto stdio válido, extensão Edge instalada e activa no perfil detectado, host executado como filho do Edge; named pipes do browser-use presentes e servidor registado nos logs do cliente. Inspeccionados os listeners/processos sem libertar portas. Não foi identificado conflito TCP nem um serviço de extensão parado. Não foi encontrado erro de permissões no recorte de logs consultado, sem excluir outros pontos. A descoberta desta sessão continua a indicar access host indisponível; o runtime exige caminho do pipe e identidade verificadora, ausentes na shell. Falha de passagem de contexto é uma hipótese delimitada, não conclusão de porta ocupada. O registo de erros do service worker não ficou acessível pela ligação de navegador indisponível. A recuperação suportada, limitada à abertura do perfil Edge detectado, não restabeleceu a leitura do URL. Não foram terminados processos, reinstalados hosts nem alteradas permissões/políticas. InvesteMoz e SGF continuam em execução. Relatório local: outputs/edge-bridge-diagnostic-20261029/report.json. Não declarar recuperada a ligação nem concluída a conferência visual.
Correcção provincial preparada no ramo codex/province-search-20261029; validação QA/instalação pendentes. A sessão SSH anterior expirou. O lançamento automático de nova consola foi recusado pela revisão automática com indicação genérica blocked by policy, sem razão adicional; foi pedido arranque/autenticação manual no helper existente. Nenhuma escrita no activo ou novo ensaio remoto ocorreu por essa tentativa.
### 29/10/2026 — filtros provinciais: correcção candidata, pendente de QA
Reproduzido no portal instalado b81f477b: /api/v1/investment/search por province_id de Maputo Província devolve5; por province textual devolve0 nas duas APIs; a API antiga com province_id ignora o parâmetro e devolve34 de Agronegócio. Preparada correcção no ramo codex/province-search-20261029, preservando a expansão de descendentes existente e aplicando a mesma selecção territorial à API antiga. Nomes provinciais só são resolvidos quando a correspondência pública exacta é inequívoca. Declarações provinciais antigas são consideradas sem atribuir distrito, posto, localidade ou coordenadas, e não prevalecem sobre territórios explícitos contraditórios. Nenhum dado cadastral é actualizado por esta correcção.
Quatro testes HTTP preparados para província/distrito/posto/localidade e localizações adicionais, onze províncias e exclusão de vizinhas/privados/eliminados, preservação das localizações e consistência de catálogo/mapa/exportação. Sintaxe PHP e diff conferidos; testes ainda não executados e candidato ainda não instalado. Recibo: docs/investmoz/unification/province-search-qa-20261029.json. A sessão SSH expirou e a nova consola automática foi recusada pela revisão de aprovação com blocked by policy (sem razão específica adicional); pedida autenticação manual no helper outputs/province-search-20261029/ssh-session.py. Não substituir ausência de ensaio por declaração de correcção concluída.
A conferência visual de Competitividade fica a cargo da equipa e permanece pendente até execução/registo; provas técnicas aceites preservadas. Ligação Edge diagnosticada separadamente: extensão/manifesto/host activos, stdio/named pipes, leitura do URL não restabelecida pela recuperação suportada do perfil. Nenhum processo terminado nem porta libertada/reconfigurada. InvesteMoz e SGF preservados. Relatório local: outputs/edge-bridge-diagnostic-20261029/report.json. Os filtros mantêm-se abertos até QA e confirmação da API instalada.
### 29/10/2026 — filtros provinciais, prova HTTP específica em QA
Quatro testes Feature/364 asserções passaram na base restaurada province_search_20261029_test, porta55441, candidato e0987afb. Seleccionar só a província, por nome público inequívoco ou ID, inclui registos provinciais, distritais, de posto e localidade e localizações adicionais, nas duas APIs; confirmada cobertura das11 províncias e exclusão de vizinhas. Privados/eliminados não entram; distrito seleccionado não recebe oportunidades apenas provinciais. Declaração provincial antiga não supera localização explícita noutro território. Maputo Província por nome/ID, página do catálogo, mapa e exportação concordam nas5 oportunidades do contexto aprovado; sem coordenadas ou distrito inventados. Campos e relações guardados permanecem inalterados. Zero escritas no activo; nenhuma migração nem alteração de permissões. O conjunto inclui provas novas da hierarquia HTTP, sem reabrir as provas aceites de Competitividade/fiscalidade.
Recibo: docs/investmoz/unification/province-search-qa-20261029.json. Instalação e confirmação da API no portal continuam pendentes; não declarar encerrado o problema por QA isolada. A conferência visual autenticada de Competitividade permanece separada, pendente de execução e registo pela equipa. A sessão SSH foi reaberta a pedido expresso da Tempo e ficou autenticada; a recusa anterior da abertura automática não constitui bloqueio actual.
### 29/10/2026 — filtros provinciais corrigidos e confirmados por API no portal: fde73438
**Estado deste recorte V2-07/V2-10 / ACT-02:** correcção de código/API implementada, instalada e confirmada em https://teste-investidor.tempo.co.mz/. Avanço normal de main e pipeline existente, versão anterior b81f477b preservada. A selecção provincial por nome público inequívoco ou identificador usa a mesma hierarquia nas APIs /api/v1/investment/search e /api/v1/opportunities/search. Inclui localizações dos níveis inferiores e localizações adicionais; preserva publicação/sector e exclui registos privados, eliminados e outras províncias. Declaração provincial legada não ganha localização distrital nem prevalece sobre território explícito contraditório. Nenhum cadastro foi alterado por esta correcção.
**Prova HTTP específica:** quatro testes/364 asserções na QA restaurada province_search_20261029_test, porta55441: província/distrito/posto/localidade, localizações adicionais, onze províncias, exclusões, invariância dos dados, catálogo/mapa/exportação. Candidato QA e0987afb; código instalado coincide por SHA-256 com o testado (delta posterior documental). Não substituir esta prova pelos 1.180 testes frontend do pipeline. Pipeline habitual passou schema1/3106, Unit59/320, frontend1180, tipos e compilação.
**Confirmação no instalado:** comparadas as quatro entradas (duas APIs × nome/ID) para cada uma das11 províncias, com resultados idênticos por província. Agronegócio/Maputo Província: todas devolvem agora5 oportunidades, IDs60–64, corrigindo0 por nome e34 na API antiga que ignorava o ID. Continuam identificadas como contexto provincial do corredor, com district e primary_territory_id nulos; mapa conserva5 sem ponto utilizável e não gera coordenadas. O export respondeu. A inclusão efectiva de níveis inferiores foi demonstrada com fixtures sintéticas exclusivamente na QA, não com dados fictícios no activo. Inhambane devolve0 neste recorte cadastral Agronegócio, de forma consistente nas quatro entradas; isto não é conclusão de ausência de potencial económico.
**Preservação:** backup fresco/restauro isolado176; comparação posterior de176 tabelas sem alterações de domínio, apenas diferenças operacionais de cache/sessões;369 ficheiros/configuração idênticos. Os4 ficheiros do incremento e10 ficheiros das provas aceites de Competitividade foram conferidos por hash no instalado; C132 passou e catálogo privado anónimo permanece HTTP401. Zero migrações/importações, novas divulgações, mudanças de permissões ou escritas sintéticas no activo. Recibo completo: docs/investmoz/unification/province-search-qa-20261029.json. Autenticação SSH renovada após reabertura expressamente pedida pela Tempo; a recusa anterior do lançamento automático não é bloqueio corrente.
**Separação mantida:** este resultado encerra a lacuna técnica de compatibilidade/expansão provincial descrita acima, sem declarar conferência visual dos controlos e sem encerrar os requisitos amplos ACT-02/V2-07/V2-10. A conferência visual autenticada de Competitividade continua **pendente — execução e registo pela equipa**. As provas aceites dessa funcionalidade não foram reabertas nem substituídas pela correcção dos filtros. Diagnóstico da ligação Edge continua separado; leitura de URL não declarada recuperada. Nenhum processo local foi terminado nem porta libertada; InvesteMoz e SGF preservados.
Para o registo da equipa: no cluster autorizado, editar conteúdo legítimo → Guardar rascunho no servidor → sair e reabrir → comparar campos/versão recuperados → conferir na ficha pública exclusivamente a versão aprovada. Registar utilizador/âmbito autorizado, URL/cluster, versão instalada, versões guardada/aprovada, resultado e divergências. Uma nova gravação não deve substituir a aprovação pública existente; nova aprovação depende da decisão de conteúdo aplicável. Sem exemplar aprovado disponível, manter essa parte da conferência pendente em vez de publicar dados sintéticos.
+38 -1
View File
@@ -3043,4 +3043,41 @@ Instalado pelo avanço normal de main e pipeline existente, preservando 23ecccfa
**Filtros provinciais: separado e aberto.** Esta entrega não alterou a lógica dos filtros nem demonstra a correcção relatada de Agronegócio/Maputo Província no portal. Preservam-se as provas e os estados históricos; não encerrar V2-07/V2-10 ou ACT-02 por causa da persistência de Competitividade. Restantes alterações permanecem na sequência posterior.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
### 29/10/2026 — V2-07/V2-10 / ACT-02, diagnóstico dos filtros provinciais em curso
Separação confirmada pela Tempo: a equipa executará e registará a conferência visual autenticada de Competitividade (continua pendente); o Codex prossegue no código/API dos filtros. Provas aceites preservadas, sem repetição das suites fiscais ou de Competitividade. Nenhuma perda de dados confirmada nesta passagem.
Reprodução pública em b81f477b: Agronegócio/Maputo Província por province_id na API /api/v1/investment/search devolve5 oportunidades (IDs60–64). O mesmo nome em province devolve0, tanto nessa API como em /api/v1/opportunities/search. A API antiga ignora province_id e devolve34, incluindo outras províncias. São lacunas de normalização/compatibilidade entre entradas, não prova de eliminação nem falha demonstrada da expansão hierárquica já existente. Corrigir apenas os deltas: resolver nomes provinciais públicos inequívocos para a mesma hierarquia e aplicar o filtro territorial na API antiga, preservando outros filtros, versões públicas, localização real e dados. Demonstrar todos os níveis inferiores por pedidos HTTP em QA isolada; província apenas contextual ou declarada não deve ganhar distrito. O problema permanece aberto até às provas específicas e à conferência da API instalada; não encerrar a conferência visual da equipa por testes técnicos.
### 29/10/2026 — duas frentes e diagnóstico local Edge
A equipa executa directamente e regista a conferência visual autenticada de Competitividade (editar → guardar → reabrir → conferir versão aprovada); estado pendente até execução. O Codex prossegue com código/API dos filtros provinciais, independentemente do navegador. Provas aceites preservadas.
Ligação identificada: com.openai.codexextension, manifesto stdio válido, extensão Edge instalada e activa no perfil detectado, host executado como filho do Edge; named pipes do browser-use presentes e servidor registado nos logs do cliente. Inspeccionados os listeners/processos sem libertar portas. Não foi identificado conflito TCP nem um serviço de extensão parado. Não foi encontrado erro de permissões no recorte de logs consultado, sem excluir outros pontos. A descoberta desta sessão continua a indicar access host indisponível; o runtime exige caminho do pipe e identidade verificadora, ausentes na shell. Falha de passagem de contexto é uma hipótese delimitada, não conclusão de porta ocupada. O registo de erros do service worker não ficou acessível pela ligação de navegador indisponível. A recuperação suportada, limitada à abertura do perfil Edge detectado, não restabeleceu a leitura do URL. Não foram terminados processos, reinstalados hosts nem alteradas permissões/políticas. InvesteMoz e SGF continuam em execução. Relatório local: outputs/edge-bridge-diagnostic-20261029/report.json. Não declarar recuperada a ligação nem concluída a conferência visual.
Correcção provincial preparada no ramo codex/province-search-20261029; validação QA/instalação pendentes. A sessão SSH anterior expirou. O lançamento automático de nova consola foi recusado pela revisão automática com indicação genérica blocked by policy, sem razão adicional; foi pedido arranque/autenticação manual no helper existente. Nenhuma escrita no activo ou novo ensaio remoto ocorreu por essa tentativa.
### 29/10/2026 — filtros provinciais: correcção candidata, pendente de QA
Reproduzido no portal instalado b81f477b: /api/v1/investment/search por province_id de Maputo Província devolve5; por province textual devolve0 nas duas APIs; a API antiga com province_id ignora o parâmetro e devolve34 de Agronegócio. Preparada correcção no ramo codex/province-search-20261029, preservando a expansão de descendentes existente e aplicando a mesma selecção territorial à API antiga. Nomes provinciais só são resolvidos quando a correspondência pública exacta é inequívoca. Declarações provinciais antigas são consideradas sem atribuir distrito, posto, localidade ou coordenadas, e não prevalecem sobre territórios explícitos contraditórios. Nenhum dado cadastral é actualizado por esta correcção.
Quatro testes HTTP preparados para província/distrito/posto/localidade e localizações adicionais, onze províncias e exclusão de vizinhas/privados/eliminados, preservação das localizações e consistência de catálogo/mapa/exportação. Sintaxe PHP e diff conferidos; testes ainda não executados e candidato ainda não instalado. Recibo: docs/investmoz/unification/province-search-qa-20261029.json. A sessão SSH expirou e a nova consola automática foi recusada pela revisão de aprovação com blocked by policy (sem razão específica adicional); pedida autenticação manual no helper outputs/province-search-20261029/ssh-session.py. Não substituir ausência de ensaio por declaração de correcção concluída.
A conferência visual de Competitividade fica a cargo da equipa e permanece pendente até execução/registo; provas técnicas aceites preservadas. Ligação Edge diagnosticada separadamente: extensão/manifesto/host activos, stdio/named pipes, leitura do URL não restabelecida pela recuperação suportada do perfil. Nenhum processo terminado nem porta libertada/reconfigurada. InvesteMoz e SGF preservados. Relatório local: outputs/edge-bridge-diagnostic-20261029/report.json. Os filtros mantêm-se abertos até QA e confirmação da API instalada.
### 29/10/2026 — filtros provinciais, prova HTTP específica em QA
Quatro testes Feature/364 asserções passaram na base restaurada province_search_20261029_test, porta55441, candidato e0987afb. Seleccionar só a província, por nome público inequívoco ou ID, inclui registos provinciais, distritais, de posto e localidade e localizações adicionais, nas duas APIs; confirmada cobertura das11 províncias e exclusão de vizinhas. Privados/eliminados não entram; distrito seleccionado não recebe oportunidades apenas provinciais. Declaração provincial antiga não supera localização explícita noutro território. Maputo Província por nome/ID, página do catálogo, mapa e exportação concordam nas5 oportunidades do contexto aprovado; sem coordenadas ou distrito inventados. Campos e relações guardados permanecem inalterados. Zero escritas no activo; nenhuma migração nem alteração de permissões. O conjunto inclui provas novas da hierarquia HTTP, sem reabrir as provas aceites de Competitividade/fiscalidade.
Recibo: docs/investmoz/unification/province-search-qa-20261029.json. Instalação e confirmação da API no portal continuam pendentes; não declarar encerrado o problema por QA isolada. A conferência visual autenticada de Competitividade permanece separada, pendente de execução e registo pela equipa. A sessão SSH foi reaberta a pedido expresso da Tempo e ficou autenticada; a recusa anterior da abertura automática não constitui bloqueio actual.
### 29/10/2026 — filtros provinciais corrigidos e confirmados por API no portal: fde73438
**Estado deste recorte V2-07/V2-10 / ACT-02:** correcção de código/API implementada, instalada e confirmada em https://teste-investidor.tempo.co.mz/. Avanço normal de main e pipeline existente, versão anterior b81f477b preservada. A selecção provincial por nome público inequívoco ou identificador usa a mesma hierarquia nas APIs /api/v1/investment/search e /api/v1/opportunities/search. Inclui localizações dos níveis inferiores e localizações adicionais; preserva publicação/sector e exclui registos privados, eliminados e outras províncias. Declaração provincial legada não ganha localização distrital nem prevalece sobre território explícito contraditório. Nenhum cadastro foi alterado por esta correcção.
**Prova HTTP específica:** quatro testes/364 asserções na QA restaurada province_search_20261029_test, porta55441: província/distrito/posto/localidade, localizações adicionais, onze províncias, exclusões, invariância dos dados, catálogo/mapa/exportação. Candidato QA e0987afb; código instalado coincide por SHA-256 com o testado (delta posterior documental). Não substituir esta prova pelos 1.180 testes frontend do pipeline. Pipeline habitual passou schema1/3106, Unit59/320, frontend1180, tipos e compilação.
**Confirmação no instalado:** comparadas as quatro entradas (duas APIs × nome/ID) para cada uma das11 províncias, com resultados idênticos por província. Agronegócio/Maputo Província: todas devolvem agora5 oportunidades, IDs60–64, corrigindo0 por nome e34 na API antiga que ignorava o ID. Continuam identificadas como contexto provincial do corredor, com district e primary_territory_id nulos; mapa conserva5 sem ponto utilizável e não gera coordenadas. O export respondeu. A inclusão efectiva de níveis inferiores foi demonstrada com fixtures sintéticas exclusivamente na QA, não com dados fictícios no activo. Inhambane devolve0 neste recorte cadastral Agronegócio, de forma consistente nas quatro entradas; isto não é conclusão de ausência de potencial económico.
**Preservação:** backup fresco/restauro isolado176; comparação posterior de176 tabelas sem alterações de domínio, apenas diferenças operacionais de cache/sessões;369 ficheiros/configuração idênticos. Os4 ficheiros do incremento e10 ficheiros das provas aceites de Competitividade foram conferidos por hash no instalado; C132 passou e catálogo privado anónimo permanece HTTP401. Zero migrações/importações, novas divulgações, mudanças de permissões ou escritas sintéticas no activo. Recibo completo: docs/investmoz/unification/province-search-qa-20261029.json. Autenticação SSH renovada após reabertura expressamente pedida pela Tempo; a recusa anterior do lançamento automático não é bloqueio corrente.
**Separação mantida:** este resultado encerra a lacuna técnica de compatibilidade/expansão provincial descrita acima, sem declarar conferência visual dos controlos e sem encerrar os requisitos amplos ACT-02/V2-07/V2-10. A conferência visual autenticada de Competitividade continua **pendente — execução e registo pela equipa**. As provas aceites dessa funcionalidade não foram reabertas nem substituídas pela correcção dos filtros. Diagnóstico da ligação Edge continua separado; leitura de URL não declarada recuperada. Nenhum processo local foi terminado nem porta libertada; InvesteMoz e SGF preservados.
Para o registo da equipa: no cluster autorizado, editar conteúdo legítimo → Guardar rascunho no servidor → sair e reabrir → comparar campos/versão recuperados → conferir na ficha pública exclusivamente a versão aprovada. Registar utilizador/âmbito autorizado, URL/cluster, versão instalada, versões guardada/aprovada, resultado e divergências. Uma nova gravação não deve substituir a aprovação pública existente; nova aprovação depende da decisão de conteúdo aplicável. Sem exemplar aprovado disponível, manter essa parte da conferência pendente em vez de publicar dados sintéticos.
@@ -0,0 +1,169 @@
<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Services\CorridorAgribusinessOpportunities;
use App\Services\InvestmentGeography;
use App\Support\IsolatedQaDatabase;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;
class ProvinceSearchCompatibilityTest extends TestCase
{
use DatabaseTransactions;
public function createApplication(): Application
{
$app = parent::createApplication();
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
throw new \LogicException('Isolated QA required');
}
return $app;
}
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
}
private function createOpportunity(?string $territory, string $prefix, string $status = 'published', ?string $province = null): Opportunity
{
return Opportunity::create(['sector_id' => 1, 'reference' => 'SYNTHETIC-QA-'.Str::uuid(), 'slug' => 'synthetic-'.Str::uuid(),
'title' => $prefix.' synthetic provincial search', 'status' => $status, 'published_at' => now()->subMinute(),
'primary_territory_id' => $territory, 'province' => $province, 'district' => null, 'geography_scope' => 'specific']);
}
private function requests(array $filters): array
{
$query = http_build_query([...$filters, 'per_page' => 50]);
return ['/api/v1/investment/search?'.$query, '/api/v1/opportunities/search?'.$query];
}
public function test_a_single_province_includes_all_lower_levels_and_linked_locations_in_both_apis(): void
{
$geo = app(InvestmentGeography::class);
$locality = $geo->publicReferences()->where('location_type', 'locality')->orderBy('id')->first();
$this->assertNotNull($locality, 'Public locality fixture required');
$path = $geo->path($locality->id);
$this->assertCount(4, $path);
$prefix = 'SYNTHETIC-'.Str::uuid();
$records = array_map(fn ($node) => $this->createOpportunity($node->id, $prefix, 'published', 'Legacy label differs from canonical hierarchy'), $path);
$linked = $this->createOpportunity(null, $prefix);
$linked->additionalTerritories()->attach($locality->id);
$records[] = $linked;
$private = $this->createOpportunity($locality->id, $prefix, 'draft');
$deleted = $this->createOpportunity($locality->id, $prefix);
$deleted->delete();
$otherProvince = $geo->publicReferences()->where('location_type', 'province')->where('id', '<>', $path[0]->id)->first();
$outside = $this->createOpportunity($otherProvince->id, $prefix);
foreach ([['province_id' => $path[0]->id], ['province' => $path[0]->name], ['geo_location_id' => $path[0]->id]] as $entry) {
foreach ($this->requests([...$entry, 'q' => $prefix]) as $url) {
$response = $this->getJson($url)->assertOk()->assertJsonPath('total', 5);
$this->assertEqualsCanonicalizing(array_map(fn ($row) => $row->id, $records), array_column($response->json('data'), 'id'), $url);
foreach ($response->json('data') as $row) {
$this->assertNull($row['district']);
}
foreach ([$private, $deleted, $outside] as $row) {
$this->assertNotContains($row->id, array_column($response->json('data'), 'id'));
}
}
}
foreach ($records as $record) {
$this->assertNull($record->fresh()->district);
$this->assertSame($record->primary_territory_id, $record->fresh()->primary_territory_id);
}
$this->assertSame([$locality->id], $linked->additionalTerritories()->pluck('geo_locations.id')->all());
foreach ($this->requests(['province_id' => $path[0]->id, 'district_id' => $path[1]->id, 'q' => $prefix]) as $url) {
// Province-only records are not silently assigned to a district.
$this->getJson($url)->assertOk()->assertJsonPath('total', 4);
}
foreach ($this->requests(['district_id' => $path[1]->id]) as $url) {
$this->getJson($url)->assertUnprocessable();
}
}
public function test_published_province_declaration_is_found_without_inventing_a_district_or_overriding_an_explicit_other_territory(): void
{
$geo = app(InvestmentGeography::class);
$province = $geo->publicReferences()->where('location_type', 'province')->first();
$other = $geo->publicReferences()->where('location_type', 'province')->where('id', '<>', $province->id)->first();
$district = $geo->publicReferences()->where('parent_id', $province->id)->where('location_type', 'district')->first();
$prefix = 'SYNTHETIC-'.Str::uuid();
$legacy = $this->createOpportunity(null, $prefix, 'published', $province->name);
$contradictory = $this->createOpportunity($other->id, $prefix, 'published', $province->name);
$linkedOutside = $this->createOpportunity(null, $prefix, 'published', $province->name);
$linkedOutside->additionalTerritories()->attach($other->id);
foreach ([['province' => $province->name], ['province_id' => $province->id], ['geo_location_id' => $province->id]] as $entry) {
foreach ($this->requests([...$entry, 'q' => $prefix]) as $url) {
$this->getJson($url)->assertOk()->assertJsonPath('total', 1)->assertJsonPath('data.0.id', $legacy->id)->assertJsonPath('data.0.district', null);
}
}
foreach ($this->requests(['province_id' => $province->id, 'district_id' => $district->id, 'q' => $prefix]) as $url) {
$this->getJson($url)->assertOk()->assertJsonPath('total', 0);
}
$this->assertNull($legacy->fresh()->primary_territory_id);
$this->assertNull($legacy->fresh()->district);
$this->assertSame($other->id, $contradictory->fresh()->primary_territory_id);
}
public function test_provincial_only_search_covers_the_eleven_provinces_without_including_neighbouring_provinces(): void
{
$geo = app(InvestmentGeography::class);
$provinces = $geo->publicReferences()->where('location_type', 'province')->get();
$this->assertCount(11, $provinces);
$prefix = 'SYNTHETIC-'.Str::uuid();
$records = [];
foreach ($provinces as $province) {
$district = $geo->publicReferences()->where('parent_id', $province->id)->where('location_type', 'district')->first();
$this->assertNotNull($district, $province->name);
$records[$province->id] = $this->createOpportunity($district->id, $prefix);
}
foreach ($provinces as $province) {
foreach ([['province_id' => $province->id], ['province' => $province->name]] as $entry) {
foreach ($this->requests([...$entry, 'q' => $prefix]) as $url) {
$this->getJson($url)->assertOk()->assertJsonPath('total', 1)->assertJsonPath('data.0.id', $records[$province->id]->id);
}
}
}
}
public function test_maputo_names_ids_page_map_and_export_share_the_existing_approved_corridor_coverage(): void
{
$provinceId = 'bfde2651-2b46-5d9c-a500-733fa55a8c5c';
$expected = Opportunity::published()->where('workflow_context->corridor_agribusiness->source_sha256', CorridorAgribusinessOpportunities::SOURCE_HASH)
->where('workflow_context->corridor_agribusiness->corridor', 'Corredor de Maputo')->pluck('id')->all();
$this->assertCount(5, $expected);
foreach ([['province' => 'Maputo Província'], ['province' => ' mApUtO Província '], ['province_id' => $provinceId]] as $entry) {
foreach ($this->requests([...$entry, 'sector' => 'agronegocio']) as $url) {
$response = $this->getJson($url)->assertOk()->assertJsonPath('total', 5);
$this->assertEqualsCanonicalizing($expected, array_column($response->json('data'), 'id'));
foreach ($response->json('data') as $row) {
$this->assertNull($row['district']);
$this->assertStringContainsString('contexto provincial do corredor', $row['territorial_summary']);
}
}
}
$query = http_build_query(['sector' => 'agronegocio', 'province' => 'Maputo Província']);
$this->get('/oportunidades?'.$query)->assertInertia(fn (AssertableInertia $page) => $page
->component('investment/catalogue')->where('filters.province_id', $provinceId)->missing('filters.province')
->has('territorySelection', 1)->where('results.total', 5));
$this->getJson('/api/v1/investment/map?'.$query)->assertOk()->assertJsonCount(0, 'features')->assertJsonCount(5, 'unlocated');
$csv = $this->get('/api/v1/investment/export?'.$query)->assertOk()->streamedContent();
foreach (Opportunity::whereIn('id', $expected)->get() as $row) {
$this->assertStringContainsString($row->title, $csv);
}
foreach ($this->requests(['sector' => 'turismo', 'province_id' => $provinceId]) as $url) {
$response = $this->getJson($url)->assertOk();
foreach ($response->json('data') as $row) {
$this->assertNotContains($row['id'], $expected);
}
}
}
}